From 94a8feaa0110a9c9928a46af2309ab3fd3abf689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Sat, 11 Apr 2026 22:53:18 +0200 Subject: [PATCH 1/7] ZJIT: Annotate Float and Integer predicates Adds method annotations so ZJIT can emit the fast CCall path for pure Float cfunc predicates and propagate return types for numeric builtin predicates. Float cfuncs (leaf, no_gc, elidable): nan?, finite? BoolExact infinite? Fixnum | NilClass Integer/Float builtins (BoolExact return type): Integer#zero?, even?, odd? Float#zero?, positive?, negative? All targeted C bodies are pure (isnan/isfinite/isinf, FIXNUM bit tests, FLOAT_ZERO_P) so the annotations are safe. Microbench with --zjit-call-threshold=2, tight while loop, 20M iters: Float#nan? 0.140s -> 0.110s (~21%) Float#finite? 0.145s -> 0.114s (~21%) Float#infinite? 0.129s -> 0.094s (~27%) --- zjit/src/cruby_methods.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index e104a0f320a505..96db1386f1c8f1 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -266,9 +266,18 @@ pub fn init() -> Annotations { annotate!(rb_cInteger, "[]", inline_integer_aref); annotate!(rb_cInteger, "to_s", types::StringExact); annotate!(rb_cString, "to_s", inline_string_to_s, types::StringExact); + annotate!(rb_cFloat, "nan?", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cFloat, "finite?", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cFloat, "infinite?", types::Fixnum.union(types::NilClass), no_gc, leaf, elidable); let thread_singleton = unsafe { rb_singleton_class(rb_cThread) }; annotate!(thread_singleton, "current", inline_thread_current, types::BasicObject, no_gc, leaf); + annotate_builtin!(rb_cInteger, "zero?", types::BoolExact); + annotate_builtin!(rb_cInteger, "even?", types::BoolExact); + annotate_builtin!(rb_cInteger, "odd?", types::BoolExact); + annotate_builtin!(rb_cFloat, "zero?", types::BoolExact); + annotate_builtin!(rb_cFloat, "positive?", types::BoolExact); + annotate_builtin!(rb_cFloat, "negative?", types::BoolExact); annotate_builtin!(rb_mKernel, "Float", types::Float); annotate_builtin!(rb_mKernel, "Integer", types::Integer); // TODO(max): Annotate rb_mKernel#class as returning types::Class. Right now there is a subtle From 54527e24c8f08dd69bc07d7e3b6425ab101e7709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Sat, 11 Apr 2026 23:48:30 +0200 Subject: [PATCH 2/7] ZJIT: Update const_send_direct_integer snapshot Reflects the BoolExact return type now inferred for Integer#zero? after the annotation added in the previous commit. --- zjit/src/hir/opt_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index f4755d5faca8de..0bc1b6cd0d3e48 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -3512,7 +3512,7 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): v14:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1008, zero?@0x1010, cme:0x1018) - v24:BasicObject = InvokeBuiltin leaf , v14 + v24:BoolExact = InvokeBuiltin leaf , v14 CheckInterrupts Return v24 "); From edb95b13a3789617095f9c4f1d4b8e9bcee23344 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Sun, 12 Apr 2026 13:56:45 +0200 Subject: [PATCH 3/7] ZJIT: Add HIR tests and benchmarks for numeric predicate annotations Add snapshot tests verifying correct HIR generation for each annotated method: - Float cfuncs (nan?, finite?, infinite?) emit CCall with BoolExact or Fixnum|NilClass return type - Integer builtins (zero?, even?, odd?) emit InvokeBuiltin with BoolExact return type - Float builtins (zero?, positive?, negative?) emit InvokeBuiltin with BoolExact return type Add benchmark yml files for Float and Integer predicates to the benchmark/ directory. --- benchmark/float_predicate.yml | 12 ++ benchmark/integer_predicate.yml | 9 ++ zjit/src/hir/opt_tests.rs | 224 ++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 benchmark/float_predicate.yml create mode 100644 benchmark/integer_predicate.yml diff --git a/benchmark/float_predicate.yml b/benchmark/float_predicate.yml new file mode 100644 index 00000000000000..b9469376660ca4 --- /dev/null +++ b/benchmark/float_predicate.yml @@ -0,0 +1,12 @@ +prelude: | + floats = [1.0, -1.0, 0.0, Float::NAN, Float::INFINITY, -Float::INFINITY] + +benchmark: + float_nan?: floats.each { |f| f.nan? } + float_finite?: floats.each { |f| f.finite? } + float_infinite?: floats.each { |f| f.infinite? } + float_zero?: floats.each { |f| f.zero? } + float_positive?: floats.each { |f| f.positive? } + float_negative?: floats.each { |f| f.negative? } + +loop_count: 1000000 diff --git a/benchmark/integer_predicate.yml b/benchmark/integer_predicate.yml new file mode 100644 index 00000000000000..7c05ff258791eb --- /dev/null +++ b/benchmark/integer_predicate.yml @@ -0,0 +1,9 @@ +prelude: | + nums = (0..9).to_a + +benchmark: + integer_zero?: nums.each { |n| n.zero? } + integer_even?: nums.each { |n| n.even? } + integer_odd?: nums.each { |n| n.odd? } + +loop_count: 1000000 diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 0bc1b6cd0d3e48..b14df62461772e 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -15647,4 +15647,228 @@ mod hir_opt_tests { Jump bb6(v81, v114) "); } + + #[test] + fn test_float_nan_p_annotation() { + eval(r#" + def test(x) = x.nan? + test(1.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, nan?@0x1010, cme:0x1018) + v23:Flonum = GuardType v10, Flonum + v24:BoolExact = CCall v23, :Float#nan?@0x1040 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_float_finite_p_annotation() { + eval(r#" + def test(x) = x.finite? + test(1.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, finite?@0x1010, cme:0x1018) + v23:Flonum = GuardType v10, Flonum + v24:BoolExact = CCall v23, :Float#finite?@0x1040 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_float_infinite_p_annotation() { + eval(r#" + def test(x) = x.infinite? + test(1.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, infinite?@0x1010, cme:0x1018) + v23:Flonum = GuardType v10, Flonum + v24:NilClass|Fixnum = CCall v23, :Float#infinite?@0x1040 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_integer_even_p_annotation() { + eval(r#" + def test(x) = x.even? + test(2) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Integer@0x1008, even?@0x1010, cme:0x1018) + v22:Fixnum = GuardType v10, Fixnum + v24:BoolExact = InvokeBuiltin leaf , v22 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_integer_odd_p_annotation() { + eval(r#" + def test(x) = x.odd? + test(3) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Integer@0x1008, odd?@0x1010, cme:0x1018) + v22:Fixnum = GuardType v10, Fixnum + v24:BoolExact = InvokeBuiltin leaf , v22 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_float_zero_p_annotation() { + eval(r#" + def test(x) = x.zero? + test(1.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, zero?@0x1010, cme:0x1018) + v22:Flonum = GuardType v10, Flonum + v24:BoolExact = InvokeBuiltin leaf , v22 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_float_positive_p_annotation() { + eval(r#" + def test(x) = x.positive? + test(1.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, positive?@0x1010, cme:0x1018) + v22:Flonum = GuardType v10, Flonum + v24:BoolExact = InvokeBuiltin leaf , v22 + CheckInterrupts + Return v24 + "); + } + + #[test] + fn test_float_negative_p_annotation() { + eval(r#" + def test(x) = x.negative? + test(-1.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :x@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :x@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, negative?@0x1010, cme:0x1018) + v22:Flonum = GuardType v10, Flonum + v24:BoolExact = InvokeBuiltin leaf , v22 + CheckInterrupts + Return v24 + "); + } } From 40189f8f951bd0f655baaf26a774ee83cb9a04a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:10:55 +0000 Subject: [PATCH 4/7] Bump the github-actions group across 1 directory with 2 updates Bumps the github-actions group with 2 updates in the / directory: [lewagon/wait-on-check-action](https://github.com/lewagon/wait-on-check-action) and [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `lewagon/wait-on-check-action` from 1.6.1 to 1.7.0 - [Release notes](https://github.com/lewagon/wait-on-check-action/releases) - [Changelog](https://github.com/lewagon/wait-on-check-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/lewagon/wait-on-check-action/compare/78dd4dd5d9b337c14c3c81f79e53bf7d222435c1...9312864dfbc9fd208e9c0417843430751c042800) Updates `taiki-e/install-action` from 2.75.10 to 2.75.13 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/85b24a67ef0c632dfefad70b9d5ce8fddb040754...eea29cff9a2b68892c0845ae3e4f45fc47ee9354) --- updated-dependencies: - dependency-name: lewagon/wait-on-check-action dependency-version: 1.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.75.13 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/dependabot_automerge.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependabot_automerge.yml b/.github/workflows/dependabot_automerge.yml index c474dbd8c5ebf5..9c65de02f09fdb 100644 --- a/.github/workflows/dependabot_automerge.yml +++ b/.github/workflows/dependabot_automerge.yml @@ -17,7 +17,7 @@ jobs: id: metadata - name: Wait for status checks - uses: lewagon/wait-on-check-action@78dd4dd5d9b337c14c3c81f79e53bf7d222435c1 # v1.6.1 + uses: lewagon/wait-on-check-action@9312864dfbc9fd208e9c0417843430751c042800 # v1.7.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 40805a3b4d64ba..e64de62c052815 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10 + - uses: taiki-e/install-action@eea29cff9a2b68892c0845ae3e4f45fc47ee9354 # v2.75.13 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index cdb1174a9a866b..dd486677ef49e7 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@85b24a67ef0c632dfefad70b9d5ce8fddb040754 # v2.75.10 + - uses: taiki-e/install-action@eea29cff9a2b68892c0845ae3e4f45fc47ee9354 # v2.75.13 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From b21043f8ac1aa2aeaaadae4153f4e059aedaf988 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 15 Apr 2026 10:02:55 +0900 Subject: [PATCH 5/7] Constify pack functions --- pack.c | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/pack.c b/pack.c index 7e17b016c77dab..94bb510e0db182 100644 --- a/pack.c +++ b/pack.c @@ -62,7 +62,7 @@ is_bigendian(void) { static int init = 0; static int endian_value; - char *p; + const char *p; if (init) return endian_value; init = 1; @@ -771,7 +771,7 @@ pack_pack(rb_execution_context_t *ec, VALUE ary, VALUE fmt, VALUE buffer) /* FALL THROUGH */ case 'p': /* pointer to string */ while (len-- > 0) { - char *t = 0; + const char *t = 0; from = NEXTFROM; if (!NIL_P(from)) { STR_FROM(from); @@ -1001,8 +1001,8 @@ static VALUE pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) { #define hexdigits ruby_hexdigits - char *s, *send; - char *p, *pend; + const char *s, *send; + const char *p, *pend; VALUE ary, associates = Qfalse; long len; AVOID_CC_BUG long tmp_len; @@ -1076,7 +1076,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) if (len > send - s) len = send - s; { long end = len; - char *t = s + len - 1; + const char *t = s + len - 1; while (t >= s) { if (*t != ' ' && *t != '\0') break; @@ -1089,7 +1089,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) case 'Z': { - char *t = s; + const char *t = s; if (len > send-s) len = send-s; while (t < s+len && *t) t++; @@ -1523,7 +1523,8 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) case 'M': { VALUE buf = rb_str_new(0, send - s); - char *ptr = RSTRING_PTR(buf), *ss = s; + char *ptr = RSTRING_PTR(buf); + const char *ss = s; int csum = 0; int c1, c2; @@ -1578,7 +1579,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) case 'P': if (sizeof(char *) <= (size_t)(send - s)) { VALUE tmp = Qnil; - char *t; + const char *t; UNPACK_FETCH(&t, char *); if (t) { @@ -1601,7 +1602,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) break; else { VALUE tmp = Qnil; - char *t; + const char *t; UNPACK_FETCH(&t, char *); if (t) { @@ -1621,7 +1622,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) if (type == 'r') { pack_flags |= INTEGER_PACK_2COMP; } - char *s0 = s; + const char *s0 = s; while (len > 0 && s < send) { if (*s & 0x80) { s++; @@ -1648,7 +1649,7 @@ pack_unpack_internal(VALUE str, VALUE fmt, enum unpack_mode mode, long offset) case 'w': { - char *s0 = s; + const char *s0 = s; while (len > 0 && s < send) { if (*s & 0x80) { s++; From 5b4d95b8d03b17c80f2b54a1e92b74df2bb9f63e Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Apr 2026 08:09:56 +0200 Subject: [PATCH 6/7] [ruby/json] Fix handling out of of range exponent in numbers Fix: https://github.com/ruby/json/issues/970 If the parsed exponent overflows a `int32_t` passing it to ryu is incorrect. We could pass it to `rb_cstr_to_dbl` but then Ruby will emit an annoying warning, instead we can coerce to `0.0` and `Inf`. https://github.com/ruby/json/commit/20454ba274 --- ext/json/parser/parser.c | 16 ++++++++++++---- test/json/json_ryu_fallback_test.rb | 8 ++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 38f3a48ce4515a..976fbbe757884d 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -855,7 +855,7 @@ NOINLINE(static) VALUE json_decode_large_float(const char *start, long len) /* Ruby JSON optimized float decoder using vendored Ryu algorithm * Accepts pre-extracted mantissa and exponent from first-pass validation */ -static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantissa, int mantissa_digits, int32_t exponent, bool negative, +static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantissa, int mantissa_digits, int64_t exponent, bool negative, const char *start, const char *end) { if (RB_UNLIKELY(config->decimal_class)) { @@ -863,13 +863,21 @@ static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantis return rb_funcallv(config->decimal_class, config->decimal_method_id, 1, &text); } + if (RB_UNLIKELY(exponent > INT32_MAX)) { + return CInfinity; + } + + if (RB_UNLIKELY(exponent < INT32_MIN)) { + return rb_float_new(0.0); + } + // Fall back to rb_cstr_to_dbl for potential subnormals (rare edge case) // Ryu has rounding issues with subnormals around 1e-310 (< 2.225e-308) if (RB_UNLIKELY(mantissa_digits > 17 || mantissa_digits + exponent < -307)) { return json_decode_large_float(start, end - start); } - return DBL2NUM(ryu_s2d_from_parts(mantissa, mantissa_digits, exponent, negative)); + return DBL2NUM(ryu_s2d_from_parts(mantissa, mantissa_digits, (int32_t)exponent, negative)); } static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig *config, long count) @@ -1144,7 +1152,7 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig const char first_digit = *state->cursor; // Variables for Ryu optimization - extract digits during parsing - int32_t exponent = 0; + int64_t exponent = 0; int decimal_point_pos = -1; uint64_t mantissa = 0; @@ -1188,7 +1196,7 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig raise_parse_error_at("invalid number: %s", state, start); } - exponent = negative_exponent ? -((int32_t)abs_exponent) : ((int32_t)abs_exponent); + exponent = negative_exponent ? -abs_exponent : abs_exponent; } if (integer) { diff --git a/test/json/json_ryu_fallback_test.rb b/test/json/json_ryu_fallback_test.rb index 59ba76d392fc9b..dc60067b91f6f7 100644 --- a/test/json/json_ryu_fallback_test.rb +++ b/test/json/json_ryu_fallback_test.rb @@ -166,4 +166,12 @@ def test_invalid_numbers_rejected end end end + + def test_large_exponent_numbers + assert_equal Float::INFINITY, JSON.parse("1e4294967296") + assert_equal 0.0, JSON.parse("1e-4294967296") + assert_equal 0.0, JSON.parse("99999999999999999e-4294967296") + assert_equal Float::INFINITY, JSON.parse("1e4294967295") + assert_equal Float::INFINITY, JSON.parse("1e4294967297") + end end From 84f7692242fa41c0e4ec4ecfe469cd91c67bd8a8 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Wed, 15 Apr 2026 08:37:32 +0200 Subject: [PATCH 7/7] [ruby/json] Fix parsing of *negative* out of bound floats. https://github.com/ruby/json/commit/1072482184 --- ext/json/parser/parser.c | 4 ++-- test/json/json_ryu_fallback_test.rb | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index 976fbbe757884d..f89a1917c589b1 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -864,11 +864,11 @@ static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantis } if (RB_UNLIKELY(exponent > INT32_MAX)) { - return CInfinity; + return negative ? CMinusInfinity : CInfinity; } if (RB_UNLIKELY(exponent < INT32_MIN)) { - return rb_float_new(0.0); + return rb_float_new(negative ? -0.0 : 0.0); } // Fall back to rb_cstr_to_dbl for potential subnormals (rare edge case) diff --git a/test/json/json_ryu_fallback_test.rb b/test/json/json_ryu_fallback_test.rb index dc60067b91f6f7..2262a6f5947540 100644 --- a/test/json/json_ryu_fallback_test.rb +++ b/test/json/json_ryu_fallback_test.rb @@ -173,5 +173,11 @@ def test_large_exponent_numbers assert_equal 0.0, JSON.parse("99999999999999999e-4294967296") assert_equal Float::INFINITY, JSON.parse("1e4294967295") assert_equal Float::INFINITY, JSON.parse("1e4294967297") + + assert_equal -Float::INFINITY, JSON.parse("-1e4294967296") + assert_equal -0.0, JSON.parse("-1e-4294967296") + assert_equal -0.0, JSON.parse("-99999999999999999e-4294967296") + assert_equal -Float::INFINITY, JSON.parse("-1e4294967295") + assert_equal -Float::INFINITY, JSON.parse("-1e4294967297") end end