aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bootstraptest/test_yjit.rb54
-rw-r--r--yjit/src/codegen.rs286
2 files changed, 197 insertions, 143 deletions
diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb
index d87454c38c..196fa454ff 100644
--- a/bootstraptest/test_yjit.rb
+++ b/bootstraptest/test_yjit.rb
@@ -1,3 +1,57 @@
+# test splat filling required and feeding rest
+assert_equal '[0, 1, 2, [3, 4]]', %q{
+ public def lead_rest(a, b, *rest)
+ [self, a, b, rest]
+ end
+
+ def call(args) = 0.lead_rest(*args)
+
+ call([1, 2, 3, 4])
+}
+
+# test missing opts are nil initialized
+assert_equal '[[0, 1, nil, 3], [0, 1, nil, 3], [0, 1, nil, 3, []], [0, 1, nil, 3, []]]', %q{
+ public def lead_opts(a, b=binding.local_variable_get(:c), c=3)
+ [self, a, b, c]
+ end
+
+ public def opts_rest(a=raise, b=binding.local_variable_get(:c), c=3, *rest)
+ [self, a, b, c, rest]
+ end
+
+ def call(args)
+ [
+ 0.lead_opts(1),
+ 0.lead_opts(*args),
+
+ 0.opts_rest(1),
+ 0.opts_rest(*args),
+ ]
+ end
+
+ call([1])
+}
+
+# test filled optionals with unspecified keyword param
+assert_equal 'ok', %q{
+ def opt_rest_opt_kw(_=1, *, k: :ok) = k
+
+ def call = opt_rest_opt_kw(0)
+
+ call
+}
+
+# test splat empty array with rest param
+assert_equal '[0, 1, 2, []]', %q{
+ public def foo(a=1, b=2, *rest)
+ [self, a, b, rest]
+ end
+
+ def call(args) = 0.foo(*args)
+
+ call([])
+}
+
# Regression test for yielding with autosplat to block with
# optional parameters. https://github.com/Shopify/yjit/issues/313
assert_equal '[:a, :b, :a, :b]', %q{
diff --git a/yjit/src/codegen.rs b/yjit/src/codegen.rs
index 11d70a3e6b..919e1662b8 100644
--- a/yjit/src/codegen.rs
+++ b/yjit/src/codegen.rs
@@ -5011,7 +5011,6 @@ struct ControlFrame {
frame_type: u32,
specval: SpecVal,
cme: *const rb_callable_method_entry_t,
- local_size: i32
}
// Codegen performing a similar (but not identical) function to vm_push_frame
@@ -5034,10 +5033,7 @@ fn gen_push_frame(
asm: &mut Assembler,
set_sp_cfp: bool, // if true CFP and SP will be switched to the callee
frame: ControlFrame,
- rest_arg: Option<(i32, Opnd)>,
) {
- assert!(frame.local_size >= 0);
-
let sp = frame.sp;
asm.comment("push cme, specval, frame type");
@@ -5127,27 +5123,6 @@ fn gen_push_frame(
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_SELF), frame.recv);
asm.mov(cfp_opnd(RUBY_OFFSET_CFP_BLOCK_CODE), 0.into());
- // This Qnil fill snippet potentially requires 2 more registers on Arm, one for Qnil and
- // another for calculating the address in case there are a lot of local variables. So doing
- // this after releasing the register for specval and the receiver to avoid register spill.
- let num_locals = frame.local_size;
- if num_locals > 0 {
- asm.comment("initialize locals");
-
- // Initialize local variables to Qnil
- for i in 0..num_locals {
- let offs = SIZEOF_VALUE_I32 * (i - num_locals - 3);
- asm.store(Opnd::mem(64, sp, offs), Qnil.into());
- }
- }
-
- if let Some((opts_missing, rest_arg)) = rest_arg {
- // We want to set the rest_param just after the optional arguments
- let index = opts_missing - num_locals - 3;
- let offset = SIZEOF_VALUE_I32 * index;
- asm.store(Opnd::mem(64, sp, offset), rest_arg);
- }
-
// Spill stack temps to let the callee use them (must be done before changing SP)
asm.spill_temps();
@@ -5379,8 +5354,7 @@ fn gen_send_cfunc(
None // Leave PC uninitialized as cfuncs shouldn't read it
},
iseq: None,
- local_size: 0,
- }, None);
+ });
if !kw_arg.is_null() {
// Build a hash from all kwargs passed
@@ -5523,6 +5497,11 @@ fn move_rest_args_to_stack(array: Opnd, num_args: u32, asm: &mut Assembler) {
asm.cmp(array_len_opnd, num_args.into());
asm.jl(Target::side_exit(Counter::guard_send_iseq_has_rest_and_splat_not_equal));
+ // Unused operands cause the backend to panic
+ if num_args == 0 {
+ return;
+ }
+
asm.comment("Push arguments from array");
// Load the address of the embedded array
@@ -5589,7 +5568,7 @@ fn push_splat_args(required_args: u32, asm: &mut Assembler) {
);
let array_len_opnd = asm.csel_nz(emb_len_opnd, array_len_opnd);
- asm.comment("Side exit if length doesn't not equal remaining args");
+ asm.comment("Guard for expected splat length");
asm.cmp(array_len_opnd, required_args.into());
asm.jne(Target::side_exit(Counter::guard_send_splatarray_length_not_equal));
@@ -5693,19 +5672,28 @@ fn gen_send_iseq(
argc: i32,
captured_opnd: Option<Opnd>,
) -> Option<CodegenStatus> {
+ // Argument count. We will change this as we gather values from
+ // sources to satisfy the callee's parameters. To help make sense
+ // of changes, note that:
+ // - Parameters syntactically on the left have lower addresses.
+ // For example, all the lead (required) and optional parameters
+ // have lower addresses than the rest parameter array.
+ // - The larger the index one passes to Assembler::stack_opnd(),
+ // the *lower* the address.
let mut argc = argc;
- // When you have keyword arguments, there is an extra object that gets
- // placed on the stack the represents a bitmap of the keywords that were not
- // specified at the call site. We need to keep track of the fact that this
- // value is present on the stack in order to properly set up the callee's
- // stack pointer.
+ // Iseqs with keyword parameters have a hidden, unnamed parameter local
+ // that the callee could use to know which keywords are unspecified
+ // (see the `checkkeyword` instruction and check `ruby --dump=insn -e 'def foo(k:itself)=k'`).
+ // We always need to set up this local if the call goes through.
let doing_kw_call = unsafe { get_iseq_flags_has_kw(iseq) };
let supplying_kws = unsafe { vm_ci_flag(ci) & VM_CALL_KWARG } != 0;
let iseq_has_rest = unsafe { get_iseq_flags_has_rest(iseq) };
+ let iseq_has_block_param = unsafe { get_iseq_flags_has_block(iseq) };
- // For computing number of locals to set up for the callee
- let mut num_params = unsafe { get_iseq_body_param_size(iseq) };
+ // For computing offsets to callee locals
+ let num_params = unsafe { get_iseq_body_param_size(iseq) };
+ let num_locals = unsafe { get_iseq_body_local_table_size(iseq) as i32 };
let mut start_pc_offset: u16 = 0;
let required_num = unsafe { get_iseq_body_param_lead_num(iseq) };
@@ -5722,7 +5710,7 @@ fn gen_send_iseq(
// Arity handling and optional parameter setup
let mut opts_filled = argc - required_num - kw_arg_num;
let opt_num = unsafe { get_iseq_body_param_opt_num(iseq) };
- // We have a rest argument so there could be more args
+ // We have a rest parameter so there could be more args
// than are required + optional. Those will go in rest.
// So we cap ops_filled at opt_num.
if iseq_has_rest {
@@ -5744,7 +5732,6 @@ fn gen_send_iseq(
exit_if_has_kwrest(asm, iseq)?;
exit_if_splat_and_ruby2_keywords(asm, jit, flags)?;
exit_if_has_rest_and_captured(asm, iseq_has_rest, captured_opnd)?;
- exit_if_has_rest_and_splat(asm, iseq_has_rest, flags)?;
exit_if_has_rest_and_supplying_kws(asm, iseq_has_rest, iseq, supplying_kws)?;
exit_if_supplying_kw_and_has_no_kw(asm, supplying_kws, iseq)?;
exit_if_supplying_kws_and_accept_no_kwargs(asm, supplying_kws, iseq)?;
@@ -5756,9 +5743,9 @@ fn gen_send_iseq(
exit_if_unsupported_block_arg_type(asm, block_arg_type)?;
// Block parameter handling. This mirrors setup_parameters_complex().
- if unsafe { get_iseq_flags_has_block(iseq) } {
+ if iseq_has_block_param {
if unsafe { get_iseq_body_local_iseq(iseq) == iseq } {
- num_params -= 1;
+ // Do nothing
} else {
// In this case (param.flags.has_block && local_iseq != iseq),
// the block argument is setup as a local variable and requires
@@ -5768,15 +5755,6 @@ fn gen_send_iseq(
}
}
- // We will handle splat case later
- if opt_num > 0 && flags & VM_CALL_ARGS_SPLAT == 0 {
- num_params -= opts_missing as u32;
- unsafe {
- let opt_table = get_iseq_body_param_opt_table(iseq);
- start_pc_offset = (*opt_table.offset(opts_filled as isize)).try_into().unwrap();
- }
- }
-
if doing_kw_call {
// Here we're calling a method with keyword arguments and specifying
// keyword arguments at this call site.
@@ -5910,40 +5888,32 @@ fn gen_send_iseq(
None
};
- // If we have a rest, optional arguments, and a splat
- // some of those splatted args will end up filling the
- // optional arguments and some will potentially end up
- // in the rest. This calculates how many are filled
- // by the splat.
- let opts_filled_with_splat: Option<i32> = {
- if iseq_has_rest && opt_num > 0 {
- splat_array_length.map(|len| {
- let num_args = (argc - 1) + len as i32;
- if num_args >= required_num {
- min(num_args - required_num, opt_num)
- } else {
- 0
- }
- })
+ // Adjust `opts_filled` and `opts_missing` taking
+ // into account the size of the splat expansion.
+ if let Some(len) = splat_array_length {
+ assert_eq!(kw_arg_num, 0); // Due to exit_if_doing_kw_and_splat().
+ // Simplifies calculation below.
+ let num_args = (argc - 1) + len as i32;
+
+ opts_filled = if num_args >= required_num {
+ min(num_args - required_num, opt_num)
} else {
- None
- }
- };
+ 0
+ };
+ opts_missing = opt_num - opts_filled;
+ }
- // If we have optional arguments filled by the splat (see comment above)
- // we need to set a few variables concerning optional arguments
- // to their correct values, as well as set the pc_offset.
- if let Some(filled) = opts_filled_with_splat {
- opts_missing = opt_num - filled;
- opts_filled = filled;
- num_params -= opts_missing as u32;
+ assert_eq!(opts_missing + opts_filled, opt_num);
+ assert!(opts_filled >= 0);
- // We are going to jump to the correct offset based on how many optional
- // params are remaining.
+ // ISeq with optional paramters start at different
+ // locations depending on the number of optionals given.
+ if opt_num > 0 {
+ assert!(opts_filled >= 0);
unsafe {
let opt_table = get_iseq_body_param_opt_table(iseq);
- start_pc_offset = (*opt_table.offset(filled as isize)).try_into().unwrap();
- };
+ start_pc_offset = opt_table.offset(opts_filled as isize).read().try_into().unwrap();
+ }
}
// We will not have None from here. You can use stack_pop / stack_pop.
@@ -6004,9 +5974,6 @@ fn gen_send_iseq(
}
}
- // Number of locals that are not parameters
- let num_locals = unsafe { get_iseq_body_local_table_size(iseq) as i32 } - (num_params as i32) + if iseq_has_rest && opt_num != 0 { 1 } else { 0 };
-
// Stack overflow check
// Note that vm_push_frame checks it against a decremented cfp, hence the multiply by 2.
// #define CHECK_VM_STACK_OVERFLOW0(cfp, sp, margin)
@@ -6020,38 +5987,21 @@ fn gen_send_iseq(
// push_splat_args does stack manipulation so we can no longer side exit
if let Some(array_length) = splat_array_length {
- let remaining_opt = (opt_num as u32 + required_num as u32).saturating_sub(array_length + (argc as u32 - 1));
-
if !iseq_has_rest {
- if opt_num > 0 {
- // We are going to jump to the correct offset based on how many optional
- // params are remaining.
- unsafe {
- let opt_table = get_iseq_body_param_opt_table(iseq);
- let offset = (opt_num - remaining_opt as i32) as isize;
- start_pc_offset = (*opt_table.offset(offset)).try_into().unwrap();
- };
- }
-
- // We are going to assume that the splat fills
- // all the remaining arguments. In the generated code
- // we test if this is true and if not side exit.
- argc = argc - 1 + array_length as i32 + remaining_opt as i32;
+ // Speculate that future splats will be done with
+ // an array that has the same length. We will insert guards.
+ argc = argc - 1 + array_length as i32;
if argc + asm.ctx.get_stack_size() as i32 > MAX_SPLAT_LENGTH {
gen_counter_incr(asm, Counter::send_splat_too_long);
return None;
}
push_splat_args(array_length, asm);
-
- for _ in 0..remaining_opt {
- // We need to push nil for the optional arguments
- let stack_ret = asm.stack_push(Type::Unknown);
- asm.mov(stack_ret, Qnil.into());
- }
}
}
// This is a .send call and we need to adjust the stack
+ // TODO: This can be more efficient if we do it before
+ // extracting from the splat array above.
if flags & VM_CALL_OPT_SEND != 0 {
handle_opt_send_shift_stack(asm, argc);
}
@@ -6061,20 +6011,24 @@ fn gen_send_iseq(
jit_save_pc(jit, asm);
gen_save_sp(asm);
- if flags & VM_CALL_ARGS_SPLAT != 0 {
+ let rest_param_array = if flags & VM_CALL_ARGS_SPLAT != 0 {
let non_rest_arg_count = argc - 1;
// We start by dupping the array because someone else might have
- // a reference to it.
+ // a reference to it. This also normalizes to an ::Array instance.
let array = asm.stack_pop(1);
let array = asm.ccall(
rb_ary_dup as *const u8,
vec![array],
);
- if non_rest_arg_count > required_num + opt_num {
+ // This is the end stack state of all `non_rest_arg_count` situations below
+ argc = required_num + opts_filled;
+
+ if non_rest_arg_count > required_num + opt_num {
// If we have more arguments than required, we need to prepend
// the items from the stack onto the array.
- let diff = (non_rest_arg_count - (required_num + opts_filled_with_splat.unwrap_or(0))) as u32;
+ let diff: u32 = (non_rest_arg_count - (required_num + opt_num))
+ .try_into().unwrap();
// diff is >0 so no need to worry about null pointer
asm.comment("load pointer to array elements");
@@ -6089,37 +6043,35 @@ fn gen_send_iseq(
);
asm.stack_pop(diff as usize);
- let stack_ret = asm.stack_push(Type::TArray);
- asm.mov(stack_ret, array);
- // We now should have the required arguments
- // and an array of all the rest arguments
- argc = required_num + opts_filled_with_splat.unwrap_or(0) + 1;
+ array
} else if non_rest_arg_count < required_num + opt_num {
// If we have fewer arguments than required, we need to take some
// from the array and move them to the stack.
+ asm.comment("take items from splat array");
+
+ let diff: u32 = (required_num - non_rest_arg_count + opts_filled)
+ .try_into().unwrap();
- let diff = (required_num - non_rest_arg_count + opts_filled_with_splat.unwrap_or(0)) as u32;
// This moves the arguments onto the stack. But it doesn't modify the array.
move_rest_args_to_stack(array, diff, asm);
// We will now slice the array to give us a new array of the correct size
- let ret = asm.ccall(rb_yjit_rb_ary_subseq_length as *const u8, vec![array, Opnd::UImm(diff as u64)]);
- let stack_ret = asm.stack_push(Type::TArray);
- asm.mov(stack_ret, ret);
+ let sliced = asm.ccall(rb_yjit_rb_ary_subseq_length as *const u8, vec![array, Opnd::UImm(diff as u64)]);
- // We now should have the required arguments
- // and an array of all the rest arguments
- argc = required_num + opts_filled_with_splat.unwrap_or(0) + 1;
+ sliced
} else {
// The arguments are equal so we can just push to the stack
+ asm.comment("same length for splat array and rest param");
assert!(non_rest_arg_count == required_num + opt_num);
- let stack_ret = asm.stack_push(Type::TArray);
- asm.mov(stack_ret, array);
+
+ array
}
} else {
+ asm.comment("rest parameter without splat");
+
assert!(argc >= required_num);
let n = (argc - required_num - opts_filled) as u32;
- argc = required_num + opts_filled + 1;
+ argc = required_num + opts_filled;
// If n is 0, then elts is never going to be read, so we can just pass null
let values_ptr = if n == 0 {
Opnd::UImm(0)
@@ -6139,9 +6091,31 @@ fn gen_send_iseq(
]
);
asm.stack_pop(n.as_usize());
- let stack_ret = asm.stack_push(Type::CArray);
- asm.mov(stack_ret, new_ary);
- }
+
+ new_ary
+ };
+
+ // Find where to put the rest parameter array
+ let rest_param = if opts_missing == 0 {
+ // All optionals are filled, the rest param goes at the top of the stack
+ argc += 1;
+ asm.stack_push(Type::CArray)
+ } else {
+ // The top of the stack will be a missing optional, but the rest
+ // parameter needs to be placed after all the missing optionals.
+ // Place it using a stack operand with a negative stack index.
+ // (Higher magnitude negative stack index have higher address.)
+ assert!(opts_missing > 0);
+ // The argument deepest in the stack will be the 0th local in the callee.
+ let callee_locals_base = argc - 1;
+ let rest_param_stack_idx = callee_locals_base - required_num - opt_num;
+ assert!(rest_param_stack_idx < 0);
+ asm.stack_opnd(rest_param_stack_idx)
+ };
+ // Store rest param to memory to avoid register shuffle as
+ // we won't be reading it for the remainder of the block.
+ asm.ctx.dealloc_temp_reg(rest_param.stack_idx());
+ asm.store(rest_param, rest_param_array);
}
if doing_kw_call {
@@ -6311,16 +6285,47 @@ fn gen_send_iseq(
argc = lead_num;
}
- // If we have a rest param and optional parameters,
- // we don't actually pass the rest parameter as an argument,
- // instead we set its value in the callee's locals
- let rest_param = if iseq_has_rest && opt_num != 0 {
- argc -= 1;
- let top = asm.stack_pop(1);
- Some((opts_missing as i32, asm.load(top)))
- } else {
- None
- };
+ fn nil_fill(comment: &'static str, fill_range: std::ops::Range<isize>, asm: &mut Assembler) {
+ if fill_range.is_empty() {
+ return;
+ }
+
+ asm.comment(comment);
+ for i in fill_range {
+ let value_slot = asm.ctx.sp_opnd(i * SIZEOF_VALUE as isize);
+ asm.store(value_slot, Qnil.into());
+ }
+ }
+
+ // Nil-initialize missing optional parameters
+ nil_fill(
+ "nil-initialize missing optionals",
+ {
+ let begin = -(argc as isize) + required_num as isize + opts_filled as isize;
+ let end = -(argc as isize) + required_num as isize + opt_num as isize;
+
+ begin..end
+ },
+ asm
+ );
+ // Nil-initialize the block parameter. It's the last parameter local
+ if iseq_has_block_param {
+ let block_param = asm.ctx.sp_opnd(
+ SIZEOF_VALUE as isize * (-(argc as isize) + num_params as isize - 1)
+ );
+ asm.store(block_param, Qnil.into());
+ }
+ // Nil-initialize non-parameter locals
+ nil_fill(
+ "nil-initialize locals",
+ {
+ let begin = -(argc as isize) + num_params as isize;
+ let end = -(argc as isize) + num_locals as isize;
+
+ begin..end
+ },
+ asm
+ );
// Points to the receiver operand on the stack unless a captured environment is used
let recv = match captured_opnd {
@@ -6339,8 +6344,9 @@ fn gen_send_iseq(
jit_save_pc(jit, asm);
// Adjust the callee's stack pointer
- let offs =
- (SIZEOF_VALUE as isize) * (3 + (num_locals as isize) + if doing_kw_call { 1 } else { 0 });
+ let offs = (SIZEOF_VALUE as isize) * (
+ -(argc as isize) + num_locals as isize + VM_ENV_DATA_SIZE as isize
+ );
let callee_sp = asm.lea(asm.ctx.sp_opnd(offs));
let specval = if let Some(prev_ep) = prev_ep {
@@ -6367,8 +6373,7 @@ fn gen_send_iseq(
sp: callee_sp,
iseq: Some(iseq),
pc: None, // We are calling into jitted code, which will set the PC as necessary
- local_size: num_locals
- }, rest_param);
+ });
// No need to set cfp->pc since the callee sets it whenever calling into routines
// that could look at it through jit_save_pc().
@@ -6488,11 +6493,6 @@ fn exit_if_has_rest_and_captured(asm: &mut Assembler, iseq_has_rest: bool, captu
}
#[must_use]
-fn exit_if_has_rest_and_splat( asm: &mut Assembler, iseq_has_rest: bool, flags: u32) -> Option<()> {
- exit_if(asm, iseq_has_rest && flags & VM_CALL_ARGS_SPLAT != 0, Counter::send_iseq_has_rest_and_splat)
-}
-
-#[must_use]
fn exit_if_has_rest_and_supplying_kws(asm: &mut Assembler, iseq_has_rest: bool, iseq: *const rb_iseq_t, supplying_kws: bool) -> Option<()> {
exit_if(
asm,