This is an automated email from the ASF dual-hosted git repository.
tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git
The following commit(s) were added to refs/heads/main by this push:
new 5818c66b [FIX] Make JSON numeric conversion independent of locale
(#948)
5818c66b is described below
commit 5818c66b43ef83f0137a2aa956dd2a70c295951d
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Nov 6 17:44:17 2026 -0500
[FIX] Make JSON numeric conversion independent of locale (#948)
JSON numbers use a decimal point regardless of the process locale. Parse
and print integers with standard character conversion. Use
floating-point character conversion with MSVC and libstdc++ 11 or newer,
and explicit C-locale conversion with libc++ and older libstdc++
libraries.
Keep the conversion logic in the existing parser and writer source
files, with no build-time feature probes or new headers. Preserve
floating-point precision, integer-like float formatting, and range-error
behavior while avoiding temporary numeric-token copies on the standard
conversion path.
---
src/ffi/extra/json_parser.cc | 82 +++++++++++++++++++++++++------------
src/ffi/extra/json_writer.cc | 52 +++++++++++++++++++----
tests/cpp/extra/test_json_parser.cc | 17 ++++++++
3 files changed, 117 insertions(+), 34 deletions(-)
diff --git a/src/ffi/extra/json_parser.cc b/src/ffi/extra/json_parser.cc
index 117ef541..8c7a14de 100644
--- a/src/ffi/extra/json_parser.cc
+++ b/src/ffi/extra/json_parser.cc
@@ -29,10 +29,18 @@
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
-#include <cinttypes>
+#include <cerrno>
+#include <charconv>
+#include <clocale>
+#include <cmath>
+#include <cstdlib>
#include <limits>
#include <utility>
+#ifdef __APPLE__
+#include <xlocale.h>
+#endif
+
namespace tvm {
namespace ffi {
namespace json {
@@ -162,11 +170,9 @@ class JSONParserContext {
// e = %x65 / %x45 ; e E
// exp = e [ minus / plus ] 1*DIGIT
// frac = decimal-point 1*DIGIT
- std::string temp_buffer;
bool maybe_int = true;
// parse [minus], cross check for Infinity/NaN/-Infinity
if (*cur_ == '-') {
- temp_buffer.push_back('-');
++cur_;
if (cur_ != end_ && *cur_ == 'I') {
if (this->MatchLiteral("Infinity", 8)) {
@@ -202,7 +208,6 @@ class JSONParserContext {
char next_char = *cur_;
if ((next_char >= '0' && next_char <= '9') || next_char == 'e' ||
next_char == 'E' ||
next_char == '+' || next_char == '-' || next_char == '.') {
- temp_buffer.push_back(next_char);
if (next_char == '.' || next_char == 'e' || next_char == 'E') {
maybe_int = false;
}
@@ -211,37 +216,60 @@ class JSONParserContext {
break;
}
}
- if (temp_buffer.empty()) {
+ if (start_pos == cur_) {
this->SetErrorExpectingValue();
return false;
}
- // parse from temp_buffer_
if (maybe_int) {
- // now try to parse the number as int64
- char* end_ptr;
- errno = 0;
- intmax_t int_val = strtoimax(temp_buffer.data(), &end_ptr, 10);
- if (errno == 0 && int_val >= std::numeric_limits<int64_t>::min() &&
- int_val <= std::numeric_limits<int64_t>::max() &&
- end_ptr == temp_buffer.data() + temp_buffer.size()) {
- *out = static_cast<int64_t>(int_val);
+ int64_t int_value;
+ auto result = std::from_chars(start_pos, cur_, int_value);
+ if (result.ec == std::errc{} && result.ptr == cur_) {
+ *out = int_value;
return true;
}
}
- {
- // now try to parse number as double
- char* end_ptr;
- errno = 0;
- double double_val = strtod(temp_buffer.data(), &end_ptr);
- if (errno == 0 && end_ptr == temp_buffer.data() + temp_buffer.size()) {
- *out = double_val;
- return true;
- } else {
- this->SetCurrentPosForBetterErrorMsg(start_pos);
- this->SetErrorExpectingValue();
- return false;
- }
+ double double_value;
+// libc++ and older libstdc++ use the C-locale fallback.
+#if defined(_MSC_VER) || (defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE >= 11)
+ auto result = std::from_chars(start_pos, cur_, double_value);
+ if (result.ec != std::errc{} || result.ptr != cur_) {
+ this->SetCurrentPosForBetterErrorMsg(start_pos);
+ this->SetErrorExpectingValue();
+ return false;
}
+ // Keep strtod's underflow behavior for subnormal results.
+ if (double_value == 0 || std::fabs(double_value) >
std::numeric_limits<double>::min()) {
+ *out = double_value;
+ return true;
+ }
+#endif
+ // Subnormals fall through to strtod_l to preserve underflow reporting
(ERANGE).
+ std::string temp_buffer(start_pos, cur_);
+ char* end_ptr;
+ // Create "C" numeric locale once to ignore process locale; twin:
json_writer.cc.
+ static const struct NumberLocale {
+#ifdef _WIN32
+ _locale_t value = _create_locale(LC_NUMERIC, "C");
+ ~NumberLocale() { _free_locale(value); }
+#else
+ locale_t value = newlocale(LC_NUMERIC_MASK, "C", nullptr);
+ ~NumberLocale() { freelocale(value); }
+#endif
+ NumberLocale() { TVM_FFI_ICHECK(value != nullptr) << "Cannot create JSON
numeric locale"; }
+ } locale;
+ errno = 0;
+#ifdef _WIN32
+ double_value = _strtod_l(temp_buffer.c_str(), &end_ptr, locale.value);
+#else
+ double_value = strtod_l(temp_buffer.c_str(), &end_ptr, locale.value);
+#endif
+ if (errno == 0 && end_ptr == temp_buffer.c_str() + temp_buffer.size()) {
+ *out = double_value;
+ return true;
+ }
+ this->SetCurrentPosForBetterErrorMsg(start_pos);
+ this->SetErrorExpectingValue();
+ return false;
}
/*!
diff --git a/src/ffi/extra/json_writer.cc b/src/ffi/extra/json_writer.cc
index 2f62feab..a4108bb7 100644
--- a/src/ffi/extra/json_writer.cc
+++ b/src/ffi/extra/json_writer.cc
@@ -31,13 +31,19 @@
#include <tvm/ffi/reflection/registry.h>
#include <tvm/ffi/string.h>
-#include <cinttypes>
+#include <charconv>
+#include <clocale>
#include <cmath>
#include <cstdint>
+#include <cstdio>
#include <string>
#include <unordered_set>
#include <utility>
+#ifdef __APPLE__
+#include <xlocale.h>
+#endif
+
namespace tvm {
namespace ffi {
namespace json {
@@ -150,8 +156,40 @@ class JSONWriter {
void WriteInt(int64_t value) {
// the biggest possible string representation of -INT64_MIN
char buffer[sizeof("-9223372036854775808") + 1];
- int size = TVM_FFI_SNPRINTF(buffer, sizeof(buffer), "%" PRId64, value);
- WriteLiteral(buffer, size);
+ auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
+ result_.append(buffer, static_cast<size_t>(result.ptr - buffer));
+ }
+
+ static int WriteFloatToBuffer(char* buffer, size_t size, double value, bool
fixed) {
+// libc++ and older libstdc++ use the C-locale fallback.
+#if defined(_MSC_VER) || (defined(_GLIBCXX_RELEASE) && _GLIBCXX_RELEASE >= 11)
+ auto result = std::to_chars(buffer, buffer + size, value,
+ fixed ? std::chars_format::fixed :
std::chars_format::general,
+ fixed ? 1 : 17);
+ return static_cast<int>(result.ptr - buffer);
+#else
+ // Create "C" numeric locale once to ignore process locale; twin:
json_parser.cc.
+ static const struct NumberLocale {
+#ifdef _WIN32
+ _locale_t value = _create_locale(LC_NUMERIC, "C");
+ ~NumberLocale() { _free_locale(value); }
+#else
+ locale_t value = newlocale(LC_NUMERIC_MASK, "C", nullptr);
+ ~NumberLocale() { freelocale(value); }
+#endif
+ NumberLocale() { TVM_FFI_ICHECK(value != nullptr) << "Cannot create JSON
numeric locale"; }
+ } locale;
+#ifdef _WIN32
+ return _snprintf_l(buffer, size, fixed ? "%.1f" : "%.17g", locale.value,
value);
+#elif defined(__APPLE__)
+ return snprintf_l(buffer, size, locale.value, fixed ? "%.1f" : "%.17g",
value);
+#else
+ locale_t previous_locale = uselocale(locale.value);
+ int count = snprintf(buffer, size, fixed ? "%.1f" : "%.17g", value);
+ uselocale(previous_locale);
+ return count;
+#endif
+#endif
}
void WriteFloat(double value) {
@@ -173,13 +211,13 @@ class JSONWriter {
// always print an extra .0 for integer so integer numbers are printed
as floats
// this helps us to distinguish between integer and float, which is
not necessary
// but helps to ensure roundtrip property of the parser/printer in
terms of int/float types
- int size = TVM_FFI_SNPRINTF(buffer, sizeof(buffer), "%.1f", int_part);
- WriteLiteral(buffer, size);
+ int size = WriteFloatToBuffer(buffer, sizeof(buffer), int_part, true);
+ result_.append(buffer, size);
} else {
// Save 17 decimal digits to avoid loss during loading JSON
// this is the maximum precision that can be represented in a double
- int size = TVM_FFI_SNPRINTF(buffer, sizeof(buffer), "%.17g", value);
- WriteLiteral(buffer, size);
+ int size = WriteFloatToBuffer(buffer, sizeof(buffer), value, false);
+ result_.append(buffer, size);
}
}
}
diff --git a/tests/cpp/extra/test_json_parser.cc
b/tests/cpp/extra/test_json_parser.cc
index 6c0d90b2..6fb0ad5a 100644
--- a/tests/cpp/extra/test_json_parser.cc
+++ b/tests/cpp/extra/test_json_parser.cc
@@ -22,7 +22,9 @@
#include <tvm/ffi/extra/json.h>
#include <tvm/ffi/extra/structural_equal.h>
+#include <clocale>
#include <cmath>
+#include <string>
namespace {
@@ -120,6 +122,21 @@ TEST(JSONParser, Number) {
EXPECT_EQ(json::Parse("123.456E+10").cast<double>(), 123.456E+10);
}
+TEST(JSONParser, NumericLocale) {
+ struct RestoreLocale {
+ std::string saved = std::setlocale(LC_NUMERIC, nullptr);
+ ~RestoreLocale() { std::setlocale(LC_NUMERIC, saved.c_str()); }
+ } restore_locale;
+ const char* found = nullptr;
+ for (const char* name : {"de_DE.UTF-8", "German_Germany.1252"}) {
+ found = std::setlocale(LC_NUMERIC, name);
+ if (found != nullptr) break;
+ }
+ if (found == nullptr) GTEST_SKIP() << "No locale with a decimal comma is
installed";
+ EXPECT_EQ(json::Parse("1234.5").cast<double>(), 1234.5);
+ EXPECT_EQ(json::Stringify(1234.5), "1234.5");
+}
+
TEST(JSONParser, WrongNumber) {
String error_msg;
EXPECT_EQ(json::Parse("123.456.789", &error_msg), nullptr);