Skip to content

Commit 99ba3c4

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 e3e8725 commit 99ba3c4

3 files changed

Lines changed: 60 additions & 33 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,17 @@ def make_range_then_exit(v)
18861886
}, call_threshold: 2
18871887
end
18881888

1889+
def test_raise_in_second_argument
1890+
assert_compiles '{ok: true}', %q{
1891+
def write(hash, key)
1892+
hash[key] = raise rescue true
1893+
hash
1894+
end
1895+
1896+
write({}, :ok)
1897+
}
1898+
end
1899+
18891900
private
18901901

18911902
# 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)
@@ -847,26 +847,19 @@ fn gen_send_without_block(
847847
asm: &mut Assembler,
848848
cd: *const rb_call_data,
849849
state: &FrameState,
850-
self_val: Opnd,
851-
args: Vec<Opnd>,
852850
) -> lir::Opnd {
853-
gen_spill_locals(jit, asm, state);
854-
// Spill the receiver and the arguments onto the stack.
855-
// They need to be on the interpreter stack to let the interpreter access them.
856-
// TODO: Avoid spilling operands that have been spilled before.
857-
// TODO: Despite https://github.com/ruby/ruby/pull/13468, Kokubun thinks this should
858-
// spill the whole stack in case it raises an exception. The HIR might need to change
859-
// for opt_aref_with, which pushes to the stack in the middle of the instruction.
860-
asm_comment!(asm, "spill receiver and arguments");
861-
for (idx, &val) in [self_val].iter().chain(args.iter()).enumerate() {
862-
// Currently, we don't move the SP register. So it's equal to the base pointer.
863-
let stack_opnd = Opnd::mem(64, SP, idx as i32 * SIZEOF_VALUE_I32);
864-
asm.mov(stack_opnd, val);
865-
}
851+
// Note that it's incorrect to use this frame state to side exit because
852+
// the state might not be on the boundary of an interpreter instruction.
853+
// For example, `opt_aref_with` pushes to the stack and then sends.
854+
asm_comment!(asm, "spill frame state");
866855

867856
// Save PC and SP
868857
gen_save_pc(asm, state);
869-
gen_save_sp(asm, 1 + args.len()); // +1 for receiver
858+
gen_save_sp(asm, state.stack().len());
859+
860+
// Spill locals and stack
861+
gen_spill_locals(jit, asm, state);
862+
gen_spill_stack(jit, asm, state);
870863

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

zjit/src/hir.rs

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
812812
}
813813
Ok(())
814814
},
815-
Insn::Snapshot { state } => write!(f, "Snapshot {}", state),
815+
Insn::Snapshot { state } => write!(f, "Snapshot {}", state.print(self.ptr_map)),
816816
Insn::Defined { op_type, v, .. } => {
817817
// op_type (enum defined_type) printing logic from iseq.c.
818818
// Not sure why rb_iseq_defined_string() isn't exhaustive.
@@ -2503,11 +2503,10 @@ pub struct FrameState {
25032503
locals: Vec<InsnId>,
25042504
}
25052505

2506-
impl FrameState {
2507-
/// Get the opcode for the current instruction
2508-
pub fn get_opcode(&self) -> i32 {
2509-
unsafe { rb_iseq_opcode_at_pc(self.iseq, self.pc) }
2510-
}
2506+
/// Print adaptor for [`FrameState`]. See [`PtrPrintMap`].
2507+
pub struct FrameStatePrinter<'a> {
2508+
inner: &'a FrameState,
2509+
ptr_map: &'a PtrPrintMap,
25112510
}
25122511

25132512
/// Compute the index of a local variable from its slot index
@@ -2614,14 +2613,24 @@ impl FrameState {
26142613
args.extend(self.locals.iter().chain(self.stack.iter()).map(|op| *op));
26152614
args
26162615
}
2616+
2617+
/// Get the opcode for the current instruction
2618+
pub fn get_opcode(&self) -> i32 {
2619+
unsafe { rb_iseq_opcode_at_pc(self.iseq, self.pc) }
2620+
}
2621+
2622+
pub fn print<'a>(&'a self, ptr_map: &'a PtrPrintMap) -> FrameStatePrinter<'a> {
2623+
FrameStatePrinter { inner: self, ptr_map }
2624+
}
26172625
}
26182626

2619-
impl std::fmt::Display for FrameState {
2627+
impl Display for FrameStatePrinter<'_> {
26202628
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
2621-
write!(f, "FrameState {{ pc: {:?}, stack: ", self.pc)?;
2622-
write_vec(f, &self.stack)?;
2629+
let inner = self.inner;
2630+
write!(f, "FrameState {{ pc: {:?}, stack: ", self.ptr_map.map_ptr(inner.pc))?;
2631+
write_vec(f, &inner.stack)?;
26232632
write!(f, ", locals: ")?;
2624-
write_vec(f, &self.locals)?;
2633+
write_vec(f, &inner.locals)?;
26252634
write!(f, " }}")
26262635
}
26272636
}
@@ -3180,9 +3189,11 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
31803189
let aref_arg = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
31813190
let args = vec![aref_arg];
31823191

3192+
let mut send_state = state.clone();
3193+
send_state.stack_push(aref_arg);
3194+
let send_state = fun.push_insn(block, Insn::Snapshot { state: send_state });
31833195
let recv = state.stack_pop()?;
3184-
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
3185-
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: exit_id });
3196+
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: send_state });
31863197
state.stack_push(send);
31873198
}
31883199
YARVINSN_opt_neq => {
@@ -3903,6 +3914,12 @@ mod tests {
39033914
expected_hir.assert_eq(&actual_hir);
39043915
}
39053916

3917+
#[track_caller]
3918+
pub fn assert_function_hir_with_frame_state(function: Function, expected_hir: Expect) {
3919+
let actual_hir = format!("{}", FunctionPrinter::with_snapshot(&function));
3920+
expected_hir.assert_eq(&actual_hir);
3921+
}
3922+
39063923
#[track_caller]
39073924
fn assert_compile_fails(method: &str, reason: ParseError) {
39083925
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", method));
@@ -5068,10 +5085,16 @@ mod tests {
50685085
eval("
50695086
def test(a) = a['string lit triggers aref_with']
50705087
");
5071-
assert_method_hir("test", expect![[r#"
5088+
5089+
let iseq = crate::cruby::with_rubyvm(|| get_method_iseq("self", "test"));
5090+
assert!(iseq_contains_opcode(iseq, YARVINSN_opt_aref_with));
5091+
let function = iseq_to_hir(iseq).unwrap();
5092+
assert_function_hir_with_frame_state(function, expect![[r#"
50725093
fn test@<compiled>:2:
50735094
bb0(v0:BasicObject, v1:BasicObject):
5074-
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
5095+
v2:Any = Snapshot FrameState { pc: 0x1000, stack: [], locals: [v1] }
5096+
v3:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008))
5097+
v4:Any = Snapshot FrameState { pc: 0x1010, stack: [v1, v3], locals: [v1] }
50755098
v5:BasicObject = SendWithoutBlock v1, :[], v3
50765099
Return v5
50775100
"#]]);

0 commit comments

Comments
 (0)