Skip to content

Commit 874a68f

Browse files
committed
ZJIT: Inline def foo(...) callees and compile the bar(...) inside them
Argument-forwarding delegators are everywhere in Rails -- `Module#delegate`, `SimpleDelegator`, `ActiveRecord::Delegation` -- and every one of them costs a `rb_vm_sendforward()` call out of JIT code, 664K of them per lobsters run. The callinfo a `bar(...)` needs to replay the call lives in the `...` local, which only the caller knows, so the standalone compilation of a forwardable ISEQ can never see it. Inlining the callee is what makes it a compile-time constant. Two halves: * `can_inline` now accepts a forwardable callee, and `inline_methods` maps its sole parameter -- the `...` local -- to an `Insn::ForwardingCallInfo` carrying the call site's callinfo. `Insn::PushInlineFrame` grows the frame by the call site's argument count and copies the arguments and the callinfo into it, the layout `vm_call_iseq_forwardable` builds. That layout is not optional: a side exit lands the interpreter on the `sendforward`, whose `vm_adjust_stack_forwarding` reads the arguments back out from below the frame at `lep - (local_table_size + argc + 2)`. `FrameState` records the extension so a JITFrame stack map still finds the caller's slots below the taller frame. * `specialize_send_forward` merges the site's callinfo with the caller's the way `vm_caller_setup_fwd_args` does -- site method name, site arguments followed by the caller's, caller's keyword table, `VM_CALL_ARGS_SIMPLE` dropped -- and emits a `SendDirect`. The merged shape travels as a `CallShape` rather than a synthesized `rb_callinfo`: there is no object to point at and no way to root one. A site this declines keeps its `Insn::SendForward`, which the interpreter still completes out of the frame extension; it never becomes a generic `Send`, whose `cd` would report the wrong argument count. Chained forwarding falls out of `CallShape::ci` being `None`, since a forwardable target's `...` local has to receive a real callinfo. A block handed to the forwarder is read back out of the frame's EP and installed as the merged call's, matching `bh = VM_ENV_BLOCK_HANDLER(GET_LEP())`; re-deriving a literal block would capture the wrong frame. `sendforward` gains a ZJIT profiling variant so the merged call has a receiver type to guard, skipping the top-of-stack slot: the `...` local is a callinfo pointer, not a `VALUE`. `super(...)` is untouched -- `vm_search_super_method` rebuilds the callinfo at run time -- and `invokesuperforward` is out of scope. lobsters, 15 iterations: sendforward fallbacks 663,770 -> 507,391 (-24%), dynamic_send_count 8,293,928 -> 8,116,616 (-2.1%), code_region_bytes +0.6%. The 446K that remain run in forwardable frames reached through megamorphic call sites, which are compiled standalone and so still have no callinfo to merge. Ported from zjit/all (e307fee). Adaptations for this branch: * `PushInlineFrame` has no `guard_state` field here, so `forwarded` is the only field added and the operand visitor and the codegen arm drop it. * `type_specialize`'s `freeze`/`-@` arms are this branch's `try_rewrite_freeze` and `try_rewrite_uminus`; only the new `SendForward` arm is added ahead of them. * `super`'s direct-send path was rewritten here by the visibility work, so the `CallShape::from_ci(ci)` threading lands in `emit_specialized_super` instead. * `Function::guard_profiled_type` does not exist here; the merged send's receiver guard is the `guard_type_recompile(Type::from_profiled_type(..))` that the ordinary `Send` path uses. * `FrameState` still derives `Clone` and `profile.rs` still keeps `opnd_types` in a `Vec`, so `profile_operands_below_top` sizes it with `resize()`. * The bindings gain `YARVINSN_zjit_sendforward` at 233 and `VM_INSTRUCTION_SIZE` 260 -> 261 in both yjit/ and zjit/, hand-patched (no bindgen here) and checked against `RubyVM::INSTRUCTION_NAMES`. * `PushInlineFrame`'s codegen and `build_stack_map` keep taking an owned, resolved `FrameState` from `Function::frame_state` rather than upstream's `frame_state_ref`. On this branch `frame_state_ref` documents itself as returning the stored state with *stale* operand ids, and both callers feed those ids to `jit.get_opnd`: the borrowed form panics with "Failed to get_opnd" on the stack-overflow-check side exit as soon as an inlined frame's snapshot holds a value that was later replaced. [reorder port note] Pulled forward from stack position 48 to position 10, onto a base that predates roughly 38 of the commits its diff carried context from. What changed relative to the original: * Dropped as foreign context, none of it present in this stack: the `super` specialization helpers (`emit_super_call_guards`, `emit_super_chain`, `emit_specialized_super`, `UnwrapSvar`), `yield`/block-handler inlining (`InlinedBlock`, `inline_block_at_yield`, `block_fallback_specializable_share`, `inline_yield_bonus_count`, the `getblockparamproxy_handler_*` and `InvokeBlockAutosplatMiss`/`InvokeBlockDynamicMiss` counters and reasons), `PushInlineFrame`'s `block_arg`/`captured` fields, `AddIseqMode::Inlined`'s `block`/`block_return_pops`, the `YARVINSN_splatarray` profiling arm, the Symbol-block guard in the ISEQ direct-send path, and the `test_kwrest_*` codegen tests. `CallShape::from_ci(ci)` threading lands on this branch's inline `super` direct-send path in `type_specialize` rather than in `emit_specialized_super`. * Behaviour dropped, deliberately: a `bar(...)` whose inlined `def foo(...)` frame was handed a block is no longer expanded. Upstream reads the frame's block handler out of its EP and installs it as the merged `SendDirect`'s `block_arg`, but this branch's `SendDirectData` has no `block_arg` field and `BlockHandler::BlockArg` is `unreachable!()` in `gen_send_iseq_direct`, so there is no way to hand a run-time handler to a direct send. Such a site keeps its `Insn::SendForward`, which the interpreter still completes out of the frame extension, and is counted as `send_forward_reject_complex_args`. `Insn::ForwardingCallInfo::has_block` is retained and is what drives the rejection, so restoring the passthrough is a local change once `block_arg` exists. `has_block` is derived from the inlining site's `blockiseq`, which on this branch is the only way an inlined frame can carry a block. * Adapted rather than taken verbatim: `specialize_send_forward` uses `assume_no_singleton_classes` + an explicit `Invariant::MethodRedefined` patch point instead of `assume_no_singleton_classes_for_send` / `assume_cme_for_send`, and resolves the receiver with `resolve_receiver_type` alone -- `Function::ancestor_dispatch_class` and the ancestor-guard dispatch it feeds do not exist here. `build_send_direct_args` takes four arguments here (no `block_arg_passthrough`). * Bindings renumbered for this stack rather than copied: this tree has 226 non-ZJIT instructions and, with `sendforward`, 33 `zjit_profile` ones, so `YARVINSN_zjit_sendforward` is 231 and `VM_INSTRUCTION_SIZE` 258 -> 259 in `zjit/src/cruby_bindings.inc.rs`. `yjit/src/cruby_bindings.inc.rs` is shifted by the same +1 (sendforward 232, size 259 -> 260) but keeps a stale `YARVINSN_zjit_getblockparamproxy` that c651c0a removed from insns.def and from the ZJIT bindings without updating the YJIT copy; that pre-existing one-instruction skew belongs to c651c0a, not here. * `zjit/src/hir/opt_tests.rs`'s `super`-forwarding snapshot was merged best-effort: this branch's guard sequence (no `UnwrapSvar`, no `recompile` suffix) with the new inlined body appended. Snapshot text is `#[cfg(test)]` and may need `INSTA_FORCE_UPDATE` once the suite runs. [reorder port note 2] Re-ported onto zjit-lobsters4 (master 76b1775 + front block), whose PR #18606 already compiles direct sends to forwardable callees with the forwardable checks folded into `can_direct_send`: * `can_direct_send_forwardable` is not re-introduced; the commit's two new checks land in `can_direct_send`'s forwardable arm instead (`ci.is_none()` joins the FORWARDABLE_CALLEE_BLOCKERS rejection, and the `original.len() != argc` ArgcParamMismatch guard precedes the u16 check). * The base's `CallerArguments` (kwarg_count/splat_arg_idx form from #18369) keeps its shape; only the commit's `argc`/`ci` fields and the `CallShape`-taking constructor are added, and every `CallerArguments::new` caller wraps its callinfo with `CallShape::from_ci(ci)`. * `build_send_direct_args` takes five arguments here (#18567's `block_arg_passthrough`); `specialize_send_forward` passes `false` and its `SendDirectData` gains the base's `block_arg: None` field. * The Symbol-block-forwardable rejection and `send_block_arg` threading from this base's front block are kept as-is around the touched call sites. * Bindings regenerated canonically: `zjit_sendforward` is 232 and `VM_INSTRUCTION_SIZE` 260 in BOTH checked-in copies (this base has `zjit_expandarray` and no `zjit_getblockparamproxy`). * Upstream #18606's `call_method_forwardable_param_with_block_literal` expectation ("stays a direct send") is superseded: the callee is now inlined and the inner `bar(...)` keeps its SendForward because the frame carries a block, matching this commit's documented block-handling rejection.
1 parent 91d3e3c commit 874a68f

10 files changed

Lines changed: 756 additions & 110 deletions

File tree

insns.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ sendforward
876876
(CALL_DATA cd, ISEQ blockiseq)
877877
(...)
878878
(VALUE val)
879+
// attr bool zjit_profile = true;
879880
// attr rb_snum_t sp_inc = sp_inc_of_sendish(cd->ci);
880881
// attr rb_snum_t comptime_sp_inc = sp_inc_of_sendish(ci);
881882
{

yjit/src/cruby_bindings.inc.rs

Lines changed: 29 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

zjit/src/codegen.rs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,12 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
11101110
Insn::Comment { .. } => return Ok(()), // comment instruction, no code generation
11111111
&Insn::Const { val: Const::Value(val) } => gen_const_value(val),
11121112
&Insn::Const { val: Const::CPtr(val) } => gen_const_cptr(val),
1113+
// The `...` local of an inlined forwardable frame is just the caller's callinfo
1114+
// pointer. `gen_push_inline_frame` already stored the same value in the frame slot.
1115+
// Materialized into a register rather than left as an immediate because `getlocal ...`
1116+
// puts it on the modeled VM stack, and a JITFrame stack map can only reconstruct a slot
1117+
// from a register or an immediate `VALUE` -- which a callinfo pointer is not.
1118+
&Insn::ForwardingCallInfo { ci, .. } => asm.load(gen_const_cptr(ci as *const u8)),
11131119
&Insn::Const { val: Const::CInt64(val) } => gen_const_long(val),
11141120
&Insn::Const { val: Const::CUInt16(val) } => gen_const_uint16(val),
11151121
&Insn::Const { val: Const::CUInt32(val) } => gen_const_uint32(val),
@@ -1165,8 +1171,11 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
11651171
*kw_bits, *jit_entry_idx, &function.frame_state(*state), *block, block_arg,
11661172
)
11671173
}
1168-
Insn::PushInlineFrame { cme, iseq, recv, num_args, blockiseq, state, .. } => {
1169-
no_output!(gen_push_inline_frame(jit, asm, function, *cme, *iseq, opnd!(recv), *num_args, &function.frame_state(*state), *blockiseq))
1174+
Insn::PushInlineFrame { cme, iseq, recv, num_args, blockiseq, forwarded, state, .. } => {
1175+
let forwarded = forwarded.as_ref().map(|forwarded| {
1176+
(forwarded.ci, forwarded.args.iter().map(|&arg| opnd!(arg)).collect::<Vec<_>>())
1177+
});
1178+
no_output!(gen_push_inline_frame(jit, asm, function, *cme, *iseq, opnd!(recv), *num_args, &function.frame_state(*state), *blockiseq, forwarded))
11701179
},
11711180
Insn::PopInlineFrame { iseq, argc, state } => {
11721181
no_output!(gen_pop_inline_frame(asm, *iseq, *argc, &function.frame_state(*state)))
@@ -2282,8 +2291,20 @@ fn gen_push_inline_frame(
22822291
num_args: u16,
22832292
state: &FrameState,
22842293
blockiseq: Option<IseqPtr>,
2294+
forwarded: Option<(*const rb_callinfo, Vec<Opnd>)>,
22852295
) {
2286-
let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.to_usize();
2296+
// A `def foo(...)` callee keeps the caller's arguments in the frame instead of binding them
2297+
// to parameters, so the frame is `argc` slots taller than the local table, exactly as
2298+
// `vm_call_iseq_forwardable` makes it. See `gen_send_iseq_direct`, which builds the same
2299+
// frame for the out-of-line call.
2300+
let forwarded_argc = forwarded.as_ref().map_or(0, |(_, args)| args.len());
2301+
debug_assert_eq!(
2302+
forwarded.is_some(),
2303+
unsafe { rb_get_iseq_flags_forwardable(iseq) },
2304+
"forwarded arguments are set exactly for a forwardable callee",
2305+
);
2306+
debug_assert!(forwarded.is_none() || forwarded_argc == num_args.to_usize());
2307+
let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.to_usize() + forwarded_argc;
22872308
let stack_growth = state.stack_size() + local_size + unsafe { get_iseq_body_stack_max(iseq) }.to_usize();
22882309
gen_stack_overflow_check(jit, asm, function, state, stack_growth);
22892310

@@ -2325,9 +2346,28 @@ fn gen_push_inline_frame(
23252346
frame_type,
23262347
specval,
23272348
write_block_code: iseq_may_write_block_code(iseq),
2328-
forwarded_argc: None, // `can_inline` rejects forwardable callees
2349+
forwarded_argc: forwarded.is_some().then_some(forwarded_argc),
23292350
});
23302351

2352+
// A forwardable callee reads its arguments out of the frame rather than out of parameter
2353+
// locals, so they have to be in memory: a side exit lands the interpreter on a `sendforward`
2354+
// whose `vm_adjust_stack_forwarding` copies them back out from below the frame, the GC scans
2355+
// them there once this frame's `cfp->sp` is published above them, and a `getlocal ...` reads
2356+
// the callinfo from the slot directly above them. This mirrors the same block in
2357+
// `gen_send_iseq_direct`; the inlined body reads none of it, because the `...` local is an
2358+
// `Insn::ForwardingCallInfo` constant and the arguments are the caller's own SSA values.
2359+
if let Some((ci, args)) = forwarded {
2360+
asm_comment!(asm, "copy forwarded arguments and callinfo to inlined callee frame");
2361+
let locals_base = state.stack().len() - args.len();
2362+
for (idx, &arg) in args.iter().enumerate() {
2363+
asm.store(Opnd::mem(64, SP, ((locals_base + idx) * SIZEOF_VALUE) as i32), arg);
2364+
}
2365+
asm.store(
2366+
Opnd::mem(64, SP, ((locals_base + forwarded_argc) * SIZEOF_VALUE) as i32),
2367+
Opnd::const_ptr(ci as *const u8),
2368+
);
2369+
}
2370+
23312371
// Publish the inlined callee's entry JITFrame before the inlined body runs.
23322372
// Frame walking functions such as rb_profile_frames can inspect the new
23332373
// CFP between this frame push and the first inlined gen_write_jit_frame, so
@@ -2392,7 +2432,10 @@ fn gen_pop_inline_frame(
23922432
argc: usize,
23932433
state: &FrameState,
23942434
) {
2395-
let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.to_usize();
2435+
// Undo `gen_push_inline_frame`'s SP math, including the extension a forwardable callee's
2436+
// frame was grown by.
2437+
let forwarded_argc = if unsafe { rb_get_iseq_flags_forwardable(iseq) } { argc } else { 0 };
2438+
let local_size = unsafe { get_iseq_body_local_table_size(iseq) }.to_usize() + forwarded_argc;
23962439
let sp_offset = (state.stack().len() + local_size - argc + VM_ENV_DATA_SIZE.to_usize()) * SIZEOF_VALUE;
23972440

23982441
asm_comment!(asm, "restore caller SP after inline");
@@ -4273,6 +4316,9 @@ fn gen_prepare_fallback_call(jit: &JITState, asm: &mut Assembler, function: &Fun
42734316
/// on the native stack are known.
42744317
fn build_stack_map(jit: &JITState, function: &Function, state: &FrameState) -> Vec<StackMapEntry> {
42754318
let mut stack = Vec::new();
4319+
// Owned rather than borrowed: `Function::frame_state` resolves the state's operands through
4320+
// the union-find, which `jit.get_opnd` below needs. `frame_state_ref` would hand back the
4321+
// stored state with stale operand ids.
42764322
let mut current_state = state.clone();
42774323
loop {
42784324
stack.extend(current_state.stack().rev().copied().map(|insn_id| {
@@ -4287,17 +4333,19 @@ fn build_stack_map(jit: &JITState, function: &Function, state: &FrameState) -> V
42874333
let Some(caller) = current_state.caller() else {
42884334
break;
42894335
};
4290-
stack.push(StackMapEntry::Skip(inline_frame_stack_gap(current_state.iseq)));
4336+
stack.push(StackMapEntry::Skip(inline_frame_stack_gap(current_state.iseq, current_state.extra_locals)));
42914337
current_state = function.frame_state(caller);
42924338
}
42934339
stack
42944340
}
42954341

4296-
fn inline_frame_stack_gap(iseq: IseqPtr) -> usize {
4342+
fn inline_frame_stack_gap(iseq: IseqPtr, extra_locals: u16) -> usize {
42974343
// The extra slot is for the callee's receiver below its local table.
42984344
// We currently never map out the stack for `invokeblock`, which doesn't
42994345
// put a receiver on cfp->sp stack.
4300-
1 + unsafe { get_iseq_body_local_table_size(iseq) }.to_usize() + VM_ENV_DATA_SIZE.to_usize()
4346+
// `extra_locals` is the frame extension a `def foo(...)` callee's arguments live in, which
4347+
// makes the frame that much taller. See `FrameState::extra_locals`.
4348+
1 + unsafe { get_iseq_body_local_table_size(iseq) }.to_usize() + extra_locals as usize + VM_ENV_DATA_SIZE.to_usize()
43014349
}
43024350

43034351
/// Prepare for calling a C function that may call an arbitrary method.

zjit/src/codegen_tests.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,171 @@ fn test_zsuper_to_forwardable_callee() {
819819
"#), @r#"[["base", 3, 4]]"#);
820820
}
821821

822+
#[test]
823+
fn test_inlined_forwarder_positional_args() {
824+
// The forwarder is inlined into its caller, so the `bar(...)` inside it sees the caller's
825+
// callinfo at compile time and becomes a direct call to `target`.
826+
assert_snapshot!(with_inlining(|| assert_inlines("
827+
def target(a, b) = a - b
828+
def fwd(...) = target(...)
829+
def entry = fwd(7, 2)
830+
200.times { entry }
831+
entry
832+
")), @"5");
833+
}
834+
835+
#[test]
836+
fn test_inlined_forwarder_keyword_args() {
837+
// `vm_caller_setup_fwd_args` gives the merged callinfo the *caller's* keyword table, so the
838+
// expanded call has to bind the trailing arguments as keywords, not as positionals.
839+
assert_snapshot!(with_inlining(|| assert_inlines(r#"
840+
def target(a, b:, c: 3) = [a, b, c]
841+
def fwd(...) = target(...)
842+
def entry = [fwd(1, b: 2), fwd(1, c: 9, b: 2)]
843+
200.times { entry }
844+
entry
845+
"#)), @"[[1, 2, 3], [1, 2, 9]]");
846+
}
847+
848+
#[test]
849+
fn test_inlined_forwarder_site_writes_its_own_args() {
850+
// `bar(x, ...)`: the merged argument list is the site's own arguments followed by the
851+
// caller's, in that order.
852+
assert_snapshot!(with_inlining(|| assert_inlines("
853+
def target(a, b, c) = [a, b, c]
854+
def fwd(x, ...) = target(x, ...)
855+
def entry = fwd(1, 2, 3)
856+
200.times { entry }
857+
entry
858+
")), @"[1, 2, 3]");
859+
}
860+
861+
#[test]
862+
fn test_inlined_forwarder_carrying_a_literal_block() {
863+
// `bh = VM_ENV_BLOCK_HANDLER(GET_LEP())`: the forwarded call gets the forwarder frame's own
864+
// block handler, which the expanded call reads back out of the frame's EP. Re-deriving the
865+
// literal block instead would capture the wrong frame, since the block belongs to `entry`.
866+
assert_snapshot!(with_inlining(|| assert_inlines("
867+
def target(x) = yield(x)
868+
def fwd(...) = target(...)
869+
def entry = fwd(4) { |v| v * 2 }
870+
200.times { entry }
871+
entry
872+
")), @"8");
873+
}
874+
875+
#[test]
876+
fn test_inlined_forwarder_block_present_on_some_calls_only() {
877+
// A `&blk` handed to the forwarder may be a Proc on one call and nothing on the next, so the
878+
// handler the expanded call passes on is only known at run time. `block_given?` in the target
879+
// has to see each call for what it was.
880+
assert_snapshot!(with_inlining(|| assert_inlines_allowing_exits(r#"
881+
def target(x) = [x, block_given? ? yield(x) : :none]
882+
def fwd(...) = target(...)
883+
def entry(i, &b) = fwd(i, &b)
884+
200.times { |i| i.even? ? entry(i) { |v| v } : entry(i) }
885+
[entry(1) { |v| v * 2 }, entry(1)]
886+
"#)), @"[[1, 2], [1, :none]]");
887+
}
888+
889+
#[test]
890+
fn test_inlined_forwarder_argument_error() {
891+
// The argument check belongs to the target, which the inlined forwarder now calls directly.
892+
assert_snapshot!(with_inlining(|| assert_inlines_allowing_exits(r#"
893+
def target(a, b) = a + b
894+
def fwd(...) = target(...)
895+
def entry = (fwd(1) rescue $!.message)
896+
200.times { entry }
897+
entry
898+
"#)), @r#""wrong number of arguments (given 1, expected 2)""#);
899+
}
900+
901+
#[test]
902+
fn test_inlined_forwarder_side_exit_resumes_the_sendforward() {
903+
// A guard inside the inlined forwarder exits to the `sendforward` instruction, and the
904+
// interpreter's `vm_adjust_stack_forwarding` rebuilds the argument list by reading below the
905+
// frame at `lep - (local_table_size + argc + 2)`. That only works because the inlined frame
906+
// push copied the arguments into those slots and put the callinfo above them, the way
907+
// `vm_call_iseq_forwardable` does.
908+
assert_snapshot!(with_inlining(|| assert_inlines_allowing_exits(r#"
909+
class A; def m(a, b, c) = [:a, a, b, c]; end
910+
class B; def m(a, b, c) = [:b, a, b, c]; end
911+
class Fwd
912+
def initialize(t) = @t = t
913+
def m(...) = @t.m(...)
914+
end
915+
fa = Fwd.new(A.new)
916+
fb = Fwd.new(B.new)
917+
# Warm up on A alone so the expanded call guards on A.
918+
200.times { fa.m(1, 2, 3) }
919+
# B fails that guard mid-forwarder.
920+
[fa.m(1, 2, 3), fb.m(4, 5, 6)]
921+
"#)), @"[[:a, 1, 2, 3], [:b, 4, 5, 6]]");
922+
}
923+
924+
#[test]
925+
fn test_inlined_forwarder_chained_forwarding_falls_back() {
926+
// The inner target is itself a `def bar(...)`, whose `...` local has to receive a real
927+
// callinfo. No `rb_callinfo` describes the merged call, so the site keeps its `sendforward`.
928+
assert_snapshot!(with_inlining(|| assert_inlines("
929+
def target(a, b:) = [a, b]
930+
def inner(...) = target(...)
931+
def outer(...) = inner(...)
932+
def entry = outer(1, b: 2)
933+
200.times { entry }
934+
entry
935+
")), @"[1, 2]");
936+
}
937+
938+
#[test]
939+
fn test_inlined_forwarder_ruby2_keywords() {
940+
// A `ruby2_keywords` frame splats into the forwarder, which keeps the call site off the
941+
// direct send entirely; the flagged Hash still has to reach the target as keywords.
942+
assert_snapshot!(with_inlining(|| assert_inlines_allowing_exits("
943+
def target(*a, **k) = [a, k]
944+
def fwd(...) = target(...)
945+
ruby2_keywords def r2k(*a) = fwd(*a)
946+
def entry = r2k(1, k: 2)
947+
200.times { entry }
948+
entry
949+
")), @"[[1], {k: 2}]");
950+
}
951+
952+
#[test]
953+
fn test_inlined_forwarder_super_is_unaffected() {
954+
// `super` out of a forwardable frame goes through `invokesuperforward`, which
955+
// `vm_search_super_method` rebuilds the callinfo for at run time. Inlining the frame must
956+
// not disturb it.
957+
assert_snapshot!(with_inlining(|| assert_inlines_allowing_exits(r#"
958+
class Base
959+
def run(*a, **k) = ["base", a, k]
960+
end
961+
class Child < Base
962+
def run(...) = super
963+
end
964+
c = Child.new
965+
def call_it(c) = c.run(1, k: 2)
966+
200.times { call_it(c) }
967+
call_it(c)
968+
"#)), @r#"["base", [1], {k: 2}]"#);
969+
}
970+
971+
#[test]
972+
fn test_inlined_forwarder_with_extra_locals() {
973+
// The `...` local is local 0 and the frame extension sits below the whole local table, so a
974+
// forwarder with locals of its own still finds its arguments where the interpreter left them.
975+
assert_snapshot!(with_inlining(|| assert_inlines("
976+
def target(a) = a * 2
977+
def fwd(...)
978+
extra = 10
979+
extra + target(...)
980+
end
981+
def entry = fwd(3)
982+
200.times { entry }
983+
entry
984+
")), @"16");
985+
}
986+
822987
#[test]
823988
fn test_setlocal_on_eval() {
824989
assert_snapshot!(inspect("

zjit/src/cruby.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,11 @@ pub fn ruby_call_method_name(cd: *const rb_call_data) -> String {
10831083
mid.contents_lossy().to_string()
10841084
}
10851085

1086+
/// The method name a callinfo names, for HIR printing.
1087+
pub fn ruby_call_info_method_name(ci: *const rb_callinfo) -> String {
1088+
unsafe { rb_vm_ci_mid(ci) }.contents_lossy().to_string()
1089+
}
1090+
10861091
/// A location in Rust code for integrating with debugging facilities defined in C.
10871092
/// Use the [src_loc!] macro to crate an instance.
10881093
pub struct SourceLocation {

0 commit comments

Comments
 (0)