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 |
This commit is contained in:
Randy Stauner 2026-01-20 23:19:34 -07:00 committed by GitHub
parent 01984fa80e
commit 6f1453dc08
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
Notes: git 2026-01-21 06:20:08 +00:00
Merged-By: tekknolagi <donotemailthisaddress@bernsteinbear.com>
6 changed files with 589 additions and 138 deletions

View File

@ -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{

View File

@ -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

View File

@ -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 {

View File

@ -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
@ -1058,6 +1058,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("
@ -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,21 +3087,104 @@ 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("
@ -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

View File

@ -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

View File

@ -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,