From bec5e51c85ded96969fd070ac5c3908d9f544723 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Thu, 24 Sep 2026 13:44:33 -0400 Subject: [PATCH 1/6] Fix Module/Class#dup of frozen object This code should not raise an error: ``` @var = Class.new.freeze.dup # unfrozen copy @var.instance_variable_set(:@test, 123) ``` It was raising `Ractor::IsolationError` because of a bug in `rb_fields_tbl_copy`. This patch also fixes the following assertion failure in debug mode: ``` Module.new.freeze.singleton_class.instance_variable_set("@a", 1) ``` Now we get the proper behavior: ``` can't modify frozen Class: #> (FrozenError) ``` Fixes [Bug #22378] --- class.c | 6 +++++- object.c | 5 +++-- test/ruby/test_class.rb | 27 +++++++++++++++++++++++++++ test/ruby/test_module.rb | 17 +++++++++++++++++ variable.c | 9 +++++++-- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/class.c b/class.c index b8b22bb81e63c7..6cde1e16f13e52 100644 --- a/class.c +++ b/class.c @@ -2992,7 +2992,11 @@ singleton_class_of(VALUE obj, bool ensure_eigenclass) RCLASS_ATTACHED_OBJECT(klass) == obj)) { klass = rb_make_metaclass(obj, klass); } - RB_FL_SET_RAW(klass, RB_OBJ_FROZEN_RAW(obj)); + if (RB_OBJ_FROZEN_RAW(obj)) { + // Freeze through rb_obj_freeze_inline so the singleton class also + // gets a frozen shape_id, not just the FL_FREEZE flag. + rb_obj_freeze_inline(klass); + } if (ensure_eigenclass && RB_TYPE_P(obj, T_CLASS)) { /* ensures an exposed class belongs to its own eigenclass */ (void)ENSURE_EIGENCLASS(klass); diff --git a/object.c b/object.c index 69127b759d7417..4a27121e9217af 100644 --- a/object.c +++ b/object.c @@ -519,8 +519,9 @@ rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze) } if (RB_OBJ_FROZEN(obj)) { - shape_id_t next_shape_id = rb_obj_shape_transition_frozen(clone); - RBASIC_SET_SHAPE_ID(clone, next_shape_id); + // Not just the shape_id transition: a frozen class or module must + // also have a frozen fields object. + rb_obj_freeze_inline(clone); } break; case Qtrue: { diff --git a/test/ruby/test_class.rb b/test/ruby/test_class.rb index 39968f2741675c..196badf870f317 100644 --- a/test/ruby/test_class.rb +++ b/test/ruby/test_class.rb @@ -377,6 +377,33 @@ def foo assert_equal("mod#foo", copy.new.foo) end + def test_dup_of_frozen_class_is_not_frozen + original = Class.new + original.instance_variable_set(:@a, 1) + original.freeze + + copy = original.dup + assert_not_predicate(copy, :frozen?) + copy.instance_variable_set(:@b, 2) + assert_equal([1, 2], [copy.instance_variable_get(:@a), copy.instance_variable_get(:@b)]) + + copy = original.clone(freeze: false) + assert_not_predicate(copy, :frozen?) + copy.instance_variable_set(:@b, 2) + assert_equal([1, 2], [copy.instance_variable_get(:@a), copy.instance_variable_get(:@b)]) + end + + def test_singleton_class_of_frozen_class_is_frozen + klass = Class.new.freeze + assert_predicate(klass.singleton_class, :frozen?) + assert_raise(FrozenError) {klass.singleton_class.instance_variable_set(:@a, 1)} + + copy = Class.new.freeze.clone + assert_predicate(copy, :frozen?) + assert_predicate(copy.singleton_class, :frozen?) + assert_raise(FrozenError) {copy.instance_variable_set(:@a, 1)} + end + def test_nested_class_removal assert_normal_exit('File.__send__(:remove_const, :Stat); at_exit{File.stat(".")}; GC.start') end diff --git a/test/ruby/test_module.rb b/test/ruby/test_module.rb index 347d4e88db855e..0b7f241325c5ba 100644 --- a/test/ruby/test_module.rb +++ b/test/ruby/test_module.rb @@ -3404,6 +3404,23 @@ def test_clone_freeze assert_not_predicate m.clone(freeze: false), :frozen? end + def test_dup_of_frozen_module_is_not_frozen + m = Module.new + m.instance_variable_set(:@a, 1) + m.freeze + + copy = m.dup + assert_not_predicate(copy, :frozen?) + copy.instance_variable_set(:@b, 2) + assert_equal([1, 2], [copy.instance_variable_get(:@a), copy.instance_variable_get(:@b)]) + end + + def test_singleton_class_of_frozen_module_is_frozen + m = Module.new.freeze + assert_predicate(m.singleton_class, :frozen?) + assert_raise(FrozenError) {m.singleton_class.instance_variable_set(:@a, 1)} + end + def test_module_name_in_singleton_method s = Object.new.singleton_class mod = s.const_set(:Foo, Module.new) diff --git a/variable.c b/variable.c index 9ea36525b31005..4d9ac0c4db6e35 100644 --- a/variable.c +++ b/variable.c @@ -4693,8 +4693,13 @@ rb_fields_tbl_copy(VALUE dst, VALUE src) VALUE fields_obj = RCLASS_WRITABLE_FIELDS_OBJ(src); if (fields_obj) { - RCLASS_WRITABLE_SET_FIELDS_OBJ(dst, rb_imemo_fields_clone(fields_obj)); - RBASIC_SET_SHAPE_ID(dst, RBASIC_SHAPE_ID(src)); + VALUE dst_fields_obj = rb_imemo_fields_clone(fields_obj); + // `dst` is a freshly allocated object, so it must not inherit `src`'s + // frozen status. Callers that need it re-freeze `dst` themselves. + shape_id_t shape_id = RBASIC_SHAPE_ID(dst_fields_obj) & ~SHAPE_ID_FL_FROZEN; + RBASIC_SET_SHAPE_ID(dst_fields_obj, shape_id); + RCLASS_WRITABLE_SET_FIELDS_OBJ(dst, dst_fields_obj); + RBASIC_SET_SHAPE_ID(dst, shape_id); } } From ea3cd7efe1f6bab78572b31774462e891ef01243 Mon Sep 17 00:00:00 2001 From: John Hawthorn Date: Thu, 24 Sep 2026 14:35:49 -0700 Subject: [PATCH 2/6] Fix reuse of MatchData assigned to $~ $~ = m = /a/.match("a").dup /b/ =~ "b" p m [Bug #22381] --- re.c | 1 + test/ruby/test_regexp.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/re.c b/re.c index 81aa79f801cf5b..01ede5b9d56ba3 100644 --- a/re.c +++ b/re.c @@ -4895,6 +4895,7 @@ match_setter(VALUE val, ID _x, VALUE *_y) { if (!NIL_P(val)) { Check_Type(val, T_MATCH); + rb_match_busy(val); } rb_backref_set(val); } diff --git a/test/ruby/test_regexp.rb b/test/ruby/test_regexp.rb index 6c7904e0c719d1..ae737a157489f6 100644 --- a/test/ruby/test_regexp.rb +++ b/test/ruby/test_regexp.rb @@ -1042,6 +1042,19 @@ def test_match_setter assert_equal("foo", $&) end + def test_match_setter_with_copy + [:dup, :clone].each do |copy| + [false, true].each do |freeze| + m = /a/.match("a").public_send(copy) + m.freeze if freeze + $~ = m + /b/ =~ "b" + assert_equal("a", m[0], "#{copy}, freeze: #{freeze}") + assert_equal("b", $&) + end + end + end + def test_match_without_regexp # create a MatchData for each assertion because the internal state may change test = proc {|&blk| "abc".sub("a", ""); blk.call($~) } From b49347c28a15c475eba951ce426564647aeeacf0 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Fri, 25 Sep 2026 14:18:38 -0400 Subject: [PATCH 3/6] [DOC] Add GC.start global option This option defaults to `true` like all other `GC.start` options. It only has effect for the default GC and when there are multiple running Ractors. If there's only a single running Ractor, the option is ignored. A global GC runs a stop-the-world full collection of all objects in all Ractors and can collect shareable objects. When `global: false` is given, it will run a ractor-local GC if there is more than 1 running Ractor. Also add `GC.stat(:global_gc_count)`. This is a useful stat and is necessary to be able to test this change. This doesn't change the behavior of `GC.start` without arguments either in a Ractor or on the main Ractor. `GC.start` with multiple Ractors would run a global GC before as well unless `full_mark: false` was given. A NEWS.md entry was added for this new `GC.start` keyword argument. NOTE: most of this commit was accidentally introduced in 60c206b683d5e55d0d8b90556c5d84070b05b3e3, but the NEWS.md changes were missing. --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index d97f66619b12ac..91c6221e1903e5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -432,6 +432,11 @@ A lot of work has gone into making Ractors more stable, performant, and usable. shareable-object growth, reclaiming dead Ractors' heaps). Allocation-heavy Ractor programs now scale like forked processes. +* `GC.start(global: false)` can be used when multiple Ractors are running to force + a Ractor-local GC. By default, `GC.start` runs a global GC (all Ractors) like + before. This can also be triggered with `GC.start(global: true)`. The `global` + keyword argument has no effect when a single Ractor is running. + Visible behavior changes: * `Ractor#value` returns the value only once; a second call raises From e1182d114ad35c52828d6ca36da590cdd1f3ee58 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 25 Sep 2026 17:14:03 +0900 Subject: [PATCH 4/6] win32/resolv: Cast method functions with RUBY_METHOD_FUNC GCC 15 in C23 mode reads `ANYARGS` as `(void)`, so the bare function pointers are rejected against Ruby headers that do not add the cast themselves, as happened with Ruby 3.1 on Windows. https://github.com/ruby/resolv/issues/92 Co-Authored-By: Claude Opus 5.5 --- ext/win32/resolv/resolv.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/win32/resolv/resolv.c b/ext/win32/resolv/resolv.c index 9150df5dc11a27..00173e5a36480f 100644 --- a/ext/win32/resolv/resolv.c +++ b/ext/win32/resolv/resolv.c @@ -247,11 +247,11 @@ InitVM_resolv(void) reg_key_class = regkey; rb_undef_alloc_func(regkey); - rb_define_private_method(singl, "get_dns_server_list", get_dns_server_list, 0); - rb_define_private_method(singl, "tcpip_params", tcpip_params_open, 0); - rb_define_method(regkey, "open", reg_open, 1); - rb_define_method(regkey, "each_key", reg_each_key, 0); - rb_define_method(regkey, "value", reg_value, 1); + rb_define_private_method(singl, "get_dns_server_list", RUBY_METHOD_FUNC(get_dns_server_list), 0); + rb_define_private_method(singl, "tcpip_params", RUBY_METHOD_FUNC(tcpip_params_open), 0); + rb_define_method(regkey, "open", RUBY_METHOD_FUNC(reg_open), 1); + rb_define_method(regkey, "each_key", RUBY_METHOD_FUNC(reg_each_key), 0); + rb_define_method(regkey, "value", RUBY_METHOD_FUNC(reg_value), 1); } void From 9a86049eb3fe3df2ad52667667f1f2d543b2f9a9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 25 Sep 2026 17:15:30 +0900 Subject: [PATCH 5/6] Fix the Win32::Resolv guard in test_win32_config.rb `Win32::Resolve` is never defined, so these tests were not run anywhere. Co-Authored-By: Claude Opus 5.5 --- test/resolv/test_win32_config.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/resolv/test_win32_config.rb b/test/resolv/test_win32_config.rb index 6167af6605d84a..ced130c9ec8970 100644 --- a/test/resolv/test_win32_config.rb +++ b/test/resolv/test_win32_config.rb @@ -3,7 +3,7 @@ require 'test/unit' require 'resolv' -if defined?(Win32::Resolve) +if defined?(Win32::Resolv) class TestWin32Config < Test::Unit::TestCase def test_get_item_property_string # Test reading a string registry value From c41b67d6b1f4041fde62533d1ee51cb1a881dd59 Mon Sep 17 00:00:00 2001 From: Nozomi Hijikata <121233810+nozomemein@users.noreply.github.com> Date: Sat, 26 Sep 2026 08:59:44 +0900 Subject: [PATCH 6/6] ZJIT: Specialize caller splats with polymorphic lengths (GH-18640) Caller splats with multiple profiled lengths currently fall back to dynamic sends, even when each length could use SendDirect. For opt_send_without_block, dispatch on profiled splat lengths within each ISEQ receiver arm and specialize argument setup for each length. Route unmatched lengths and receivers to a shared dynamic Send. Ideally, we could remove duplicated ArrayLength HIR in the CFG, but this is left for a follow-up. Explain that monomorphic profiles use guarded specialization, while length dispatch handles polymorphic and skewed-polymorphic profiles. --- zjit/src/codegen_tests.rs | 55 ++++ zjit/src/hir.rs | 132 +++++++-- zjit/src/hir/opt_tests.rs | 555 ++++++++++++++++++++++++++++++++------ 3 files changed, 640 insertions(+), 102 deletions(-) diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 83a20ceb20659c..266d1d6d91f5a9 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -7855,6 +7855,61 @@ fn test_send_caller_splat_arguments_with_block_literal() { assert_snapshot!(assert_compiles("entry([1, 2, 3])"), @"7"); } +#[test] +fn test_send_polymorphic_caller_splat_arguments() { + set_call_threshold(3); + eval(" + def test(*args) = args + def entry(args) = test(*args) + entry([1]) + entry([2, 3]) + "); + // Unprofiled lengths use the original Send without leaving compiled code. + assert_snapshot!(assert_compiles("[entry([4]), entry([5, 6]), entry([]), entry([7, 8, 9])]"), @"[[4], [5, 6], [], [7, 8, 9]]"); +} + +#[test] +fn test_send_polymorphic_receiver_with_polymorphic_caller_splat() { + set_call_threshold(5); + eval(" + class CallerSplatA + def target(*args) = args + end + class CallerSplatB + def target(*args) = args + end + class CallerSplatC + def target(*args) = args + end + def entry(recv, args) = recv.target(*args) + entry(CallerSplatA.new, [1]) + entry(CallerSplatB.new, [2, 3]) + entry(CallerSplatA.new, [4, 5]) + entry(CallerSplatB.new, [6]) + "); + // Both a new length and an unprofiled receiver use the shared original Send. + assert_snapshot!(assert_compiles(" + [entry(CallerSplatA.new, [7]), entry(CallerSplatB.new, [8, 9]), + entry(CallerSplatA.new, [1, 2, 3]), entry(CallerSplatC.new, [10, 11, 12])] + "), @"[[7], [8, 9], [1, 2, 3], [10, 11, 12]]"); +} + +#[test] +fn test_send_polymorphic_caller_splat_with_cfunc_receiver() { + set_call_threshold(5); + eval(" + class CallerSplatFetch + def fetch(*args) = args + end + def entry(recv, args) = recv.fetch(*args) + entry(CallerSplatFetch.new, [1]) + entry([10], [0]) + entry(CallerSplatFetch.new, [2, 3]) + entry([], [0, 20]) + "); + assert_snapshot!(assert_compiles("[entry(CallerSplatFetch.new, [4]), entry([10], [0]), entry([], [0, 20])]"), @"[[4], 10, 20]"); +} + #[test] fn test_send_caller_splat_length_mismatch_side_exits() { eval(" diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index b12967bd16fbd7..3af02ba4cab010 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -4160,22 +4160,76 @@ impl Function { args } - /// Select the monomorphic caller-splat length while translating the Send. - /// The selected length is attached to every receiver dispatch arm so later - /// specialization does not need to read the profile again. - fn monomorphic_caller_splat_length(&self, ci: *const rb_callinfo, state: InsnId) -> Option { - if self.policy.no_side_exits { - return None; - } - if unsafe { rb_vm_ci_flag(ci) } & VM_CALL_ARGS_SPLAT == 0 { - return None; + /// Select caller-splat lengths for a callsite. + fn caller_splat_lengths(&self, ci: *const rb_callinfo, recv: InsnId, state: InsnId, profiles: &ProfileOracle) -> Vec { + if self.policy.no_side_exits || unsafe { rb_vm_ci_flag(ci) } & VM_CALL_ARGS_SPLAT == 0 { + return vec![]; } let frame_state = self.frame_state_ref(state); - let summary = get_or_create_iseq_payload(frame_state.iseq).profile.get_splat_length_summary(frame_state.insn_idx)?; - if !summary.is_monomorphic() { - return None; + let Some(splat_length_summary) = get_or_create_iseq_payload(frame_state.iseq).profile.get_splat_length_summary(frame_state.insn_idx) else { + return vec![]; + }; + // A single observed length uses the existing guarded specialization without dispatch. + if splat_length_summary.is_monomorphic() { + return splat_length_summary.bucket(0).into_iter().collect(); + } + // Only build length dispatch for polymorphic or skewed-polymorphic profiles. + // Leave megamorphic profiles, including skewed ones, unspecialized. + if !(splat_length_summary.is_polymorphic() || splat_length_summary.is_skewed_polymorphic()) { + return vec![]; + } + + // This lookup only limits CFG expansion to ISEQ callees. type_specialize + // still checks visibility and installs the method assumptions as before. + let receiver = self.profile_summary(profiles, recv, state); + let klass = if receiver.is_monomorphic() || receiver.is_skewed_polymorphic() { + Some(receiver.bucket(0).class()) + } else { + self.type_of(recv).runtime_exact_ruby_class() + }; + let Some(klass) = klass else { return vec![] }; + let mut cme = unsafe { rb_callable_method_entry(klass, vm_ci_mid(ci)) }; + if cme.is_null() { return vec![]; } + cme = unsafe { rb_check_overloaded_cme(cme, ci) }; + while unsafe { get_cme_def_type(cme) } == VM_METHOD_TYPE_ALIAS { + cme = unsafe { rb_aliased_callable_method_entry(cme) }; + } + if unsafe { get_cme_def_type(cme) } != VM_METHOD_TYPE_ISEQ { + return vec![]; + } + splat_length_summary.buckets().iter().flatten().copied().collect() + } + + /// Split one ISEQ receiver arm by length. Misses retain the original splat + /// operand in the shared fallback; only matched arms may expand its elements. + fn dispatch_caller_splat(&mut self, mut block: BlockId, send: Insn, lengths: &[SplatLength], fallback_block: BlockId, join_block: BlockId) { + let Insn::Send { cd, ref args, state, .. } = send else { unreachable!() }; + let ci = unsafe { (*cd).ci }; + let flags = unsafe { rb_vm_ci_flag(ci) }; + let insn_idx = self.frame_state_ref(state).insn_idx() as u32; + // Unlike argument setup, this still sees trailing block/keyword-splat operands. + let trailing = usize::from(flags & VM_CALL_ARGS_BLOCKARG != 0) + usize::from(flags & VM_CALL_KW_SPLAT != 0); + let caller_args = CallerArguments::new(&args[..args.len() - trailing], ci); + let array = args[caller_args.splat_arg_idx.unwrap()]; + let actual = self.push_insn(block, Insn::ArrayLength { array }); + for (index, &length) in lengths.iter().enumerate() { + let expected = self.push_insn(block, Insn::Const { val: Const::CInt64(i64::from(length)) }); + let matches = self.push_insn(block, Insn::IsBitEqual { left: actual, right: expected }); + let matched_block = self.new_block(insn_idx); + let next_block = if index + 1 == lengths.len() { fallback_block } else { self.new_block(insn_idx) }; + self.push_insn(block, Insn::CondBranch { + val: matches, + if_true: BranchEdge { target: matched_block, args: vec![] }, + if_false: BranchEdge { target: next_block, args: vec![] }, + }); + let mut selected_send = send.clone(); + if let Insn::Send { caller_splat_length, .. } = &mut selected_send { + *caller_splat_length = Some(length); + } + let result = self.push_insn(matched_block, selected_send); + self.push_insn(matched_block, Insn::Jump(BranchEdge { target: join_block, args: vec![result] })); + block = next_block; } - summary.bucket(0) } /// Guard the caller-splat length selected for this runtime path. @@ -4185,9 +4239,8 @@ impl Function { caller_splat: CallerSplat, state: InsnId, ) { - // Recompile after enough side exits have re-profiled the original Send. Any - // second observed length makes the distribution non-monomorphic, so the next - // version keeps the dynamic Send instead of emitting the same guard again. + // Re-profile on length mismatches so recompilation can use the updated + // distribution, including additional lengths observed at this call site. let length = self.push_insn(block, Insn::ArrayLength { array: caller_splat.array }); self.push_insn(block, Insn::GuardBitEquals { val: length, @@ -4973,8 +5026,8 @@ impl Function { // Count the profile shape for every caller-splat execution; // complex_arg_pass_caller_splat separately tracks fallbacks. self.count_caller_splat_profile(block, state); - // `add_iseq_to_hir` selects caller-splat lengths before building - // receiver dispatch. A Send without a selected length stays dynamic. + // A Send without a caller-splat expansion selected by + // `add_iseq_to_hir` stays dynamic. let Some(length) = caller_splat_length else { self.count(block, Counter::complex_arg_pass_caller_splat); self.set_dynamic_send_reason(insn_id, ComplexArgPass); @@ -10223,23 +10276,28 @@ fn add_iseq_to_hir( let args = state.stack_pop_n(argc as usize)?; let recv = state.stack_pop()?; - let caller_splat_length = fun.monomorphic_caller_splat_length(call_info, exit_id); + let lengths = fun.caller_splat_lengths(call_info, recv, exit_id, &profiles); + let caller_splat_length = if lengths.len() == 1 { Some(lengths[0]) } else { None }; if let Some(summary) = fun.polymorphic_summary(&profiles, recv, exit_id) { let join_block = fun.new_block(insn_idx); let join_param = fun.push_insn(join_block, Insn::Param); + let fallback_block = fun.new_block(insn_idx); // Dedup by expected type so immediate/heap variants // under the same Ruby class can still get separate branches. - let mut seen_types = Vec::with_capacity(summary.buckets().len()); + let mut receiver_types = Vec::with_capacity(summary.buckets().len()); for &profiled_type in summary.buckets() { if profiled_type.is_empty() { break; } let expected = Type::from_profiled_type(profiled_type); - if seen_types.iter().any(|ty: &Type| ty.bit_equal(expected)) { + if receiver_types.iter().any(|ty: &Type| ty.bit_equal(expected)) { continue; } - seen_types.push(expected); + receiver_types.push(expected); + } + for (index, &expected) in receiver_types.iter().enumerate() { let iftrue_block = fun.new_block(insn_idx); - let fall_through = fun.new_block(insn_idx); + // The final receiver miss joins length misses at the shared fallback. + let fall_through = if index + 1 == receiver_types.len() { fallback_block } else { fun.new_block(insn_idx) }; fun.push_insn(block, Insn::CondBranchHasType(Box::new(CondBranchHasTypeData { val: recv, expected, @@ -10259,8 +10317,18 @@ fn add_iseq_to_hir( // exact type, and resolve_receiver_type prefers profiles over types. profiles.copy_entries_except(exit_id, snapshot, recv, fun); let refined_recv = fun.push_insn(iftrue_block, Insn::RefineType { val: recv, new_type: expected }); - let send = fun.push_insn(iftrue_block, Insn::Send { recv: refined_recv, cd, block: None, args: args.clone(), caller_splat_length, state: snapshot, reason: Uncategorized(opcode.into()) }); - fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] })); + // Set the refined type now; otherwise length selection sees Any and cannot + // identify the ISEQ callee needed for polymorphic splat dispatch. + fun.insn_types[refined_recv] = expected; + let lengths = fun.caller_splat_lengths(call_info, refined_recv, snapshot, &profiles); + let send = Insn::Send { recv: refined_recv, cd, block: None, args: args.clone(), caller_splat_length, state: snapshot, reason: Uncategorized(opcode.into()) }; + if lengths.len() > 1 { + // Receiver and length misses share the original, unexpanded Send. + fun.dispatch_caller_splat(iftrue_block, send, &lengths, fallback_block, join_block); + } else { + let send = fun.push_insn(iftrue_block, send); + fun.push_insn(iftrue_block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] })); + } } // In the fallthrough case, do a generic interpreter send and then join. let reason = SendPolymorphicFallback; @@ -10269,6 +10337,17 @@ fn add_iseq_to_hir( state.stack_push(join_param); // Continue compilation from the join block at the next instruction. block = join_block; + } else if lengths.len() > 1 { + // Without polymorphic receiver dispatch, branch only on the profiled splat lengths. + let join_block = fun.new_block(insn_idx); + let join_param = fun.push_insn(join_block, Insn::Param); + let fallback_block = fun.new_block(insn_idx); + let send = Insn::Send { recv, cd, block: None, args, caller_splat_length: None, state: exit_id, reason: Uncategorized(opcode.into()) }; + fun.dispatch_caller_splat(block, send.clone(), &lengths, fallback_block, join_block); + let send = fun.push_insn(fallback_block, send); + fun.push_insn(fallback_block, Insn::Jump(BranchEdge { target: join_block, args: vec![send] })); + state.stack_push(join_param); + block = join_block; } else { // Maybe monomorphic; handled in type_specialize let send = fun.push_insn(block, Insn::Send { recv, cd, block: None, args, caller_splat_length, state: exit_id, reason: Uncategorized(opcode.into()) }); @@ -10301,7 +10380,8 @@ fn add_iseq_to_hir( } else { None }; - let caller_splat_length = fun.monomorphic_caller_splat_length(call_info, exit_id); + let lengths = fun.caller_splat_lengths(call_info, recv, exit_id, &profiles); + let caller_splat_length = if lengths.len() == 1 { Some(lengths[0]) } else { None }; if let Some(summary) = fun.polymorphic_summary(&profiles, recv, exit_id) { let join_block = fun.new_block(insn_idx); let join_param = fun.push_insn(join_block, Insn::Param); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index d8179b45424fe3..fad027c54c22cd 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -1953,20 +1953,20 @@ mod hir_opt_tests { v11:BasicObject = LoadArg :mode@3 Jump bb3(v8, v9, v10, v11) bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:BasicObject): - CondBranchHasType v15, ObjectSubclass[class_exact:A], bb8(), bb9() - bb8(): + CondBranchHasType v15, ObjectSubclass[class_exact:A], bb9(), bb10() + bb9(): PatchPoint NoSingletonClass(A@0x1008) PatchPoint MethodRedefined(A@0x1008, foo@0x1010, cme:0x1018) v167:Fixnum[1] = Const Value(1) Jump bb7(v167) - bb9(): - CondBranchHasType v15, ObjectSubclass[class_exact:B], bb10(), bb11() bb10(): + CondBranchHasType v15, ObjectSubclass[class_exact:B], bb11(), bb8() + bb11(): PatchPoint NoSingletonClass(B@0x1040) PatchPoint MethodRedefined(B@0x1040, foo@0x1010, cme:0x1048) v170:Fixnum[2] = Const Value(2) Jump bb7(v170) - bb11(): + bb8(): v32:BasicObject = Send v15, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb7(v32) bb7(v21:BasicObject): @@ -10196,18 +10196,18 @@ mod hir_opt_tests { v27:NilClass = Const Value(nil) Jump bb5(v16, v27) bb5(v30:BasicObject, v31:Falsy): - CondBranchHasType v31, FalseClass, bb8(), bb9() - bb8(): + CondBranchHasType v31, FalseClass, bb9(), bb10() + bb9(): PatchPoint MethodRedefined(FalseClass@0x1008, !@0x1010, cme:0x1018) v55:TrueClass = Const Value(true) Jump bb7(v55) - bb9(): - CondBranchHasType v31, NilClass, bb10(), bb11() bb10(): + CondBranchHasType v31, NilClass, bb11(), bb8() + bb11(): PatchPoint MethodRedefined(NilClass@0x1040, !@0x1010, cme:0x1018) v58:TrueClass = Const Value(true) Jump bb7(v58) - bb11(): + bb8(): v46:BasicObject = Send v31, :! # SendFallbackReason: Send: polymorphic fallback Jump bb7(v46) bb7(v35:BasicObject): @@ -10987,13 +10987,13 @@ mod hir_opt_tests { Jump bb3(v6, v7) bb3(v9:HeapBasicObject, v10:BasicObject): v17:Fixnum[5] = Const Value(5) - CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() - bb5(): + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb6(), bb5() + bb6(): v23:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint MethodRedefined(C@0x1008, foo=@0x1010, cme:0x1018) SetIvar v23, :@foo, v17 Jump bb4(v17) - bb6(): + bb5(): v26:BasicObject = Send v10, :foo=, v17 # SendFallbackReason: Send: polymorphic fallback Jump bb4(v26) bb4(v20:BasicObject): @@ -11652,14 +11652,14 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() - bb5(): + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb6(), bb5() + bb6(): v18:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) v30:BasicObject = GetIvar v18, :@foo Jump bb4(v30) - bb6(): + bb5(): v21:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb4(v21) bb4(v15:BasicObject): @@ -16221,8 +16221,8 @@ mod hir_opt_tests { Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): v19:ArrayExact = ToArray v13 - CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatA], bb5(), bb6() - bb5(): + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatA], bb6(), bb7() + bb6(): v24:ObjectSubclass[class_exact:CallerSplatA] = RefineType v12, ObjectSubclass[class_exact:CallerSplatA] PatchPoint NoSingletonClass(CallerSplatA@0x1008) v40:CInt64 = ArrayLength v19 @@ -16237,9 +16237,9 @@ mod hir_opt_tests { CheckInterrupts PopInlineFrame Jump bb4(v47) - bb6(): - CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatB], bb7(), bb8() bb7(): + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatB], bb8(), bb5() + bb8(): v29:ObjectSubclass[class_exact:CallerSplatB] = RefineType v12, ObjectSubclass[class_exact:CallerSplatB] PatchPoint NoSingletonClass(CallerSplatB@0x1060) v51:CInt64 = ArrayLength v19 @@ -16254,7 +16254,7 @@ mod hir_opt_tests { CheckInterrupts PopInlineFrame Jump bb4(v58) - bb8(): + bb5(): v32:BasicObject = Send v12, :target, v19 # SendFallbackReason: Send: polymorphic fallback Jump bb4(v32) bb4(v21:BasicObject): @@ -16577,7 +16577,7 @@ mod hir_opt_tests { } #[test] - fn dont_specialize_call_to_iseq_with_polymorphic_caller_splat() { + fn specialize_call_to_iseq_with_polymorphic_caller_splat() { enable_zjit_stats(); set_call_threshold(3); eval(" @@ -16608,9 +16608,62 @@ mod hir_opt_tests { IncrCounter zjit_insn_count v21:ArrayExact = ToArray v12 IncrCounter zjit_insn_count + v25:CInt64 = ArrayLength v21 + v26:CInt64[2] = Const CInt64(2) + v27:CBool = IsBitEqual v25, v26 + CondBranch v27, bb6(), bb7() + bb6(): + IncrCounter caller_splat_profile_polymorphic + v45:CInt64 = ArrayLength v21 + v46:CInt64[2] = GuardBitEquals v45, CInt64(2) recompile + v47:CInt64 = CCall v21, :rb_jit_ruby2_keywords_splat_p@0x1001 + v48:CInt64[0] = GuardBitEquals v47, CInt64(0) + IncrCounter caller_splat_optimized + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v51:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v11, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + v52:CInt64[0] = Const CInt64(0) + v53:BasicObject = ArrayAref v21, v52 + v54:CInt64[1] = Const CInt64(1) + v55:BasicObject = ArrayAref v21, v54 + v56:ArrayExact = NewArray v53, v55 + PushInlineFrame :foo, v51 (0x1040), num_args=1 + IncrCounter inline_iseq_optimized_send_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + CheckInterrupts + PopInlineFrame + Jump bb4(v56) + bb7(): + v31:CInt64[1] = Const CInt64(1) + v32:CBool = IsBitEqual v25, v31 + CondBranch v32, bb8(), bb5() + bb8(): + IncrCounter caller_splat_profile_polymorphic + v60:CInt64 = ArrayLength v21 + v61:CInt64[1] = GuardBitEquals v60, CInt64(1) recompile + v62:CInt64 = CCall v21, :rb_jit_ruby2_keywords_splat_p@0x1001 + v63:CInt64[0] = GuardBitEquals v62, CInt64(0) + IncrCounter caller_splat_optimized + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v66:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v11, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + v67:CInt64[0] = Const CInt64(0) + v68:BasicObject = ArrayAref v21, v67 + v69:ArrayExact = NewArray v68 + PushInlineFrame :foo, v66 (0x1040), num_args=1 + IncrCounter inline_iseq_optimized_send_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + CheckInterrupts + PopInlineFrame + Jump bb4(v69) + bb5(): IncrCounter caller_splat_profile_polymorphic IncrCounter complex_arg_pass_caller_splat - v24:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Complex argument passing + IncrCounter caller_splat_profile_polymorphic + IncrCounter complex_arg_pass_caller_splat + v36:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Complex argument passing + Jump bb4(v36) + bb4(v24:BasicObject): IncrCounter zjit_insn_count CheckInterrupts Return v24 @@ -16618,7 +16671,302 @@ mod hir_opt_tests { } #[test] - fn dont_repeat_caller_splat_length_guard_for_skewed_polymorphic_profile() { + fn specialize_polymorphic_receiver_with_polymorphic_caller_splat() { + set_call_threshold(5); + eval(" + class CallerSplatA + def target(*args) = args + end + class CallerSplatB + def target(*args) = args + end + def test(recv, args) = recv.target(*args) + test(CallerSplatA.new, [1]) + test(CallerSplatB.new, [2, 3]) + test(CallerSplatA.new, [4, 5]) + test(CallerSplatB.new, [6]) + test(CallerSplatA.new, [7]) + "); + assert_snapshot!(hir_string("test"), @" + fn test@:8: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :recv@0x1000 + v4:BasicObject = LoadField v2, :args@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :recv@1 + v9:BasicObject = LoadArg :args@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + v19:ArrayExact = ToArray v13 + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatB], bb6(), bb7() + bb6(): + v24:ObjectSubclass[class_exact:CallerSplatB] = RefineType v12, ObjectSubclass[class_exact:CallerSplatB] + v25:CInt64 = ArrayLength v19 + v26:CInt64[1] = Const CInt64(1) + v27:CBool = IsBitEqual v25, v26 + CondBranch v27, bb8(), bb9() + bb8(): + PatchPoint NoSingletonClass(CallerSplatB@0x1008) + v58:CInt64 = ArrayLength v19 + v59:CInt64[1] = GuardBitEquals v58, CInt64(1) recompile + v60:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v61:CInt64[0] = GuardBitEquals v60, CInt64(0) + PatchPoint MethodRedefined(CallerSplatB@0x1008, target@0x1011, cme:0x1018) + v63:CInt64[0] = Const CInt64(0) + v64:BasicObject = ArrayAref v19, v63 + v65:ArrayExact = NewArray v64 + PushInlineFrame :target, v24 (0x1040), num_args=1 + CheckInterrupts + PopInlineFrame + Jump bb4(v65) + bb9(): + v31:CInt64[2] = Const CInt64(2) + v32:CBool = IsBitEqual v25, v31 + CondBranch v32, bb10(), bb5() + bb10(): + PatchPoint NoSingletonClass(CallerSplatB@0x1008) + v69:CInt64 = ArrayLength v19 + v70:CInt64[2] = GuardBitEquals v69, CInt64(2) recompile + v71:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v72:CInt64[0] = GuardBitEquals v71, CInt64(0) + PatchPoint MethodRedefined(CallerSplatB@0x1008, target@0x1011, cme:0x1018) + v74:CInt64[0] = Const CInt64(0) + v75:BasicObject = ArrayAref v19, v74 + v76:CInt64[1] = Const CInt64(1) + v77:BasicObject = ArrayAref v19, v76 + v78:ArrayExact = NewArray v75, v77 + PushInlineFrame :target, v24 (0x1040), num_args=1 + CheckInterrupts + PopInlineFrame + Jump bb4(v78) + bb7(): + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatA], bb11(), bb5() + bb11(): + v38:ObjectSubclass[class_exact:CallerSplatA] = RefineType v12, ObjectSubclass[class_exact:CallerSplatA] + v39:CInt64 = ArrayLength v19 + v40:CInt64[1] = Const CInt64(1) + v41:CBool = IsBitEqual v39, v40 + CondBranch v41, bb12(), bb13() + bb12(): + PatchPoint NoSingletonClass(CallerSplatA@0x1060) + v82:CInt64 = ArrayLength v19 + v83:CInt64[1] = GuardBitEquals v82, CInt64(1) recompile + v84:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v85:CInt64[0] = GuardBitEquals v84, CInt64(0) + PatchPoint MethodRedefined(CallerSplatA@0x1060, target@0x1011, cme:0x1068) + v87:CInt64[0] = Const CInt64(0) + v88:BasicObject = ArrayAref v19, v87 + v89:ArrayExact = NewArray v88 + PushInlineFrame :target, v38 (0x1090), num_args=1 + CheckInterrupts + PopInlineFrame + Jump bb4(v89) + bb13(): + v45:CInt64[2] = Const CInt64(2) + v46:CBool = IsBitEqual v39, v45 + CondBranch v46, bb14(), bb5() + bb14(): + PatchPoint NoSingletonClass(CallerSplatA@0x1060) + v93:CInt64 = ArrayLength v19 + v94:CInt64[2] = GuardBitEquals v93, CInt64(2) recompile + v95:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v96:CInt64[0] = GuardBitEquals v95, CInt64(0) + PatchPoint MethodRedefined(CallerSplatA@0x1060, target@0x1011, cme:0x1068) + v98:CInt64[0] = Const CInt64(0) + v99:BasicObject = ArrayAref v19, v98 + v100:CInt64[1] = Const CInt64(1) + v101:BasicObject = ArrayAref v19, v100 + v102:ArrayExact = NewArray v99, v101 + PushInlineFrame :target, v38 (0x1090), num_args=1 + CheckInterrupts + PopInlineFrame + Jump bb4(v102) + bb5(): + v50:BasicObject = Send v12, :target, v19 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v50) + bb4(v21:BasicObject): + CheckInterrupts + Return v21 + "); + } + + #[test] + fn specialize_polymorphic_caller_splat_only_for_iseq_receiver() { + set_call_threshold(5); + eval(" + class CallerSplatFetch + def fetch(*args) = args + end + def test(recv, args) = recv.fetch(*args) + test(CallerSplatFetch.new, [1]) + test([10], [0]) + test(CallerSplatFetch.new, [2, 3]) + test([], [0, 20]) + test(CallerSplatFetch.new, [4]) + "); + assert_snapshot!(hir_string("test"), @" + fn test@:5: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :recv@0x1000 + v4:BasicObject = LoadField v2, :args@0x1001 + Jump bb3(v1, v3, v4) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :recv@1 + v9:BasicObject = LoadArg :args@2 + Jump bb3(v7, v8, v9) + bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): + v19:ArrayExact = ToArray v13 + CondBranchHasType v12, ArrayExact, bb6(), bb7() + bb6(): + v24:ArrayExact = RefineType v12, ArrayExact + v25:BasicObject = Send v24, :fetch, v19 # SendFallbackReason: Complex argument passing + Jump bb4(v25) + bb7(): + CondBranchHasType v12, ObjectSubclass[class_exact:CallerSplatFetch], bb8(), bb5() + bb8(): + v29:ObjectSubclass[class_exact:CallerSplatFetch] = RefineType v12, ObjectSubclass[class_exact:CallerSplatFetch] + v30:CInt64 = ArrayLength v19 + v31:CInt64[2] = Const CInt64(2) + v32:CBool = IsBitEqual v30, v31 + CondBranch v32, bb9(), bb10() + bb9(): + PatchPoint NoSingletonClass(CallerSplatFetch@0x1008) + v49:CInt64 = ArrayLength v19 + v50:CInt64[2] = GuardBitEquals v49, CInt64(2) recompile + v51:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v52:CInt64[0] = GuardBitEquals v51, CInt64(0) + PatchPoint MethodRedefined(CallerSplatFetch@0x1008, fetch@0x1011, cme:0x1018) + v54:CInt64[0] = Const CInt64(0) + v55:BasicObject = ArrayAref v19, v54 + v56:CInt64[1] = Const CInt64(1) + v57:BasicObject = ArrayAref v19, v56 + v58:ArrayExact = NewArray v55, v57 + PushInlineFrame :fetch, v29 (0x1040), num_args=1 + CheckInterrupts + PopInlineFrame + Jump bb4(v58) + bb10(): + v36:CInt64[1] = Const CInt64(1) + v37:CBool = IsBitEqual v30, v36 + CondBranch v37, bb11(), bb5() + bb11(): + PatchPoint NoSingletonClass(CallerSplatFetch@0x1008) + v62:CInt64 = ArrayLength v19 + v63:CInt64[1] = GuardBitEquals v62, CInt64(1) recompile + v64:CInt64 = CCall v19, :rb_jit_ruby2_keywords_splat_p@0x1010 + v65:CInt64[0] = GuardBitEquals v64, CInt64(0) + PatchPoint MethodRedefined(CallerSplatFetch@0x1008, fetch@0x1011, cme:0x1018) + v67:CInt64[0] = Const CInt64(0) + v68:BasicObject = ArrayAref v19, v67 + v69:ArrayExact = NewArray v68 + PushInlineFrame :fetch, v29 (0x1040), num_args=1 + CheckInterrupts + PopInlineFrame + Jump bb4(v69) + bb5(): + v41:BasicObject = Send v12, :fetch, v19 # SendFallbackReason: Send: polymorphic fallback + Jump bb4(v41) + bb4(v21:BasicObject): + CheckInterrupts + Return v21 + "); + } + + #[test] + fn dont_specialize_polymorphic_caller_splat_length_with_argc_mismatch() { + enable_zjit_stats(); + set_call_threshold(3); + eval(" + def foo(a) = a + def test(args) = foo(*args) + test([1]) + begin + test([1, 2]) + rescue ArgumentError + end + test([3]) + "); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :args@0x1000 + IncrCounterPtr + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :args@1 + IncrCounterPtr + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + v21:ArrayExact = ToArray v12 + IncrCounter zjit_insn_count + v25:CInt64 = ArrayLength v21 + v26:CInt64[2] = Const CInt64(2) + v27:CBool = IsBitEqual v25, v26 + CondBranch v27, bb6(), bb7() + bb6(): + IncrCounter caller_splat_profile_polymorphic + IncrCounter send_direct_fallback_context_send + IncrCounter caller_splat_profile_polymorphic + IncrCounter send_direct_fallback_context_send + v29:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Argument count does not match parameter count + Jump bb4(v29) + bb7(): + v31:CInt64[1] = Const CInt64(1) + v32:CBool = IsBitEqual v25, v31 + CondBranch v32, bb8(), bb5() + bb8(): + IncrCounter caller_splat_profile_polymorphic + v47:CInt64 = ArrayLength v21 + v48:CInt64[1] = GuardBitEquals v47, CInt64(1) recompile + v49:CInt64 = CCall v21, :rb_jit_ruby2_keywords_splat_p@0x1001 + v50:CInt64[0] = GuardBitEquals v49, CInt64(0) + IncrCounter caller_splat_optimized + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v53:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v11, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + v54:CInt64[0] = Const CInt64(0) + v55:BasicObject = ArrayAref v21, v54 + PushInlineFrame :foo, v53 (0x1040), num_args=1 + IncrCounter inline_iseq_optimized_send_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + CheckInterrupts + PopInlineFrame + Jump bb4(v55) + bb5(): + IncrCounter caller_splat_profile_polymorphic + IncrCounter complex_arg_pass_caller_splat + IncrCounter caller_splat_profile_polymorphic + IncrCounter complex_arg_pass_caller_splat + v36:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Complex argument passing + Jump bb4(v36) + bb4(v24:BasicObject): + IncrCounter zjit_insn_count + CheckInterrupts + Return v24 + "); + } + + #[test] + fn specialize_caller_splat_after_recompiling_with_skewed_polymorphic_profile() { enable_zjit_stats(); set_call_threshold(5); set_max_versions(4); @@ -16642,8 +16990,8 @@ mod hir_opt_tests { // the monomorphic version for recompilation. eval("test([1, 2])"); - // The next version must keep the dynamic Send because the accumulated - // length profile is skewed polymorphic rather than monomorphic. + // The next version dispatches both accumulated lengths, rather than + // repeating the monomorphic guard that rejected the second length. assert_snapshot!(hir_string("test"), @" fn test@:5: bb1(): @@ -16665,9 +17013,62 @@ mod hir_opt_tests { IncrCounter zjit_insn_count v21:ArrayExact = ToArray v12 IncrCounter zjit_insn_count + v25:CInt64 = ArrayLength v21 + v26:CInt64[1] = Const CInt64(1) + v27:CBool = IsBitEqual v25, v26 + CondBranch v27, bb6(), bb7() + bb6(): + IncrCounter caller_splat_profile_skewed_polymorphic + v45:CInt64 = ArrayLength v21 + v46:CInt64[1] = GuardBitEquals v45, CInt64(1) recompile + v47:CInt64 = CCall v21, :rb_jit_ruby2_keywords_splat_p@0x1001 + v48:CInt64[0] = GuardBitEquals v47, CInt64(0) + IncrCounter caller_splat_optimized + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v51:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v11, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + v52:CInt64[0] = Const CInt64(0) + v53:BasicObject = ArrayAref v21, v52 + v54:ArrayExact = NewArray v53 + PushInlineFrame :foo, v51 (0x1040), num_args=1 + IncrCounter inline_iseq_optimized_send_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + CheckInterrupts + PopInlineFrame + Jump bb4(v54) + bb7(): + v31:CInt64[2] = Const CInt64(2) + v32:CBool = IsBitEqual v25, v31 + CondBranch v32, bb8(), bb5() + bb8(): + IncrCounter caller_splat_profile_skewed_polymorphic + v58:CInt64 = ArrayLength v21 + v59:CInt64[2] = GuardBitEquals v58, CInt64(2) recompile + v60:CInt64 = CCall v21, :rb_jit_ruby2_keywords_splat_p@0x1001 + v61:CInt64[0] = GuardBitEquals v60, CInt64(0) + IncrCounter caller_splat_optimized + PatchPoint MethodRedefined(Object@0x1008, foo@0x1010, cme:0x1018) + v64:ObjectSubclass[class_exact*:Object@VALUE(0x1008)] = GuardType v11, ObjectSubclass[class_exact*:Object@VALUE(0x1008)] recompile + v65:CInt64[0] = Const CInt64(0) + v66:BasicObject = ArrayAref v21, v65 + v67:CInt64[1] = Const CInt64(1) + v68:BasicObject = ArrayAref v21, v67 + v69:ArrayExact = NewArray v66, v68 + PushInlineFrame :foo, v64 (0x1040), num_args=1 + IncrCounter inline_iseq_optimized_send_count + IncrCounter zjit_insn_count + IncrCounter zjit_insn_count + CheckInterrupts + PopInlineFrame + Jump bb4(v69) + bb5(): IncrCounter caller_splat_profile_skewed_polymorphic IncrCounter complex_arg_pass_caller_splat - v24:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Complex argument passing + IncrCounter caller_splat_profile_skewed_polymorphic + IncrCounter complex_arg_pass_caller_splat + v36:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Complex argument passing + Jump bb4(v36) + bb4(v24:BasicObject): IncrCounter zjit_insn_count CheckInterrupts Return v24 @@ -16678,6 +17079,8 @@ mod hir_opt_tests { fn dont_specialize_call_to_iseq_with_caller_splat_on_final_version() { enable_zjit_stats(); set_max_versions(2); + // Invalidate test itself rather than an inlined copy in the driving loop. + set_inline_threshold(0); eval(" def foo(*args) = args def test(args) = foo(*args) @@ -16708,7 +17111,7 @@ mod hir_opt_tests { IncrCounter zjit_insn_count v21:ArrayExact = ToArray v12 IncrCounter zjit_insn_count - IncrCounter caller_splat_profile_polymorphic + IncrCounter caller_splat_profile_skewed_polymorphic IncrCounter complex_arg_pass_caller_splat v24:BasicObject = Send v11, :foo, v21 # SendFallbackReason: Complex argument passing IncrCounter zjit_insn_count @@ -18023,19 +18426,19 @@ mod hir_opt_tests { bb3(v9:BasicObject, v10:BasicObject): PatchPoint StableConstantNames(0x1008, String) v16:ClassSubclass[String@0x1010] = Const Value(VALUE(0x1010)) - CondBranchHasType v10, Fixnum, bb5(), bb6() - bb5(): + CondBranchHasType v10, Fixnum, bb6(), bb7() + bb6(): PatchPoint MethodRedefined(Integer@0x1018, is_a?@0x1020, cme:0x1028) v43:FalseClass = Const Value(false) Jump bb4(v43) - bb6(): - CondBranchHasType v10, StringExact, bb7(), bb8() bb7(): + CondBranchHasType v10, StringExact, bb8(), bb5() + bb8(): PatchPoint NoSingletonClass(String@0x1010) PatchPoint MethodRedefined(String@0x1010, is_a?@0x1020, cme:0x1028) v44:TrueClass = Const Value(true) Jump bb4(v44) - bb8(): + bb5(): v29:BasicObject = Send v10, :is_a?, v16 # SendFallbackReason: Send: polymorphic fallback Jump bb4(v29) bb4(v18:BasicObject): @@ -20912,20 +21315,20 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() - bb5(): + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb6(), bb7() + bb6(): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) v40:Fixnum[3] = Const Value(3) Jump bb4(v40) - bb6(): - CondBranchHasType v10, ObjectSubclass[class_exact:D], bb7(), bb8() bb7(): + CondBranchHasType v10, ObjectSubclass[class_exact:D], bb8(), bb5() + bb8(): PatchPoint NoSingletonClass(D@0x1040) PatchPoint MethodRedefined(D@0x1040, foo@0x1010, cme:0x1048) v43:Fixnum[4] = Const Value(4) Jump bb4(v43) - bb8(): + bb5(): v26:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb4(v26) bb4(v15:BasicObject): @@ -20964,19 +21367,19 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() - bb5(): + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb6(), bb7() + bb6(): v18:ObjectSubclass[class_exact:C] = RefineType v10, ObjectSubclass[class_exact:C] PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, itself@0x1010, cme:0x1018) Jump bb4(v18) - bb6(): - CondBranchHasType v10, Fixnum, bb7(), bb8() bb7(): + CondBranchHasType v10, Fixnum, bb8(), bb5() + bb8(): v23:Fixnum = RefineType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1040, itself@0x1010, cme:0x1018) Jump bb4(v23) - bb8(): + bb5(): v26:BasicObject = Send v10, :itself # SendFallbackReason: Send: polymorphic fallback Jump bb4(v26) bb4(v15:BasicObject): @@ -21058,8 +21461,8 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :i@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - CondBranchHasType v12, ArrayExact, bb5(), bb6() - bb5(): + CondBranchHasType v12, ArrayExact, bb6(), bb7() + bb6(): v23:ArrayExact = RefineType v12, ArrayExact PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) @@ -21072,15 +21475,15 @@ mod hir_opt_tests { v47:CInt64 = GuardGreaterEq v45, v46 v48:BasicObject = ArrayAref v23, v47 Jump bb4(v48) - bb6(): - CondBranchHasType v12, HashExact, bb7(), bb8() bb7(): + CondBranchHasType v12, HashExact, bb8(), bb5() + bb8(): v28:HashExact = RefineType v12, HashExact PatchPoint NoSingletonClass(Hash@0x1040) PatchPoint MethodRedefined(Hash@0x1040, []@0x1010, cme:0x1048) v52:BasicObject = HashAref v28, v13 Jump bb4(v52) - bb8(): + bb5(): v31:BasicObject = Send v12, :[], v13 # SendFallbackReason: Send: polymorphic fallback Jump bb4(v31) bb4(v20:BasicObject): @@ -21121,20 +21524,20 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, Fixnum, bb5(), bb6() - bb5(): + CondBranchHasType v10, Fixnum, bb6(), bb7() + bb6(): v18:Fixnum = RefineType v10, Fixnum PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) v35:StringExact = CCallVariadic v18, :Integer#to_s@0x1040 Jump bb4(v35) - bb6(): - CondBranchHasType v10, Bignum, bb7(), bb8() bb7(): + CondBranchHasType v10, Bignum, bb8(), bb5() + bb8(): v23:Bignum = RefineType v10, Bignum PatchPoint MethodRedefined(Integer@0x1008, to_s@0x1010, cme:0x1018) v38:StringExact = CCallVariadic v23, :Integer#to_s@0x1040 Jump bb4(v38) - bb8(): + bb5(): v26:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback Jump bb4(v26) bb4(v15:BasicObject): @@ -21172,20 +21575,20 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, Flonum, bb5(), bb6() - bb5(): + CondBranchHasType v10, Flonum, bb6(), bb7() + bb6(): v18:Flonum = RefineType v10, Flonum PatchPoint MethodRedefined(Float@0x1008, to_s@0x1010, cme:0x1018) v35:BasicObject = CCallWithFrame v18, :Float#to_s@0x1040 Jump bb4(v35) - bb6(): - CondBranchHasType v10, HeapFloat, bb7(), bb8() bb7(): + CondBranchHasType v10, HeapFloat, bb8(), bb5() + bb8(): v23:HeapFloat = RefineType v10, HeapFloat PatchPoint MethodRedefined(Float@0x1008, to_s@0x1010, cme:0x1018) v38:BasicObject = CCallWithFrame v23, :Float#to_s@0x1040 Jump bb4(v38) - bb8(): + bb5(): v26:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback Jump bb4(v26) bb4(v15:BasicObject): @@ -21223,20 +21626,20 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :x@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, StaticSymbol, bb5(), bb6() - bb5(): + CondBranchHasType v10, StaticSymbol, bb6(), bb7() + bb6(): v18:StaticSymbol = RefineType v10, StaticSymbol PatchPoint MethodRedefined(Symbol@0x1008, to_s@0x1010, cme:0x1018) v34:StringExact = InvokeBuiltin leaf , v18 Jump bb4(v34) - bb6(): - CondBranchHasType v10, DynamicSymbol, bb7(), bb8() bb7(): + CondBranchHasType v10, DynamicSymbol, bb8(), bb5() + bb8(): v23:DynamicSymbol = RefineType v10, DynamicSymbol PatchPoint MethodRedefined(Symbol@0x1008, to_s@0x1010, cme:0x1018) v36:StringExact = InvokeBuiltin leaf , v23 Jump bb4(v36) - bb8(): + bb5(): v26:BasicObject = Send v10, :to_s # SendFallbackReason: Send: polymorphic fallback Jump bb4(v26) bb4(v15:BasicObject): @@ -21278,13 +21681,13 @@ mod hir_opt_tests { v7:BasicObject = LoadArg :o@1 Jump bb3(v6, v7) bb3(v9:BasicObject, v10:BasicObject): - CondBranchHasType v10, ObjectSubclass[class_exact:C], bb5(), bb6() - bb5(): + CondBranchHasType v10, ObjectSubclass[class_exact:C], bb6(), bb5() + bb6(): PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, foo@0x1010, cme:0x1018) v30:Fixnum[3] = Const Value(3) Jump bb4(v30) - bb6(): + bb5(): v21:BasicObject = Send v10, :foo # SendFallbackReason: Send: polymorphic fallback Jump bb4(v21) bb4(v15:BasicObject): @@ -22795,21 +23198,21 @@ mod hir_opt_tests { v9:BasicObject = LoadArg :b@2 Jump bb3(v7, v8, v9) bb3(v11:BasicObject, v12:BasicObject, v13:BasicObject): - CondBranchHasType v12, HeapFloat, bb5(), bb6() - bb5(): + CondBranchHasType v12, HeapFloat, bb6(), bb7() + bb6(): v23:HeapFloat = RefineType v12, HeapFloat PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) v40:BasicObject = CCallWithFrame v23, :Float#*@0x1040, v13 Jump bb4(v40) - bb6(): - CondBranchHasType v12, Flonum, bb7(), bb8() bb7(): + CondBranchHasType v12, Flonum, bb8(), bb5() + bb8(): v28:Flonum = RefineType v12, Flonum PatchPoint MethodRedefined(Float@0x1008, *@0x1010, cme:0x1018) v43:Flonum = GuardType v13, Flonum recompile v44:Float = FloatMul v28, v43 Jump bb4(v44) - bb8(): + bb5(): v31:BasicObject = Send v12, :*, v13 # SendFallbackReason: Send: polymorphic fallback Jump bb4(v31) bb4(v20:BasicObject): @@ -23109,20 +23512,20 @@ mod hir_opt_tests { bb4(): PatchPoint NoEPEscape(f) v43:Fixnum[1] = Const Value(1) - CondBranchHasType v12, Fixnum, bb10(), bb11() - bb10(): + CondBranchHasType v12, Fixnum, bb11(), bb12() + bb11(): v49:Fixnum = RefineType v12, Fixnum PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) v82:Fixnum = FixnumAdd v49, v43 Jump bb9(v82) - bb11(): - CondBranchHasType v12, Flonum, bb12(), bb13() bb12(): + CondBranchHasType v12, Flonum, bb13(), bb10() + bb13(): v54:Flonum = RefineType v12, Flonum PatchPoint MethodRedefined(Float@0x1040, +@0x1010, cme:0x1048) v85:Float = FloatAdd v54, v43 Jump bb9(v85) - bb13(): + bb10(): PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) v88:Fixnum = GuardType v12, Fixnum recompile v89:Fixnum = FixnumAdd v88, v43