Fix #378: Replace hand-rolled number parsing with std::from_chars

The custom parse_num floating-point implementation produced slightly
different results from the C++ compiler's own literal parsing (e.g.
1.1e-4 was off by a few ULPs). Replace it with std::from_chars which
is locale-independent and matches compiler precision exactly. Also
migrate integer parsing in buildInt from std::stoll/stoull to
std::from_chars, fix the JSON number parser to pass the full numeric
string to from_chars instead of splitting mantissa/exponent and using
std::pow, and update parse_string to use from_chars for arithmetic types.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-11 18:25:47 -06:00
parent 0d1ceed05d
commit 6517bb38a7
6 changed files with 110 additions and 84 deletions

View File

@ -404,6 +404,10 @@ if(BUILD_TESTING)
target_link_libraries(integer_literal_test ${LIBS} ${CHAISCRIPT_LIBS})
add_test(NAME Integer_Literal_Test COMMAND integer_literal_test)
add_executable(float_literal_test unittests/float_literal_test.cpp)
target_link_libraries(float_literal_test ${LIBS} ${CHAISCRIPT_LIBS})
add_test(NAME Float_Literal_Test COMMAND float_literal_test)
if(MULTITHREAD_SUPPORT_ENABLED)
add_executable(multithreaded_test unittests/multithreaded_test.cpp)
target_link_libraries(multithreaded_test ${LIBS})

View File

@ -69,6 +69,7 @@ static_assert(_MSC_FULL_VER >= 190024210, "Visual C++ 2015 Update 3 or later req
#define CHAISCRIPT_DEBUG false
#endif
#include <charconv>
#include <cmath>
#include <memory>
#include <string>
@ -138,50 +139,8 @@ namespace chaiscript {
template<typename T>
[[nodiscard]] auto parse_num(const std::string_view t_str) -> typename std::enable_if<!std::is_integral<T>::value, T>::type {
T t = 0;
T base{};
T decimal_place = 0;
int exponent = 0;
for (const auto c : t_str) {
switch (c) {
case '.':
decimal_place = 10;
break;
case 'e':
case 'E':
exponent = 1;
decimal_place = 0;
base = t;
t = 0;
break;
case '-':
exponent = -1;
break;
case '+':
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
if (decimal_place < 10) {
t *= 10;
t += static_cast<T>(c - '0');
} else {
t += static_cast<T>(c - '0') / decimal_place;
decimal_place *= 10;
}
break;
default:
break;
}
}
return exponent ? base * std::pow(T(10), t * static_cast<T>(exponent)) : t;
std::from_chars(t_str.data(), t_str.data() + t_str.size(), t);
return t;
}
struct str_equal {

View File

@ -10,6 +10,8 @@
#ifndef CHAISCRIPT_BOOTSTRAP_HPP_
#define CHAISCRIPT_BOOTSTRAP_HPP_
#include <charconv>
#include "../utility/utility.hpp"
#include "register_function.hpp"
@ -91,17 +93,19 @@ namespace chaiscript::bootstrap {
m.add(fun([](const Boxed_Number &bn) { return bn.get_as<T>(); }), type);
}
/// Internal function for converting from a string to a value
/// uses ostream operator >> to perform the conversion
template<typename Input>
Input parse_string(const std::string &i) {
if constexpr (!std::is_same<Input, wchar_t>::value && !std::is_same<Input, char16_t>::value && !std::is_same<Input, char32_t>::value) {
if constexpr (std::is_same_v<Input, wchar_t> || std::is_same_v<Input, char16_t> || std::is_same_v<Input, char32_t>) {
throw std::runtime_error("Parsing of wide characters is not yet supported");
} else if constexpr (std::is_arithmetic_v<Input> && !std::is_same_v<Input, char>) {
Input t{};
std::from_chars(i.data(), i.data() + i.size(), t);
return t;
} else {
std::stringstream ss(i);
Input t;
ss >> t;
return t;
} else {
throw std::runtime_error("Parsing of wide characters is not yet supported");
}
}

View File

@ -11,6 +11,7 @@
#define CHAISCRIPT_PARSER_HPP_
#include <cctype>
#include <charconv>
#include <cstring>
#include <exception>
#include <iostream>
@ -757,6 +758,9 @@ namespace chaiscript {
t_val.remove_prefix(2);
}
const auto *const first = t_val.data();
const auto *const last = first + i - (prefixed ? 2 : 0);
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-compare"
@ -770,41 +774,40 @@ namespace chaiscript {
#endif
try {
/// TODO fix this to use from_chars
auto u = std::stoll(std::string(t_val), nullptr, base);
{
long long u = 0;
auto [ptr, ec] = std::from_chars(first, last, u, base);
if (!unsigned_ && !long_ && u >= std::numeric_limits<int>::min() && u <= std::numeric_limits<int>::max()) {
return const_var(static_cast<int>(u));
} else if ((unsigned_ || base != 10) && !long_ && u >= std::numeric_limits<unsigned int>::min()
&& u <= std::numeric_limits<unsigned int>::max()) {
return const_var(static_cast<unsigned int>(u));
} else if (!unsigned_ && !longlong_ && u >= std::numeric_limits<long>::min() && u <= std::numeric_limits<long>::max()) {
return const_var(static_cast<long>(u));
} else if ((unsigned_ || base != 10) && !longlong_ && u >= std::numeric_limits<unsigned long>::min()
&& u <= std::numeric_limits<unsigned long>::max()) {
return const_var(static_cast<unsigned long>(u));
} else if (!unsigned_ && u >= std::numeric_limits<long long>::min() && u <= std::numeric_limits<long long>::max()) {
return const_var(static_cast<long long>(u));
} else {
return const_var(static_cast<unsigned long long>(u));
}
} catch (const std::out_of_range &) {
// too big to be signed
try {
/// TODO fix this to use from_chars
auto u = std::stoull(std::string(t_val), nullptr, base);
if (!longlong_ && u >= std::numeric_limits<unsigned long>::min() && u <= std::numeric_limits<unsigned long>::max()) {
if (ec == std::errc()) {
if (!unsigned_ && !long_ && u >= std::numeric_limits<int>::min() && u <= std::numeric_limits<int>::max()) {
return const_var(static_cast<int>(u));
} else if ((unsigned_ || base != 10) && !long_ && u >= std::numeric_limits<unsigned int>::min()
&& u <= std::numeric_limits<unsigned int>::max()) {
return const_var(static_cast<unsigned int>(u));
} else if (!unsigned_ && !longlong_ && u >= std::numeric_limits<long>::min() && u <= std::numeric_limits<long>::max()) {
return const_var(static_cast<long>(u));
} else if ((unsigned_ || base != 10) && !longlong_ && u >= std::numeric_limits<unsigned long>::min()
&& u <= std::numeric_limits<unsigned long>::max()) {
return const_var(static_cast<unsigned long>(u));
} else if (!unsigned_ && u >= std::numeric_limits<long long>::min() && u <= std::numeric_limits<long long>::max()) {
return const_var(static_cast<long long>(u));
} else {
return const_var(static_cast<unsigned long long>(u));
}
} catch (const std::out_of_range &) {
// it's just simply too big
return const_var(std::numeric_limits<long long>::max());
}
unsigned long long uu = 0;
const auto result = std::from_chars(first, last, uu, base);
if (result.ec == std::errc()) {
if (!longlong_ && uu >= std::numeric_limits<unsigned long>::min() && uu <= std::numeric_limits<unsigned long>::max()) {
return const_var(static_cast<unsigned long>(uu));
} else {
return const_var(static_cast<unsigned long long>(uu));
}
}
return const_var(std::numeric_limits<long long>::max());
}
#ifdef __GNUC__

View File

@ -550,14 +550,18 @@ namespace chaiscript::json {
}
--offset;
if (isDouble) {
return JSON((isNegative ? -1 : 1) * chaiscript::parse_num<double>(val) * std::pow(10, exp));
} else {
if (isDouble || !exp_str.empty()) {
std::string full_num;
if (isNegative) { full_num += '-'; }
full_num += val;
if (!exp_str.empty()) {
return JSON((isNegative ? -1 : 1) * static_cast<double>(chaiscript::parse_num<std::int64_t>(val)) * std::pow(10, exp));
} else {
return JSON((isNegative ? -1 : 1) * chaiscript::parse_num<std::int64_t>(val));
full_num += 'e';
if (isExpNegative) { full_num += '-'; }
full_num += exp_str;
}
return JSON(chaiscript::parse_num<double>(full_num));
} else {
return JSON((isNegative ? -1 : 1) * chaiscript::parse_num<std::int64_t>(val));
}
}

View File

@ -0,0 +1,52 @@
#include "../static_libs/chaiscript_parser.hpp"
#include "../static_libs/chaiscript_stdlib.hpp"
#include <chaiscript/chaiscript_basic.hpp>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <string>
#define TEST_FLOAT_LITERAL(v) test_float_literal(v, #v)
template<typename T>
bool test_float_literal(const T val, const std::string &str) {
chaiscript::ChaiScript_Basic chai(create_chaiscript_stdlib(), create_chaiscript_parser());
const T val2 = chai.eval<T>(str);
const bool pass = (val == val2);
std::cout << (pass ? "PASS" : "FAIL") << " (" << str << ") C++="
<< std::setprecision(20) << val << " chai=" << val2 << "\n";
return pass;
}
int main() {
bool success = true;
// Issue #378: scientific notation parsing inaccuracies
success = TEST_FLOAT_LITERAL(1.1e-4) && success;
success = TEST_FLOAT_LITERAL(1.5e+3) && success;
success = TEST_FLOAT_LITERAL(3.14159) && success;
success = TEST_FLOAT_LITERAL(2.718281828459045) && success;
success = TEST_FLOAT_LITERAL(1.0e10) && success;
success = TEST_FLOAT_LITERAL(1.0e-10) && success;
success = TEST_FLOAT_LITERAL(0.1) && success;
success = TEST_FLOAT_LITERAL(0.2) && success;
success = TEST_FLOAT_LITERAL(1.7976931348623157e+308) && success;
success = TEST_FLOAT_LITERAL(2.2250738585072014e-308) && success;
// Float suffix
success = TEST_FLOAT_LITERAL(1.1e-4f) && success;
success = TEST_FLOAT_LITERAL(3.14f) && success;
// Long double suffix
success = TEST_FLOAT_LITERAL(1.1e-4l) && success;
success = TEST_FLOAT_LITERAL(3.14l) && success;
if (success) {
std::cout << "All float literal tests passed.\n";
return 0;
} else {
std::cout << "FLOAT LITERAL TEST FAILURE\n";
return 1;
}
}