Skip to content

Commit 1f76bab

Browse files
committed
ZJIT: Resurrect string key of opt_aref_with
The immediate problem this fixes is the frozen status of the key object in the `recv[key]` call. This also repairs an invariant that `FrameState` should always be at a YARV instruction boundary. It's important to have this because if a `SendWithoutBlock` is reduced to `SendWithoutBlockDirect`, the direct variant may want to side exit, which needs a `FrameState` as described.
1 parent e49c29e commit 1f76bab

5 files changed

Lines changed: 55 additions & 21 deletions

File tree

test/ruby/test_zjit.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,10 +1058,16 @@ def foo(#{'_,' * 39} n40) = n40
10581058

10591059
def test_opt_aref_with
10601060
assert_compiles ':ok', %q{
1061-
def aref_with(hash) = hash["key"]
1061+
def test(hash) = hash["key"]
10621062
1063-
aref_with({ "key" => :ok })
1064-
}
1063+
test({ "key" => :ok })
1064+
}, insns: [:opt_aref_with]
1065+
1066+
assert_compiles 'nil', %q{
1067+
def self.[](key) = (raise if key.frozen?)
1068+
def test = self["frozen string literal: false"]
1069+
test
1070+
}, insns: [:opt_aref_with]
10651071
end
10661072

10671073
def test_putself

zjit/bindgen/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ fn main() {
246246
.allowlist_function("rb_obj_as_string_result")
247247
.allowlist_function("rb_str_byte_substr")
248248
.allowlist_function("rb_str_substr_two_fixnums")
249+
.allowlist_function("rb_str_resurrect")
249250

250251
// From include/ruby/internal/intern/parse.h
251252
.allowlist_function("rb_backref_get")

zjit/src/codegen.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,7 @@ fn gen_insn(cb: &mut CodeBlock, jit: &mut JITState, asm: &mut Assembler, functio
370370
Insn::InvokeBuiltin { bf, .. } if bf.argc + 2 > (C_ARG_OPNDS.len() as i32) => return None,
371371
Insn::InvokeBuiltin { bf, args, state, .. } => gen_invokebuiltin(jit, asm, &function.frame_state(*state), bf, opnds!(args)),
372372
Insn::Return { val } => no_output!(gen_return(asm, opnd!(val))),
373+
&Insn::ArefWith { receiver: _, key, cd, send_state } => gen_opt_aref_with(jit, asm, cd, opnd!(key), &function.frame_state(send_state)),
373374
Insn::FixnumAdd { left, right, state } => gen_fixnum_add(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)),
374375
Insn::FixnumSub { left, right, state } => gen_fixnum_sub(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)),
375376
Insn::FixnumMult { left, right, state } => gen_fixnum_mult(jit, asm, opnd!(left), opnd!(right), &function.frame_state(*state)),
@@ -861,16 +862,17 @@ fn gen_if_false(jit: &mut JITState, asm: &mut Assembler, val: lir::Opnd, branch:
861862
asm.write_label(if_true);
862863
}
863864

865+
unsafe extern "C" {
866+
fn rb_vm_opt_send_without_block(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE;
867+
}
868+
864869
/// Compile a dynamic dispatch without block
865870
fn gen_send_without_block(
866871
jit: &mut JITState,
867872
asm: &mut Assembler,
868873
cd: *const rb_call_data,
869874
state: &FrameState,
870875
) -> lir::Opnd {
871-
// Note that it's incorrect to use this frame state to side exit because
872-
// the state might not be on the boundary of an interpreter instruction.
873-
// For example, `opt_aref_with` pushes to the stack and then sends.
874876
asm_comment!(asm, "spill frame state");
875877

876878
// Save PC and SP
@@ -882,9 +884,6 @@ fn gen_send_without_block(
882884
gen_spill_stack(jit, asm, state);
883885

884886
asm_comment!(asm, "call #{} with dynamic dispatch", ruby_call_method_name(cd));
885-
unsafe extern "C" {
886-
fn rb_vm_opt_send_without_block(ec: EcPtr, cfp: CfpPtr, cd: VALUE) -> VALUE;
887-
}
888887
let ret = asm.ccall(
889888
rb_vm_opt_send_without_block as *const u8,
890889
vec![EC, CFP, (cd as usize).into()],
@@ -960,6 +959,31 @@ fn gen_send_without_block_direct(
960959
ret
961960
}
962961

962+
/// Compile the equivalent of the general path of `opt_aref_with`
963+
fn gen_opt_aref_with(jit: &mut JITState, asm: &mut Assembler, cd: *const rb_call_data, key: Opnd, state: &FrameState) -> Opnd {
964+
// First, resurrect the string key. Save PC before allocating.
965+
gen_save_pc(asm, state);
966+
let key = asm_ccall!(asm, rb_str_resurrect, key);
967+
968+
// Put resurrected key at the top of the stack
969+
asm.mov(Opnd::mem(VALUE_BITS, SP, state.stack().len() as i32 * SIZEOF_VALUE_I32), key);
970+
971+
// Spill the rest of the frame
972+
gen_spill_stack(jit, asm, state);
973+
gen_save_sp(asm, state.stack().len() + 1); // +1 for the key
974+
gen_spill_locals(jit, asm, state);
975+
976+
asm_comment!(asm, "call [] with dynamic dispatch");
977+
let ret = asm.ccall(
978+
rb_vm_opt_send_without_block as *const u8,
979+
vec![EC, CFP, (cd as usize).into()],
980+
);
981+
// TODO(max): Add a PatchPoint here that can side-exit the function if the callee messed with
982+
// the frame's locals
983+
984+
ret
985+
}
986+
963987
/// Compile a string resurrection
964988
fn gen_string_copy(asm: &mut Assembler, recv: Opnd, chilled: bool, state: &FrameState) -> Opnd {
965989
// TODO: split rb_ec_str_resurrect into separate functions

zjit/src/cruby_bindings.inc.rs

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

zjit/src/hir.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ pub enum Insn {
564564
return_type: Option<Type>, // None for unannotated builtins
565565
},
566566

567+
/// Parallel of `opt_aref_with` in the interpreter.
568+
ArefWith { receiver: InsnId, key: InsnId, send_state: InsnId, cd: *const rb_call_data },
569+
567570
/// Control flow instructions
568571
Return { val: InsnId },
569572
/// Non-local control flow. See the throw YARV instruction
@@ -849,6 +852,7 @@ impl<'a> std::fmt::Display for InsnPrinter<'a> {
849852
Insn::ToNewArray { val, .. } => write!(f, "ToNewArray {val}"),
850853
Insn::ArrayExtend { left, right, .. } => write!(f, "ArrayExtend {left}, {right}"),
851854
Insn::ArrayPush { array, val, .. } => write!(f, "ArrayPush {array}, {val}"),
855+
Insn::ArefWith { receiver, key, .. } => write!(f, "ArefWith {receiver}, {key}"),
852856
Insn::ObjToString { val, .. } => { write!(f, "ObjToString {val}") },
853857
Insn::StringIntern { val, .. } => { write!(f, "StringIntern {val}") },
854858
Insn::AnyToString { val, str, .. } => { write!(f, "AnyToString {val}, str: {str}") },
@@ -1289,6 +1293,7 @@ impl Function {
12891293
NewHash { elements: found_elements, state: find!(state) }
12901294
}
12911295
&NewRange { low, high, flag, state } => NewRange { low: find!(low), high: find!(high), flag, state: find!(state) },
1296+
&ArefWith { receiver: self_val, key, send_state, cd } => ArefWith { receiver: find!(self_val), key: find!(key), send_state: find!(send_state), cd },
12921297
&ArrayMax { ref elements, state } => ArrayMax { elements: find_vec!(elements), state: find!(state) },
12931298
&SetGlobal { id, val, state } => SetGlobal { id, val: find!(val), state },
12941299
&GetIvar { self_val, id, state } => GetIvar { self_val: find!(self_val), id, state },
@@ -1382,6 +1387,7 @@ impl Function {
13821387
Insn::DefinedIvar { .. } => types::BasicObject,
13831388
Insn::GetConstantPath { .. } => types::BasicObject,
13841389
Insn::ArrayMax { .. } => types::BasicObject,
1390+
Insn::ArefWith { .. } => types::BasicObject,
13851391
Insn::GetGlobal { .. } => types::BasicObject,
13861392
Insn::GetIvar { .. } => types::BasicObject,
13871393
Insn::GetSpecialSymbol { .. } => types::BasicObject,
@@ -2018,6 +2024,7 @@ impl Function {
20182024
| &Insn::FixnumDiv { left, right, state }
20192025
| &Insn::FixnumMod { left, right, state }
20202026
| &Insn::ArrayExtend { left, right, state }
2027+
| &Insn::ArefWith { receiver: left, key: right, send_state: state, cd: _ }
20212028
=> {
20222029
worklist.push_back(left);
20232030
worklist.push_back(right);
@@ -3193,23 +3200,18 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
31933200
// NB: opt_aref_with has an instruction argument for the call at get_arg(0)
31943201
let cd: *const rb_call_data = get_arg(pc, 1).as_ptr();
31953202
let call_info = unsafe { rb_get_call_data_ci(cd) };
3203+
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
31963204
if unknown_call_type(unsafe { rb_vm_ci_flag(call_info) }) {
31973205
// Unknown call type; side-exit into the interpreter
3198-
let exit_id = fun.push_insn(block, Insn::Snapshot { state: exit_state });
31993206
fun.push_insn(block, Insn::SideExit { state: exit_id, reason: SideExitReason::UnknownCallType });
32003207
break; // End the block
32013208
}
32023209
let argc = unsafe { vm_ci_argc((*cd).ci) };
32033210

32043211
assert_eq!(1, argc, "opt_aref_with should only be emitted for argc=1");
3205-
let aref_arg = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
3206-
let args = vec![aref_arg];
3207-
3208-
let mut send_state = state.clone();
3209-
send_state.stack_push(aref_arg);
3210-
let send_state = fun.push_insn(block, Insn::Snapshot { state: send_state });
3212+
let key = fun.push_insn(block, Insn::Const { val: Const::Value(get_arg(pc, 0)) });
32113213
let recv = state.stack_pop()?;
3212-
let send = fun.push_insn(block, Insn::SendWithoutBlock { self_val: recv, cd, args, state: send_state });
3214+
let send = fun.push_insn(block, Insn::ArefWith { receiver: recv, key, cd, send_state: exit_id });
32133215
state.stack_push(send);
32143216
}
32153217
YARVINSN_opt_neq => {
@@ -5229,8 +5231,8 @@ mod tests {
52295231
assert_snapshot!(hir_string("test"), @r"
52305232
fn test@<compiled>:2:
52315233
bb0(v0:BasicObject, v1:BasicObject):
5232-
v3:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
5233-
v5:BasicObject = SendWithoutBlock v1, :[], v3
5234+
v4:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000))
5235+
v5:BasicObject = ArefWith v1, v4
52345236
CheckInterrupts
52355237
Return v5
52365238
");

0 commit comments

Comments
 (0)