From 5cec11f45a9b6282e9bc402b599dd95159b86649 Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Sun, 11 Jan 2026 16:25:13 -0500 Subject: [PATCH 1/8] ZJIT: Inline Array#length --- zjit/src/cruby_methods.rs | 14 +++++++++-- zjit/src/hir.rs | 1 + zjit/src/hir/opt_tests.rs | 50 +++++++++++++++++++-------------------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 4aa9068cb17c16..e6c26148cfb687 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -219,8 +219,7 @@ pub fn init() -> Annotations { annotate!(rb_cString, "ascii_only?", types::BoolExact, no_gc, leaf); annotate!(rb_cModule, "name", types::StringExact.union(types::NilClass), no_gc, leaf, elidable); annotate!(rb_cModule, "===", inline_module_eqq, types::BoolExact, no_gc, leaf); - annotate!(rb_cArray, "length", types::Fixnum, no_gc, leaf, elidable); - annotate!(rb_cArray, "size", types::Fixnum, no_gc, leaf, elidable); + annotate!(rb_cArray, "length", inline_array_length, types::Fixnum, no_gc, leaf, elidable); annotate!(rb_cArray, "empty?", types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cArray, "reverse", types::ArrayExact, leaf, elidable); annotate!(rb_cArray, "join", types::StringExact); @@ -537,6 +536,17 @@ fn inline_module_eqq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::In None } +fn inline_array_length(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::Array, state) { + let recv = fun.coerce_to(block, recv, types::Array, state); + let length_cint = fun.push_insn(block, hir::Insn::ArrayLength { array: recv }); + let result = fun.push_insn(block, hir::Insn::BoxFixnum { val: length_cint, state }); + return Some(result); + } + None +} + fn inline_integer_succ(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { if !args.is_empty() { return None; } if fun.likely_a(recv, types::Fixnum, state) { diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index c6a104fbd176b0..41ddb3ae6934d9 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1027,6 +1027,7 @@ impl Insn { // NewHash's operands may be hashed and compared for equality, which could have // side-effects. Insn::NewHash { elements, .. } => !elements.is_empty(), + Insn::ArrayLength { .. } => false, Insn::ArrayDup { .. } => false, Insn::HashDup { .. } => false, Insn::Test { .. } => false, diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3ad07596b71f58..4fc47dc611c4f1 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2291,7 +2291,7 @@ mod hir_opt_tests { fn eliminate_array_length() { eval(" def test - x = [].length + [].length 5 end "); @@ -2300,21 +2300,18 @@ mod hir_opt_tests { bb0(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb2(v1, v2) - bb1(v5:BasicObject): + Jump bb2(v1) + bb1(v4:BasicObject): EntryPoint JIT(0) - v6:NilClass = Const Value(nil) - Jump bb2(v5, v6) - bb2(v8:BasicObject, v9:NilClass): - v13:ArrayExact = NewArray + Jump bb2(v4) + bb2(v6:BasicObject): + v10:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v29:Fixnum = CCall v13, :Array#length@0x1038 - v20:Fixnum[5] = Const Value(5) + v17:Fixnum[5] = Const Value(5) CheckInterrupts - Return v20 + Return v17 "); } @@ -2435,7 +2432,7 @@ mod hir_opt_tests { fn eliminate_array_size() { eval(" def test - x = [].size + [].size 5 end "); @@ -2444,21 +2441,18 @@ mod hir_opt_tests { bb0(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb2(v1, v2) - bb1(v5:BasicObject): + Jump bb2(v1) + bb1(v4:BasicObject): EntryPoint JIT(0) - v6:NilClass = Const Value(nil) - Jump bb2(v5, v6) - bb2(v8:BasicObject, v9:NilClass): - v13:ArrayExact = NewArray + Jump bb2(v4) + bb2(v6:BasicObject): + v10:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v29:Fixnum = CCall v13, :Array#size@0x1038 - v20:Fixnum[5] = Const Value(5) + v17:Fixnum[5] = Const Value(5) CheckInterrupts - Return v20 + Return v17 "); } @@ -3554,8 +3548,9 @@ mod hir_opt_tests { v18:ArrayExact = NewArray v11, v12 PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) + v29:CInt64 = ArrayLength v18 + v30:Fixnum = BoxFixnum v29 IncrCounter inline_cfunc_optimized_send_count - v30:Fixnum = CCall v18, :Array#length@0x1038 CheckInterrupts Return v30 "); @@ -3581,8 +3576,9 @@ mod hir_opt_tests { v18:ArrayExact = NewArray v11, v12 PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) + v29:CInt64 = ArrayLength v18 + v30:Fixnum = BoxFixnum v29 IncrCounter inline_cfunc_optimized_send_count - v30:Fixnum = CCall v18, :Array#size@0x1038 CheckInterrupts Return v30 "); @@ -7170,8 +7166,9 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact + v24:CInt64 = ArrayLength v23 + v25:Fixnum = BoxFixnum v24 IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall v23, :Array#length@0x1038 CheckInterrupts Return v25 "); @@ -7198,8 +7195,9 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact + v24:CInt64 = ArrayLength v23 + v25:Fixnum = BoxFixnum v24 IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall v23, :Array#size@0x1038 CheckInterrupts Return v25 "); From 41d9eb784b4beba681205f7443e6c6454d0fca6e Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 12 Jan 2026 11:49:13 -0500 Subject: [PATCH 2/8] ZJIT: Inline Array#empty? --- zjit/src/cruby_methods.rs | 15 ++++++++++++++- zjit/src/hir/opt_tests.rs | 9 ++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index e6c26148cfb687..2d5bb3b62f70a4 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -220,7 +220,7 @@ pub fn init() -> Annotations { annotate!(rb_cModule, "name", types::StringExact.union(types::NilClass), no_gc, leaf, elidable); annotate!(rb_cModule, "===", inline_module_eqq, types::BoolExact, no_gc, leaf); annotate!(rb_cArray, "length", inline_array_length, types::Fixnum, no_gc, leaf, elidable); - annotate!(rb_cArray, "empty?", types::BoolExact, no_gc, leaf, elidable); + annotate!(rb_cArray, "empty?", inline_array_empty_p, types::BoolExact, no_gc, leaf, elidable); annotate!(rb_cArray, "reverse", types::ArrayExact, leaf, elidable); annotate!(rb_cArray, "join", types::StringExact); annotate!(rb_cArray, "[]", inline_array_aref); @@ -547,6 +547,19 @@ fn inline_array_length(fun: &mut hir::Function, block: hir::BlockId, recv: hir:: None } +fn inline_array_empty_p(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::Array, state) { + let recv = fun.coerce_to(block, recv, types::Array, state); + let length_cint = fun.push_insn(block, hir::Insn::ArrayLength { array: recv }); + let zero = fun.push_insn(block, hir::Insn::Const { val: hir::Const::CInt64(0) }); + let result_c = fun.push_insn(block, hir::Insn::IsBitEqual { left: length_cint, right: zero }); + let result = fun.push_insn(block, hir::Insn::BoxBool { val: result_c }); + return Some(result); + } + None +} + fn inline_integer_succ(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { if !args.is_empty() { return None; } if fun.likely_a(recv, types::Fixnum, state) { diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 4fc47dc611c4f1..3b6f694810e821 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -5411,7 +5411,7 @@ mod hir_opt_tests { } #[test] - fn test_specialize_array_empty_p_to_ccall() { + fn test_specialize_array_empty_p() { eval(" def test(a) = a.empty? @@ -5431,10 +5431,13 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, empty?@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact + v24:CInt64 = ArrayLength v23 + v25:CInt64[0] = Const CInt64(0) + v26:CBool = IsBitEqual v24, v25 + v27:BoolExact = BoxBool v26 IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall v23, :Array#empty?@0x1038 CheckInterrupts - Return v25 + Return v27 "); } From 351616af8c92329e143db24969125ca62f8b6ffc Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Fri, 9 Jan 2026 14:32:28 -0700 Subject: [PATCH 3/8] ZJIT: Add snapshot tests for direct send --- zjit/src/hir/tests.rs | 119 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 9810e48145c16d..4528901e9f982b 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -14,6 +14,16 @@ mod snapshot_tests { format!("{}", FunctionPrinter::with_snapshot(&function)) } + #[track_caller] + fn optimized_hir_string(method: &str) -> String { + let iseq = crate::cruby::with_rubyvm(|| get_proc_iseq(&format!("{}.method(:{})", "self", method))); + unsafe { crate::cruby::rb_zjit_profile_disable(iseq) }; + let mut function = iseq_to_hir(iseq).unwrap(); + function.optimize(); + function.validate().unwrap(); + format!("{}", FunctionPrinter::with_snapshot(&function)) + } + #[test] fn test_new_array_with_elements() { eval("def test(a, b) = [a, b]"); @@ -41,6 +51,115 @@ mod snapshot_tests { Return v18 "); } + + #[test] + fn test_send_direct_with_reordered_kwargs_has_snapshot() { + eval(" + def foo(a:, b:, c:) = [a, b, c] + def test = foo(c: 3, a: 1, b: 2) + test + test + "); + assert_snapshot!(optimized_hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v8:Any = Snapshot FrameState { pc: 0x1000, stack: [], locals: [] } + PatchPoint NoTracePoint + v11:Fixnum[3] = Const Value(3) + v13:Fixnum[1] = Const Value(1) + v15:Fixnum[2] = Const Value(2) + v16:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15], locals: [] } + PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Object@0x1010) + v24:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)] + v25:BasicObject = SendWithoutBlockDirect v24, :foo (0x1048), v13, v15, v11 + v18:Any = Snapshot FrameState { pc: 0x1050, stack: [v25], locals: [] } + PatchPoint NoTracePoint + CheckInterrupts + Return v25 + "); + } + + #[test] + fn test_send_direct_with_kwargs_in_order_has_snapshot() { + eval(" + def foo(a:, b:) = [a, b] + def test = foo(a: 1, b: 2) + test + test + "); + assert_snapshot!(optimized_hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v8:Any = Snapshot FrameState { pc: 0x1000, stack: [], locals: [] } + PatchPoint NoTracePoint + v11:Fixnum[1] = Const Value(1) + v13:Fixnum[2] = Const Value(2) + v14:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] } + PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Object@0x1010) + v22:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)] + v23:BasicObject = SendWithoutBlockDirect v22, :foo (0x1048), v11, v13 + v16:Any = Snapshot FrameState { pc: 0x1050, stack: [v23], locals: [] } + PatchPoint NoTracePoint + CheckInterrupts + Return v23 + "); + } + + #[test] + fn test_send_direct_with_many_kwargs_no_reorder_snapshot() { + eval(" + def foo(five, six, a:, b:, c:, d:, e:, f:) = [a, b, c, d, five, six, e, f] + def test = foo(5, 6, d: 4, c: 3, a: 1, b: 2, e: 7, f: 8) + test + test + "); + assert_snapshot!(optimized_hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v8:Any = Snapshot FrameState { pc: 0x1000, stack: [], locals: [] } + PatchPoint NoTracePoint + v11:Fixnum[5] = Const Value(5) + v13:Fixnum[6] = Const Value(6) + v15:Fixnum[4] = Const Value(4) + v17:Fixnum[3] = Const Value(3) + v19:Fixnum[1] = Const Value(1) + v21:Fixnum[2] = Const Value(2) + v23:Fixnum[7] = Const Value(7) + v25:Fixnum[8] = Const Value(8) + v26:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15, v17, v19, v21, v23, v25], locals: [] } + PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) + PatchPoint NoSingletonClass(Object@0x1010) + v34:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)] + v35:BasicObject = SendWithoutBlockDirect v34, :foo (0x1048), v11, v13, v19, v21, v17, v15, v23, v25 + v28:Any = Snapshot FrameState { pc: 0x1050, stack: [v35], locals: [] } + PatchPoint NoTracePoint + CheckInterrupts + Return v35 + "); + } } #[cfg(test)] From d81a11d4e61f67b6fb0aaa44aaa7ead4022148dd Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Thu, 8 Jan 2026 10:01:43 -0700 Subject: [PATCH 4/8] ZJIT: Snapshot FrameState with reordered args before direct send You can see the reordered args in the new Snapshot right before the DirectSend insn: v14:Any = Snapshot FrameState { pc: 0x00, stack: [v6, v11, v13], locals: [] } PatchPoint MethodRedefined(Object@0x00, a@0x00, cme:0x00) PatchPoint NoSingletonClass(Object@0x00) v22:HeapObject[class_exact*:Object@VALUE(0x00)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x00)] - v23:BasicObject = SendWithoutBlockDirect v22, :a (0x00), v13, v11 - v16:Any = Snapshot FrameState { pc: 0x00, stack: [v23], locals: [] } + v23:Any = Snapshot FrameState { pc: 0x00, stack: [v6, v13, v11], locals: [] } + v24:BasicObject = SendWithoutBlockDirect v22, :a (0x00), v13, v11 + v16:Any = Snapshot FrameState { pc: 0x00, stack: [v24], locals: [] } --- test/ruby/test_zjit.rb | 37 ++++++++++++++++++++++++++++++ zjit/src/hir.rs | 48 ++++++++++++++++++++++++++++++++------- zjit/src/hir/opt_tests.rs | 20 ++++++++-------- zjit/src/hir/tests.rs | 14 +++++++----- 4 files changed, 95 insertions(+), 24 deletions(-) diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb index 43db676d5d1cd3..cf3c46b3ed56a7 100644 --- a/test/ruby/test_zjit.rb +++ b/test/ruby/test_zjit.rb @@ -383,6 +383,43 @@ def f(a) }, call_threshold: 2 end + def test_kwargs_with_exit_and_local_invalidation + assert_compiles ':ok', %q{ + def a(b:, c:) + if c == :b + return -> {} + end + Class # invalidate locals + + raise "c is :b!" if c == :b + end + + def test + # note opposite order of kwargs + a(c: :c, b: :b) + end + + 4.times { test } + :ok + }, call_threshold: 2 + end + + def test_kwargs_with_max_direct_send_arg_count + # Ensure that we only reorder the args when we _can_ use direct send (< 6 args). + assert_compiles '[[1, 2, 3, 4, 5, 6, 7, 8]]', %q{ + def kwargs(five, six, a:, b:, c:, d:, e:, f:) + [a, b, c, d, five, six, e, f] + end + + 5.times.flat_map do + [ + kwargs(5, 6, d: 4, c: 3, a: 1, b: 2, e: 7, f: 8), + kwargs(5, 6, d: 4, c: 3, b: 2, a: 1, e: 7, f: 8) + ] + end.uniq + }, call_threshold: 2 + end + def test_setlocal_on_eval assert_compiles '1', %q{ @b = binding diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 41ddb3ae6934d9..36aee8056c7fa6 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6,6 +6,7 @@ #![allow(clippy::if_same_then_else)] #![allow(clippy::match_like_matches_macro)] use crate::{ + backend::lir::C_ARG_OPNDS, cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState, json::Json }; use std::{ @@ -2699,19 +2700,30 @@ impl Function { } let kwarg = unsafe { rb_vm_ci_kwarg(ci) }; - let processed_args = if !kwarg.is_null() { + let (send_state, processed_args) = if !kwarg.is_null() { match self.reorder_keyword_arguments(&args, kwarg, iseq) { - Ok(reordered) => reordered, + Ok(reordered) => { + // Only use reordered state if args fit in C registers. + // Fallback to interpreter needs original order for kwarg handling. + // NOTE: This needs to match with the condition in codegen.rs. + if reordered.len() + 1 <= C_ARG_OPNDS.len() { + let new_state = self.frame_state(state).with_reordered_args(&reordered); + let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state }); + (snapshot, reordered) + } else { + (state, reordered) + } + } Err(reason) => { self.set_dynamic_send_reason(insn_id, reason); self.push_insn_id(block, insn_id); continue; } } } else { - args.clone() + (state, args.clone()) }; - let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state }); + let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state: send_state }); self.make_equal_to(insn_id, send_direct); } else if def_type == VM_METHOD_TYPE_BMETHOD { let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) }; @@ -2748,19 +2760,30 @@ impl Function { } let kwarg = unsafe { rb_vm_ci_kwarg(ci) }; - let processed_args = if !kwarg.is_null() { + let (send_state, processed_args) = if !kwarg.is_null() { match self.reorder_keyword_arguments(&args, kwarg, iseq) { - Ok(reordered) => reordered, + Ok(reordered) => { + // Only use reordered state if args fit in C registers. + // Fallback to interpreter needs original order for kwarg handling. + // NOTE: This needs to match with the condition in codegen.rs. + if reordered.len() + 1 <= C_ARG_OPNDS.len() { + let new_state = self.frame_state(state).with_reordered_args(&reordered); + let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state }); + (snapshot, reordered) + } else { + (state, reordered) + } + } Err(reason) => { self.set_dynamic_send_reason(insn_id, reason); self.push_insn_id(block, insn_id); continue; } } } else { - args.clone() + (state, args.clone()) }; - let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state }); + let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state: send_state }); self.make_equal_to(insn_id, send_direct); } else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() { // Check if we're accessing ivars of a Class or Module object as they require single-ractor mode. @@ -5045,6 +5068,15 @@ impl FrameState { state.stack.clear(); state } + + /// Return itself with send args reordered. Used when kwargs are reordered for callee. + fn with_reordered_args(&self, reordered_args: &[InsnId]) -> Self { + let mut state = self.clone(); + let args_start = state.stack.len() - reordered_args.len(); + state.stack.truncate(args_start); + state.stack.extend_from_slice(reordered_args); + state + } } /// Print adaptor for [`FrameState`]. See [`PtrPrintMap`]. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 3b6f694810e821..93e59b35fbafd6 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -2816,9 +2816,9 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Object@0x1000) v22:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] - v23:BasicObject = SendWithoutBlockDirect v22, :foo (0x1038), v11, v13 + v24:BasicObject = SendWithoutBlockDirect v22, :foo (0x1038), v11, v13 CheckInterrupts - Return v23 + Return v24 "); } @@ -2846,9 +2846,9 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Object@0x1000) v24:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] - v25:BasicObject = SendWithoutBlockDirect v24, :foo (0x1038), v13, v15, v11 + v26:BasicObject = SendWithoutBlockDirect v24, :foo (0x1038), v13, v15, v11 CheckInterrupts - Return v25 + Return v26 "); } @@ -2876,9 +2876,9 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Object@0x1000) v24:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] - v25:BasicObject = SendWithoutBlockDirect v24, :foo (0x1038), v11, v15, v13 + v26:BasicObject = SendWithoutBlockDirect v24, :foo (0x1038), v11, v15, v13 CheckInterrupts - Return v25 + Return v26 "); } @@ -2933,16 +2933,16 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Object@0x1000) v37:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] - v38:BasicObject = SendWithoutBlockDirect v37, :foo (0x1038), v11, v13, v15 + v39:BasicObject = SendWithoutBlockDirect v37, :foo (0x1038), v11, v13, v15 v20:Fixnum[1] = Const Value(1) v22:Fixnum[2] = Const Value(2) v24:Fixnum[4] = Const Value(4) v26:Fixnum[3] = Const Value(3) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Object@0x1000) - v41:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] - v42:BasicObject = SendWithoutBlockDirect v41, :foo (0x1038), v20, v22, v26, v24 - v30:ArrayExact = NewArray v38, v42 + v42:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)] + v44:BasicObject = SendWithoutBlockDirect v42, :foo (0x1038), v20, v22, v26, v24 + v30:ArrayExact = NewArray v39, v44 CheckInterrupts Return v30 "); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 4528901e9f982b..44dceba27f5b2a 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -79,11 +79,12 @@ mod snapshot_tests { PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Object@0x1010) v24:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)] - v25:BasicObject = SendWithoutBlockDirect v24, :foo (0x1048), v13, v15, v11 - v18:Any = Snapshot FrameState { pc: 0x1050, stack: [v25], locals: [] } + v25:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v13, v15, v11], locals: [] } + v26:BasicObject = SendWithoutBlockDirect v24, :foo (0x1048), v13, v15, v11 + v18:Any = Snapshot FrameState { pc: 0x1050, stack: [v26], locals: [] } PatchPoint NoTracePoint CheckInterrupts - Return v25 + Return v26 "); } @@ -113,11 +114,12 @@ mod snapshot_tests { PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Object@0x1010) v22:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)] - v23:BasicObject = SendWithoutBlockDirect v22, :foo (0x1048), v11, v13 - v16:Any = Snapshot FrameState { pc: 0x1050, stack: [v23], locals: [] } + v23:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] } + v24:BasicObject = SendWithoutBlockDirect v22, :foo (0x1048), v11, v13 + v16:Any = Snapshot FrameState { pc: 0x1050, stack: [v24], locals: [] } PatchPoint NoTracePoint CheckInterrupts - Return v23 + Return v24 "); } From ee1aa78bee5f5c46ebcd75a3fe3eff03787b0b44 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:21:42 +0100 Subject: [PATCH 5/8] [ruby/prism] Correctly expose ripper state It is for example used by `irb`, `rdoc`, `syntax_suggest` https://github.com/ruby/prism/commit/255aeb2485 --- lib/prism/lex_compat.rb | 72 ++++----------------------- lib/prism/prism.gemspec | 1 + lib/prism/translation/ripper.rb | 25 ++++++++++ lib/prism/translation/ripper/lexer.rb | 46 +++++++++++++++++ test/prism/ruby/ripper_test.rb | 11 ++-- 5 files changed, 87 insertions(+), 68 deletions(-) create mode 100644 lib/prism/translation/ripper/lexer.rb diff --git a/lib/prism/lex_compat.rb b/lib/prism/lex_compat.rb index ebfb19e56d999d..46f6130357e024 100644 --- a/lib/prism/lex_compat.rb +++ b/lib/prism/lex_compat.rb @@ -198,58 +198,6 @@ def deconstruct_keys(keys) "__END__": :on___end__ }.freeze - # Pretty much a 1:1 copy of Ripper::Lexer::State. We list all the available states - # to reimplement to_s without using Ripper. - class State - # Ripper-internal bitflags. - ALL = %i[ - BEG END ENDARG ENDFN ARG CMDARG MID FNAME DOT CLASS LABEL LABELED FITEM - ].map.with_index.to_h { |name, i| [2 ** i, name] } - ALL[0] = :NONE - ALL.freeze - ALL.each { |value, name| const_set(name, value) } - - # :stopdoc: - - attr_reader :to_int, :to_s - - def initialize(i) - @to_int = i - @to_s = state_name(i) - freeze - end - - def [](index) - case index - when 0, :to_int - @to_int - when 1, :to_s - @to_s - else - nil - end - end - - alias to_i to_int - alias inspect to_s - def pretty_print(q) q.text(to_s) end - def ==(i) super or to_int == i end - def &(i) self.class.new(to_int & i) end - def |(i) self.class.new(to_int | i) end - def allbits?(i) to_int.allbits?(i) end - def anybits?(i) to_int.anybits?(i) end - def nobits?(i) to_int.nobits?(i) end - - # :startdoc: - - private - - # Convert the state flags into the format exposed by ripper. - def state_name(bits) - ALL.filter_map { |flag, name| name if bits & flag != 0 }.join("|") - end - end - # When we produce tokens, we produce the same arrays that Ripper does. # However, we add a couple of convenience methods onto them to make them a # little easier to work with. We delegate all other methods to the array. @@ -300,8 +248,8 @@ def ==(other) # :nodoc: class IdentToken < Token def ==(other) # :nodoc: (self[0...-1] == other[0...-1]) && ( - (other[3] == State::LABEL | State::END) || - (other[3] & (State::ARG | State::CMDARG) != 0) + (other[3] == Translation::Ripper::EXPR_LABEL | Translation::Ripper::EXPR_END) || + (other[3] & (Translation::Ripper::EXPR_ARG | Translation::Ripper::EXPR_CMDARG) != 0) ) end end @@ -312,8 +260,8 @@ class IgnoredNewlineToken < Token def ==(other) # :nodoc: return false unless self[0...-1] == other[0...-1] - if self[3] == State::ARG | State::LABELED - other[3] & State::ARG | State::LABELED != 0 + if self[3] == Translation::Ripper::EXPR_ARG | Translation::Ripper::EXPR_LABELED + other[3] & Translation::Ripper::EXPR_ARG | Translation::Ripper::EXPR_LABELED != 0 else self[3] == other[3] end @@ -331,8 +279,8 @@ def ==(other) # :nodoc: class ParamToken < Token def ==(other) # :nodoc: (self[0...-1] == other[0...-1]) && ( - (other[3] == State::END) || - (other[3] == State::END | State::LABEL) + (other[3] == Translation::Ripper::EXPR_END) || + (other[3] == Translation::Ripper::EXPR_END | Translation::Ripper::EXPR_LABEL) ) end end @@ -727,7 +675,7 @@ def result event = RIPPER.fetch(token.type) value = token.value - lex_state = State.new(lex_state) + lex_state = Translation::Ripper::Lexer::State.new(lex_state) token = case event @@ -741,7 +689,7 @@ def result last_heredoc_end = token.location.end_offset IgnoreStateToken.new([[lineno, column], event, value, lex_state]) when :on_ident - if lex_state == State::END + if lex_state == Translation::Ripper::EXPR_END # If we have an identifier that follows a method name like: # # def foo bar @@ -751,7 +699,7 @@ def result # yet. We do this more accurately, so we need to allow comparing # against both END and END|LABEL. ParamToken.new([[lineno, column], event, value, lex_state]) - elsif lex_state == State::END | State::LABEL + elsif lex_state == Translation::Ripper::EXPR_END | Translation::Ripper::EXPR_LABEL # In the event that we're comparing identifiers, we're going to # allow a little divergence. Ripper doesn't account for local # variables introduced through named captures in regexes, and we @@ -791,7 +739,7 @@ def result counter += { on_embexpr_beg: -1, on_embexpr_end: 1 }[current_event] || 0 end - State.new(result_value[current_index][1]) + Translation::Ripper::Lexer::State.new(result_value[current_index][1]) else previous_state end diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index 6a3bed32d8bd7c..9f5d458d6c8efe 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -104,6 +104,7 @@ Gem::Specification.new do |spec| "lib/prism/translation/parser/compiler.rb", "lib/prism/translation/parser/lexer.rb", "lib/prism/translation/ripper.rb", + "lib/prism/translation/ripper/lexer.rb", "lib/prism/translation/ripper/sexp.rb", "lib/prism/translation/ripper/shim.rb", "lib/prism/translation/ruby_parser.rb", diff --git a/lib/prism/translation/ripper.rb b/lib/prism/translation/ripper.rb index 00d5f80af4b2f8..a901a726921375 100644 --- a/lib/prism/translation/ripper.rb +++ b/lib/prism/translation/ripper.rb @@ -424,9 +424,34 @@ def self.sexp_raw(src, filename = "-", lineno = 1, raise_errors: false) end end + autoload :Lexer, "prism/translation/ripper/lexer" autoload :SexpBuilder, "prism/translation/ripper/sexp" autoload :SexpBuilderPP, "prism/translation/ripper/sexp" + # :stopdoc: + # This is not part of the public API but used by some gems. + + # Ripper-internal bitflags. + LEX_STATE_NAMES = %i[ + BEG END ENDARG ENDFN ARG CMDARG MID FNAME DOT CLASS LABEL LABELED FITEM + ].map.with_index.to_h { |name, i| [2 ** i, name] }.freeze + private_constant :LEX_STATE_NAMES + + LEX_STATE_NAMES.each do |value, key| + const_set("EXPR_#{key}", value) + end + EXPR_NONE = 0 + EXPR_VALUE = EXPR_BEG + EXPR_BEG_ANY = EXPR_BEG | EXPR_MID | EXPR_CLASS + EXPR_ARG_ANY = EXPR_ARG | EXPR_CMDARG + EXPR_END_ANY = EXPR_END | EXPR_ENDARG | EXPR_ENDFN + + def self.lex_state_name(state) + LEX_STATE_NAMES.filter_map { |flag, name| name if state & flag != 0 }.join("|") + end + + # :startdoc: + # The source that is being parsed. attr_reader :source diff --git a/lib/prism/translation/ripper/lexer.rb b/lib/prism/translation/ripper/lexer.rb new file mode 100644 index 00000000000000..ed02e965747d81 --- /dev/null +++ b/lib/prism/translation/ripper/lexer.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +# :markup: markdown + +require_relative "../ripper" + +module Prism + module Translation + class Ripper + class Lexer # :nodoc: + # :stopdoc: + class State + + attr_reader :to_int, :to_s + + def initialize(i) + @to_int = i + @to_s = Ripper.lex_state_name(i) + freeze + end + + def [](index) + case index + when 0, :to_int + @to_int + when 1, :to_s + @to_s + else + nil + end + end + + alias to_i to_int + alias inspect to_s + def pretty_print(q) q.text(to_s) end + def ==(i) super or to_int == i end + def &(i) self.class.new(to_int & i) end + def |(i) self.class.new(to_int | i) end + def allbits?(i) to_int.allbits?(i) end + def anybits?(i) to_int.anybits?(i) end + def nobits?(i) to_int.nobits?(i) end + end + # :startdoc: + end + end + end +end diff --git a/test/prism/ruby/ripper_test.rb b/test/prism/ruby/ripper_test.rb index 9d64c5c70ce5a5..bbd85585a9237e 100644 --- a/test/prism/ruby/ripper_test.rb +++ b/test/prism/ruby/ripper_test.rb @@ -65,13 +65,12 @@ class RipperTest < TestCase # Check that the hardcoded values don't change without us noticing. def test_internals - actual = LexCompat::State::ALL - expected = Ripper.constants.select { |name| name.start_with?("EXPR_") } - expected -= %i[EXPR_VALUE EXPR_BEG_ANY EXPR_ARG_ANY EXPR_END_ANY] + actual = Translation::Ripper.constants.select { |name| name.start_with?("EXPR_") }.sort + expected = Ripper.constants.select { |name| name.start_with?("EXPR_") }.sort - assert_equal(expected.size, actual.size) - expected.each do |const_name| - assert_equal(const_name.to_s.delete_prefix("EXPR_").to_sym, actual[Ripper.const_get(const_name)]) + assert_equal(expected, actual) + expected.zip(actual).each do |ripper, prism| + assert_equal(Ripper.const_get(ripper), Translation::Ripper.const_get(prism)) end end From 328655633bc46887f46d7be2df974beb4ff89b7c Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Mon, 12 Jan 2026 10:32:10 -0500 Subject: [PATCH 6/8] ZJIT: Optimize Integer#[] This is used a lot in optcarrot. --- zjit/src/codegen.rs | 5 ++ zjit/src/cruby_methods.rs | 12 +++++ zjit/src/hir.rs | 13 +++++ zjit/src/hir/opt_tests.rs | 106 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 8d832c2e2569b3..d6a2ebec740c09 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -439,6 +439,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio gen_fixnum_rshift(asm, opnd!(left), shift_amount) } &Insn::FixnumMod { left, right, state } => gen_fixnum_mod(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), + &Insn::FixnumAref { recv, index } => gen_fixnum_aref(asm, opnd!(recv), opnd!(index)), Insn::IsNil { val } => gen_isnil(asm, opnd!(val)), &Insn::IsMethodCfunc { val, cd, cfunc, state: _ } => gen_is_method_cfunc(jit, asm, opnd!(val), cd, cfunc), &Insn::IsBitEqual { left, right } => gen_is_bit_equal(asm, opnd!(left), opnd!(right)), @@ -1911,6 +1912,10 @@ fn gen_fixnum_mod(jit: &mut JITState, asm: &mut Assembler, left: lir::Opnd, righ asm_ccall!(asm, rb_fix_mod_fix, left, right) } +fn gen_fixnum_aref(asm: &mut Assembler, recv: lir::Opnd, index: lir::Opnd) -> lir::Opnd { + asm_ccall!(asm, rb_fix_aref, recv, index) +} + // Compile val == nil fn gen_isnil(asm: &mut Assembler, val: lir::Opnd) -> lir::Opnd { asm.cmp(val, Qnil.into()); diff --git a/zjit/src/cruby_methods.rs b/zjit/src/cruby_methods.rs index 2d5bb3b62f70a4..b8082024728acc 100644 --- a/zjit/src/cruby_methods.rs +++ b/zjit/src/cruby_methods.rs @@ -255,6 +255,7 @@ pub fn init() -> Annotations { annotate!(rb_cInteger, "<=", inline_integer_le); annotate!(rb_cInteger, "<<", inline_integer_lshift); annotate!(rb_cInteger, ">>", inline_integer_rshift); + annotate!(rb_cInteger, "[]", inline_integer_aref); annotate!(rb_cInteger, "to_s", types::StringExact); annotate!(rb_cString, "to_s", inline_string_to_s, types::StringExact); let thread_singleton = unsafe { rb_singleton_class(rb_cThread) }; @@ -679,6 +680,17 @@ fn inline_integer_rshift(fun: &mut hir::Function, block: hir::BlockId, recv: hir try_inline_fixnum_op(fun, block, &|left, right| hir::Insn::FixnumRShift { left, right }, BOP_GTGT, recv, other, state) } +fn inline_integer_aref(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], state: hir::InsnId) -> Option { + let &[index] = args else { return None; }; + if fun.likely_a(recv, types::Fixnum, state) && fun.likely_a(index, types::Fixnum, state) { + let recv = fun.coerce_to(block, recv, types::Fixnum, state); + let index = fun.coerce_to(block, index, types::Fixnum, state); + let result = fun.push_insn(block, hir::Insn::FixnumAref { recv, index }); + return Some(result); + } + None +} + fn inline_basic_object_eq(fun: &mut hir::Function, block: hir::BlockId, recv: hir::InsnId, args: &[hir::InsnId], _state: hir::InsnId) -> Option { let &[other] = args else { return None; }; let c_result = fun.push_insn(block, hir::Insn::IsBitEqual { left: recv, right: other }); diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 36aee8056c7fa6..6cdb7993d071d7 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -765,6 +765,7 @@ pub enum Insn { /// Convert a C `long` to a Ruby `Fixnum`. Side exit on overflow. BoxFixnum { val: InsnId, state: InsnId }, UnboxFixnum { val: InsnId }, + FixnumAref { recv: InsnId, index: InsnId }, // TODO(max): In iseq body types that are not ISEQ_TYPE_METHOD, rewrite to Constant false. Defined { op_type: usize, obj: VALUE, pushval: VALUE, v: InsnId, state: InsnId }, GetConstantPath { ic: *const iseq_inline_constant_cache, state: InsnId }, @@ -1050,6 +1051,7 @@ impl Insn { Insn::FixnumXor { .. } => false, Insn::FixnumLShift { .. } => false, Insn::FixnumRShift { .. } => false, + Insn::FixnumAref { .. } => false, Insn::GetLocal { .. } => false, Insn::IsNil { .. } => false, Insn::LoadPC => false, @@ -1254,6 +1256,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> { Insn::BoxBool { val } => write!(f, "BoxBool {val}"), Insn::BoxFixnum { val, .. } => write!(f, "BoxFixnum {val}"), Insn::UnboxFixnum { val } => write!(f, "UnboxFixnum {val}"), + Insn::FixnumAref { recv, index } => write!(f, "FixnumAref {recv}, {index}"), Insn::Jump(target) => { write!(f, "Jump {target}") } Insn::IfTrue { val, target } => { write!(f, "IfTrue {val}, {target}") } Insn::IfFalse { val, target } => { write!(f, "IfFalse {val}, {target}") } @@ -1971,6 +1974,7 @@ impl Function { &BoxBool { val } => BoxBool { val: find!(val) }, &BoxFixnum { val, state } => BoxFixnum { val: find!(val), state: find!(state) }, &UnboxFixnum { val } => UnboxFixnum { val: find!(val) }, + &FixnumAref { recv, index } => FixnumAref { recv: find!(recv), index: find!(index) }, Jump(target) => Jump(find_branch_edge!(target)), &IfTrue { val, ref target } => IfTrue { val: find!(val), target: find_branch_edge!(target) }, &IfFalse { val, ref target } => IfFalse { val: find!(val), target: find_branch_edge!(target) }, @@ -2184,6 +2188,7 @@ impl Function { Insn::BoxBool { .. } => types::BoolExact, Insn::BoxFixnum { .. } => types::Fixnum, Insn::UnboxFixnum { .. } => types::CInt64, + Insn::FixnumAref { .. } => types::Fixnum, Insn::StringCopy { .. } => types::StringExact, Insn::StringIntern { .. } => types::Symbol, Insn::StringConcat { .. } => types::StringExact, @@ -4150,6 +4155,10 @@ impl Function { &Insn::ObjectAllocClass { state, .. } | &Insn::SideExit { state, .. } => worklist.push_back(state), &Insn::UnboxFixnum { val } => worklist.push_back(val), + &Insn::FixnumAref { recv, index } => { + worklist.push_back(recv); + worklist.push_back(index); + } &Insn::IsA { val, class } => { worklist.push_back(val); worklist.push_back(class); @@ -4817,6 +4826,10 @@ impl Function { Insn::UnboxFixnum { val } => { self.assert_subtype(insn_id, val, types::Fixnum) } + Insn::FixnumAref { recv, index } => { + self.assert_subtype(insn_id, recv, types::Fixnum)?; + self.assert_subtype(insn_id, index, types::Fixnum) + } Insn::FixnumAdd { left, right, .. } | Insn::FixnumSub { left, right, .. } | Insn::FixnumMult { left, right, .. } diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 93e59b35fbafd6..1d360bed71b44c 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1201,6 +1201,112 @@ mod hir_opt_tests { "); } + #[test] + fn integer_aref_with_fixnum_emits_fixnum_aref() { + eval(" + def test(a, b) = a[b] + test(3, 4) + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:BasicObject = GetLocal :a, l0, SP@5 + v3:BasicObject = GetLocal :b, l0, SP@4 + Jump bb2(v1, v2, v3) + bb1(v6:BasicObject, v7:BasicObject, v8:BasicObject): + EntryPoint JIT(0) + Jump bb2(v6, v7, v8) + bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): + PatchPoint MethodRedefined(Integer@0x1000, []@0x1008, cme:0x1010) + v26:Fixnum = GuardType v11, Fixnum + v27:Fixnum = GuardType v12, Fixnum + v28:Fixnum = FixnumAref v26, v27 + IncrCounter inline_cfunc_optimized_send_count + CheckInterrupts + Return v28 + "); + } + + #[test] + fn elide_fixnum_aref() { + eval(" + def test + 1[2] + 5 + end + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:3: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[1] = Const Value(1) + v12:Fixnum[2] = Const Value(2) + PatchPoint MethodRedefined(Integer@0x1000, []@0x1008, cme:0x1010) + IncrCounter inline_cfunc_optimized_send_count + v19:Fixnum[5] = Const Value(5) + CheckInterrupts + Return v19 + "); + } + + #[test] + fn do_not_optimize_integer_aref_with_too_many_args() { + eval(" + def test = 1[2, 3] + "); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[1] = Const Value(1) + v12:Fixnum[2] = Const Value(2) + v14:Fixnum[3] = Const Value(3) + PatchPoint MethodRedefined(Integer@0x1000, []@0x1008, cme:0x1010) + v23:BasicObject = CCallVariadic v10, :Integer#[]@0x1038, v12, v14 + CheckInterrupts + Return v23 + "); + } + + #[test] + fn do_not_optimize_integer_aref_with_non_fixnum() { + eval(r#" + def test = 1["x"] + "#); + assert_snapshot!(hir_string("test"), @r" + fn test@:2: + bb0(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb2(v1) + bb1(v4:BasicObject): + EntryPoint JIT(0) + Jump bb2(v4) + bb2(v6:BasicObject): + v10:Fixnum[1] = Const Value(1) + v12:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) + v13:StringExact = StringCopy v12 + PatchPoint MethodRedefined(Integer@0x1008, []@0x1010, cme:0x1018) + v23:BasicObject = CCallVariadic v10, :Integer#[]@0x1040, v13 + CheckInterrupts + Return v23 + "); + } + #[test] fn test_optimize_send_into_fixnum_lt_both_profiled() { eval(" From 2daed3c02258e32fb2f0b25f3f0dc0461478de5b Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Fri, 9 Jan 2026 21:22:08 -0500 Subject: [PATCH 7/8] [ruby/mmtk] Clear slot when object is freed https://github.com/ruby/mmtk/commit/87290e45b2 --- gc/mmtk/mmtk.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gc/mmtk/mmtk.c b/gc/mmtk/mmtk.c index 03210536131a09..e58d88025bf5e9 100644 --- a/gc/mmtk/mmtk.c +++ b/gc/mmtk/mmtk.c @@ -330,6 +330,11 @@ rb_mmtk_call_obj_free(MMTk_ObjectReference object) } rb_gc_obj_free(objspace, obj); + + // TODO: uncomment this when done debugging +// #ifdef MMTK_DEBUG + memset((void *)obj, 0, rb_gc_impl_obj_slot_size(obj)); +// #endif } static size_t From 7c91db9ea020f297f99095765a962284d3d08f3d Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Mon, 12 Jan 2026 16:28:06 -0700 Subject: [PATCH 8/8] ZJIT: Check arg limit before pushing SendWithoutBLockDirect insn (#15854) This reduces some processing and makes the HIR more accurate. --- zjit/src/codegen.rs | 3 --- zjit/src/hir.rs | 32 ++++++++++++-------------------- zjit/src/hir/tests.rs | 9 +++------ 3 files changed, 15 insertions(+), 29 deletions(-) diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index d6a2ebec740c09..4a186d960c062c 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -401,9 +401,6 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::Send { cd, blockiseq, state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::SendWithoutBlock { cd, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason), - // Give up SendWithoutBlockDirect for 6+ args since asm.ccall() doesn't support it. - Insn::SendWithoutBlockDirect { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self - gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::TooManyArgsForLir), Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state)), &Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason), &Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason), diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 6cdb7993d071d7..a959ab07f413f4 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1662,6 +1662,12 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq return false; } + // asm.ccall() doesn't support 6+ args + if args.len() + 1 > C_ARG_OPNDS.len() { // +1 for self + function.set_dynamic_send_reason(send_insn, TooManyArgsForLir); + return false; + } + // Because we exclude e.g. post parameters above, they are also excluded from the sum below. let lead_num = params.lead_num; let opt_num = params.opt_num; @@ -2708,16 +2714,9 @@ impl Function { let (send_state, processed_args) = if !kwarg.is_null() { match self.reorder_keyword_arguments(&args, kwarg, iseq) { Ok(reordered) => { - // Only use reordered state if args fit in C registers. - // Fallback to interpreter needs original order for kwarg handling. - // NOTE: This needs to match with the condition in codegen.rs. - if reordered.len() + 1 <= C_ARG_OPNDS.len() { - let new_state = self.frame_state(state).with_reordered_args(&reordered); - let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state }); - (snapshot, reordered) - } else { - (state, reordered) - } + let new_state = self.frame_state(state).with_reordered_args(&reordered); + let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state }); + (snapshot, reordered) } Err(reason) => { self.set_dynamic_send_reason(insn_id, reason); @@ -2768,16 +2767,9 @@ impl Function { let (send_state, processed_args) = if !kwarg.is_null() { match self.reorder_keyword_arguments(&args, kwarg, iseq) { Ok(reordered) => { - // Only use reordered state if args fit in C registers. - // Fallback to interpreter needs original order for kwarg handling. - // NOTE: This needs to match with the condition in codegen.rs. - if reordered.len() + 1 <= C_ARG_OPNDS.len() { - let new_state = self.frame_state(state).with_reordered_args(&reordered); - let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state }); - (snapshot, reordered) - } else { - (state, reordered) - } + let new_state = self.frame_state(state).with_reordered_args(&reordered); + let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state }); + (snapshot, reordered) } Err(reason) => { self.set_dynamic_send_reason(insn_id, reason); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 44dceba27f5b2a..2ae31e862dc0ef 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -152,14 +152,11 @@ mod snapshot_tests { v23:Fixnum[7] = Const Value(7) v25:Fixnum[8] = Const Value(8) v26:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13, v15, v17, v19, v21, v23, v25], locals: [] } - PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020) - PatchPoint NoSingletonClass(Object@0x1010) - v34:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)] - v35:BasicObject = SendWithoutBlockDirect v34, :foo (0x1048), v11, v13, v19, v21, v17, v15, v23, v25 - v28:Any = Snapshot FrameState { pc: 0x1050, stack: [v35], locals: [] } + v27:BasicObject = SendWithoutBlock v6, :foo, v11, v13, v15, v17, v19, v21, v23, v25 # SendFallbackReason: Too many arguments for LIR + v28:Any = Snapshot FrameState { pc: 0x1010, stack: [v27], locals: [] } PatchPoint NoTracePoint CheckInterrupts - Return v35 + Return v27 "); } }