Skip to content

ZJIT: optimize complex argument parameter features for keywords #866

Description

@rwstauner

From liquid-render:

Top-6 popular complex argument-parameter features not optimized (100.0% of total 3,997,781):
         param_kw: 1,476,228 (36.9%)
     caller_splat: 1,461,387 (36.6%)
     caller_kwarg:   808,697 (20.2%)
        param_opt:   191,528 ( 4.8%)
  caller_blockarg:    29,971 ( 0.7%)
       param_rest:    29,970 ( 0.7%)

This involves several complex scenarios for when the callee takes keyword params.

I added some tests for these:

This will require setup like what is done in the interpreter's setup_parameters_complex:

ruby/vm_args.c

Lines 590 to 976 in f100298

static int
setup_parameters_complex(rb_execution_context_t * const ec, const rb_iseq_t * const iseq,
struct rb_calling_info *const calling,
const struct rb_callinfo *ci,
VALUE * const locals, const enum arg_setup_type arg_setup_type)
{
const int min_argc = ISEQ_BODY(iseq)->param.lead_num + ISEQ_BODY(iseq)->param.post_num;
const int max_argc = (ISEQ_BODY(iseq)->param.flags.has_rest == FALSE) ? min_argc + ISEQ_BODY(iseq)->param.opt_num : UNLIMITED_ARGUMENTS;
int given_argc;
unsigned int ci_flag = vm_ci_flag(ci);
unsigned int kw_flag = ci_flag & (VM_CALL_KWARG | VM_CALL_KW_SPLAT | VM_CALL_KW_SPLAT_MUT);
int opt_pc = 0, allow_autosplat = !kw_flag;
struct args_info args_body, *args;
VALUE keyword_hash = Qnil;
VALUE * const orig_sp = ec->cfp->sp;
unsigned int i;
VALUE flag_keyword_hash = 0;
VALUE splat_flagged_keyword_hash = 0;
VALUE converted_keyword_hash = 0;
VALUE rest_last = 0;
const rb_callable_method_entry_t *cme = calling->cc ? vm_cc_cme(calling->cc) : NULL;
vm_check_canary(ec, orig_sp);
/*
* Extend SP for GC.
*
* [pushed values] [uninitialized values]
* <- ci->argc -->
* <- ISEQ_BODY(iseq)->param.size------------>
* ^ locals ^ sp
*
* =>
* [pushed values] [initialized values ]
* <- ci->argc -->
* <- ISEQ_BODY(iseq)->param.size------------>
* ^ locals ^ sp
*/
for (i=calling->argc; i<ISEQ_BODY(iseq)->param.size; i++) {
locals[i] = Qnil;
}
ec->cfp->sp = &locals[i];
/* setup args */
args = &args_body;
given_argc = args->argc = calling->argc;
args->argv = locals;
args->rest_dupped = ci_flag & VM_CALL_ARGS_SPLAT_MUT;
if (UNLIKELY(ISEQ_BODY(iseq)->param.flags.anon_rest)) {
if ((ci_flag & VM_CALL_ARGS_SPLAT) &&
given_argc == ISEQ_BODY(iseq)->param.lead_num + (kw_flag ? 2 : 1) &&
!ISEQ_BODY(iseq)->param.flags.has_opt &&
!ISEQ_BODY(iseq)->param.flags.has_post &&
!ISEQ_BODY(iseq)->param.flags.ruby2_keywords &&
(!kw_flag ||
!ISEQ_BODY(iseq)->param.flags.has_kw ||
!ISEQ_BODY(iseq)->param.flags.has_kwrest ||
!ISEQ_BODY(iseq)->param.flags.accepts_no_kwarg)) {
args->rest_dupped = true;
}
}
if (kw_flag & VM_CALL_KWARG) {
args->kw_arg = vm_ci_kwarg(ci);
if (ISEQ_BODY(iseq)->param.flags.has_kw) {
int kw_len = args->kw_arg->keyword_len;
/* copy kw_argv */
args->kw_argv = ALLOCA_N(VALUE, kw_len);
args->argc -= kw_len;
given_argc -= kw_len;
MEMCPY(args->kw_argv, locals + args->argc, VALUE, kw_len);
}
else {
args->kw_argv = NULL;
given_argc = args_kw_argv_to_hash(args);
kw_flag |= VM_CALL_KW_SPLAT | VM_CALL_KW_SPLAT_MUT;
}
}
else {
args->kw_arg = NULL;
args->kw_argv = NULL;
}
if ((ci_flag & VM_CALL_ARGS_SPLAT) && (ci_flag & VM_CALL_KW_SPLAT)) {
// f(*a, **kw)
args->rest_index = 0;
keyword_hash = locals[--args->argc];
args->rest = locals[--args->argc];
if (ignore_keyword_hash_p(keyword_hash, iseq, &kw_flag, &converted_keyword_hash)) {
keyword_hash = Qnil;
}
else if (UNLIKELY(ISEQ_BODY(iseq)->param.flags.ruby2_keywords)) {
converted_keyword_hash = check_kwrestarg(converted_keyword_hash, &kw_flag);
flag_keyword_hash = converted_keyword_hash;
arg_rest_dup(args);
rb_ary_push(args->rest, converted_keyword_hash);
keyword_hash = Qnil;
}
else if (!ISEQ_BODY(iseq)->param.flags.has_kwrest && !ISEQ_BODY(iseq)->param.flags.has_kw) {
converted_keyword_hash = check_kwrestarg(converted_keyword_hash, &kw_flag);
if (ISEQ_BODY(iseq)->param.flags.has_rest) {
arg_rest_dup(args);
rb_ary_push(args->rest, converted_keyword_hash);
keyword_hash = Qnil;
}
else {
// Avoid duping rest when not necessary
// Copy rest elements and converted keyword hash directly to VM stack
const VALUE *argv = RARRAY_CONST_PTR(args->rest);
int j, i=args->argc, rest_len = RARRAY_LENINT(args->rest);
if (rest_len) {
CHECK_VM_STACK_OVERFLOW(ec->cfp, rest_len+1);
given_argc += rest_len;
args->argc += rest_len;
for (j=0; rest_len > 0; rest_len--, i++, j++) {
locals[i] = argv[j];
}
}
locals[i] = converted_keyword_hash;
given_argc--;
args->argc++;
args->rest = Qfalse;
ci_flag &= ~(VM_CALL_ARGS_SPLAT|VM_CALL_KW_SPLAT);
keyword_hash = Qnil;
goto arg_splat_and_kw_splat_flattened;
}
}
else {
keyword_hash = converted_keyword_hash;
}
int len = RARRAY_LENINT(args->rest);
given_argc += len - 2;
}
else if (ci_flag & VM_CALL_ARGS_SPLAT) {
// f(*a)
args->rest_index = 0;
args->rest = locals[--args->argc];
int len = RARRAY_LENINT(args->rest);
given_argc += len - 1;
if (!kw_flag && len > 0) {
rest_last = RARRAY_AREF(args->rest, len - 1);
if (RB_TYPE_P(rest_last, T_HASH) && FL_TEST_RAW(rest_last, RHASH_PASS_AS_KEYWORDS)) {
// def f(**kw); a = [..., kw]; g(*a)
splat_flagged_keyword_hash = rest_last;
if (!(RHASH_EMPTY_P(rest_last) || ISEQ_BODY(iseq)->param.flags.has_kw) || (ISEQ_BODY(iseq)->param.flags.has_kwrest)) {
rest_last = rb_hash_dup(rest_last);
}
kw_flag |= VM_CALL_KW_SPLAT | VM_CALL_KW_SPLAT_MUT;
// Unset rest_dupped set by anon_rest as we may need to modify splat in this case
args->rest_dupped = false;
if (ignore_keyword_hash_p(rest_last, iseq, &kw_flag, &converted_keyword_hash)) {
if (ISEQ_BODY(iseq)->param.flags.has_rest) {
// Only duplicate/modify splat array if it will be used
arg_rest_dup(args);
rb_ary_pop(args->rest);
}
else if (arg_setup_type == arg_setup_block && !ISEQ_BODY(iseq)->param.flags.has_kwrest) {
// Avoid hash allocation for empty hashes
// Copy rest elements except empty keyword hash directly to VM stack
flatten_rest_args(ec, args, locals, &ci_flag);
keyword_hash = Qnil;
kw_flag = 0;
}
given_argc--;
}
else if (!ISEQ_BODY(iseq)->param.flags.has_rest) {
// Avoid duping rest when not necessary
// Copy rest elements and converted keyword hash directly to VM stack
flatten_rest_args(ec, args, locals, &ci_flag);
if (ISEQ_BODY(iseq)->param.flags.has_kw || ISEQ_BODY(iseq)->param.flags.has_kwrest) {
given_argc--;
keyword_hash = converted_keyword_hash;
}
else {
locals[args->argc] = converted_keyword_hash;
args->argc += 1;
keyword_hash = Qnil;
kw_flag = 0;
}
}
else {
if (rest_last != converted_keyword_hash) {
rest_last = converted_keyword_hash;
arg_rest_dup(args);
RARRAY_ASET(args->rest, len - 1, rest_last);
}
if (ISEQ_BODY(iseq)->param.flags.ruby2_keywords && rest_last) {
flag_keyword_hash = rest_last;
}
else if (ISEQ_BODY(iseq)->param.flags.has_kw || ISEQ_BODY(iseq)->param.flags.has_kwrest) {
arg_rest_dup(args);
rb_ary_pop(args->rest);
given_argc--;
keyword_hash = rest_last;
}
}
}
}
}
else {
args->rest = Qfalse;
if (args->argc > 0 && (kw_flag & VM_CALL_KW_SPLAT)) {
// f(**kw)
VALUE last_arg = args->argv[args->argc-1];
if (ignore_keyword_hash_p(last_arg, iseq, &kw_flag, &converted_keyword_hash)) {
args->argc--;
given_argc--;
}
else {
if (!(kw_flag & VM_CALL_KW_SPLAT_MUT) && !ISEQ_BODY(iseq)->param.flags.has_kw) {
converted_keyword_hash = rb_hash_dup(converted_keyword_hash);
kw_flag |= VM_CALL_KW_SPLAT_MUT;
}
if (last_arg != converted_keyword_hash) {
last_arg = converted_keyword_hash;
args->argv[args->argc-1] = last_arg;
}
if (ISEQ_BODY(iseq)->param.flags.ruby2_keywords) {
flag_keyword_hash = last_arg;
}
else if (ISEQ_BODY(iseq)->param.flags.has_kw || ISEQ_BODY(iseq)->param.flags.has_kwrest) {
args->argc--;
given_argc--;
keyword_hash = last_arg;
}
}
}
}
if (flag_keyword_hash) {
FL_SET_RAW(flag_keyword_hash, RHASH_PASS_AS_KEYWORDS);
}
arg_splat_and_kw_splat_flattened:
if (kw_flag && ISEQ_BODY(iseq)->param.flags.accepts_no_kwarg) {
rb_raise(rb_eArgError, "no keywords accepted");
}
switch (arg_setup_type) {
case arg_setup_method:
break; /* do nothing special */
case arg_setup_block:
if (given_argc == 1 &&
allow_autosplat &&
!splat_flagged_keyword_hash &&
(min_argc > 0 || ISEQ_BODY(iseq)->param.opt_num > 1) &&
!ISEQ_BODY(iseq)->param.flags.ambiguous_param0 &&
!((ISEQ_BODY(iseq)->param.flags.has_kw ||
ISEQ_BODY(iseq)->param.flags.has_kwrest)
&& max_argc == 1) &&
args_check_block_arg0(args)) {
given_argc = RARRAY_LENINT(args->rest);
}
break;
}
/* argc check */
if (given_argc < min_argc) {
if (arg_setup_type == arg_setup_block) {
CHECK_VM_STACK_OVERFLOW(ec->cfp, min_argc);
given_argc = min_argc;
args_extend(args, min_argc);
}
else {
argument_arity_error(ec, iseq, cme, given_argc, min_argc, max_argc);
}
}
if (given_argc > max_argc && max_argc != UNLIMITED_ARGUMENTS) {
if (arg_setup_type == arg_setup_block) {
/* truncate */
args_reduce(args, given_argc - max_argc);
given_argc = max_argc;
}
else {
argument_arity_error(ec, iseq, cme, given_argc, min_argc, max_argc);
}
}
if (ISEQ_BODY(iseq)->param.flags.has_lead) {
args_setup_lead_parameters(args, ISEQ_BODY(iseq)->param.lead_num, locals + 0);
}
if (ISEQ_BODY(iseq)->param.flags.has_rest || ISEQ_BODY(iseq)->param.flags.has_post){
args_copy(args);
}
if (ISEQ_BODY(iseq)->param.flags.has_post) {
args_setup_post_parameters(args, ISEQ_BODY(iseq)->param.post_num, locals + ISEQ_BODY(iseq)->param.post_start);
}
if (ISEQ_BODY(iseq)->param.flags.has_opt) {
int opt = args_setup_opt_parameters(args, ISEQ_BODY(iseq)->param.opt_num, locals + ISEQ_BODY(iseq)->param.lead_num);
opt_pc = (int)ISEQ_BODY(iseq)->param.opt_table[opt];
}
if (ISEQ_BODY(iseq)->param.flags.has_rest) {
if (UNLIKELY(ISEQ_BODY(iseq)->param.flags.anon_rest && args->argc == 0 && !args->rest && !ISEQ_BODY(iseq)->param.flags.has_post)) {
*(locals + ISEQ_BODY(iseq)->param.rest_start) = args->rest = rb_cArray_empty_frozen;
args->rest_index = 0;
}
else {
args_setup_rest_parameter(args, locals + ISEQ_BODY(iseq)->param.rest_start);
VALUE ary = *(locals + ISEQ_BODY(iseq)->param.rest_start);
VALUE index = RARRAY_LEN(ary) - 1;
if (splat_flagged_keyword_hash &&
!ISEQ_BODY(iseq)->param.flags.ruby2_keywords &&
!ISEQ_BODY(iseq)->param.flags.has_kw &&
!ISEQ_BODY(iseq)->param.flags.has_kwrest &&
RARRAY_AREF(ary, index) == splat_flagged_keyword_hash) {
((struct RHash *)rest_last)->basic.flags &= ~RHASH_PASS_AS_KEYWORDS;
RARRAY_ASET(ary, index, rest_last);
}
}
}
if (ISEQ_BODY(iseq)->param.flags.has_kw) {
VALUE * const klocals = locals + ISEQ_BODY(iseq)->param.keyword->bits_start - ISEQ_BODY(iseq)->param.keyword->num;
if (args->kw_argv != NULL) {
const struct rb_callinfo_kwarg *kw_arg = args->kw_arg;
args_setup_kw_parameters(ec, iseq, cme, args->kw_argv, kw_arg->keyword_len, kw_arg->keywords, klocals);
}
else if (!NIL_P(keyword_hash)) {
bool remove_hash_value = false;
if (ISEQ_BODY(iseq)->param.flags.has_kwrest) {
keyword_hash = check_kwrestarg(keyword_hash, &kw_flag);
remove_hash_value = true;
}
args_setup_kw_parameters_from_kwsplat(ec, iseq, cme, keyword_hash, klocals, remove_hash_value);
}
else {
#if VM_CHECK_MODE > 0
if (args_argc(args) != 0) {
VM_ASSERT(ci_flag & VM_CALL_ARGS_SPLAT);
VM_ASSERT(!(ci_flag & (VM_CALL_KWARG | VM_CALL_KW_SPLAT | VM_CALL_KW_SPLAT_MUT)));
VM_ASSERT(!kw_flag);
VM_ASSERT(!ISEQ_BODY(iseq)->param.flags.has_rest);
VM_ASSERT(RARRAY_LENINT(args->rest) > 0);
VM_ASSERT(RB_TYPE_P(rest_last, T_HASH));
VM_ASSERT(FL_TEST_RAW(rest_last, RHASH_PASS_AS_KEYWORDS));
VM_ASSERT(args_argc(args) == 1);
}
#endif
args_setup_kw_parameters(ec, iseq, cme, NULL, 0, NULL, klocals);
}
}
else if (ISEQ_BODY(iseq)->param.flags.has_kwrest) {
args_setup_kw_rest_parameter(keyword_hash, locals + ISEQ_BODY(iseq)->param.keyword->rest_start,
kw_flag, ISEQ_BODY(iseq)->param.flags.anon_kwrest);
}
else if (!NIL_P(keyword_hash) && RHASH_SIZE(keyword_hash) > 0 && arg_setup_type == arg_setup_method) {
argument_kw_error(ec, iseq, cme, "unknown", rb_hash_keys(keyword_hash));
}
if (ISEQ_BODY(iseq)->param.flags.has_block) {
if (ISEQ_BODY(iseq)->local_iseq == iseq) {
/* Do nothing */
}
else {
args_setup_block_parameter(ec, calling, locals + ISEQ_BODY(iseq)->param.block_start);
}
}
#if 0
{
int i;
for (i=0; i<ISEQ_BODY(iseq)->param.size; i++) {
ruby_debug_printf("local[%d] = %p\n", i, (void *)locals[i]);
}
}
#endif
ec->cfp->sp = orig_sp;
return opt_pc;
}

and yjit's gen_iseq_kw_call:

ruby/yjit/src/codegen.rs

Lines 8426 to 8663 in f100298

// Codegen for keyword argument handling. Essentially private to gen_send_iseq() since
// there are a lot of preconditions to check before reaching this code.
fn gen_iseq_kw_call(
jit: &mut JITState,
asm: &mut Assembler,
ci_kwarg: *const rb_callinfo_kwarg,
iseq: *const rb_iseq_t,
mut argc: i32,
has_kwrest: bool,
) -> i32 {
let caller_keyword_len_i32: i32 = if ci_kwarg.is_null() {
0
} else {
unsafe { get_cikw_keyword_len(ci_kwarg) }
};
let caller_keyword_len: usize = caller_keyword_len_i32.try_into().unwrap();
let anon_kwrest = unsafe { rb_get_iseq_flags_anon_kwrest(iseq) && !get_iseq_flags_has_kw(iseq) };
// This struct represents the metadata about the callee-specified
// keyword parameters.
let keyword = unsafe { get_iseq_body_param_keyword(iseq) };
asm_comment!(asm, "keyword args");
// This is the list of keyword arguments that the callee specified
// in its initial declaration.
let callee_kwargs = unsafe { (*keyword).table };
let callee_kw_count_i32: i32 = unsafe { (*keyword).num };
let callee_kw_count: usize = callee_kw_count_i32.try_into().unwrap();
let keyword_required_num: usize = unsafe { (*keyword).required_num }.try_into().unwrap();
// Here we're going to build up a list of the IDs that correspond to
// the caller-specified keyword arguments. If they're not in the
// same order as the order specified in the callee declaration, then
// we're going to need to generate some code to swap values around
// on the stack.
let mut kwargs_order: Vec<ID> = vec![0; cmp::max(caller_keyword_len, callee_kw_count)];
for kwarg_idx in 0..caller_keyword_len {
let sym = unsafe { get_cikw_keywords_idx(ci_kwarg, kwarg_idx.try_into().unwrap()) };
kwargs_order[kwarg_idx] = unsafe { rb_sym2id(sym) };
}
let mut unspecified_bits = 0;
// The stack_opnd() index to the 0th keyword argument.
let kwargs_stack_base = caller_keyword_len_i32 - 1;
// Build the keyword rest parameter hash before we make any changes to the order of
// the supplied keyword arguments
let kwrest_type = if has_kwrest {
c_callable! {
fn build_kw_rest(rest_mask: u64, stack_kwargs: *const VALUE, keywords: *const rb_callinfo_kwarg) -> VALUE {
if keywords.is_null() {
return unsafe { rb_hash_new() };
}
// Use the total number of supplied keywords as a size upper bound
let keyword_len = unsafe { (*keywords).keyword_len } as usize;
let hash = unsafe { rb_hash_new_with_size(keyword_len as u64) };
// Put pairs into the kwrest hash as the mask describes
for kwarg_idx in 0..keyword_len {
if (rest_mask & (1 << kwarg_idx)) != 0 {
unsafe {
let keyword_symbol = (*keywords).keywords.as_ptr().add(kwarg_idx).read();
let keyword_value = stack_kwargs.add(kwarg_idx).read();
rb_hash_aset(hash, keyword_symbol, keyword_value);
}
}
}
return hash;
}
}
asm_comment!(asm, "build kwrest hash");
// Make a bit mask describing which keywords should go into kwrest.
let mut rest_mask: u64 = 0;
// Index for one argument that will go into kwrest.
let mut rest_collected_idx = None;
for (supplied_kw_idx, &supplied_kw) in kwargs_order.iter().take(caller_keyword_len).enumerate() {
let mut found = false;
for callee_idx in 0..callee_kw_count {
let callee_kw = unsafe { callee_kwargs.add(callee_idx).read() };
if callee_kw == supplied_kw {
found = true;
break;
}
}
if !found {
rest_mask |= 1 << supplied_kw_idx;
if rest_collected_idx.is_none() {
rest_collected_idx = Some(supplied_kw_idx as i32);
}
}
}
let (kwrest, kwrest_type) = if rest_mask == 0 && anon_kwrest {
// In case the kwrest hash should be empty and is anonymous in the callee,
// we can pass nil instead of allocating. Anonymous kwrest can only be
// delegated, and nil is the same as an empty hash when delegating.
(Qnil.into(), Type::Nil)
} else {
// Save PC and SP before allocating
jit_save_pc(jit, asm);
gen_save_sp(asm);
// Build the kwrest hash. `struct rb_callinfo_kwarg` is malloc'd, so no GC concerns.
let kwargs_start = asm.lea(asm.ctx.sp_opnd(-caller_keyword_len_i32));
let hash = asm.ccall(
build_kw_rest as _,
vec![rest_mask.into(), kwargs_start, Opnd::const_ptr(ci_kwarg.cast())]
);
(hash, Type::THash)
};
// The kwrest parameter sits after `unspecified_bits` if the callee specifies any
// keywords.
let stack_kwrest_idx = kwargs_stack_base - callee_kw_count_i32 - i32::from(callee_kw_count > 0);
let stack_kwrest = asm.stack_opnd(stack_kwrest_idx);
// If `stack_kwrest` already has another argument there, we need to stow it elsewhere
// first before putting kwrest there. Use `rest_collected_idx` because that value went
// into kwrest so the slot is now free.
let kwrest_idx = callee_kw_count + usize::from(callee_kw_count > 0);
if let (Some(rest_collected_idx), true) = (rest_collected_idx, kwrest_idx < caller_keyword_len) {
let rest_collected = asm.stack_opnd(kwargs_stack_base - rest_collected_idx);
let mapping = asm.ctx.get_opnd_mapping(stack_kwrest.into());
asm.mov(rest_collected, stack_kwrest);
asm.ctx.set_opnd_mapping(rest_collected.into(), mapping);
// Update our bookkeeping to inform the reordering step later.
kwargs_order[rest_collected_idx as usize] = kwargs_order[kwrest_idx];
kwargs_order[kwrest_idx] = 0;
}
// Put kwrest straight into memory, since we might pop it later
asm.ctx.dealloc_reg(stack_kwrest.reg_opnd());
asm.mov(stack_kwrest, kwrest);
if stack_kwrest_idx >= 0 {
asm.ctx.set_opnd_mapping(stack_kwrest.into(), TempMapping::MapToStack(kwrest_type));
}
Some(kwrest_type)
} else {
None
};
// Ensure the stack is large enough for the callee
for _ in caller_keyword_len..callee_kw_count {
argc += 1;
asm.stack_push(Type::Unknown);
}
// Now this is the stack_opnd() index to the 0th keyword argument.
let kwargs_stack_base = kwargs_order.len() as i32 - 1;
// Next, we're going to loop through every keyword that was
// specified by the caller and make sure that it's in the correct
// place. If it's not we're going to swap it around with another one.
for kwarg_idx in 0..callee_kw_count {
let callee_kwarg = unsafe { callee_kwargs.add(kwarg_idx).read() };
// If the argument is already in the right order, then we don't
// need to generate any code since the expected value is already
// in the right place on the stack.
if callee_kwarg == kwargs_order[kwarg_idx] {
continue;
}
// In this case the argument is not in the right place, so we
// need to find its position where it _should_ be and swap with
// that location.
for swap_idx in 0..kwargs_order.len() {
if callee_kwarg == kwargs_order[swap_idx] {
// First we're going to generate the code that is going
// to perform the actual swapping at runtime.
let swap_idx_i32: i32 = swap_idx.try_into().unwrap();
let kwarg_idx_i32: i32 = kwarg_idx.try_into().unwrap();
let offset0 = kwargs_stack_base - swap_idx_i32;
let offset1 = kwargs_stack_base - kwarg_idx_i32;
stack_swap(asm, offset0, offset1);
// Next we're going to do some bookkeeping on our end so
// that we know the order that the arguments are
// actually in now.
kwargs_order.swap(kwarg_idx, swap_idx);
break;
}
}
}
// Now that every caller specified kwarg is in the right place, filling
// in unspecified default paramters won't overwrite anything.
for kwarg_idx in keyword_required_num..callee_kw_count {
if kwargs_order[kwarg_idx] != unsafe { callee_kwargs.add(kwarg_idx).read() } {
let default_param_idx = kwarg_idx - keyword_required_num;
let mut default_value = unsafe { (*keyword).default_values.add(default_param_idx).read() };
if default_value == Qundef {
// Qundef means that this value is not constant and must be
// recalculated at runtime, so we record it in unspecified_bits
// (Qnil is then used as a placeholder instead of Qundef).
unspecified_bits |= 0x01 << default_param_idx;
default_value = Qnil;
}
let default_param = asm.stack_opnd(kwargs_stack_base - kwarg_idx as i32);
let param_type = Type::from(default_value);
asm.mov(default_param, default_value.into());
asm.ctx.set_opnd_mapping(default_param.into(), TempMapping::MapToStack(param_type));
}
}
// Pop extra arguments that went into kwrest now that they're at stack top
if has_kwrest && caller_keyword_len > callee_kw_count {
let extra_kwarg_count = caller_keyword_len - callee_kw_count;
asm.stack_pop(extra_kwarg_count);
argc = argc - extra_kwarg_count as i32;
}
// Keyword arguments cause a special extra local variable to be
// pushed onto the stack that represents the parameters that weren't
// explicitly given a value and have a non-constant default.
if callee_kw_count > 0 {
let unspec_opnd = VALUE::fixnum_from_usize(unspecified_bits).as_u64();
let top = asm.stack_push(Type::Fixnum);
asm.mov(top, unspec_opnd.into());
argc += 1;
}
// The kwrest parameter sits after `unspecified_bits`
if let Some(kwrest_type) = kwrest_type {
let kwrest = asm.stack_push(kwrest_type);
// We put the kwrest parameter in memory earlier
asm.ctx.dealloc_reg(kwrest.reg_opnd());
argc += 1;
}
argc
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions