mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-05-01 03:19:28 +08:00
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:
parent
0d1ceed05d
commit
6517bb38a7
@ -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})
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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__
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
unittests/float_literal_test.cpp
Normal file
52
unittests/float_literal_test.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user