Skip to content

Commit 8f81d2b

Browse files
rwstaunerXrXr
authored andcommitted
ZJIT: Don't inline non-parameter locals
1 parent 2f151e7 commit 8f81d2b

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

test/ruby/test_zjit.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,21 @@ def test(n)
340340
}
341341
end
342342

343+
def test_return_nonparam_local
344+
# Use dead code (if false) to create a local without initialization instructions.
345+
assert_compiles 'nil', %q{
346+
def foo(a)
347+
if false
348+
x = nil
349+
end
350+
x
351+
end
352+
def test = foo(1)
353+
test
354+
test
355+
}, call_threshold: 2
356+
end
357+
343358
def test_setlocal_on_eval
344359
assert_compiles '1', %q{
345360
@b = binding

zjit/src/hir.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,12 @@ fn iseq_get_return_value(iseq: IseqPtr, captured_opnd: Option<InsnId>, ci_flags:
17311731
let ep_offset = unsafe { *rb_iseq_pc_at_idx(iseq, 1) }.as_u32();
17321732
let local_idx = ep_offset_to_local_idx(iseq, ep_offset);
17331733

1734+
// Only inline if the local is a parameter (not a method-defined local) as we are indexing args.
1735+
let param_size = unsafe { rb_get_iseq_body_param_size(iseq) } as usize;
1736+
if local_idx >= param_size {
1737+
return None;
1738+
}
1739+
17341740
if unsafe { rb_simple_iseq_p(iseq) } {
17351741
return Some(IseqReturn::LocalVariable(local_idx.try_into().unwrap()));
17361742
}

zjit/src/hir/opt_tests.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,44 @@ mod hir_opt_tests {
761761
");
762762
}
763763

764+
#[test]
765+
fn test_no_inline_nonparam_local_return() {
766+
// Methods that return non-parameter local variables should NOT be inlined,
767+
// because the local variable index will be out of bounds for args.
768+
// The method must have a parameter so param_size > 0, and return a local
769+
// that's not a parameter so local_idx >= param_size.
770+
// Use dead code (if false) to create a local without initialization instructions,
771+
// resulting in just getlocal + leave which enters the inlining code path.
772+
eval("
773+
def foo(a)
774+
if false
775+
x = nil
776+
end
777+
x
778+
end
779+
def test = foo(1)
780+
test; test
781+
");
782+
assert_snapshot!(hir_string("test"), @r"
783+
fn test@<compiled>:8:
784+
bb0():
785+
EntryPoint interpreter
786+
v1:BasicObject = LoadSelf
787+
Jump bb2(v1)
788+
bb1(v4:BasicObject):
789+
EntryPoint JIT(0)
790+
Jump bb2(v4)
791+
bb2(v6:BasicObject):
792+
v11:Fixnum[1] = Const Value(1)
793+
PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010)
794+
PatchPoint NoSingletonClass(Object@0x1000)
795+
v20:HeapObject[class_exact*:Object@VALUE(0x1000)] = GuardType v6, HeapObject[class_exact*:Object@VALUE(0x1000)]
796+
v21:BasicObject = SendWithoutBlockDirect v20, :foo (0x1038), v11
797+
CheckInterrupts
798+
Return v21
799+
");
800+
}
801+
764802
#[test]
765803
fn test_optimize_send_to_aliased_cfunc() {
766804
eval("

0 commit comments

Comments
 (0)