From 9d46b0c735877f152a0b4b16b8153c6f395dee28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Wed, 15 Apr 2026 21:34:50 +0200 Subject: [PATCH 1/8] ZJIT: Fix getlocal with level=0 reading stale EP data (#16736) * ZJIT: Fix getlocal with level=0 reading stale EP data YARVINSN_getlocal always loaded from EP memory, even for level=0 locals. But setlocal_WC_0 only updates the JIT's FrameState without writing to EP. When the Ruby compiler emits getlocal (non-WC variant) for a level=0 local after setlocal_WC_0, the EP load returned stale data. This caused Array#pack with buffer: keyword to silently lose the buffer argument when the array contained local variable references, because the buffer VALUE read from EP was nil/stale instead of the actual string. Fix: when level=0 and locals are not invalidated, use the FrameState value (same optimization as getlocal_WC_0). This matches what setlocal_WC_0 writes to. * ZJIT: Add regression test for getlocal level=0 fix Verify that Array#pack with a buffer: keyword reads the correct buffer VALUE when the buffer is a local variable set via setlocal_WC_0 and read via getlocal (non-WC variant). Before the fix, this returned b.size == 1 (stale EP data) instead of 2 (buffer updated by pack). --- zjit/src/codegen_tests.rs | 16 ++++++++++++++++ zjit/src/hir.rs | 14 ++++++++++++-- zjit/src/hir/tests.rs | 6 +----- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 474cbee5b5b6ba..b2cc401694f083 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -5632,3 +5632,19 @@ fn test_loop_terminates() { end "#), @"3"); } + +// Regression test: getlocal with level=0 after setlocal_WC_0 was loading stale EP +// memory, causing Array#pack with buffer: keyword to receive the wrong buffer VALUE. +// See https://github.com/ruby/ruby/pull/16736 +#[test] +fn test_getlocal_level_zero_after_setlocal_wc_0() { + assert_snapshot!(inspect(r#" + def test + b = +"x" + v = 2 + [v].pack("C*", buffer: b) + b.size + end + test + "#), @"2"); +} diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 786a994d8b0146..1c474c7ac35231 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -7363,8 +7363,18 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { YARVINSN_getlocal => { let ep_offset = get_arg(pc, 0).as_u32(); let level = get_arg(pc, 1).as_u32(); - let ep = fun.push_insn(block, Insn::GetEP { level }); - state.stack_push(fun.get_local_from_ep(block, ep, ep_offset, level, types::BasicObject)); + if level == 0 && !local_inval { + // Same optimization as getlocal_WC_0: use FrameState + let val = state.getlocal(ep_offset); + state.stack_push(val); + } else { + let ep = fun.push_insn(block, Insn::GetEP { level }); + let val = fun.get_local_from_ep(block, ep, ep_offset, level, types::BasicObject); + if level == 0 { + state.setlocal(ep_offset, val); + } + state.stack_push(val); + } } YARVINSN_setlocal => { let ep_offset = get_arg(pc, 0).as_u32(); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 4cb01d02c89635..dae3619f78f11e 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -2900,10 +2900,8 @@ pub(crate) mod hir_build_tests { v31:StringExact = StringCopy v30 v37:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) v38:StringExact = StringCopy v37 - v40:CPtr = GetEP 0 - v41:BasicObject = LoadField v40, :buf@0x1018 PatchPoint BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_PACK) - v44:String = ArrayPackBuffer v16, v17, fmt: v38, buf: v41 + v42:String = ArrayPackBuffer v16, v17, fmt: v38, buf: v31 PatchPoint NoEPEscape(test) CheckInterrupts Return v31 @@ -2949,8 +2947,6 @@ pub(crate) mod hir_build_tests { v31:StringExact = StringCopy v30 v37:StringExact[VALUE(0x1010)] = Const Value(VALUE(0x1010)) v38:StringExact = StringCopy v37 - v40:CPtr = GetEP 0 - v41:BasicObject = LoadField v40, :buf@0x1018 SideExit PatchPoint(BOPRedefined(ARRAY_REDEFINED_OP_FLAG, BOP_PACK)) "); } From 9eea01fccbb811c71fc4660e15d5221a4a13d9f9 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 15 Apr 2026 23:24:26 +0200 Subject: [PATCH 2/8] [ruby/date] Exclude test causing transient timeouts on TruffleRuby https://github.com/ruby/date/commit/840494d1ae --- test/date/test_date_parse.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/date/test_date_parse.rb b/test/date/test_date_parse.rb index 720624c02e338b..8308f258d50e39 100644 --- a/test/date/test_date_parse.rb +++ b/test/date/test_date_parse.rb @@ -591,13 +591,7 @@ def test__parse_odd_offset end def test__parse_too_long_year - # Math.log10 does not support so big numbers like 10^100_000 on TruffleRuby - unless RUBY_ENGINE == 'truffleruby' - str = "Jan 1" + "0" * 100_000 - h = EnvUtil.timeout(3) {Date._parse(str, limit: 100_010)} - assert_equal(100_000, Math.log10(h[:year])) - assert_equal(1, h[:mon]) - end + omit 'transient' if RUBY_ENGINE == 'truffleruby' str = "Jan - 1" + "0" * 100_000 h = EnvUtil.timeout(3) {Date._parse(str, limit: 100_010)} From c25acd472204be2d7ad6e3e1aa70d26a4fff3e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Mon, 13 Apr 2026 12:23:28 +0200 Subject: [PATCH 3/8] ZJIT: Inline Float arithmetic (+, -, *, /) Add FloatAdd, FloatSub, FloatMul, FloatDiv HIR instructions that lower to gen_prepare_leaf_call_with_gc followed by a direct ccall to rb_float_plus/minus/mul/div. This skips CCallWithFrame overhead (frame push/pop, stack and locals spill) while remaining GC-safe since PC and SP are saved to cfp before the call. The inline functions guard the receiver as Flonum (cheap tag check: (val & 3) == 2) and accept either Flonum or Fixnum as the second operand. HeapFloat operands fall back to CCallWithFrame via the default Send path. Using Flonum guards instead of Float guards avoids an expensive class pointer load from memory. ruby-bench nbody: 46ms -> 33ms (-28%) --- test/ruby/test_zjit.rb | 10 +++ zjit/src/codegen.rs | 28 +++++++ zjit/src/cruby_methods.rs | 43 +++++++++++ zjit/src/hir.rs | 39 ++++++++++ zjit/src/hir/opt_tests.rs | 155 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 275 insertions(+) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 84ded50300b114..4feef1217e5c15 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -418,6 +418,16 @@ def run(n) = n.times { make } RUBY end + def test_float_arithmetic + assert_compiles '4.0', 'def test = 1.5 + 2.5; test' + assert_compiles '6.0', 'def test = 2.0 * 3.0; test' + assert_compiles '1.5', 'def test = 3.5 - 2.0; test' + assert_compiles '2.5', 'def test = 5.0 / 2.0; test' + assert_compiles '4.5', 'def test = 1.5 * 3; test' # Float * Fixnum + assert_compiles 'true', 'def test = (Float::NAN + 1.0).nan?; test' + assert_compiles 'Infinity', 'def test = Float::INFINITY * 2.0; test' + end + private # Assert that every method call in `test_script` can be compiled by ZJIT diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index da15d30d03c741..2602010176ea91 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -675,6 +675,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::FixnumSub { left, right, state } => gen_fixnum_sub(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)), Insn::FixnumMult { left, right, state } => gen_fixnum_mult(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)), Insn::FixnumDiv { left, right, state } => gen_fixnum_div(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)), + Insn::FloatAdd { recv, other, state } => gen_float_add(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), + Insn::FloatSub { recv, other, state } => gen_float_sub(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), + Insn::FloatMul { recv, other, state } => gen_float_mul(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), + Insn::FloatDiv { recv, other, state } => gen_float_div(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), Insn::FixnumEq { left, right } => gen_fixnum_eq(asm, opnd!(left), opnd!(right)), Insn::FixnumNeq { left, right } => gen_fixnum_neq(asm, opnd!(left), opnd!(right)), Insn::FixnumLt { left, right } => gen_fixnum_lt(asm, opnd!(left), opnd!(right)), @@ -2301,6 +2305,30 @@ fn gen_fixnum_div(jit: &mut JITState, asm: &mut Assembler, left: lir::Opnd, righ asm_ccall!(asm, rb_jit_fix_div_fix, left, right) } +/// Compile Float + Float +fn gen_float_add(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_leaf_call_with_gc(asm, state); + asm_ccall!(asm, rb_float_plus, recv, other) +} + +/// Compile Float - Float +fn gen_float_sub(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_leaf_call_with_gc(asm, state); + asm_ccall!(asm, rb_float_minus, recv, other) +} + +/// Compile Float * Float +fn gen_float_mul(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_leaf_call_with_gc(asm, state); + asm_ccall!(asm, rb_float_mul, recv, other) +} + +/// Compile Float / Float +fn gen_float_div(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_leaf_call_with_gc(asm, state); + asm_ccall!(asm, rb_float_div, recv, other) +} + /// Compile Fixnum == Fixnum fn gen_fixnum_eq(asm: &mut Assembler, left: lir::Opnd, right: lir::Opnd) -> lir::Opnd { asm.cmp(left, right); diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 96db1386f1c8f1..6895a34b60fd32 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -265,6 +265,10 @@ pub fn init() -> Annotations { annotate!(rb_cInteger, ">>", inline_integer_rshift); annotate!(rb_cInteger, "[]", inline_integer_aref); annotate!(rb_cInteger, "to_s", types::StringExact); + annotate!(rb_cFloat, "+", inline_float_plus); + annotate!(rb_cFloat, "-", inline_float_minus); + annotate!(rb_cFloat, "*", inline_float_mul); + annotate!(rb_cFloat, "/", inline_float_div); 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); @@ -629,6 +633,45 @@ fn inline_integer_xor(fun: &mut hir::Function, block: hir::BlockId, recv: hir::I None } +fn try_inline_float_op(fun: &mut hir::Function, block: hir::BlockId, f: &dyn Fn(hir::InsnId, hir::InsnId) -> hir::Insn, bop: u32, recv: hir::InsnId, other: hir::InsnId, state: hir::InsnId) -> Option { + if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, FLOAT_REDEFINED_OP_FLAG) } { + return None; + } + // Receiver must be Flonum (cheap tag check: (val & 3) == 2). + // The other operand can be Flonum or Fixnum since rb_float_plus/minus/mul/div + // handle both via fast paths (FIXNUM_P check + cast to double). + // HeapFloat falls back to CCallWithFrame via the default Send path. + if fun.likely_a(recv, types::Flonum, state) + && (fun.likely_a(other, types::Flonum, state) || fun.likely_a(other, types::Fixnum, state)) + { + let recv = fun.coerce_to(block, recv, types::Flonum, state); + let other_type = if fun.likely_a(other, types::Flonum, state) { types::Flonum } else { types::Fixnum }; + let other = fun.coerce_to(block, other, other_type, state); + return Some(fun.push_insn(block, f(recv, other))); + } + None +} + +fn inline_float_plus(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + try_inline_float_op(fun, block, &|recv, other| hir::Insn::FloatAdd { recv, other, state }, BOP_PLUS, recv, other, state) +} + +fn inline_float_minus(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + try_inline_float_op(fun, block, &|recv, other| hir::Insn::FloatSub { recv, other, state }, BOP_MINUS, recv, other, state) +} + +fn inline_float_mul(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + try_inline_float_op(fun, block, &|recv, other| hir::Insn::FloatMul { recv, other, state }, BOP_MULT, recv, other, state) +} + +fn inline_float_div(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[other] = args else { return None; }; + try_inline_float_op(fun, block, &|recv, other| hir::Insn::FloatDiv { recv, other, state }, BOP_DIV, recv, other, state) +} + fn try_inline_fixnum_op(fun: &mut hir::Function, block: hir::BlockId, f: &dyn Fn(hir::InsnId, hir::InsnId) -> hir::Insn, bop: u32, left: hir::InsnId, right: hir::InsnId, state: hir::InsnId) -> Option { if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, INTEGER_REDEFINED_OP_FLAG) } { // If the basic operation is already redefined, we cannot optimize it. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 1c474c7ac35231..b278187e47c171 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1092,6 +1092,12 @@ pub enum Insn { FixnumLShift { left: InsnId, right: InsnId, state: InsnId }, FixnumRShift { left: InsnId, right: InsnId }, + /// Float arithmetic: delegates to rb_float_plus/minus/mul/div with GC preparation + FloatAdd { recv: InsnId, other: InsnId, state: InsnId }, + FloatSub { recv: InsnId, other: InsnId, state: InsnId }, + FloatMul { recv: InsnId, other: InsnId, state: InsnId }, + FloatDiv { recv: InsnId, other: InsnId, state: InsnId }, + // Distinct from `Send` with `mid:to_s` because does not have a patch point for String to_s being redefined ObjToString { val: InsnId, cd: *const rb_call_data, state: InsnId }, AnyToString { val: InsnId, str: InsnId, state: InsnId }, @@ -1284,6 +1290,14 @@ macro_rules! for_each_operand_impl { $visit_one!(right); $visit_one!(state); } + Insn::FloatAdd { recv, other, state } + | Insn::FloatSub { recv, other, state } + | Insn::FloatMul { recv, other, state } + | Insn::FloatDiv { recv, other, state } => { + $visit_one!(recv); + $visit_one!(other); + $visit_one!(state); + } Insn::FixnumLt { left, right } | Insn::FixnumLe { left, right } | Insn::FixnumGt { left, right } @@ -1627,6 +1641,10 @@ impl Insn { Insn::FixnumMult { .. } => effects::Empty, Insn::FixnumDiv { .. } => effects::Any, Insn::FixnumMod { .. } => effects::Any, + Insn::FloatAdd { .. } => effects::Any, + Insn::FloatSub { .. } => effects::Any, + Insn::FloatMul { .. } => effects::Any, + Insn::FloatDiv { .. } => effects::Any, Insn::FixnumEq { .. } => effects::Empty, Insn::FixnumNeq { .. } => effects::Empty, Insn::FixnumLt { .. } => effects::Empty, @@ -2021,6 +2039,10 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::FixnumMult { left, right, .. } => { write!(f, "FixnumMult {left}, {right}") }, Insn::FixnumDiv { left, right, .. } => { write!(f, "FixnumDiv {left}, {right}") }, Insn::FixnumMod { left, right, .. } => { write!(f, "FixnumMod {left}, {right}") }, + Insn::FloatAdd { recv, other, .. } => { write!(f, "FloatAdd {recv}, {other}") }, + Insn::FloatSub { recv, other, .. } => { write!(f, "FloatSub {recv}, {other}") }, + Insn::FloatMul { recv, other, .. } => { write!(f, "FloatMul {recv}, {other}") }, + Insn::FloatDiv { recv, other, .. } => { write!(f, "FloatDiv {recv}, {other}") }, Insn::FixnumEq { left, right, .. } => { write!(f, "FixnumEq {left}, {right}") }, Insn::FixnumNeq { left, right, .. } => { write!(f, "FixnumNeq {left}, {right}") }, Insn::FixnumLt { left, right, .. } => { write!(f, "FixnumLt {left}, {right}") }, @@ -2840,6 +2862,10 @@ impl Function { &FixnumMult { left, right, state } => FixnumMult { left: find!(left), right: find!(right), state }, &FixnumDiv { left, right, state } => FixnumDiv { left: find!(left), right: find!(right), state }, &FixnumMod { left, right, state } => FixnumMod { left: find!(left), right: find!(right), state }, + &FloatAdd { recv, other, state } => FloatAdd { recv: find!(recv), other: find!(other), state }, + &FloatSub { recv, other, state } => FloatSub { recv: find!(recv), other: find!(other), state }, + &FloatMul { recv, other, state } => FloatMul { recv: find!(recv), other: find!(other), state }, + &FloatDiv { recv, other, state } => FloatDiv { recv: find!(recv), other: find!(other), state }, &FixnumNeq { left, right } => FixnumNeq { left: find!(left), right: find!(right) }, &FixnumEq { left, right } => FixnumEq { left: find!(left), right: find!(right) }, &FixnumGt { left, right } => FixnumGt { left: find!(left), right: find!(right) }, @@ -3103,6 +3129,10 @@ impl Function { Insn::FixnumMult { .. } => types::Fixnum, Insn::FixnumDiv { .. } => types::Fixnum, Insn::FixnumMod { .. } => types::Fixnum, + Insn::FloatAdd { .. } => types::Float, + Insn::FloatSub { .. } => types::Float, + Insn::FloatMul { .. } => types::Float, + Insn::FloatDiv { .. } => types::Float, Insn::FixnumEq { .. } => types::BoolExact, Insn::FixnumNeq { .. } => types::BoolExact, Insn::FixnumLt { .. } => types::BoolExact, @@ -6211,6 +6241,15 @@ impl Function { self.assert_subtype(insn_id, left, types::Fixnum)?; self.assert_subtype(insn_id, right, types::Fixnum) } + Insn::FloatAdd { recv, other, .. } + | Insn::FloatSub { recv, other, .. } + | Insn::FloatMul { recv, other, .. } + | Insn::FloatDiv { recv, other, .. } + => { + self.assert_subtype(insn_id, recv, types::Flonum)?; + // other can be Flonum or Fixnum (rb_float_plus etc. handle both) + self.assert_subtype(insn_id, other, types::Flonum.union(types::Fixnum)) + } Insn::FixnumLShift { left, right, .. } | Insn::FixnumRShift { left, right, .. } => { self.assert_subtype(insn_id, left, types::Fixnum)?; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index b14df62461772e..641e903c183de0 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -15871,4 +15871,159 @@ mod hir_opt_tests { Return v24 "); } + #[test] + fn test_float_add_inline() { + eval(r#" + def test(a, b) = a + b + test(1.0, 2.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :b@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, +@0x1010, cme:0x1018) + v28:Flonum = GuardType v12, Flonum + v29:Flonum = GuardType v13, Flonum + v30:Float = FloatAdd v28, v29 + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_float_mul_inline() { + eval(r#" + def test(a, b) = a * b + test(1.5, 2.5) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :b@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) + v28:Flonum = GuardType v12, Flonum + v29:Flonum = GuardType v13, Flonum + v30:Float = FloatMul v28, v29 + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_float_sub_inline() { + eval(r#" + def test(a, b) = a - b + test(5.0, 3.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :b@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, -@0x1010, cme:0x1018) + v28:Flonum = GuardType v12, Flonum + v29:Flonum = GuardType v13, Flonum + v30:Float = FloatSub v28, v29 + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_float_div_inline() { + eval(r#" + def test(a, b) = a / b + test(10.0, 3.0) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :b@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, /@0x1010, cme:0x1018) + v28:Flonum = GuardType v12, Flonum + v29:Flonum = GuardType v13, Flonum + v30:Float = FloatDiv v28, v29 + CheckInterrupts + Return v30 + "); + } + + #[test] + fn test_float_mul_fixnum_inline() { + eval(r#" + def test(a, b) = a * b + test(1.5, 3) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + v4:BasicObject = LoadField v2, :b@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :a@1 + v9:BasicObject = LoadArg :b@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) + v28:Flonum = GuardType v12, Flonum + v29:Fixnum = GuardType v13, Fixnum + v30:Float = FloatMul v28, v29 + CheckInterrupts + Return v30 + "); + } + } From 4cbb9d145bb102588df907890eb3686217d03c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Mon, 13 Apr 2026 12:30:05 +0200 Subject: [PATCH 4/8] ZJIT: Inline Float#to_i Add FloatToInt HIR instruction that truncates a Flonum to Integer via rb_jit_flo_to_i with GC preparation. The helper uses trunc() for truncation toward zero, then returns Fixnum (LONG2FIX) or Bignum (rb_dbl2big) depending on magnitude. rb_jit_flo_to_i is a new JIT helper in jit.c, following the same pattern as rb_jit_fix_div_fix for wrapping a static C function. --- jit.c | 10 ++++++++++ test/ruby/test_zjit.rb | 2 ++ zjit/bindgen/src/main.rs | 1 + zjit/src/codegen.rs | 7 +++++++ zjit/src/cruby_bindings.inc.rs | 1 + zjit/src/cruby_methods.rs | 11 +++++++++++ zjit/src/hir.rs | 13 +++++++++++++ zjit/src/hir/opt_tests.rs | 28 ++++++++++++++++++++++++++++ 8 files changed, 73 insertions(+) diff --git a/jit.c b/jit.c index e335875322bb24..8ec524515c79f0 100644 --- a/jit.c +++ b/jit.c @@ -827,6 +827,16 @@ rb_jit_fix_div_fix(VALUE recv, VALUE obj) return rb_fix_div_fix(recv, obj); } +VALUE +rb_jit_flo_to_i(VALUE num) +{ + double f = trunc(RFLOAT_VALUE(num)); + if (FIXABLE(f)) { + return LONG2FIX((long)f); + } + return rb_dbl2big(f); +} + // YJIT/ZJIT need this function to never allocate and never raise VALUE rb_yarv_str_eql_internal(VALUE str1, VALUE str2) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 4feef1217e5c15..a56fea6d51a417 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -426,6 +426,8 @@ def test_float_arithmetic assert_compiles '4.5', 'def test = 1.5 * 3; test' # Float * Fixnum assert_compiles 'true', 'def test = (Float::NAN + 1.0).nan?; test' assert_compiles 'Infinity', 'def test = Float::INFINITY * 2.0; test' + assert_compiles '3', 'def test = 3.7.to_i; test' + assert_compiles '-2', 'def test = (-2.9).to_i; test' end private diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index b3f390026bc144..bcb3170704211c 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -287,6 +287,7 @@ fn main() { .allowlist_function("rb_jit_get_page_size") .allowlist_function("rb_jit_array_len") .allowlist_function("rb_jit_fix_div_fix") + .allowlist_function("rb_jit_flo_to_i") .allowlist_function("rb_jit_iseq_builtin_attrs") .allowlist_function("rb_jit_str_concat_codepoint") .allowlist_function("rb_zjit_iseq_inspect") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 2602010176ea91..b22257d6b29fd8 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -679,6 +679,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio Insn::FloatSub { recv, other, state } => gen_float_sub(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), Insn::FloatMul { recv, other, state } => gen_float_mul(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), Insn::FloatDiv { recv, other, state } => gen_float_div(asm, opnd!(recv), opnd!(other), &function.frame_state(*state)), + Insn::FloatToInt { recv, state } => gen_float_to_int(asm, opnd!(recv), &function.frame_state(*state)), Insn::FixnumEq { left, right } => gen_fixnum_eq(asm, opnd!(left), opnd!(right)), Insn::FixnumNeq { left, right } => gen_fixnum_neq(asm, opnd!(left), opnd!(right)), Insn::FixnumLt { left, right } => gen_fixnum_lt(asm, opnd!(left), opnd!(right)), @@ -2329,6 +2330,12 @@ fn gen_float_div(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: asm_ccall!(asm, rb_float_div, recv, other) } +/// Compile Float#to_i (truncate to integer) +fn gen_float_to_int(asm: &mut Assembler, recv: lir::Opnd, state: &FrameState) -> lir::Opnd { + gen_prepare_leaf_call_with_gc(asm, state); + asm_ccall!(asm, rb_jit_flo_to_i, recv) +} + /// Compile Fixnum == Fixnum fn gen_fixnum_eq(asm: &mut Assembler, left: lir::Opnd, right: lir::Opnd) -> lir::Opnd { asm.cmp(left, right); diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index e110c7a5ef125f..0e8583391cb3c1 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2306,6 +2306,7 @@ unsafe extern "C" { end: *mut ::std::os::raw::c_void, ); pub fn rb_jit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE; + pub fn rb_jit_flo_to_i(num: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_jit_str_concat_codepoint(str_: VALUE, codepoint: VALUE); pub fn rb_jit_shape_capacity(shape_id: shape_id_t) -> attr_index_t; diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 6895a34b60fd32..93ba7da6403321 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -269,6 +269,8 @@ pub fn init() -> Annotations { annotate!(rb_cFloat, "-", inline_float_minus); annotate!(rb_cFloat, "*", inline_float_mul); annotate!(rb_cFloat, "/", inline_float_div); + annotate!(rb_cFloat, "to_i", inline_float_to_i); + annotate!(rb_cFloat, "to_int", inline_float_to_i); 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); @@ -672,6 +674,15 @@ fn inline_float_div(fun: &mut hir::Function, block: hir::BlockId, recv: hir::Ins try_inline_float_op(fun, block, &|recv, other| hir::Insn::FloatDiv { recv, other, state }, BOP_DIV, recv, other, state) } +fn inline_float_to_i(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[] = args else { return None; }; + if fun.likely_a(recv, types::Flonum, state) { + let recv = fun.coerce_to(block, recv, types::Flonum, state); + return Some(fun.push_insn(block, hir::Insn::FloatToInt { recv, state })); + } + None +} + fn try_inline_fixnum_op(fun: &mut hir::Function, block: hir::BlockId, f: &dyn Fn(hir::InsnId, hir::InsnId) -> hir::Insn, bop: u32, left: hir::InsnId, right: hir::InsnId, state: hir::InsnId) -> Option { if !unsafe { rb_BASIC_OP_UNREDEFINED_P(bop, INTEGER_REDEFINED_OP_FLAG) } { // If the basic operation is already redefined, we cannot optimize it. diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b278187e47c171..671249f7010cca 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1097,6 +1097,8 @@ pub enum Insn { FloatSub { recv: InsnId, other: InsnId, state: InsnId }, FloatMul { recv: InsnId, other: InsnId, state: InsnId }, FloatDiv { recv: InsnId, other: InsnId, state: InsnId }, + /// Float#to_i: truncate float to integer via rb_jit_flo_to_i + FloatToInt { recv: InsnId, state: InsnId }, // Distinct from `Send` with `mid:to_s` because does not have a patch point for String to_s being redefined ObjToString { val: InsnId, cd: *const rb_call_data, state: InsnId }, @@ -1298,6 +1300,10 @@ macro_rules! for_each_operand_impl { $visit_one!(other); $visit_one!(state); } + Insn::FloatToInt { recv, state } => { + $visit_one!(recv); + $visit_one!(state); + } Insn::FixnumLt { left, right } | Insn::FixnumLe { left, right } | Insn::FixnumGt { left, right } @@ -1645,6 +1651,7 @@ impl Insn { Insn::FloatSub { .. } => effects::Any, Insn::FloatMul { .. } => effects::Any, Insn::FloatDiv { .. } => effects::Any, + Insn::FloatToInt { .. } => effects::Any, Insn::FixnumEq { .. } => effects::Empty, Insn::FixnumNeq { .. } => effects::Empty, Insn::FixnumLt { .. } => effects::Empty, @@ -2043,6 +2050,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::FloatSub { recv, other, .. } => { write!(f, "FloatSub {recv}, {other}") }, Insn::FloatMul { recv, other, .. } => { write!(f, "FloatMul {recv}, {other}") }, Insn::FloatDiv { recv, other, .. } => { write!(f, "FloatDiv {recv}, {other}") }, + Insn::FloatToInt { recv, .. } => { write!(f, "FloatToInt {recv}") }, Insn::FixnumEq { left, right, .. } => { write!(f, "FixnumEq {left}, {right}") }, Insn::FixnumNeq { left, right, .. } => { write!(f, "FixnumNeq {left}, {right}") }, Insn::FixnumLt { left, right, .. } => { write!(f, "FixnumLt {left}, {right}") }, @@ -2866,6 +2874,7 @@ impl Function { &FloatSub { recv, other, state } => FloatSub { recv: find!(recv), other: find!(other), state }, &FloatMul { recv, other, state } => FloatMul { recv: find!(recv), other: find!(other), state }, &FloatDiv { recv, other, state } => FloatDiv { recv: find!(recv), other: find!(other), state }, + &FloatToInt { recv, state } => FloatToInt { recv: find!(recv), state }, &FixnumNeq { left, right } => FixnumNeq { left: find!(left), right: find!(right) }, &FixnumEq { left, right } => FixnumEq { left: find!(left), right: find!(right) }, &FixnumGt { left, right } => FixnumGt { left: find!(left), right: find!(right) }, @@ -3133,6 +3142,7 @@ impl Function { Insn::FloatSub { .. } => types::Float, Insn::FloatMul { .. } => types::Float, Insn::FloatDiv { .. } => types::Float, + Insn::FloatToInt { .. } => types::Integer, Insn::FixnumEq { .. } => types::BoolExact, Insn::FixnumNeq { .. } => types::BoolExact, Insn::FixnumLt { .. } => types::BoolExact, @@ -6250,6 +6260,9 @@ impl Function { // other can be Flonum or Fixnum (rb_float_plus etc. handle both) self.assert_subtype(insn_id, other, types::Flonum.union(types::Fixnum)) } + Insn::FloatToInt { recv, .. } => { + self.assert_subtype(insn_id, recv, types::Flonum) + } Insn::FixnumLShift { left, right, .. } | Insn::FixnumRShift { left, right, .. } => { self.assert_subtype(insn_id, left, types::Fixnum)?; diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 641e903c183de0..08b2136f86b04b 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -15995,6 +15995,34 @@ mod hir_opt_tests { "); } + #[test] + fn test_float_to_i_inline() { + eval(r#" + def test(a) = a.to_i + test(3.7) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:2: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :a@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v6:BasicObject = LoadArg :self@0 + v7:BasicObject = LoadArg :a@1 + Jump bb3(v6, v7) + bb3(v9:BasicObject, v10:BasicObject): + PatchPoint MethodRedefined(Float@0x1008, to_i@0x1010, cme:0x1018) + v23:Flonum = GuardType v10, Flonum + v24:Integer = FloatToInt v23 + CheckInterrupts + Return v24 + "); + } + #[test] fn test_float_mul_fixnum_inline() { eval(r#" From aaad9e0f0640c4efe3987b2154c877ba108fc0a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Tue, 14 Apr 2026 20:18:03 +0200 Subject: [PATCH 5/8] ZJIT: Move flo_to_i helper to zjit.c and use original flo_to_i Address review feedback: 1. Move the helper from jit.c (shared YJIT/ZJIT glue) to zjit.c since it is only used by ZJIT. 2. Instead of duplicating the truncation logic, export flo_to_i from numeric.c and call it from the ZJIT helper, keeping the implementation centralized. The new rb_zjit_flo_to_i wrapper in zjit.c delegates directly to flo_to_i, so there is no deviation from the Float#to_i semantics. --- internal/numeric.h | 1 + jit.c | 10 ---------- numeric.c | 4 ++-- zjit.c | 8 ++++++++ zjit/bindgen/src/main.rs | 2 +- zjit/src/codegen.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/internal/numeric.h b/internal/numeric.h index 280c008952ed7d..bc131489f50976 100644 --- a/internal/numeric.h +++ b/internal/numeric.h @@ -70,6 +70,7 @@ VALUE rb_float_minus(VALUE x, VALUE y); VALUE rb_int_mul(VALUE x, VALUE y); VALUE rb_float_mul(VALUE x, VALUE y); VALUE rb_float_div(VALUE x, VALUE y); +VALUE flo_to_i(VALUE num); VALUE rb_int_idiv(VALUE x, VALUE y); VALUE rb_int_modulo(VALUE x, VALUE y); VALUE rb_int2str(VALUE num, int base); diff --git a/jit.c b/jit.c index 8ec524515c79f0..e335875322bb24 100644 --- a/jit.c +++ b/jit.c @@ -827,16 +827,6 @@ rb_jit_fix_div_fix(VALUE recv, VALUE obj) return rb_fix_div_fix(recv, obj); } -VALUE -rb_jit_flo_to_i(VALUE num) -{ - double f = trunc(RFLOAT_VALUE(num)); - if (FIXABLE(f)) { - return LONG2FIX((long)f); - } - return rb_dbl2big(f); -} - // YJIT/ZJIT need this function to never allocate and never raise VALUE rb_yarv_str_eql_internal(VALUE str1, VALUE str2) diff --git a/numeric.c b/numeric.c index 287294f9b5a899..0b4360bc277fb6 100644 --- a/numeric.c +++ b/numeric.c @@ -183,7 +183,7 @@ static VALUE fix_rshift(long, unsigned long); static VALUE int_pow(long x, unsigned long y); static VALUE rb_int_floor(VALUE num, int ndigits); static VALUE rb_int_ceil(VALUE num, int ndigits); -static VALUE flo_to_i(VALUE num); +VALUE flo_to_i(VALUE num); static int float_round_overflow(int ndigits, int binexp); static int float_round_underflow(int ndigits, int binexp); @@ -2554,7 +2554,7 @@ float_round_underflow(int ndigits, int binexp) * */ -static VALUE +VALUE flo_to_i(VALUE num) { double f = RFLOAT_VALUE(num); diff --git a/zjit.c b/zjit.c index fdebc922f0fc85..4e0383c914e810 100644 --- a/zjit.c +++ b/zjit.c @@ -155,6 +155,14 @@ rb_zjit_defined_ivar(VALUE obj, ID id, VALUE pushval) return result ? pushval : Qnil; } +// Wrapper to call static flo_to_i from Rust. Keeps the truncation logic +// centralized in numeric.c. +VALUE +rb_zjit_flo_to_i(VALUE num) +{ + return flo_to_i(num); +} + bool rb_zjit_method_tracing_currently_enabled(void) { diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index bcb3170704211c..b390658f2fddea 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -287,8 +287,8 @@ fn main() { .allowlist_function("rb_jit_get_page_size") .allowlist_function("rb_jit_array_len") .allowlist_function("rb_jit_fix_div_fix") - .allowlist_function("rb_jit_flo_to_i") .allowlist_function("rb_jit_iseq_builtin_attrs") + .allowlist_function("rb_zjit_flo_to_i") .allowlist_function("rb_jit_str_concat_codepoint") .allowlist_function("rb_zjit_iseq_inspect") .allowlist_function("rb_zjit_iseq_insn_set") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index b22257d6b29fd8..e492c3149fc8cf 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2333,7 +2333,7 @@ fn gen_float_div(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: /// Compile Float#to_i (truncate to integer) fn gen_float_to_int(asm: &mut Assembler, recv: lir::Opnd, state: &FrameState) -> lir::Opnd { gen_prepare_leaf_call_with_gc(asm, state); - asm_ccall!(asm, rb_jit_flo_to_i, recv) + asm_ccall!(asm, rb_zjit_flo_to_i, recv) } /// Compile Fixnum == Fixnum diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 0e8583391cb3c1..b45adfaf8c1511 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2306,7 +2306,7 @@ unsafe extern "C" { end: *mut ::std::os::raw::c_void, ); pub fn rb_jit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE; - pub fn rb_jit_flo_to_i(num: VALUE) -> VALUE; + pub fn rb_zjit_flo_to_i(num: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_jit_str_concat_codepoint(str_: VALUE, codepoint: VALUE); pub fn rb_jit_shape_capacity(shape_id: shape_id_t) -> attr_index_t; From 03f1b569b13a00281e5974eaca2c2359dab96525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Tue, 14 Apr 2026 20:38:58 +0200 Subject: [PATCH 6/8] ZJIT: Keep flo_to_i static, add rb_flo_to_i public wrapper The previous commit exposed flo_to_i as a non-static global, which tripped tool/leaked-globals because it lacks the rb_ prefix. Keep flo_to_i static and add a new public wrapper rb_flo_to_i in numeric.c that delegates to it. ZJIT now calls rb_flo_to_i directly from Rust (no zjit.c wrapper needed). --- internal/numeric.h | 2 +- numeric.c | 10 ++++++++-- zjit.c | 8 -------- zjit/bindgen/src/main.rs | 2 +- zjit/src/codegen.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 2 +- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/numeric.h b/internal/numeric.h index bc131489f50976..a24432df1e6450 100644 --- a/internal/numeric.h +++ b/internal/numeric.h @@ -70,7 +70,7 @@ VALUE rb_float_minus(VALUE x, VALUE y); VALUE rb_int_mul(VALUE x, VALUE y); VALUE rb_float_mul(VALUE x, VALUE y); VALUE rb_float_div(VALUE x, VALUE y); -VALUE flo_to_i(VALUE num); +VALUE rb_flo_to_i(VALUE num); VALUE rb_int_idiv(VALUE x, VALUE y); VALUE rb_int_modulo(VALUE x, VALUE y); VALUE rb_int2str(VALUE num, int base); diff --git a/numeric.c b/numeric.c index 0b4360bc277fb6..02a7fbed0cd4c8 100644 --- a/numeric.c +++ b/numeric.c @@ -183,7 +183,7 @@ static VALUE fix_rshift(long, unsigned long); static VALUE int_pow(long x, unsigned long y); static VALUE rb_int_floor(VALUE num, int ndigits); static VALUE rb_int_ceil(VALUE num, int ndigits); -VALUE flo_to_i(VALUE num); +static VALUE flo_to_i(VALUE num); static int float_round_overflow(int ndigits, int binexp); static int float_round_underflow(int ndigits, int binexp); @@ -2554,7 +2554,7 @@ float_round_underflow(int ndigits, int binexp) * */ -VALUE +static VALUE flo_to_i(VALUE num) { double f = RFLOAT_VALUE(num); @@ -2565,6 +2565,12 @@ flo_to_i(VALUE num) return dbl2ival(f); } +VALUE +rb_flo_to_i(VALUE num) +{ + return flo_to_i(num); +} + /* * call-seq: * truncate(ndigits = 0) -> float or integer diff --git a/zjit.c b/zjit.c index 4e0383c914e810..fdebc922f0fc85 100644 --- a/zjit.c +++ b/zjit.c @@ -155,14 +155,6 @@ rb_zjit_defined_ivar(VALUE obj, ID id, VALUE pushval) return result ? pushval : Qnil; } -// Wrapper to call static flo_to_i from Rust. Keeps the truncation logic -// centralized in numeric.c. -VALUE -rb_zjit_flo_to_i(VALUE num) -{ - return flo_to_i(num); -} - bool rb_zjit_method_tracing_currently_enabled(void) { diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index b390658f2fddea..4b30398cef8c18 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -288,7 +288,7 @@ fn main() { .allowlist_function("rb_jit_array_len") .allowlist_function("rb_jit_fix_div_fix") .allowlist_function("rb_jit_iseq_builtin_attrs") - .allowlist_function("rb_zjit_flo_to_i") + .allowlist_function("rb_flo_to_i") .allowlist_function("rb_jit_str_concat_codepoint") .allowlist_function("rb_zjit_iseq_inspect") .allowlist_function("rb_zjit_iseq_insn_set") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index e492c3149fc8cf..72068c1e5b1775 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2333,7 +2333,7 @@ fn gen_float_div(asm: &mut Assembler, recv: lir::Opnd, other: lir::Opnd, state: /// Compile Float#to_i (truncate to integer) fn gen_float_to_int(asm: &mut Assembler, recv: lir::Opnd, state: &FrameState) -> lir::Opnd { gen_prepare_leaf_call_with_gc(asm, state); - asm_ccall!(asm, rb_zjit_flo_to_i, recv) + asm_ccall!(asm, rb_flo_to_i, recv) } /// Compile Fixnum == Fixnum diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index b45adfaf8c1511..b1d86edfa72173 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2306,7 +2306,7 @@ unsafe extern "C" { end: *mut ::std::os::raw::c_void, ); pub fn rb_jit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE; - pub fn rb_zjit_flo_to_i(num: VALUE) -> VALUE; + pub fn rb_flo_to_i(num: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_jit_str_concat_codepoint(str_: VALUE, codepoint: VALUE); pub fn rb_jit_shape_capacity(shape_id: shape_id_t) -> attr_index_t; From 429c499270e997d8307c57a0ca35ba0655f77db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Hasi=C5=84ski?= Date: Tue, 14 Apr 2026 21:21:41 +0200 Subject: [PATCH 7/8] ZJIT: Fix bindgen ordering for rb_flo_to_i The zjit-bindgen CI check regenerates cruby_bindings.inc.rs from headers and diffs against the committed file. Since rb_flo_to_i is declared next to rb_float_plus/minus/mul/div in internal/numeric.h, bindgen emits it there. Match that order in both the allowlist and the generated output. --- zjit/bindgen/src/main.rs | 2 +- zjit/src/cruby_bindings.inc.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 4b30398cef8c18..ba6d91a4fe8480 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -201,6 +201,7 @@ fn main() { .allowlist_function("rb_float_minus") .allowlist_function("rb_float_mul") .allowlist_function("rb_float_div") + .allowlist_function("rb_flo_to_i") .allowlist_type("ruby_rstring_private_flags") .allowlist_function("rb_ec_str_resurrect") .allowlist_function("rb_str_concat_literals") @@ -288,7 +289,6 @@ fn main() { .allowlist_function("rb_jit_array_len") .allowlist_function("rb_jit_fix_div_fix") .allowlist_function("rb_jit_iseq_builtin_attrs") - .allowlist_function("rb_flo_to_i") .allowlist_function("rb_jit_str_concat_codepoint") .allowlist_function("rb_zjit_iseq_inspect") .allowlist_function("rb_zjit_iseq_insn_set") diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index b1d86edfa72173..1ef1ee15d51997 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2139,6 +2139,7 @@ unsafe extern "C" { pub fn rb_float_minus(x: VALUE, y: VALUE) -> VALUE; pub fn rb_float_mul(x: VALUE, y: VALUE) -> VALUE; pub fn rb_float_div(x: VALUE, y: VALUE) -> VALUE; + pub fn rb_flo_to_i(num: VALUE) -> VALUE; pub fn rb_fix_aref(fix: VALUE, idx: VALUE) -> VALUE; pub fn rb_vm_insn_addr2opcode(addr: *const ::std::os::raw::c_void) -> ::std::os::raw::c_int; pub fn rb_iseq_line_no(iseq: *const rb_iseq_t, pos: usize) -> ::std::os::raw::c_uint; @@ -2306,7 +2307,6 @@ unsafe extern "C" { end: *mut ::std::os::raw::c_void, ); pub fn rb_jit_fix_div_fix(recv: VALUE, obj: VALUE) -> VALUE; - pub fn rb_flo_to_i(num: VALUE) -> VALUE; pub fn rb_yarv_str_eql_internal(str1: VALUE, str2: VALUE) -> VALUE; pub fn rb_jit_str_concat_codepoint(str_: VALUE, codepoint: VALUE); pub fn rb_jit_shape_capacity(shape_id: shape_id_t) -> attr_index_t; From e6508f099ab6e0b98937e7cb181c850671ddb5ae Mon Sep 17 00:00:00 2001 From: Bob Singh Date: Wed, 15 Apr 2026 19:32:14 -0500 Subject: [PATCH 8/8] [DOC] fix typos and improve clarity in build guide (#16753) Co-authored-by: Bob Singh --- doc/contributing/building_ruby.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/contributing/building_ruby.md b/doc/contributing/building_ruby.md index 286bd1f1161635..a283a2f3dbd3ef 100644 --- a/doc/contributing/building_ruby.md +++ b/doc/contributing/building_ruby.md @@ -21,7 +21,7 @@ * We can upgrade this version to system ruby version of the latest Ubuntu LTS. * git - 2.32 or later - * Anterior versions may work; 2.32 or later will prevent build + * Earlier versions may work; 2.32 or later will prevent build errors in case your system `.gitconfig` uses `$HOME` paths. 2. Install optional, recommended dependencies: @@ -151,7 +151,7 @@ ruby ├── build # Created in step 2 and populated in step 4 │ ├── GNUmakefile # Generated by `../configure` │ ├── Makefile # Generated by `../configure` -│ ├── object.o # Compiled object file, built my `make` +│ ├── object.o # Compiled object file, built by `make` │ └── ... other compiled `.o` object files │ │ # Other interesting files: