From 6517bb38a7028e21a16e42f4298a8324d4c36d79 Mon Sep 17 00:00:00 2001 From: leftibot Date: Sat, 11 Apr 2026 18:25:47 -0600 Subject: [PATCH] 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) --- CMakeLists.txt | 4 ++ include/chaiscript/chaiscript_defines.hpp | 47 +------------- include/chaiscript/dispatchkit/bootstrap.hpp | 14 +++-- .../chaiscript/language/chaiscript_parser.hpp | 61 ++++++++++--------- include/chaiscript/utility/json.hpp | 16 +++-- unittests/float_literal_test.cpp | 52 ++++++++++++++++ 6 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 unittests/float_literal_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7fcb4bc2..f0a1e776 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/include/chaiscript/chaiscript_defines.hpp b/include/chaiscript/chaiscript_defines.hpp index f50510f0..a609cb2f 100644 --- a/include/chaiscript/chaiscript_defines.hpp +++ b/include/chaiscript/chaiscript_defines.hpp @@ -69,6 +69,7 @@ static_assert(_MSC_FULL_VER >= 190024210, "Visual C++ 2015 Update 3 or later req #define CHAISCRIPT_DEBUG false #endif +#include #include #include #include @@ -138,50 +139,8 @@ namespace chaiscript { template [[nodiscard]] auto parse_num(const std::string_view t_str) -> typename std::enable_if::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(c - '0'); - } else { - t += static_cast(c - '0') / decimal_place; - decimal_place *= 10; - } - break; - default: - break; - } - } - return exponent ? base * std::pow(T(10), t * static_cast(exponent)) : t; + std::from_chars(t_str.data(), t_str.data() + t_str.size(), t); + return t; } struct str_equal { diff --git a/include/chaiscript/dispatchkit/bootstrap.hpp b/include/chaiscript/dispatchkit/bootstrap.hpp index e5d18255..bbdc67f8 100644 --- a/include/chaiscript/dispatchkit/bootstrap.hpp +++ b/include/chaiscript/dispatchkit/bootstrap.hpp @@ -10,6 +10,8 @@ #ifndef CHAISCRIPT_BOOTSTRAP_HPP_ #define CHAISCRIPT_BOOTSTRAP_HPP_ +#include + #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(); }), type); } - /// Internal function for converting from a string to a value - /// uses ostream operator >> to perform the conversion template Input parse_string(const std::string &i) { - if constexpr (!std::is_same::value && !std::is_same::value && !std::is_same::value) { + if constexpr (std::is_same_v || std::is_same_v || std::is_same_v) { + throw std::runtime_error("Parsing of wide characters is not yet supported"); + } else if constexpr (std::is_arithmetic_v && !std::is_same_v) { + 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"); } } diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index be2a7525..15e316da 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -11,6 +11,7 @@ #define CHAISCRIPT_PARSER_HPP_ #include +#include #include #include #include @@ -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::min() && u <= std::numeric_limits::max()) { - return const_var(static_cast(u)); - } else if ((unsigned_ || base != 10) && !long_ && u >= std::numeric_limits::min() - && u <= std::numeric_limits::max()) { - return const_var(static_cast(u)); - } else if (!unsigned_ && !longlong_ && u >= std::numeric_limits::min() && u <= std::numeric_limits::max()) { - return const_var(static_cast(u)); - } else if ((unsigned_ || base != 10) && !longlong_ && u >= std::numeric_limits::min() - && u <= std::numeric_limits::max()) { - return const_var(static_cast(u)); - } else if (!unsigned_ && u >= std::numeric_limits::min() && u <= std::numeric_limits::max()) { - return const_var(static_cast(u)); - } else { - return const_var(static_cast(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::min() && u <= std::numeric_limits::max()) { + if (ec == std::errc()) { + if (!unsigned_ && !long_ && u >= std::numeric_limits::min() && u <= std::numeric_limits::max()) { + return const_var(static_cast(u)); + } else if ((unsigned_ || base != 10) && !long_ && u >= std::numeric_limits::min() + && u <= std::numeric_limits::max()) { + return const_var(static_cast(u)); + } else if (!unsigned_ && !longlong_ && u >= std::numeric_limits::min() && u <= std::numeric_limits::max()) { + return const_var(static_cast(u)); + } else if ((unsigned_ || base != 10) && !longlong_ && u >= std::numeric_limits::min() + && u <= std::numeric_limits::max()) { return const_var(static_cast(u)); + } else if (!unsigned_ && u >= std::numeric_limits::min() && u <= std::numeric_limits::max()) { + return const_var(static_cast(u)); } else { return const_var(static_cast(u)); } - } catch (const std::out_of_range &) { - // it's just simply too big - return const_var(std::numeric_limits::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::min() && uu <= std::numeric_limits::max()) { + return const_var(static_cast(uu)); + } else { + return const_var(static_cast(uu)); + } + } + + return const_var(std::numeric_limits::max()); } #ifdef __GNUC__ diff --git a/include/chaiscript/utility/json.hpp b/include/chaiscript/utility/json.hpp index aa8fe698..90877e78 100644 --- a/include/chaiscript/utility/json.hpp +++ b/include/chaiscript/utility/json.hpp @@ -550,14 +550,18 @@ namespace chaiscript::json { } --offset; - if (isDouble) { - return JSON((isNegative ? -1 : 1) * chaiscript::parse_num(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(chaiscript::parse_num(val)) * std::pow(10, exp)); - } else { - return JSON((isNegative ? -1 : 1) * chaiscript::parse_num(val)); + full_num += 'e'; + if (isExpNegative) { full_num += '-'; } + full_num += exp_str; } + return JSON(chaiscript::parse_num(full_num)); + } else { + return JSON((isNegative ? -1 : 1) * chaiscript::parse_num(val)); } } diff --git a/unittests/float_literal_test.cpp b/unittests/float_literal_test.cpp new file mode 100644 index 00000000..e1cb67f7 --- /dev/null +++ b/unittests/float_literal_test.cpp @@ -0,0 +1,52 @@ +#include "../static_libs/chaiscript_parser.hpp" +#include "../static_libs/chaiscript_stdlib.hpp" +#include + +#include +#include +#include +#include + +#define TEST_FLOAT_LITERAL(v) test_float_literal(v, #v) + +template +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(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; + } +}