Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/string/aset.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ searches for the +n+th matched group:

s = 'hello'
# => "hello"
s[/(h)(e)(l+)(o)/, 5] = 'foo # Raises IndexError: index 5 out of regexp.
s[/(h)(e)(l+)(o)/, 5] = 'foo' # Raises IndexError: index 5 out of regexp.

s = 'hello'
s[/nosuch/] = 'foo' # Raises IndexError: regexp not matched.
Expand Down
13 changes: 11 additions & 2 deletions zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,11 @@ pub fn iseq_opcode_at_idx(iseq: IseqPtr, insn_idx: u32) -> u32 {
unsafe { rb_iseq_opcode_at_pc(iseq, pc) as u32 }
}

/// Return true if a given ISEQ is known to escape EP to the heap on entry.
/// Return true if a given ISEQ starts with EP escaped to the heap on entry.
///
/// As of vm_push_frame(), EP is always equal to BP. However, after pushing
/// a frame, some ISEQ setups call vm_bind_update_env(), which redirects EP.
pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
pub fn iseq_ep_starts_escaped(iseq: IseqPtr) -> bool {
match unsafe { get_iseq_body_type(iseq) } {
// The EP of the <main> frame points to TOPLEVEL_BINDING
ISEQ_TYPE_MAIN |
Expand All @@ -334,6 +334,15 @@ pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
}
}

/// Return true if ZJIT may directly call this ISEQ from another JIT-compiled ISEQ.
pub fn iseq_supports_jit_entry(iseq: IseqPtr) -> bool {
match unsafe { get_iseq_body_type(iseq) } {
// These ISEQs are only entered by the interpreter.
ISEQ_TYPE_MAIN | ISEQ_TYPE_EVAL => false,
_ => true,
}
}

/// Index of the local variable that has a rest parameter if any
pub fn iseq_rest_param_idx(params: &IseqParameters) -> Option<i32> {
// TODO(alan): replace with `params.rest_start`
Expand Down
44 changes: 24 additions & 20 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#![allow(clippy::match_like_matches_macro)]
use crate::{
backend::lir::C_ARG_OPNDS,
cast::IntoUsize, codegen::{local_idx_to_ep_offset, max_iseq_versions}, cruby::*, invariants::{self}, payload::get_or_create_iseq_payload, options::{debug, get_option, DumpHIR, InlineDepth}, state::ZJITState, json::Json,
cast::IntoUsize, codegen::{local_idx_to_ep_offset, max_iseq_versions}, cruby::*, invariants::{self, iseq_seen_ep_escape}, payload::get_or_create_iseq_payload, options::{debug, get_option, DumpHIR, InlineDepth}, state::ZJITState, json::Json,
state,
};
use std::{
Expand Down Expand Up @@ -4404,7 +4404,7 @@ impl Function {

// Reject callees whose environment pointer can escape (e.g., via binding).
// TODO (nirvdrum 2026-04-15) The interaction between inlined frames and EP escape hasn't been verified.
if iseq_escapes_ep(callee_iseq) || crate::invariants::iseq_escapes_ep(callee_iseq) {
if iseq_ep_starts_escaped(callee_iseq) || iseq_seen_ep_escape(callee_iseq) {
incr_counter!(inline_reject_ep_escapes);
return false;
}
Expand Down Expand Up @@ -7391,7 +7391,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result<Function, ParseError> {
/// (the method inliner).
///
/// When `mode` is `AddIseqMode::Standalone`, generate the interpreter entry
/// block and a JIT entry block for each opt-table entry, push the JIT entry
/// block and, for ISEQs that can be entered by JIT-to-JIT calls, a JIT entry
/// block for each opt-table entry, push the JIT entry
/// blocks onto `fun.jit_entry_blocks`, and run the post-translation passes
/// (`seal_entries`, `set_param_types`, `infer_types`) before returning. When
/// `mode` is `AddIseqMode::Inlined`, only the body blocks are produced and the
Expand Down Expand Up @@ -7427,13 +7428,15 @@ fn add_iseq_to_hir(
let jit_entry_insns = unsafe { iseq.params() }.opt_table_slice().iter().copied().map(VALUE::as_u32).collect::<Vec<_>>();
let BytecodeInfo { jump_targets } = compute_bytecode_info(iseq, &jit_entry_insns);

let compile_jit_entries = matches!(mode, AddIseqMode::Standalone) && iseq_supports_jit_entry(iseq);

// Make all empty basic blocks. The ordering of the BBs matters for getting fallthrough jumps
// in good places, but it's not necessary for correctness. TODO: Higher quality scheduling during lowering.
let mut insn_idx_to_block = HashMap::new();
let mut body_entry_blocks = Vec::with_capacity(jit_entry_insns.len());
// Make blocks for optionals first, and put them right next to their JIT entrypoint
for insn_idx in jit_entry_insns.iter().copied() {
if matches!(mode, AddIseqMode::Standalone) {
if compile_jit_entries {
let jit_entry_block = fun.new_block(insn_idx);
fun.jit_entry_blocks.push(jit_entry_block);
}
Expand All @@ -7451,23 +7454,25 @@ fn add_iseq_to_hir(
// Compile an entry_block for the interpreter
compile_entry_block(fun, jit_entry_insns.as_slice(), &insn_idx_to_block);

// Compile all JIT-to-JIT entry blocks
for (jit_entry_idx, insn_idx) in jit_entry_insns.iter().enumerate() {
let target_block = insn_idx_to_block.get(insn_idx)
.copied()
.expect("we make a block for each jump target and \
each entry in the ISEQ opt_table is a jump target");
compile_jit_entry_block(fun, jit_entry_idx, target_block);
if compile_jit_entries {
// Compile all JIT-to-JIT entry blocks
for (jit_entry_idx, insn_idx) in jit_entry_insns.iter().enumerate() {
let target_block = insn_idx_to_block.get(insn_idx)
.copied()
.expect("we make a block for each jump target and \
each entry in the ISEQ opt_table is a jump target");
compile_jit_entry_block(fun, jit_entry_idx, target_block);
}
}
}

// Check if the EP is escaped for the ISEQ from the beginning. We give up
// optimizing locals in that case because they're shared with other frames.
let ep_starts_escaped = iseq_escapes_ep(iseq);
let ep_starts_escaped = iseq_ep_starts_escaped(iseq);
// Check if the EP has been escaped at some point in the ISEQ. If it has, then we assume that
// its EP is shared with other frames.
let ep_has_been_escaped = crate::invariants::iseq_escapes_ep(iseq);
let ep_escaped = ep_starts_escaped || ep_has_been_escaped;
let seen_ep_escape = iseq_seen_ep_escape(iseq);
let ep_escaped = ep_starts_escaped || seen_ep_escape;

// Iteratively fill out basic blocks using a queue.
// TODO(max): Basic block arguments at edges
Expand Down Expand Up @@ -9374,7 +9379,7 @@ fn compile_entry_state(fun: &mut Function) -> (InsnId, FrameState) {
// If the ISEQ does not escape EP, we can assume EP + 1 == SP
// TODO: This should maybe also consider if the EP has historically been escaped in this iseq.
// (see: https://github.com/Shopify/ruby/issues/774)
let use_sp = !iseq_escapes_ep(iseq);
let use_sp = !iseq_ep_starts_escaped(iseq);
let mut base: Option<InsnId> = None;
for local_idx in 0..num_locals(iseq) {
if local_idx < param_size {
Expand Down Expand Up @@ -9426,10 +9431,9 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent
let opt_num: usize = params.opt_num.try_into().expect("iseq param opt_num >= 0");
let lead_num: usize = params.lead_num.try_into().expect("iseq param lead_num >= 0");
let passed_opt_num = jit_entry_idx;
// We don't need to check crate::cruby::iseq_escapes_ep because we
// don't enter ISEQ_TYPE_MAIN/ISEQ_TYPE_EVAL using JIT-to-JIT calls.
// TODO: Stop compiling such JIT entries (Shopify/ruby#992)
let iseq_escapes_ep = crate::invariants::iseq_escapes_ep(iseq);
// We don't need to check iseq_ep_starts_escaped() because we
// don't compile JIT entries for ISEQ_TYPE_MAIN/ISEQ_TYPE_EVAL.
let seen_ep_escape = iseq_seen_ep_escape(iseq);

// If the iseq has keyword parameters, the keyword bits local will be appended to the local table.
let kw_bits_idx: Option<usize> = if unsafe { rb_get_iseq_flags_has_kw(iseq) } {
Expand Down Expand Up @@ -9485,7 +9489,7 @@ fn compile_jit_entry_state(fun: &mut Function, jit_entry_block: BlockId, jit_ent
// Once an ISEQ has escaped EP, HIR getlocal may need to read from the
// VM frame instead of FrameState. Direct JIT-to-JIT entry passes locals
// as C arguments, so initialize the frame slots here before such reads.
if iseq_escapes_ep {
if seen_ep_escape {
let ep_offset = local_idx_to_ep_offset(iseq, local_idx);
let local_id = unsafe { rb_zjit_local_id(iseq, local_idx.try_into().unwrap()) };
let ep = *ep.get_or_insert_with(|| fun.push_insn(jit_entry_block, Insn::GetEP { level: 0 }));
Expand Down
11 changes: 11 additions & 0 deletions zjit/src/hir/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,17 @@ pub(crate) mod hir_build_tests {
");
}

#[test]
fn test_no_jit_entry_blocks_for_eval_iseqs() {
let wrapped_iseq = eval("eval('RubyVM::InstructionSequence.of(caller_locations(0, 1).first)')");
let eval_iseq = unsafe { rb_iseqw_to_iseq(wrapped_iseq) };
assert_eq!(unsafe { get_iseq_body_type(eval_iseq) }, ISEQ_TYPE_EVAL);
assert!(!iseq_supports_jit_entry(eval_iseq));
unsafe { crate::cruby::rb_zjit_profile_disable(eval_iseq) };
let eval_hir = hir_string_function(&iseq_to_hir(eval_iseq).unwrap());
assert!(!eval_hir.contains("EntryPoint JIT("), "{eval_hir}");
}

#[test]
fn test_putobject() {
eval("def test = 123");
Expand Down
2 changes: 1 addition & 1 deletion zjit/src/invariants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ pub fn track_no_ep_escape_assumption(
}

/// Returns true if a given ISEQ has previously escaped environment pointer.
pub fn iseq_escapes_ep(iseq: IseqPtr) -> bool {
pub fn iseq_seen_ep_escape(iseq: IseqPtr) -> bool {
ZJITState::get_invariants().ep_escape_iseqs.contains(&iseq)
}

Expand Down