From 28908a95c4d303e3cf12f03b0b475a48778435a7 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 19 Nov 2025 21:46:12 +0100 Subject: [PATCH 01/13] Fix provided features spec with --repeat 2 --- spec/ruby/core/kernel/require_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb index 81777c5313a528..d680859dd6195d 100644 --- a/spec/ruby/core/kernel/require_spec.rb +++ b/spec/ruby/core/kernel/require_spec.rb @@ -32,13 +32,14 @@ features.sort.should == provided.sort + requires = provided ruby_version_is "3.5" do - provided.map! { |f| f == "pathname" ? "pathname.so" : f } + requires = requires.map { |f| f == "pathname" ? "pathname.so" : f } end - code = provided.map { |f| "puts require #{f.inspect}\n" }.join + code = requires.map { |f| "puts require #{f.inspect}\n" }.join required = ruby_exe(code, options: '--disable-gems') - required.should == "false\n" * provided.size + required.should == "false\n" * requires.size end it_behaves_like :kernel_require_basic, :require, CodeLoadingSpecs::Method.new From 4a1af72a13d41dcc38af7d69ea1f44856265d43f Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 19 Nov 2025 12:58:24 -0800 Subject: [PATCH 02/13] ZJIT: Count all calls to C functions from generated code (#15240) lobsters: ``` Top-20 calls to C functions from JIT code (79.9% of total 97,004,883): rb_vm_opt_send_without_block: 19,874,212 (20.5%) rb_vm_setinstancevariable: 9,774,841 (10.1%) rb_ivar_get: 9,358,866 ( 9.6%) rb_hash_aref: 6,828,948 ( 7.0%) rb_vm_send: 6,441,551 ( 6.6%) rb_vm_env_write: 5,375,989 ( 5.5%) rb_vm_invokesuper: 3,037,836 ( 3.1%) Module#===: 2,562,446 ( 2.6%) rb_ary_entry: 2,354,546 ( 2.4%) Kernel#is_a?: 1,424,092 ( 1.5%) rb_vm_opt_getconstant_path: 1,344,923 ( 1.4%) Thread.current: 1,300,822 ( 1.3%) rb_zjit_defined_ivar: 1,222,613 ( 1.3%) rb_vm_invokeblock: 1,184,555 ( 1.2%) Hash#[]=: 1,061,969 ( 1.1%) rb_ary_push: 1,024,987 ( 1.1%) rb_ary_new_capa: 904,003 ( 0.9%) rb_str_buf_append: 833,782 ( 0.9%) rb_class_allocate_instance: 822,626 ( 0.8%) Hash#fetch: 755,913 ( 0.8%) ``` railsbench: ``` Top-20 calls to C functions from JIT code (74.8% of total 189,170,268): rb_vm_opt_send_without_block: 29,870,307 (15.8%) rb_vm_setinstancevariable: 17,631,199 ( 9.3%) rb_hash_aref: 16,928,890 ( 8.9%) rb_ivar_get: 14,441,240 ( 7.6%) rb_vm_env_write: 11,571,001 ( 6.1%) rb_vm_send: 11,153,457 ( 5.9%) rb_vm_invokesuper: 7,568,267 ( 4.0%) Module#===: 6,065,923 ( 3.2%) Hash#[]=: 2,842,990 ( 1.5%) rb_ary_entry: 2,766,125 ( 1.5%) rb_ary_push: 2,722,079 ( 1.4%) rb_vm_invokeblock: 2,594,398 ( 1.4%) Thread.current: 2,560,129 ( 1.4%) rb_str_getbyte: 1,965,627 ( 1.0%) Kernel#is_a?: 1,961,815 ( 1.0%) rb_vm_opt_getconstant_path: 1,863,678 ( 1.0%) rb_hash_new_with_size: 1,796,456 ( 0.9%) rb_class_allocate_instance: 1,785,043 ( 0.9%) String#empty?: 1,713,414 ( 0.9%) rb_ary_new_capa: 1,678,834 ( 0.9%) ``` shipit: ``` Top-20 calls to C functions from JIT code (83.4% of total 182,402,821): rb_vm_opt_send_without_block: 45,753,484 (25.1%) rb_ivar_get: 21,020,650 (11.5%) rb_vm_setinstancevariable: 17,528,603 ( 9.6%) rb_hash_aref: 11,892,856 ( 6.5%) rb_vm_send: 11,723,471 ( 6.4%) rb_vm_env_write: 10,434,452 ( 5.7%) Module#===: 4,225,048 ( 2.3%) rb_vm_invokesuper: 3,705,906 ( 2.0%) Thread.current: 3,337,603 ( 1.8%) rb_ary_entry: 3,114,378 ( 1.7%) Hash#[]=: 2,509,912 ( 1.4%) Array#empty?: 2,282,994 ( 1.3%) rb_vm_invokeblock: 2,210,511 ( 1.2%) Hash#fetch: 2,017,960 ( 1.1%) _bi20: 1,975,147 ( 1.1%) rb_zjit_defined_ivar: 1,897,127 ( 1.0%) rb_vm_opt_getconstant_path: 1,813,294 ( 1.0%) rb_ary_new_capa: 1,615,406 ( 0.9%) Kernel#is_a?: 1,567,854 ( 0.9%) rb_class_allocate_instance: 1,560,035 ( 0.9%) ``` Thanks to @eregon for the idea. Co-authored-by: Jacob Denbeaux Co-authored-by: Alan Wu --- zjit.rb | 1 + zjit/src/backend/lir.rs | 12 ++++++ zjit/src/codegen.rs | 68 +++++++++++++++++++------------- zjit/src/cruby.rs | 12 ++++-- zjit/src/hir.rs | 11 ++++-- zjit/src/hir/opt_tests.rs | 82 +++++++++++++++++++-------------------- zjit/src/state.rs | 9 +++++ zjit/src/stats.rs | 7 ++++ 8 files changed, 127 insertions(+), 75 deletions(-) diff --git a/zjit.rb b/zjit.rb index bb6d4d3cdca16c..fc306c19a47fba 100644 --- a/zjit.rb +++ b/zjit.rb @@ -174,6 +174,7 @@ def stats_string # Show counters independent from exit_* or dynamic_send_* print_counters_with_prefix(prefix: 'not_inlined_cfuncs_', prompt: 'not inlined C methods', buf:, stats:, limit: 20) + print_counters_with_prefix(prefix: 'ccall_', prompt: 'calls to C functions from JIT code', buf:, stats:, limit: 20) # Don't show not_annotated_cfuncs right now because it mostly duplicates not_inlined_cfuncs # print_counters_with_prefix(prefix: 'not_annotated_cfuncs_', prompt: 'not annotated C methods', buf:, stats:, limit: 20) diff --git a/zjit/src/backend/lir.rs b/zjit/src/backend/lir.rs index cb8382a43c940c..3c9bf72023d90b 100644 --- a/zjit/src/backend/lir.rs +++ b/zjit/src/backend/lir.rs @@ -2065,6 +2065,17 @@ impl Assembler { out } + pub fn count_call_to(&mut self, fn_name: &str) { + // We emit ccalls while initializing the JIT. Unfortunately, we skip those because + // otherwise we have no counter pointers to read. + if crate::state::ZJITState::has_instance() && get_option!(stats) { + let ccall_counter_pointers = crate::state::ZJITState::get_ccall_counter_pointers(); + let counter_ptr = ccall_counter_pointers.entry(fn_name.to_string()).or_insert_with(|| Box::new(0)); + let counter_ptr: &mut u64 = counter_ptr.as_mut(); + self.incr_counter(Opnd::const_ptr(counter_ptr), 1.into()); + } + } + pub fn cmp(&mut self, left: Opnd, right: Opnd) { self.push_insn(Insn::Cmp { left, right }); } @@ -2389,6 +2400,7 @@ pub(crate) use asm_comment; macro_rules! asm_ccall { [$asm: ident, $fn_name:ident, $($args:expr),* ] => {{ $crate::backend::lir::asm_comment!($asm, concat!("call ", stringify!($fn_name))); + $asm.count_call_to(stringify!($fn_name)); $asm.ccall($fn_name as *const u8, vec![$($args),*]) }}; } diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 8fc66791a665ad..18266b46933e6c 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -419,14 +419,14 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio &Insn::GuardLess { left, right, state } => gen_guard_less(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), &Insn::GuardGreaterEq { left, right, state } => gen_guard_greater_eq(jit, asm, opnd!(left), opnd!(right), &function.frame_state(state)), Insn::PatchPoint { invariant, state } => no_output!(gen_patch_point(jit, asm, invariant, &function.frame_state(*state))), - Insn::CCall { cfunc, args, name: _, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, opnds!(args)), + Insn::CCall { cfunc, args, name, return_type: _, elidable: _ } => gen_ccall(asm, *cfunc, *name, opnds!(args)), // Give up CCallWithFrame for 7+ args since asm.ccall() doesn't support it. Insn::CCallWithFrame { cd, state, args, .. } if args.len() > C_ARG_OPNDS.len() => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), SendFallbackReason::CCallWithFrameTooManyArgs), - Insn::CCallWithFrame { cfunc, args, cme, state, blockiseq, .. } => - gen_ccall_with_frame(jit, asm, *cfunc, opnds!(args), *cme, *blockiseq, &function.frame_state(*state)), - Insn::CCallVariadic { cfunc, recv, args, name: _, cme, state, return_type: _, elidable: _ } => { - gen_ccall_variadic(jit, asm, *cfunc, opnd!(recv), opnds!(args), *cme, &function.frame_state(*state)) + Insn::CCallWithFrame { cfunc, name, args, cme, state, blockiseq, .. } => + gen_ccall_with_frame(jit, asm, *cfunc, *name, opnds!(args), *cme, *blockiseq, &function.frame_state(*state)), + Insn::CCallVariadic { cfunc, recv, args, name, cme, state, return_type: _, elidable: _ } => { + gen_ccall_variadic(jit, asm, *cfunc, *name, opnd!(recv), opnds!(args), *cme, &function.frame_state(*state)) } Insn::GetIvar { self_val, id, state: _ } => gen_getivar(asm, opnd!(self_val), *id), Insn::SetGlobal { id, val, state } => no_output!(gen_setglobal(jit, asm, *id, opnd!(val), &function.frame_state(*state))), @@ -697,6 +697,7 @@ fn gen_invokebuiltin(jit: &JITState, asm: &mut Assembler, state: &FrameState, bf let mut cargs = vec![EC]; cargs.extend(args); + asm.count_call_to(unsafe { std::ffi::CStr::from_ptr(bf.name).to_str().unwrap() }); asm.ccall(bf.func_ptr as *const u8, cargs) } @@ -754,6 +755,7 @@ fn gen_ccall_with_frame( jit: &mut JITState, asm: &mut Assembler, cfunc: *const u8, + name: ID, args: Vec, cme: *const rb_callable_method_entry_t, blockiseq: Option, @@ -801,6 +803,7 @@ fn gen_ccall_with_frame( asm.mov(CFP, new_cfp); asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); + asm.count_call_to(&name.contents_lossy()); let result = asm.ccall(cfunc, args); asm_comment!(asm, "pop C frame"); @@ -817,7 +820,8 @@ fn gen_ccall_with_frame( /// Lowering for [`Insn::CCall`]. This is a low-level raw call that doesn't know /// anything about the callee, so handling for e.g. GC safety is dealt with elsewhere. -fn gen_ccall(asm: &mut Assembler, cfunc: *const u8, args: Vec) -> lir::Opnd { +fn gen_ccall(asm: &mut Assembler, cfunc: *const u8, name: ID, args: Vec) -> lir::Opnd { + asm.count_call_to(&name.contents_lossy()); asm.ccall(cfunc, args) } @@ -827,6 +831,7 @@ fn gen_ccall_variadic( jit: &mut JITState, asm: &mut Assembler, cfunc: *const u8, + name: ID, recv: Opnd, args: Vec, cme: *const rb_callable_method_entry_t, @@ -859,6 +864,7 @@ fn gen_ccall_variadic( asm.store(Opnd::mem(64, EC, RUBY_OFFSET_EC_CFP), CFP); let argv_ptr = gen_push_opnds(asm, &args); + asm.count_call_to(&name.contents_lossy()); let result = asm.ccall(cfunc, vec![args.len().into(), argv_ptr, recv]); gen_pop_opnds(asm, &args); @@ -1169,9 +1175,10 @@ fn gen_send( unsafe extern "C" { fn rb_vm_send(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; } - asm.ccall( - rb_vm_send as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into()], + asm_ccall!( + asm, + rb_vm_send, + EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into() ) } @@ -1192,9 +1199,10 @@ fn gen_send_forward( unsafe extern "C" { fn rb_vm_sendforward(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; } - asm.ccall( - rb_vm_sendforward as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into()], + asm_ccall!( + asm, + rb_vm_sendforward, + EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into() ) } @@ -1213,9 +1221,10 @@ fn gen_send_without_block( unsafe extern "C" { fn rb_vm_opt_send_without_block(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE; } - asm.ccall( - rb_vm_opt_send_without_block as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd)], + asm_ccall!( + asm, + rb_vm_opt_send_without_block, + EC, CFP, Opnd::const_ptr(cd) ) } @@ -1331,9 +1340,10 @@ fn gen_invokeblock( unsafe extern "C" { fn rb_vm_invokeblock(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE; } - asm.ccall( - rb_vm_invokeblock as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd)], + asm_ccall!( + asm, + rb_vm_invokeblock, + EC, CFP, Opnd::const_ptr(cd) ) } @@ -1353,9 +1363,10 @@ fn gen_invokesuper( unsafe extern "C" { fn rb_vm_invokesuper(ec: EcPtr, cfp: CfpPtr, cd: VALUE, blockiseq: IseqPtr) -> VALUE; } - asm.ccall( - rb_vm_invokesuper as *const u8, - vec![EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into()], + asm_ccall!( + asm, + rb_vm_invokesuper, + EC, CFP, Opnd::const_ptr(cd), VALUE::from(blockiseq).into() ) } @@ -1436,9 +1447,10 @@ fn gen_array_include( unsafe extern "C" { fn rb_vm_opt_newarray_include_p(ec: EcPtr, num: c_long, elts: *const VALUE, target: VALUE) -> VALUE; } - asm.ccall( - rb_vm_opt_newarray_include_p as *const u8, - vec![EC, num.into(), elements_ptr, target], + asm_ccall!( + asm, + rb_vm_opt_newarray_include_p, + EC, num.into(), elements_ptr, target ) } @@ -1454,9 +1466,10 @@ fn gen_dup_array_include( unsafe extern "C" { fn rb_vm_opt_duparray_include_p(ec: EcPtr, ary: VALUE, target: VALUE) -> VALUE; } - asm.ccall( - rb_vm_opt_duparray_include_p as *const u8, - vec![EC, ary.into(), target], + asm_ccall!( + asm, + rb_vm_opt_duparray_include_p, + EC, ary.into(), target ) } @@ -1527,6 +1540,7 @@ fn gen_object_alloc_class(asm: &mut Assembler, class: VALUE, state: &FrameState) let alloc_func = unsafe { rb_zjit_class_get_alloc_func(class) }; assert!(alloc_func.is_some(), "class {} passed to ObjectAllocClass must have an allocator", get_class_name(class)); asm_comment!(asm, "call allocator for class {}", get_class_name(class)); + asm.count_call_to(&format!("{}::allocator", get_class_name(class))); asm.ccall(alloc_func.unwrap() as *const u8, vec![class.into()]) } } diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index db47385bc88321..61c25a4092bdc4 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -775,11 +775,17 @@ pub fn rust_str_to_ruby(str: &str) -> VALUE { unsafe { rb_utf8_str_new(str.as_ptr() as *const _, str.len() as i64) } } -/// Produce a Ruby symbol from a Rust string slice -pub fn rust_str_to_sym(str: &str) -> VALUE { +/// Produce a Ruby ID from a Rust string slice +pub fn rust_str_to_id(str: &str) -> ID { let c_str = CString::new(str).unwrap(); let c_ptr: *const c_char = c_str.as_ptr(); - unsafe { rb_id2sym(rb_intern(c_ptr)) } + unsafe { rb_intern(c_ptr) } +} + +/// Produce a Ruby symbol from a Rust string slice +pub fn rust_str_to_sym(str: &str) -> VALUE { + let id = rust_str_to_id(str); + unsafe { rb_id2sym(id) } } /// Produce an owned Rust String from a C char pointer diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 58638f30f0264d..982400db5030cd 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -2889,12 +2889,13 @@ impl Function { let mut cfunc_args = vec![recv]; cfunc_args.append(&mut args); + let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); let ccall = fun.push_insn(block, Insn::CCallWithFrame { cd, cfunc, args: cfunc_args, cme, - name: method_id, + name, state, return_type: types::BasicObject, elidable: false, @@ -3018,6 +3019,7 @@ impl Function { // No inlining; emit a call let cfunc = unsafe { get_mct_func(cfunc) }.cast(); + let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); let mut cfunc_args = vec![recv]; cfunc_args.append(&mut args); let return_type = props.return_type; @@ -3025,7 +3027,7 @@ impl Function { // Filter for a leaf and GC free function if props.leaf && props.no_gc { fun.push_insn(block, Insn::IncrCounter(Counter::inline_cfunc_optimized_send_count)); - let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name: method_id, return_type, elidable }); + let ccall = fun.push_insn(block, Insn::CCall { cfunc, args: cfunc_args, name, return_type, elidable }); fun.make_equal_to(send_insn_id, ccall); } else { if get_option!(stats) { @@ -3036,7 +3038,7 @@ impl Function { cfunc, args: cfunc_args, cme, - name: method_id, + name, state, return_type, elidable, @@ -3099,12 +3101,13 @@ impl Function { } let return_type = props.return_type; let elidable = props.elidable; + let name = rust_str_to_id(&qualified_method_name(unsafe { (*cme).owner }, unsafe { (*cme).called_id })); let ccall = fun.push_insn(block, Insn::CCallVariadic { cfunc, recv, args, cme, - name: method_id, + name, state, return_type, elidable, diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index fadb6ced5f1c0a..19f0e91b47e82b 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -560,7 +560,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(CustomEq@0x1000, !=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(CustomEq@0x1000) v28:HeapObject[class_exact:CustomEq] = GuardType v9, HeapObject[class_exact:CustomEq] - v29:BoolExact = CCallWithFrame !=@0x1038, v28, v9 + v29:BoolExact = CCallWithFrame BasicObject#!=@0x1038, v28, v9 v20:NilClass = Const Value(nil) CheckInterrupts Return v20 @@ -784,7 +784,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1000, fun_new_map@0x1008, cme:0x1010) PatchPoint NoSingletonClass(C@0x1000) v24:ArraySubclass[class_exact:C] = GuardType v13, ArraySubclass[class_exact:C] - v25:BasicObject = CCallWithFrame fun_new_map@0x1038, v24, block=0x1040 + v25:BasicObject = CCallWithFrame C#fun_new_map@0x1038, v24, block=0x1040 v16:BasicObject = GetLocal l0, EP@3 CheckInterrupts Return v25 @@ -1043,7 +1043,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Object@0x1008, puts@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Object@0x1008) v22:HeapObject[class_exact*:Object@VALUE(0x1008)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1008)] - v23:BasicObject = CCallVariadic puts@0x1040, v22, v12 + v23:BasicObject = CCallVariadic Kernel#puts@0x1040, v22, v12 CheckInterrupts Return v23 "); @@ -2241,7 +2241,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Module@0x1010, name@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Module@0x1010) IncrCounter inline_cfunc_optimized_send_count - v34:StringExact|NilClass = CCall name@0x1048, v29 + v34:StringExact|NilClass = CCall Module#name@0x1048, v29 PatchPoint NoEPEscape(test) v22:Fixnum[1] = Const Value(1) CheckInterrupts @@ -2273,7 +2273,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v29:Fixnum = CCall length@0x1038, v13 + v29:Fixnum = CCall Array#length@0x1038, v13 v20:Fixnum[5] = Const Value(5) CheckInterrupts Return v20 @@ -2417,7 +2417,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v29:Fixnum = CCall size@0x1038, v13 + v29:Fixnum = CCall Array#size@0x1038, v13 v20:Fixnum[5] = Const Value(5) CheckInterrupts Return v20 @@ -3150,7 +3150,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1008, new@0x1010, cme:0x1018) PatchPoint MethodRedefined(Class@0x1040, new@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Class@0x1040) - v57:BasicObject = CCallVariadic new@0x1048, v46, v16 + v57:BasicObject = CCallVariadic Array.new@0x1048, v46, v16 CheckInterrupts Return v57 "); @@ -3181,7 +3181,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Set@0x1008, initialize@0x1040, cme:0x1048) PatchPoint NoSingletonClass(Set@0x1008) v49:SetExact = GuardType v18, SetExact - v50:BasicObject = CCallVariadic initialize@0x1070, v49 + v50:BasicObject = CCallVariadic Set#initialize@0x1070, v49 CheckInterrupts CheckInterrupts Return v18 @@ -3211,7 +3211,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1008, new@0x1010, cme:0x1018) PatchPoint MethodRedefined(Class@0x1040, new@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Class@0x1040) - v54:BasicObject = CCallVariadic new@0x1048, v43 + v54:BasicObject = CCallVariadic String.new@0x1048, v43 CheckInterrupts Return v54 "); @@ -3243,7 +3243,7 @@ mod hir_opt_tests { v50:RegexpExact = ObjectAllocClass Regexp:VALUE(0x1008) PatchPoint MethodRedefined(Regexp@0x1008, initialize@0x1048, cme:0x1050) PatchPoint NoSingletonClass(Regexp@0x1008) - v54:BasicObject = CCallVariadic initialize@0x1078, v50, v17 + v54:BasicObject = CCallVariadic Regexp#initialize@0x1078, v50, v17 CheckInterrupts CheckInterrupts Return v50 @@ -3271,7 +3271,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, length@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v30:Fixnum = CCall length@0x1038, v18 + v30:Fixnum = CCall Array#length@0x1038, v18 CheckInterrupts Return v30 "); @@ -3298,7 +3298,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, size@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) IncrCounter inline_cfunc_optimized_send_count - v30:Fixnum = CCall size@0x1038, v18 + v30:Fixnum = CCall Array#size@0x1038, v18 CheckInterrupts Return v30 "); @@ -3458,7 +3458,7 @@ mod hir_opt_tests { v10:HashExact = NewHash PatchPoint MethodRedefined(Hash@0x1000, dup@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Hash@0x1000) - v22:BasicObject = CCallWithFrame dup@0x1038, v10 + v22:BasicObject = CCallWithFrame Kernel#dup@0x1038, v10 v14:BasicObject = SendWithoutBlock v22, :freeze CheckInterrupts Return v14 @@ -3551,7 +3551,7 @@ mod hir_opt_tests { v10:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, dup@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) - v22:BasicObject = CCallWithFrame dup@0x1038, v10 + v22:BasicObject = CCallWithFrame Kernel#dup@0x1038, v10 v14:BasicObject = SendWithoutBlock v22, :freeze CheckInterrupts Return v14 @@ -3645,7 +3645,7 @@ mod hir_opt_tests { v11:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, dup@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v23:BasicObject = CCallWithFrame dup@0x1040, v11 + v23:BasicObject = CCallWithFrame String#dup@0x1040, v11 v15:BasicObject = SendWithoutBlock v23, :freeze CheckInterrupts Return v15 @@ -3740,7 +3740,7 @@ mod hir_opt_tests { v11:StringExact = StringCopy v10 PatchPoint MethodRedefined(String@0x1008, dup@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) - v23:BasicObject = CCallWithFrame dup@0x1040, v11 + v23:BasicObject = CCallWithFrame String#dup@0x1040, v11 v15:BasicObject = SendWithoutBlock v23, :-@ CheckInterrupts Return v15 @@ -3882,7 +3882,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1008, to_s@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) v31:ArrayExact = GuardType v9, ArrayExact - v32:BasicObject = CCallWithFrame to_s@0x1040, v31 + v32:BasicObject = CCallWithFrame Array#to_s@0x1040, v31 v19:String = AnyToString v9, str: v32 v21:StringExact = StringConcat v13, v19 CheckInterrupts @@ -4745,7 +4745,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall empty?@0x1038, v23 + v25:BoolExact = CCall Array#empty?@0x1038, v23 CheckInterrupts Return v25 "); @@ -4773,7 +4773,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Hash@0x1000) v23:HashExact = GuardType v9, HashExact IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall empty?@0x1038, v23 + v25:BoolExact = CCall Hash#empty?@0x1038, v23 CheckInterrupts Return v25 "); @@ -5036,7 +5036,7 @@ mod hir_opt_tests { v11:ArrayExact = ArrayDup v10 PatchPoint MethodRedefined(Array@0x1008, map@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) - v21:BasicObject = CCallWithFrame map@0x1040, v11, block=0x1048 + v21:BasicObject = CCallWithFrame Array#map@0x1040, v11, block=0x1048 CheckInterrupts Return v21 "); @@ -5484,7 +5484,7 @@ mod hir_opt_tests { v10:ArrayExact = NewArray PatchPoint MethodRedefined(Array@0x1000, reverse@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) - v20:ArrayExact = CCallWithFrame reverse@0x1038, v10 + v20:ArrayExact = CCallWithFrame Array#reverse@0x1038, v10 CheckInterrupts Return v20 "); @@ -5537,7 +5537,7 @@ mod hir_opt_tests { v13:StringExact = StringCopy v12 PatchPoint MethodRedefined(Array@0x1008, join@0x1010, cme:0x1018) PatchPoint NoSingletonClass(Array@0x1008) - v23:StringExact = CCallVariadic join@0x1040, v10, v13 + v23:StringExact = CCallVariadic Array#join@0x1040, v10, v13 CheckInterrupts Return v23 "); @@ -5859,7 +5859,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Class@0x1010, current@0x1018, cme:0x1020) PatchPoint NoSingletonClass(Class@0x1010) IncrCounter inline_cfunc_optimized_send_count - v25:BasicObject = CCall current@0x1048, v20 + v25:BasicObject = CCall Thread.current@0x1048, v20 CheckInterrupts Return v25 "); @@ -5889,7 +5889,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, []=@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v31:ArrayExact = GuardType v9, ArrayExact - v32:BasicObject = CCallVariadic []=@0x1038, v31, v16, v18 + v32:BasicObject = CCallVariadic Array#[]=@0x1038, v31, v16, v18 CheckInterrupts Return v18 "); @@ -5980,7 +5980,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(Array@0x1000, push@0x1008, cme:0x1010) PatchPoint NoSingletonClass(Array@0x1000) v28:ArrayExact = GuardType v9, ArrayExact - v29:BasicObject = CCallVariadic push@0x1038, v28, v14, v16, v18 + v29:BasicObject = CCallVariadic Array#push@0x1038, v28, v14, v16, v18 CheckInterrupts Return v29 "); @@ -6008,7 +6008,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall length@0x1038, v23 + v25:Fixnum = CCall Array#length@0x1038, v23 CheckInterrupts Return v25 "); @@ -6036,7 +6036,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Array@0x1000) v23:ArrayExact = GuardType v9, ArrayExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall size@0x1038, v23 + v25:Fixnum = CCall Array#size@0x1038, v23 CheckInterrupts Return v25 "); @@ -6064,7 +6064,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1008, =~@0x1010, cme:0x1018) PatchPoint NoSingletonClass(String@0x1008) v25:StringExact = GuardType v9, StringExact - v26:BasicObject = CCallWithFrame =~@0x1040, v25, v14 + v26:BasicObject = CCallWithFrame String#=~@0x1040, v25, v14 CheckInterrupts Return v26 "); @@ -6235,7 +6235,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1000, setbyte@0x1008, cme:0x1010) PatchPoint NoSingletonClass(String@0x1000) v30:StringExact = GuardType v13, StringExact - v31:BasicObject = CCallWithFrame setbyte@0x1038, v30, v14, v15 + v31:BasicObject = CCallWithFrame String#setbyte@0x1038, v30, v14, v15 CheckInterrupts Return v31 "); @@ -6264,7 +6264,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact IncrCounter inline_cfunc_optimized_send_count - v25:BoolExact = CCall empty?@0x1038, v23 + v25:BoolExact = CCall String#empty?@0x1038, v23 CheckInterrupts Return v25 "); @@ -6348,7 +6348,7 @@ mod hir_opt_tests { bb2(v8:BasicObject, v9:BasicObject): PatchPoint MethodRedefined(Integer@0x1000, succ@0x1008, cme:0x1010) v22:Integer = GuardType v9, Integer - v23:BasicObject = CCallWithFrame succ@0x1038, v22 + v23:BasicObject = CCallWithFrame Integer#succ@0x1038, v22 CheckInterrupts Return v23 "); @@ -6405,7 +6405,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(String@0x1000, <<@0x1008, cme:0x1010) PatchPoint NoSingletonClass(String@0x1000) v27:StringExact = GuardType v11, StringExact - v28:BasicObject = CCallWithFrame <<@0x1038, v27, v12 + v28:BasicObject = CCallWithFrame String#<<@0x1038, v27, v12 CheckInterrupts Return v28 "); @@ -6465,7 +6465,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(MyString@0x1000, <<@0x1008, cme:0x1010) PatchPoint NoSingletonClass(MyString@0x1000) v27:StringSubclass[class_exact:MyString] = GuardType v11, StringSubclass[class_exact:MyString] - v28:BasicObject = CCallWithFrame <<@0x1038, v27, v12 + v28:BasicObject = CCallWithFrame String#<<@0x1038, v27, v12 CheckInterrupts Return v28 "); @@ -6622,7 +6622,7 @@ mod hir_opt_tests { bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): PatchPoint MethodRedefined(Integer@0x1000, ^@0x1008, cme:0x1010) v25:Integer = GuardType v11, Integer - v26:BasicObject = CCallWithFrame ^@0x1038, v25, v12 + v26:BasicObject = CCallWithFrame Integer#^@0x1038, v25, v12 CheckInterrupts Return v26 "); @@ -6645,7 +6645,7 @@ mod hir_opt_tests { bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): PatchPoint MethodRedefined(Integer@0x1000, ^@0x1008, cme:0x1010) v25:Fixnum = GuardType v11, Fixnum - v26:BasicObject = CCallWithFrame ^@0x1038, v25, v12 + v26:BasicObject = CCallWithFrame Integer#^@0x1038, v25, v12 CheckInterrupts Return v26 "); @@ -6668,7 +6668,7 @@ mod hir_opt_tests { bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject): PatchPoint MethodRedefined(TrueClass@0x1000, ^@0x1008, cme:0x1010) v25:TrueClass = GuardType v11, TrueClass - v26:BasicObject = CCallWithFrame ^@0x1038, v25, v12 + v26:BasicObject = CCallWithFrame TrueClass#^@0x1038, v25, v12 CheckInterrupts Return v26 "); @@ -6718,7 +6718,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Hash@0x1000) v23:HashExact = GuardType v9, HashExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall size@0x1038, v23 + v25:Fixnum = CCall Hash#size@0x1038, v23 CheckInterrupts Return v25 "); @@ -7086,7 +7086,7 @@ mod hir_opt_tests { PatchPoint MethodRedefined(C@0x1008, respond_to?@0x1010, cme:0x1018) PatchPoint NoSingletonClass(C@0x1008) v24:HeapObject[class_exact:C] = GuardType v9, HeapObject[class_exact:C] - v25:BasicObject = CCallVariadic respond_to?@0x1040, v24, v14 + v25:BasicObject = CCallVariadic Kernel#respond_to?@0x1040, v24, v14 CheckInterrupts Return v25 "); @@ -7637,7 +7637,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall size@0x1038, v23 + v25:Fixnum = CCall String#size@0x1038, v23 CheckInterrupts Return v25 "); @@ -7756,7 +7756,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(String@0x1000) v23:StringExact = GuardType v9, StringExact IncrCounter inline_cfunc_optimized_send_count - v25:Fixnum = CCall length@0x1038, v23 + v25:Fixnum = CCall String#length@0x1038, v23 CheckInterrupts Return v25 "); @@ -7922,7 +7922,7 @@ mod hir_opt_tests { PatchPoint NoSingletonClass(Class@0x1038) v30:ModuleSubclass[class_exact*:Class@VALUE(0x1038)] = GuardType v26, ModuleSubclass[class_exact*:Class@VALUE(0x1038)] IncrCounter inline_cfunc_optimized_send_count - v32:StringExact|NilClass = CCall name@0x1070, v30 + v32:StringExact|NilClass = CCall Module#name@0x1070, v30 CheckInterrupts Return v32 "); diff --git a/zjit/src/state.rs b/zjit/src/state.rs index 06296eb8f20d08..fd59161812a7ee 100644 --- a/zjit/src/state.rs +++ b/zjit/src/state.rs @@ -59,6 +59,9 @@ pub struct ZJITState { /// Counter pointers for un-annotated C functions not_annotated_frame_cfunc_counter_pointers: HashMap>, + /// Counter pointers for all calls to any kind of C function from JIT code + ccall_counter_pointers: HashMap>, + /// Locations of side exists within generated code exit_locations: Option, } @@ -135,6 +138,7 @@ impl ZJITState { exit_trampoline_with_counter: exit_trampoline, full_frame_cfunc_counter_pointers: HashMap::new(), not_annotated_frame_cfunc_counter_pointers: HashMap::new(), + ccall_counter_pointers: HashMap::new(), exit_locations, }; unsafe { ZJIT_STATE = Enabled(zjit_state); } @@ -215,6 +219,11 @@ impl ZJITState { &mut ZJITState::get_instance().not_annotated_frame_cfunc_counter_pointers } + /// Get a mutable reference to ccall counter pointers + pub fn get_ccall_counter_pointers() -> &'static mut HashMap> { + &mut ZJITState::get_instance().ccall_counter_pointers + } + /// Was --zjit-save-compiled-iseqs specified? pub fn should_log_compiled_iseqs() -> bool { get_option!(log_compiled_iseqs).is_some() diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs index b0ca28d258506a..df172997ce9793 100644 --- a/zjit/src/stats.rs +++ b/zjit/src/stats.rs @@ -689,6 +689,13 @@ pub extern "C" fn rb_zjit_stats(_ec: EcPtr, _self: VALUE, target_key: VALUE) -> set_stat_usize!(hash, &key_string, **counter); } + // Set ccall counters + let ccall = ZJITState::get_ccall_counter_pointers(); + for (signature, counter) in ccall.iter() { + let key_string = format!("ccall_{}", signature); + set_stat_usize!(hash, &key_string, **counter); + } + hash } From 4e1f20fee6d97b6dc65e0d4eac1f9cc37312bd5f Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Wed, 19 Nov 2025 13:08:26 -0800 Subject: [PATCH 03/13] [ruby/error_highlight] Fix prism_spot_def_for_name for singletons Previously calling a singleton method with invalid arguments would give: RuntimeError: Incompatible locations This is because `join` wants the operator to come before the location https://github.com/ruby/error_highlight/commit/44920551dd --- lib/error_highlight/base.rb | 2 +- test/error_highlight/test_error_highlight.rb | 56 ++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/error_highlight/base.rb b/lib/error_highlight/base.rb index 22e0bf0dc39738..bc4a62c9d63508 100644 --- a/lib/error_highlight/base.rb +++ b/lib/error_highlight/base.rb @@ -913,7 +913,7 @@ def prism_spot_constant_path_operator_write # ^^^ def prism_spot_def_for_name location = @node.name_loc - location = location.join(@node.operator_loc) if @node.operator_loc + location = @node.operator_loc.join(location) if @node.operator_loc prism_location(location) end diff --git a/test/error_highlight/test_error_highlight.rb b/test/error_highlight/test_error_highlight.rb index 1276a0a0d93a7b..d3ca99021b9ad2 100644 --- a/test/error_highlight/test_error_highlight.rb +++ b/test/error_highlight/test_error_highlight.rb @@ -1733,6 +1733,62 @@ def test_spot_with_node assert_equal expected_spot, actual_spot end + module SingletonMethodWithSpacing + LINENO = __LINE__ + 1 + def self . baz(x:) + x + end + end + + def test_singleton_method_with_spacing_missing_keyword + lineno = __LINE__ + assert_error_message(ArgumentError, <<~END) do +missing keyword: :x (ArgumentError) + + caller: #{ __FILE__ }:#{ lineno + 16 } + | SingletonMethodWithSpacing.baz + ^^^^ + callee: #{ __FILE__ }:#{ SingletonMethodWithSpacing::LINENO } + #{ + MethodDefLocationSupported ? + "| def self . baz(x:) + ^^^^^" : + "(cannot highlight method definition; try Ruby 4.0 or later)" + } + END + + SingletonMethodWithSpacing.baz + end + end + + module SingletonMethodMultipleKwargs + LINENO = __LINE__ + 1 + def self.run(shop_id:, param1:) + shop_id + param1 + end + end + + def test_singleton_method_multiple_missing_keywords + lineno = __LINE__ + assert_error_message(ArgumentError, <<~END) do +missing keywords: :shop_id, :param1 (ArgumentError) + + caller: #{ __FILE__ }:#{ lineno + 16 } + | SingletonMethodMultipleKwargs.run + ^^^^ + callee: #{ __FILE__ }:#{ SingletonMethodMultipleKwargs::LINENO } + #{ + MethodDefLocationSupported ? + "| def self.run(shop_id:, param1:) + ^^^^" : + "(cannot highlight method definition; try Ruby 4.0 or later)" + } + END + + SingletonMethodMultipleKwargs.run + end + end + private def find_node_by_id(node, node_id) From ba2b97a9440d92e78d519fbcbdecc25b72a42705 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 19 Nov 2025 23:37:37 +0100 Subject: [PATCH 04/13] Update to ruby/spec@6e62695 --- spec/ruby/command_line/dash_0_spec.rb | 2 +- spec/ruby/core/enumerable/to_set_spec.rb | 4 ++-- spec/ruby/core/file/stat/birthtime_spec.rb | 2 +- spec/ruby/core/kernel/caller_locations_spec.rb | 4 ++-- spec/ruby/core/kernel/caller_spec.rb | 4 ++-- spec/ruby/core/kernel/inspect_spec.rb | 2 +- spec/ruby/core/kernel/require_spec.rb | 4 ++-- spec/ruby/core/marshal/dump_spec.rb | 6 +++--- spec/ruby/core/method/source_location_spec.rb | 4 ++-- spec/ruby/core/module/ruby2_keywords_spec.rb | 2 +- spec/ruby/core/module/set_temporary_name_spec.rb | 2 +- spec/ruby/core/objectspace/_id2ref_spec.rb | 4 ++-- spec/ruby/core/proc/ruby2_keywords_spec.rb | 2 +- spec/ruby/core/proc/source_location_spec.rb | 8 ++++---- spec/ruby/core/process/status/bit_and_spec.rb | 4 ++-- spec/ruby/core/process/status/right_shift_spec.rb | 4 ++-- spec/ruby/core/range/max_spec.rb | 4 ++-- spec/ruby/core/range/reverse_each_spec.rb | 2 +- spec/ruby/core/set/compare_by_identity_spec.rb | 4 ++-- spec/ruby/core/set/divide_spec.rb | 4 ++-- spec/ruby/core/set/equal_value_spec.rb | 2 +- spec/ruby/core/set/flatten_merge_spec.rb | 2 +- spec/ruby/core/set/flatten_spec.rb | 4 ++-- spec/ruby/core/set/hash_spec.rb | 2 +- spec/ruby/core/set/join_spec.rb | 2 +- spec/ruby/core/set/pretty_print_cycle_spec.rb | 4 ++-- spec/ruby/core/set/proper_subset_spec.rb | 2 +- spec/ruby/core/set/proper_superset_spec.rb | 2 +- spec/ruby/core/set/shared/inspect.rb | 8 ++++---- spec/ruby/core/set/sortedset/sortedset_spec.rb | 2 +- spec/ruby/core/set/subset_spec.rb | 2 +- spec/ruby/core/set/superset_spec.rb | 2 +- spec/ruby/core/unboundmethod/source_location_spec.rb | 4 ++-- spec/ruby/language/numbered_parameters_spec.rb | 4 ++-- spec/ruby/language/predefined_spec.rb | 8 ++++---- spec/ruby/language/regexp_spec.rb | 2 +- spec/ruby/language/send_spec.rb | 2 +- spec/ruby/language/variables_spec.rb | 4 ++-- spec/ruby/library/cgi/cookie/domain_spec.rb | 2 +- spec/ruby/library/cgi/cookie/expires_spec.rb | 2 +- spec/ruby/library/cgi/cookie/initialize_spec.rb | 2 +- spec/ruby/library/cgi/cookie/name_spec.rb | 2 +- spec/ruby/library/cgi/cookie/parse_spec.rb | 2 +- spec/ruby/library/cgi/cookie/path_spec.rb | 2 +- spec/ruby/library/cgi/cookie/secure_spec.rb | 2 +- spec/ruby/library/cgi/cookie/to_s_spec.rb | 2 +- spec/ruby/library/cgi/cookie/value_spec.rb | 2 +- spec/ruby/library/cgi/escapeElement_spec.rb | 4 ++-- spec/ruby/library/cgi/htmlextension/a_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/base_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/blockquote_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/br_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/caption_spec.rb | 2 +- .../ruby/library/cgi/htmlextension/checkbox_group_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/checkbox_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/doctype_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/file_field_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/form_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/frame_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/frameset_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/hidden_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/html_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/image_button_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/img_spec.rb | 2 +- .../ruby/library/cgi/htmlextension/multipart_form_spec.rb | 2 +- .../ruby/library/cgi/htmlextension/password_field_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/popup_menu_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/radio_button_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/radio_group_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/reset_spec.rb | 2 +- .../ruby/library/cgi/htmlextension/scrolling_list_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/submit_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/text_field_spec.rb | 2 +- spec/ruby/library/cgi/htmlextension/textarea_spec.rb | 2 +- spec/ruby/library/cgi/http_header_spec.rb | 2 +- spec/ruby/library/cgi/initialize_spec.rb | 2 +- spec/ruby/library/cgi/out_spec.rb | 2 +- spec/ruby/library/cgi/parse_spec.rb | 2 +- spec/ruby/library/cgi/pretty_spec.rb | 2 +- spec/ruby/library/cgi/print_spec.rb | 2 +- .../library/cgi/queryextension/accept_charset_spec.rb | 2 +- .../library/cgi/queryextension/accept_encoding_spec.rb | 2 +- .../library/cgi/queryextension/accept_language_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/accept_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/auth_type_spec.rb | 2 +- .../ruby/library/cgi/queryextension/cache_control_spec.rb | 2 +- .../library/cgi/queryextension/content_length_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/content_type_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/cookies_spec.rb | 2 +- .../library/cgi/queryextension/element_reference_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/from_spec.rb | 2 +- .../library/cgi/queryextension/gateway_interface_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/has_key_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/host_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/include_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/key_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/keys_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/multipart_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/negotiate_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/params_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/path_info_spec.rb | 2 +- .../library/cgi/queryextension/path_translated_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/pragma_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/query_string_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/raw_cookie2_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/raw_cookie_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/referer_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/remote_addr_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/remote_host_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/remote_ident_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/remote_user_spec.rb | 2 +- .../library/cgi/queryextension/request_method_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/script_name_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/server_name_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/server_port_spec.rb | 2 +- .../library/cgi/queryextension/server_protocol_spec.rb | 2 +- .../library/cgi/queryextension/server_software_spec.rb | 2 +- spec/ruby/library/cgi/queryextension/user_agent_spec.rb | 2 +- spec/ruby/library/cgi/rfc1123_date_spec.rb | 2 +- spec/ruby/library/cgi/unescapeElement_spec.rb | 4 ++-- spec/ruby/library/cgi/unescape_spec.rb | 4 ++-- spec/ruby/library/net-http/http/post_spec.rb | 2 +- .../ruby/library/net-http/httpgenericrequest/exec_spec.rb | 4 ++-- spec/ruby/library/stringscanner/check_spec.rb | 4 ---- spec/ruby/library/stringscanner/check_until_spec.rb | 6 ------ spec/ruby/library/stringscanner/exist_spec.rb | 4 ---- spec/ruby/library/stringscanner/get_byte_spec.rb | 4 ---- spec/ruby/library/stringscanner/getch_spec.rb | 4 ---- spec/ruby/library/stringscanner/scan_byte_spec.rb | 4 ---- spec/ruby/library/stringscanner/scan_integer_spec.rb | 8 +------- spec/ruby/library/stringscanner/scan_until_spec.rb | 6 ------ spec/ruby/library/stringscanner/search_full_spec.rb | 4 ---- spec/ruby/library/stringscanner/skip_until_spec.rb | 6 ------ spec/ruby/optional/capi/ext/rubyspec.h | 4 ++-- spec/ruby/optional/capi/ext/set_spec.c | 2 +- spec/ruby/optional/capi/ext/thread_spec.c | 4 ++-- spec/ruby/optional/capi/set_spec.rb | 2 +- spec/ruby/optional/capi/string_spec.rb | 6 +++--- spec/ruby/optional/capi/thread_spec.rb | 2 +- spec/ruby/security/cve_2020_10663_spec.rb | 2 +- spec/ruby/shared/kernel/raise.rb | 4 ++-- 141 files changed, 168 insertions(+), 216 deletions(-) diff --git a/spec/ruby/command_line/dash_0_spec.rb b/spec/ruby/command_line/dash_0_spec.rb index 73c5e29004eb42..2ce4f49b5e4d83 100755 --- a/spec/ruby/command_line/dash_0_spec.rb +++ b/spec/ruby/command_line/dash_0_spec.rb @@ -5,7 +5,7 @@ ruby_exe("puts $/, $-0", options: "-072").should == ":\n:\n" end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "sets $/ and $-0 as a frozen string" do ruby_exe("puts $/.frozen?, $-0.frozen?", options: "-072").should == "true\ntrue\n" end diff --git a/spec/ruby/core/enumerable/to_set_spec.rb b/spec/ruby/core/enumerable/to_set_spec.rb index e0437fea613bd9..e1fcd3a20d0273 100644 --- a/spec/ruby/core/enumerable/to_set_spec.rb +++ b/spec/ruby/core/enumerable/to_set_spec.rb @@ -11,7 +11,7 @@ [1, 2, 3].to_set { |x| x * x }.should == Set[1, 4, 9] end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "instantiates an object of provided as the first argument set class" do set = nil proc{set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass)}.should complain(/Enumerable#to_set/) @@ -20,7 +20,7 @@ end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "instantiates an object of provided as the first argument set class" do set = [1, 2, 3].to_set(EnumerableSpecs::SetSubclass) set.should be_kind_of(EnumerableSpecs::SetSubclass) diff --git a/spec/ruby/core/file/stat/birthtime_spec.rb b/spec/ruby/core/file/stat/birthtime_spec.rb index adecee15b0ec86..9aa39297b24d3c 100644 --- a/spec/ruby/core/file/stat/birthtime_spec.rb +++ b/spec/ruby/core/file/stat/birthtime_spec.rb @@ -1,7 +1,7 @@ require_relative '../../../spec_helper' platform_is(:windows, :darwin, :freebsd, :netbsd, - *ruby_version_is("3.5") { :linux }, + *ruby_version_is("4.0") { :linux }, ) do not_implemented_messages = [ "birthtime() function is unimplemented", # unsupported OS/version diff --git a/spec/ruby/core/kernel/caller_locations_spec.rb b/spec/ruby/core/kernel/caller_locations_spec.rb index 6074879d594f8f..a917dba504a931 100644 --- a/spec/ruby/core/kernel/caller_locations_spec.rb +++ b/spec/ruby/core/kernel/caller_locations_spec.rb @@ -83,7 +83,7 @@ end end - ruby_version_is "3.4"..."3.5" do + ruby_version_is "3.4"..."4.0" do it "includes core library methods defined in Ruby" do file, line = Kernel.instance_method(:tap).source_location file.should.start_with?(' { Kernel.instance_method(:tap).source_location } do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "includes core library methods defined in Ruby" do file, line = Kernel.instance_method(:tap).source_location file.should.start_with?(' *a, b { } diff --git a/spec/ruby/core/proc/source_location_spec.rb b/spec/ruby/core/proc/source_location_spec.rb index 484466f5774771..69b4e2fd8273b6 100644 --- a/spec/ruby/core/proc/source_location_spec.rb +++ b/spec/ruby/core/proc/source_location_spec.rb @@ -53,12 +53,12 @@ end it "works even if the proc was created on the same line" do - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do proc { true }.source_location.should == [__FILE__, __LINE__] Proc.new { true }.source_location.should == [__FILE__, __LINE__] -> { true }.source_location.should == [__FILE__, __LINE__] end - ruby_version_is("3.5") do + ruby_version_is("4.0") do proc { true }.source_location.should == [__FILE__, __LINE__, 11, __LINE__, 19] Proc.new { true }.source_location.should == [__FILE__, __LINE__, 15, __LINE__, 23] -> { true }.source_location.should == [__FILE__, __LINE__, 8, __LINE__, 17] @@ -94,10 +94,10 @@ it "works for eval with a given line" do proc = eval('-> {}', nil, "foo", 100) location = proc.source_location - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("3.5") do + ruby_version_is("4.0") do location.should == ["foo", 100, 2, 100, 5] end end diff --git a/spec/ruby/core/process/status/bit_and_spec.rb b/spec/ruby/core/process/status/bit_and_spec.rb index 0e0edb0afa3958..a80536462947f2 100644 --- a/spec/ruby/core/process/status/bit_and_spec.rb +++ b/spec/ruby/core/process/status/bit_and_spec.rb @@ -1,6 +1,6 @@ require_relative '../../../spec_helper' -ruby_version_is ""..."3.5" do +ruby_version_is ""..."4.0" do describe "Process::Status#&" do it "returns a bitwise and of the integer status of an exited child" do @@ -17,7 +17,7 @@ end end - ruby_version_is "3.3"..."3.5" do + ruby_version_is "3.3"..."4.0" do it "raises an ArgumentError if mask is negative" do suppress_warning do ruby_exe("exit(0)") diff --git a/spec/ruby/core/process/status/right_shift_spec.rb b/spec/ruby/core/process/status/right_shift_spec.rb index a1ab75141a3822..355aaf4c9532cb 100644 --- a/spec/ruby/core/process/status/right_shift_spec.rb +++ b/spec/ruby/core/process/status/right_shift_spec.rb @@ -1,6 +1,6 @@ require_relative '../../../spec_helper' -ruby_version_is ""..."3.5" do +ruby_version_is ""..."4.0" do describe "Process::Status#>>" do it "returns a right shift of the integer status of an exited child" do @@ -16,7 +16,7 @@ end end - ruby_version_is "3.3"..."3.5" do + ruby_version_is "3.3"..."4.0" do it "raises an ArgumentError if shift value is negative" do suppress_warning do ruby_exe("exit(0)") diff --git a/spec/ruby/core/range/max_spec.rb b/spec/ruby/core/range/max_spec.rb index 8b83f69a5a2121..09371f52987862 100644 --- a/spec/ruby/core/range/max_spec.rb +++ b/spec/ruby/core/range/max_spec.rb @@ -55,7 +55,7 @@ (..1.0).max.should == 1.0 end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "raises for an exclusive beginless Integer range" do -> { (...1).max @@ -63,7 +63,7 @@ end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "returns the end point for exclusive beginless Integer ranges" do (...1).max.should == 0 end diff --git a/spec/ruby/core/range/reverse_each_spec.rb b/spec/ruby/core/range/reverse_each_spec.rb index b51e04c3fff24e..56390cc0da4822 100644 --- a/spec/ruby/core/range/reverse_each_spec.rb +++ b/spec/ruby/core/range/reverse_each_spec.rb @@ -88,7 +88,7 @@ (1..3).reverse_each.size.should == 3 end - ruby_bug "#20936", "3.4"..."3.5" do + ruby_bug "#20936", "3.4"..."4.0" do it "returns Infinity when Range size is infinite" do (..3).reverse_each.size.should == Float::INFINITY end diff --git a/spec/ruby/core/set/compare_by_identity_spec.rb b/spec/ruby/core/set/compare_by_identity_spec.rb index 0dda6d79f09177..238dc117a6ccfa 100644 --- a/spec/ruby/core/set/compare_by_identity_spec.rb +++ b/spec/ruby/core/set/compare_by_identity_spec.rb @@ -90,7 +90,7 @@ def o.hash; 123; end set.to_a.sort.should == [a1, a2].sort end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "raises a FrozenError on frozen sets" do set = Set.new.freeze -> { @@ -99,7 +99,7 @@ def o.hash; 123; end end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "raises a FrozenError on frozen sets" do set = Set.new.freeze -> { diff --git a/spec/ruby/core/set/divide_spec.rb b/spec/ruby/core/set/divide_spec.rb index cbe0042f16e6ae..c6c6003e99d8b6 100644 --- a/spec/ruby/core/set/divide_spec.rb +++ b/spec/ruby/core/set/divide_spec.rb @@ -25,7 +25,7 @@ set.map{ |x| x.to_a.sort }.sort.should == [[1], [3, 4], [6], [9, 10, 11]] end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "yields each two Object to the block" do ret = [] Set[1, 2].divide { |x, y| ret << [x, y] } @@ -33,7 +33,7 @@ end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "yields each two Object to the block" do ret = [] Set[1, 2].divide { |x, y| ret << [x, y] } diff --git a/spec/ruby/core/set/equal_value_spec.rb b/spec/ruby/core/set/equal_value_spec.rb index e3514928c816d3..721a79a3f1370b 100644 --- a/spec/ruby/core/set/equal_value_spec.rb +++ b/spec/ruby/core/set/equal_value_spec.rb @@ -24,7 +24,7 @@ set1.should == set2 end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true when a Set and a Set-like object contain the same elements" do Set[1, 2, 3].should == SetSpecs::SetLike.new([1, 2, 3]) diff --git a/spec/ruby/core/set/flatten_merge_spec.rb b/spec/ruby/core/set/flatten_merge_spec.rb index d7c2b306579443..13cedeead953de 100644 --- a/spec/ruby/core/set/flatten_merge_spec.rb +++ b/spec/ruby/core/set/flatten_merge_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' describe "Set#flatten_merge" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "is protected" do Set.should have_protected_instance_method("flatten_merge") end diff --git a/spec/ruby/core/set/flatten_spec.rb b/spec/ruby/core/set/flatten_spec.rb index 870eccc2f10c99..f2cb3dfa524a35 100644 --- a/spec/ruby/core/set/flatten_spec.rb +++ b/spec/ruby/core/set/flatten_spec.rb @@ -16,7 +16,7 @@ -> { set.flatten }.should raise_error(ArgumentError) end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when Set contains a Set-like object" do it "returns a copy of self with each included Set-like object flattened" do Set[SetSpecs::SetLike.new([1])].flatten.should == Set[1] @@ -48,7 +48,7 @@ end version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when Set contains a Set-like object" do it "flattens self, including Set-like objects" do Set[SetSpecs::SetLike.new([1])].flatten!.should == Set[1] diff --git a/spec/ruby/core/set/hash_spec.rb b/spec/ruby/core/set/hash_spec.rb index 4b4696e34ccbf3..63a0aa66a55ef9 100644 --- a/spec/ruby/core/set/hash_spec.rb +++ b/spec/ruby/core/set/hash_spec.rb @@ -10,7 +10,7 @@ Set[1, 2, 3].hash.should_not == Set[:a, "b", ?c].hash end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do # see https://github.com/jruby/jruby/issues/8393 it "is equal to nil.hash for an uninitialized Set" do Set.allocate.hash.should == nil.hash diff --git a/spec/ruby/core/set/join_spec.rb b/spec/ruby/core/set/join_spec.rb index cdb593597d8641..1c1e8a8af8457d 100644 --- a/spec/ruby/core/set/join_spec.rb +++ b/spec/ruby/core/set/join_spec.rb @@ -20,7 +20,7 @@ set.join(' | ').should == "a | b | c" end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "calls #to_a to convert the Set in to an Array" do set = Set[:a, :b, :c] set.should_receive(:to_a).and_return([:a, :b, :c]) diff --git a/spec/ruby/core/set/pretty_print_cycle_spec.rb b/spec/ruby/core/set/pretty_print_cycle_spec.rb index d4cca515e2a2d3..7e6017c112b77e 100644 --- a/spec/ruby/core/set/pretty_print_cycle_spec.rb +++ b/spec/ruby/core/set/pretty_print_cycle_spec.rb @@ -3,10 +3,10 @@ describe "Set#pretty_print_cycle" do it "passes the 'pretty print' representation of a self-referencing Set to the pretty print writer" do pp = mock("PrettyPrint") - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do pp.should_receive(:text).with("#") end - ruby_version_is("3.5") do + ruby_version_is("4.0") do pp.should_receive(:text).with("Set[...]") end Set[1, 2, 3].pretty_print_cycle(pp) diff --git a/spec/ruby/core/set/proper_subset_spec.rb b/spec/ruby/core/set/proper_subset_spec.rb index a84c4197c23dd6..fb7848c0015200 100644 --- a/spec/ruby/core/set/proper_subset_spec.rb +++ b/spec/ruby/core/set/proper_subset_spec.rb @@ -34,7 +34,7 @@ end version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a proper subset of" do Set[1, 2, 3].proper_subset?(SetSpecs::SetLike.new([1, 2, 3, 4])).should be_true diff --git a/spec/ruby/core/set/proper_superset_spec.rb b/spec/ruby/core/set/proper_superset_spec.rb index 653411f6b23452..dc1e87e2308e67 100644 --- a/spec/ruby/core/set/proper_superset_spec.rb +++ b/spec/ruby/core/set/proper_superset_spec.rb @@ -32,7 +32,7 @@ -> { Set[].proper_superset?(Object.new) }.should raise_error(ArgumentError) end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a proper superset of" do Set[1, 2, 3, 4].proper_superset?(SetSpecs::SetLike.new([1, 2, 3])).should be_true diff --git a/spec/ruby/core/set/shared/inspect.rb b/spec/ruby/core/set/shared/inspect.rb index fbc7486acd61d4..a90af66c980dbf 100644 --- a/spec/ruby/core/set/shared/inspect.rb +++ b/spec/ruby/core/set/shared/inspect.rb @@ -7,13 +7,13 @@ Set[:a, "b", Set[?c]].send(@method).should be_kind_of(String) end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "does include the elements of the set" do Set["1"].send(@method).should == 'Set["1"]' end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "does include the elements of the set" do Set["1"].send(@method).should == '#' end @@ -23,7 +23,7 @@ Set["1", "2"].send(@method).should include('", "') end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "correctly handles cyclic-references" do set1 = Set[] set2 = Set[set1] @@ -33,7 +33,7 @@ end end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "correctly handles cyclic-references" do set1 = Set[] set2 = Set[set1] diff --git a/spec/ruby/core/set/sortedset/sortedset_spec.rb b/spec/ruby/core/set/sortedset/sortedset_spec.rb index 41f010e011c8de..f3c1ec058d80ac 100644 --- a/spec/ruby/core/set/sortedset/sortedset_spec.rb +++ b/spec/ruby/core/set/sortedset/sortedset_spec.rb @@ -1,7 +1,7 @@ require_relative '../../../spec_helper' describe "SortedSet" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "raises error including message that it has been extracted from the set stdlib" do -> { SortedSet diff --git a/spec/ruby/core/set/subset_spec.rb b/spec/ruby/core/set/subset_spec.rb index cde61d7cd7745f..112bd9b38adc12 100644 --- a/spec/ruby/core/set/subset_spec.rb +++ b/spec/ruby/core/set/subset_spec.rb @@ -34,7 +34,7 @@ end version_is(set_version, ""..."1.1.0") do #ruby_version_is ""..."3.3" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a subset of" do Set[1, 2, 3].subset?(SetSpecs::SetLike.new([1, 2, 3, 4])).should be_true diff --git a/spec/ruby/core/set/superset_spec.rb b/spec/ruby/core/set/superset_spec.rb index 9d7bab964a235c..9b3df2d047d4c0 100644 --- a/spec/ruby/core/set/superset_spec.rb +++ b/spec/ruby/core/set/superset_spec.rb @@ -32,7 +32,7 @@ -> { Set[].superset?(Object.new) }.should raise_error(ArgumentError) end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do context "when comparing to a Set-like object" do it "returns true if passed a Set-like object that self is a superset of" do Set[1, 2, 3, 4].superset?(SetSpecs::SetLike.new([1, 2, 3])).should be_true diff --git a/spec/ruby/core/unboundmethod/source_location_spec.rb b/spec/ruby/core/unboundmethod/source_location_spec.rb index 2391d07d9958ef..85078ff34e8cd5 100644 --- a/spec/ruby/core/unboundmethod/source_location_spec.rb +++ b/spec/ruby/core/unboundmethod/source_location_spec.rb @@ -55,10 +55,10 @@ eval('def m; end', nil, "foo", 100) end location = c.instance_method(:m).source_location - ruby_version_is(""..."3.5") do + ruby_version_is(""..."4.0") do location.should == ["foo", 100] end - ruby_version_is("3.5") do + ruby_version_is("4.0") do location.should == ["foo", 100, 0, 100, 10] end end diff --git a/spec/ruby/language/numbered_parameters_spec.rb b/spec/ruby/language/numbered_parameters_spec.rb index 39ddd6fee83e19..de532c326d4cd1 100644 --- a/spec/ruby/language/numbered_parameters_spec.rb +++ b/spec/ruby/language/numbered_parameters_spec.rb @@ -90,14 +90,14 @@ proc { _2 }.parameters.should == [[:opt, :_1], [:opt, :_2]] end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "affects binding local variables" do -> { _1; binding.local_variables }.call("a").should == [:_1] -> { _2; binding.local_variables }.call("a", "b").should == [:_1, :_2] end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "does not affect binding local variables" do -> { _1; binding.local_variables }.call("a").should == [] -> { _2; binding.local_variables }.call("a", "b").should == [] diff --git a/spec/ruby/language/predefined_spec.rb b/spec/ruby/language/predefined_spec.rb index d90e19858ae5e9..f2488615aaec37 100644 --- a/spec/ruby/language/predefined_spec.rb +++ b/spec/ruby/language/predefined_spec.rb @@ -687,7 +687,7 @@ def foo $VERBOSE = @verbose end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "can be assigned a String" do str = +"abc" $/ = str @@ -695,7 +695,7 @@ def foo end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "makes a new frozen String from the assigned String" do string_subclass = Class.new(String) str = string_subclass.new("abc") @@ -763,7 +763,7 @@ def foo $VERBOSE = @verbose end - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "can be assigned a String" do str = +"abc" $-0 = str @@ -771,7 +771,7 @@ def foo end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "makes a new frozen String from the assigned String" do string_subclass = Class.new(String) str = string_subclass.new("abc") diff --git a/spec/ruby/language/regexp_spec.rb b/spec/ruby/language/regexp_spec.rb index dbf341b19ea526..ce344b5b05f067 100644 --- a/spec/ruby/language/regexp_spec.rb +++ b/spec/ruby/language/regexp_spec.rb @@ -112,7 +112,7 @@ /foo.(?<=\d)/.match("fooA foo1").to_a.should == ["foo1"] end - ruby_bug "#13671", ""..."3.5" do # https://bugs.ruby-lang.org/issues/13671 + ruby_bug "#13671", ""..."4.0" do # https://bugs.ruby-lang.org/issues/13671 it "handles a lookbehind with ss characters" do r = Regexp.new("(? "application/x-www-form-urlencoded" }.inspect.delete("{}")) diff --git a/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb b/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb index 0912e5a71f0e71..a09f9d5becec42 100644 --- a/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb +++ b/spec/ruby/library/net-http/httpgenericrequest/exec_spec.rb @@ -31,7 +31,7 @@ end describe "when a request body is set" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "sets the 'Content-Type' header to 'application/x-www-form-urlencoded' unless the 'Content-Type' header is supplied" do request = Net::HTTPGenericRequest.new("POST", true, true, "/some/path") request.body = "Some Content" @@ -64,7 +64,7 @@ end describe "when a body stream is set" do - ruby_version_is ""..."3.5" do + ruby_version_is ""..."4.0" do it "sets the 'Content-Type' header to 'application/x-www-form-urlencoded' unless the 'Content-Type' header is supplied" do request = Net::HTTPGenericRequest.new("POST", true, true, "/some/path", "Content-Length" => "10") diff --git a/spec/ruby/library/stringscanner/check_spec.rb b/spec/ruby/library/stringscanner/check_spec.rb index 235f2f22e954fd..5e855e154ad4de 100644 --- a/spec/ruby/library/stringscanner/check_spec.rb +++ b/spec/ruby/library/stringscanner/check_spec.rb @@ -39,7 +39,6 @@ context "when #check was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.check("This") @@ -47,7 +46,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "raises IndexError when matching succeeded" do @s.check("This") @@ -68,7 +66,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -80,7 +77,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/check_until_spec.rb b/spec/ruby/library/stringscanner/check_until_spec.rb index 701a703ebe8352..582da66b375a9f 100644 --- a/spec/ruby/library/stringscanner/check_until_spec.rb +++ b/spec/ruby/library/stringscanner/check_until_spec.rb @@ -35,7 +35,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.check_until("a") @@ -45,7 +44,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -76,7 +74,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #check_until was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.check_until("This") @@ -84,7 +81,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.check_until("This") @@ -105,7 +101,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -117,7 +112,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/exist_spec.rb b/spec/ruby/library/stringscanner/exist_spec.rb index 3f40c7a5a5a763..a408fd0b8dc1c7 100644 --- a/spec/ruby/library/stringscanner/exist_spec.rb +++ b/spec/ruby/library/stringscanner/exist_spec.rb @@ -64,7 +64,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #exist? was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.exist?("This") @@ -72,7 +71,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.exist?("This") @@ -93,7 +91,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -105,7 +102,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/get_byte_spec.rb b/spec/ruby/library/stringscanner/get_byte_spec.rb index b3c2b7f678edd6..144859abc92a8c 100644 --- a/spec/ruby/library/stringscanner/get_byte_spec.rb +++ b/spec/ruby/library/stringscanner/get_byte_spec.rb @@ -32,7 +32,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil" do s = StringScanner.new("This is a test") @@ -41,7 +40,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError" do s = StringScanner.new("This is a test") @@ -58,7 +56,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") @@ -71,7 +68,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") diff --git a/spec/ruby/library/stringscanner/getch_spec.rb b/spec/ruby/library/stringscanner/getch_spec.rb index c9c3eb6fd3ecce..d369391b140ce8 100644 --- a/spec/ruby/library/stringscanner/getch_spec.rb +++ b/spec/ruby/library/stringscanner/getch_spec.rb @@ -33,7 +33,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil" do s = StringScanner.new("This is a test") @@ -42,7 +41,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError" do s = StringScanner.new("This is a test") @@ -59,7 +57,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") @@ -73,7 +70,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("This is a test") diff --git a/spec/ruby/library/stringscanner/scan_byte_spec.rb b/spec/ruby/library/stringscanner/scan_byte_spec.rb index c60e22be4f508c..aa2decc8f747ba 100644 --- a/spec/ruby/library/stringscanner/scan_byte_spec.rb +++ b/spec/ruby/library/stringscanner/scan_byte_spec.rb @@ -43,7 +43,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil" do s = StringScanner.new("abc") @@ -52,7 +51,6 @@ s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError" do s = StringScanner.new("abc") @@ -69,7 +67,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("abc") @@ -83,7 +80,6 @@ s[:a].should == nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do s = StringScanner.new("abc") diff --git a/spec/ruby/library/stringscanner/scan_integer_spec.rb b/spec/ruby/library/stringscanner/scan_integer_spec.rb index a0b3685bae1086..fe0d26f4049076 100644 --- a/spec/ruby/library/stringscanner/scan_integer_spec.rb +++ b/spec/ruby/library/stringscanner/scan_integer_spec.rb @@ -25,7 +25,7 @@ end # https://github.com/ruby/strscan/issues/130 - ruby_bug "", "3.4"..."3.5" do # introduced in strscan v3.1.1 + ruby_bug "", "3.4"..."4.0" do # introduced in strscan v3.1.1 it "sets the last match result" do s = StringScanner.new("42abc") s.scan_integer @@ -68,7 +68,6 @@ }.should raise_error(ArgumentError, "Unsupported integer base: 5, expected 10 or 16") end - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "does not match '0x' prefix on its own" do StringScanner.new("0x").scan_integer(base: 16).should == nil @@ -76,7 +75,6 @@ StringScanner.new("+0x").scan_integer(base: 16).should == nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "matches '0' in a '0x' that is followed by non-hex characters" do @@ -96,7 +94,6 @@ describe "#[] successive call with a capture group name" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil substring when matching succeeded" do s = StringScanner.new("42") @@ -105,7 +102,6 @@ s[:a].should == nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do s = StringScanner.new("42") @@ -131,7 +127,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "does not ignore the previous matching with Regexp" do s = StringScanner.new("42") @@ -145,7 +140,6 @@ s[:a].should == "42" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "ignores the previous matching with Regexp" do s = StringScanner.new("42") diff --git a/spec/ruby/library/stringscanner/scan_until_spec.rb b/spec/ruby/library/stringscanner/scan_until_spec.rb index 737d83a14ca32d..610060d6f1ee25 100644 --- a/spec/ruby/library/stringscanner/scan_until_spec.rb +++ b/spec/ruby/library/stringscanner/scan_until_spec.rb @@ -41,7 +41,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.scan_until("a") @@ -51,7 +50,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -82,7 +80,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #scan_until was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.scan_until("This") @@ -90,7 +87,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.scan_until("This") @@ -111,7 +107,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -123,7 +118,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/library/stringscanner/search_full_spec.rb b/spec/ruby/library/stringscanner/search_full_spec.rb index a089da2043b1ea..197adfda4d4519 100644 --- a/spec/ruby/library/stringscanner/search_full_spec.rb +++ b/spec/ruby/library/stringscanner/search_full_spec.rb @@ -50,7 +50,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.search_full("is a", false, false) @@ -60,7 +59,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -91,7 +89,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #search_full was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.search_full("This", false, false) @@ -99,7 +96,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.search_full("This", false, false) diff --git a/spec/ruby/library/stringscanner/skip_until_spec.rb b/spec/ruby/library/stringscanner/skip_until_spec.rb index f5be4b5ceb0a15..5d73d8f0b91104 100644 --- a/spec/ruby/library/stringscanner/skip_until_spec.rb +++ b/spec/ruby/library/stringscanner/skip_until_spec.rb @@ -38,7 +38,6 @@ end # https://github.com/ruby/strscan/issues/131 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.1" it "sets the last match result if given a String" do @s.skip_until("a") @@ -48,7 +47,6 @@ @s.post_match.should == " test" end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "sets the last match result if given a String" do @@ -79,7 +77,6 @@ version_is StringScanner::Version, "3.1.1" do # ruby_version_is "3.4" context "when #skip_until was called with a String pattern" do # https://github.com/ruby/strscan/issues/139 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "returns nil when matching succeeded" do @s.skip_until("This") @@ -87,7 +84,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4.3" it "raises IndexError when matching succeeded" do @s.skip_until("This") @@ -108,7 +104,6 @@ end # https://github.com/ruby/strscan/issues/135 - ruby_version_is ""..."3.5" do # Don't run on 3.5.0dev that already contains not released fixes version_is StringScanner::Version, "3.1.1"..."3.1.3" do # ruby_version_is "3.4.0"..."3.4.3" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) @@ -120,7 +115,6 @@ @s[:a].should be_nil end end - end version_is StringScanner::Version, "3.1.3" do # ruby_version_is "3.4" it "ignores the previous matching with Regexp" do @s.exist?(/(?This)/) diff --git a/spec/ruby/optional/capi/ext/rubyspec.h b/spec/ruby/optional/capi/ext/rubyspec.h index 8aaec36f465818..6c4bea5da0e124 100644 --- a/spec/ruby/optional/capi/ext/rubyspec.h +++ b/spec/ruby/optional/capi/ext/rubyspec.h @@ -35,8 +35,8 @@ (RUBY_API_VERSION_MAJOR == (major) && RUBY_API_VERSION_MINOR < (minor))) #define RUBY_VERSION_SINCE(major,minor) (!RUBY_VERSION_BEFORE(major, minor)) -#if RUBY_VERSION_SINCE(3, 5) -#define RUBY_VERSION_IS_3_5 +#if RUBY_VERSION_SINCE(4, 0) +#define RUBY_VERSION_IS_4_0 #endif #if RUBY_VERSION_SINCE(3, 4) diff --git a/spec/ruby/optional/capi/ext/set_spec.c b/spec/ruby/optional/capi/ext/set_spec.c index 7af922fd49ea96..11a271b361ba6b 100644 --- a/spec/ruby/optional/capi/ext/set_spec.c +++ b/spec/ruby/optional/capi/ext/set_spec.c @@ -1,7 +1,7 @@ #include "ruby.h" #include "rubyspec.h" -#ifdef RUBY_VERSION_IS_3_5 +#ifdef RUBY_VERSION_IS_4_0 #ifdef __cplusplus extern "C" { #endif diff --git a/spec/ruby/optional/capi/ext/thread_spec.c b/spec/ruby/optional/capi/ext/thread_spec.c index 6ee111b7b7ea72..ac77e4e813b517 100644 --- a/spec/ruby/optional/capi/ext/thread_spec.c +++ b/spec/ruby/optional/capi/ext/thread_spec.c @@ -166,7 +166,7 @@ static VALUE thread_spec_ruby_native_thread_p_new_thread(VALUE self) { #endif } -#ifdef RUBY_VERSION_IS_3_5 +#ifdef RUBY_VERSION_IS_4_0 static VALUE thread_spec_ruby_thread_has_gvl_p(VALUE self) { return ruby_thread_has_gvl_p() ? Qtrue : Qfalse; } @@ -185,7 +185,7 @@ void Init_thread_spec(void) { rb_define_method(cls, "rb_thread_create", thread_spec_rb_thread_create, 2); rb_define_method(cls, "ruby_native_thread_p", thread_spec_ruby_native_thread_p, 0); rb_define_method(cls, "ruby_native_thread_p_new_thread", thread_spec_ruby_native_thread_p_new_thread, 0); -#ifdef RUBY_VERSION_IS_3_5 +#ifdef RUBY_VERSION_IS_4_0 rb_define_method(cls, "ruby_thread_has_gvl_p", thread_spec_ruby_thread_has_gvl_p, 0); #endif } diff --git a/spec/ruby/optional/capi/set_spec.rb b/spec/ruby/optional/capi/set_spec.rb index 3b7ee812c56ade..3e35be0505fffa 100644 --- a/spec/ruby/optional/capi/set_spec.rb +++ b/spec/ruby/optional/capi/set_spec.rb @@ -1,6 +1,6 @@ require_relative 'spec_helper' -ruby_version_is "3.5" do +ruby_version_is "4.0" do load_extension("set") describe "C-API Set function" do diff --git a/spec/ruby/optional/capi/string_spec.rb b/spec/ruby/optional/capi/string_spec.rb index 605c43769ddb0b..72f20ee6a52455 100644 --- a/spec/ruby/optional/capi/string_spec.rb +++ b/spec/ruby/optional/capi/string_spec.rb @@ -193,7 +193,7 @@ def inspect it "returns a new String object filled with \\0 bytes" do lens = [4] - ruby_version_is "3.5" do + ruby_version_is "4.0" do lens << 100 end @@ -1230,7 +1230,7 @@ def inspect -> { str.upcase! }.should raise_error(RuntimeError, 'can\'t modify string; temporarily locked') end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "raises FrozenError if string is frozen" do str = -"rb_str_locktmp" -> { @s.rb_str_locktmp(str) }.should raise_error(FrozenError) @@ -1254,7 +1254,7 @@ def inspect -> { @s.rb_str_unlocktmp(+"test") }.should raise_error(RuntimeError, 'temporal unlocking already unlocked string') end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "raises FrozenError if string is frozen" do str = -"rb_str_locktmp" -> { @s.rb_str_unlocktmp(str) }.should raise_error(FrozenError) diff --git a/spec/ruby/optional/capi/thread_spec.rb b/spec/ruby/optional/capi/thread_spec.rb index cd9ae8ff1923bb..117726f0e2a392 100644 --- a/spec/ruby/optional/capi/thread_spec.rb +++ b/spec/ruby/optional/capi/thread_spec.rb @@ -185,7 +185,7 @@ def call_capi_rb_thread_wakeup end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do describe "ruby_thread_has_gvl_p" do it "returns true if the current thread has the GVL" do @t.ruby_thread_has_gvl_p.should be_true diff --git a/spec/ruby/security/cve_2020_10663_spec.rb b/spec/ruby/security/cve_2020_10663_spec.rb index 80e860348b10ad..c44a13a0dd4b5d 100644 --- a/spec/ruby/security/cve_2020_10663_spec.rb +++ b/spec/ruby/security/cve_2020_10663_spec.rb @@ -1,6 +1,6 @@ require_relative '../spec_helper' -ruby_version_is ""..."3.5" do +ruby_version_is ""..."4.0" do require 'json' module JSONSpecs diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index 8432c835946d6e..2be06ea797aa6d 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -141,7 +141,7 @@ def e.exception end end - ruby_version_is "3.5" do + ruby_version_is "4.0" do it "allows cause keyword argument" do cause = StandardError.new("original error") result = nil @@ -272,7 +272,7 @@ def e.exception end describe :kernel_raise_across_contexts, shared: true do - ruby_version_is "3.5" do + ruby_version_is "4.0" do describe "with cause keyword argument" do it "uses the cause from the calling context" do original_cause = nil From d487e396bdeb33d0069bab2475103aa9b7109607 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 18 Nov 2025 18:47:31 -0500 Subject: [PATCH 05/13] ZJIT: [DOC] Comment copy-editing --- zjit/src/hir.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 982400db5030cd..8e6241f0dafe0f 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -1485,8 +1485,7 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq return false; } - // Check argument count against callee's parameters. Note that correctness for this calculation - // relies on rejecting features above. + // Because we exclude e.g. post parameters above, they are also excluded from the sum below. let lead_num = unsafe { get_iseq_body_param_lead_num(iseq) }; let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) }; can_send = c_int::try_from(args.len()) @@ -4545,8 +4544,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { let jit_entry_insns = jit_entry_insns(iseq); let BytecodeInfo { jump_targets, has_blockiseq } = compute_bytecode_info(iseq, &jit_entry_insns); - // Make all empty basic blocks. The ordering of the BBs matters as it is taken as a schedule - // in the backend without a scheduling pass. TODO: Higher quality scheduling during lowering. + // Make all empty basic blocks. The ordering of the BBs matters for getting fallthrough jumps + // in good places, but it's not necessary for correctness. TODO: Higher quality scheduling during lowering. let mut insn_idx_to_block = HashMap::new(); // Make blocks for optionals first, and put them right next to their JIT entrypoint for insn_idx in jit_entry_insns.iter().copied() { From 63a6290ce0bf1a7145c545632b22a5dfa170ea6a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 19 Nov 2025 22:03:11 +0000 Subject: [PATCH 06/13] [DOC] Update yjit.md to use a different email --- doc/yjit/yjit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/yjit/yjit.md b/doc/yjit/yjit.md index 73db7237109322..24aa163e60d299 100644 --- a/doc/yjit/yjit.md +++ b/doc/yjit/yjit.md @@ -14,7 +14,7 @@ This project is open source and falls under the same license as CRuby.

If you're using YJIT in production, please - share your success stories with us! + share your success stories with us!

If you wish to learn more about the approach taken, here are some conference talks and publications: From 2ed287da177ea792e0673eaf7764cc7ca1fca8a1 Mon Sep 17 00:00:00 2001 From: Aiden Fox Ivey Date: Wed, 19 Nov 2025 18:00:44 -0500 Subject: [PATCH 07/13] ZJIT: Add Iongraph compatibility (#14999) ## Components This PR adds functionality to visualize HIR using the [Iongraph](https://spidermonkey.dev/blog/2025/10/28/iongraph-web.html) tool first created for use with Spidermonkey. ## Justification Iongraph's viewer is (as mentioned in the article above) a few notches above graphviz for viewing large CFGs. It also allows easily inspecting different compiler optimization passes and multiple functions in the same browser window. Since Spidermonkey is using this format, it may be beneficial to use it for our own JIT development. The requirement for JSON is downstream from that of the Iongraph format. As for writing the implementation myself, ZJIT leans towards having fewer dependencies, so this is the preferred approach. ## How does it look? image image Right now, it's aesthetically minimal, but is fairly robust. ## Functionality Using `--zjit-dump-hir-iongraph` will dump all compiled functions into a directory named `/tmp/zjit-iongraph-{PROCESS_PID}`. Each file will be named `func_{ZJIT_FUNC_NAME}.json`. In order to use them in the Iongraph viewer, you'll need to use `jq` to collate them to a single file. An example invocation of `jq` is shown below for reference. The name of the file created does not matter to my understanding. `jq --slurp --null-input '.functions=inputs | .version=2' /tmp/zjit-iongraph-{PROCESS_PID}/func*.json > ~/Downloads/foo.json` From there, you can use https://mozilla-spidermonkey.github.io/iongraph/ to view your trace. ### Caveats - The upstream Iongraph viewer doesn't allow you to click arguments to an instruction to find the instruction that they originate from when using the format that this PR generates. (I have made a small fork at https://github.com/aidenfoxivey/iongraph that fixes that functionality via https://github.com/aidenfoxivey/iongraph/commit/9e9c29b41c4dbb35cf66cb6161e5b19c8b796379.patch) - The upstream Iongraph viewer can sometimes show "exiting edges" in the CFG as being not attached to the box representing its basic block. image (Image courtesy of @tekknolagi) This is because the original tool was (to our understanding) written for an SSA format that does not use extended basic blocks. (Extended basic blocks let you put a jump instruction, conditional or otherwise, anywhere in the basic block.) This means that our format may generate more outgoing edges than the viewer is written to handle. --- doc/zjit.md | 8 + zjit/src/hir.rs | 466 +++++++++++++++++++++++- zjit/src/hir/tests.rs | 818 ++++++++++++++++++++++++++++++++++++++++++ zjit/src/options.rs | 6 + 4 files changed, 1281 insertions(+), 17 deletions(-) diff --git a/doc/zjit.md b/doc/zjit.md index 3d7ee33abfa438..bb20b9f6924bac 100644 --- a/doc/zjit.md +++ b/doc/zjit.md @@ -162,6 +162,14 @@ A file called `zjit_exits_{pid}.dump` will be created in the same directory as ` stackprof path/to/zjit_exits_{pid}.dump ``` +### Viewing HIR in Iongraph + +Using `--zjit-dump-hir-iongraph` will dump all compiled functions into a directory named `/tmp/zjit-iongraph-{PROCESS_PID}`. Each file will be named `func_{ZJIT_FUNC_NAME}.json`. In order to use them in the Iongraph viewer, you'll need to use `jq` to collate them to a single file. An example invocation of `jq` is shown below for reference. + +`jq --slurp --null-input '.functions=inputs | .version=2' /tmp/zjit-iongraph-{PROCESS_PID}/func*.json > ~/Downloads/ion.json` + +From there, you can use https://mozilla-spidermonkey.github.io/iongraph/ to view your trace. + ### Printing ZJIT Errors `--zjit-debug` prints ZJIT compilation errors and other diagnostics: diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 8e6241f0dafe0f..2640507e33fab5 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6,7 +6,7 @@ #![allow(clippy::if_same_then_else)] #![allow(clippy::match_like_matches_macro)] use crate::{ - cast::IntoUsize, codegen::local_idx_to_ep_offset, cruby::*, payload::{get_or_create_iseq_payload, IseqPayload}, options::{debug, get_option, DumpHIR}, state::ZJITState + 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::{ cell::RefCell, collections::{HashMap, HashSet, VecDeque}, ffi::{c_void, c_uint, c_int, CStr}, fmt::Display, mem::{align_of, size_of}, ptr, slice::Iter @@ -39,7 +39,7 @@ impl std::fmt::Display for InsnId { } /// The index of a [`Block`], which effectively acts like a pointer. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub struct BlockId(pub usize); impl From for usize { @@ -3686,23 +3686,171 @@ impl Function { } } + /// Helper function to make an Iongraph JSON "instruction". + /// `uses`, `memInputs` and `attributes` are left empty for now, but may be populated + /// in the future. + fn make_iongraph_instr(id: InsnId, inputs: Vec, opcode: &str, ty: &str) -> Json { + Json::object() + // Add an offset of 0x1000 to avoid the `ptr` being 0x0, which iongraph rejects. + .insert("ptr", id.0 + 0x1000) + .insert("id", id.0) + .insert("opcode", opcode) + .insert("attributes", Json::empty_array()) + .insert("inputs", Json::Array(inputs)) + .insert("uses", Json::empty_array()) + .insert("memInputs", Json::empty_array()) + .insert("type", ty) + .build() + } + + /// Helper function to make an Iongraph JSON "block". + fn make_iongraph_block(id: BlockId, predecessors: Vec, successors: Vec, instructions: Vec, attributes: Vec<&str>, loop_depth: u32) -> Json { + Json::object() + // Add an offset of 0x1000 to avoid the `ptr` being 0x0, which iongraph rejects. + .insert("ptr", id.0 + 0x1000) + .insert("id", id.0) + .insert("loopDepth", loop_depth) + .insert("attributes", Json::array(attributes)) + .insert("predecessors", Json::array(predecessors.iter().map(|x| x.0).collect::>())) + .insert("successors", Json::array(successors.iter().map(|x| x.0).collect::>())) + .insert("instructions", Json::array(instructions)) + .build() + } + + /// Helper function to make an Iongraph JSON "function". + /// Note that `lir` is unpopulated right now as ZJIT doesn't use its functionality. + fn make_iongraph_function(pass_name: &str, hir_blocks: Vec) -> Json { + Json::object() + .insert("name", pass_name) + .insert("mir", Json::object() + .insert("blocks", Json::array(hir_blocks)) + .build() + ) + .insert("lir", Json::object() + .insert("blocks", Json::empty_array()) + .build() + ) + .build() + } + + /// Generate an iongraph JSON pass representation for this function. + pub fn to_iongraph_pass(&self, pass_name: &str) -> Json { + let mut ptr_map = PtrPrintMap::identity(); + if cfg!(test) { + ptr_map.map_ptrs = true; + } + + let mut hir_blocks = Vec::new(); + let cfi = ControlFlowInfo::new(self); + let dominators = Dominators::new(self); + let loop_info = LoopInfo::new(&cfi, &dominators); + + // Push each block from the iteration in reverse post order to `hir_blocks`. + for block_id in self.rpo() { + // Create the block with instructions. + let block = &self.blocks[block_id.0]; + let predecessors = cfi.predecessors(block_id).collect(); + let successors = cfi.successors(block_id).collect(); + let mut instructions = Vec::new(); + + // Process all instructions (parameters and body instructions). + // Parameters are currently guaranteed to be Parameter instructions, but in the future + // they might be refined to other instruction kinds by the optimizer. + for insn_id in block.params.iter().chain(block.insns.iter()) { + let insn_id = self.union_find.borrow().find_const(*insn_id); + let insn = self.find(insn_id); + + // Snapshots are not serialized, so skip them. + if matches!(insn, Insn::Snapshot {..}) { + continue; + } + + // Instructions with no output or an empty type should have an empty type field. + let type_str = if insn.has_output() { + let insn_type = self.type_of(insn_id); + if insn_type.is_subtype(types::Empty) { + String::new() + } else { + insn_type.print(&ptr_map).to_string() + } + } else { + String::new() + }; + + + let opcode = insn.print(&ptr_map).to_string(); + + // Traverse the worklist to get inputs for a given instruction. + let mut inputs = VecDeque::new(); + self.worklist_traverse_single_insn(&insn, &mut inputs); + let inputs: Vec = inputs.into_iter().map(|x| x.0.into()).collect(); + + instructions.push( + Self::make_iongraph_instr( + insn_id, + inputs, + &opcode, + &type_str + ) + ); + } + + let mut attributes = vec![]; + if loop_info.is_back_edge_source(block_id) { + attributes.push("backedge"); + } + if loop_info.is_loop_header(block_id) { + attributes.push("loopheader"); + } + let loop_depth = loop_info.loop_depth(block_id); + + hir_blocks.push(Self::make_iongraph_block( + block_id, + predecessors, + successors, + instructions, + attributes, + loop_depth, + )); + } + + Self::make_iongraph_function(pass_name, hir_blocks) + } + /// Run all the optimization passes we have. pub fn optimize(&mut self) { + let mut passes: Vec = Vec::new(); + let should_dump = get_option!(dump_hir_iongraph); + + macro_rules! run_pass { + ($name:ident) => { + self.$name(); + #[cfg(debug_assertions)] self.assert_validates(); + if should_dump { + passes.push( + self.to_iongraph_pass(stringify!($name)) + ); + } + } + } + + if should_dump { + passes.push(self.to_iongraph_pass("unoptimized")); + } + // Function is assumed to have types inferred already - self.type_specialize(); - #[cfg(debug_assertions)] self.assert_validates(); - self.inline(); - #[cfg(debug_assertions)] self.assert_validates(); - self.optimize_getivar(); - #[cfg(debug_assertions)] self.assert_validates(); - self.optimize_c_calls(); - #[cfg(debug_assertions)] self.assert_validates(); - self.fold_constants(); - #[cfg(debug_assertions)] self.assert_validates(); - self.clean_cfg(); - #[cfg(debug_assertions)] self.assert_validates(); - self.eliminate_dead_code(); - #[cfg(debug_assertions)] self.assert_validates(); + run_pass!(type_specialize); + run_pass!(inline); + run_pass!(optimize_getivar); + run_pass!(optimize_c_calls); + run_pass!(fold_constants); + run_pass!(clean_cfg); + run_pass!(eliminate_dead_code); + + if should_dump { + let iseq_name = iseq_get_location(self.iseq, 0); + self.dump_iongraph(&iseq_name, passes); + } } /// Dump HIR passed to codegen if specified by options. @@ -3723,6 +3871,32 @@ impl Function { } } + pub fn dump_iongraph(&self, function_name: &str, passes: Vec) { + fn sanitize_for_filename(name: &str) -> String { + name.chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '_' || c == '-' { + c + } else { + '_' + } + }) + .collect() + } + + use std::io::Write; + let dir = format!("/tmp/zjit-iongraph-{}", std::process::id()); + std::fs::create_dir_all(&dir).expect("Unable to create directory."); + let sanitized = sanitize_for_filename(function_name); + let path = format!("{dir}/func_{sanitized}.json"); + let mut file = std::fs::File::create(path).unwrap(); + let json = Json::object() + .insert("name", function_name) + .insert("passes", passes) + .build(); + writeln!(file, "{}", json).unwrap(); + } + /// Validates the following: /// 1. Basic block jump args match parameter arity. /// 2. Every terminator must be in the last position. @@ -4087,7 +4261,13 @@ impl Function { impl<'a> std::fmt::Display for FunctionPrinter<'a> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let fun = &self.fun; - let iseq_name = iseq_get_location(fun.iseq, 0); + // In tests, there may not be an iseq to get location from. + let iseq_name = if fun.iseq.is_null() { + String::from("") + } else { + iseq_get_location(fun.iseq, 0) + }; + // In tests, strip the line number for builtin ISEQs to make tests stable across line changes let iseq_name = if cfg!(test) && iseq_name.contains("@ { + f: &'a Function, + dominators: Vec>, +} + +impl<'a> Dominators<'a> { + pub fn new(f: &'a Function) -> Self { + let mut cfi = ControlFlowInfo::new(f); + Self::with_cfi(f, &mut cfi) + } + + pub fn with_cfi(f: &'a Function, cfi: &mut ControlFlowInfo) -> Self { + let block_ids = f.rpo(); + let mut dominators = vec![vec![]; f.blocks.len()]; + + // Compute dominators for each node using fixed point iteration. + // Approach can be found in Figure 1 of: + // https://www.cs.tufts.edu/~nr/cs257/archive/keith-cooper/dom14.pdf + // + // Initially we set: + // + // dom(entry) = {entry} for each entry block + // dom(b != entry) = {all nodes} + // + // Iteratively, apply: + // + // dom(b) = {b} union intersect(dom(p) for p in predecessors(b)) + // + // When we've run the algorithm and the dominator set no longer changes + // between iterations, then we have found the dominator sets. + + // Set up entry blocks. + // Entry blocks are only dominated by themselves. + for entry_block in &f.entry_blocks() { + dominators[entry_block.0] = vec![*entry_block]; + } + + // Setup the initial dominator sets. + for block_id in &block_ids { + if !f.entry_blocks().contains(block_id) { + // Non entry blocks are initially dominated by all other blocks. + dominators[block_id.0] = block_ids.clone(); + } + } + + let mut changed = true; + while changed { + changed = false; + + for block_id in &block_ids { + if *block_id == f.entry_block { + continue; + } + + // Get all predecessors for a given block. + let block_preds: Vec = cfi.predecessors(*block_id).collect(); + if block_preds.is_empty() { + continue; + } + + let mut new_doms = dominators[block_preds[0].0].clone(); + + // Compute the intersection of predecessor dominator sets into `new_doms`. + for pred_id in &block_preds[1..] { + let pred_doms = &dominators[pred_id.0]; + // Only keep a dominator in `new_doms` if it is also found in pred_doms + new_doms.retain(|d| pred_doms.contains(d)); + } + + // Insert sorted into `new_doms`. + match new_doms.binary_search(block_id) { + Ok(_) => {} + Err(pos) => new_doms.insert(pos, *block_id) + } + + // If we have computed a new dominator set, then we can update + // the dominators and mark that we need another iteration. + if dominators[block_id.0] != new_doms { + dominators[block_id.0] = new_doms; + changed = true; + } + } + } + + Self { f, dominators } + } + + + pub fn is_dominated_by(&self, left: BlockId, right: BlockId) -> bool { + self.dominators(left).any(|&b| b == right) + } + + pub fn dominators(&self, block: BlockId) -> Iter<'_, BlockId> { + self.dominators[block.0].iter() + } +} + +pub struct ControlFlowInfo<'a> { + function: &'a Function, + successor_map: HashMap>, + predecessor_map: HashMap>, +} + +impl<'a> ControlFlowInfo<'a> { + pub fn new(function: &'a Function) -> Self { + let mut successor_map: HashMap> = HashMap::new(); + let mut predecessor_map: HashMap> = HashMap::new(); + let uf = function.union_find.borrow(); + + for block_id in function.rpo() { + let block = &function.blocks[block_id.0]; + + // Since ZJIT uses extended basic blocks, one must check all instructions + // for their ability to jump to another basic block, rather than just + // the instructions at the end of a given basic block. + let successors: Vec = block + .insns + .iter() + .map(|&insn_id| uf.find_const(insn_id)) + .filter_map(|insn_id| { + Self::extract_jump_target(&function.insns[insn_id.0]) + }) + .collect(); + + // Update predecessors for successor blocks. + for &succ_id in &successors { + predecessor_map + .entry(succ_id) + .or_default() + .push(block_id); + } + + // Store successors for this block. + successor_map.insert(block_id, successors); + } + + Self { + function, + successor_map, + predecessor_map, + } + } + + pub fn is_succeeded_by(&self, left: BlockId, right: BlockId) -> bool { + self.successor_map.get(&right).is_some_and(|set| set.contains(&left)) + } + + pub fn is_preceded_by(&self, left: BlockId, right: BlockId) -> bool { + self.predecessor_map.get(&right).is_some_and(|set| set.contains(&left)) + } + + pub fn predecessors(&self, block: BlockId) -> impl Iterator { + self.predecessor_map.get(&block).into_iter().flatten().copied() + } + + pub fn successors(&self, block: BlockId) -> impl Iterator { + self.successor_map.get(&block).into_iter().flatten().copied() + } + + /// Helper function to extract the target of a jump instruction. + fn extract_jump_target(insn: &Insn) -> Option { + match insn { + Insn::Jump(target) + | Insn::IfTrue { target, .. } + | Insn::IfFalse { target, .. } => Some(target.target), + _ => None, + } + } +} + +pub struct LoopInfo<'a> { + cfi: &'a ControlFlowInfo<'a>, + dominators: &'a Dominators<'a>, + loop_depths: HashMap, + loop_headers: BlockSet, + back_edge_sources: BlockSet, +} + +impl<'a> LoopInfo<'a> { + pub fn new(cfi: &'a ControlFlowInfo<'a>, dominators: &'a Dominators<'a>) -> Self { + let mut loop_headers: BlockSet = BlockSet::with_capacity(cfi.function.num_blocks()); + let mut loop_depths: HashMap = HashMap::new(); + let mut back_edge_sources: BlockSet = BlockSet::with_capacity(cfi.function.num_blocks()); + let rpo = cfi.function.rpo(); + + for &block in &rpo { + loop_depths.insert(block, 0); + } + + // Collect loop headers. + for &block in &rpo { + // Initialize the loop depths. + for predecessor in cfi.predecessors(block) { + if dominators.is_dominated_by(predecessor, block) { + // Found a loop header, so then identify the natural loop. + loop_headers.insert(block); + back_edge_sources.insert(predecessor); + let loop_blocks = Self::find_natural_loop(cfi, block, predecessor); + // Increment the loop depth. + for loop_block in &loop_blocks { + *loop_depths.get_mut(loop_block).expect("Loop block should be populated.") += 1; + } + } + } + } + + Self { + cfi, + dominators, + loop_depths, + loop_headers, + back_edge_sources, + } + } + + fn find_natural_loop( + cfi: &ControlFlowInfo, + header: BlockId, + back_edge_source: BlockId, + ) -> HashSet { + // todo(aidenfoxivey): Reimplement using BlockSet + let mut loop_blocks = HashSet::new(); + let mut stack = vec![back_edge_source]; + + loop_blocks.insert(header); + loop_blocks.insert(back_edge_source); + + while let Some(block) = stack.pop() { + for pred in cfi.predecessors(block) { + // Pushes to stack only if `pred` wasn't already in `loop_blocks`. + if loop_blocks.insert(pred) { + stack.push(pred) + } + } + } + + loop_blocks + } + + pub fn loop_depth(&self, block: BlockId) -> u32 { + self.loop_depths.get(&block).copied().unwrap_or(0) + } + + pub fn is_back_edge_source(&self, block: BlockId) -> bool { + self.back_edge_sources.get(block) + } + + pub fn is_loop_header(&self, block: BlockId) -> bool { + self.loop_headers.get(block) + } +} + #[cfg(test)] mod union_find_tests { use super::UnionFind; diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index abf2f9497c2875..a00ca97e85a8b0 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -3425,3 +3425,821 @@ pub mod hir_build_tests { "); } } + + /// Test successor and predecessor set computations. + #[cfg(test)] + mod control_flow_info_tests { + use super::*; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + #[test] + fn test_linked_list() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + function.push_insn(bb1, Insn::Jump(edge(bb2))); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + + assert!(cfi.is_preceded_by(bb1, bb2)); + assert!(cfi.is_succeeded_by(bb2, bb1)); + assert!(cfi.predecessors(bb3).eq([bb2])); + } + + #[test] + fn test_diamond() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + let v1 = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: v1, target: edge(bb2)}); + function.push_insn(bb0, Insn::Jump(edge(bb1))); + function.push_insn(bb1, Insn::Jump(edge(bb3))); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + + assert!(cfi.is_preceded_by(bb2, bb3)); + assert!(cfi.is_preceded_by(bb1, bb3)); + assert!(!cfi.is_preceded_by(bb0, bb3)); + assert!(cfi.is_succeeded_by(bb1, bb0)); + assert!(cfi.is_succeeded_by(bb3, bb1)); + } + } + + /// Test dominator set computations. + #[cfg(test)] + mod dom_tests { + use super::*; + use insta::assert_snapshot; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + fn assert_dominators_contains_self(function: &Function, dominators: &Dominators) { + for (i, _) in function.blocks.iter().enumerate() { + // Ensure that each dominating set contains the block itself. + assert!(dominators.is_dominated_by(BlockId(i), BlockId(i))); + } + } + + #[test] + fn test_linked_list() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + function.push_insn(bb1, Insn::Jump(edge(bb2))); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + Jump bb2() + bb2(): + Jump bb3() + bb3(): + v3:Any = Const CBool(true) + Return v3 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb1, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb1, bb2, bb3].iter())); + } + + #[test] + fn test_diamond() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + + let val = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val, target: edge(bb1)}); + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + function.push_insn(bb2, Insn::Jump(edge(bb3))); + function.push_insn(bb1, Insn::Jump(edge(bb3))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + IfTrue v0, bb1() + Jump bb2() + bb1(): + Jump bb3() + bb2(): + Jump bb3() + bb3(): + v5:Any = Const CBool(true) + Return v5 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb3].iter())); + } + + #[test] + fn test_complex_cfg() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + let bb6 = function.new_block(0); + let bb7 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + let v0 = function.push_insn(bb1, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb1, Insn::IfTrue { val: v0, target: edge(bb2)}); + function.push_insn(bb1, Insn::Jump(edge(bb4))); + + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let v1 = function.push_insn(bb3, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb3, Insn::IfTrue { val: v1, target: edge(bb5)}); + function.push_insn(bb3, Insn::Jump(edge(bb7))); + + function.push_insn(bb4, Insn::Jump(edge(bb5))); + + function.push_insn(bb5, Insn::Jump(edge(bb6))); + + function.push_insn(bb6, Insn::Jump(edge(bb7))); + + let retval = function.push_insn(bb7, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb7, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + v1:Any = Const Value(false) + IfTrue v1, bb2() + Jump bb4() + bb2(): + Jump bb3() + bb3(): + v5:Any = Const Value(false) + IfTrue v5, bb5() + Jump bb7() + bb4(): + Jump bb5() + bb5(): + Jump bb6() + bb6(): + Jump bb7() + bb7(): + v11:Any = Const CBool(true) + Return v11 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb1, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb1, bb2, bb3].iter())); + assert!(dominators.dominators(bb4).eq([bb0, bb1, bb4].iter())); + assert!(dominators.dominators(bb5).eq([bb0, bb1, bb5].iter())); + assert!(dominators.dominators(bb6).eq([bb0, bb1, bb5, bb6].iter())); + assert!(dominators.dominators(bb7).eq([bb0, bb1, bb7].iter())); + } + + #[test] + fn test_back_edges() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + + let v0 = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: v0, target: edge(bb1)}); + function.push_insn(bb0, Insn::Jump(edge(bb4))); + + let v1 = function.push_insn(bb1, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb1, Insn::IfTrue { val: v1, target: edge(bb2)}); + function.push_insn(bb1, Insn::Jump(edge(bb3))); + + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + function.push_insn(bb4, Insn::Jump(edge(bb5))); + + let v2 = function.push_insn(bb5, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb5, Insn::IfTrue { val: v2, target: edge(bb3)}); + function.push_insn(bb5, Insn::Jump(edge(bb4))); + + let retval = function.push_insn(bb3, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb3, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + IfTrue v0, bb1() + Jump bb4() + bb1(): + v3:Any = Const Value(false) + IfTrue v3, bb2() + Jump bb3() + bb2(): + Jump bb3() + bb4(): + Jump bb5() + bb5(): + v8:Any = Const Value(false) + IfTrue v8, bb3() + Jump bb4() + bb3(): + v11:Any = Const CBool(true) + Return v11 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + assert!(dominators.dominators(bb0).eq([bb0].iter())); + assert!(dominators.dominators(bb1).eq([bb0, bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb0, bb1, bb2].iter())); + assert!(dominators.dominators(bb3).eq([bb0, bb3].iter())); + assert!(dominators.dominators(bb4).eq([bb0, bb4].iter())); + assert!(dominators.dominators(bb5).eq([bb0, bb4, bb5].iter())); + } + + #[test] + fn test_multiple_entry_blocks() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + function.jit_entry_blocks.push(bb1); + let bb2 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb2, Insn::Return { val: retval }); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb2() + bb1(): + Jump bb2() + bb2(): + v2:Any = Const CBool(true) + Return v2 + "); + + let dominators = Dominators::new(&function); + assert_dominators_contains_self(&function, &dominators); + + assert!(dominators.dominators(bb1).eq([bb1].iter())); + assert!(dominators.dominators(bb2).eq([bb2].iter())); + + assert!(!dominators.is_dominated_by(bb1, bb2)); + } + } + + /// Test loop information computation. +#[cfg(test)] +mod loop_info_tests { + use super::*; + use insta::assert_snapshot; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + #[test] + fn test_loop_depth() { + // ┌─────┐ + // │ bb0 │ + // └──┬──┘ + // │ + // ┌──▼──┐ ┌─────┐ + // │ bb2 ◄──────┼ bb1 ◄─┐ + // └──┬──┘ └─────┘ │ + // └─────────────────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + let val = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb2, Insn::IfTrue { val, target: edge(bb1)}); + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb2, Insn::Return { val: retval }); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb2() + v1:Any = Const Value(false) + bb2(): + IfTrue v1, bb1() + v3:Any = Const CBool(true) + Return v3 + bb1(): + Jump bb2() + "); + + assert!(loop_info.is_loop_header(bb2)); + assert!(loop_info.is_back_edge_source(bb1)); + assert_eq!(loop_info.loop_depth(bb1), 1); + } + + #[test] + fn test_nested_loops() { + // ┌─────┐ + // │ bb0 ◄─────┐ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb1 ◄───┐ │ + // └──┬──┘ │ │ + // │ │ │ + // ┌──▼──┐ │ │ + // │ bb2 ┼───┘ │ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb3 ┼─────┘ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb4 │ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let cond = function.push_insn(bb2, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb2, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb2, Insn::Jump(edge(bb3))); + + let cond = function.push_insn(bb3, Insn::Const { val: Const::Value(Qtrue) }); + let _ = function.push_insn(bb3, Insn::IfTrue { val: cond, target: edge(bb0) }); + function.push_insn(bb3, Insn::Jump(edge(bb4))); + + let retval = function.push_insn(bb4, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb4, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + Jump bb2() + bb2(): + v2:Any = Const Value(false) + IfTrue v2, bb1() + Jump bb3() + bb3(): + v5:Any = Const Value(true) + IfTrue v5, bb0() + Jump bb4() + bb4(): + v8:Any = Const CBool(true) + Return v8 + "); + + assert!(loop_info.is_loop_header(bb0)); + assert!(loop_info.is_loop_header(bb1)); + + assert_eq!(loop_info.loop_depth(bb0), 1); + assert_eq!(loop_info.loop_depth(bb1), 2); + assert_eq!(loop_info.loop_depth(bb2), 2); + assert_eq!(loop_info.loop_depth(bb3), 1); + assert_eq!(loop_info.loop_depth(bb4), 0); + + assert!(loop_info.is_back_edge_source(bb2)); + assert!(loop_info.is_back_edge_source(bb3)); + } + + #[test] + fn test_complex_loops() { + // ┌─────┐ + // ┌──────► bb0 │ + // │ └──┬──┘ + // │ ┌────┴────┐ + // │ ┌──▼──┐ ┌──▼──┐ + // │ │ bb1 ◄─┐ │ bb3 ◄─┐ + // │ └──┬──┘ │ └──┬──┘ │ + // │ │ │ │ │ + // │ ┌──▼──┐ │ ┌──▼──┐ │ + // │ │ bb2 ┼─┘ │ bb4 ┼─┘ + // │ └──┬──┘ └──┬──┘ + // │ └────┬────┘ + // │ ┌──▼──┐ + // └──────┼ bb5 │ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb6 │ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + let bb6 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb0, Insn::Jump(edge(bb3))); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let _ = function.push_insn(bb2, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb2, Insn::Jump(edge(bb5))); + + function.push_insn(bb3, Insn::Jump(edge(bb4))); + + let _ = function.push_insn(bb4, Insn::IfTrue { val: cond, target: edge(bb3) }); + function.push_insn(bb4, Insn::Jump(edge(bb5))); + + let _ = function.push_insn(bb5, Insn::IfTrue { val: cond, target: edge(bb0) }); + function.push_insn(bb5, Insn::Jump(edge(bb6))); + + let retval = function.push_insn(bb6, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb6, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + IfTrue v0, bb1() + Jump bb3() + bb1(): + Jump bb2() + bb2(): + IfTrue v0, bb1() + Jump bb5() + bb3(): + Jump bb4() + bb4(): + IfTrue v0, bb3() + Jump bb5() + bb5(): + IfTrue v0, bb0() + Jump bb6() + bb6(): + v11:Any = Const CBool(true) + Return v11 + "); + + assert!(loop_info.is_loop_header(bb0)); + assert!(loop_info.is_loop_header(bb1)); + assert!(!loop_info.is_loop_header(bb2)); + assert!(loop_info.is_loop_header(bb3)); + assert!(!loop_info.is_loop_header(bb5)); + assert!(!loop_info.is_loop_header(bb4)); + assert!(!loop_info.is_loop_header(bb6)); + + assert_eq!(loop_info.loop_depth(bb0), 1); + assert_eq!(loop_info.loop_depth(bb1), 2); + assert_eq!(loop_info.loop_depth(bb2), 2); + assert_eq!(loop_info.loop_depth(bb3), 2); + assert_eq!(loop_info.loop_depth(bb4), 2); + assert_eq!(loop_info.loop_depth(bb5), 1); + assert_eq!(loop_info.loop_depth(bb6), 0); + + assert!(loop_info.is_back_edge_source(bb2)); + assert!(loop_info.is_back_edge_source(bb4)); + assert!(loop_info.is_back_edge_source(bb5)); + } + + #[test] + fn linked_list_non_loop() { + // ┌─────┐ + // │ bb0 │ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb1 │ + // └──┬──┘ + // │ + // ┌──▼──┐ + // │ bb2 │ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + let _ = function.push_insn(bb0, Insn::Jump(edge(bb1))); + let _ = function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb2, Insn::Return { val: retval }); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + Jump bb1() + bb1(): + Jump bb2() + bb2(): + v2:Any = Const CBool(true) + Return v2 + "); + + assert!(!loop_info.is_loop_header(bb0)); + assert!(!loop_info.is_loop_header(bb1)); + assert!(!loop_info.is_loop_header(bb2)); + + assert!(!loop_info.is_back_edge_source(bb0)); + assert!(!loop_info.is_back_edge_source(bb1)); + assert!(!loop_info.is_back_edge_source(bb2)); + + assert_eq!(loop_info.loop_depth(bb0), 0); + assert_eq!(loop_info.loop_depth(bb1), 0); + assert_eq!(loop_info.loop_depth(bb2), 0); + } + + #[test] + fn triple_nested_loop() { + // ┌─────┐ + // │ bb0 ◄──┐ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb1 ◄─┐│ + // └──┬──┘ ││ + // │ ││ + // ┌──▼──┐ ││ + // │ bb2 ◄┐││ + // └──┬──┘│││ + // │ │││ + // ┌──▼──┐│││ + // │ bb3 ┼┘││ + // └──┬──┘ ││ + // │ ││ + // ┌──▼──┐ ││ + // │ bb4 ┼─┘│ + // └──┬──┘ │ + // │ │ + // ┌──▼──┐ │ + // │ bb5 ┼──┘ + // └─────┘ + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + let bb3 = function.new_block(0); + let bb4 = function.new_block(0); + let bb5 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb0, Insn::Jump(edge(bb1))); + let _ = function.push_insn(bb1, Insn::Jump(edge(bb2))); + let _ = function.push_insn(bb2, Insn::Jump(edge(bb3))); + let _ = function.push_insn(bb3, Insn::Jump(edge(bb4))); + let _ = function.push_insn(bb3, Insn::IfTrue {val: cond, target: edge(bb2)}); + let _ = function.push_insn(bb4, Insn::Jump(edge(bb5))); + let _ = function.push_insn(bb4, Insn::IfTrue {val: cond, target: edge(bb1)}); + let _ = function.push_insn(bb5, Insn::IfTrue {val: cond, target: edge(bb0)}); + + assert_snapshot!(format!("{}", FunctionPrinter::without_snapshot(&function)), @r" + fn : + bb0(): + v0:Any = Const Value(false) + Jump bb1() + bb1(): + Jump bb2() + bb2(): + Jump bb3() + bb3(): + Jump bb4() + IfTrue v0, bb2() + bb4(): + Jump bb5() + IfTrue v0, bb1() + bb5(): + IfTrue v0, bb0() + "); + + let cfi = ControlFlowInfo::new(&function); + let dominators = Dominators::new(&function); + let loop_info = LoopInfo::new(&cfi, &dominators); + + assert!(!loop_info.is_back_edge_source(bb0)); + assert!(!loop_info.is_back_edge_source(bb1)); + assert!(!loop_info.is_back_edge_source(bb2)); + assert!(loop_info.is_back_edge_source(bb3)); + assert!(loop_info.is_back_edge_source(bb4)); + assert!(loop_info.is_back_edge_source(bb5)); + + assert_eq!(loop_info.loop_depth(bb0), 1); + assert_eq!(loop_info.loop_depth(bb1), 2); + assert_eq!(loop_info.loop_depth(bb2), 3); + assert_eq!(loop_info.loop_depth(bb3), 3); + assert_eq!(loop_info.loop_depth(bb4), 2); + assert_eq!(loop_info.loop_depth(bb5), 1); + + assert!(loop_info.is_loop_header(bb0)); + assert!(loop_info.is_loop_header(bb1)); + assert!(loop_info.is_loop_header(bb2)); + assert!(!loop_info.is_loop_header(bb3)); + assert!(!loop_info.is_loop_header(bb4)); + assert!(!loop_info.is_loop_header(bb5)); + } + } + +/// Test dumping to iongraph format. +#[cfg(test)] +mod iongraph_tests { + use super::*; + use insta::assert_snapshot; + + fn edge(target: BlockId) -> BranchEdge { + BranchEdge { target, args: vec![] } + } + + #[test] + fn test_simple_function() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + + let retval = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::Return { val: retval }); + + let json = function.to_iongraph_pass("simple"); + assert_snapshot!(json.to_string(), @r#"{"name":"simple", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"Return v0", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_two_blocks() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb1))); + + let retval = function.push_insn(bb1, Insn::Const { val: Const::CBool(false) }); + function.push_insn(bb1, Insn::Return { val: retval }); + + let json = function.to_iongraph_pass("two_blocks"); + assert_snapshot!(json.to_string(), @r#"{"name":"two_blocks", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[1], "instructions":[{"ptr":4096, "id":0, "opcode":"Jump bb1()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4097, "id":1, "opcode":"Const CBool(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4098, "id":2, "opcode":"Return v1", "attributes":[], "inputs":[1], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_multiple_instructions() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + + let val1 = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::Return { val: val1 }); + + let json = function.to_iongraph_pass("multiple_instructions"); + assert_snapshot!(json.to_string(), @r#"{"name":"multiple_instructions", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"Return v0", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_conditional_branch() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::IfTrue { val: cond, target: edge(bb1) }); + + let retval1 = function.push_insn(bb0, Insn::Const { val: Const::CBool(false) }); + function.push_insn(bb0, Insn::Return { val: retval1 }); + + let retval2 = function.push_insn(bb1, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb1, Insn::Return { val: retval2 }); + + let json = function.to_iongraph_pass("conditional_branch"); + assert_snapshot!(json.to_string(), @r#"{"name":"conditional_branch", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[1], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"IfTrue v0, bb1()", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}, {"ptr":4098, "id":2, "opcode":"Const CBool(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4099, "id":3, "opcode":"Return v2", "attributes":[], "inputs":[2], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4100, "id":4, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4101, "id":5, "opcode":"Return v4", "attributes":[], "inputs":[4], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_loop_structure() { + let mut function = Function::new(std::ptr::null()); + + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + let val = function.push_insn(bb0, Insn::Const { val: Const::Value(Qfalse) }); + let _ = function.push_insn(bb2, Insn::IfTrue { val, target: edge(bb1)}); + let retval = function.push_insn(bb2, Insn::Const { val: Const::CBool(true) }); + let _ = function.push_insn(bb2, Insn::Return { val: retval }); + + function.push_insn(bb1, Insn::Jump(edge(bb2))); + + let json = function.to_iongraph_pass("loop_structure"); + assert_snapshot!(json.to_string(), @r#"{"name":"loop_structure", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[2], "instructions":[{"ptr":4096, "id":0, "opcode":"Jump bb2()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}, {"ptr":4097, "id":1, "opcode":"Const Value(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}]}, {"ptr":4098, "id":2, "loopDepth":1, "attributes":["loopheader"], "predecessors":[0, 1], "successors":[1], "instructions":[{"ptr":4098, "id":2, "opcode":"IfTrue v1, bb1()", "attributes":[], "inputs":[1], "uses":[], "memInputs":[], "type":""}, {"ptr":4099, "id":3, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4100, "id":4, "opcode":"Return v3", "attributes":[], "inputs":[3], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":1, "attributes":["backedge"], "predecessors":[2], "successors":[2], "instructions":[{"ptr":4101, "id":5, "opcode":"Jump bb2()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + + #[test] + fn test_multiple_successors() { + let mut function = Function::new(std::ptr::null()); + let bb0 = function.entry_block; + let bb1 = function.new_block(0); + let bb2 = function.new_block(0); + + let cond = function.push_insn(bb0, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb0, Insn::IfTrue { val: cond, target: edge(bb1) }); + function.push_insn(bb0, Insn::Jump(edge(bb2))); + + let retval1 = function.push_insn(bb1, Insn::Const { val: Const::CBool(true) }); + function.push_insn(bb1, Insn::Return { val: retval1 }); + + let retval2 = function.push_insn(bb2, Insn::Const { val: Const::CBool(false) }); + function.push_insn(bb2, Insn::Return { val: retval2 }); + + let json = function.to_iongraph_pass("multiple_successors"); + assert_snapshot!(json.to_string(), @r#"{"name":"multiple_successors", "mir":{"blocks":[{"ptr":4096, "id":0, "loopDepth":0, "attributes":[], "predecessors":[], "successors":[1, 2], "instructions":[{"ptr":4096, "id":0, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4097, "id":1, "opcode":"IfTrue v0, bb1()", "attributes":[], "inputs":[0], "uses":[], "memInputs":[], "type":""}, {"ptr":4098, "id":2, "opcode":"Jump bb2()", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4097, "id":1, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4099, "id":3, "opcode":"Const CBool(true)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4100, "id":4, "opcode":"Return v3", "attributes":[], "inputs":[3], "uses":[], "memInputs":[], "type":""}]}, {"ptr":4098, "id":2, "loopDepth":0, "attributes":[], "predecessors":[0], "successors":[], "instructions":[{"ptr":4101, "id":5, "opcode":"Const CBool(false)", "attributes":[], "inputs":[], "uses":[], "memInputs":[], "type":"Any"}, {"ptr":4102, "id":6, "opcode":"Return v5", "attributes":[], "inputs":[5], "uses":[], "memInputs":[], "type":""}]}]}, "lir":{"blocks":[]}}"#); + } + } diff --git a/zjit/src/options.rs b/zjit/src/options.rs index c165035eaa1af0..b7e2c71cefcd65 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -70,6 +70,9 @@ pub struct Options { /// Dump High-level IR to the given file in Graphviz format after optimization pub dump_hir_graphviz: Option, + /// Dump High-level IR in Iongraph JSON format after optimization to /tmp/zjit-iongraph-{$PID} + pub dump_hir_iongraph: bool, + /// Dump low-level IR pub dump_lir: Option>, @@ -106,6 +109,7 @@ impl Default for Options { dump_hir_init: None, dump_hir_opt: None, dump_hir_graphviz: None, + dump_hir_iongraph: false, dump_lir: None, dump_disasm: false, trace_side_exits: None, @@ -353,6 +357,8 @@ fn parse_option(str_ptr: *const std::os::raw::c_char) -> Option<()> { options.dump_hir_graphviz = Some(opt_val); } + ("dump-hir-iongraph", "") => options.dump_hir_iongraph = true, + ("dump-lir", "") => options.dump_lir = Some(HashSet::from([DumpLIR::init])), ("dump-lir", filters) => { let mut dump_lirs = HashSet::new(); From 4107a41020003d7106883b78891560e05d299310 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 18 Nov 2025 17:45:27 -0500 Subject: [PATCH 08/13] ZJIT: Re-link the test binary when only miniruby changes --- zjit/build.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zjit/build.rs b/zjit/build.rs index 6aec5407f62af7..4ee3d65b33062e 100644 --- a/zjit/build.rs +++ b/zjit/build.rs @@ -5,9 +5,11 @@ fn main() { // option_env! automatically registers a rerun-if-env-changed if let Some(ruby_build_dir) = option_env!("RUBY_BUILD_DIR") { - // Link against libminiruby + // Link against libminiruby.a println!("cargo:rustc-link-search=native={ruby_build_dir}"); println!("cargo:rustc-link-lib=static:-bundle=miniruby"); + // Re-link when libminiruby.a changes + println!("cargo:rerun-if-changed={ruby_build_dir}/libminiruby.a"); // System libraries that libminiruby needs. Has to be // ordered after -lminiruby above. From 2cd792a1cfefeaee948b321bbc14cb86acc2d456 Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Tue, 18 Nov 2025 17:46:52 -0500 Subject: [PATCH 09/13] ZJIT: Fix assertion failure when profiling VM_BLOCK_HANDLER_NONE As can be seen in vm_block_handler_verify(), VM_BLOCK_HANDLER_NONE is not a valid argument for vm_block_handler(). Store nil in the profiler when seen instead of crashing. --- vm_insnhelper.c | 3 +++ zjit/src/profile.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/vm_insnhelper.c b/vm_insnhelper.c index 8495ee59ef438e..7626d461352c8d 100644 --- a/vm_insnhelper.c +++ b/vm_insnhelper.c @@ -6041,11 +6041,14 @@ vm_define_method(const rb_execution_context_t *ec, VALUE obj, ID id, VALUE iseqv } // Return the untagged block handler: +// * If it's VM_BLOCK_HANDLER_NONE, return nil // * If it's an ISEQ or an IFUNC, fetch it from its rb_captured_block // * If it's a PROC or SYMBOL, return it as is static VALUE rb_vm_untag_block_handler(VALUE block_handler) { + if (VM_BLOCK_HANDLER_NONE == block_handler) return Qnil; + switch (vm_block_handler_type(block_handler)) { case block_handler_type_iseq: case block_handler_type_ifunc: { diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 47bae3ac633a86..8c8190609d7fae 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -361,3 +361,17 @@ impl IseqProfile { } } } + +#[cfg(test)] +mod tests { + use crate::cruby::*; + + #[test] + fn can_profile_block_handler() { + with_rubyvm(|| eval(" + def foo = yield + foo rescue 0 + foo rescue 0 + ")); + } +} From fa02d7a01f5e7516de8eb3c7f92ec75c50c06e3f Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Nov 2025 17:09:39 -0500 Subject: [PATCH 10/13] Implement heap_live_slots in GC.stat_heap [Feature #20408] --- gc/default/default.c | 3 +++ test/ruby/test_gc.rb | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gc/default/default.c b/gc/default/default.c index 82741458bb8ff3..7bd9b3c74b0377 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7592,6 +7592,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) enum gc_stat_heap_sym { gc_stat_heap_sym_slot_size, + gc_stat_heap_sym_heap_live_slots, gc_stat_heap_sym_heap_eden_pages, gc_stat_heap_sym_heap_eden_slots, gc_stat_heap_sym_total_allocated_pages, @@ -7610,6 +7611,7 @@ setup_gc_stat_heap_symbols(void) if (gc_stat_heap_symbols[0] == 0) { #define S(s) gc_stat_heap_symbols[gc_stat_heap_sym_##s] = ID2SYM(rb_intern_const(#s)) S(slot_size); + S(heap_live_slots); S(heap_eden_pages); S(heap_eden_slots); S(total_allocated_pages); @@ -7631,6 +7633,7 @@ stat_one_heap(rb_heap_t *heap, VALUE hash, VALUE key) rb_hash_aset(hash, gc_stat_heap_symbols[gc_stat_heap_sym_##name], SIZET2NUM(attr)); SET(slot_size, heap->slot_size); + SET(heap_live_slots, heap->total_allocated_objects - heap->total_freed_objects - heap->final_slots_count); SET(heap_eden_pages, heap->total_pages); SET(heap_eden_slots, heap->total_slots); SET(total_allocated_pages, heap->total_allocated_pages); diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 7695fd33cf9945..38e91366adbff3 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -231,6 +231,7 @@ def test_stat_heap end assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size] + assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages] assert_operator stat_heap[:heap_eden_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 @@ -261,7 +262,7 @@ def test_stat_heap_all GC.stat_heap(i, stat_heap) # Remove keys that can vary between invocations - %i(total_allocated_objects).each do |sym| + %i(total_allocated_objects heap_live_slots).each do |sym| stat_heap[sym] = stat_heap_all[i][sym] = 0 end @@ -286,6 +287,7 @@ def test_stat_heap_constraints hash.each { |k, v| stat_heap_sum[k] += v } end + assert_equal stat[:heap_live_slots], stat_heap_sum[:heap_live_slots] assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages] assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects] From 83bf05427d882b9d5b9adf500abe3471eef14dd1 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Nov 2025 17:13:47 -0500 Subject: [PATCH 11/13] Implement heap_free_slots in GC.stat_heap [Feature #20408] --- gc/default/default.c | 3 +++ test/ruby/test_gc.rb | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gc/default/default.c b/gc/default/default.c index 7bd9b3c74b0377..35ca2ec1078dc2 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7593,6 +7593,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) enum gc_stat_heap_sym { gc_stat_heap_sym_slot_size, gc_stat_heap_sym_heap_live_slots, + gc_stat_heap_sym_heap_free_slots, gc_stat_heap_sym_heap_eden_pages, gc_stat_heap_sym_heap_eden_slots, gc_stat_heap_sym_total_allocated_pages, @@ -7612,6 +7613,7 @@ setup_gc_stat_heap_symbols(void) #define S(s) gc_stat_heap_symbols[gc_stat_heap_sym_##s] = ID2SYM(rb_intern_const(#s)) S(slot_size); S(heap_live_slots); + S(heap_free_slots); S(heap_eden_pages); S(heap_eden_slots); S(total_allocated_pages); @@ -7634,6 +7636,7 @@ stat_one_heap(rb_heap_t *heap, VALUE hash, VALUE key) SET(slot_size, heap->slot_size); SET(heap_live_slots, heap->total_allocated_objects - heap->total_freed_objects - heap->final_slots_count); + SET(heap_free_slots, heap->total_slots - (heap->total_allocated_objects - heap->total_freed_objects)); SET(heap_eden_pages, heap->total_pages); SET(heap_eden_slots, heap->total_slots); SET(total_allocated_pages, heap->total_allocated_pages); diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 38e91366adbff3..06bacf1b1a131c 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -232,6 +232,7 @@ def test_stat_heap assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size] assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] + assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages] assert_operator stat_heap[:heap_eden_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 @@ -262,7 +263,7 @@ def test_stat_heap_all GC.stat_heap(i, stat_heap) # Remove keys that can vary between invocations - %i(total_allocated_objects heap_live_slots).each do |sym| + %i(total_allocated_objects heap_live_slots heap_free_slots).each do |sym| stat_heap[sym] = stat_heap_all[i][sym] = 0 end @@ -288,6 +289,7 @@ def test_stat_heap_constraints end assert_equal stat[:heap_live_slots], stat_heap_sum[:heap_live_slots] + assert_equal stat[:heap_free_slots], stat_heap_sum[:heap_free_slots] assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages] assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects] From f5f69d41146d5e17e93ec5b219ae6f5ecd59e38b Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Wed, 19 Nov 2025 17:15:23 -0500 Subject: [PATCH 12/13] Implement heap_final_slots in GC.stat_heap [Feature #20408] --- gc/default/default.c | 3 +++ test/ruby/test_gc.rb | 2 ++ 2 files changed, 5 insertions(+) diff --git a/gc/default/default.c b/gc/default/default.c index 35ca2ec1078dc2..42561543d1a7c7 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7594,6 +7594,7 @@ enum gc_stat_heap_sym { gc_stat_heap_sym_slot_size, gc_stat_heap_sym_heap_live_slots, gc_stat_heap_sym_heap_free_slots, + gc_stat_heap_sym_heap_final_slots, gc_stat_heap_sym_heap_eden_pages, gc_stat_heap_sym_heap_eden_slots, gc_stat_heap_sym_total_allocated_pages, @@ -7614,6 +7615,7 @@ setup_gc_stat_heap_symbols(void) S(slot_size); S(heap_live_slots); S(heap_free_slots); + S(heap_final_slots); S(heap_eden_pages); S(heap_eden_slots); S(total_allocated_pages); @@ -7637,6 +7639,7 @@ stat_one_heap(rb_heap_t *heap, VALUE hash, VALUE key) SET(slot_size, heap->slot_size); SET(heap_live_slots, heap->total_allocated_objects - heap->total_freed_objects - heap->final_slots_count); SET(heap_free_slots, heap->total_slots - (heap->total_allocated_objects - heap->total_freed_objects)); + SET(heap_final_slots, heap->final_slots_count); SET(heap_eden_pages, heap->total_pages); SET(heap_eden_slots, heap->total_slots); SET(total_allocated_pages, heap->total_allocated_pages); diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 06bacf1b1a131c..6639013a54ca32 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -233,6 +233,7 @@ def test_stat_heap assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size] assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] + assert_operator stat_heap[:heap_final_slots], :<=, stat[:heap_final_slots] assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages] assert_operator stat_heap[:heap_eden_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 @@ -290,6 +291,7 @@ def test_stat_heap_constraints assert_equal stat[:heap_live_slots], stat_heap_sum[:heap_live_slots] assert_equal stat[:heap_free_slots], stat_heap_sum[:heap_free_slots] + assert_equal stat[:heap_final_slots], stat_heap_sum[:heap_final_slots] assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages] assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects] From 167c3dbaa052d7ad34c374fc6c5f2cceab76b3ac Mon Sep 17 00:00:00 2001 From: Alan Wu Date: Wed, 19 Nov 2025 19:03:23 -0500 Subject: [PATCH 13/13] Omit a test on s390x linux tripping over a git bug This test has been reliably failing on recent trunk versions. See: --- tool/test/test_sync_default_gems.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tool/test/test_sync_default_gems.rb b/tool/test/test_sync_default_gems.rb index cdbbb0c5394dc3..4a2688850be12a 100755 --- a/tool/test/test_sync_default_gems.rb +++ b/tool/test/test_sync_default_gems.rb @@ -319,6 +319,9 @@ def test_delete_after_conflict end def test_squash_merge + if RUBY_PLATFORM =~ /s390x/ + omit("git 2.43.0 bug on s390x ubuntu 24.04: BUG: log-tree.c:1058: did a remerge diff without remerge_objdir?!?") + end # 2---. <- branch # / \ # 1---3---3'<- merge commit with conflict resolution