summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRandy Stauner <randy@r4s6.net>2026-01-20 23:19:34 -0700
committerGitHub <noreply@github.com>2026-01-21 01:19:34 -0500
commit6f1453dc083caecf32c1c7f55491c4ac6ddd7a62 (patch)
tree533803112d223a08e3fe4690123f033bd16d5479
parent01984fa80e8b8de0c0b1b726101ffadece5ea5ca (diff)
ZJIT: Support optional keyword arguments in direct send (#15873)
This fills in constants when unspecified optional keyword args have static default values. For complex defaults we calculate the kw_bits and utilize the checkkeyword logic we already had. The following benchmarks used to register param_kw_opt. Some of them (like graphql*) just trade that for some other complexity, or "too_many_args_for_lir". Notable improvements include activerecord where the previous param_kw_opt count has a corresponding drop in complex args and dynamic_send_count and a nearly equal rise in optimized_send_count. The gains are similar but not as complete in hexapdf, liquid-render, lobsters, railsbench, shipit. | Benchmark | param_kw_opt | Δ one_or_more_complex | Δ too_many_args | Δ dynamic_send | Δ optimized_send | |-----------|-------------:|----------------------:|----------------:|---------------:|-----------------:| | activerecord | 6,307,141 | -6,253,823 | +4,084 | -6,306,223 | +6,279,766 | | blurhash | 21 | -21 | +0 | -23 | +20 | | chunky-png | 813,604 | -813,604 | +0 | -813,616 | +813,556 | | erubi-rails | 1,590,395 | -590,274 | +35,578 | -552,914 | +550,826 | | fluentd | 4,906 | -4,854 | +21 | -5,745 | +5,080 | | graphql | 1,610,439 | -1,610,432 | +1,605,751 | -4,688 | +4,628 | | graphql-native | 16,332,386 | -16,332,375 | +16,309,681 | -22,701 | +22,638 | | hexapdf | 9,165,465 | -9,124,509 | +203,754 | -8,920,727 | +8,839,295 | | liquid-compile | 14,817 | -14,792 | +0 | -14,705 | +15,045 | | liquid-render | 3,994,905 | -3,994,901 | +0 | -3,994,868 | +3,020,779 | | lobsters | 2,467,510 | -2,297,298 | +205,610 | -2,216,583 | +1,694,092 | | protoboeuf | 11,521 | -11,521 | +0 | -11,523 | +11,520 | | psych-load | 77,612 | -77,609 | +29,942 | -77,613 | -12,242 | | rack | 2,743 | -2,742 | +0 | -2,750 | +2,668 | | railsbench | 3,579,778 | -2,517,615 | +432,575 | -2,084,480 | +1,882,928 | | ruby-lsp | 287,171 | -379,716 | +37 | -409,368 | -267,248 | | rubyboy | 5,993,004 | -5,993,003 | +0 | -5,993,006 | +5,992,993 | | sequel | 182,652 | -182,631 | +0 | -182,563 | +122,687 | | shipit | 3,289,456 | -2,778,419 | +306,867 | -3,201,395 | +1,068,505 | | tinygql | 2,732 | -2,732 | +1 | -2,734 | +2,729 |
-rw-r--r--test/ruby/test_zjit.rb239
-rw-r--r--zjit/src/codegen.rs19
-rw-r--r--zjit/src/hir.rs259
-rw-r--r--zjit/src/hir/opt_tests.rs169
-rw-r--r--zjit/src/hir/tests.rs36
-rw-r--r--zjit/src/stats.rs5
6 files changed, 589 insertions, 138 deletions
diff --git a/test/ruby/test_zjit.rb b/test/ruby/test_zjit.rb
index 1cc43be971..e347986abc 100644
--- a/test/ruby/test_zjit.rb
+++ b/test/ruby/test_zjit.rb
@@ -833,6 +833,61 @@ class TestZJIT < Test::Unit::TestCase
}, call_threshold: 2
end
+ def test_pos_optional_with_maybe_too_many_args
+ assert_compiles '[[1, 2, 3, 4, 5, 6], [10, 20, 30, 4, 5, 6], [10, 20, 30, 40, 50, 60]]', %q{
+ def target(a = 1, b = 2, c = 3, d = 4, e = 5, f:) = [a, b, c, d, e, f]
+ def test = [target(f: 6), target(10, 20, 30, f: 6), target(10, 20, 30, 40, 50, f: 60)]
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_kwarg_partial_optional
+ assert_compiles '[[1, 2, 3], [1, 20, 3], [10, 2, 30]]', %q{
+ def test(a: 1, b: 2, c: 3) = [a, b, c]
+ def entry = [test, test(b: 20), test(c: 30, a: 10)]
+ entry
+ entry
+ }, call_threshold: 2
+ end
+
+ def test_send_kwarg_optional_a_lot
+ assert_compiles '[[1, 2, 3, 4, 5, 6], [1, 2, 3, 7, 8, 9], [2, 4, 6, 8, 10, 12]]', %q{
+ def test(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6) = [a, b, c, d, e, f]
+ def entry = [test, test(d: 7, f: 9, e: 8), test(f: 12, e: 10, d: 8, c: 6, b: 4, a: 2)]
+ entry
+ entry
+ }, call_threshold: 2
+ end
+
+ def test_send_kwarg_non_constant_default
+ assert_compiles '[[1, 2], [10, 2]]', %q{
+ def make_default = 2
+ def test(a: 1, b: make_default) = [a, b]
+ def entry = [test, test(a: 10)]
+ entry
+ entry
+ }, call_threshold: 2
+ end
+
+ def test_send_kwarg_optional_static_with_side_exit
+ # verify frame reconstruction with synthesized keyword defaults is correct
+ assert_compiles '[10, 2, 10]', %q{
+ def callee(a: 1, b: 2)
+ # use binding to force side-exit
+ x = binding.local_variable_get(:a)
+ [a, b, x]
+ end
+
+ def entry
+ callee(a: 10) # b should get default value
+ end
+
+ entry
+ entry
+ }, call_threshold: 2
+ end
+
def test_send_all_arg_types
assert_compiles '[:req, :opt, :post, :kwr, :kwo, true]', %q{
def test(a, b = :opt, c, d:, e: :kwo) = [a, b, c, d, e, block_given?]
@@ -1388,6 +1443,190 @@ class TestZJIT < Test::Unit::TestCase
}, call_threshold: 2
end
+ def test_invokesuper_with_optional_keyword_args
+ assert_compiles '[1, 2, 3]', %q{
+ class Parent
+ def foo(a, b: 2, c: 3) = [a, b, c]
+ end
+
+ class Child < Parent
+ def foo(a) = super(a)
+ end
+
+ def test = Child.new.foo(1)
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_with_non_constant_keyword_default
+ assert_compiles '[[2, 4, 16], [10, 4, 16], [2, 20, 16], [2, 4, 30], [10, 20, 30]]', %q{
+ def dbl(x = 1) = x * 2
+
+ def foo(a: dbl, b: dbl(2), c: dbl(2 ** 3))
+ [a, b, c]
+ end
+
+ def test
+ [
+ foo,
+ foo(a: 10),
+ foo(b: 20),
+ foo(c: 30),
+ foo(a: 10, b: 20, c: 30)
+ ]
+ end
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_with_non_constant_keyword_default_not_evaluated_when_provided
+ assert_compiles '[1, 2, 3]', %q{
+ def foo(a: raise, b: raise, c: raise)
+ [a, b, c]
+ end
+
+ def test
+ foo(a: 1, b: 2, c: 3)
+ end
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_with_non_constant_keyword_default_evaluated_when_not_provided
+ assert_compiles '["a", "b", "c"]', %q{
+ def raise_a = raise "a"
+ def raise_b = raise "b"
+ def raise_c = raise "c"
+
+ def foo(a: raise_a, b: raise_b, c: raise_c)
+ [a, b, c]
+ end
+
+ def test_a
+ foo(b: 2, c: 3)
+ rescue RuntimeError => e
+ e.message
+ end
+
+ def test_b
+ foo(a: 1, c: 3)
+ rescue RuntimeError => e
+ e.message
+ end
+
+ def test_c
+ foo(a: 1, b: 2)
+ rescue RuntimeError => e
+ e.message
+ end
+
+ def test
+ [test_a, test_b, test_c]
+ end
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_with_non_constant_keyword_default_jit_to_jit
+ # Test that kw_bits passing works correctly in JIT-to-JIT calls
+ assert_compiles '[2, 4, 6]', %q{
+ def make_default(x) = x * 2
+
+ def callee(a: make_default(1), b: make_default(2), c: make_default(3))
+ [a, b, c]
+ end
+
+ def caller_method
+ callee
+ end
+
+ # Warm up callee first so it gets JITted
+ callee
+ callee
+
+ # Now warm up caller - this creates JIT-to-JIT call
+ caller_method
+ caller_method
+ }, call_threshold: 2
+ end
+
+ def test_send_with_non_constant_keyword_default_side_exit
+ # Verify frame reconstruction includes correct values for non-constant defaults
+ assert_compiles '[10, 2, 30]', %q{
+ def make_b = 2
+
+ def callee(a: 1, b: make_b, c: 3)
+ x = binding.local_variable_get(:a)
+ y = binding.local_variable_get(:b)
+ z = binding.local_variable_get(:c)
+ [x, y, z]
+ end
+
+ def test
+ callee(a: 10, c: 30)
+ end
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_with_non_constant_keyword_default_evaluation_order
+ # Verify defaults are evaluated left-to-right and only when not provided
+ assert_compiles '[["a", "b", "c"], ["b", "c"], ["a", "c"], ["a", "b"]]', %q{
+ def log(x)
+ $order << x
+ x
+ end
+
+ def foo(a: log("a"), b: log("b"), c: log("c"))
+ [a, b, c]
+ end
+
+ def test
+ results = []
+
+ $order = []
+ foo
+ results << $order.dup
+
+ $order = []
+ foo(a: "A")
+ results << $order.dup
+
+ $order = []
+ foo(b: "B")
+ results << $order.dup
+
+ $order = []
+ foo(c: "C")
+ results << $order.dup
+
+ results
+ end
+
+ test
+ test
+ }, call_threshold: 2
+ end
+
+ def test_send_with_too_many_non_constant_keyword_defaults
+ assert_compiles '35', %q{
+ def many_kwargs( k1: 1, k2: 2, k3: 3, k4: 4, k5: 5, k6: 6, k7: 7, k8: 8, k9: 9, k10: 10, k11: 11, k12: 12, k13: 13, k14: 14, k15: 15, k16: 16, k17: 17, k18: 18, k19: 19, k20: 20, k21: 21, k22: 22, k23: 23, k24: 24, k25: 25, k26: 26, k27: 27, k28: 28, k29: 29, k30: 30, k31: 31, k32: 32, k33: 33, k34: k33 + 1) = k1 + k34
+ def t = many_kwargs
+ t
+ t
+ }, call_threshold: 2
+ end
+
def test_invokebuiltin
# Not using assert_compiles due to register spill
assert_runs '["."]', %q{
diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs
index 4dae41bf02..a3cf09d7c4 100644
--- a/zjit/src/codegen.rs
+++ b/zjit/src/codegen.rs
@@ -401,7 +401,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
&Insn::Send { cd, blockiseq, state, reason, .. } => gen_send(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::SendForward { cd, blockiseq, state, reason, .. } => gen_send_forward(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::SendWithoutBlock { cd, state, reason, .. } => gen_send_without_block(jit, asm, cd, &function.frame_state(state), reason),
- Insn::SendWithoutBlockDirect { cme, iseq, recv, args, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), &function.frame_state(*state), None),
+ Insn::SendWithoutBlockDirect { cme, iseq, recv, args, kw_bits, state, .. } => gen_send_iseq_direct(cb, jit, asm, *cme, *iseq, opnd!(recv), opnds!(args), *kw_bits, &function.frame_state(*state), None),
&Insn::InvokeSuper { cd, blockiseq, state, reason, .. } => gen_invokesuper(jit, asm, cd, blockiseq, &function.frame_state(state), reason),
&Insn::InvokeBlock { cd, state, reason, .. } => gen_invokeblock(jit, asm, cd, &function.frame_state(state), reason),
Insn::InvokeProc { recv, args, state, kw_splat } => gen_invokeproc(jit, asm, opnd!(recv), opnds!(args), *kw_splat, &function.frame_state(*state)),
@@ -1358,6 +1358,7 @@ fn gen_send_iseq_direct(
iseq: IseqPtr,
recv: Opnd,
args: Vec<Opnd>,
+ kw_bits: u32,
state: &FrameState,
block_handler: Option<Opnd>,
) -> lir::Opnd {
@@ -1404,12 +1405,13 @@ fn gen_send_iseq_direct(
// Write "keyword_bits" to the callee's frame if the callee accepts keywords.
// This is a synthetic local/parameter that the callee reads via checkkeyword to determine
// which optional keyword arguments need their defaults evaluated.
+ // We write this to the local table slot at bits_start so that:
+ // 1. The interpreter can read it via checkkeyword if we side-exit
+ // 2. The JIT entry can read it via GetLocal
if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
let keyword = unsafe { rb_get_iseq_body_param_keyword(iseq) };
let bits_start = unsafe { (*keyword).bits_start } as usize;
- // Currently we only support required keywords, so all bits are 0 (all keywords specified).
- // TODO: When supporting optional keywords, calculate actual unspecified_bits here.
- let unspecified_bits = VALUE::fixnum_from_usize(0);
+ let unspecified_bits = VALUE::fixnum_from_usize(kw_bits as usize);
let bits_offset = (state.stack().len() - args.len() + bits_start) * SIZEOF_VALUE;
asm_comment!(asm, "write keyword bits to callee frame");
asm.store(Opnd::mem(64, SP, bits_offset as i32), unspecified_bits.into());
@@ -1435,10 +1437,11 @@ fn gen_send_iseq_direct(
let lead_num = params.lead_num as u32;
let opt_num = params.opt_num as u32;
let keyword = params.keyword;
- let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } } as u32;
- let req_num = lead_num + kw_req_num;
- assert!(args.len() as u32 <= req_num + opt_num);
- let num_optionals_passed = args.len() as u32 - req_num;
+ let kw_total_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).num } } as u32;
+ assert!(args.len() as u32 <= lead_num + opt_num + kw_total_num);
+ // For computing optional positional entry point, only count positional args
+ let positional_argc = args.len() as u32 - kw_total_num;
+ let num_optionals_passed = positional_argc.saturating_sub(lead_num);
num_optionals_passed
} else {
0
diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs
index 6c2bd09ad3..fc49dc0c3c 100644
--- a/zjit/src/hir.rs
+++ b/zjit/src/hir.rs
@@ -625,9 +625,9 @@ pub enum SendFallbackReason {
SendWithoutBlockBopRedefined,
SendWithoutBlockOperandsNotFixnum,
SendWithoutBlockDirectKeywordMismatch,
- SendWithoutBlockDirectOptionalKeywords,
SendWithoutBlockDirectKeywordCountMismatch,
SendWithoutBlockDirectMissingKeyword,
+ SendWithoutBlockDirectTooManyKeywords,
SendPolymorphic,
SendMegamorphic,
SendNoProfiles,
@@ -686,9 +686,9 @@ impl Display for SendFallbackReason {
SendWithoutBlockBopRedefined => write!(f, "SendWithoutBlock: basic operation was redefined"),
SendWithoutBlockOperandsNotFixnum => write!(f, "SendWithoutBlock: operands are not fixnums"),
SendWithoutBlockDirectKeywordMismatch => write!(f, "SendWithoutBlockDirect: keyword mismatch"),
- SendWithoutBlockDirectOptionalKeywords => write!(f, "SendWithoutBlockDirect: optional keywords"),
SendWithoutBlockDirectKeywordCountMismatch => write!(f, "SendWithoutBlockDirect: keyword count mismatch"),
SendWithoutBlockDirectMissingKeyword => write!(f, "SendWithoutBlockDirect: missing keyword"),
+ SendWithoutBlockDirectTooManyKeywords => write!(f, "SendWithoutBlockDirect: too many keywords for fixnum bitmask"),
SendPolymorphic => write!(f, "Send: polymorphic call site"),
SendMegamorphic => write!(f, "Send: megamorphic call site"),
SendNoProfiles => write!(f, "Send: no profile data available"),
@@ -947,6 +947,7 @@ pub enum Insn {
cme: *const rb_callable_method_entry_t,
iseq: IseqPtr,
args: Vec<InsnId>,
+ kw_bits: u32,
state: InsnId,
},
@@ -1792,7 +1793,7 @@ pub enum ValidationError {
MiscValidationError(InsnId, String),
}
-fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq_t, send_insn: InsnId, args: &[InsnId]) -> bool {
+fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq_t, ci: *const rb_callinfo, send_insn: InsnId, args: &[InsnId]) -> bool {
let mut can_send = true;
let mut count_failure = |counter| {
can_send = false;
@@ -1807,44 +1808,43 @@ fn can_direct_send(function: &mut Function, block: BlockId, iseq: *const rb_iseq
if 0 != params.flags.forwardable() { count_failure(complex_arg_pass_param_forwardable) }
if 0 != params.flags.has_kwrest() { count_failure(complex_arg_pass_param_kwrest) }
- if 0 != params.flags.has_kw() {
- let keyword = params.keyword;
- if !keyword.is_null() {
- let num = unsafe { (*keyword).num };
- let required_num = unsafe { (*keyword).required_num };
- // Only support required keywords for now (no optional keywords)
- if num != required_num {
- count_failure(complex_arg_pass_param_kw_opt)
- }
- }
- }
if !can_send {
function.set_dynamic_send_reason(send_insn, ComplexArgPass);
return false;
}
- // asm.ccall() doesn't support 6+ args
- if args.len() + 1 > C_ARG_OPNDS.len() { // +1 for self
- function.set_dynamic_send_reason(send_insn, TooManyArgsForLir);
- return false;
- }
-
// Because we exclude e.g. post parameters above, they are also excluded from the sum below.
let lead_num = params.lead_num;
let opt_num = params.opt_num;
let keyword = params.keyword;
let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } };
- let req_num = lead_num + kw_req_num;
+ let kw_total_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).num } };
+ // Minimum args: all required positional + all required keywords
+ let min_argc = lead_num + kw_req_num;
+ // Maximum args: all positional (required + optional) + all keywords (required + optional)
+ let max_argc = lead_num + opt_num + kw_total_num;
+
can_send = c_int::try_from(args.len())
.as_ref()
- .map(|argc| (req_num..=req_num + opt_num).contains(argc))
+ .map(|argc| (min_argc..=max_argc).contains(argc))
.unwrap_or(false);
if !can_send {
function.set_dynamic_send_reason(send_insn, ArgcParamMismatch);
return false
}
+ // asm.ccall() doesn't support 6+ args. Compute the final argc after keyword setup:
+ // final_argc = caller's positional args + callee's total keywords (all kw slots are filled).
+ let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
+ let caller_kw_count = if kwarg.is_null() { 0 } else { (unsafe { get_cikw_keyword_len(kwarg) }) as usize };
+ let caller_positional = args.len() - caller_kw_count;
+ let final_argc = caller_positional + kw_total_num as usize;
+ if final_argc + 1 > C_ARG_OPNDS.len() { // +1 for self
+ function.set_dynamic_send_reason(send_insn, TooManyArgsForLir);
+ return false;
+ }
+
can_send
}
@@ -2208,12 +2208,13 @@ impl Function {
state,
reason,
},
- &SendWithoutBlockDirect { recv, cd, cme, iseq, ref args, state } => SendWithoutBlockDirect {
+ &SendWithoutBlockDirect { recv, cd, cme, iseq, ref args, kw_bits, state } => SendWithoutBlockDirect {
recv: find!(recv),
cd,
cme,
iseq,
args: find_vec!(args),
+ kw_bits,
state,
},
&Send { recv, cd, blockiseq, ref args, state, reason } => Send {
@@ -2596,31 +2597,74 @@ impl Function {
}
}
- /// Reorder keyword arguments to match the callee's expectation.
+ /// Prepare arguments for a direct send, handling keyword argument reordering and default synthesis.
+ /// Returns the (state, processed_args, kw_bits) to use for the SendWithoutBlockDirect instruction,
+ /// or Err with the fallback reason if direct send isn't possible.
+ fn prepare_direct_send_args(
+ &mut self,
+ block: BlockId,
+ args: &[InsnId],
+ ci: *const rb_callinfo,
+ iseq: IseqPtr,
+ state: InsnId,
+ ) -> Result<(InsnId, Vec<InsnId>, u32), SendFallbackReason> {
+ let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
+ let (processed_args, caller_argc, kw_bits) = self.setup_keyword_arguments(block, args, kwarg, iseq)?;
+
+ // If args were reordered or synthesized, create a new snapshot with the updated stack
+ let send_state = if processed_args != args {
+ let new_state = self.frame_state(state).with_replaced_args(&processed_args, caller_argc);
+ self.push_insn(block, Insn::Snapshot { state: new_state })
+ } else {
+ state
+ };
+
+ Ok((send_state, processed_args, kw_bits))
+ }
+
+ /// Reorder keyword arguments to match the callee's expected order, and synthesize
+ /// default values for any optional keywords not provided by the caller.
///
- /// Returns Ok with reordered arguments if successful, or Err with the fallback reason if not.
- fn reorder_keyword_arguments(
- &self,
+ /// The output always contains all of the callee's keyword arguments (required + optional),
+ /// so the returned vec may be larger than the input args.
+ ///
+ /// Returns Ok with (processed_args, caller_argc, kw_bits) if successful, or Err with the fallback reason if not.
+ /// - caller_argc: number of arguments the caller actually pushed (for stack calculations)
+ /// - kw_bits: bitmask indicating which optional keywords were NOT provided by the caller
+ /// (used by checkkeyword to determine if non-constant defaults need evaluation)
+ fn setup_keyword_arguments(
+ &mut self,
+ block: BlockId,
args: &[InsnId],
kwarg: *const rb_callinfo_kwarg,
iseq: IseqPtr,
- ) -> Result<Vec<InsnId>, SendFallbackReason> {
+ ) -> Result<(Vec<InsnId>, usize, u32), SendFallbackReason> {
let callee_keyword = unsafe { rb_get_iseq_body_param_keyword(iseq) };
if callee_keyword.is_null() {
- // Caller is passing kwargs but callee doesn't expect them.
- return Err(SendWithoutBlockDirectKeywordMismatch);
+ if !kwarg.is_null() {
+ // Caller is passing kwargs but callee doesn't expect them.
+ return Err(SendWithoutBlockDirectKeywordMismatch);
+ }
+ // Neither caller nor callee have keywords - nothing to do
+ return Ok((args.to_vec(), args.len(), 0));
}
- let caller_kw_count = unsafe { get_cikw_keyword_len(kwarg) } as usize;
+ // kwarg may be null if caller passes no keywords but callee has optional keywords
+ let caller_kw_count = if kwarg.is_null() { 0 } else { (unsafe { get_cikw_keyword_len(kwarg) }) as usize };
let callee_kw_count = unsafe { (*callee_keyword).num } as usize;
+
+ // When there are 31+ keywords, CRuby uses a hash instead of a fixnum bitmask
+ // for kw_bits. Fall back to VM dispatch for this rare case.
+ if callee_kw_count >= VM_KW_SPECIFIED_BITS_MAX as usize {
+ return Err(SendWithoutBlockDirectTooManyKeywords);
+ }
+
let callee_kw_required = unsafe { (*callee_keyword).required_num } as usize;
let callee_kw_table = unsafe { (*callee_keyword).table };
+ let default_values = unsafe { (*callee_keyword).default_values };
- // For now, only handle the case where all keywords are required.
- if callee_kw_count != callee_kw_required {
- return Err(SendWithoutBlockDirectOptionalKeywords);
- }
- if caller_kw_count != callee_kw_count {
+ // Caller can't provide more keywords than callee expects (no **kwrest support yet).
+ if caller_kw_count > callee_kw_count {
return Err(SendWithoutBlockDirectKeywordCountMismatch);
}
@@ -2629,13 +2673,35 @@ impl Function {
// Build a mapping from caller keywords to their positions.
let mut caller_kw_order: Vec<ID> = Vec::with_capacity(caller_kw_count);
- for i in 0..caller_kw_count {
- let sym = unsafe { get_cikw_keywords_idx(kwarg, i as i32) };
- let id = unsafe { rb_sym2id(sym) };
- caller_kw_order.push(id);
+ if !kwarg.is_null() {
+ for i in 0..caller_kw_count {
+ let sym = unsafe { get_cikw_keywords_idx(kwarg, i as i32) };
+ let id = unsafe { rb_sym2id(sym) };
+ caller_kw_order.push(id);
+ }
+ }
+
+ // Verify all caller keywords are expected by callee (no unknown keywords).
+ // Without **kwrest, unexpected keywords should raise ArgumentError at runtime.
+ for &caller_id in &caller_kw_order {
+ let mut found = false;
+ for i in 0..callee_kw_count {
+ let expected_id = unsafe { *callee_kw_table.add(i) };
+ if caller_id == expected_id {
+ found = true;
+ break;
+ }
+ }
+ if !found {
+ // Caller is passing an unknown keyword - this will raise ArgumentError.
+ // Fall back to VM dispatch to handle the error.
+ return Err(SendWithoutBlockDirectKeywordMismatch);
+ }
}
// Reorder keyword arguments to match callee expectation.
+ // Track which optional keywords were not provided via kw_bits.
+ let mut kw_bits: u32 = 0;
let mut reordered_kw_args: Vec<InsnId> = Vec::with_capacity(callee_kw_count);
for i in 0..callee_kw_count {
let expected_id = unsafe { *callee_kw_table.add(i) };
@@ -2652,14 +2718,36 @@ impl Function {
if !found {
// Required keyword not provided by caller which will raise an ArgumentError.
- return Err(SendWithoutBlockDirectMissingKeyword);
+ if i < callee_kw_required {
+ return Err(SendWithoutBlockDirectMissingKeyword);
+ }
+
+ // Optional keyword not provided - use default value
+ let default_idx = i - callee_kw_required;
+ let default_value = unsafe { *default_values.add(default_idx) };
+
+ if default_value == Qundef {
+ // Non-constant default (e.g., `def foo(a: compute())`).
+ // Set the bit so checkkeyword knows to evaluate the default at runtime.
+ // Push Qnil as a placeholder; the callee's checkkeyword will detect this
+ // and branch to evaluate the default expression.
+ kw_bits |= 1 << default_idx;
+ let nil_insn = self.push_insn(block, Insn::Const { val: Const::Value(Qnil) });
+ reordered_kw_args.push(nil_insn);
+ } else {
+ // Constant default value - use it directly
+ let const_insn = self.push_insn(block, Insn::Const { val: Const::Value(default_value) });
+ reordered_kw_args.push(const_insn);
+ }
}
}
// Replace the keyword arguments with the reordered ones.
+ // Keep track of the original caller argc for stack calculations.
+ let caller_argc = args.len();
let mut processed_args = args[..kw_args_start].to_vec();
processed_args.extend(reordered_kw_args);
- Ok(processed_args)
+ Ok((processed_args, caller_argc, kw_bits))
}
/// Resolve the receiver type for method dispatch optimization.
@@ -2894,7 +2982,7 @@ impl Function {
// Only specialize positional-positional calls
// TODO(max): Handle other kinds of parameter passing
let iseq = unsafe { get_def_iseq_ptr((*cme).def) };
- if !can_direct_send(self, block, iseq, insn_id, args.as_slice()) {
+ if !can_direct_send(self, block, iseq, ci, insn_id, args.as_slice()) {
self.push_insn_id(block, insn_id); continue;
}
// Check singleton class assumption first, before emitting other patchpoints
@@ -2907,24 +2995,12 @@ impl Function {
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
}
- let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
- let (send_state, processed_args) = if !kwarg.is_null() {
- match self.reorder_keyword_arguments(&args, kwarg, iseq) {
- Ok(reordered) => {
- let new_state = self.frame_state(state).with_reordered_args(&reordered);
- let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state });
- (snapshot, reordered)
- }
- Err(reason) => {
- self.set_dynamic_send_reason(insn_id, reason);
- self.push_insn_id(block, insn_id); continue;
- }
- }
- } else {
- (state, args.clone())
+ let Ok((send_state, processed_args, kw_bits)) = self.prepare_direct_send_args(block, &args, ci, iseq, state)
+ .inspect_err(|&reason| self.set_dynamic_send_reason(insn_id, reason)) else {
+ self.push_insn_id(block, insn_id); continue;
};
- let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state: send_state });
+ let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state });
self.make_equal_to(insn_id, send_direct);
} else if def_type == VM_METHOD_TYPE_BMETHOD {
let procv = unsafe { rb_get_def_bmethod_proc((*cme).def) };
@@ -2939,7 +3015,7 @@ impl Function {
let capture = unsafe { proc_block.as_.captured.as_ref() };
let iseq = unsafe { *capture.code.iseq.as_ref() };
- if !can_direct_send(self, block, iseq, insn_id, args.as_slice()) {
+ if !can_direct_send(self, block, iseq, ci, insn_id, args.as_slice()) {
self.push_insn_id(block, insn_id); continue;
}
// Can't pass a block to a block for now
@@ -2962,24 +3038,12 @@ impl Function {
recv = self.push_insn(block, Insn::GuardType { val: recv, guard_type: Type::from_profiled_type(profiled_type), state });
}
- let kwarg = unsafe { rb_vm_ci_kwarg(ci) };
- let (send_state, processed_args) = if !kwarg.is_null() {
- match self.reorder_keyword_arguments(&args, kwarg, iseq) {
- Ok(reordered) => {
- let new_state = self.frame_state(state).with_reordered_args(&reordered);
- let snapshot = self.push_insn(block, Insn::Snapshot { state: new_state });
- (snapshot, reordered)
- }
- Err(reason) => {
- self.set_dynamic_send_reason(insn_id, reason);
- self.push_insn_id(block, insn_id); continue;
- }
- }
- } else {
- (state, args.clone())
+ let Ok((send_state, processed_args, kw_bits)) = self.prepare_direct_send_args(block, &args, ci, iseq, state)
+ .inspect_err(|&reason| self.set_dynamic_send_reason(insn_id, reason)) else {
+ self.push_insn_id(block, insn_id); continue;
};
- let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, state: send_state });
+ let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect { recv, cd, cme, iseq, args: processed_args, kw_bits, state: send_state });
self.make_equal_to(insn_id, send_direct);
} else if def_type == VM_METHOD_TYPE_IVAR && args.is_empty() {
// Check if we're accessing ivars of a Class or Module object as they require single-ractor mode.
@@ -3332,7 +3396,7 @@ impl Function {
// Check if the super method's parameters support direct send.
// If not, we can't do direct dispatch.
let super_iseq = unsafe { get_def_iseq_ptr((*super_cme).def) };
- if !can_direct_send(self, block, super_iseq, insn_id, args.as_slice()) {
+ if !can_direct_send(self, block, super_iseq, ci, insn_id, args.as_slice()) {
self.push_insn_id(block, insn_id);
self.set_dynamic_send_reason(insn_id, SuperTargetComplexArgsPass);
continue;
@@ -3360,14 +3424,20 @@ impl Function {
state
});
+ let Ok((send_state, processed_args, kw_bits)) = self.prepare_direct_send_args(block, &args, ci, super_iseq, state)
+ .inspect_err(|&reason| self.set_dynamic_send_reason(insn_id, reason)) else {
+ self.push_insn_id(block, insn_id); continue;
+ };
+
// Use SendWithoutBlockDirect with the super method's CME and ISEQ.
let send_direct = self.push_insn(block, Insn::SendWithoutBlockDirect {
recv,
cd,
cme: super_cme,
iseq: super_iseq,
- args,
- state
+ args: processed_args,
+ kw_bits,
+ state: send_state
});
self.make_equal_to(insn_id, send_direct);
}
@@ -5455,12 +5525,13 @@ impl FrameState {
state
}
- /// Return itself with send args reordered. Used when kwargs are reordered for callee.
- fn with_reordered_args(&self, reordered_args: &[InsnId]) -> Self {
+ /// Return itself with send args replaced. Used when kwargs are reordered/synthesized for callee.
+ /// `original_argc` is the number of args originally on the stack (before processing).
+ fn with_replaced_args(&self, new_args: &[InsnId], original_argc: usize) -> Self {
let mut state = self.clone();
- let args_start = state.stack.len() - reordered_args.len();
+ let args_start = state.stack.len() - original_argc;
state.stack.truncate(args_start);
- state.stack.extend_from_slice(reordered_args);
+ state.stack.extend_from_slice(new_args);
state
}
}
@@ -6121,7 +6192,17 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
let ep_offset = get_arg(pc, 0).as_u32();
let index = get_arg(pc, 1).as_u64();
let index: u8 = index.try_into().map_err(|_| ParseError::MalformedIseq(insn_idx))?;
- let val = fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0, use_sp: false, rest_param: false });
+ // Use FrameState to get kw_bits when possible, just like getlocal_WC_0.
+ let val = if !local_inval {
+ state.getlocal(ep_offset)
+ } else if ep_escaped || has_blockiseq {
+ fun.push_insn(block, Insn::GetLocal { ep_offset, level: 0, use_sp: false, rest_param: false })
+ } else {
+ let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state.without_locals() });
+ fun.push_insn(block, Insn::PatchPoint { invariant: Invariant::NoEPEscape(iseq), state: exit_id });
+ local_inval = false;
+ state.getlocal(ep_offset)
+ };
state.stack_push(fun.push_insn(block, Insn::FixnumBitCheck { val, index }));
}
YARVINSN_opt_getconstant_path => {
@@ -6845,10 +6926,12 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent
// Omitted optionals are locals, so they start as nils before their code run
entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::Const { val: Const::Value(Qnil) }));
} else if Some(local_idx) == kw_bits_idx {
- // We currently only support required keywords so the unspecified bits will always be zero.
- // TODO: Make this a parameter when we start writing anything other than zero.
- let unspecified_bits = VALUE::fixnum_from_usize(0);
- entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::Const { val: Const::Value(unspecified_bits) }));
+ // Read the kw_bits value written by the caller to the callee frame.
+ // This tells us which optional keywords were NOT provided and need their defaults evaluated.
+ // Note: The caller writes kw_bits to memory via gen_send_iseq_direct but does NOT pass it
+ // as a C argument, so we must read it from memory using GetLocal rather than Param.
+ let ep_offset = local_idx_to_ep_offset(iseq, local_idx) as u32;
+ entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::GetLocal { level: 0, ep_offset, use_sp: false, rest_param: false }));
} else if local_idx < param_size {
entry_state.locals.push(fun.push_insn(jit_entry_block, Insn::Param));
} else {
diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs
index 60b4c25986..9fa622dd2c 100644
--- a/zjit/src/hir/opt_tests.rs
+++ b/zjit/src/hir/opt_tests.rs
@@ -628,7 +628,7 @@ mod hir_opt_tests {
Jump bb2(v1, v2, v3)
bb1(v6:BasicObject, v7:BasicObject):
EntryPoint JIT(0)
- v8:Fixnum[0] = Const Value(0)
+ v8:BasicObject = GetLocal <empty>, l0, EP@3
Jump bb2(v6, v7, v8)
bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject):
CheckInterrupts
@@ -1059,6 +1059,47 @@ mod hir_opt_tests {
}
#[test]
+ fn test_call_with_correct_and_too_many_args_for_method() {
+ eval("
+ def target(a = 1, b = 2, c = 3, d = 4) = [a, b, c, d]
+ def test = [target(), target(10, 20, 30), begin; target(10, 20, 30, 40, 50) rescue ArgumentError; end]
+ test
+ test
+ ");
+ assert_snapshot!(hir_string("test"), @r"
+ fn test@<compiled>:3:
+ bb0():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ Jump bb2(v1)
+ bb1(v4:BasicObject):
+ EntryPoint JIT(0)
+ Jump bb2(v4)
+ bb2(v6:BasicObject):
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010)
+ v44:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v45:BasicObject = SendWithoutBlockDirect v44, :target (0x1038)
+ v14:Fixnum[10] = Const Value(10)
+ v16:Fixnum[20] = Const Value(20)
+ v18:Fixnum[30] = Const Value(30)
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010)
+ v48:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v49:BasicObject = SendWithoutBlockDirect v48, :target (0x1038), v14, v16, v18
+ v24:Fixnum[10] = Const Value(10)
+ v26:Fixnum[20] = Const Value(20)
+ v28:Fixnum[30] = Const Value(30)
+ v30:Fixnum[40] = Const Value(40)
+ v32:Fixnum[50] = Const Value(50)
+ v34:BasicObject = SendWithoutBlock v6, :target, v24, v26, v28, v30, v32 # SendFallbackReason: Argument count does not match parameter count
+ v37:ArrayExact = NewArray v45, v49, v34
+ CheckInterrupts
+ Return v37
+ ");
+ }
+
+ #[test]
fn test_optimize_variadic_ccall() {
eval("
def test
@@ -2927,9 +2968,9 @@ mod hir_opt_tests {
PatchPoint NoSingletonClass(Object@0x1000)
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
v22:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
- v24:BasicObject = SendWithoutBlockDirect v22, :foo (0x1038), v11, v13
+ v23:BasicObject = SendWithoutBlockDirect v22, :foo (0x1038), v11, v13
CheckInterrupts
- Return v24
+ Return v23
");
}
@@ -2994,7 +3035,7 @@ mod hir_opt_tests {
}
#[test]
- fn dont_specialize_call_with_positional_and_optional_kw() {
+ fn specialize_call_with_positional_and_optional_kw() {
eval("
def foo(x, a: 1) = [x, a]
def test = foo(0, a: 2)
@@ -3013,10 +3054,12 @@ mod hir_opt_tests {
bb2(v6:BasicObject):
v11:Fixnum[0] = Const Value(0)
v13:Fixnum[2] = Const Value(2)
- IncrCounter complex_arg_pass_param_kw_opt
- v15:BasicObject = SendWithoutBlock v6, :foo, v11, v13 # SendFallbackReason: Complex argument passing
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
+ v22:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v23:BasicObject = SendWithoutBlockDirect v22, :foo (0x1038), v11, v13
CheckInterrupts
- Return v15
+ Return v23
");
}
@@ -3044,22 +3087,105 @@ mod hir_opt_tests {
PatchPoint NoSingletonClass(Object@0x1000)
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
v37:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
- v39:BasicObject = SendWithoutBlockDirect v37, :foo (0x1038), v11, v13, v15
+ v38:BasicObject = SendWithoutBlockDirect v37, :foo (0x1038), v11, v13, v15
v20:Fixnum[1] = Const Value(1)
v22:Fixnum[2] = Const Value(2)
v24:Fixnum[4] = Const Value(4)
v26:Fixnum[3] = Const Value(3)
PatchPoint NoSingletonClass(Object@0x1000)
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
- v42:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
- v44:BasicObject = SendWithoutBlockDirect v42, :foo (0x1038), v20, v22, v26, v24
- v30:ArrayExact = NewArray v39, v44
+ v41:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v43:BasicObject = SendWithoutBlockDirect v41, :foo (0x1038), v20, v22, v26, v24
+ v30:ArrayExact = NewArray v38, v43
CheckInterrupts
Return v30
");
}
#[test]
+ fn specialize_call_with_pos_optional_and_kw_optional() {
+ eval("
+ def foo(r, x = 2, a:, b: 4) = [r, x, a, b]
+ def test = [foo(1, a: 3), foo(1, 2, b: 40, a: 30)] # with and without the optionals
+ test
+ test
+ ");
+ assert_snapshot!(hir_string("test"), @r"
+ fn test@<compiled>:3:
+ bb0():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ Jump bb2(v1)
+ bb1(v4:BasicObject):
+ EntryPoint JIT(0)
+ Jump bb2(v4)
+ bb2(v6:BasicObject):
+ v11:Fixnum[1] = Const Value(1)
+ v13:Fixnum[3] = Const Value(3)
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
+ v35:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v36:Fixnum[4] = Const Value(4)
+ v38:BasicObject = SendWithoutBlockDirect v35, :foo (0x1038), v11, v13, v36
+ v18:Fixnum[1] = Const Value(1)
+ v20:Fixnum[2] = Const Value(2)
+ v22:Fixnum[40] = Const Value(40)
+ v24:Fixnum[30] = Const Value(30)
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
+ v41:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v43:BasicObject = SendWithoutBlockDirect v41, :foo (0x1038), v18, v20, v24, v22
+ v28:ArrayExact = NewArray v38, v43
+ CheckInterrupts
+ Return v28
+ ");
+ }
+
+ #[test]
+ fn test_call_with_pos_optional_and_maybe_too_many_args() {
+ eval("
+ def target(a = 1, b = 2, c = 3, d = 4, e = 5, f:) = [a, b, c, d, e, f]
+ def test = [target(f: 6), target(10, 20, 30, f: 6), target(10, 20, 30, 40, 50, f: 60)]
+ test
+ test
+ ");
+ assert_snapshot!(hir_string("test"), @r"
+ fn test@<compiled>:3:
+ bb0():
+ EntryPoint interpreter
+ v1:BasicObject = LoadSelf
+ Jump bb2(v1)
+ bb1(v4:BasicObject):
+ EntryPoint JIT(0)
+ Jump bb2(v4)
+ bb2(v6:BasicObject):
+ v11:Fixnum[6] = Const Value(6)
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010)
+ v48:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v49:BasicObject = SendWithoutBlockDirect v48, :target (0x1038), v11
+ v16:Fixnum[10] = Const Value(10)
+ v18:Fixnum[20] = Const Value(20)
+ v20:Fixnum[30] = Const Value(30)
+ v22:Fixnum[6] = Const Value(6)
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, target@0x1008, cme:0x1010)
+ v52:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v53:BasicObject = SendWithoutBlockDirect v52, :target (0x1038), v16, v18, v20, v22
+ v27:Fixnum[10] = Const Value(10)
+ v29:Fixnum[20] = Const Value(20)
+ v31:Fixnum[30] = Const Value(30)
+ v33:Fixnum[40] = Const Value(40)
+ v35:Fixnum[50] = Const Value(50)
+ v37:Fixnum[60] = Const Value(60)
+ v39:BasicObject = SendWithoutBlock v6, :target, v27, v29, v31, v33, v35, v37 # SendFallbackReason: Too many arguments for LIR
+ v41:ArrayExact = NewArray v49, v53, v39
+ CheckInterrupts
+ Return v41
+ ");
+ }
+
+ #[test]
fn test_send_call_to_iseq_with_optional_kw() {
eval("
def foo(a: 1) = a
@@ -3078,10 +3204,12 @@ mod hir_opt_tests {
Jump bb2(v4)
bb2(v6:BasicObject):
v11:Fixnum[2] = Const Value(2)
- IncrCounter complex_arg_pass_param_kw_opt
- v13:BasicObject = SendWithoutBlock v6, :foo, v11 # SendFallbackReason: Complex argument passing
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
+ v20:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v21:BasicObject = SendWithoutBlockDirect v20, :foo (0x1038), v11
CheckInterrupts
- Return v13
+ Return v21
");
}
@@ -3112,7 +3240,7 @@ mod hir_opt_tests {
}
#[test]
- fn dont_specialize_call_to_iseq_with_optional_param_kw() {
+ fn specialize_call_to_iseq_with_optional_param_kw_using_default() {
eval("
def foo(int: 1) = int + 1
def test = foo
@@ -3129,10 +3257,13 @@ mod hir_opt_tests {
EntryPoint JIT(0)
Jump bb2(v4)
bb2(v6:BasicObject):
- IncrCounter complex_arg_pass_param_kw_opt
- v11:BasicObject = SendWithoutBlock v6, :foo # SendFallbackReason: Complex argument passing
+ PatchPoint NoSingletonClass(Object@0x1000)
+ PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
+ v18:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
+ v19:Fixnum[1] = Const Value(1)
+ v21:BasicObject = SendWithoutBlockDirect v18, :foo (0x1038), v19
CheckInterrupts
- Return v11
+ Return v21
");
}
@@ -3506,7 +3637,6 @@ mod hir_opt_tests {
PatchPoint MethodRedefined(Hash@0x1008, new@0x1009, cme:0x1010)
v46:HashExact = ObjectAllocClass Hash:VALUE(0x1008)
IncrCounter complex_arg_pass_param_block
- IncrCounter complex_arg_pass_param_kw_opt
v20:BasicObject = SendWithoutBlock v46, :initialize # SendFallbackReason: Complex argument passing
CheckInterrupts
CheckInterrupts
@@ -9836,7 +9966,6 @@ mod hir_opt_tests {
IncrCounter complex_arg_pass_param_rest
IncrCounter complex_arg_pass_param_block
IncrCounter complex_arg_pass_param_kwrest
- IncrCounter complex_arg_pass_param_kw_opt
v13:BasicObject = SendWithoutBlock v6, :fancy, v11 # SendFallbackReason: Complex argument passing
CheckInterrupts
Return v13
diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs
index 1ce5488f47..3b0f591599 100644
--- a/zjit/src/hir/tests.rs
+++ b/zjit/src/hir/tests.rs
@@ -114,12 +114,11 @@ mod snapshot_tests {
PatchPoint NoSingletonClass(Object@0x1010)
PatchPoint MethodRedefined(Object@0x1010, foo@0x1018, cme:0x1020)
v22:HeapObject[class_exact*:Object@VALUE(0x1010)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1010)]
- v23:Any = Snapshot FrameState { pc: 0x1008, stack: [v6, v11, v13], locals: [] }
- v24:BasicObject = SendWithoutBlockDirect v22, :foo (0x1048), v11, v13
- v16:Any = Snapshot FrameState { pc: 0x1050, stack: [v24], locals: [] }
+ v23:BasicObject = SendWithoutBlockDirect v22, :foo (0x1048), v11, v13
+ v16:Any = Snapshot FrameState { pc: 0x1050, stack: [v23], locals: [] }
PatchPoint NoTracePoint
CheckInterrupts
- Return v24
+ Return v23
");
}
@@ -3114,7 +3113,7 @@ pub mod hir_build_tests {
Jump bb2(v1, v2, v3, v4)
bb1(v7:BasicObject, v8:BasicObject, v9:BasicObject):
EntryPoint JIT(0)
- v10:Fixnum[0] = Const Value(0)
+ v10:BasicObject = GetLocal <empty>, l0, EP@3
Jump bb2(v7, v8, v9, v10)
bb2(v12:BasicObject, v13:BasicObject, v14:BasicObject, v15:BasicObject):
v19:Float = InvokeBuiltin rb_f_float, v12, v13, v14
@@ -3165,7 +3164,7 @@ pub mod hir_build_tests {
Jump bb2(v1, v2, v3, v4, v5, v6)
bb1(v9:BasicObject, v10:BasicObject, v11:BasicObject, v13:BasicObject):
EntryPoint JIT(0)
- v12:Fixnum[0] = Const Value(0)
+ v12:BasicObject = GetLocal <empty>, l0, EP@5
v14:NilClass = Const Value(nil)
Jump bb2(v9, v10, v11, v12, v13, v14)
bb2(v16:BasicObject, v17:BasicObject, v18:BasicObject, v19:BasicObject, v20:BasicObject, v21:NilClass):
@@ -3226,7 +3225,7 @@ pub mod hir_build_tests {
Jump bb2(v1, v2, v3, v4, v5)
bb1(v8:BasicObject, v9:BasicObject, v10:BasicObject, v11:BasicObject):
EntryPoint JIT(0)
- v12:Fixnum[0] = Const Value(0)
+ v12:BasicObject = GetLocal <empty>, l0, EP@3
Jump bb2(v8, v9, v10, v11, v12)
bb2(v14:BasicObject, v15:BasicObject, v16:BasicObject, v17:BasicObject, v18:BasicObject):
v25:FalseClass = Const Value(false)
@@ -3647,21 +3646,20 @@ pub mod hir_build_tests {
Jump bb2(v1, v2, v3)
bb1(v6:BasicObject, v7:BasicObject):
EntryPoint JIT(0)
- v8:Fixnum[0] = Const Value(0)
+ v8:BasicObject = GetLocal <empty>, l0, EP@3
Jump bb2(v6, v7, v8)
bb2(v10:BasicObject, v11:BasicObject, v12:BasicObject):
- v15:BasicObject = GetLocal <empty>, l0, EP@3
- v16:BoolExact = FixnumBitCheck v15, 0
+ v15:BoolExact = FixnumBitCheck v12, 0
CheckInterrupts
- v19:CBool = Test v16
- IfTrue v19, bb3(v10, v11, v12)
- v22:Fixnum[1] = Const Value(1)
- v24:Fixnum[1] = Const Value(1)
- v27:BasicObject = SendWithoutBlock v22, :+, v24 # SendFallbackReason: Uncategorized(opt_plus)
- Jump bb3(v10, v27, v12)
- bb3(v30:BasicObject, v31:BasicObject, v32:BasicObject):
+ v18:CBool = Test v15
+ IfTrue v18, bb3(v10, v11, v12)
+ v21:Fixnum[1] = Const Value(1)
+ v23:Fixnum[1] = Const Value(1)
+ v26:BasicObject = SendWithoutBlock v21, :+, v23 # SendFallbackReason: Uncategorized(opt_plus)
+ Jump bb3(v10, v26, v12)
+ bb3(v29:BasicObject, v30:BasicObject, v31:BasicObject):
CheckInterrupts
- Return v31
+ Return v30
");
}
@@ -3719,7 +3717,7 @@ pub mod hir_build_tests {
Jump bb2(v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35)
bb1(v38:BasicObject, v39:BasicObject, v40:BasicObject, v41:BasicObject, v42:BasicObject, v43:BasicObject, v44:BasicObject, v45:BasicObject, v46:BasicObject, v47:BasicObject, v48:BasicObject, v49:BasicObject, v50:BasicObject, v51:BasicObject, v52:BasicObject, v53:BasicObject, v54:BasicObject, v55:BasicObject, v56:BasicObject, v57:BasicObject, v58:BasicObject, v59:BasicObject, v60:BasicObject, v61:BasicObject, v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:BasicObject, v67:BasicObject, v68:BasicObject, v69:BasicObject, v70:BasicObject, v71:BasicObject):
EntryPoint JIT(0)
- v72:Fixnum[0] = Const Value(0)
+ v72:BasicObject = GetLocal <empty>, l0, EP@3
Jump bb2(v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63, v64, v65, v66, v67, v68, v69, v70, v71, v72)
bb2(v74:BasicObject, v75:BasicObject, v76:BasicObject, v77:BasicObject, v78:BasicObject, v79:BasicObject, v80:BasicObject, v81:BasicObject, v82:BasicObject, v83:BasicObject, v84:BasicObject, v85:BasicObject, v86:BasicObject, v87:BasicObject, v88:BasicObject, v89:BasicObject, v90:BasicObject, v91:BasicObject, v92:BasicObject, v93:BasicObject, v94:BasicObject, v95:BasicObject, v96:BasicObject, v97:BasicObject, v98:BasicObject, v99:BasicObject, v100:BasicObject, v101:BasicObject, v102:BasicObject, v103:BasicObject, v104:BasicObject, v105:BasicObject, v106:BasicObject, v107:BasicObject, v108:BasicObject):
SideExit TooManyKeywordParameters
diff --git a/zjit/src/stats.rs b/zjit/src/stats.rs
index f619c798fc..cf100dcda2 100644
--- a/zjit/src/stats.rs
+++ b/zjit/src/stats.rs
@@ -228,9 +228,9 @@ make_counters! {
send_fallback_send_without_block_bop_redefined,
send_fallback_send_without_block_operands_not_fixnum,
send_fallback_send_without_block_direct_keyword_mismatch,
- send_fallback_send_without_block_direct_optional_keywords,
send_fallback_send_without_block_direct_keyword_count_mismatch,
send_fallback_send_without_block_direct_missing_keyword,
+ send_fallback_send_without_block_direct_too_many_keywords,
send_fallback_send_polymorphic,
send_fallback_send_megamorphic,
send_fallback_send_no_profiles,
@@ -387,7 +387,6 @@ make_counters! {
// Unsupported parameter features
complex_arg_pass_param_rest,
complex_arg_pass_param_post,
- complex_arg_pass_param_kw_opt,
complex_arg_pass_param_kwrest,
complex_arg_pass_param_block,
complex_arg_pass_param_forwardable,
@@ -599,9 +598,9 @@ pub fn send_fallback_counter(reason: crate::hir::SendFallbackReason) -> Counter
SendWithoutBlockBopRedefined => send_fallback_send_without_block_bop_redefined,
SendWithoutBlockOperandsNotFixnum => send_fallback_send_without_block_operands_not_fixnum,
SendWithoutBlockDirectKeywordMismatch => send_fallback_send_without_block_direct_keyword_mismatch,
- SendWithoutBlockDirectOptionalKeywords => send_fallback_send_without_block_direct_optional_keywords,
SendWithoutBlockDirectKeywordCountMismatch=> send_fallback_send_without_block_direct_keyword_count_mismatch,
SendWithoutBlockDirectMissingKeyword => send_fallback_send_without_block_direct_missing_keyword,
+ SendWithoutBlockDirectTooManyKeywords => send_fallback_send_without_block_direct_too_many_keywords,
SendPolymorphic => send_fallback_send_polymorphic,
SendMegamorphic => send_fallback_send_megamorphic,
SendNoProfiles => send_fallback_send_no_profiles,