Skip to content

ZJIT: optimize complex argument parameter features for keywords #866

@rwstauner

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
}

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