Skip to content

Commit 7778c29

Browse files
committed
RJIT: Support keyword arguments
1 parent cd94bcd commit 7778c29

2 files changed

Lines changed: 219 additions & 18 deletions

File tree

lib/ruby_vm/rjit/insn_compiler.rb

Lines changed: 215 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,14 +1150,7 @@ def dupn(jit, ctx, asm)
11501150
# @param ctx [RubyVM::RJIT::Context]
11511151
# @param asm [RubyVM::RJIT::Assembler]
11521152
def swap(jit, ctx, asm)
1153-
stack0_mem = ctx.stack_opnd(0)
1154-
stack1_mem = ctx.stack_opnd(1)
1155-
1156-
asm.mov(:rax, stack0_mem)
1157-
asm.mov(:rcx, stack1_mem)
1158-
asm.mov(stack0_mem, :rcx)
1159-
asm.mov(stack1_mem, :rax)
1160-
1153+
stack_swap(jit, ctx, asm, 0, 1)
11611154
KeepCompiling
11621155
end
11631156

@@ -3243,6 +3236,16 @@ def lookup_cfunc_codegen(cme_def)
32433236
@cfunc_codegen_table[cme_def.method_serial]
32443237
end
32453238

3239+
def stack_swap(_jit, ctx, asm, offset0, offset1)
3240+
stack0_mem = ctx.stack_opnd(offset0)
3241+
stack1_mem = ctx.stack_opnd(offset1)
3242+
3243+
asm.mov(:rax, stack0_mem)
3244+
asm.mov(:rcx, stack1_mem)
3245+
asm.mov(stack0_mem, :rcx)
3246+
asm.mov(stack1_mem, :rax)
3247+
end
3248+
32463249
def jit_getlocal_generic(jit, ctx, asm, idx:, level:)
32473250
# Load environment pointer EP at level
32483251
ep_reg = :rax
@@ -4232,17 +4235,87 @@ def jit_call_iseq(jit, ctx, asm, cme, calling, iseq, frame_type: nil, prev_ep: n
42324235
end
42334236

42344237
if doing_kw_call
4235-
asm.incr_counter(:send_iseq_kw_call)
4236-
return CantCompile
4238+
# Here we're calling a method with keyword arguments and specifying
4239+
# keyword arguments at this call site.
4240+
4241+
# This struct represents the metadata about the callee-specified
4242+
# keyword parameters.
4243+
keyword = iseq.body.param.keyword
4244+
keyword_num = keyword.num
4245+
keyword_required_num = keyword.required_num
4246+
4247+
required_kwargs_filled = 0
4248+
4249+
if keyword_num > 30
4250+
# We have so many keywords that (1 << num) encoded as a FIXNUM
4251+
# (which shifts it left one more) no longer fits inside a 32-bit
4252+
# immediate.
4253+
asm.incr_counter(:send_iseq_too_many_kwargs)
4254+
return CantCompile
4255+
end
4256+
4257+
# Check that the kwargs being passed are valid
4258+
if supplying_kws
4259+
# This is the list of keyword arguments that the callee specified
4260+
# in its initial declaration.
4261+
# SAFETY: see compile.c for sizing of this slice.
4262+
callee_kwargs = keyword_num.times.map { |i| keyword.table[i] }
4263+
4264+
# Here we're going to build up a list of the IDs that correspond to
4265+
# the caller-specified keyword arguments. If they're not in the
4266+
# same order as the order specified in the callee declaration, then
4267+
# we're going to need to generate some code to swap values around
4268+
# on the stack.
4269+
caller_kwargs = []
4270+
kw_arg.keyword_len.times do |kwarg_idx|
4271+
sym = C.to_ruby(kw_arg[:keywords][kwarg_idx])
4272+
caller_kwargs << C.rb_sym2id(sym)
4273+
end
4274+
4275+
# First, we're going to be sure that the names of every
4276+
# caller-specified keyword argument correspond to a name in the
4277+
# list of callee-specified keyword parameters.
4278+
caller_kwargs.each do |caller_kwarg|
4279+
search_result = callee_kwargs.map.with_index.find { |kwarg, _| kwarg == caller_kwarg }
4280+
4281+
case search_result
4282+
in nil
4283+
# If the keyword was never found, then we know we have a
4284+
# mismatch in the names of the keyword arguments, so we need to
4285+
# bail.
4286+
asm.incr_counter(:send_iseq_kwargs_mismatch)
4287+
return CantCompile
4288+
in _, callee_idx if callee_idx < keyword_required_num
4289+
# Keep a count to ensure all required kwargs are specified
4290+
required_kwargs_filled += 1
4291+
else
4292+
end
4293+
end
4294+
end
4295+
assert_equal(true, required_kwargs_filled <= keyword_required_num)
4296+
if required_kwargs_filled != keyword_required_num
4297+
asm.incr_counter(:send_iseq_kwargs_mismatch)
4298+
return CantCompile
4299+
end
42374300
end
42384301

42394302
# Check if we need the arg0 splat handling of vm_callee_setup_block_arg
42404303
arg_setup_block = (block_handler == :captured) # arg_setup_type: arg_setup_block (invokeblock)
42414304
block_arg0_splat = arg_setup_block && argc == 1 &&
42424305
iseq.body.param.flags.has_lead && !iseq.body.param.flags.ambiguous_param0
42434306
if block_arg0_splat
4244-
asm.incr_counter(:send_iseq_block_arg0_splat)
4245-
return CantCompile
4307+
# If block_arg0_splat, we still need side exits after splat, but
4308+
# doing push_splat_args here disallows it. So bail out.
4309+
if flags & VM_CALL_ARGS_SPLAT != 0 && !iseq_has_rest
4310+
asm.incr_counter(:invokeblock_iseq_arg0_args_splat)
4311+
return CantCompile
4312+
end
4313+
# The block_arg0_splat implementation is for the rb_simple_iseq_p case,
4314+
# but doing_kw_call means it's not a simple ISEQ.
4315+
if doing_kw_call
4316+
asm.incr_counter(:invokeblock_iseq_arg0_has_kw)
4317+
return CantCompile
4318+
end
42464319
end
42474320
if flags & C::VM_CALL_ARGS_SPLAT != 0 && !iseq_has_rest
42484321
splat_array_length = false
@@ -4373,8 +4446,131 @@ def jit_call_iseq(jit, ctx, asm, cme, calling, iseq, frame_type: nil, prev_ep: n
43734446
end
43744447

43754448
if doing_kw_call
4376-
asm.incr_counter(:send_iseq_kw_call)
4377-
return CantCompile
4449+
# Here we're calling a method with keyword arguments and specifying
4450+
# keyword arguments at this call site.
4451+
4452+
# Number of positional arguments the callee expects before the first
4453+
# keyword argument
4454+
args_before_kw = required_num + opt_num
4455+
4456+
# This struct represents the metadata about the caller-specified
4457+
# keyword arguments.
4458+
ci_kwarg = calling.kwarg
4459+
caller_keyword_len = if ci_kwarg.nil?
4460+
0
4461+
else
4462+
ci_kwarg.keyword_len
4463+
end
4464+
4465+
# This struct represents the metadata about the callee-specified
4466+
# keyword parameters.
4467+
keyword = iseq.body.param.keyword
4468+
4469+
asm.comment('keyword args')
4470+
4471+
# This is the list of keyword arguments that the callee specified
4472+
# in its initial declaration.
4473+
callee_kwargs = keyword.table
4474+
total_kwargs = keyword.num
4475+
4476+
# Here we're going to build up a list of the IDs that correspond to
4477+
# the caller-specified keyword arguments. If they're not in the
4478+
# same order as the order specified in the callee declaration, then
4479+
# we're going to need to generate some code to swap values around
4480+
# on the stack.
4481+
caller_kwargs = []
4482+
4483+
caller_keyword_len.times do |kwarg_idx|
4484+
sym = C.to_ruby(ci_kwarg[:keywords][kwarg_idx])
4485+
caller_kwargs << C.rb_sym2id(sym)
4486+
end
4487+
kwarg_idx = caller_keyword_len
4488+
4489+
unspecified_bits = 0
4490+
4491+
keyword_required_num = keyword.required_num
4492+
(keyword_required_num...total_kwargs).each do |callee_idx|
4493+
already_passed = false
4494+
callee_kwarg = callee_kwargs[callee_idx]
4495+
4496+
caller_keyword_len.times do |caller_idx|
4497+
if caller_kwargs[caller_idx] == callee_kwarg
4498+
already_passed = true
4499+
break
4500+
end
4501+
end
4502+
4503+
unless already_passed
4504+
# Reserve space on the stack for each default value we'll be
4505+
# filling in (which is done in the next loop). Also increments
4506+
# argc so that the callee's SP is recorded correctly.
4507+
argc += 1
4508+
default_arg = ctx.stack_push
4509+
4510+
# callee_idx - keyword->required_num is used in a couple of places below.
4511+
req_num = keyword.required_num
4512+
extra_args = callee_idx - req_num
4513+
4514+
# VALUE default_value = keyword->default_values[callee_idx - keyword->required_num];
4515+
default_value = keyword.default_values[extra_args]
4516+
4517+
if default_value == Qundef
4518+
# Qundef means that this value is not constant and must be
4519+
# recalculated at runtime, so we record it in unspecified_bits
4520+
# (Qnil is then used as a placeholder instead of Qundef).
4521+
unspecified_bits |= 0x01 << extra_args
4522+
default_value = Qnil
4523+
end
4524+
4525+
asm.mov(:rax, default_value)
4526+
asm.mov(default_arg, :rax)
4527+
4528+
caller_kwargs[kwarg_idx] = callee_kwarg
4529+
kwarg_idx += 1
4530+
end
4531+
end
4532+
4533+
assert_equal(kwarg_idx, total_kwargs)
4534+
4535+
# Next, we're going to loop through every keyword that was
4536+
# specified by the caller and make sure that it's in the correct
4537+
# place. If it's not we're going to swap it around with another one.
4538+
total_kwargs.times do |kwarg_idx|
4539+
callee_kwarg = callee_kwargs[kwarg_idx]
4540+
4541+
# If the argument is already in the right order, then we don't
4542+
# need to generate any code since the expected value is already
4543+
# in the right place on the stack.
4544+
if callee_kwarg == caller_kwargs[kwarg_idx]
4545+
next
4546+
end
4547+
4548+
# In this case the argument is not in the right place, so we
4549+
# need to find its position where it _should_ be and swap with
4550+
# that location.
4551+
((kwarg_idx + 1)...total_kwargs).each do |swap_idx|
4552+
if callee_kwarg == caller_kwargs[swap_idx]
4553+
# First we're going to generate the code that is going
4554+
# to perform the actual swapping at runtime.
4555+
offset0 = argc - 1 - swap_idx - args_before_kw
4556+
offset1 = argc - 1 - kwarg_idx - args_before_kw
4557+
stack_swap(jit, ctx, asm, offset0, offset1)
4558+
4559+
# Next we're going to do some bookkeeping on our end so
4560+
# that we know the order that the arguments are
4561+
# actually in now.
4562+
caller_kwargs[kwarg_idx], caller_kwargs[swap_idx] =
4563+
caller_kwargs[swap_idx], caller_kwargs[kwarg_idx]
4564+
4565+
break
4566+
end
4567+
end
4568+
end
4569+
4570+
# Keyword arguments cause a special extra local variable to be
4571+
# pushed onto the stack that represents the parameters that weren't
4572+
# explicitly given a value and have a non-constant default.
4573+
asm.mov(ctx.stack_opnd(-1), C.to_value(unspecified_bits))
43784574
end
43794575

43804576
# Same as vm_callee_setup_block_arg_arg0_check and vm_callee_setup_block_arg_arg0_splat
@@ -4395,6 +4591,7 @@ def jit_call_iseq(jit, ctx, asm, cme, calling, iseq, frame_type: nil, prev_ep: n
43954591
local_size: num_locals,
43964592
stack_max: iseq.body.stack_max,
43974593
prev_ep:,
4594+
doing_kw_call:,
43984595
)
43994596

44004597
# Create a context for the callee
@@ -4953,12 +5150,11 @@ def jit_call_symbol(jit, ctx, asm, cme, calling, known_recv_class, flags)
49535150
# @param jit [RubyVM::RJIT::JITState]
49545151
# @param ctx [RubyVM::RJIT::Context]
49555152
# @param asm [RubyVM::RJIT::Assembler]
4956-
def jit_push_frame(jit, ctx, asm, cme, flags, argc, frame_type, block_handler, iseq: nil, local_size: 0, stack_max: 0, prev_ep: nil)
5153+
def jit_push_frame(jit, ctx, asm, cme, flags, argc, frame_type, block_handler, iseq: nil, local_size: 0, stack_max: 0, prev_ep: nil, doing_kw_call: nil)
49575154
# Save caller SP and PC before pushing a callee frame for backtrace and side exits
49585155
asm.comment('save SP to caller CFP')
49595156
recv_idx = argc # blockarg is already popped
49605157
recv_idx += (block_handler == :captured) ? 0 : 1 # receiver is not on stack when captured->self is used
4961-
# TODO: consider doing_kw_call
49625158
if iseq
49635159
# Skip setting this to SP register. This cfp->sp will be copied to SP on leave insn.
49645160
asm.lea(:rax, ctx.sp_opnd(C.VALUE.size * -recv_idx)) # Pop receiver and arguments to prepare for side exits
@@ -4977,7 +5173,8 @@ def jit_push_frame(jit, ctx, asm, cme, flags, argc, frame_type, block_handler, i
49775173
end
49785174

49795175
asm.comment('set up EP with managing data')
4980-
ep_offset = ctx.sp_offset + local_size + 2
5176+
sp_offset = ctx.sp_offset + 3 + local_size + (doing_kw_call ? 1 : 0)
5177+
ep_offset = sp_offset - 1
49815178
# ep[-2]: cref_or_me
49825179
asm.mov(:rax, cme.to_i)
49835180
asm.mov([SP, C.VALUE.size * (ep_offset - 2)], :rax)
@@ -5032,7 +5229,7 @@ def jit_push_frame(jit, ctx, asm, cme, flags, argc, frame_type, block_handler, i
50325229
asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:block_code)], 0)
50335230
# Update SP register only for ISEQ calls. SP-relative operations should be done above this.
50345231
sp_reg = iseq ? SP : :rax
5035-
asm.lea(sp_reg, [SP, C.VALUE.size * (ctx.sp_offset + local_size + 3)])
5232+
asm.lea(sp_reg, [SP, C.VALUE.size * sp_offset])
50365233
asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:sp)], sp_reg)
50375234
asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:__bp__)], sp_reg) # TODO: get rid of this!!
50385235

rjit_c.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ RJIT_RUNTIME_COUNTERS(
7272
send_iseq_has_rest_and_optional,
7373
send_iseq_arity_error,
7474
send_iseq_missing_optional_kw,
75+
send_iseq_too_many_kwargs,
76+
send_iseq_kwargs_mismatch,
7577

7678
send_cfunc_variadic,
7779
send_cfunc_too_many_args,
@@ -115,6 +117,8 @@ RJIT_RUNTIME_COUNTERS(
115117
invokeblock_iseq_arg0_splat,
116118
invokeblock_ifunc_args_splat,
117119
invokeblock_ifunc_kw_splat,
120+
invokeblock_iseq_arg0_args_splat,
121+
invokeblock_iseq_arg0_has_kw,
118122

119123
getivar_megamorphic,
120124
getivar_not_heap,

0 commit comments

Comments
 (0)