diff --git a/doc/string/aset.rdoc b/doc/string/aset.rdoc index 98c58b59cc43a1..2cfb190cdf879a 100644 --- a/doc/string/aset.rdoc +++ b/doc/string/aset.rdoc @@ -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. diff --git a/zjit/src/cruby.rs b/zjit/src/cruby.rs index 14443fa4aa5ee5..d8bd1d95574a98 100644 --- a/zjit/src/cruby.rs +++ b/zjit/src/cruby.rs @@ -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
frame points to TOPLEVEL_BINDING ISEQ_TYPE_MAIN | @@ -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 { // TODO(alan): replace with `params.rest_start` diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 013996ba9f3cb2..bf6789238de225 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -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::{ @@ -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; } @@ -7391,7 +7391,8 @@ pub fn iseq_to_hir(iseq: *const rb_iseq_t) -> Result { /// (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 @@ -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::>(); 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); } @@ -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 @@ -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 = None; for local_idx in 0..num_locals(iseq) { if local_idx < param_size { @@ -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 = if unsafe { rb_get_iseq_flags_has_kw(iseq) } { @@ -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 })); diff --git a/zjit/src/hir/tests.rs b/zjit/src/hir/tests.rs index 74e40b8189cf1f..34f389f1381b87 100644 --- a/zjit/src/hir/tests.rs +++ b/zjit/src/hir/tests.rs @@ -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"); diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index 0fa800755d5cb5..1433b8b9b3ce56 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -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) }