Skip to content

Commit 99964fc

Browse files
committed
ZJIT: Support optional keyword args in direct send
1 parent 1bea320 commit 99964fc

6 files changed

Lines changed: 260 additions & 119 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,52 @@ def test = [target(f: 6), target(10, 20, 30, f: 6), target(10, 20, 30, 40, 50, f
774774
}, call_threshold: 2
775775
end
776776

777+
def test_send_kwarg_partial_optional
778+
assert_compiles '[[1, 2, 3], [1, 20, 3], [10, 2, 30]]', %q{
779+
def test(a: 1, b: 2, c: 3) = [a, b, c]
780+
def entry = [test, test(b: 20), test(c: 30, a: 10)]
781+
entry
782+
entry
783+
}, call_threshold: 2
784+
end
785+
786+
def test_send_kwarg_optional_a_lot
787+
assert_compiles '[[1, 2, 3, 4, 5, 6], [1, 2, 3, 7, 8, 9], [2, 4, 6, 8, 10, 12]]', %q{
788+
def test(a: 1, b: 2, c: 3, d: 4, e: 5, f: 6) = [a, b, c, d, e, f]
789+
def entry = [test, test(d: 7, f: 9, e: 8), test(f: 12, e: 10, d: 8, c: 6, b: 4, a: 2)]
790+
entry
791+
entry
792+
}, call_threshold: 2
793+
end
794+
795+
def test_send_kwarg_non_constant_default
796+
assert_compiles '[[1, 2], [10, 2]]', %q{
797+
def make_default = 2
798+
def test(a: 1, b: make_default) = [a, b]
799+
def entry = [test, test(a: 10)]
800+
entry
801+
entry
802+
}, call_threshold: 2
803+
end
804+
805+
def test_send_kwarg_optional_static_with_side_exit
806+
# verify frame reconstruction with synthesized keyword defaults is correct
807+
assert_compiles '[10, 2, 10]', %q{
808+
def callee(a: 1, b: 2)
809+
# use binding to force side-exit
810+
x = binding.local_variable_get(:a)
811+
[a, b, x]
812+
end
813+
814+
def entry
815+
callee(a: 10) # b should get default value
816+
end
817+
818+
entry
819+
entry
820+
}, call_threshold: 2
821+
end
822+
777823
def test_send_all_arg_types
778824
assert_compiles '[:req, :opt, :post, :kwr, :kwo, true]', %q{
779825
def test(a, b = :opt, c, d:, e: :kwo) = [a, b, c, d, e, block_given?]
@@ -1329,6 +1375,23 @@ def test
13291375
}, call_threshold: 2
13301376
end
13311377

1378+
def test_invokesuper_with_optional_keyword_args
1379+
assert_compiles '[1, 2, 3]', %q{
1380+
class Parent
1381+
def foo(a, b: 2, c: 3) = [a, b, c]
1382+
end
1383+
1384+
class Child < Parent
1385+
def foo(a) = super(a)
1386+
end
1387+
1388+
def test = Child.new.foo(1)
1389+
1390+
test
1391+
test
1392+
}, call_threshold: 2
1393+
end
1394+
13321395
def test_invokebuiltin
13331396
# Not using assert_compiles due to register spill
13341397
assert_runs '["."]', %q{

zjit/src/codegen.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,11 +1403,14 @@ fn gen_send_iseq_direct(
14031403
// Write "keyword_bits" to the callee's frame if the callee accepts keywords.
14041404
// This is a synthetic local/parameter that the callee reads via checkkeyword to determine
14051405
// which optional keyword arguments need their defaults evaluated.
1406+
// We write this to the local table slot at bits_start so that:
1407+
// 1. The interpreter can read it via checkkeyword if we side-exit
1408+
// 2. The JIT entry can read it via GetLocal
14061409
if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
14071410
let keyword = unsafe { rb_get_iseq_body_param_keyword(iseq) };
14081411
let bits_start = unsafe { (*keyword).bits_start } as usize;
1409-
// Currently we only support required keywords, so all bits are 0 (all keywords specified).
1410-
// TODO: When supporting optional keywords, calculate actual unspecified_bits here.
1412+
// kw_bits is always 0 because constant defaults are inlined directly,
1413+
// and non-constant defaults (Qundef) cause fallback to VM dispatch.
14111414
let unspecified_bits = VALUE::fixnum_from_usize(0);
14121415
let bits_offset = (state.stack().len() - args.len() + bits_start) * SIZEOF_VALUE;
14131416
asm_comment!(asm, "write keyword bits to callee frame");
@@ -1434,10 +1437,11 @@ fn gen_send_iseq_direct(
14341437
let lead_num = params.lead_num as u32;
14351438
let opt_num = params.opt_num as u32;
14361439
let keyword = params.keyword;
1437-
let kw_req_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).required_num } } as u32;
1438-
let req_num = lead_num + kw_req_num;
1439-
assert!(args.len() as u32 <= req_num + opt_num);
1440-
let num_optionals_passed = args.len() as u32 - req_num;
1440+
let kw_total_num = if keyword.is_null() { 0 } else { unsafe { (*keyword).num } } as u32;
1441+
assert!(args.len() as u32 <= lead_num + opt_num + kw_total_num);
1442+
// For computing optional positional entry point, only count positional args
1443+
let positional_argc = args.len() as u32 - kw_total_num;
1444+
let num_optionals_passed = positional_argc.saturating_sub(lead_num);
14411445
num_optionals_passed
14421446
} else {
14431447
0

0 commit comments

Comments
 (0)