Skip to content

Commit 5b5b5b3

Browse files
committed
ZJIT: Spill whole FrameState in Insn::SendWithoutBlock
Previously, we only spilled the arguments necessary for the particular send. In case the callee raises and a rescue resumes the ISEQ, that did not present a complete stack state. E.g. in `[1, (raise rescue 2)]` the raise send only spills `self`, when `1` also needs to be spilled. Spill the whole stack. Adjust parsing for `opt_aref_with` since the key argument for the send now needs to be described by the frame state of the send. This changes the contract for `Insn::SendWithoutBlock` to use arguments from the interpreter stack as described by its frame state.
1 parent 8c24e66 commit 5b5b5b3

3 files changed

Lines changed: 61 additions & 33 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,17 @@ def make_range_then_exit(v)
19021902
}, call_threshold: 2
19031903
end
19041904

1905+
def test_raise_in_second_argument
1906+
assert_compiles '{ok: true}', %q{
1907+
def write(hash, key)
1908+
hash[key] = raise rescue true
1909+
hash
1910+
end
1911+
1912+
write({}, :ok)
1913+
}
1914+
end
1915+
19051916
private
19061917

19071918
# Assert that every method call in `test_script` can be compiled by ZJIT

zjit/src/codegen.rs

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,10 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
353353
Insn::Jump(branch) => no_output!(gen_jump(jit, asm, branch)),
354354
Insn::IfTrue { val, target } => no_output!(gen_if_true(jit, asm, opnd!(val), target)),
355355
Insn::IfFalse { val, target } => no_output!(gen_if_false(jit, asm, opnd!(val), target)),
356-
Insn::SendWithoutBlock { cd, state, self_val, args, .. } => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), opnd!(self_val), opnds!(args)),
356+
Insn::SendWithoutBlock { cd, state, .. } => gen_send_without_block(jit, asm, *cd, &function.frame_state(*state)),
357357
// Give up SendWithoutBlockDirect for 6+ args since asm.ccall() doesn't support it.
358-
Insn::SendWithoutBlockDirect { cd, state, self_val, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self
359-
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state), opnd!(self_val), opnds!(args)),
358+
Insn::SendWithoutBlockDirect { cd, state, args, .. } if args.len() + 1 > C_ARG_OPNDS.len() => // +1 for self
359+
gen_send_without_block(jit, asm, *cd, &function.frame_state(*state)),
360360
Insn::SendWithoutBlockDirect { cme, iseq, self_val, args, state, .. } => gen_send_without_block_direct(cb, jit, asm, *cme, *iseq, opnd!(self_val), opnds!(args), &function.frame_state(*state)),
361361
// Ensure we have enough room fit ec, self, and arguments
362362
// TODO remove this check when we have stack args (we can use Time.new to test it)
@@ -859,26 +859,19 @@ fn gen_send_without_block(
859859
asm: &mut Assembler,
860860
cd: *const rb_call_data,
861861
state: &FrameState,
862-
self_val: Opnd,
863-
args: Vec<Opnd>,
864862
) -> lir::Opnd {
865-
gen_spill_locals(jit, asm, state);
866-
// Spill the receiver and the arguments onto the stack.
867-
// They need to be on the interpreter stack to let the interpreter access them.
868-
// TODO: Avoid spilling operands that have been spilled before.
869-
// TODO: Despite https://github.com/ruby/ruby/pull/13468, Kokubun thinks this should
870-
// spill the whole stack in case it raises an exception. The HIR might need to change
871-
// for opt_aref_with, which pushes to the stack in the middle of the instruction.
872-
asm_comment!(asm, "spill receiver and arguments");
873-
for (idx, &val) in [self_val].iter().chain(args.iter()).enumerate() {
874-
// Currently, we don't move the SP register. So it's equal to the base pointer.
875-
let stack_opnd = Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32);
876-
asm.mov(stack_opnd, val);
877-
}
863+
// Note that it's incorrect to use this frame state to side exit because
864+
// the state might not be on the boundary of an interpreter instruction.
865+
// For example, `opt_aref_with` pushes to the stack and then sends.
866+
asm_comment!(asm, "spill frame state");
878867

879868
// Save PC and SP
880869
gen_save_pc(asm, state);
881-
gen_save_sp(asm, 1 + args.len()); // +1 for receiver
870+
gen_save_sp(asm, state.stack().len());
871+
872+
// Spill locals and stack
873+
gen_spill_locals(jit, asm, state);
874+
gen_spill_stack(jit, asm, state);
882875

883876
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
884877
unsafe extern "C" {

zjit/src/hir.rs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -818,7 +818,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
818818
}
819819
Ok(())
820820
},
821-
Insn::Snapshot { state } => write!(f, "Snapshot {}", state),
821+
Insn::Snapshot { state } => write!(f, "Snapshot {}", state.print(self.ptr_map)),
822822
Insn::Defined { op_type, v, .. } => {
823823
// op_type (enum defined_type) printing logic from iseq.c.
824824
// Not sure why rb_iseq_defined_string() isn't exhaustive.
@@ -2513,11 +2513,10 @@ pub struct FrameState {
25132513
locals: Vec<InsnId>,
25142514
}
25152515

2516-
impl FrameState {
2517-
/// Get the opcode for the current instruction
2518-
pub fn get_opcode(&self) -> i32 {
2519-
unsafe { rb_iseq_opcode_at_pc(self.iseq, self.pc) }
2520-
}
2516+
/// Print adaptor for [`FrameState`]. See [`PtrPrintMap`].
2517+
pub struct FrameStatePrinter<'a> {
2518+
inner: &'a FrameState,
2519+
ptr_map: &'a PtrPrintMap,
25212520
}
25222521

25232522
/// Compute the index of a local variable from its slot index
@@ -2624,14 +2623,24 @@ impl FrameState {
26242623
args.extend(self.locals.iter().chain(self.stack.iter()).map(|op| *op));
26252624
args
26262625
}
2626+
2627+
/// Get the opcode for the current instruction
2628+
pub fn get_opcode(&self) -> i32 {
2629+
unsafe { rb_iseq_opcode_at_pc(self.iseq, self.pc) }
2630+
}
2631+
2632+
pub fn print<'a>(&'a self, ptr_map: &'a PtrPrintMap) -> FrameStatePrinter<'a> {
2633+
FrameStatePrinter { inner: self, ptr_map }
2634+
}
26272635
}
26282636

2629-
impl std::fmt::Display for FrameState {
2637+
impl Display for FrameStatePrinter<'_> {
26302638
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2631-
write!(f, "FrameState {{ pc: {:?}, stack: ", self.pc)?;
2632-
write_vec(f, &self.stack)?;
2639+
let inner = self.inner;
2640+
write!(f, "FrameState {{ pc: {:?}, stack: ", self.ptr_map.map_ptr(inner.pc))?;
2641+
write_vec(f, &inner.stack)?;
26332642
write!(f, ", locals: ")?;
2634-
write_vec(f, &self.locals)?;
2643+
write_vec(f, &inner.locals)?;
26352644
write!(f, " }}")
26362645
}
26372646
}
@@ -3195,9 +3204,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
31953204
let aref_arg = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
31963205
let args = vec![aref_arg];
31973206

3207+
let mut send_state = state.clone();
3208+
send_state.stack_push(aref_arg);
3209+
let send_state = fun.push_insn(block, Insn::Snapshot { state: send_state });
31983210
let recv = state.stack_pop()?;
3199-
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
3200-
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: exit_id });
3211+
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: send_state });
32013212
state.stack_push(send);
32023213
}
32033214
YARVINSN_opt_neq => {
@@ -3920,6 +3931,12 @@ mod tests {
39203931
expected_hir.assert_eq(&actual_hir);
39213932
}
39223933

3934+
#[track_caller]
3935+
pub fn assert_function_hir_with_frame_state(function: Function, expected_hir: Expect) {
3936+
let actual_hir = format!("{}", FunctionPrinter::with_snapshot(&function));
3937+
expected_hir.assert_eq(&actual_hir);
3938+
}
3939+
39233940
#[track_caller]
39243941
fn assert_compile_fails(method: &str, reason: ParseError) {
39253942
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
@@ -5154,11 +5171,18 @@ mod tests {
51545171
eval("
51555172
def test(a) = a['string lit triggers aref_with']
51565173
");
5157-
assert_method_hir("test", expect![[r#"
5174+
5175+
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", "test"));
5176+
assert!(iseq_contains_opcode(iseq, YARVINSN_opt_aref_with));
5177+
let function = iseq_to_hir(iseq).unwrap();
5178+
assert_function_hir_with_frame_state(function, expect![[r#"
51585179
fn test@<compiled>:2:
51595180
bb0(v0:BasicObject, v1:BasicObject):
5160-
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
5181+
v2:Any = Snapshot FrameState { pc: 0x1000, stack: [], locals: [v1] }
5182+
v3:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008))
5183+
v4:Any = Snapshot FrameState { pc: 0x1010, stack: [v1, v3], locals: [v1] }
51615184
v5:BasicObject = SendWithoutBlock v1, :[], v3
5185+
v6:Any = Snapshot FrameState { pc: 0x1018, stack: [v5], locals: [v1] }
51625186
CheckInterrupts
51635187
Return v5
51645188
"#]]);

0 commit comments

Comments
 (0)