Skip to main content

stryke/
vm.rs

1use std::collections::{HashMap, VecDeque};
2use std::io::{self, Write as IoWrite};
3use std::sync::Arc;
4
5use indexmap::IndexMap;
6use parking_lot::RwLock;
7use rayon::prelude::*;
8
9
10use crate::ast::{BinOp, Block, Expr, MatchArm, PerlTypeName, Sigil, SubSigParam};
11use crate::bytecode::{BuiltinId, Chunk, Op, RuntimeSubDecl, SpliceExprEntry};
12use crate::compiler::scalar_compound_op_from_byte;
13use crate::error::{ErrorKind, StrykeError, StrykeResult};
14use crate::perl_fs::read_file_text_perl_compat;
15use crate::pmap_progress::{FanProgress, PmapProgress};
16use crate::sort_fast::{sort_magic_cmp, SortBlockFast};
17use crate::value::{
18    perl_list_range_expand, perl_shl_i64, perl_shr_i64, PerlBarrier, PerlHeap, PipelineInner,
19    PipelineOp, StrykeAsyncTask, StrykeSub, StrykeValue,
20};
21use crate::vm_helper::{
22    fold_preduce_init_step, merge_preduce_init_partials, preduce_init_fold_identity, Flow,
23    FlowOrError, VMHelper, WantarrayCtx,
24};
25use parking_lot::Mutex;
26use std::sync::Barrier;
27
28/// Stable reference for empty-stack [`VM::peek`] (not a temporary `&StrykeValue::UNDEF`).
29static PEEK_UNDEF: StrykeValue = StrykeValue::UNDEF;
30
31/// Immutable snapshot of [`VM`] pools for rayon workers (cheap `Arc` clones; no `&mut VM` in closures).
32struct ParallelBlockVmShared {
33    ops: Arc<Vec<Op>>,
34    names: Arc<Vec<String>>,
35    constants: Arc<Vec<StrykeValue>>,
36    lines: Arc<Vec<usize>>,
37    sub_entries: Vec<(u16, usize, bool)>,
38    static_sub_calls: Vec<(usize, bool, u16)>,
39    blocks: Vec<Block>,
40    code_ref_sigs: Vec<Vec<SubSigParam>>,
41    block_bytecode_ranges: Vec<Option<(usize, usize)>>,
42    map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
43    grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
44    regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
45    given_entries: Vec<(Expr, Block)>,
46    given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
47    eval_timeout_entries: Vec<(Expr, Block)>,
48    eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
49    algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
50    algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
51    par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
52    par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
53    pwatch_entries: Vec<(Expr, Expr)>,
54    substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
55    keys_expr_entries: Vec<Expr>,
56    keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
57    map_expr_entries: Vec<Expr>,
58    grep_expr_entries: Vec<Expr>,
59    regex_flip_flop_rhs_expr_entries: Vec<Expr>,
60    values_expr_entries: Vec<Expr>,
61    values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
62    delete_expr_entries: Vec<Expr>,
63    exists_expr_entries: Vec<Expr>,
64    push_expr_entries: Vec<(Expr, Vec<Expr>)>,
65    pop_expr_entries: Vec<Expr>,
66    shift_expr_entries: Vec<Expr>,
67    unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
68    splice_expr_entries: Vec<SpliceExprEntry>,
69    lvalues: Vec<Expr>,
70    ast_eval_exprs: Vec<Expr>,
71    format_decls: Vec<(String, Vec<String>)>,
72    use_overload_entries: Vec<Vec<(String, String)>>,
73    runtime_sub_decls: Arc<Vec<RuntimeSubDecl>>,
74    runtime_advice_decls: Arc<Vec<crate::bytecode::RuntimeAdviceDecl>>,
75    jit_sub_invoke_threshold: u32,
76    op_len_plus_one: usize,
77    static_sub_closure_subs: Vec<Option<Arc<StrykeSub>>>,
78    sub_entry_by_name: HashMap<u16, (usize, bool)>,
79}
80
81impl ParallelBlockVmShared {
82    fn from_vm(vm: &VM<'_>) -> Self {
83        let n = vm.ops.len().saturating_add(1);
84        Self {
85            ops: Arc::clone(&vm.ops),
86            names: Arc::clone(&vm.names),
87            constants: Arc::clone(&vm.constants),
88            lines: Arc::clone(&vm.lines),
89            sub_entries: vm.sub_entries.clone(),
90            static_sub_calls: vm.static_sub_calls.clone(),
91            blocks: vm.blocks.clone(),
92            code_ref_sigs: vm.code_ref_sigs.clone(),
93            block_bytecode_ranges: vm.block_bytecode_ranges.clone(),
94            map_expr_bytecode_ranges: vm.map_expr_bytecode_ranges.clone(),
95            grep_expr_bytecode_ranges: vm.grep_expr_bytecode_ranges.clone(),
96            regex_flip_flop_rhs_expr_bytecode_ranges: vm
97                .regex_flip_flop_rhs_expr_bytecode_ranges
98                .clone(),
99            given_entries: vm.given_entries.clone(),
100            given_topic_bytecode_ranges: vm.given_topic_bytecode_ranges.clone(),
101            eval_timeout_entries: vm.eval_timeout_entries.clone(),
102            eval_timeout_expr_bytecode_ranges: vm.eval_timeout_expr_bytecode_ranges.clone(),
103            algebraic_match_entries: vm.algebraic_match_entries.clone(),
104            algebraic_match_subject_bytecode_ranges: vm
105                .algebraic_match_subject_bytecode_ranges
106                .clone(),
107            par_lines_entries: vm.par_lines_entries.clone(),
108            par_walk_entries: vm.par_walk_entries.clone(),
109            pwatch_entries: vm.pwatch_entries.clone(),
110            substr_four_arg_entries: vm.substr_four_arg_entries.clone(),
111            keys_expr_entries: vm.keys_expr_entries.clone(),
112            keys_expr_bytecode_ranges: vm.keys_expr_bytecode_ranges.clone(),
113            map_expr_entries: vm.map_expr_entries.clone(),
114            grep_expr_entries: vm.grep_expr_entries.clone(),
115            regex_flip_flop_rhs_expr_entries: vm.regex_flip_flop_rhs_expr_entries.clone(),
116            values_expr_entries: vm.values_expr_entries.clone(),
117            values_expr_bytecode_ranges: vm.values_expr_bytecode_ranges.clone(),
118            delete_expr_entries: vm.delete_expr_entries.clone(),
119            exists_expr_entries: vm.exists_expr_entries.clone(),
120            push_expr_entries: vm.push_expr_entries.clone(),
121            pop_expr_entries: vm.pop_expr_entries.clone(),
122            shift_expr_entries: vm.shift_expr_entries.clone(),
123            unshift_expr_entries: vm.unshift_expr_entries.clone(),
124            splice_expr_entries: vm.splice_expr_entries.clone(),
125            lvalues: vm.lvalues.clone(),
126            ast_eval_exprs: vm.ast_eval_exprs.clone(),
127            format_decls: vm.format_decls.clone(),
128            use_overload_entries: vm.use_overload_entries.clone(),
129            runtime_sub_decls: Arc::clone(&vm.runtime_sub_decls),
130            runtime_advice_decls: Arc::clone(&vm.runtime_advice_decls),
131            jit_sub_invoke_threshold: vm.jit_sub_invoke_threshold,
132            op_len_plus_one: n,
133            static_sub_closure_subs: vm.static_sub_closure_subs.clone(),
134            sub_entry_by_name: vm.sub_entry_by_name.clone(),
135        }
136    }
137
138    fn worker_vm<'a>(&self, interp: &'a mut VMHelper) -> VM<'a> {
139        let n = self.op_len_plus_one;
140        VM {
141            names: Arc::clone(&self.names),
142            constants: Arc::clone(&self.constants),
143            ops: Arc::clone(&self.ops),
144            lines: Arc::clone(&self.lines),
145            sub_entries: self.sub_entries.clone(),
146            static_sub_calls: self.static_sub_calls.clone(),
147            blocks: self.blocks.clone(),
148            code_ref_sigs: self.code_ref_sigs.clone(),
149            block_bytecode_ranges: self.block_bytecode_ranges.clone(),
150            map_expr_bytecode_ranges: self.map_expr_bytecode_ranges.clone(),
151            grep_expr_bytecode_ranges: self.grep_expr_bytecode_ranges.clone(),
152            regex_flip_flop_rhs_expr_bytecode_ranges: self
153                .regex_flip_flop_rhs_expr_bytecode_ranges
154                .clone(),
155            given_entries: self.given_entries.clone(),
156            given_topic_bytecode_ranges: self.given_topic_bytecode_ranges.clone(),
157            eval_timeout_entries: self.eval_timeout_entries.clone(),
158            eval_timeout_expr_bytecode_ranges: self.eval_timeout_expr_bytecode_ranges.clone(),
159            algebraic_match_entries: self.algebraic_match_entries.clone(),
160            algebraic_match_subject_bytecode_ranges: self
161                .algebraic_match_subject_bytecode_ranges
162                .clone(),
163            par_lines_entries: self.par_lines_entries.clone(),
164            par_walk_entries: self.par_walk_entries.clone(),
165            pwatch_entries: self.pwatch_entries.clone(),
166            substr_four_arg_entries: self.substr_four_arg_entries.clone(),
167            keys_expr_entries: self.keys_expr_entries.clone(),
168            keys_expr_bytecode_ranges: self.keys_expr_bytecode_ranges.clone(),
169            map_expr_entries: self.map_expr_entries.clone(),
170            grep_expr_entries: self.grep_expr_entries.clone(),
171            regex_flip_flop_rhs_expr_entries: self.regex_flip_flop_rhs_expr_entries.clone(),
172            values_expr_entries: self.values_expr_entries.clone(),
173            values_expr_bytecode_ranges: self.values_expr_bytecode_ranges.clone(),
174            delete_expr_entries: self.delete_expr_entries.clone(),
175            exists_expr_entries: self.exists_expr_entries.clone(),
176            push_expr_entries: self.push_expr_entries.clone(),
177            pop_expr_entries: self.pop_expr_entries.clone(),
178            shift_expr_entries: self.shift_expr_entries.clone(),
179            unshift_expr_entries: self.unshift_expr_entries.clone(),
180            splice_expr_entries: self.splice_expr_entries.clone(),
181            lvalues: self.lvalues.clone(),
182            ast_eval_exprs: self.ast_eval_exprs.clone(),
183            format_decls: self.format_decls.clone(),
184            use_overload_entries: self.use_overload_entries.clone(),
185            runtime_sub_decls: Arc::clone(&self.runtime_sub_decls),
186            runtime_advice_decls: Arc::clone(&self.runtime_advice_decls),
187            ip: 0,
188            stack: Vec::with_capacity(256),
189            call_stack: Vec::with_capacity(32),
190            wantarray_stack: Vec::with_capacity(8),
191            interp,
192            jit_enabled: false,
193            sub_jit_skip_linear: vec![false; n],
194            sub_jit_skip_block: vec![false; n],
195            sub_fusevm_meta: vec![None; n],
196            uscore_name_idx: None,
197            sub_entry_at_ip: {
198                let mut v = vec![false; n];
199                for (_, e, _) in &self.sub_entries {
200                    if *e < v.len() {
201                        v[*e] = true;
202                    }
203                }
204                v
205            },
206            sub_entry_invoke_count: vec![0; n],
207            jit_sub_invoke_threshold: self.jit_sub_invoke_threshold,
208            jit_buf_slot: Vec::new(),
209            jit_buf_plain: Vec::new(),
210            jit_buf_arg: Vec::new(),
211            jit_trampoline_out: None,
212            jit_trampoline_depth: 0,
213            halt: false,
214            try_stack: Vec::new(),
215            pending_catch_error: None,
216            exit_main_dispatch: false,
217            exit_main_dispatch_value: None,
218            static_sub_closure_subs: self.static_sub_closure_subs.clone(),
219            sub_entry_by_name: self.sub_entry_by_name.clone(),
220            block_region_mode: false,
221            block_region_end: 0,
222            block_region_return: None,
223        }
224    }
225}
226
227#[inline]
228fn vm_interp_result(r: Result<StrykeValue, FlowOrError>, line: usize) -> StrykeResult<StrykeValue> {
229    match r {
230        Ok(v) => Ok(v),
231        Err(FlowOrError::Error(e)) => Err(e),
232        Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
233            "unexpected control flow in tree-assisted opcode",
234            line,
235        )),
236    }
237}
238
239/// Saved state for `try { } catch (…) { } finally { }`.
240/// Jump targets live in [`Op::TryPush`] and are patched after emission; we only store the op index.
241#[derive(Debug, Clone, PartialEq)]
242pub(crate) enum TryState {
243    /// Executing the `try` body — die here jumps to `catch`.
244    Trying,
245    /// Executing the `catch` body — die here runs `finally` (if present) then propagates outward.
246    Catching,
247    /// Executing the `finally` body — die here overrides any deferred error and propagates outward.
248    Finalizing,
249}
250
251#[derive(Debug, Clone)]
252pub(crate) struct TryFrame {
253    pub(crate) try_push_op_idx: usize,
254    pub(crate) state: TryState,
255    /// When `catch` itself throws and a `finally` exists, the new error is parked here so
256    /// `TryFinallyEnd` can re-raise it after `finally` runs.
257    pub(crate) deferred_error: Option<StrykeError>,
258}
259
260/// Saved state when entering a function call.
261#[derive(Debug)]
262struct CallFrame {
263    return_ip: usize,
264    stack_base: usize,
265    scope_depth: usize,
266    saved_wantarray: WantarrayCtx,
267    /// [`stryke_jit_call_sub`] — no bytecode resume; result stored in [`VM::jit_trampoline_out`].
268    jit_trampoline_return: bool,
269    /// Synthetic frame for [`Op::BlockReturnValue`] (`map`/`grep`/`sort` block bytecode), paired with
270    /// `scope_push_hook` at [`VM::run_block_region`] entry (not a sub call; no closure capture).
271    block_region: bool,
272    /// Wall-clock start for [`crate::profiler::Profiler::exit_sub`] (paired with `enter_sub` on `Call`).
273    sub_profiler_start: Option<std::time::Instant>,
274}
275
276/// Stack-based bytecode virtual machine.
277/// Which fusevm-bridge dispatch path applies to a sub. Cached in
278/// [`VM::sub_fusevm_meta`] so `try_fusevm_subroutine` skips re-running 6+
279/// `segment_is_*` detectors on every call to a hot sub.
280#[derive(Clone, Copy, PartialEq, Eq)]
281enum FusevmDispatch {
282    /// Pure-integer segment (slots as unboxed i64s).
283    Int,
284    /// Float-bearing segment, integer-or-float result.
285    Float,
286    /// `chr($n)` — int operand, owned-string result.
287    IntStr,
288    /// `substr($s, $n)` / `$s x $n` — string handle + int per-slot.
289    StrInt,
290    /// `substr("abc", $n)` / `"prefix" x $n` — literal-string + int slot.
291    LitStrInt,
292    /// `sprintf("FMT", $arg)` — literal-string fmt + any-typed slot, owned-string result.
293    LitStrSprintf,
294    /// String-bearing → int (compare/concat/unary/binary-int/general analyzer).
295    Str,
296    /// Any-value unary (defined/ref) — bypass type gate.
297    ValUnary,
298}
299
300/// Cached fusevm-bridge eligibility for a sub at IP. Records only the
301/// detector verdict so `try_fusevm_subroutine` can skip the 13+
302/// `segment_is_*` calls (each walking the segment) on every invocation of a
303/// hot sub. The per-call seg-derivation, slot-kind inference, and arg-bind
304/// remap still run — those depend on having the actual seg slice in hand;
305/// caching them would require owning a Vec<Op> per sub. The detector results
306/// are the bulk of the recomputed work, and they're tiny to cache.
307#[derive(Clone)]
308struct FusevmSubElig {
309    dispatch: FusevmDispatch,
310    /// For StrInt only: which slot is the string handle.
311    str_handle_slot: Option<u8>,
312    /// Whether to bypass `is_string_like` on slot seeding (ValUnary +
313    /// LitStrSprintf).
314    #[allow(dead_code)]
315    bypass_type_gate: bool,
316    /// Cached fusevm chunk built by `run_linear_segment_cached` on the first
317    /// call to this sub; reused on every subsequent call. Avoids re-running
318    /// `build_chunk` (segment-op walk + chunk-Vec allocation + jump-fixup
319    /// pass) on the per-record hot path. Stored as `Arc` so vm.rs can hand
320    /// out cheap borrows to the bridge without copying the chunk's Vec<Op>.
321    /// `OnceCell` for interior mutability — the cache is populated once on
322    /// first build then read-only, so `&FusevmSubElig` suffices for hits.
323    cached_chunk: std::cell::OnceCell<std::sync::Arc<fusevm::Chunk>>,
324}
325
326/// Single-pass eligibility check + dispatch resolution: runs each
327/// `segment_is_*` detector exactly once and resolves which `FusevmDispatch`
328/// (if any) the segment falls into. Returns `None` when no detector matches
329/// — the bridge bails to the interpreter.
330///
331/// Called once per sub-entry IP via [`VM::sub_fusevm_meta`] caching, then
332/// the result is reused across every invocation of that sub.
333fn compute_fusevm_elig(seg: &[Op], seg_ip: usize) -> Option<FusevmSubElig> {
334    use crate::fusevm_bridge as fb;
335    if fb::segment_is_fusevm_eligible(seg, seg_ip) {
336        return Some(FusevmSubElig {
337            dispatch: FusevmDispatch::Int,
338            str_handle_slot: None,
339            bypass_type_gate: false,
340            cached_chunk: std::cell::OnceCell::new(),
341        });
342    }
343    if fb::segment_is_string_compare_eligible(seg, seg_ip)
344        || fb::segment_is_string_concat_eligible(seg, seg_ip)
345        || fb::segment_is_string_unary_eligible(seg, seg_ip)
346        || fb::segment_is_string_binary_int_eligible(seg, seg_ip)
347        || fb::segment_is_string_bearing_int_result_eligible(seg, seg_ip)
348    {
349        return Some(FusevmSubElig {
350            dispatch: FusevmDispatch::Str,
351            str_handle_slot: None,
352            bypass_type_gate: false,
353            cached_chunk: std::cell::OnceCell::new(),
354        });
355    }
356    if fb::segment_is_any_value_unary_int_eligible(seg, seg_ip)
357        || fb::segment_is_any_value_unary_str_eligible(seg, seg_ip)
358    {
359        return Some(FusevmSubElig {
360            dispatch: FusevmDispatch::ValUnary,
361            str_handle_slot: None,
362            bypass_type_gate: true,
363            cached_chunk: std::cell::OnceCell::new(),
364        });
365    }
366    if fb::segment_is_fusevm_float_eligible(seg, seg_ip) {
367        return Some(FusevmSubElig {
368            dispatch: FusevmDispatch::Float,
369            str_handle_slot: None,
370            bypass_type_gate: false,
371            cached_chunk: std::cell::OnceCell::new(),
372        });
373    }
374    if fb::segment_is_int_to_string_eligible(seg, seg_ip) {
375        return Some(FusevmSubElig {
376            dispatch: FusevmDispatch::IntStr,
377            str_handle_slot: None,
378            bypass_type_gate: false,
379            cached_chunk: std::cell::OnceCell::new(),
380        });
381    }
382    if let Some(str_slot) = fb::string_handle_slot(seg, seg_ip) {
383        return Some(FusevmSubElig {
384            dispatch: FusevmDispatch::StrInt,
385            str_handle_slot: Some(str_slot),
386            bypass_type_gate: false,
387            cached_chunk: std::cell::OnceCell::new(),
388        });
389    }
390    if fb::segment_is_literal_string_int_to_string_eligible(seg, seg_ip) {
391        return Some(FusevmSubElig {
392            dispatch: FusevmDispatch::LitStrInt,
393            str_handle_slot: None,
394            bypass_type_gate: false,
395            cached_chunk: std::cell::OnceCell::new(),
396        });
397    }
398    if fb::segment_is_literal_string_anyval_sprintf_eligible(seg, seg_ip) {
399        return Some(FusevmSubElig {
400            dispatch: FusevmDispatch::LitStrSprintf,
401            str_handle_slot: None,
402            bypass_type_gate: true,
403            cached_chunk: std::cell::OnceCell::new(),
404        });
405    }
406    None
407}
408
409pub struct VM<'a> {
410    /// Shared with parallel workers via [`Self::new_parallel_worker`] (cheap `Arc` clones).
411    names: Arc<Vec<String>>,
412    /// `constants` field.
413    constants: Arc<Vec<StrykeValue>>,
414    /// `ops` field.
415    ops: Arc<Vec<Op>>,
416    /// `lines` field.
417    lines: Arc<Vec<usize>>,
418    /// `sub_entries` field.
419    sub_entries: Vec<(u16, usize, bool)>,
420    /// See [`Chunk::static_sub_calls`] (`Op::CallStaticSubId`).
421    static_sub_calls: Vec<(usize, bool, u16)>,
422    /// `blocks` field.
423    blocks: Vec<Block>,
424    /// `code_ref_sigs` field.
425    code_ref_sigs: Vec<Vec<SubSigParam>>,
426    /// Optional `ops[start..end]` lowering for [`Self::blocks`] (see [`Chunk::block_bytecode_ranges`]).
427    block_bytecode_ranges: Vec<Option<(usize, usize)>>,
428    /// Optional lowering for [`Chunk::map_expr_entries`] (see [`Chunk::map_expr_bytecode_ranges`]).
429    map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
430    /// Optional lowering for [`Chunk::grep_expr_entries`] (see [`Chunk::grep_expr_bytecode_ranges`]).
431    grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
432    /// `given_entries` field.
433    given_entries: Vec<(Expr, Block)>,
434    /// `given_topic_bytecode_ranges` field.
435    given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
436    /// `eval_timeout_entries` field.
437    eval_timeout_entries: Vec<(Expr, Block)>,
438    /// `eval_timeout_expr_bytecode_ranges` field.
439    eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
440    /// `algebraic_match_entries` field.
441    algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
442    /// `algebraic_match_subject_bytecode_ranges` field.
443    algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
444    /// `par_lines_entries` field.
445    par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
446    /// `par_walk_entries` field.
447    par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
448    /// `pwatch_entries` field.
449    pwatch_entries: Vec<(Expr, Expr)>,
450    /// `substr_four_arg_entries` field.
451    substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
452    /// `keys_expr_entries` field.
453    keys_expr_entries: Vec<Expr>,
454    /// `keys_expr_bytecode_ranges` field.
455    keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
456    /// `map_expr_entries` field.
457    map_expr_entries: Vec<Expr>,
458    /// `grep_expr_entries` field.
459    grep_expr_entries: Vec<Expr>,
460    /// `regex_flip_flop_rhs_expr_entries` field.
461    regex_flip_flop_rhs_expr_entries: Vec<Expr>,
462    /// `regex_flip_flop_rhs_expr_bytecode_ranges` field.
463    regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
464    /// `values_expr_entries` field.
465    values_expr_entries: Vec<Expr>,
466    /// `values_expr_bytecode_ranges` field.
467    values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
468    /// `delete_expr_entries` field.
469    delete_expr_entries: Vec<Expr>,
470    /// `exists_expr_entries` field.
471    exists_expr_entries: Vec<Expr>,
472    /// `push_expr_entries` field.
473    push_expr_entries: Vec<(Expr, Vec<Expr>)>,
474    /// `pop_expr_entries` field.
475    pop_expr_entries: Vec<Expr>,
476    /// `shift_expr_entries` field.
477    shift_expr_entries: Vec<Expr>,
478    /// `unshift_expr_entries` field.
479    unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
480    /// `splice_expr_entries` field.
481    splice_expr_entries: Vec<SpliceExprEntry>,
482    /// `lvalues` field.
483    lvalues: Vec<Expr>,
484    /// `ast_eval_exprs` field.
485    ast_eval_exprs: Vec<Expr>,
486    /// `format_decls` field.
487    format_decls: Vec<(String, Vec<String>)>,
488    /// `use_overload_entries` field.
489    use_overload_entries: Vec<Vec<(String, String)>>,
490    /// `runtime_sub_decls` field.
491    runtime_sub_decls: Arc<Vec<RuntimeSubDecl>>,
492    /// `runtime_advice_decls` field.
493    runtime_advice_decls: Arc<Vec<crate::bytecode::RuntimeAdviceDecl>>,
494    pub(crate) ip: usize,
495    /// `stack` field.
496    stack: Vec<StrykeValue>,
497    /// `call_stack` field.
498    call_stack: Vec<CallFrame>,
499    /// Paired with [`Op::WantarrayPush`] / [`Op::WantarrayPop`] (e.g. `splice` list vs scalar return).
500    wantarray_stack: Vec<WantarrayCtx>,
501    /// `interp` field.
502    interp: &'a mut VMHelper,
503    /// When `false`, [`VM::execute`] skips Cranelift JIT (linear, block, and subroutine linear) and
504    /// uses only the opcode interpreter. Default `true`.
505    jit_enabled: bool,
506    /// `sub_jit_skip_linear[ip]` — true when linear sub-JIT cannot apply (control flow / calls).
507    /// Indexed by IP for O(1) lookup instead of hashing (recursive subs like fib hit this millions of times).
508    sub_jit_skip_linear: Vec<bool>,
509    /// `sub_jit_skip_block[ip]` — true when block sub-JIT cannot apply.
510    sub_jit_skip_block: Vec<bool>,
511    /// Per-sub-IP cache for the fusevm bridge's `try_fusevm_subroutine`
512    /// eligibility analysis. First call to a sub computes flags + slot kinds
513    /// once (running 6+ `segment_is_*` detectors + the abstract-stack analyzer
514    /// + prologue recognition + the `_` name lookup); subsequent calls hit
515    /// the cache for O(1) dispatch. `None` = not yet analyzed; `Some(None)` =
516    /// known-not-eligible (skip without re-trying); `Some(Some(meta))` = cached
517    /// dispatch metadata.
518    sub_fusevm_meta: Vec<Option<Option<FusevmSubElig>>>,
519    /// Cached index of the `_` name (for `@_`) — populated lazily by the first
520    /// fusevm-bridge call. Avoids a linear `self.names.iter().position` scan
521    /// on every sub call.
522    uscore_name_idx: Option<Option<u16>>,
523    /// `sub_entry_at_ip[ip]` — faster than hashing on every opcode (recursive subs dispatch millions of ops).
524    sub_entry_at_ip: Vec<bool>,
525    /// Invocations per sub-entry IP (tiered JIT: interpreter until count exceeds threshold).
526    sub_entry_invoke_count: Vec<u32>,
527    /// Minimum invocations before attempting subroutine JIT. Override with `STRYKE_JIT_SUB_INVOKES` (default 50).
528    jit_sub_invoke_threshold: u32,
529    /// Reused `i64` tables for sub-JIT / top-level JIT attempts (avoids `vec![0; n]` on every try).
530    jit_buf_slot: Vec<i64>,
531    /// `jit_buf_plain` field.
532    jit_buf_plain: Vec<i64>,
533    /// `jit_buf_arg` field.
534    jit_buf_arg: Vec<i64>,
535    /// Set when running [`VM::jit_trampoline_run_sub`]; [`Op::ReturnValue`] stores here and exits dispatch.
536    jit_trampoline_out: Option<StrykeValue>,
537    /// Nesting depth for [`Self::jit_trampoline_run_sub`]; dispatch breaks on [`Self::jit_trampoline_out`] only when `> 0`.
538    jit_trampoline_depth: u32,
539    /// Set by [`Op::Halt`]; outer loop exits after handling [`Self::try_recover_from_exception`].
540    halt: bool,
541    /// Stack of active `try` regions (LIFO).
542    try_stack: Vec<TryFrame>,
543    /// Value to bind in the next [`Op::CatchReceive`] (set before jumping to `catch_ip`).
544    /// Carries the original `die`-value when one was supplied (preserves hash/array refs);
545    /// otherwise a string copy of the formatted error message.
546    pub(crate) pending_catch_error: Option<StrykeValue>,
547    /// [`Op::Return`] / [`Op::ReturnValue`] with no caller frame: exit the main dispatch loop (was `break`).
548    exit_main_dispatch: bool,
549    /// Top-level [`Op::ReturnValue`] with no frame: value for implicit return (was `last = val; break`).
550    exit_main_dispatch_value: Option<StrykeValue>,
551    /// [`Chunk::static_sub_calls`] index → pre-resolved [`StrykeSub`] for closure restore (stash key lookup once at VM build).
552    static_sub_closure_subs: Vec<Option<Arc<StrykeSub>>>,
553    /// O(1) [`Chunk::sub_entries`] lookup (same first-wins semantics as the old linear scan).
554    sub_entry_by_name: HashMap<u16, (usize, bool)>,
555    /// When executing [`Chunk::block_bytecode_ranges`] via [`Self::run_block_region`].
556    block_region_mode: bool,
557    /// `block_region_end` field.
558    block_region_end: usize,
559    /// `block_region_return` field.
560    block_region_return: Option<StrykeValue>,
561}
562
563impl<'a> VM<'a> {
564    /// `new` — see implementation.
565    pub fn new(chunk: &Chunk, interp: &'a mut VMHelper) -> Self {
566        let static_sub_closure_subs: Vec<Option<Arc<StrykeSub>>> = chunk
567            .static_sub_calls
568            .iter()
569            .map(|(_, _, name_idx)| {
570                let nm = chunk.names[*name_idx as usize].as_str();
571                interp.subs.get(nm).cloned()
572            })
573            .collect();
574        let mut sub_entry_by_name = HashMap::with_capacity(chunk.sub_entries.len());
575        for &(n, ip, sa) in &chunk.sub_entries {
576            sub_entry_by_name.entry(n).or_insert((ip, sa));
577        }
578        Self {
579            names: Arc::new(chunk.names.clone()),
580            constants: Arc::new(chunk.constants.clone()),
581            ops: Arc::new(chunk.ops.clone()),
582            lines: Arc::new(chunk.lines.clone()),
583            sub_entries: chunk.sub_entries.clone(),
584            static_sub_calls: chunk.static_sub_calls.clone(),
585            blocks: chunk.blocks.clone(),
586            code_ref_sigs: chunk.code_ref_sigs.clone(),
587            block_bytecode_ranges: chunk.block_bytecode_ranges.clone(),
588            map_expr_bytecode_ranges: chunk.map_expr_bytecode_ranges.clone(),
589            grep_expr_bytecode_ranges: chunk.grep_expr_bytecode_ranges.clone(),
590            regex_flip_flop_rhs_expr_bytecode_ranges: chunk
591                .regex_flip_flop_rhs_expr_bytecode_ranges
592                .clone(),
593            given_entries: chunk.given_entries.clone(),
594            given_topic_bytecode_ranges: chunk.given_topic_bytecode_ranges.clone(),
595            eval_timeout_entries: chunk.eval_timeout_entries.clone(),
596            eval_timeout_expr_bytecode_ranges: chunk.eval_timeout_expr_bytecode_ranges.clone(),
597            algebraic_match_entries: chunk.algebraic_match_entries.clone(),
598            algebraic_match_subject_bytecode_ranges: chunk
599                .algebraic_match_subject_bytecode_ranges
600                .clone(),
601            par_lines_entries: chunk.par_lines_entries.clone(),
602            par_walk_entries: chunk.par_walk_entries.clone(),
603            pwatch_entries: chunk.pwatch_entries.clone(),
604            substr_four_arg_entries: chunk.substr_four_arg_entries.clone(),
605            keys_expr_entries: chunk.keys_expr_entries.clone(),
606            keys_expr_bytecode_ranges: chunk.keys_expr_bytecode_ranges.clone(),
607            map_expr_entries: chunk.map_expr_entries.clone(),
608            grep_expr_entries: chunk.grep_expr_entries.clone(),
609            regex_flip_flop_rhs_expr_entries: chunk.regex_flip_flop_rhs_expr_entries.clone(),
610            values_expr_entries: chunk.values_expr_entries.clone(),
611            values_expr_bytecode_ranges: chunk.values_expr_bytecode_ranges.clone(),
612            delete_expr_entries: chunk.delete_expr_entries.clone(),
613            exists_expr_entries: chunk.exists_expr_entries.clone(),
614            push_expr_entries: chunk.push_expr_entries.clone(),
615            pop_expr_entries: chunk.pop_expr_entries.clone(),
616            shift_expr_entries: chunk.shift_expr_entries.clone(),
617            unshift_expr_entries: chunk.unshift_expr_entries.clone(),
618            splice_expr_entries: chunk.splice_expr_entries.clone(),
619            lvalues: chunk.lvalues.clone(),
620            ast_eval_exprs: chunk.ast_eval_exprs.clone(),
621            format_decls: chunk.format_decls.clone(),
622            use_overload_entries: chunk.use_overload_entries.clone(),
623            runtime_sub_decls: Arc::new(chunk.runtime_sub_decls.clone()),
624            runtime_advice_decls: Arc::new(chunk.runtime_advice_decls.clone()),
625            ip: 0,
626            stack: Vec::with_capacity(256),
627            call_stack: Vec::with_capacity(32),
628            wantarray_stack: Vec::with_capacity(8),
629            interp,
630            jit_enabled: true,
631            sub_jit_skip_linear: vec![false; chunk.ops.len().saturating_add(1)],
632            sub_jit_skip_block: vec![false; chunk.ops.len().saturating_add(1)],
633            sub_fusevm_meta: vec![None; chunk.ops.len().saturating_add(1)],
634            uscore_name_idx: None,
635            sub_entry_at_ip: {
636                let mut v = vec![false; chunk.ops.len().saturating_add(1)];
637                for (_, e, _) in &chunk.sub_entries {
638                    if *e < v.len() {
639                        v[*e] = true;
640                    }
641                }
642                v
643            },
644            sub_entry_invoke_count: vec![0; chunk.ops.len().saturating_add(1)],
645            jit_sub_invoke_threshold: std::env::var("STRYKE_JIT_SUB_INVOKES")
646                .ok()
647                .and_then(|s| s.parse().ok())
648                .unwrap_or(50),
649            jit_buf_slot: Vec::new(),
650            jit_buf_plain: Vec::new(),
651            jit_buf_arg: Vec::new(),
652            jit_trampoline_out: None,
653            jit_trampoline_depth: 0,
654            halt: false,
655            try_stack: Vec::new(),
656            pending_catch_error: None,
657            exit_main_dispatch: false,
658            exit_main_dispatch_value: None,
659            static_sub_closure_subs,
660            sub_entry_by_name,
661            block_region_mode: false,
662            block_region_end: 0,
663            block_region_return: None,
664        }
665    }
666
667    /// Pop a synthetic [`CallFrame::block_region`] frame if dispatch exited before
668    /// [`Op::BlockReturnValue`] (error or fallthrough), restoring stack and scope.
669    fn unwind_stale_block_region_frame(&mut self) {
670        if let Some(frame) = self.call_stack.pop() {
671            if frame.block_region {
672                self.interp.wantarray_kind = frame.saved_wantarray;
673                self.stack.truncate(frame.stack_base);
674                self.interp.pop_scope_to_depth(frame.scope_depth);
675            } else {
676                self.call_stack.push(frame);
677            }
678        }
679    }
680
681    /// Run `ops[start..end]` (exclusive) for a compiled `map`/`grep`/`sort` block body.
682    ///
683    /// Matches [`VMHelper::exec_block`]: `$_` / `$a` / `$b` are set in the caller before each
684    /// iteration; then one block-local scope frame is pushed (no closure capture) and the body runs
685    /// inline. [`Op::BlockReturnValue`] unwinds that frame via [`Self::unwind_stale_block_region_frame`]
686    /// on error paths here.
687    fn run_block_region(
688        &mut self,
689        start: usize,
690        end: usize,
691        op_count: &mut u64,
692    ) -> StrykeResult<StrykeValue> {
693        // Tier-0 JIT fast path: hand the block body to fusevm's bridge if
694        // the per-IP eligibility cache says it's lowerable. Bypasses the
695        // call-frame + scope-frame push and the interpreter dispatch loop
696        // entirely on cache hits — `try_fusevm_block_region` reads the
697        // block's `$_`/outer-scope vars via the same plain-remap mechanism
698        // signature subs use, runs the seg via `run_linear_segment_cached`,
699        // and returns the block's result value. On cache miss / ineligible
700        // body, falls through to the interpreter path below with no
701        // observable effect.
702        if let Some(v) = self.try_fusevm_block_region(start, end)? {
703            // Count the iteration as one op for the global op-count limit
704            // (mirrors what the interpreter would charge for the body run).
705            *op_count = op_count.saturating_add(1);
706            return Ok(v);
707        }
708
709        let resume_ip = self.ip;
710        let saved_mode = self.block_region_mode;
711        let saved_end = self.block_region_end;
712        let saved_ret = self.block_region_return.take();
713
714        let scope_depth_before = self.interp.scope.depth();
715        let saved_wa = self.interp.wantarray_kind;
716
717        self.call_stack.push(CallFrame {
718            return_ip: 0,
719            stack_base: self.stack.len(),
720            scope_depth: scope_depth_before,
721            saved_wantarray: saved_wa,
722            jit_trampoline_return: false,
723            block_region: true,
724            sub_profiler_start: None,
725        });
726        self.interp.scope_push_hook();
727        self.interp.wantarray_kind = WantarrayCtx::Scalar;
728        self.ip = start;
729        self.block_region_mode = true;
730        self.block_region_end = end;
731        self.block_region_return = None;
732
733        let r = self.run_main_dispatch_loop(StrykeValue::UNDEF, op_count, false);
734        let out = self.block_region_return.take();
735
736        self.block_region_return = saved_ret;
737        self.block_region_mode = saved_mode;
738        self.block_region_end = saved_end;
739        self.ip = resume_ip;
740
741        match r {
742            Ok(_) => {
743                if let Some(val) = out {
744                    Ok(val)
745                } else {
746                    self.unwind_stale_block_region_frame();
747                    Err(StrykeError::runtime(
748                        "block bytecode region did not finish with BlockReturnValue",
749                        self.line(),
750                    ))
751                }
752            }
753            Err(e) => {
754                self.unwind_stale_block_region_frame();
755                Err(e)
756            }
757        }
758    }
759
760    #[inline]
761    fn extend_map_outputs(dst: &mut Vec<StrykeValue>, val: StrykeValue, peel_array_ref: bool) {
762        dst.extend(val.map_flatten_outputs(peel_array_ref));
763    }
764
765    fn map_with_block_common(
766        &mut self,
767        list: Vec<StrykeValue>,
768        block_idx: u16,
769        peel_array_ref: bool,
770        op_count: &mut u64,
771    ) -> StrykeResult<()> {
772        if list.len() == 1 {
773            if let Some(p) = list[0].as_pipeline() {
774                if peel_array_ref {
775                    return Err(StrykeError::runtime(
776                        "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
777                        self.line(),
778                    ));
779                }
780                let idx = block_idx as usize;
781                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
782                let line = self.line();
783                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
784                self.push(StrykeValue::pipeline(Arc::clone(&p)));
785                return Ok(());
786            }
787        }
788        let idx = block_idx as usize;
789        // map's BLOCK is list context. The shared block bytecode region is compiled with a
790        // scalar-context tail (grep/sort consumers need that), so when the block's tail is
791        // list-sensitive (`($_, $_*10)`, `1..$_`, `reverse …`, an array variable, …) fall
792        // back to the interpreter's list-tail [`Interpreter::exec_block_with_tail`]. For
793        // plain scalar tails (`$_ * 2`, `f($_)`, string ops) the bytecode region produces
794        // the same value in either context, so keep using it for speed.
795        let block_tail_is_list_sensitive = self
796            .blocks
797            .get(idx)
798            .and_then(|b| b.last())
799            .map(|stmt| match &stmt.kind {
800                crate::ast::StmtKind::Expression(expr) => {
801                    crate::compiler::expr_tail_is_list_sensitive(expr)
802                }
803                _ => true,
804            })
805            .unwrap_or(true);
806        if !block_tail_is_list_sensitive {
807            if let Some(&(start, end)) =
808                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
809            {
810                // Save / restore the topic chain across the iter loop so
811                // this map stage doesn't leak its final `_` into the
812                // enclosing block's topic. Without this, a per-iter outer
813                // block reading `_` after `inner |> map { … }` returns the
814                // inner pipe's last iter value instead of the outer iter.
815                // Mirrors the sort-block save/restore in vm_helper.rs.
816                let saved_chain = self.interp.scope.save_topic_chain();
817                let mut result = Vec::new();
818                for item in list {
819                    self.interp.scope.set_topic(item);
820                    let val = self.run_block_region(start, end, op_count)?;
821                    Self::extend_map_outputs(&mut result, val, peel_array_ref);
822                }
823                self.interp.scope.restore_topic_chain(saved_chain);
824                self.push(StrykeValue::array(result));
825                return Ok(());
826            }
827        }
828        let block = self.blocks[idx].clone();
829        let saved_chain = self.interp.scope.save_topic_chain();
830        let mut result = Vec::new();
831        for item in list {
832            self.interp.scope.set_topic(item);
833            match self.interp.exec_block_with_tail(&block, WantarrayCtx::List) {
834                Ok(val) => Self::extend_map_outputs(&mut result, val, peel_array_ref),
835                Err(FlowOrError::Error(e)) => {
836                    self.interp.scope.restore_topic_chain(saved_chain);
837                    return Err(e);
838                }
839                Err(_) => {}
840            }
841        }
842        self.interp.scope.restore_topic_chain(saved_chain);
843        self.push(StrykeValue::array(result));
844        Ok(())
845    }
846
847    fn map_with_expr_common(
848        &mut self,
849        list: Vec<StrykeValue>,
850        expr_idx: u16,
851        peel_array_ref: bool,
852        op_count: &mut u64,
853    ) -> StrykeResult<()> {
854        let idx = expr_idx as usize;
855        let dispatch_coderef = !crate::compat_mode();
856        // EXPR-form `map EXPR, LIST`: no block boundary, so use
857        // `set_topic_local` (rebinds `_`/`_0` only, no chain shift, no
858        // slot 1+ zero). Block-form `map { ... }` goes through a
859        // separate dispatch path that uses full `set_topic`.
860        if let Some(&(start, end)) = self
861            .map_expr_bytecode_ranges
862            .get(idx)
863            .and_then(|r| r.as_ref())
864        {
865            let mut result = Vec::new();
866            for item in list {
867                self.interp.scope.set_topic_local(item.clone());
868                let val = self.run_block_region(start, end, op_count)?;
869                let val = self.maybe_call_coderef_with_item(val, &item, dispatch_coderef)?;
870                Self::extend_map_outputs(&mut result, val, peel_array_ref);
871            }
872            self.push(StrykeValue::array(result));
873        } else {
874            let e = self.map_expr_entries[idx].clone();
875            let mut result = Vec::new();
876            for item in list {
877                self.interp.scope.set_topic_local(item.clone());
878                let val = vm_interp_result(
879                    self.interp.eval_expr_ctx(&e, WantarrayCtx::List),
880                    self.line(),
881                )?;
882                let val = self.maybe_call_coderef_with_item(val, &item, dispatch_coderef)?;
883                Self::extend_map_outputs(&mut result, val, peel_array_ref);
884            }
885            self.push(StrykeValue::array(result));
886        }
887        Ok(())
888    }
889
890    /// If `val` is a code reference and `dispatch` is true (i.e. not in
891    /// `--compat` mode), call it with `item` as the sole argument and
892    /// return the call result. Otherwise return `val` unchanged. Powers
893    /// the "coderef-in-expr-position" feature for `grep $f, @l`,
894    /// `map $f, @l`, and pipe-forward `|> grep $f`.
895    fn maybe_call_coderef_with_item(
896        &mut self,
897        val: StrykeValue,
898        item: &StrykeValue,
899        dispatch: bool,
900    ) -> StrykeResult<StrykeValue> {
901        if !dispatch {
902            return Ok(val);
903        }
904        if let Some(sub) = val.as_code_ref() {
905            let sub = sub.clone();
906            let line = self.line();
907            return vm_interp_result(
908                self.interp
909                    .call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line),
910                line,
911            );
912        }
913        Ok(val)
914    }
915
916    /// Consecutive groups: key from block with `$_`; keys compared with [`StrykeValue::str_eq`].
917    fn chunk_by_with_block_common(
918        &mut self,
919        list: Vec<StrykeValue>,
920        block_idx: u16,
921        op_count: &mut u64,
922    ) -> StrykeResult<()> {
923        if list.is_empty() {
924            self.push(StrykeValue::array(vec![]));
925            return Ok(());
926        }
927        let idx = block_idx as usize;
928        let mut chunks: Vec<StrykeValue> = Vec::new();
929        let mut run: Vec<StrykeValue> = Vec::new();
930        let mut prev_key: Option<StrykeValue> = None;
931
932        let eval_key =
933            |vm: &mut VM, item: StrykeValue, op_count: &mut u64| -> StrykeResult<StrykeValue> {
934                vm.interp.scope.set_topic(item);
935                if let Some(&(start, end)) =
936                    vm.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
937                {
938                    vm.run_block_region(start, end, op_count)
939                } else {
940                    let block = vm.blocks[idx].clone();
941                    match vm.interp.exec_block(&block) {
942                        Ok(val) => Ok(val),
943                        Err(FlowOrError::Error(e)) => Err(e),
944                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
945                        Err(_) => Ok(StrykeValue::UNDEF),
946                    }
947                }
948            };
949
950        for item in list {
951            let key = eval_key(self, item.clone(), op_count)?;
952            match &prev_key {
953                None => {
954                    run.push(item);
955                    prev_key = Some(key);
956                }
957                Some(pk) => {
958                    if key.str_eq(pk) {
959                        run.push(item);
960                    } else {
961                        chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
962                            std::mem::take(&mut run),
963                        ))));
964                        run.push(item);
965                        prev_key = Some(key);
966                    }
967                }
968            }
969        }
970        if !run.is_empty() {
971            chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
972        }
973        self.push(StrykeValue::array(chunks));
974        Ok(())
975    }
976
977    fn chunk_by_with_expr_common(
978        &mut self,
979        list: Vec<StrykeValue>,
980        expr_idx: u16,
981        op_count: &mut u64,
982    ) -> StrykeResult<()> {
983        if list.is_empty() {
984            self.push(StrykeValue::array(vec![]));
985            return Ok(());
986        }
987        let idx = expr_idx as usize;
988        let mut chunks: Vec<StrykeValue> = Vec::new();
989        let mut run: Vec<StrykeValue> = Vec::new();
990        let mut prev_key: Option<StrykeValue> = None;
991        for item in list {
992            self.interp.scope.set_topic(item.clone());
993            let key = if let Some(&(start, end)) = self
994                .map_expr_bytecode_ranges
995                .get(idx)
996                .and_then(|r| r.as_ref())
997            {
998                self.run_block_region(start, end, op_count)?
999            } else {
1000                let e = &self.map_expr_entries[idx];
1001                vm_interp_result(
1002                    self.interp.eval_expr_ctx(e, WantarrayCtx::Scalar),
1003                    self.line(),
1004                )?
1005            };
1006            match &prev_key {
1007                None => {
1008                    run.push(item);
1009                    prev_key = Some(key);
1010                }
1011                Some(pk) => {
1012                    if key.str_eq(pk) {
1013                        run.push(item);
1014                    } else {
1015                        chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
1016                            std::mem::take(&mut run),
1017                        ))));
1018                        run.push(item);
1019                        prev_key = Some(key);
1020                    }
1021                }
1022            }
1023        }
1024        if !run.is_empty() {
1025            chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
1026        }
1027        self.push(StrykeValue::array(chunks));
1028        Ok(())
1029    }
1030
1031    #[inline]
1032    fn sub_jit_skip_linear_test(&self, ip: usize) -> bool {
1033        self.sub_jit_skip_linear.get(ip).copied().unwrap_or(false)
1034    }
1035
1036    #[inline]
1037    fn sub_jit_skip_linear_mark(&mut self, ip: usize) {
1038        if ip >= self.sub_jit_skip_linear.len() {
1039            self.sub_jit_skip_linear.resize(ip + 1, false);
1040        }
1041        self.sub_jit_skip_linear[ip] = true;
1042    }
1043
1044    #[inline]
1045    fn sub_jit_skip_block_test(&self, ip: usize) -> bool {
1046        self.sub_jit_skip_block.get(ip).copied().unwrap_or(false)
1047    }
1048
1049    #[inline]
1050    fn sub_jit_skip_block_mark(&mut self, ip: usize) {
1051        if ip >= self.sub_jit_skip_block.len() {
1052            self.sub_jit_skip_block.resize(ip + 1, false);
1053        }
1054        self.sub_jit_skip_block[ip] = true;
1055    }
1056
1057    /// Enable or disable Cranelift JIT for this execution. Disabling skips compilation and buffer
1058    /// prefetch for JIT paths (pure interpreter).
1059    pub fn set_jit_enabled(&mut self, enabled: bool) {
1060        self.jit_enabled = enabled;
1061    }
1062
1063    #[inline]
1064    fn push(&mut self, val: StrykeValue) {
1065        self.stack.push(val);
1066    }
1067
1068    #[inline]
1069    fn pop(&mut self) -> StrykeValue {
1070        self.stack.pop().unwrap_or(StrykeValue::UNDEF)
1071    }
1072
1073    /// Convert a name-based binding ref (`\@array`, `\%hash`, `\$scalar`) into a
1074    /// real `Arc`-based ref by snapshotting the current scope data.  This must be
1075    /// called before the declaring scope is destroyed (e.g. on function return)
1076    /// so the ref survives scope exit — matching Perl 5's refcount semantics.
1077    fn resolve_binding_ref(&self, val: StrykeValue) -> StrykeValue {
1078        if let Some(name) = val.as_array_binding_name() {
1079            let data = self.interp.scope.get_array(&name);
1080            return StrykeValue::array_ref(Arc::new(RwLock::new(data)));
1081        }
1082        if let Some(name) = val.as_hash_binding_name() {
1083            let data = self.interp.scope.get_hash(&name);
1084            return StrykeValue::hash_ref(Arc::new(RwLock::new(data)));
1085        }
1086        if let Some(name) = val.as_scalar_binding_name() {
1087            let data = self.interp.scope.get_scalar(&name);
1088            return StrykeValue::scalar_ref(Arc::new(RwLock::new(data)));
1089        }
1090        val
1091    }
1092
1093    /// Pop `n` array-slice index specs (TOS = last spec). Each spec is a scalar index or an array
1094    /// of indices (list-context `..`, `qw/.../`, parenthesized list), matching
1095    /// [`crate::compiler::Compiler::compile_array_slice_index_expr`]. Returns flattened indices in
1096    /// source order (first spec’s indices first).
1097    fn pop_flattened_array_slice_specs(&mut self, n: usize) -> Vec<i64> {
1098        let mut chunks: Vec<Vec<i64>> = Vec::with_capacity(n);
1099        for _ in 0..n {
1100            let spec = self.pop();
1101            let mut flat = Vec::new();
1102            if let Some(av) = spec.as_array_vec() {
1103                for pv in av.iter() {
1104                    flat.push(pv.to_int());
1105                }
1106            } else {
1107                flat.push(spec.to_int());
1108            }
1109            chunks.push(flat);
1110        }
1111        chunks.reverse();
1112        chunks.into_iter().flatten().collect()
1113    }
1114
1115    /// Call operands are pushed so the rightmost syntactic argument is on top. Restore
1116    /// left-to-right order, then flatten list-valued operands (`qw/.../`, list literals, hashes)
1117    /// into successive scalars — matching Perl's argument list for simple calls. Reversing after
1118    /// flattening would incorrectly reverse elements inside expanded lists.
1119    fn pop_call_operands_flattened(&mut self, argc: usize) -> Vec<StrykeValue> {
1120        let mut slots = Vec::with_capacity(argc);
1121        for _ in 0..argc {
1122            slots.push(self.pop());
1123        }
1124        slots.reverse();
1125        let mut out = Vec::new();
1126        for v in slots {
1127            if let Some(items) = v.as_array_vec() {
1128                out.extend(items);
1129            } else if let Some(h) = v.as_hash_map() {
1130                for (k, val) in h {
1131                    out.push(StrykeValue::string(k));
1132                    out.push(val);
1133                }
1134            } else {
1135                out.push(v);
1136            }
1137        }
1138        out
1139    }
1140
1141    /// Like [`Self::pop_call_operands_flattened`], but each syntactic argument stays one
1142    /// [`StrykeValue`] (`zip` / `mesh` need full lists per operand, not Perl's flattened `@_`).
1143    fn pop_call_operands_preserved(&mut self, argc: usize) -> Vec<StrykeValue> {
1144        let mut slots = Vec::with_capacity(argc);
1145        for _ in 0..argc {
1146            slots.push(self.pop());
1147        }
1148        slots.reverse();
1149        slots
1150    }
1151
1152    #[inline]
1153    fn call_preserve_operand_arrays(name: &str) -> bool {
1154        // Stryke builtins are unprefixed; `CORE::` callers route to bare names.
1155        let name = name.strip_prefix("CORE::").unwrap_or(name);
1156        matches!(
1157            name,
1158            "zip"
1159                | "zip_longest"
1160                | "zip_shortest"
1161                | "mesh"
1162                | "mesh_longest"
1163                | "mesh_shortest"
1164                | "take"
1165                | "head"
1166                | "tail"
1167                | "drop"
1168                // `len` / `count` / … must receive list-valued operands as **one** value.
1169                // Otherwise `len stat $path` flattens `@_`: empty stat → 0 args → `$_` fallback
1170                // (wrong), success → 13 args → list_count semantics (wrong for `len`).
1171                | "len"
1172                | "cnt"
1173                | "count"
1174                | "list_count"
1175                | "list_size"
1176        )
1177    }
1178
1179    fn flatten_array_slice_specs_ordered_values(
1180        &self,
1181        specs: &[StrykeValue],
1182    ) -> Result<Vec<i64>, StrykeError> {
1183        let mut out = Vec::new();
1184        for spec in specs {
1185            if let Some(av) = spec.as_array_vec() {
1186                for pv in av.iter() {
1187                    out.push(pv.to_int());
1188                }
1189            } else {
1190                out.push(spec.to_int());
1191            }
1192        }
1193        Ok(out)
1194    }
1195
1196    /// Hash `{…}` slice key slots in source order (each slot may expand to many string keys).
1197    fn flatten_hash_slice_key_slots(key_vals: &[StrykeValue]) -> Vec<String> {
1198        let mut ks = Vec::new();
1199        for kv in key_vals {
1200            if let Some(vv) = kv.as_array_vec() {
1201                ks.extend(vv.iter().map(|x| x.to_string()));
1202            } else {
1203                ks.push(kv.to_string());
1204            }
1205        }
1206        ks
1207    }
1208
1209    #[inline]
1210    fn peek(&self) -> &StrykeValue {
1211        self.stack.last().unwrap_or(&PEEK_UNDEF)
1212    }
1213
1214    #[inline]
1215    fn constant(&self, idx: u16) -> &StrykeValue {
1216        &self.constants[idx as usize]
1217    }
1218
1219    fn line(&self) -> usize {
1220        self.lines
1221            .get(self.ip.saturating_sub(1))
1222            .copied()
1223            .unwrap_or(0)
1224    }
1225
1226    /// Tier-0 JIT: run a `ReturnValue`-terminated subroutine body on the shared
1227    /// [`fusevm`] runtime when its ops are in the strict universal-integer slot
1228    /// subset (see [`crate::fusevm_bridge::segment_is_fusevm_eligible`]).
1229    ///
1230    /// Reuses the same segment analysis and `i64` slot marshaling as
1231    /// [`Self::try_jit_subroutine_linear`], so it only accepts bodies that path
1232    /// would also accept. Returns `Ok(true)` when fusevm executed the sub and the
1233    /// VM should continue at `return_ip`; `Ok(false)` falls through to
1234    /// strykelang's own JIT/interpreter with no observable effect.
1235    fn try_fusevm_subroutine(&mut self) -> Result<bool, StrykeError> {
1236        let ip = self.ip;
1237        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1238        let ops: &Vec<Op> = &self.ops;
1239        let ops = ops as *const Vec<Op>;
1240        let ops = unsafe { &*ops };
1241        let Some((full_seg, term)) = crate::jit::sub_entry_segment(ops, ip) else {
1242            return Ok(false);
1243        };
1244        if !matches!(term, crate::jit::SubTerminator::Value) {
1245            return Ok(false);
1246        }
1247
1248        // Most real subs open with the `my (...) = @_` argument-unpacking prologue
1249        // (array ops the universal subset can't JIT). Recognize that fixed idiom and
1250        // *skip* it: the declared scalar slots are seeded directly from `@_`, and the
1251        // remaining body — the part that's actually pure arithmetic/string work — is
1252        // what we hand to fusevm. Without this, virtually no real sub is ever
1253        // eligible, so the on-disk JIT cache never engages for them.
1254        let uscore = match self.uscore_name_idx {
1255            Some(cached) => cached,
1256            None => {
1257                let v = self.names.iter().position(|n| n == "_").and_then(|p| u16::try_from(p).ok());
1258                self.uscore_name_idx = Some(v);
1259                v
1260            }
1261        };
1262        let (seg, seg_ip, arg_binds): (&[Op], usize, Vec<(u8, usize)>) = match uscore
1263            .and_then(|u| crate::jit::recognize_args_unpack_prologue(full_seg, u))
1264        {
1265            Some((plen, binds)) => (&full_seg[plen..], ip + plen, binds),
1266            None => (full_seg, ip, Vec::new()),
1267        };
1268
1269        // Signature subs (`sub f($x,$y){ $x + $y }`) reference their parameters by
1270        // name (`GetScalarPlain`), not via the `@_`-unpack prologue, so they were
1271        // never JIT-eligible. Remap those *read-only* named reads to synthetic slots
1272        // (numbered past every real slot the body already uses) seeded from scope
1273        // below. The rewrite is value-independent, so the disk-cache `op_hash` stays
1274        // stable across calls. We bail (no remap) if any named scalar is written.
1275        let base_slot = crate::jit::linear_slot_ops_max_index_seq(seg)
1276            .map(|m| m as usize + 1)
1277            .unwrap_or(0);
1278        let plain_remap: Option<(Vec<Op>, Vec<(u8, u16)>)> = if base_slot <= u8::MAX as usize {
1279            crate::jit::plain_scalar_read_names(seg).and_then(|pnames| {
1280                if base_slot + pnames.len() <= u8::MAX as usize + 1 {
1281                    Some(crate::jit::remap_plain_reads_to_slots(
1282                        seg,
1283                        &pnames,
1284                        base_slot as u8,
1285                    ))
1286                } else {
1287                    None
1288                }
1289            })
1290        } else {
1291            None
1292        };
1293        let (seg, plain_binds): (&[Op], &[(u8, u16)]) = match &plain_remap {
1294            Some((normalized, binds)) => (normalized.as_slice(), binds.as_slice()),
1295            None => (seg, &[][..]),
1296        };
1297
1298        // Per-sub-IP eligibility cache: first call to this sub runs all 13+
1299        // detectors; subsequent calls hit the cache. `None` = not yet
1300        // analyzed; `Some(None)` = known-not-eligible (skip the bridge);
1301        // `Some(Some(elig))` = cached dispatch verdict.
1302        //
1303        // Use the ORIGINAL sub-entry ip as the cache key (not seg_ip, which
1304        // moves around when plain_remap_data fires — the cache shouldn't be
1305        // sensitive to which form of the seg we analyzed).
1306        let cache_ip = ip;
1307        if cache_ip >= self.sub_fusevm_meta.len() {
1308            self.sub_fusevm_meta.resize(cache_ip + 1, None);
1309        }
1310        // Ensure the entry is populated (compute on miss). Borrow-checker note:
1311        // we can't return a `&FusevmSubElig` from the match while also keeping
1312        // `&mut self` available for the seeder below, so we extract the
1313        // `Copy` fields up front and re-borrow the entry later via index when
1314        // we need the (interior-mut) cached_chunk OnceCell.
1315        match &self.sub_fusevm_meta[cache_ip] {
1316            Some(None) => return Ok(false),
1317            Some(Some(_)) => {}
1318            None => {
1319                let computed = compute_fusevm_elig(seg, seg_ip);
1320                self.sub_fusevm_meta[cache_ip] = Some(computed);
1321                if matches!(&self.sub_fusevm_meta[cache_ip], Some(None)) {
1322                    return Ok(false);
1323                }
1324            }
1325        }
1326        // Extract the Copy dispatch fields (released the borrow on next line).
1327        let (dispatch, str_handle_slot) = {
1328            let e = self.sub_fusevm_meta[cache_ip]
1329                .as_ref()
1330                .and_then(|x| x.as_ref())
1331                .expect("populated above");
1332            (e.dispatch, e.str_handle_slot)
1333        };
1334        let str_ok = matches!(dispatch, FusevmDispatch::Str);
1335        let lit_str_sprintf_ok = matches!(dispatch, FusevmDispatch::LitStrSprintf);
1336        let val_unary_ok = matches!(dispatch, FusevmDispatch::ValUnary);
1337
1338        // Map each arg-bound slot to its `@_` index. Slots not in this map are body
1339        // locals (seeded 0 when write-before-read) or, for string segments, read
1340        // from the current scope.
1341        let arg_of_slot = |slot: u8| -> Option<usize> {
1342            arg_binds.iter().find(|(s, _)| *s == slot).map(|(_, a)| *a)
1343        };
1344        // `@_` is already populated at the sub entry (the prologue we skipped would
1345        // have read it); fetch it once to seed the bound slots.
1346        let argv: Vec<StrykeValue> = if arg_binds.is_empty() {
1347            Vec::new()
1348        } else {
1349            self.interp.scope.get_array("_")
1350        };
1351
1352        // Resolve each remapped signature-parameter slot to its current scope value
1353        // (read once by name, up front). These flow into the synthetic slots below
1354        // exactly like `@_`-bound args, but sourced from the named scalar instead.
1355        let plain_vals: Vec<(u8, StrykeValue)> = plain_binds
1356            .iter()
1357            .map(|(slot, name_idx)| {
1358                let nm = self.names[*name_idx as usize].clone();
1359                (*slot, self.interp.scope.get_scalar(&nm))
1360            })
1361            .collect();
1362        let plain_of_slot = |slot: u8| -> Option<&StrykeValue> {
1363            plain_vals.iter().find(|(s, _)| *s == slot).map(|(_, v)| v)
1364        };
1365
1366        // Marshal the slots the body reads. Integer segments seed unboxed i64 values
1367        // (and seed 0 for write-before-read slots via `slot_undef_prefill_ok_seq`, so
1368        // the chunk stays identical across calls). String-comparison/concat segments
1369        // instead seed the raw NaN-boxed `StrykeValue` bits as i64 handles, which the
1370        // host helper reconstructs; those are routed only when every operand is a
1371        // plain string (`is_string_like`), bailing to the interpreter otherwise so
1372        // operator-overloading and numeric-coercion semantics are kept. Arg-bound
1373        // slots are seeded from `@_` instead of the (skipped) prologue's declarations,
1374        // and remapped signature-param slots from their named scope scalar.
1375        let mut slot_n = 0usize;
1376        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1377            let n = max as usize + 1;
1378            self.jit_buf_slot.resize(n, 0);
1379            // Per-slot kind for str-bearing integer-result segments: `length($s)`,
1380            // `length($s) >= $min`, `index($s, "x") > 0`, etc. — slots whose
1381            // value the segment uses as a *string handle* are seeded as raw bits;
1382            // slots used as *plain ints* are seeded unboxed. The general
1383            // analyzer infers each slot's kind from how its value gets
1384            // consumed; `None` means use the legacy str_ok blanket rule.
1385            let str_slot_kinds: Option<Vec<bool>> = if str_ok {
1386                crate::fusevm_bridge::string_bearing_int_result_slot_kinds(seg, seg_ip)
1387            } else {
1388                None
1389            };
1390            // Whether slot `i` marshals as a NaN-boxed string handle (vs an unboxed
1391            // integer). For the mixed `substr`/`x`-repeat family only the
1392            // designated string slot wants a handle. The general str-bearing
1393            // analyzer (if it matched) returns a per-slot kind map. Otherwise
1394            // uniform `str_ok || val_unary_ok` for the all-string / any-value
1395            // families. For `val_unary_ok` we additionally bypass the
1396            // `is_string_like` gate inside the seeder below (see the
1397            // `bypass_type_gate` flag), because `defined` accepts UNDEF and
1398            // any other type.
1399            let wants_string = |i: u8| -> bool {
1400                match str_handle_slot {
1401                    Some(str_slot) => i == str_slot,
1402                    None => match &str_slot_kinds {
1403                        Some(kinds) => kinds.get(i as usize).copied().unwrap_or(false),
1404                        None => str_ok || val_unary_ok || lit_str_sprintf_ok,
1405                    },
1406                }
1407            };
1408            // Whether to bypass the `is_string_like` gate when seeding a handle
1409            // slot — true for any-value segments (`defined`/`ref`) AND for
1410            // `sprintf("FMT", $arg)` which dispatches arg-type by format
1411            // directive inside the helper, not at seed time.
1412            let bypass_type_gate = val_unary_ok || lit_str_sprintf_ok;
1413            for i in 0..=max {
1414                let bound = arg_of_slot(i);
1415                self.jit_buf_slot[i as usize] = if let Some(pv) = plain_of_slot(i) {
1416                    if wants_string(i) {
1417                        if !bypass_type_gate && !pv.is_string_like() {
1418                            return Ok(false);
1419                        }
1420                        pv.raw_bits() as i64
1421                    } else {
1422                        match pv.as_integer() {
1423                            Some(v) => v,
1424                            None => return Ok(false),
1425                        }
1426                    }
1427                } else if wants_string(i) {
1428                    // SAFETY: must use `shallow_clone` (Arc::clone) rather than the
1429                    // default `Clone` (which deep-clones the heap payload — new Arc,
1430                    // new String). The seeded `i64` is a *handle* to the heap pointer;
1431                    // when this local `v` drops at the end of the iteration its Arc
1432                    // refcount must NOT take the heap with it, or the chunk's JIT
1433                    // helper would dereference freed memory at execution time.
1434                    // `shallow_clone` bumps the original Arc held by `argv[a]` (which
1435                    // outlives this whole function), so the heap stays alive across
1436                    // the JIT call. Same reasoning for `get_scalar_slot`'s return: it
1437                    // already gives back a `StrykeValue` whose Arc is held by the
1438                    // scope, so a normal value-move keeps the heap alive.
1439                    let v = match bound {
1440                        Some(a) => match argv.get(a) {
1441                            Some(vr) => vr.shallow_clone(),
1442                            None => StrykeValue::UNDEF,
1443                        },
1444                        None => self.interp.scope.get_scalar_slot(i),
1445                    };
1446                    if !bypass_type_gate && !v.is_string_like() {
1447                        return Ok(false);
1448                    }
1449                    v.raw_bits() as i64
1450                } else if let Some(a) = bound {
1451                    match argv.get(a).and_then(|v| v.as_integer()) {
1452                        Some(v) => v,
1453                        None => return Ok(false),
1454                    }
1455                } else if crate::jit::slot_undef_prefill_ok_seq(seg, i) {
1456                    0
1457                } else {
1458                    match self.interp.scope.get_scalar_slot(i).as_integer() {
1459                        Some(v) => v,
1460                        None => return Ok(false),
1461                    }
1462                };
1463            }
1464            slot_n = n;
1465        }
1466
1467        // Refresh the `length` helper's view of the runtime `utf8` pragma so a
1468        // JIT-computed `length($s)` matches the interpreter under `use utf8` /
1469        // `no utf8` (which toggle the pragma at runtime). Cheap and harmless for
1470        // non-length segments.
1471        crate::fusevm_bridge::set_utf8_pragma(self.interp.utf8_pragma);
1472        // Hand the bridge a borrow of the cached fusevm chunk if we have one;
1473        // on cache miss the bridge returns the freshly-built Arc so we can
1474        // populate the OnceCell for the next call. The OnceCell ::get/::set
1475        // pair are &self methods so we don't need a fresh &mut self here.
1476        let cached_chunk_arc: Option<std::sync::Arc<fusevm::Chunk>> = self
1477            .sub_fusevm_meta
1478            .get(cache_ip)
1479            .and_then(|x| x.as_ref())
1480            .and_then(|x| x.as_ref())
1481            .and_then(|e| e.cached_chunk.get().cloned());
1482        let (result_opt, fresh_chunk) = crate::fusevm_bridge::run_linear_segment_cached(
1483            seg,
1484            seg_ip,
1485            &mut self.jit_buf_slot[..slot_n],
1486            term,
1487            &self.constants,
1488            cached_chunk_arc.as_ref(),
1489        );
1490        if let Some(fresh) = fresh_chunk {
1491            if let Some(e) = self
1492                .sub_fusevm_meta
1493                .get(cache_ip)
1494                .and_then(|x| x.as_ref())
1495                .and_then(|x| x.as_ref())
1496            {
1497                let _ = e.cached_chunk.set(fresh);
1498            }
1499        }
1500        let Some(v) = result_opt else {
1501            return Ok(false);
1502        };
1503
1504        // The eligible segment is a whole sub body terminated by `ReturnValue`; every
1505        // slot it touches is a frame-local declared inside the sub. Those locals are
1506        // discarded when the call frame is popped below, so there is nothing to write
1507        // back — only the return value `v` propagates. (Writing them back would be
1508        // wrong: because the fusevm chunk replaces the body's `DeclareScalarSlot` ops,
1509        // this frame never *owns* the slots, so `set_scalar_slot` would walk outward
1510        // and clobber the caller's identically-numbered slots.)
1511        if let Some(frame) = self.call_stack.pop() {
1512            self.interp.wantarray_kind = frame.saved_wantarray;
1513            self.stack.truncate(frame.stack_base);
1514            self.interp.pop_scope_to_depth(frame.scope_depth);
1515            if frame.jit_trampoline_return {
1516                self.jit_trampoline_out = Some(v);
1517            } else {
1518                self.push(v);
1519                self.ip = frame.return_ip;
1520            }
1521        }
1522        Ok(true)
1523    }
1524
1525    /// Tier-0 JIT for block-region bodies (map/grep/sort/foreach inline blocks
1526    /// run by [`Self::run_block_region`]). Parallels [`Self::try_fusevm_subroutine`]
1527    /// but:
1528    /// - Takes the precomputed `(start, end)` region instead of scanning for a
1529    ///   `ReturnValue` terminator. The region ends with `Op::BlockReturnValue`;
1530    ///   the bridge runs the seg without that terminator and the result becomes
1531    ///   the block's return value (the runtime equivalent of what
1532    ///   `BlockReturnValue` would have pushed into `block_region_return`).
1533    /// - Has no `@_`-unpack prologue — blocks don't have arg lists. Synthetic-
1534    ///   slot remapping via [`crate::jit::plain_scalar_read_names`] still
1535    ///   applies for `$_` / outer-scope reads, mirroring how signature-sub
1536    ///   reads are handled in `try_fusevm_subroutine`.
1537    /// - Returns `Ok(Some(value))` on JIT success (caller bypasses the
1538    ///   interpreter dispatch loop entirely, no call-frame push needed), or
1539    ///   `Ok(None)` to fall through to the existing block-region interpreter
1540    ///   path (which pushes a fresh call frame + scope frame and runs the
1541    ///   main dispatch loop).
1542    ///
1543    /// The cache (`sub_fusevm_meta`) is keyed by `start` IP — block-region
1544    /// starts don't collide with sub-entry IPs (different positions in the
1545    /// bytecode), so reusing the same `Vec<Option<Option<FusevmSubElig>>>`
1546    /// is safe and saves a separate per-block-IP cache structure.
1547    fn try_fusevm_block_region(
1548        &mut self,
1549        start: usize,
1550        end: usize,
1551    ) -> Result<Option<StrykeValue>, StrykeError> {
1552        let ops: &Vec<Op> = &self.ops;
1553        let ops = ops as *const Vec<Op>;
1554        let ops = unsafe { &*ops };
1555
1556        // Sanity-check the range and require BlockReturnValue as the
1557        // terminator. Block regions with mid-flow terminators (early returns,
1558        // `last`/`next` from outer loops, etc.) fall through to the
1559        // interpreter.
1560        if start >= end || end > ops.len() {
1561            return Ok(None);
1562        }
1563        if !matches!(ops.get(end - 1), Some(Op::BlockReturnValue)) {
1564            return Ok(None);
1565        }
1566        let full_seg = &ops[start..end - 1];
1567        if full_seg.is_empty() {
1568            return Ok(None);
1569        }
1570
1571        // Bail on any WRITE to a scalar (named or slot) inside the block.
1572        //
1573        // Unlike sub bodies (which run in their own scope and discard locals on
1574        // return), block bodies share the outer scope. `map { $s += $_ }` /
1575        // `e { $n++ }` / `foreach { $sum += $_ }` mutate outer-captured
1576        // lexicals, and the bridge has NO writeback path:
1577        //   - For NAMED writes (`Op::SetScalarPlain` / `ScalarCompoundAssign` /
1578        //     `PreInc(name_idx)` etc.) — the bridge's plain-remap rewrites only
1579        //     reads; writes were never plumbed back to scope.
1580        //   - For SLOT writes (`Op::SetScalarSlot` / `AddAssignSlotSlot` /
1581        //     `PreIncSlot` etc.) — the bridge runs ops against `jit_buf_slot`
1582        //     but never copies the slot values back to
1583        //     `scope.set_scalar_slot(i, ...)` after the seg runs (see seeder
1584        //     loop above; there is no symmetric drain loop after
1585        //     `run_linear_segment_cached`).
1586        //
1587        // Net effect when not bailed: `$s == 0` after a map that should have
1588        // summed it to 6 (regression in `map_block_mutates_outer_lexical`,
1589        // `e_block_visit_count_matches_input_length`, and 6 sibling tests in
1590        // the `ep_each_iteration_pin` suite). Also `pfor { $x = 1 }` is
1591        // supposed to raise a Runtime error from the parallel-write guard —
1592        // the bridge would let the body succeed silently
1593        // (`parallel_block_rejects_captured_lexical_assignment`).
1594        //
1595        // This bail is conservative: pure-read blocks (`grep { length($_) > 5 }`,
1596        // `map { $_ * 2 }`) carry no scalar-write ops and stay bridge-eligible.
1597        // Block-local `my $x = …; …; $x` is also bailed for now since the slot
1598        // index alone doesn't distinguish block-local from outer; a future
1599        // refinement can use `DeclareScalarSlot` as a watermark to allow
1600        // slot-writes whose targets were declared inside the seg.
1601        for op in full_seg {
1602            match op {
1603                Op::SetScalar(_)
1604                | Op::SetScalarPlain(_)
1605                | Op::SetScalarKeep(_)
1606                | Op::SetScalarKeepPlain(_)
1607                | Op::PreInc(_)
1608                | Op::PreDec(_)
1609                | Op::PostInc(_)
1610                | Op::PostDec(_)
1611                | Op::ScalarCompoundAssign { .. }
1612                | Op::SetScalarSlot(_)
1613                | Op::SetScalarSlotKeep(_)
1614                | Op::PreIncSlot(_)
1615                | Op::PreIncSlotVoid(_)
1616                | Op::PostIncSlot(_)
1617                | Op::PreDecSlot(_)
1618                | Op::PostDecSlot(_)
1619                | Op::AddAssignSlotSlot(_, _)
1620                | Op::AddAssignSlotSlotVoid(_, _) => return Ok(None),
1621                // String comparisons require operand decode beyond the i64
1622                // seeder; e.g. `grep { _ eq $node } @seen` reads `$node` from
1623                // an outer-scope slot, but the bridge seeds slot values as
1624                // `raw_bits() as i64` and compares with int semantics. For
1625                // an integer-typed `$node` this is fine; for a string
1626                // (`"SF"`), the int form is a stale handle that compares as
1627                // some unrelated i64. The grep returns "no match" for items
1628                // that should match, which makes `next if grep { ... }` not
1629                // skip when it should — caught by
1630                // `demo_graph_bfs` which fell into a BFS-without-seen-check
1631                // infinite loop. Conservatively bail on these compares; the
1632                // bridge can still lower numeric grep bodies (`grep { _ > N }`).
1633                Op::StrEq | Op::StrNe => return Ok(None),
1634                _ => {}
1635            }
1636        }
1637
1638        // Same plain-scalar-read remap as sig subs: rewrite GetScalarPlain(name)
1639        // → GetScalarSlot(synth) so the bridge can seed `$_`/outer-scope reads
1640        // through its slot mechanism. Bails on any named-scalar WRITE.
1641        let base_slot = crate::jit::linear_slot_ops_max_index_seq(full_seg)
1642            .map(|m| m as usize + 1)
1643            .unwrap_or(0);
1644        let plain_remap: Option<(Vec<Op>, Vec<(u8, u16)>)> = if base_slot <= u8::MAX as usize {
1645            crate::jit::plain_scalar_read_names(full_seg).and_then(|pnames| {
1646                if base_slot + pnames.len() <= u8::MAX as usize + 1 {
1647                    Some(crate::jit::remap_plain_reads_to_slots(
1648                        full_seg,
1649                        &pnames,
1650                        base_slot as u8,
1651                    ))
1652                } else {
1653                    None
1654                }
1655            })
1656        } else {
1657            None
1658        };
1659        let (seg, plain_binds): (&[Op], &[(u8, u16)]) = match &plain_remap {
1660            Some((normalized, binds)) => (normalized.as_slice(), binds.as_slice()),
1661            None => (full_seg, &[][..]),
1662        };
1663        let seg_ip = start;
1664
1665        // Eligibility cache (same Vec used by try_fusevm_subroutine — IPs
1666        // don't collide between sub-entries and block-region starts).
1667        let cache_ip = start;
1668        if cache_ip >= self.sub_fusevm_meta.len() {
1669            self.sub_fusevm_meta.resize(cache_ip + 1, None);
1670        }
1671        match &self.sub_fusevm_meta[cache_ip] {
1672            Some(None) => return Ok(None),
1673            Some(Some(_)) => {}
1674            None => {
1675                let computed = compute_fusevm_elig(seg, seg_ip);
1676                self.sub_fusevm_meta[cache_ip] = Some(computed);
1677                if matches!(&self.sub_fusevm_meta[cache_ip], Some(None)) {
1678                    return Ok(None);
1679                }
1680            }
1681        }
1682        let (dispatch, str_handle_slot) = {
1683            let e = self.sub_fusevm_meta[cache_ip]
1684                .as_ref()
1685                .and_then(|x| x.as_ref())
1686                .expect("populated above");
1687            (e.dispatch, e.str_handle_slot)
1688        };
1689        let str_ok = matches!(dispatch, FusevmDispatch::Str);
1690        let lit_str_sprintf_ok = matches!(dispatch, FusevmDispatch::LitStrSprintf);
1691        let val_unary_ok = matches!(dispatch, FusevmDispatch::ValUnary);
1692
1693        // Resolve each plain-remapped synthetic slot to its current scope
1694        // value. For grep `{ length($_) > 1 }` this reads `$_` (the topic,
1695        // set by the caller before each iteration) into one synth slot.
1696        let plain_vals: Vec<(u8, StrykeValue)> = plain_binds
1697            .iter()
1698            .map(|(slot, name_idx)| {
1699                let nm = self.names[*name_idx as usize].clone();
1700                (*slot, self.interp.scope.get_scalar(&nm))
1701            })
1702            .collect();
1703        let plain_of_slot = |slot: u8| -> Option<&StrykeValue> {
1704            plain_vals.iter().find(|(s, _)| *s == slot).map(|(_, v)| v)
1705        };
1706
1707        // Seeder. Same per-slot kind matrix as try_fusevm_subroutine, minus
1708        // the @_ arg-bound branch (blocks have no args). Body-local slots
1709        // get 0 (write-before-read) or are read from the current scope.
1710        let mut slot_n = 0usize;
1711        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1712            let n = max as usize + 1;
1713            self.jit_buf_slot.resize(n, 0);
1714            let str_slot_kinds: Option<Vec<bool>> = if str_ok {
1715                crate::fusevm_bridge::string_bearing_int_result_slot_kinds(seg, seg_ip)
1716            } else {
1717                None
1718            };
1719            let wants_string = |i: u8| -> bool {
1720                match str_handle_slot {
1721                    Some(str_slot) => i == str_slot,
1722                    None => match &str_slot_kinds {
1723                        Some(kinds) => kinds.get(i as usize).copied().unwrap_or(false),
1724                        None => str_ok || val_unary_ok || lit_str_sprintf_ok,
1725                    },
1726                }
1727            };
1728            // Block regions bypass the `is_string_like` gate unconditionally:
1729            // block items (`$_` from grep/map/sort) can be any type — numbers,
1730            // strings, refs — and Perl-style stringification is the standard
1731            // (`length(42)` == 2, `42 eq "42"`, etc.). The bridge's str
1732            // helpers call `as_str()` / `length_value()` / `ord_value()` /
1733            // etc. which all stringify internally, matching the interpreter's
1734            // semantics for non-overloaded values. The narrow risk is an
1735            // operator-overload (`use overload '""'`) producing a different
1736            // string than the helpers' naive stringification — for grep/map
1737            // bodies the convention is to use unblessed scalars, so this
1738            // tradeoff is the right default.
1739            //
1740            // For ValUnary / LitStrSprintf the gate-bypass was already needed
1741            // for sub-call dispatch (`defined`/`ref`/`sprintf` accept any
1742            // type); the unconditional `true` here covers both those cases
1743            // and the general block-region case.
1744            let bypass_type_gate = true;
1745            for i in 0..=max {
1746                self.jit_buf_slot[i as usize] = if let Some(pv) = plain_of_slot(i) {
1747                    if wants_string(i) {
1748                        if !bypass_type_gate && !pv.is_string_like() {
1749                            return Ok(None);
1750                        }
1751                        pv.raw_bits() as i64
1752                    } else {
1753                        match pv.as_integer() {
1754                            Some(v) => v,
1755                            None => return Ok(None),
1756                        }
1757                    }
1758                } else if wants_string(i) {
1759                    let v = self.interp.scope.get_scalar_slot(i);
1760                    if !bypass_type_gate && !v.is_string_like() {
1761                        return Ok(None);
1762                    }
1763                    v.raw_bits() as i64
1764                } else if crate::jit::slot_undef_prefill_ok_seq(seg, i) {
1765                    0
1766                } else {
1767                    match self.interp.scope.get_scalar_slot(i).as_integer() {
1768                        Some(v) => v,
1769                        None => return Ok(None),
1770                    }
1771                };
1772            }
1773            // Silence unused-variable warnings for the val_*_ok flags now
1774            // that bypass_type_gate is unconditionally true.
1775            let _ = (val_unary_ok, lit_str_sprintf_ok);
1776            slot_n = n;
1777        }
1778
1779        crate::fusevm_bridge::set_utf8_pragma(self.interp.utf8_pragma);
1780        let cached_chunk_arc: Option<std::sync::Arc<fusevm::Chunk>> = self
1781            .sub_fusevm_meta
1782            .get(cache_ip)
1783            .and_then(|x| x.as_ref())
1784            .and_then(|x| x.as_ref())
1785            .and_then(|e| e.cached_chunk.get().cloned());
1786        let (result_opt, fresh_chunk) = crate::fusevm_bridge::run_linear_segment_cached(
1787            seg,
1788            seg_ip,
1789            &mut self.jit_buf_slot[..slot_n],
1790            crate::jit::SubTerminator::Value,
1791            &self.constants,
1792            cached_chunk_arc.as_ref(),
1793        );
1794        if let Some(fresh) = fresh_chunk {
1795            if let Some(e) = self
1796                .sub_fusevm_meta
1797                .get(cache_ip)
1798                .and_then(|x| x.as_ref())
1799                .and_then(|x| x.as_ref())
1800            {
1801                let _ = e.cached_chunk.set(fresh);
1802            }
1803        }
1804        Ok(result_opt)
1805    }
1806
1807    /// Cranelift linear JIT for a subroutine body when `ip` is a compiled sub entry (see `Chunk::sub_entries`).
1808    /// Returns `Ok(true)` when the sub was executed natively and the VM should continue at `return_ip`.
1809    fn try_jit_subroutine_linear(&mut self) -> Result<bool, StrykeError> {
1810        let ip = self.ip;
1811        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1812        if self.sub_jit_skip_linear_test(ip) {
1813            return Ok(false);
1814        }
1815        let ops: &Vec<Op> = &self.ops;
1816        let ops = ops as *const Vec<Op>;
1817        let ops = unsafe { &*ops };
1818        let constants: &Vec<StrykeValue> = &self.constants;
1819        let constants = constants as *const Vec<StrykeValue>;
1820        let constants = unsafe { &*constants };
1821        let names: &Vec<String> = &self.names;
1822        let names = names as *const Vec<String>;
1823        let names = unsafe { &*names };
1824        let Some((seg, _)) = crate::jit::sub_entry_segment(ops, ip) else {
1825            return Ok(false);
1826        };
1827        // `try_run_linear_sub` rejects these segments without compiling — skip expensive work before
1828        // resize/fill of reusable scratch buffers (`jit_buf_*`).
1829        if crate::jit::segment_blocks_subroutine_linear_jit(seg, &self.sub_entries) {
1830            self.sub_jit_skip_linear_mark(ip);
1831            return Ok(false);
1832        }
1833        let mut slot_len: Option<usize> = None;
1834        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1835            let n = max as usize + 1;
1836            self.jit_buf_slot.resize(n, 0);
1837            let mut ok = true;
1838            for i in 0..=max {
1839                let pv = self.interp.scope.get_scalar_slot(i);
1840                self.jit_buf_slot[i as usize] = match pv.as_integer() {
1841                    Some(v) => v,
1842                    None if pv.is_undef() && crate::jit::slot_undef_prefill_ok_seq(seg, i) => 0,
1843                    None => {
1844                        ok = false;
1845                        break;
1846                    }
1847                };
1848            }
1849            if ok {
1850                slot_len = Some(n);
1851            }
1852        }
1853        let mut plain_len: Option<usize> = None;
1854        if let Some(max) = crate::jit::linear_plain_ops_max_index_seq(seg) {
1855            if (max as usize) < names.len() {
1856                let n = max as usize + 1;
1857                self.jit_buf_plain.resize(n, 0);
1858                let mut ok = true;
1859                for i in 0..=max {
1860                    let nm = names[i as usize].as_str();
1861                    match self.interp.scope.get_scalar(nm).as_integer() {
1862                        Some(v) => self.jit_buf_plain[i as usize] = v,
1863                        None => {
1864                            ok = false;
1865                            break;
1866                        }
1867                    }
1868                }
1869                if ok {
1870                    plain_len = Some(n);
1871                }
1872            }
1873        }
1874        let mut arg_len: Option<usize> = None;
1875        if let Some(max) = crate::jit::linear_arg_ops_max_index_seq(seg) {
1876            if let Some(frame) = self.call_stack.last() {
1877                let base = frame.stack_base;
1878                let n = max as usize + 1;
1879                self.jit_buf_arg.resize(n, 0);
1880                let mut ok = true;
1881                for i in 0..=max {
1882                    let pos = base + i as usize;
1883                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1884                    match pv.as_integer() {
1885                        Some(v) => self.jit_buf_arg[i as usize] = v,
1886                        None => {
1887                            ok = false;
1888                            break;
1889                        }
1890                    }
1891                }
1892                if ok {
1893                    arg_len = Some(n);
1894                }
1895            }
1896        }
1897        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1898        let slot_buf = slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1899        let plain_buf = plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1900        let arg_buf = arg_len.map(|n| &self.jit_buf_arg[..n]);
1901        let Some(v) = crate::jit::try_run_linear_sub(
1902            ops,
1903            ip,
1904            slot_buf,
1905            plain_buf,
1906            arg_buf,
1907            constants,
1908            &self.sub_entries,
1909            vm_ptr,
1910        ) else {
1911            return Ok(false);
1912        };
1913        if let Some(n) = slot_len {
1914            let buf = &self.jit_buf_slot[..n];
1915            for idx in crate::jit::linear_slot_ops_written_indices_seq(seg) {
1916                self.interp
1917                    .scope
1918                    .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
1919            }
1920        }
1921        if let Some(n) = plain_len {
1922            let buf = &self.jit_buf_plain[..n];
1923            for idx in crate::jit::linear_plain_ops_written_indices_seq(seg) {
1924                let name = names[idx as usize].as_str();
1925                self.interp
1926                    .scope
1927                    .set_scalar(name, StrykeValue::integer(buf[idx as usize]))
1928                    .map_err(|e| e.at_line(self.line()))?;
1929            }
1930        }
1931        if let Some(frame) = self.call_stack.pop() {
1932            self.interp.wantarray_kind = frame.saved_wantarray;
1933            self.stack.truncate(frame.stack_base);
1934            self.interp.pop_scope_to_depth(frame.scope_depth);
1935            if frame.jit_trampoline_return {
1936                self.jit_trampoline_out = Some(v);
1937            } else {
1938                self.push(v);
1939                self.ip = frame.return_ip;
1940            }
1941        }
1942        Ok(true)
1943    }
1944
1945    /// Cranelift block JIT for a subroutine with control flow (see [`crate::jit::block_jit_validate_sub`]).
1946    fn try_jit_subroutine_block(&mut self) -> Result<bool, StrykeError> {
1947        let ip = self.ip;
1948        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1949        if self.sub_jit_skip_block_test(ip) {
1950            return Ok(false);
1951        }
1952        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1953        let ops: &Vec<Op> = &self.ops;
1954        let constants: &Vec<StrykeValue> = &self.constants;
1955        let names: &Vec<String> = &self.names;
1956        let Some((full_body, term)) = crate::jit::sub_full_body(ops, ip) else {
1957            return Ok(false);
1958        };
1959        if crate::jit::sub_body_blocks_subroutine_block_jit(full_body) {
1960            self.sub_jit_skip_block_mark(ip);
1961            return Ok(false);
1962        }
1963        let Some(validated) =
1964            crate::jit::block_jit_validate_sub(full_body, constants, term, &self.sub_entries)
1965        else {
1966            self.sub_jit_skip_block_mark(ip);
1967            return Ok(false);
1968        };
1969        let block_buf_mode = validated.buffer_mode();
1970
1971        let mut b_slot_len: Option<usize> = None;
1972        if let Some(max) = crate::jit::block_slot_ops_max_index(full_body) {
1973            let n = max as usize + 1;
1974            self.jit_buf_slot.resize(n, 0);
1975            let mut ok = true;
1976            for i in 0..=max {
1977                let pv = self.interp.scope.get_scalar_slot(i);
1978                self.jit_buf_slot[i as usize] = match block_buf_mode {
1979                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => pv.raw_bits() as i64,
1980                    crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1981                        Some(v) => v,
1982                        None if pv.is_undef()
1983                            && crate::jit::block_slot_undef_prefill_ok(full_body, i) =>
1984                        {
1985                            0
1986                        }
1987                        None => {
1988                            ok = false;
1989                            break;
1990                        }
1991                    },
1992                };
1993            }
1994            if ok {
1995                b_slot_len = Some(n);
1996            }
1997        }
1998
1999        let mut b_plain_len: Option<usize> = None;
2000        if let Some(max) = crate::jit::block_plain_ops_max_index(full_body) {
2001            if (max as usize) < names.len() {
2002                let n = max as usize + 1;
2003                self.jit_buf_plain.resize(n, 0);
2004                let mut ok = true;
2005                for i in 0..=max {
2006                    let nm = names[i as usize].as_str();
2007                    let pv = self.interp.scope.get_scalar(nm);
2008                    self.jit_buf_plain[i as usize] = match block_buf_mode {
2009                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2010                            pv.raw_bits() as i64
2011                        }
2012                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2013                            Some(v) => v,
2014                            None => {
2015                                ok = false;
2016                                break;
2017                            }
2018                        },
2019                    };
2020                }
2021                if ok {
2022                    b_plain_len = Some(n);
2023                }
2024            }
2025        }
2026
2027        let mut b_arg_len: Option<usize> = None;
2028        if let Some(max) = crate::jit::block_arg_ops_max_index(full_body) {
2029            if let Some(frame) = self.call_stack.last() {
2030                let base = frame.stack_base;
2031                let n = max as usize + 1;
2032                self.jit_buf_arg.resize(n, 0);
2033                let mut ok = true;
2034                for i in 0..=max {
2035                    let pos = base + i as usize;
2036                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
2037                    self.jit_buf_arg[i as usize] = match block_buf_mode {
2038                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2039                            pv.raw_bits() as i64
2040                        }
2041                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2042                            Some(v) => v,
2043                            None => {
2044                                ok = false;
2045                                break;
2046                            }
2047                        },
2048                    };
2049                }
2050                if ok {
2051                    b_arg_len = Some(n);
2052                }
2053            }
2054        }
2055
2056        let block_slot_buf = b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
2057        let block_plain_buf = b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
2058        let block_arg_buf = b_arg_len.map(|n| &self.jit_buf_arg[..n]);
2059
2060        let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
2061            full_body,
2062            block_slot_buf,
2063            block_plain_buf,
2064            block_arg_buf,
2065            constants,
2066            Some(validated),
2067            vm_ptr,
2068            &self.sub_entries,
2069        ) else {
2070            self.sub_jit_skip_block_mark(ip);
2071            return Ok(false);
2072        };
2073
2074        if let Some(n) = b_slot_len {
2075            let buf = &self.jit_buf_slot[..n];
2076            for idx in crate::jit::block_slot_ops_written_indices(full_body) {
2077                let bits = buf[idx as usize] as u64;
2078                let pv = match buf_mode {
2079                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2080                        StrykeValue::from_raw_bits(bits)
2081                    }
2082                    crate::jit::BlockJitBufferMode::I64AsInteger => {
2083                        StrykeValue::integer(buf[idx as usize])
2084                    }
2085                };
2086                self.interp.scope.set_scalar_slot(idx, pv);
2087            }
2088        }
2089        if let Some(n) = b_plain_len {
2090            let buf = &self.jit_buf_plain[..n];
2091            for idx in crate::jit::block_plain_ops_written_indices(full_body) {
2092                let name = names[idx as usize].as_str();
2093                let bits = buf[idx as usize] as u64;
2094                let pv = match buf_mode {
2095                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2096                        StrykeValue::from_raw_bits(bits)
2097                    }
2098                    crate::jit::BlockJitBufferMode::I64AsInteger => {
2099                        StrykeValue::integer(buf[idx as usize])
2100                    }
2101                };
2102                self.interp
2103                    .scope
2104                    .set_scalar(name, pv)
2105                    .map_err(|e| e.at_line(self.line()))?;
2106            }
2107        }
2108        if let Some(frame) = self.call_stack.pop() {
2109            self.interp.wantarray_kind = frame.saved_wantarray;
2110            self.stack.truncate(frame.stack_base);
2111            self.interp.pop_scope_to_depth(frame.scope_depth);
2112            if frame.jit_trampoline_return {
2113                self.jit_trampoline_out = Some(v);
2114            } else {
2115                self.push(v);
2116                self.ip = frame.return_ip;
2117            }
2118        }
2119        Ok(true)
2120    }
2121
2122    fn run_method_op(
2123        &mut self,
2124        name_idx: u16,
2125        argc: u8,
2126        wa: u8,
2127        super_call: bool,
2128    ) -> StrykeResult<()> {
2129        let method_owned = self.names[name_idx as usize].clone();
2130        let argc = argc as usize;
2131        let want = WantarrayCtx::from_byte(wa);
2132        let mut args = Vec::with_capacity(argc);
2133        for _ in 0..argc {
2134            args.push(self.pop());
2135        }
2136        args.reverse();
2137        let obj = self.pop();
2138        let method = method_owned.as_str();
2139        if let Some(r) = crate::pchannel::dispatch_method(&obj, method, &args, self.line()) {
2140            self.push(r?);
2141            return Ok(());
2142        }
2143        if let Some(r) = self
2144            .interp
2145            .try_native_method(&obj, method, &args, self.line())
2146        {
2147            self.push(r?);
2148            return Ok(());
2149        }
2150        let class = if let Some(b) = obj.as_blessed_ref() {
2151            b.class.clone()
2152        } else if let Some(s) = obj.as_str() {
2153            s
2154        } else {
2155            return Err(StrykeError::runtime(
2156                "Can't call method on non-object",
2157                self.line(),
2158            ));
2159        };
2160        if method == "VERSION" && !super_call {
2161            if let Some(ver) = self.interp.package_version_scalar(class.as_str())? {
2162                self.push(ver);
2163                return Ok(());
2164            }
2165        }
2166        // UNIVERSAL methods: isa, can, DOES
2167        if !super_call {
2168            match method {
2169                "isa" => {
2170                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
2171                    let mro = self.interp.mro_linearize(&class);
2172                    let result = mro.iter().any(|c| c == &target);
2173                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
2174                    return Ok(());
2175                }
2176                "can" => {
2177                    let target_method = args.first().map(|v| v.to_string()).unwrap_or_default();
2178                    let found = self
2179                        .interp
2180                        .resolve_method_full_name(&class, &target_method, false)
2181                        .and_then(|fq| self.interp.subs.get(&fq))
2182                        .is_some();
2183                    if found {
2184                        self.push(StrykeValue::code_ref(std::sync::Arc::new(
2185                            crate::value::StrykeSub {
2186                                name: target_method,
2187                                params: vec![],
2188                                body: vec![],
2189                                closure_env: None,
2190                                prototype: None,
2191                                fib_like: None,
2192                            },
2193                        )));
2194                    } else {
2195                        self.push(StrykeValue::UNDEF);
2196                    }
2197                    return Ok(());
2198                }
2199                "DOES" => {
2200                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
2201                    let mro = self.interp.mro_linearize(&class);
2202                    let result = mro.iter().any(|c| c == &target);
2203                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
2204                    return Ok(());
2205                }
2206                _ => {}
2207            }
2208        }
2209        let mut all_args = vec![obj];
2210        all_args.extend(args);
2211        let full_name = match self
2212            .interp
2213            .resolve_method_full_name(&class, method, super_call)
2214        {
2215            Some(f) => f,
2216            None => {
2217                return Err(StrykeError::runtime(
2218                    format!(
2219                        "Can't locate method \"{}\" via inheritance (invocant \"{}\")",
2220                        method, class
2221                    ),
2222                    self.line(),
2223                ));
2224            }
2225        };
2226        if let Some(sub) = self.interp.subs.get(&full_name).cloned() {
2227            let saved_wa = self.interp.wantarray_kind;
2228            self.interp.wantarray_kind = want;
2229            self.interp.scope_push_hook();
2230            self.interp.scope.declare_array("_", all_args);
2231            if let Some(ref env) = sub.closure_env {
2232                self.interp.scope.restore_capture(env);
2233            }
2234            let line = self.line();
2235            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
2236            self.interp
2237                .apply_sub_signature(sub.as_ref(), &argv, line)
2238                .map_err(|e| e.at_line(line))?;
2239            self.interp.scope.declare_array("_", argv);
2240            let result = self.interp.exec_block_no_scope(&sub.body);
2241            self.interp.wantarray_kind = saved_wa;
2242            self.interp.scope_pop_hook();
2243            match result {
2244                Ok(v) => self.push(v),
2245                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
2246                    self.push(v)
2247                }
2248                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2249                Err(_) => self.push(StrykeValue::UNDEF),
2250            }
2251        } else if method == "new" && !super_call {
2252            if class == "Set" {
2253                self.push(crate::value::set_from_elements(
2254                    all_args.into_iter().skip(1),
2255                ));
2256            } else if let Some(def) = self.interp.struct_defs.get(&class).cloned() {
2257                let line = self.line();
2258                let mut provided = Vec::new();
2259                let mut i = 1;
2260                while i + 1 < all_args.len() {
2261                    let k = all_args[i].to_string();
2262                    let v = all_args[i + 1].clone();
2263                    provided.push((k, v));
2264                    i += 2;
2265                }
2266                let mut defaults = Vec::with_capacity(def.fields.len());
2267                for field in &def.fields {
2268                    if let Some(ref expr) = field.default {
2269                        let val = self.interp.eval_expr(expr).map_err(|e| match e {
2270                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
2271                            _ => StrykeError::runtime("default evaluation flow", line),
2272                        })?;
2273                        defaults.push(Some(val));
2274                    } else {
2275                        defaults.push(None);
2276                    }
2277                }
2278                let v =
2279                    crate::native_data::struct_new_with_defaults(&def, &provided, &defaults, line)?;
2280                self.push(v);
2281            } else if let Some(def) = self.interp.class_defs.get(&class).cloned() {
2282                // Stryke `class` declarations route through `class_construct`
2283                // so the result is a real `ClassInstance` (typed-my checks,
2284                // isa walk, BUILD hooks). Without this the bytecode path
2285                // fell through to the default Perl-style blessed-hashref
2286                // below, breaking method dispatch for `$self` binding.
2287                // Mirrors the tree-walker fix in `vm_helper::builtin_new`.
2288                // Skip `all_args[0]` (the class-name receiver) since
2289                // `class_construct` expects user args only.
2290                let line = self.line();
2291                let user_args: Vec<StrykeValue> = all_args.into_iter().skip(1).collect();
2292                let v =
2293                    self.interp
2294                        .class_construct(&def, user_args, line)
2295                        .map_err(|e| match e {
2296                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
2297                            _ => StrykeError::runtime("class_construct flow", line),
2298                        })?;
2299                self.push(v);
2300            } else {
2301                let mut map = IndexMap::new();
2302                let mut i = 1;
2303                while i + 1 < all_args.len() {
2304                    map.insert(all_args[i].to_string(), all_args[i + 1].clone());
2305                    i += 2;
2306                }
2307                self.push(StrykeValue::blessed(Arc::new(
2308                    crate::value::BlessedRef::new_blessed(class, StrykeValue::hash(map)),
2309                )));
2310            }
2311        } else if let Some(result) =
2312            self.interp
2313                .try_autoload_call(&full_name, all_args, self.line(), want, Some(&class))
2314        {
2315            match result {
2316                Ok(v) => self.push(v),
2317                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
2318                    self.push(v)
2319                }
2320                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2321                Err(_) => self.push(StrykeValue::UNDEF),
2322            }
2323        } else {
2324            return Err(StrykeError::runtime(
2325                format!(
2326                    "Can't locate method \"{}\" in package \"{}\"",
2327                    method, class
2328                ),
2329                self.line(),
2330            ));
2331        }
2332        Ok(())
2333    }
2334
2335    fn run_fan_block(
2336        &mut self,
2337        block_idx: u16,
2338        n: usize,
2339        line: usize,
2340        progress: bool,
2341    ) -> StrykeResult<()> {
2342        let block = self.blocks[block_idx as usize].clone();
2343        let subs = self.interp.subs.clone();
2344        let (scope_capture, atomic_arrays, atomic_hashes) =
2345            self.interp.scope.capture_with_atomics();
2346        // Worker bodies execute via the tree walker (`exec_block_no_scope`) which uses
2347        // `tree_scalar_storage_name` to rewrite `$x` → `Pkg::x`. That helper consults
2348        // `english_lexical_scalars` + `our_lexical_scalars` — empty in a fresh worker —
2349        // so without copying the parent's sets, `our` / `oursync` reads see UNDEF.
2350        let lex_scalars = self.interp.english_lexical_scalars_clone();
2351        let our_scalars = self.interp.our_lexical_scalars_clone();
2352        let fan_progress = FanProgress::new(progress, n);
2353        let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
2354        (0..n).into_par_iter().for_each(|i| {
2355            if first_err.lock().is_some() {
2356                return;
2357            }
2358            fan_progress.start_worker(i);
2359            let mut local_interp = VMHelper::new();
2360            local_interp.subs = subs.clone();
2361            local_interp.suppress_stdout = progress;
2362            local_interp.scope.restore_capture(&scope_capture);
2363            local_interp
2364                .scope
2365                .restore_atomics(&atomic_arrays, &atomic_hashes);
2366            local_interp.set_english_lexical_scalars(lex_scalars.clone());
2367            local_interp.set_our_lexical_scalars(our_scalars.clone());
2368            local_interp.enable_parallel_guard();
2369            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
2370            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
2371            local_interp.scope_push_hook();
2372            match local_interp.exec_block_no_scope(&block) {
2373                Ok(_) => {}
2374                Err(e) => {
2375                    let stryke = match e {
2376                        FlowOrError::Error(stryke) => stryke,
2377                        FlowOrError::Flow(_) => StrykeError::runtime(
2378                            "return/last/next/redo not supported inside fan block",
2379                            line,
2380                        ),
2381                    };
2382                    let mut g = first_err.lock();
2383                    if g.is_none() {
2384                        *g = Some(stryke);
2385                    }
2386                }
2387            }
2388            local_interp.scope_pop_hook();
2389            crate::parallel_trace::fan_worker_set_index(None);
2390            fan_progress.finish_worker(i);
2391        });
2392        fan_progress.finish();
2393        if let Some(e) = first_err.lock().take() {
2394            return Err(e);
2395        }
2396        self.push(StrykeValue::UNDEF);
2397        Ok(())
2398    }
2399
2400    fn run_fan_cap_block(
2401        &mut self,
2402        block_idx: u16,
2403        n: usize,
2404        line: usize,
2405        progress: bool,
2406    ) -> StrykeResult<()> {
2407        let block = self.blocks[block_idx as usize].clone();
2408        let subs = self.interp.subs.clone();
2409        let (scope_capture, atomic_arrays, atomic_hashes) =
2410            self.interp.scope.capture_with_atomics();
2411        // See run_fan_block for why we copy lexical-scalar tracking sets.
2412        let lex_scalars = self.interp.english_lexical_scalars_clone();
2413        let our_scalars = self.interp.our_lexical_scalars_clone();
2414        let fan_progress = FanProgress::new(progress, n);
2415        let pairs: Vec<(usize, Result<StrykeValue, FlowOrError>)> = (0..n)
2416            .into_par_iter()
2417            .map(|i| {
2418                fan_progress.start_worker(i);
2419                let mut local_interp = VMHelper::new();
2420                local_interp.subs = subs.clone();
2421                local_interp.suppress_stdout = progress;
2422                local_interp.scope.restore_capture(&scope_capture);
2423                local_interp
2424                    .scope
2425                    .restore_atomics(&atomic_arrays, &atomic_hashes);
2426                local_interp.set_english_lexical_scalars(lex_scalars.clone());
2427                local_interp.set_our_lexical_scalars(our_scalars.clone());
2428                local_interp.enable_parallel_guard();
2429                local_interp.scope.set_topic(StrykeValue::integer(i as i64));
2430                crate::parallel_trace::fan_worker_set_index(Some(i as i64));
2431                local_interp.scope_push_hook();
2432                let res = local_interp.exec_block_no_scope(&block);
2433                local_interp.scope_pop_hook();
2434                crate::parallel_trace::fan_worker_set_index(None);
2435                fan_progress.finish_worker(i);
2436                (i, res)
2437            })
2438            .collect();
2439        fan_progress.finish();
2440        let mut pairs = pairs;
2441        pairs.sort_by_key(|(i, _)| *i);
2442        let mut out = Vec::with_capacity(n);
2443        for (_, r) in pairs {
2444            match r {
2445                Ok(v) => out.push(v),
2446                Err(e) => {
2447                    let stryke = match e {
2448                        FlowOrError::Error(stryke) => stryke,
2449                        FlowOrError::Flow(_) => StrykeError::runtime(
2450                            "return/last/next/redo not supported inside fan_cap block",
2451                            line,
2452                        ),
2453                    };
2454                    return Err(stryke);
2455                }
2456            }
2457        }
2458        self.push(StrykeValue::array(out));
2459        Ok(())
2460    }
2461
2462    fn require_scalar_mutable(&self, name: &str) -> StrykeResult<()> {
2463        if self.interp.scope.is_scalar_frozen(name) {
2464            return Err(StrykeError::syntax(
2465                format!("cannot assign to frozen variable `${}`", name),
2466                self.line(),
2467            ));
2468        }
2469        Ok(())
2470    }
2471
2472    fn require_array_mutable(&self, name: &str) -> StrykeResult<()> {
2473        if self.interp.scope.is_array_frozen(name) {
2474            return Err(StrykeError::syntax(
2475                format!("cannot modify frozen array `@{}`", name),
2476                self.line(),
2477            ));
2478        }
2479        Ok(())
2480    }
2481
2482    fn require_hash_mutable(&self, name: &str) -> StrykeResult<()> {
2483        if self.interp.scope.is_hash_frozen(name) || Self::is_reflection_hash(name) {
2484            return Err(StrykeError::syntax(
2485                format!("cannot modify frozen hash `%{}`", name),
2486                self.line(),
2487            ));
2488        }
2489        Ok(())
2490    }
2491
2492    /// Reflection hashes are frozen builtins even before lazy init.
2493    fn is_reflection_hash(name: &str) -> bool {
2494        matches!(name, "b" | "pc" | "e" | "a" | "d" | "c" | "p" | "all")
2495            || name.starts_with("stryke::")
2496    }
2497
2498    /// Run bytecode: first attempts Cranelift method JIT for eligible numeric fragments (unless
2499    /// [`VM::set_jit_enabled`] disabled it). For block JIT, `block_jit_validate` runs once per attempt;
2500    /// buffers may use `StrykeValue::raw_bits` for `defined`-style control flow. Then the main opcode
2501    /// interpreter loop.
2502    pub fn execute(&mut self) -> StrykeResult<StrykeValue> {
2503        let ops_ref: &Vec<Op> = &self.ops;
2504        let ops = ops_ref as *const Vec<Op>;
2505        // SAFETY: ops doesn't change during execution; pointer avoids borrow on self
2506        let ops = unsafe { &*ops };
2507        let names_ref: &Vec<String> = &self.names;
2508        let names = names_ref as *const Vec<String>;
2509        // SAFETY: names doesn't change during execution; pointer avoids borrow on self
2510        let names = unsafe { &*names };
2511        let constants_ref: &Vec<StrykeValue> = &self.constants;
2512        let constants = constants_ref as *const Vec<StrykeValue>;
2513        // SAFETY: constants doesn't change during execution; pointer avoids borrow on self
2514        let constants = unsafe { &*constants };
2515        let mut last = StrykeValue::UNDEF;
2516        // Safety limit: [`run_main_dispatch_loop`] counts ops (1B cap).
2517        let mut op_count: u64 = 0;
2518
2519        // Match Perl signal delivery: deliver `%SIG` and set `$^C` latch (Unix).
2520        crate::perl_signal::poll(self.interp)?;
2521        if self.jit_enabled {
2522            let mut top_slot_len: Option<usize> = None;
2523            if let Some(max) = crate::jit::linear_slot_ops_max_index(ops) {
2524                let n = max as usize + 1;
2525                self.jit_buf_slot.resize(n, 0);
2526                let mut ok = true;
2527                for i in 0..=max {
2528                    let pv = self.interp.scope.get_scalar_slot(i);
2529                    self.jit_buf_slot[i as usize] = match pv.as_integer() {
2530                        Some(v) => v,
2531                        None if pv.is_undef() && crate::jit::slot_undef_prefill_ok(ops, i) => 0,
2532                        None => {
2533                            ok = false;
2534                            break;
2535                        }
2536                    };
2537                }
2538                if ok {
2539                    top_slot_len = Some(n);
2540                }
2541            }
2542
2543            let mut top_plain_len: Option<usize> = None;
2544            if let Some(max) = crate::jit::linear_plain_ops_max_index(ops) {
2545                if (max as usize) < names.len() {
2546                    let n = max as usize + 1;
2547                    self.jit_buf_plain.resize(n, 0);
2548                    let mut ok = true;
2549                    for i in 0..=max {
2550                        let nm = names[i as usize].as_str();
2551                        match self.interp.scope.get_scalar(nm).as_integer() {
2552                            Some(v) => self.jit_buf_plain[i as usize] = v,
2553                            None => {
2554                                ok = false;
2555                                break;
2556                            }
2557                        }
2558                    }
2559                    if ok {
2560                        top_plain_len = Some(n);
2561                    }
2562                }
2563            }
2564
2565            let mut top_arg_len: Option<usize> = None;
2566            if let Some(max) = crate::jit::linear_arg_ops_max_index(ops) {
2567                if let Some(frame) = self.call_stack.last() {
2568                    let base = frame.stack_base;
2569                    let n = max as usize + 1;
2570                    self.jit_buf_arg.resize(n, 0);
2571                    let mut ok = true;
2572                    for i in 0..=max {
2573                        let pos = base + i as usize;
2574                        let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
2575                        match pv.as_integer() {
2576                            Some(v) => self.jit_buf_arg[i as usize] = v,
2577                            None => {
2578                                ok = false;
2579                                break;
2580                            }
2581                        }
2582                    }
2583                    if ok {
2584                        top_arg_len = Some(n);
2585                    }
2586                }
2587            }
2588
2589            let slot_buf = top_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
2590            let plain_buf = top_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
2591            let arg_buf = top_arg_len.map(|n| &self.jit_buf_arg[..n]);
2592
2593            if let Some(v) =
2594                crate::jit::try_run_linear_ops(ops, slot_buf, plain_buf, arg_buf, constants)
2595            {
2596                if let Some(n) = top_slot_len {
2597                    let buf = &self.jit_buf_slot[..n];
2598                    for idx in crate::jit::linear_slot_ops_written_indices(ops) {
2599                        self.interp
2600                            .scope
2601                            .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
2602                    }
2603                }
2604                if let Some(n) = top_plain_len {
2605                    let buf = &self.jit_buf_plain[..n];
2606                    for idx in crate::jit::linear_plain_ops_written_indices(ops) {
2607                        let name = names[idx as usize].as_str();
2608                        self.interp
2609                            .scope
2610                            .set_scalar(name, StrykeValue::integer(buf[idx as usize]))?;
2611                    }
2612                }
2613                return Ok(v);
2614            }
2615
2616            // ── Block JIT: try to compile sequences with control flow (loops, conditionals). ──
2617            if let Some(validated) =
2618                crate::jit::block_jit_validate(ops, constants, &self.sub_entries)
2619            {
2620                let block_buf_mode = validated.buffer_mode();
2621
2622                let mut top_b_slot_len: Option<usize> = None;
2623                if let Some(max) = crate::jit::block_slot_ops_max_index(ops) {
2624                    let n = max as usize + 1;
2625                    self.jit_buf_slot.resize(n, 0);
2626                    let mut ok = true;
2627                    for i in 0..=max {
2628                        let pv = self.interp.scope.get_scalar_slot(i);
2629                        self.jit_buf_slot[i as usize] = match block_buf_mode {
2630                            crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2631                                pv.raw_bits() as i64
2632                            }
2633                            crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2634                                Some(v) => v,
2635                                None if pv.is_undef()
2636                                    && crate::jit::block_slot_undef_prefill_ok(ops, i) =>
2637                                {
2638                                    0
2639                                }
2640                                None => {
2641                                    ok = false;
2642                                    break;
2643                                }
2644                            },
2645                        };
2646                    }
2647                    if ok {
2648                        top_b_slot_len = Some(n);
2649                    }
2650                }
2651
2652                let mut top_b_plain_len: Option<usize> = None;
2653                if let Some(max) = crate::jit::block_plain_ops_max_index(ops) {
2654                    if (max as usize) < names.len() {
2655                        let n = max as usize + 1;
2656                        self.jit_buf_plain.resize(n, 0);
2657                        let mut ok = true;
2658                        for i in 0..=max {
2659                            let nm = names[i as usize].as_str();
2660                            let pv = self.interp.scope.get_scalar(nm);
2661                            self.jit_buf_plain[i as usize] = match block_buf_mode {
2662                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2663                                    pv.raw_bits() as i64
2664                                }
2665                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2666                                    match pv.as_integer() {
2667                                        Some(v) => v,
2668                                        None => {
2669                                            ok = false;
2670                                            break;
2671                                        }
2672                                    }
2673                                }
2674                            };
2675                        }
2676                        if ok {
2677                            top_b_plain_len = Some(n);
2678                        }
2679                    }
2680                }
2681
2682                let mut top_b_arg_len: Option<usize> = None;
2683                if let Some(max) = crate::jit::block_arg_ops_max_index(ops) {
2684                    if let Some(frame) = self.call_stack.last() {
2685                        let base = frame.stack_base;
2686                        let n = max as usize + 1;
2687                        self.jit_buf_arg.resize(n, 0);
2688                        let mut ok = true;
2689                        for i in 0..=max {
2690                            let pos = base + i as usize;
2691                            let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
2692                            self.jit_buf_arg[i as usize] = match block_buf_mode {
2693                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2694                                    pv.raw_bits() as i64
2695                                }
2696                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2697                                    match pv.as_integer() {
2698                                        Some(v) => v,
2699                                        None => {
2700                                            ok = false;
2701                                            break;
2702                                        }
2703                                    }
2704                                }
2705                            };
2706                        }
2707                        if ok {
2708                            top_b_arg_len = Some(n);
2709                        }
2710                    }
2711                }
2712
2713                let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
2714                let block_slot_buf = top_b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
2715                let block_plain_buf = top_b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
2716                let block_arg_buf = top_b_arg_len.map(|n| &self.jit_buf_arg[..n]);
2717
2718                if let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
2719                    ops,
2720                    block_slot_buf,
2721                    block_plain_buf,
2722                    block_arg_buf,
2723                    constants,
2724                    Some(validated),
2725                    vm_ptr,
2726                    &self.sub_entries,
2727                ) {
2728                    if let Some(n) = top_b_slot_len {
2729                        let buf = &self.jit_buf_slot[..n];
2730                        for idx in crate::jit::block_slot_ops_written_indices(ops) {
2731                            let bits = buf[idx as usize] as u64;
2732                            let pv = match buf_mode {
2733                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2734                                    StrykeValue::from_raw_bits(bits)
2735                                }
2736                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2737                                    StrykeValue::integer(buf[idx as usize])
2738                                }
2739                            };
2740                            self.interp.scope.set_scalar_slot(idx, pv);
2741                        }
2742                    }
2743                    if let Some(n) = top_b_plain_len {
2744                        let buf = &self.jit_buf_plain[..n];
2745                        for idx in crate::jit::block_plain_ops_written_indices(ops) {
2746                            let name = names[idx as usize].as_str();
2747                            let bits = buf[idx as usize] as u64;
2748                            let pv = match buf_mode {
2749                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2750                                    StrykeValue::from_raw_bits(bits)
2751                                }
2752                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2753                                    StrykeValue::integer(buf[idx as usize])
2754                                }
2755                            };
2756                            self.interp.scope.set_scalar(name, pv)?;
2757                        }
2758                    }
2759                    return Ok(v);
2760                }
2761            }
2762        }
2763
2764        last = self.run_main_dispatch_loop(last, &mut op_count, true)?;
2765
2766        Ok(last)
2767    }
2768
2769    /// `die` / runtime errors inside `try` jump to `catch_ip` unless the error is [`ErrorKind::Exit`].
2770    ///
2771    /// Walks the try stack top-down looking for a frame still in `Trying` state. Frames in
2772    /// `Catching` / `Finalizing` are skipped (and possibly popped) so that re-raising from
2773    /// inside `catch` or `finally` propagates outward instead of re-entering the same handler.
2774    /// If a `Catching` frame has a `finally`, that finally still runs (with the new error
2775    /// deferred) before the propagation continues.
2776    fn try_recover_from_exception(&mut self, e: &StrykeError) -> StrykeResult<bool> {
2777        if matches!(e.kind, ErrorKind::Exit(_)) {
2778            return Ok(false);
2779        }
2780        loop {
2781            let Some(frame) = self.try_stack.last() else {
2782                return Ok(false);
2783            };
2784            let op_idx = frame.try_push_op_idx;
2785            let Op::TryPush {
2786                catch_ip,
2787                finally_ip,
2788                ..
2789            } = &self.ops[op_idx]
2790            else {
2791                return Ok(false);
2792            };
2793            let catch_ip = *catch_ip;
2794            let finally_ip = *finally_ip;
2795            match frame.state {
2796                TryState::Trying => {
2797                    let val = e
2798                        .die_value
2799                        .clone()
2800                        .unwrap_or_else(|| StrykeValue::string(e.to_string()));
2801                    self.pending_catch_error = Some(val);
2802                    if let Some(top) = self.try_stack.last_mut() {
2803                        top.state = TryState::Catching;
2804                    }
2805                    self.ip = catch_ip;
2806                    return Ok(true);
2807                }
2808                TryState::Catching => {
2809                    if let Some(fin_ip) = finally_ip {
2810                        if let Some(top) = self.try_stack.last_mut() {
2811                            top.state = TryState::Finalizing;
2812                            top.deferred_error = Some(e.clone());
2813                        }
2814                        self.ip = fin_ip;
2815                        return Ok(true);
2816                    }
2817                    self.try_stack.pop();
2818                }
2819                TryState::Finalizing => {
2820                    // Finally itself threw — drop deferred (if any) and keep propagating.
2821                    self.try_stack.pop();
2822                }
2823            }
2824        }
2825    }
2826
2827    /// Stash lookup only (qualified key from compiler); avoids `resolve_sub_by_name`'s package fallback on hot calls.
2828    #[inline]
2829    fn sub_for_closure_restore(&self, name: &str) -> Option<Arc<StrykeSub>> {
2830        self.interp.subs.get(name).cloned()
2831    }
2832
2833    /// AOP: run before-advice → original (or around) → after-advice for `name`.
2834    /// Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). Args are popped synchronously
2835    /// off the stack; the original is invoked via `Interpreter::call_sub` so the retval is
2836    /// available to after-advice and to the around block (via `proceed`).
2837    #[cold]
2838    fn dispatch_with_advice(
2839        &mut self,
2840        name: &str,
2841        closure_sub_hint: Option<Arc<StrykeSub>>,
2842        argc: usize,
2843        want: WantarrayCtx,
2844        preserve_arrays: bool,
2845    ) -> StrykeResult<()> {
2846        use crate::ast::AdviceKind;
2847
2848        let line = self.line();
2849
2850        let args = if preserve_arrays {
2851            self.pop_call_operands_preserved(argc)
2852        } else {
2853            self.pop_call_operands_flattened(argc)
2854        };
2855
2856        let sub_opt = closure_sub_hint.or_else(|| self.interp.resolve_sub_by_name(name));
2857
2858        let matching: Vec<crate::aop::Intercept> = self
2859            .interp
2860            .intercepts
2861            .iter()
2862            .filter(|i| crate::aop::glob_match(&i.pattern, name))
2863            .cloned()
2864            .collect();
2865
2866        // Context vars visible to advice bodies (mirrors zshrs INTERCEPT_NAME / INTERCEPT_ARGS).
2867        self.interp
2868            .scope
2869            .declare_scalar("INTERCEPT_NAME", StrykeValue::string(name.to_string()));
2870        self.interp
2871            .scope
2872            .declare_array("INTERCEPT_ARGS", args.clone());
2873
2874        self.interp.intercept_active_names.push(name.to_string());
2875
2876        // Run all matching `before` advices via the bytecode VM (`run_block_region`).
2877        // We never fall back to `interp.exec_block` here — the advice body must use the
2878        // same name resolution as the surrounding bytecode (see the source-level test
2879        // in `tests/tree_walker_absent_aop.rs`).
2880        for adv in matching
2881            .iter()
2882            .filter(|i| matches!(i.kind, AdviceKind::Before))
2883        {
2884            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2885                self.interp.intercept_active_names.pop();
2886                return Err(e);
2887            }
2888        }
2889
2890        let around = matching
2891            .iter()
2892            .find(|i| matches!(i.kind, AdviceKind::Around));
2893
2894        let t0 = std::time::Instant::now();
2895        let retval = if let Some(around) = around {
2896            self.interp
2897                .intercept_ctx_stack
2898                .push(crate::aop::InterceptCtx {
2899                    name: name.to_string(),
2900                    args: args.clone(),
2901                    proceeded: false,
2902                    retval: StrykeValue::UNDEF,
2903                });
2904            let exec_res = self.run_advice_body_bytecode(around, line);
2905            let _ctx = self.interp.intercept_ctx_stack.pop();
2906            // AspectJ-style: the around block's evaluated value is the call's return.
2907            // If the user wants to forward the original's value, they say `proceed()`
2908            // as the last expression; if they want to transform, `proceed() + 1`; if
2909            // they want to replace, just emit a value without calling proceed.
2910            match exec_res {
2911                Ok(v) => v,
2912                Err(e) => {
2913                    self.interp.intercept_active_names.pop();
2914                    return Err(e);
2915                }
2916            }
2917        } else if let Some(sub) = sub_opt {
2918            match self.interp.call_sub(&sub, args.clone(), want, line) {
2919                Ok(v) => v,
2920                Err(FlowOrError::Flow(Flow::Return(v))) => v,
2921                Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2922                Err(FlowOrError::Error(e)) => {
2923                    self.interp.intercept_active_names.pop();
2924                    return Err(e.at_line(line));
2925                }
2926            }
2927        } else {
2928            // Sub not resolvable — fall back to builtins (matches the non-advice fallback).
2929            let saved_wa_call = self.interp.wantarray_kind;
2930            self.interp.wantarray_kind = want;
2931            let r = crate::builtins::try_builtin(self.interp, name, &args, line);
2932            self.interp.wantarray_kind = saved_wa_call;
2933            match r {
2934                Some(Ok(v)) => v,
2935                Some(Err(e)) => {
2936                    self.interp.intercept_active_names.pop();
2937                    return Err(e.at_line(line));
2938                }
2939                None => {
2940                    self.interp.intercept_active_names.pop();
2941                    return Err(StrykeError::runtime(
2942                        format!("undefined sub `{}` (advice fallback)", name),
2943                        line,
2944                    ));
2945                }
2946            }
2947        };
2948        let elapsed = t0.elapsed();
2949
2950        // Timing context vars for after-advice (matches zshrs INTERCEPT_MS / INTERCEPT_US).
2951        self.interp.scope.declare_scalar(
2952            "INTERCEPT_MS",
2953            StrykeValue::float(elapsed.as_secs_f64() * 1000.0),
2954        );
2955        self.interp.scope.declare_scalar(
2956            "INTERCEPT_US",
2957            StrykeValue::integer(elapsed.as_micros() as i64),
2958        );
2959        self.interp
2960            .scope
2961            .declare_scalar("INTERCEPT_RESULT", retval.clone());
2962
2963        for adv in matching
2964            .iter()
2965            .filter(|i| matches!(i.kind, AdviceKind::After))
2966        {
2967            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2968                self.interp.intercept_active_names.pop();
2969                return Err(e);
2970            }
2971        }
2972
2973        self.interp.intercept_active_names.pop();
2974        self.push(retval);
2975        Ok(())
2976    }
2977
2978    /// Dispatch one advice body through the VM bytecode helper (`run_block_region`),
2979    /// the same path used by `map { }` / `grep { }` blocks. Always returns the body's
2980    /// final value on success. The body is required to have a lowered bytecode region
2981    /// (`Chunk::block_bytecode_ranges[idx]`) — the compiler's fourth pass populates
2982    /// this for every chunk block, so the only reason it would be missing is if the
2983    /// body contains a construct the lowering rejects (e.g. a literal `return`); in
2984    /// that case we error out loudly rather than silently fall back to the
2985    /// tree-walker. See `tests/tree_walker_absent_aop.rs`.
2986    #[inline]
2987    fn run_advice_body_bytecode(
2988        &mut self,
2989        adv: &crate::aop::Intercept,
2990        line: usize,
2991    ) -> StrykeResult<StrykeValue> {
2992        let idx = adv.body_block_idx as usize;
2993        let range = self
2994            .block_bytecode_ranges
2995            .get(idx)
2996            .copied()
2997            .flatten()
2998            .ok_or_else(|| {
2999                StrykeError::runtime(
3000                    format!(
3001                        "AOP {} advice body for `{}` could not be lowered to bytecode \
3002                         (likely contains a construct unsupported by block lowering, \
3003                         e.g. a literal `return`); rewrite the body without it",
3004                        match adv.kind {
3005                            crate::ast::AdviceKind::Before => "before",
3006                            crate::ast::AdviceKind::After => "after",
3007                            crate::ast::AdviceKind::Around => "around",
3008                        },
3009                        adv.pattern,
3010                    ),
3011                    line,
3012                )
3013            })?;
3014        let mut op_count: u64 = 0;
3015        self.run_block_region(range.0, range.1, &mut op_count)
3016    }
3017
3018    fn vm_dispatch_user_call(
3019        &mut self,
3020        name_idx: u16,
3021        entry_opt: Option<(usize, bool)>,
3022        argc_u8: u8,
3023        wa_byte: u8,
3024        // Pre-resolved sub for `Op::CallStaticSubId` (stash lookup once in `VM::new`).
3025        closure_sub_hint: Option<Arc<StrykeSub>>,
3026    ) -> StrykeResult<()> {
3027        let name_owned = self.names[name_idx as usize].clone();
3028        let name = name_owned.as_str();
3029        let argc = argc_u8 as usize;
3030        let want = WantarrayCtx::from_byte(wa_byte);
3031
3032        // AOP advice path: at least one matching intercept and no re-entrancy guard for `name`.
3033        // Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). The fast-path skip below is the
3034        // common case (no intercepts registered); when the registry is non-empty we still bail
3035        // out cheaply unless a glob actually matches.
3036        if !self.interp.intercepts.is_empty()
3037            && !self.interp.intercept_active_names.iter().any(|n| n == name)
3038            && self
3039                .interp
3040                .intercepts
3041                .iter()
3042                .any(|i| crate::aop::glob_match(&i.pattern, name))
3043        {
3044            let preserve = Self::call_preserve_operand_arrays(name);
3045            return self.dispatch_with_advice(&name_owned, closure_sub_hint, argc, want, preserve);
3046        }
3047
3048        if let Some((entry_ip, stack_args)) = entry_opt {
3049            let saved_wa = self.interp.wantarray_kind;
3050            let sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3051            if let Some(p) = &mut self.interp.profiler {
3052                p.enter_sub(name);
3053            }
3054            self.interp.debugger_enter_sub(name);
3055
3056            // Fib-shaped recursive-add fast path: if the target sub is tagged with a
3057            // `fib_like` pattern (detected at sub-registration time in the compiler and
3058            // cached in `static_sub_closure_subs`), skip frame setup entirely and
3059            // evaluate the closed-form-ish iterative version. `bench_fib` collapses from
3060            // ~2.7M recursive VM calls to a single `while` loop.
3061            let fib_sub: Option<Arc<StrykeSub>> = closure_sub_hint
3062                .clone()
3063                .or_else(|| self.sub_for_closure_restore(name));
3064            if let Some(ref sub_arc) = fib_sub {
3065                if let Some(pat) = sub_arc.fib_like.as_ref() {
3066                    // stack_args path pushes exactly `argc` ints; non-stack_args pops them
3067                    // off the stack into @_. Only the argc==1 / integer case qualifies.
3068                    if argc == 1 {
3069                        let top_idx = self.stack.len().saturating_sub(1);
3070                        if let Some(n0) = self.stack.get(top_idx).and_then(|v| v.as_integer()) {
3071                            let result = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
3072                            // Drop the arg, push the result, keep wantarray as the caller had it.
3073                            self.stack.truncate(top_idx);
3074                            self.push(StrykeValue::integer(result));
3075                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, sub_prof_t0) {
3076                                p.exit_sub(t0.elapsed());
3077                            }
3078                            self.interp.debugger_leave_sub();
3079                            self.interp.wantarray_kind = saved_wa;
3080                            return Ok(());
3081                        }
3082                    }
3083                }
3084            }
3085
3086            if stack_args {
3087                let eff_argc = if argc == 0 {
3088                    self.push(self.interp.scope.get_scalar("_").clone());
3089                    1
3090                } else {
3091                    argc
3092                };
3093                let stack_base = self.stack.len() - eff_argc;
3094                self.call_stack.push(CallFrame {
3095                    return_ip: self.ip,
3096                    stack_base,
3097                    scope_depth: self.interp.scope.depth(),
3098                    saved_wantarray: saved_wa,
3099                    jit_trampoline_return: false,
3100                    block_region: false,
3101                    sub_profiler_start: sub_prof_t0,
3102                });
3103                self.interp.wantarray_kind = want;
3104                self.interp.scope_push_hook();
3105                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
3106                if let Some(ref sub) = closure_sub {
3107                    if let Some(ref env) = sub.closure_env {
3108                        self.interp.scope.restore_capture(env);
3109                    }
3110                    self.interp.current_sub_stack.push(sub.clone());
3111                }
3112                self.ip = entry_ip;
3113            } else {
3114                let args = if Self::call_preserve_operand_arrays(name) {
3115                    self.pop_call_operands_preserved(argc)
3116                } else {
3117                    self.pop_call_operands_flattened(argc)
3118                };
3119                // Only substitute $_ when the call site has no syntactic arguments (argc == 0).
3120                // When argc > 0 but args is empty (e.g., passing an empty array), keep args empty.
3121                let args = if argc == 0 {
3122                    self.interp.with_topic_default_args(args)
3123                } else {
3124                    args
3125                };
3126                self.call_stack.push(CallFrame {
3127                    return_ip: self.ip,
3128                    stack_base: self.stack.len(),
3129                    scope_depth: self.interp.scope.depth(),
3130                    saved_wantarray: saved_wa,
3131                    jit_trampoline_return: false,
3132                    block_region: false,
3133                    sub_profiler_start: sub_prof_t0,
3134                });
3135                self.interp.wantarray_kind = want;
3136                self.interp.scope_push_hook();
3137                self.interp.scope.declare_array("_", args);
3138                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
3139                if let Some(ref sub) = closure_sub {
3140                    if let Some(ref env) = sub.closure_env {
3141                        self.interp.scope.restore_capture(env);
3142                    }
3143                    let line = self.line();
3144                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
3145                    self.interp
3146                        .apply_sub_signature(sub.as_ref(), &argv, line)
3147                        .map_err(|e| e.at_line(line))?;
3148                    self.interp.scope.declare_array("_", argv.clone());
3149                    self.interp.scope.set_closure_args(&argv);
3150                    self.interp.current_sub_stack.push(sub.clone());
3151                }
3152                self.ip = entry_ip;
3153            }
3154        } else {
3155            let args = if Self::call_preserve_operand_arrays(name) {
3156                self.pop_call_operands_preserved(argc)
3157            } else {
3158                self.pop_call_operands_flattened(argc)
3159            };
3160
3161            let saved_wa_call = self.interp.wantarray_kind;
3162            self.interp.wantarray_kind = want;
3163            // Bare callable spelling: builtins always win in default mode.
3164            // Skip the user-sub resolve below so `fn sum {}` declared in a
3165            // non-main package never shadows the global `sum` on a bare
3166            // call. `--compat` (Perl 5 mode) restores UDF-wins semantics.
3167            let is_bare_builtin = !crate::compat_mode()
3168                && !name.contains("::")
3169                && crate::builtins::is_callable_spelling(name);
3170            if let Some(r) = crate::builtins::try_builtin(self.interp, name, &args, self.line()) {
3171                self.interp.wantarray_kind = saved_wa_call;
3172                self.push(r?);
3173            } else {
3174                self.interp.wantarray_kind = saved_wa_call;
3175                let maybe_sub = if is_bare_builtin {
3176                    None
3177                } else {
3178                    self.interp.resolve_sub_by_name(name)
3179                };
3180                if let Some(sub) = maybe_sub {
3181                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3182                    if let Some(p) = &mut self.interp.profiler {
3183                        p.enter_sub(name);
3184                    }
3185                    self.interp.debugger_enter_sub(name);
3186                    // Only substitute $_ when argc == 0; passing an empty array keeps args empty.
3187                    let args = if argc == 0 {
3188                        self.interp.with_topic_default_args(args)
3189                    } else {
3190                        args
3191                    };
3192                    let saved_wa = self.interp.wantarray_kind;
3193                    self.interp.wantarray_kind = want;
3194                    self.interp.scope_push_hook();
3195                    self.interp.scope.declare_array("_", args);
3196                    if let Some(ref env) = sub.closure_env {
3197                        self.interp.scope.restore_capture(env);
3198                    }
3199                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
3200                    let line = self.line();
3201                    self.interp
3202                        .apply_sub_signature(&sub, &argv, line)
3203                        .map_err(|e| e.at_line(line))?;
3204                    let result = {
3205                        self.interp.scope.declare_array("_", argv.clone());
3206                        self.interp.scope.set_closure_args(&argv);
3207                        self.interp
3208                            .exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List)
3209                    };
3210                    self.interp.wantarray_kind = saved_wa;
3211                    self.interp.scope_pop_hook();
3212                    match result {
3213                        Ok(v) => self.push(v),
3214                        Err(crate::vm_helper::FlowOrError::Flow(
3215                            crate::vm_helper::Flow::Return(v),
3216                        )) => self.push(v),
3217                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3218                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3219                                p.exit_sub(t0.elapsed());
3220                            }
3221                            self.interp.debugger_leave_sub();
3222                            return Err(e);
3223                        }
3224                        Err(_) => self.push(StrykeValue::UNDEF),
3225                    }
3226                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3227                        p.exit_sub(t0.elapsed());
3228                    }
3229                    self.interp.debugger_leave_sub();
3230                } else if !name.contains("::")
3231                    && matches!(
3232                        name,
3233                        "uniq"
3234                            | "distinct"
3235                            | "uniqstr"
3236                            | "uniqint"
3237                            | "uniqnum"
3238                            | "shuffle"
3239                            | "sample"
3240                            | "chunked"
3241                            | "windowed"
3242                            | "zip"
3243                            | "zip_shortest"
3244                            | "zip_longest"
3245                            | "mesh"
3246                            | "mesh_shortest"
3247                            | "mesh_longest"
3248                            | "any"
3249                            | "all"
3250                            | "none"
3251                            | "notall"
3252                            | "first"
3253                            | "find_index"
3254                            | "firstidx"
3255                            | "first_index"
3256                            | "reduce"
3257                            | "reductions"
3258                            | "sum"
3259                            | "sum0"
3260                            | "product"
3261                            | "min"
3262                            | "max"
3263                            | "minstr"
3264                            | "maxstr"
3265                            | "mean"
3266                            | "median"
3267                            | "mode"
3268                            | "stddev"
3269                            | "variance"
3270                            | "pairs"
3271                            | "unpairs"
3272                            | "pairkeys"
3273                            | "pairvalues"
3274                            | "pairgrep"
3275                            | "pairmap"
3276                            | "pairfirst"
3277                            // Scalar/Sub/utf8-utility bare builtins (no module — direct names)
3278                            | "blessed"
3279                            | "refaddr"
3280                            | "reftype"
3281                            | "looks_like_number"
3282                            | "weaken"
3283                            | "unweaken"
3284                            | "isweak"
3285                            | "set_subname"
3286                            | "subname"
3287                            | "unicode_to_native"
3288                    )
3289                {
3290                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3291                    if let Some(p) = &mut self.interp.profiler {
3292                        p.enter_sub(name);
3293                    }
3294                    self.interp.debugger_enter_sub(name);
3295                    let saved_wa = self.interp.wantarray_kind;
3296                    self.interp.wantarray_kind = want;
3297                    let out = self
3298                        .interp
3299                        .call_bare_list_builtin(name, args, self.line(), want);
3300                    self.interp.wantarray_kind = saved_wa;
3301                    match out {
3302                        Ok(v) => self.push(v),
3303                        Err(crate::vm_helper::FlowOrError::Flow(
3304                            crate::vm_helper::Flow::Return(v),
3305                        )) => self.push(v),
3306                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3307                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3308                                p.exit_sub(t0.elapsed());
3309                            }
3310                            self.interp.debugger_leave_sub();
3311                            return Err(e);
3312                        }
3313                        Err(_) => self.push(StrykeValue::UNDEF),
3314                    }
3315                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3316                        p.exit_sub(t0.elapsed());
3317                    }
3318                    self.interp.debugger_leave_sub();
3319                } else if let Some(result) = self.interp.try_autoload_call(
3320                    name,
3321                    if argc == 0 {
3322                        self.interp.with_topic_default_args(args.clone())
3323                    } else {
3324                        args.clone()
3325                    },
3326                    self.line(),
3327                    want,
3328                    None,
3329                ) {
3330                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3331                    if let Some(p) = &mut self.interp.profiler {
3332                        p.enter_sub(name);
3333                    }
3334                    self.interp.debugger_enter_sub(name);
3335                    match result {
3336                        Ok(v) => self.push(v),
3337                        Err(crate::vm_helper::FlowOrError::Flow(
3338                            crate::vm_helper::Flow::Return(v),
3339                        )) => self.push(v),
3340                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3341                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3342                                p.exit_sub(t0.elapsed());
3343                            }
3344                            self.interp.debugger_leave_sub();
3345                            return Err(e);
3346                        }
3347                        Err(_) => self.push(StrykeValue::UNDEF),
3348                    }
3349                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3350                        p.exit_sub(t0.elapsed());
3351                    }
3352                    self.interp.debugger_leave_sub();
3353                } else if let Some(def) = self.interp.struct_defs.get(name).cloned() {
3354                    // Struct constructor: Point(x => 1, y => 2) or Point(1, 2)
3355                    let result = self.interp.struct_construct(&def, args, self.line());
3356                    match result {
3357                        Ok(v) => self.push(v),
3358                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
3359                        _ => self.push(StrykeValue::UNDEF),
3360                    }
3361                } else if let Some(def) = self.interp.class_defs.get(name).cloned() {
3362                    // Class constructor: Dog(name => "Rex") or Dog("Rex", 5)
3363                    let result = self.interp.class_construct(&def, args, self.line());
3364                    match result {
3365                        Ok(v) => self.push(v),
3366                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
3367                        _ => self.push(StrykeValue::UNDEF),
3368                    }
3369                } else if let Some((prefix, suffix)) = name.rsplit_once("::") {
3370                    // Enum variant constructor: Color::Red or Maybe::Some(value)
3371                    if let Some(def) = self.interp.enum_defs.get(prefix).cloned() {
3372                        let result = self.interp.enum_construct(&def, suffix, args, self.line());
3373                        match result {
3374                            Ok(v) => self.push(v),
3375                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
3376                            _ => self.push(StrykeValue::UNDEF),
3377                        }
3378                    // Static class method: Math::add(...)
3379                    } else if let Some(def) = self.interp.class_defs.get(prefix).cloned() {
3380                        if let Some(m) = def.method(suffix) {
3381                            if m.is_static {
3382                                if let Some(ref body) = m.body {
3383                                    let params = m.params.clone();
3384                                    match self.interp.call_static_class_method(
3385                                        body,
3386                                        &params,
3387                                        args.clone(),
3388                                        self.line(),
3389                                    ) {
3390                                        Ok(v) => self.push(v),
3391                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3392                                            return Err(e)
3393                                        }
3394                                        Err(crate::vm_helper::FlowOrError::Flow(
3395                                            crate::vm_helper::Flow::Return(v),
3396                                        )) => self.push(v),
3397                                        _ => self.push(StrykeValue::UNDEF),
3398                                    }
3399                                } else {
3400                                    self.push(StrykeValue::UNDEF);
3401                                }
3402                            } else {
3403                                return Err(StrykeError::runtime(
3404                                    format!("method `{}` is not static", suffix),
3405                                    self.line(),
3406                                ));
3407                            }
3408                        } else if def.static_fields.iter().any(|sf| sf.name == suffix) {
3409                            // Static field access: getter (0 args) or setter (1 arg)
3410                            let key = format!("{}::{}", prefix, suffix);
3411                            match args.len() {
3412                                0 => {
3413                                    let val = self.interp.scope.get_scalar(&key);
3414                                    self.push(val);
3415                                }
3416                                1 => {
3417                                    let _ = self.interp.scope.set_scalar(&key, args[0].clone());
3418                                    self.push(args[0].clone());
3419                                }
3420                                _ => {
3421                                    return Err(StrykeError::runtime(
3422                                        format!(
3423                                            "static field `{}::{}` takes 0 or 1 arguments",
3424                                            prefix, suffix
3425                                        ),
3426                                        self.line(),
3427                                    ));
3428                                }
3429                            }
3430                        } else {
3431                            return Err(StrykeError::runtime(
3432                                self.interp.undefined_subroutine_call_message(name),
3433                                self.line(),
3434                            ));
3435                        }
3436                    } else {
3437                        return Err(StrykeError::runtime(
3438                            self.interp.undefined_subroutine_call_message(name),
3439                            self.line(),
3440                        ));
3441                    }
3442                } else {
3443                    return Err(StrykeError::runtime(
3444                        self.interp.undefined_subroutine_call_message(name),
3445                        self.line(),
3446                    ));
3447                }
3448            }
3449        }
3450        Ok(())
3451    }
3452
3453    #[inline]
3454    fn push_binop_with_overload<F>(
3455        &mut self,
3456        op: BinOp,
3457        a: StrykeValue,
3458        b: StrykeValue,
3459        default: F,
3460    ) -> StrykeResult<()>
3461    where
3462        F: FnOnce(&StrykeValue, &StrykeValue) -> StrykeResult<StrykeValue>,
3463    {
3464        let line = self.line();
3465        if let Some(exec_res) = self.interp.try_overload_binop(op, &a, &b, line) {
3466            self.push(vm_interp_result(exec_res, line)?);
3467        } else {
3468            self.push(default(&a, &b)?);
3469        }
3470        Ok(())
3471    }
3472
3473    pub(crate) fn concat_stack_values(
3474        &mut self,
3475        a: StrykeValue,
3476        b: StrykeValue,
3477    ) -> StrykeResult<StrykeValue> {
3478        let line = self.line();
3479        if let Some(exec_res) = self.interp.try_overload_binop(BinOp::Concat, &a, &b, line) {
3480            vm_interp_result(exec_res, line)
3481        } else {
3482            let sa = match self.interp.stringify_value(a, line) {
3483                Ok(s) => s,
3484                Err(FlowOrError::Error(e)) => return Err(e),
3485                Err(FlowOrError::Flow(_)) => {
3486                    return Err(StrykeError::runtime(
3487                        "concat: unexpected control flow",
3488                        line,
3489                    ));
3490                }
3491            };
3492            let sb = match self.interp.stringify_value(b, line) {
3493                Ok(s) => s,
3494                Err(FlowOrError::Error(e)) => return Err(e),
3495                Err(FlowOrError::Flow(_)) => {
3496                    return Err(StrykeError::runtime(
3497                        "concat: unexpected control flow",
3498                        line,
3499                    ));
3500                }
3501            };
3502            let mut s = sa;
3503            s.push_str(&sb);
3504            Ok(StrykeValue::string(s))
3505        }
3506    }
3507
3508    fn run_main_dispatch_loop(
3509        &mut self,
3510        mut last: StrykeValue,
3511        op_count: &mut u64,
3512        init_dispatch: bool,
3513    ) -> StrykeResult<StrykeValue> {
3514        if init_dispatch {
3515            self.halt = false;
3516            self.exit_main_dispatch = false;
3517            self.exit_main_dispatch_value = None;
3518        }
3519        let ops_ref: &Vec<Op> = &self.ops;
3520        let ops = ops_ref as *const Vec<Op>;
3521        let ops = unsafe { &*ops };
3522        let names_ref: &Vec<String> = &self.names;
3523        let names = names_ref as *const Vec<String>;
3524        let names = unsafe { &*names };
3525        let constants_ref: &Vec<StrykeValue> = &self.constants;
3526        let constants = constants_ref as *const Vec<StrykeValue>;
3527        let constants = unsafe { &*constants };
3528        let len = ops.len();
3529        const MAX_OPS: u64 = 1_000_000_000;
3530        loop {
3531            if self.jit_trampoline_depth > 0 && self.jit_trampoline_out.is_some() {
3532                break;
3533            }
3534            if self.block_region_return.is_some() {
3535                break;
3536            }
3537            if self.block_region_mode && self.ip >= self.block_region_end {
3538                return Err(StrykeError::runtime(
3539                    "block bytecode region fell through without BlockReturnValue",
3540                    self.line(),
3541                ));
3542            }
3543            if self.ip >= len {
3544                break;
3545            }
3546
3547            if !self.block_region_mode
3548                && self.jit_enabled
3549                && self.sub_entry_at_ip.get(self.ip).copied().unwrap_or(false)
3550            {
3551                let sub_ip = self.ip;
3552                if sub_ip >= self.sub_entry_invoke_count.len() {
3553                    self.sub_entry_invoke_count.resize(sub_ip + 1, 0);
3554                }
3555                let c = &mut self.sub_entry_invoke_count[sub_ip];
3556                if *c <= self.jit_sub_invoke_threshold {
3557                    *c = c.saturating_add(1);
3558                }
3559                let should_try_jit = *c > self.jit_sub_invoke_threshold
3560                    && (!self.sub_jit_skip_linear_test(sub_ip)
3561                        || !self.sub_jit_skip_block_test(sub_ip));
3562                if should_try_jit {
3563                    // Tier 0: shared fusevm runtime. Falls through to strykelang's
3564                    // own JIT below when the segment isn't in the universal-integer
3565                    // subset fusevm handles.
3566                    if self.try_fusevm_subroutine()? {
3567                        continue;
3568                    }
3569                    if !self.sub_jit_skip_linear_test(sub_ip) && self.try_jit_subroutine_linear()? {
3570                        continue;
3571                    }
3572                    if !self.sub_jit_skip_block_test(sub_ip) && self.try_jit_subroutine_block()? {
3573                        continue;
3574                    }
3575                }
3576            }
3577
3578            *op_count += 1;
3579            // `%SIG` delivery and the execution cap: same cadence as the old per-op poll (signals
3580            // remain responsive; hot loops avoid a syscall/atomic path every opcode).
3581            if (*op_count & 0x3FF) == 0 {
3582                crate::perl_signal::poll(self.interp)?;
3583                if *op_count > MAX_OPS {
3584                    return Err(StrykeError::runtime(
3585                        "VM execution limit exceeded (possible infinite loop)",
3586                        self.line(),
3587                    ));
3588                }
3589            }
3590
3591            let ip_before = self.ip;
3592            let line = self.lines.get(ip_before).copied().unwrap_or(0);
3593            let op = &ops[self.ip];
3594            self.ip += 1;
3595
3596            // Debugger hook: check if we should stop at this line
3597            if let Some(ref mut dbg) = self.interp.debugger {
3598                if dbg.should_stop(line) {
3599                    let call_stack = self.interp.debug_call_stack.clone();
3600                    match dbg.prompt(line, &self.interp.scope, &call_stack) {
3601                        crate::debugger::DebugAction::Quit => {
3602                            return Err(StrykeError::runtime("debugger: quit", line));
3603                        }
3604                        crate::debugger::DebugAction::Continue => {}
3605                        crate::debugger::DebugAction::Prompt => {}
3606                    }
3607                }
3608            }
3609
3610            let op_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3611            // Closure: `?` / `return Err` inside `match op` must not return from
3612            // `run_main_dispatch_loop` — they must become `__op_res` so `try_recover_from_exception`
3613            // can run before propagating.
3614            let __op_res: StrykeResult<()> = (|| -> StrykeResult<()> {
3615                match op {
3616                    Op::Nop => Ok(()),
3617                    // ── Constants ──
3618                    Op::LoadInt(n) => {
3619                        self.push(StrykeValue::integer(*n));
3620                        Ok(())
3621                    }
3622                    Op::LoadFloat(f) => {
3623                        self.push(StrykeValue::float(*f));
3624                        Ok(())
3625                    }
3626                    Op::LoadConst(idx) => {
3627                        self.push(self.constant(*idx).clone());
3628                        Ok(())
3629                    }
3630                    Op::LoadUndef => {
3631                        self.push(StrykeValue::UNDEF);
3632                        Ok(())
3633                    }
3634                    Op::RuntimeErrorConst(idx) => {
3635                        let msg = self.constant(*idx).to_string();
3636                        let line = self.line();
3637                        Err(crate::error::StrykeError::runtime(msg, line))
3638                    }
3639                    Op::BarewordRvalue(name_idx) => {
3640                        let name = names[*name_idx as usize].clone();
3641                        let line = self.line();
3642                        let out = vm_interp_result(
3643                            self.interp.resolve_bareword_rvalue(
3644                                &name,
3645                                crate::vm_helper::WantarrayCtx::Scalar,
3646                                line,
3647                            ),
3648                            line,
3649                        )?;
3650                        self.push(out);
3651                        Ok(())
3652                    }
3653
3654                    // ── Stack ──
3655                    Op::Pop => {
3656                        let v = self.pop();
3657                        // Drain iterators used as void statements so side effects fire.
3658                        if v.is_iterator() {
3659                            let iter = v.into_iterator();
3660                            while iter.next_item().is_some() {}
3661                        }
3662                        Ok(())
3663                    }
3664                    Op::Dup => {
3665                        let v = self.peek().dup_stack();
3666                        self.push(v);
3667                        Ok(())
3668                    }
3669                    Op::Dup2 => {
3670                        let b = self.pop();
3671                        let a = self.pop();
3672                        self.push(a.dup_stack());
3673                        self.push(b.dup_stack());
3674                        self.push(a);
3675                        self.push(b);
3676                        Ok(())
3677                    }
3678                    Op::Swap => {
3679                        let top = self.pop();
3680                        let below = self.pop();
3681                        self.push(top);
3682                        self.push(below);
3683                        Ok(())
3684                    }
3685                    Op::Rot => {
3686                        let c = self.pop();
3687                        let b = self.pop();
3688                        let a = self.pop();
3689                        self.push(b);
3690                        self.push(c);
3691                        self.push(a);
3692                        Ok(())
3693                    }
3694                    Op::ValueScalarContext => {
3695                        let v = self.pop();
3696                        self.push(v.scalar_context());
3697                        Ok(())
3698                    }
3699                    Op::ListFirst => {
3700                        let v = self.pop();
3701                        let first = if let Some(arr) = v.as_array_vec() {
3702                            arr.first().cloned().unwrap_or(StrykeValue::UNDEF)
3703                        } else {
3704                            v
3705                        };
3706                        self.push(first);
3707                        Ok(())
3708                    }
3709
3710                    // ── Scalars ──
3711                    Op::GetScalar(idx) => {
3712                        let n = names[*idx as usize].as_str();
3713                        let val = self.interp.get_special_var(n);
3714                        self.push(val);
3715                        Ok(())
3716                    }
3717                    Op::GetScalarPlain(idx) => {
3718                        let n = names[*idx as usize].as_str();
3719                        let val = self.interp.scope.get_scalar(n);
3720                        self.push(val);
3721                        Ok(())
3722                    }
3723                    Op::SetScalar(idx) => {
3724                        let val = self.pop();
3725                        let n = names[*idx as usize].as_str();
3726                        self.require_scalar_mutable(n)?;
3727                        self.interp.maybe_invalidate_regex_capture_memo(n);
3728                        self.interp
3729                            .set_special_var(n, &val)
3730                            .map_err(|e| e.at_line(self.line()))?;
3731                        Ok(())
3732                    }
3733                    Op::SetScalarPlain(idx) => {
3734                        let val = self.pop();
3735                        let n = names[*idx as usize].as_str();
3736                        self.require_scalar_mutable(n)?;
3737                        self.interp.maybe_invalidate_regex_capture_memo(n);
3738                        self.interp
3739                            .scope
3740                            .set_scalar(n, val)
3741                            .map_err(|e| e.at_line(self.line()))?;
3742                        Ok(())
3743                    }
3744                    Op::SetScalarKeep(idx) => {
3745                        let val = self.peek().dup_stack();
3746                        let n = names[*idx as usize].as_str();
3747                        self.require_scalar_mutable(n)?;
3748                        self.interp.maybe_invalidate_regex_capture_memo(n);
3749                        self.interp
3750                            .set_special_var(n, &val)
3751                            .map_err(|e| e.at_line(self.line()))?;
3752                        Ok(())
3753                    }
3754                    Op::SetScalarKeepPlain(idx) => {
3755                        let val = self.peek().dup_stack();
3756                        let n = names[*idx as usize].as_str();
3757                        self.require_scalar_mutable(n)?;
3758                        self.interp.maybe_invalidate_regex_capture_memo(n);
3759                        self.interp
3760                            .scope
3761                            .set_scalar(n, val)
3762                            .map_err(|e| e.at_line(self.line()))?;
3763                        Ok(())
3764                    }
3765                    Op::DeclareScalar(idx) => {
3766                        let val = self.pop();
3767                        let n = names[*idx as usize].as_str();
3768                        self.interp
3769                            .scope
3770                            .declare_scalar_frozen(n, val, false, None)
3771                            .map_err(|e| e.at_line(self.line()))?;
3772                        Ok(())
3773                    }
3774                    Op::DeclareScalarFrozen(idx) => {
3775                        let val = self.pop();
3776                        let n = names[*idx as usize].as_str();
3777                        self.interp
3778                            .scope
3779                            .declare_scalar_frozen(n, val, true, None)
3780                            .map_err(|e| e.at_line(self.line()))?;
3781                        Ok(())
3782                    }
3783                    Op::DeclareScalarTyped(idx, tyb) => {
3784                        let val = self.pop();
3785                        let n = names[*idx as usize].as_str();
3786                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
3787                            StrykeError::runtime(
3788                                format!("invalid typed scalar type byte {}", tyb),
3789                                self.line(),
3790                            )
3791                        })?;
3792                        self.interp
3793                            .scope
3794                            .declare_scalar_frozen(n, val, false, Some(ty))
3795                            .map_err(|e| e.at_line(self.line()))?;
3796                        Ok(())
3797                    }
3798                    Op::DeclareScalarTypedFrozen(idx, tyb) => {
3799                        let val = self.pop();
3800                        let n = names[*idx as usize].as_str();
3801                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
3802                            StrykeError::runtime(
3803                                format!("invalid typed scalar type byte {}", tyb),
3804                                self.line(),
3805                            )
3806                        })?;
3807                        self.interp
3808                            .scope
3809                            .declare_scalar_frozen(n, val, true, Some(ty))
3810                            .map_err(|e| e.at_line(self.line()))?;
3811                        Ok(())
3812                    }
3813                    Op::DeclareScalarTypedUser(name_idx, type_idx, flag) => {
3814                        let val = self.pop();
3815                        let n = names[*name_idx as usize].as_str();
3816                        let type_name = names[*type_idx as usize].clone();
3817                        let is_enum = (flag & 0b01) != 0;
3818                        let is_frozen = (flag & 0b10) != 0;
3819                        let ty = if is_enum {
3820                            PerlTypeName::Enum(type_name)
3821                        } else {
3822                            // Struct variant covers struct, class, and any
3823                            // user-defined nominal type — `check_value` for
3824                            // `Struct(name)` already accepts class instances
3825                            // via `c.isa(name)`.
3826                            PerlTypeName::Struct(type_name)
3827                        };
3828                        self.interp
3829                            .scope
3830                            .declare_scalar_frozen(n, val, is_frozen, Some(ty))
3831                            .map_err(|e| e.at_line(self.line()))?;
3832                        Ok(())
3833                    }
3834
3835                    // ── State variables (persist across calls) ──
3836                    Op::DeclareStateScalar(idx) => {
3837                        let init_val = self.pop();
3838                        let n = names[*idx as usize].as_str();
3839                        // Key by source line + name (matches interpreter's state_key format)
3840                        let state_key = format!("{}:{}", self.line(), n);
3841                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3842                            prev.clone()
3843                        } else {
3844                            self.interp
3845                                .state_vars
3846                                .insert(state_key.clone(), init_val.clone());
3847                            init_val
3848                        };
3849                        self.interp
3850                            .scope
3851                            .declare_scalar_frozen(n, val, false, None)
3852                            .map_err(|e| e.at_line(self.line()))?;
3853                        // Register for save-back when scope pops
3854                        if let Some(frame) = self.interp.state_bindings_stack.last_mut() {
3855                            frame.push((n.to_string(), state_key));
3856                        }
3857                        Ok(())
3858                    }
3859                    Op::DeclareStateArray(idx) => {
3860                        let init_val = self.pop();
3861                        let n = names[*idx as usize].as_str();
3862                        let state_key = format!("{}:{}", self.line(), n);
3863                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3864                            prev.clone()
3865                        } else {
3866                            self.interp
3867                                .state_vars
3868                                .insert(state_key.clone(), init_val.clone());
3869                            init_val
3870                        };
3871                        self.interp.scope.declare_array(n, val.to_list());
3872                        Ok(())
3873                    }
3874                    Op::DeclareStateHash(idx) => {
3875                        let init_val = self.pop();
3876                        let n = names[*idx as usize].as_str();
3877                        let state_key = format!("{}:{}", self.line(), n);
3878                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3879                            prev.clone()
3880                        } else {
3881                            self.interp
3882                                .state_vars
3883                                .insert(state_key.clone(), init_val.clone());
3884                            init_val
3885                        };
3886                        let items = val.to_list();
3887                        let mut map = IndexMap::new();
3888                        let mut i = 0;
3889                        while i + 1 < items.len() {
3890                            map.insert(items[i].to_string(), items[i + 1].clone());
3891                            i += 2;
3892                        }
3893                        self.interp.scope.declare_hash(n, map);
3894                        Ok(())
3895                    }
3896
3897                    // ── Arrays ──
3898                    Op::GetArray(idx) => {
3899                        let n = names[*idx as usize].as_str();
3900                        let arr = self.interp.scope.get_array(n);
3901                        self.push(StrykeValue::array(arr));
3902                        Ok(())
3903                    }
3904                    Op::SetArray(idx) => {
3905                        let val = self.pop();
3906                        let n = names[*idx as usize].as_str();
3907                        self.require_array_mutable(n)?;
3908                        self.interp
3909                            .scope
3910                            .set_array(n, val.to_list())
3911                            .map_err(|e| e.at_line(self.line()))?;
3912                        Ok(())
3913                    }
3914                    Op::DeclareArray(idx) => {
3915                        let val = self.pop();
3916                        let n = names[*idx as usize].as_str();
3917                        self.interp.scope.declare_array(n, val.to_list());
3918                        Ok(())
3919                    }
3920                    Op::DeclareArrayFrozen(idx) => {
3921                        let val = self.pop();
3922                        let n = names[*idx as usize].as_str();
3923                        self.interp
3924                            .scope
3925                            .declare_array_frozen(n, val.to_list(), true);
3926                        Ok(())
3927                    }
3928                    Op::GetArrayElem(idx) => {
3929                        let index = self.pop().to_int();
3930                        let n = names[*idx as usize].as_str();
3931                        // Stryke string-index sugar: bareword `_[N]` parses
3932                        // to a `__topicstr__N` synthetic name. Index the
3933                        // scalar (`$_` / `$_N`) by char.
3934                        if let Some(real) = n.strip_prefix("__topicstr__") {
3935                            let s = self.interp.scope.get_scalar(real).to_string();
3936                            let cnt = s.chars().count() as i64;
3937                            let i = if index < 0 { index + cnt } else { index };
3938                            let v = if i >= 0 && i < cnt {
3939                                s.chars()
3940                                    .nth(i as usize)
3941                                    .map(|c| StrykeValue::string(c.to_string()))
3942                                    .unwrap_or(StrykeValue::UNDEF)
3943                            } else {
3944                                StrykeValue::UNDEF
3945                            };
3946                            self.push(v);
3947                            return Ok(());
3948                        }
3949                        // Stryke (non-compat) sugar: `$s[i]` indexes by
3950                        // Unicode char when `@s` is missing or empty but
3951                        // `$s` is a non-empty string. NB: `$_[0]` keeps
3952                        // Perl's `@_`-access semantics because `@_` is
3953                        // populated inside any sub; the bareword `_[0]`
3954                        // parses to the same AST so it behaves identically.
3955                        // Use `substr(_, 0, 1)` for char-of-topic inside
3956                        // a sub. Compat mode = Perl semantics.
3957                        if !crate::compat_mode() && self.interp.scope.scalar_binding_exists(n) {
3958                            let prefer_scalar = self.interp.scope.get_array(n).is_empty();
3959                            if prefer_scalar {
3960                                let s = self.interp.scope.get_scalar(n).to_string();
3961                                if !s.is_empty() {
3962                                    let cnt = s.chars().count() as i64;
3963                                    let i = if index < 0 { index + cnt } else { index };
3964                                    let v = if i >= 0 && i < cnt {
3965                                        s.chars()
3966                                            .nth(i as usize)
3967                                            .map(|c| StrykeValue::string(c.to_string()))
3968                                            .unwrap_or(StrykeValue::UNDEF)
3969                                    } else {
3970                                        StrykeValue::UNDEF
3971                                    };
3972                                    self.push(v);
3973                                    return Ok(());
3974                                }
3975                            }
3976                        }
3977                        let val = self.interp.scope.get_array_element(n, index);
3978                        self.push(val);
3979                        Ok(())
3980                    }
3981                    Op::ExistsArrayElem(idx) => {
3982                        let index = self.pop().to_int();
3983                        let n = names[*idx as usize].as_str();
3984                        let yes = self.interp.scope.exists_array_element(n, index);
3985                        self.push(StrykeValue::integer(if yes { 1 } else { 0 }));
3986                        Ok(())
3987                    }
3988                    Op::DeleteArrayElem(idx) => {
3989                        let index = self.pop().to_int();
3990                        let n = names[*idx as usize].as_str();
3991                        self.require_array_mutable(n)?;
3992                        let v = self
3993                            .interp
3994                            .scope
3995                            .delete_array_element(n, index)
3996                            .map_err(|e| e.at_line(self.line()))?;
3997                        self.push(v);
3998                        Ok(())
3999                    }
4000                    Op::SetArrayElem(idx) => {
4001                        let index = self.pop().to_int();
4002                        let val = self.pop();
4003                        let n = names[*idx as usize].as_str();
4004                        self.require_array_mutable(n)?;
4005                        self.interp
4006                            .scope
4007                            .set_array_element(n, index, val)
4008                            .map_err(|e| e.at_line(self.line()))?;
4009                        Ok(())
4010                    }
4011                    Op::SetArrayElemKeep(idx) => {
4012                        let index = self.pop().to_int();
4013                        let val = self.pop();
4014                        let val_keep = val.clone();
4015                        let n = names[*idx as usize].as_str();
4016                        self.require_array_mutable(n)?;
4017                        let line = self.line();
4018                        self.interp
4019                            .scope
4020                            .set_array_element(n, index, val)
4021                            .map_err(|e| e.at_line(line))?;
4022                        self.push(val_keep);
4023                        Ok(())
4024                    }
4025                    Op::PushArray(idx) => {
4026                        let val = self.pop();
4027                        let n = names[*idx as usize].as_str();
4028                        self.require_array_mutable(n)?;
4029                        let line = self.line();
4030                        if let Some(items) = val.as_array_vec() {
4031                            for item in items {
4032                                self.interp
4033                                    .scope
4034                                    .push_to_array(n, item)
4035                                    .map_err(|e| e.at_line(line))?;
4036                            }
4037                        } else {
4038                            self.interp
4039                                .scope
4040                                .push_to_array(n, val)
4041                                .map_err(|e| e.at_line(line))?;
4042                        }
4043                        Ok(())
4044                    }
4045                    Op::PopArray(idx) => {
4046                        let n = names[*idx as usize].as_str();
4047                        self.require_array_mutable(n)?;
4048                        let line = self.line();
4049                        let val = self
4050                            .interp
4051                            .scope
4052                            .pop_from_array(n)
4053                            .map_err(|e| e.at_line(line))?;
4054                        self.push(val);
4055                        Ok(())
4056                    }
4057                    Op::ShiftArray(idx) => {
4058                        let n = names[*idx as usize].as_str();
4059                        self.require_array_mutable(n)?;
4060                        let line = self.line();
4061                        let val = self
4062                            .interp
4063                            .scope
4064                            .shift_from_array(n)
4065                            .map_err(|e| e.at_line(line))?;
4066                        self.push(val);
4067                        Ok(())
4068                    }
4069                    Op::PushArrayDeref => {
4070                        let val = self.pop();
4071                        let r = self.pop();
4072                        let line = self.line();
4073                        vm_interp_result(
4074                            self.interp
4075                                .push_array_deref_value(r.clone(), val, line)
4076                                .map(|_| StrykeValue::UNDEF),
4077                            line,
4078                        )?;
4079                        self.push(r);
4080                        Ok(())
4081                    }
4082                    Op::ArrayDerefLen => {
4083                        let r = self.pop();
4084                        let line = self.line();
4085                        let n = match self.interp.array_deref_len(r, line) {
4086                            Ok(n) => n,
4087                            Err(FlowOrError::Error(e)) => return Err(e),
4088                            Err(FlowOrError::Flow(_)) => {
4089                                return Err(StrykeError::runtime(
4090                                    "unexpected flow in tree-assisted opcode",
4091                                    line,
4092                                ));
4093                            }
4094                        };
4095                        self.push(StrykeValue::integer(n));
4096                        Ok(())
4097                    }
4098                    Op::PopArrayDeref => {
4099                        let r = self.pop();
4100                        let line = self.line();
4101                        let v = vm_interp_result(self.interp.pop_array_deref(r, line), line)?;
4102                        self.push(v);
4103                        Ok(())
4104                    }
4105                    Op::ShiftArrayDeref => {
4106                        let r = self.pop();
4107                        let line = self.line();
4108                        let v = vm_interp_result(self.interp.shift_array_deref(r, line), line)?;
4109                        self.push(v);
4110                        Ok(())
4111                    }
4112                    Op::UnshiftArrayDeref(n_extra) => {
4113                        let n = *n_extra as usize;
4114                        let mut vals: Vec<StrykeValue> = Vec::with_capacity(n);
4115                        for _ in 0..n {
4116                            vals.push(self.pop());
4117                        }
4118                        vals.reverse();
4119                        let r = self.pop();
4120                        let line = self.line();
4121                        let len = match self.interp.unshift_array_deref_multi(r, vals, line) {
4122                            Ok(n) => n,
4123                            Err(FlowOrError::Error(e)) => return Err(e),
4124                            Err(FlowOrError::Flow(_)) => {
4125                                return Err(StrykeError::runtime(
4126                                    "unexpected flow in tree-assisted opcode",
4127                                    line,
4128                                ));
4129                            }
4130                        };
4131                        self.push(StrykeValue::integer(len));
4132                        Ok(())
4133                    }
4134                    Op::SpliceArrayDeref(n_rep) => {
4135                        let n = *n_rep as usize;
4136                        let mut rep_vals: Vec<StrykeValue> = Vec::with_capacity(n);
4137                        for _ in 0..n {
4138                            rep_vals.push(self.pop());
4139                        }
4140                        rep_vals.reverse();
4141                        let length_val = self.pop();
4142                        let offset_val = self.pop();
4143                        let aref = self.pop();
4144                        let line = self.line();
4145                        let v = vm_interp_result(
4146                            self.interp
4147                                .splice_array_deref(aref, offset_val, length_val, rep_vals, line),
4148                            line,
4149                        )?;
4150                        self.push(v);
4151                        Ok(())
4152                    }
4153                    Op::ArrayLen(idx) => {
4154                        let len = self.interp.scope.array_len(&self.names[*idx as usize]);
4155                        self.push(StrykeValue::integer(len as i64));
4156                        Ok(())
4157                    }
4158                    Op::ArraySlicePart(idx) => {
4159                        let spec = self.pop();
4160                        let n = names[*idx as usize].as_str();
4161                        let mut out = Vec::new();
4162                        if let Some(indices) = spec.as_array_vec() {
4163                            for pv in indices {
4164                                out.push(self.interp.scope.get_array_element(n, pv.to_int()));
4165                            }
4166                        } else {
4167                            out.push(self.interp.scope.get_array_element(n, spec.to_int()));
4168                        }
4169                        self.push(StrykeValue::array(out));
4170                        Ok(())
4171                    }
4172                    Op::GetArrayFromIndex(idx, start) => {
4173                        let n = names[*idx as usize].as_str();
4174                        let arr = self.interp.scope.get_array(n);
4175                        let start = *start as usize;
4176                        let out: Vec<StrykeValue> = if start >= arr.len() {
4177                            Vec::new()
4178                        } else {
4179                            arr[start..].to_vec()
4180                        };
4181                        self.push(StrykeValue::array(out));
4182                        Ok(())
4183                    }
4184                    Op::ArrayConcatTwo => {
4185                        let b = self.pop();
4186                        let a = self.pop();
4187                        let mut av = a.as_array_vec().unwrap_or_else(|| vec![a]);
4188                        let bv = b.as_array_vec().unwrap_or_else(|| vec![b]);
4189                        av.extend(bv);
4190                        self.push(StrykeValue::array(av));
4191                        Ok(())
4192                    }
4193
4194                    // ── Hashes ──
4195                    Op::GetHash(idx) => {
4196                        let n = names[*idx as usize].as_str();
4197                        self.interp.touch_env_hash(n);
4198                        let h = self.interp.scope.get_hash(n);
4199                        self.push(StrykeValue::hash(h));
4200                        Ok(())
4201                    }
4202                    Op::SetHash(idx) => {
4203                        let val = self.pop();
4204                        let items = val.to_list();
4205                        let mut map = IndexMap::new();
4206                        let mut i = 0;
4207                        while i + 1 < items.len() {
4208                            map.insert(items[i].to_string(), items[i + 1].clone());
4209                            i += 2;
4210                        }
4211                        let n = names[*idx as usize].as_str();
4212                        self.require_hash_mutable(n)?;
4213                        self.interp
4214                            .scope
4215                            .set_hash(n, map)
4216                            .map_err(|e| e.at_line(self.line()))?;
4217                        Ok(())
4218                    }
4219                    Op::DeclareHash(idx) => {
4220                        let val = self.pop();
4221                        let n = names[*idx as usize].as_str();
4222                        // `our %h;` (no initializer) compiles as
4223                        // LoadUndef + DeclareHash. For package-qualified
4224                        // names (the `our` form), we must NOT clobber
4225                        // existing data — re-declaring in a subsequent
4226                        // EVAL on the same persistent VMHelper should
4227                        // preserve cross-EVAL state. For lexical names
4228                        // (the `my` form, no `::` qualifier), the
4229                        // declare-only path SHOULD initialize to empty
4230                        // every time (a fresh `my %h;` inside a loop
4231                        // must reset per iteration; preserving prior
4232                        // data would silently leak state across loops
4233                        // and break demos like de_bruijn_sequence).
4234                        // Bug fix 2026-05-27, refined to gate on
4235                        // package-qualification after de_bruijn regression.
4236                        if val.is_undef() && n.contains("::") {
4237                            let existing = self.interp.scope.get_hash(n);
4238                            self.interp.scope.declare_hash(n, existing);
4239                        } else {
4240                            let items = val.to_list();
4241                            let mut map = IndexMap::new();
4242                            let mut i = 0;
4243                            while i + 1 < items.len() {
4244                                map.insert(items[i].to_string(), items[i + 1].clone());
4245                                i += 2;
4246                            }
4247                            self.interp.scope.declare_hash(n, map);
4248                        }
4249                        Ok(())
4250                    }
4251                    Op::DeclareHashFrozen(idx) => {
4252                        let val = self.pop();
4253                        let items = val.to_list();
4254                        let mut map = IndexMap::new();
4255                        let mut i = 0;
4256                        while i + 1 < items.len() {
4257                            map.insert(items[i].to_string(), items[i + 1].clone());
4258                            i += 2;
4259                        }
4260                        let n = names[*idx as usize].as_str();
4261                        self.interp.scope.declare_hash_frozen(n, map, true);
4262                        Ok(())
4263                    }
4264                    Op::LocalDeclareScalar(idx) => {
4265                        let val = self.pop();
4266                        let n = names[*idx as usize].as_str();
4267                        // `local $X` on a special var (`$/`, `$\`, `$,`, `$"`, …) — see
4268                        // Perl's `local` handler. Save prior value to
4269                        // the interpreter's `special_var_restore_frames` so `scope_pop_hook`
4270                        // restores the backing field on block exit.
4271                        if VMHelper::is_special_scalar_name_for_set(n) {
4272                            let old = self.interp.get_special_var(n);
4273                            if let Some(frame) = self.interp.special_var_restore_frames.last_mut() {
4274                                frame.push((n.to_string(), old));
4275                            }
4276                            let line = self.line();
4277                            self.interp
4278                                .set_special_var(n, &val)
4279                                .map_err(|e| e.at_line(line))?;
4280                        }
4281                        self.interp
4282                            .scope
4283                            .local_set_scalar(n, val.clone())
4284                            .map_err(|e| e.at_line(self.line()))?;
4285                        self.push(val);
4286                        Ok(())
4287                    }
4288                    Op::LocalDeclareArray(idx) => {
4289                        let val = self.pop();
4290                        let n = names[*idx as usize].as_str();
4291                        self.interp
4292                            .scope
4293                            .local_set_array(n, val.to_list())
4294                            .map_err(|e| e.at_line(self.line()))?;
4295                        self.push(val);
4296                        Ok(())
4297                    }
4298                    Op::LocalDeclareHash(idx) => {
4299                        let val = self.pop();
4300                        let items = val.to_list();
4301                        let mut map = IndexMap::new();
4302                        let mut i = 0;
4303                        while i + 1 < items.len() {
4304                            map.insert(items[i].to_string(), items[i + 1].clone());
4305                            i += 2;
4306                        }
4307                        let n = names[*idx as usize].as_str();
4308                        self.interp.touch_env_hash(n);
4309                        self.interp
4310                            .scope
4311                            .local_set_hash(n, map)
4312                            .map_err(|e| e.at_line(self.line()))?;
4313                        self.push(val);
4314                        Ok(())
4315                    }
4316                    Op::LocalDeclareHashElement(idx) => {
4317                        let key = self.pop().to_string();
4318                        let val = self.pop();
4319                        let n = names[*idx as usize].as_str();
4320                        self.interp.touch_env_hash(n);
4321                        self.interp
4322                            .scope
4323                            .local_set_hash_element(n, key.as_str(), val.clone())
4324                            .map_err(|e| e.at_line(self.line()))?;
4325                        self.push(val);
4326                        Ok(())
4327                    }
4328                    Op::LocalDeclareArrayElement(idx) => {
4329                        let index = self.pop().to_int();
4330                        let val = self.pop();
4331                        let n = names[*idx as usize].as_str();
4332                        self.require_array_mutable(n)?;
4333                        self.interp
4334                            .scope
4335                            .local_set_array_element(n, index, val.clone())
4336                            .map_err(|e| e.at_line(self.line()))?;
4337                        self.push(val);
4338                        Ok(())
4339                    }
4340                    Op::LocalDeclareTypeglob(lhs_i, rhs_opt) => {
4341                        let lhs = names[*lhs_i as usize].as_str();
4342                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
4343                        let line = self.line();
4344                        self.interp
4345                            .local_declare_typeglob(lhs, rhs, line)
4346                            .map_err(|e| e.at_line(line))?;
4347                        Ok(())
4348                    }
4349                    Op::LocalDeclareTypeglobDynamic(rhs_opt) => {
4350                        let lhs = self.pop().to_string();
4351                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
4352                        let line = self.line();
4353                        self.interp
4354                            .local_declare_typeglob(lhs.as_str(), rhs, line)
4355                            .map_err(|e| e.at_line(line))?;
4356                        Ok(())
4357                    }
4358                    Op::GetHashElem(idx) => {
4359                        let key = self.pop().to_string();
4360                        let n = names[*idx as usize].as_str();
4361                        self.interp.touch_env_hash(n);
4362                        let val = self.interp.scope.get_hash_element(n, &key);
4363                        self.push(val);
4364                        Ok(())
4365                    }
4366                    Op::SetHashElem(idx) => {
4367                        let key = self.pop().to_string();
4368                        let val = self.pop();
4369                        let n = names[*idx as usize].as_str();
4370                        self.require_hash_mutable(n)?;
4371                        self.interp.touch_env_hash(n);
4372                        self.interp
4373                            .scope
4374                            .set_hash_element(n, &key, val)
4375                            .map_err(|e| e.at_line(self.line()))?;
4376                        Ok(())
4377                    }
4378                    Op::SetHashElemKeep(idx) => {
4379                        let key = self.pop().to_string();
4380                        let val = self.pop();
4381                        let val_keep = val.clone();
4382                        let n = names[*idx as usize].as_str();
4383                        self.require_hash_mutable(n)?;
4384                        self.interp.touch_env_hash(n);
4385                        let line = self.line();
4386                        self.interp
4387                            .scope
4388                            .set_hash_element(n, &key, val)
4389                            .map_err(|e| e.at_line(line))?;
4390                        self.push(val_keep);
4391                        Ok(())
4392                    }
4393                    Op::DeleteHashElem(idx) => {
4394                        let key = self.pop().to_string();
4395                        let n = names[*idx as usize].as_str();
4396                        self.require_hash_mutable(n)?;
4397                        self.interp.touch_env_hash(n);
4398                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
4399                            let class = obj
4400                                .as_blessed_ref()
4401                                .map(|b| b.class.clone())
4402                                .unwrap_or_default();
4403                            let full = format!("{}::DELETE", class);
4404                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
4405                                let line = self.line();
4406                                let v = vm_interp_result(
4407                                    self.interp.call_sub(
4408                                        &sub,
4409                                        vec![obj, StrykeValue::string(key)],
4410                                        WantarrayCtx::Scalar,
4411                                        line,
4412                                    ),
4413                                    line,
4414                                )?;
4415                                self.push(v);
4416                                return Ok(());
4417                            }
4418                        }
4419                        let val = self
4420                            .interp
4421                            .scope
4422                            .delete_hash_element(n, &key)
4423                            .map_err(|e| e.at_line(self.line()))?;
4424                        self.push(val);
4425                        Ok(())
4426                    }
4427                    Op::ExistsHashElem(idx) => {
4428                        let key = self.pop().to_string();
4429                        let n = names[*idx as usize].as_str();
4430                        self.interp.touch_env_hash(n);
4431                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
4432                            let class = obj
4433                                .as_blessed_ref()
4434                                .map(|b| b.class.clone())
4435                                .unwrap_or_default();
4436                            let full = format!("{}::EXISTS", class);
4437                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
4438                                let line = self.line();
4439                                let v = vm_interp_result(
4440                                    self.interp.call_sub(
4441                                        &sub,
4442                                        vec![obj, StrykeValue::string(key)],
4443                                        WantarrayCtx::Scalar,
4444                                        line,
4445                                    ),
4446                                    line,
4447                                )?;
4448                                self.push(v);
4449                                return Ok(());
4450                            }
4451                        }
4452                        let exists = self.interp.scope.exists_hash_element(n, &key);
4453                        self.push(StrykeValue::integer(if exists { 1 } else { 0 }));
4454                        Ok(())
4455                    }
4456                    Op::ExistsArrowHashElem => {
4457                        let key = self.pop().to_string();
4458                        let container = self.pop();
4459                        let line = self.line();
4460                        let yes = vm_interp_result(
4461                            self.interp
4462                                .exists_arrow_hash_element(container, &key, line)
4463                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
4464                                .map_err(FlowOrError::Error),
4465                            line,
4466                        )?;
4467                        self.push(yes);
4468                        Ok(())
4469                    }
4470                    Op::DeleteArrowHashElem => {
4471                        let key = self.pop().to_string();
4472                        let container = self.pop();
4473                        let line = self.line();
4474                        let v = vm_interp_result(
4475                            self.interp
4476                                .delete_arrow_hash_element(container, &key, line)
4477                                .map_err(FlowOrError::Error),
4478                            line,
4479                        )?;
4480                        self.push(v);
4481                        Ok(())
4482                    }
4483                    Op::ExistsArrowArrayElem => {
4484                        let idx = self.pop().to_int();
4485                        let container = self.pop();
4486                        let line = self.line();
4487                        let yes = vm_interp_result(
4488                            self.interp
4489                                .exists_arrow_array_element(container, idx, line)
4490                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
4491                                .map_err(FlowOrError::Error),
4492                            line,
4493                        )?;
4494                        self.push(yes);
4495                        Ok(())
4496                    }
4497                    Op::DeleteArrowArrayElem => {
4498                        let idx = self.pop().to_int();
4499                        let container = self.pop();
4500                        let line = self.line();
4501                        let v = vm_interp_result(
4502                            self.interp
4503                                .delete_arrow_array_element(container, idx, line)
4504                                .map_err(FlowOrError::Error),
4505                            line,
4506                        )?;
4507                        self.push(v);
4508                        Ok(())
4509                    }
4510                    Op::HashKeys(idx) => {
4511                        let n = names[*idx as usize].as_str();
4512                        self.interp.touch_env_hash(n);
4513                        let h = self.interp.scope.get_hash(n);
4514                        let keys: Vec<StrykeValue> =
4515                            h.keys().map(|k| StrykeValue::string(k.clone())).collect();
4516                        self.push(StrykeValue::array(keys));
4517                        Ok(())
4518                    }
4519                    Op::HashKeysScalar(idx) => {
4520                        let n = names[*idx as usize].as_str();
4521                        self.interp.touch_env_hash(n);
4522                        let h = self.interp.scope.get_hash(n);
4523                        self.push(StrykeValue::integer(h.len() as i64));
4524                        Ok(())
4525                    }
4526                    Op::HashValues(idx) => {
4527                        let n = names[*idx as usize].as_str();
4528                        self.interp.touch_env_hash(n);
4529                        let h = self.interp.scope.get_hash(n);
4530                        let vals: Vec<StrykeValue> = h.values().cloned().collect();
4531                        self.push(StrykeValue::array(vals));
4532                        Ok(())
4533                    }
4534                    Op::HashValuesScalar(idx) => {
4535                        let n = names[*idx as usize].as_str();
4536                        self.interp.touch_env_hash(n);
4537                        let h = self.interp.scope.get_hash(n);
4538                        self.push(StrykeValue::integer(h.len() as i64));
4539                        Ok(())
4540                    }
4541                    Op::KeysFromValue => {
4542                        let val = self.pop();
4543                        let line = self.line();
4544                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
4545                        self.push(v);
4546                        Ok(())
4547                    }
4548                    Op::KeysFromValueScalar => {
4549                        let val = self.pop();
4550                        let line = self.line();
4551                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
4552                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
4553                        self.push(StrykeValue::integer(n));
4554                        Ok(())
4555                    }
4556                    Op::ValuesFromValue => {
4557                        let val = self.pop();
4558                        let line = self.line();
4559                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
4560                        self.push(v);
4561                        Ok(())
4562                    }
4563                    Op::ValuesFromValueScalar => {
4564                        let val = self.pop();
4565                        let line = self.line();
4566                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
4567                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
4568                        self.push(StrykeValue::integer(n));
4569                        Ok(())
4570                    }
4571
4572                    // ── Arithmetic (integer fast paths) ──
4573                    Op::Add => {
4574                        let b = self.pop();
4575                        let a = self.pop();
4576                        self.push_binop_with_overload(BinOp::Add, a, b, |a, b| {
4577                            if let Some(s) = crate::sketches::try_sketch_binop(
4578                                crate::sketches::SketchOp::Add,
4579                                a,
4580                                b,
4581                            ) {
4582                                return Ok(s);
4583                            }
4584                            Ok(crate::value::compat_add(a, b))
4585                        })
4586                    }
4587                    Op::Sub => {
4588                        let b = self.pop();
4589                        let a = self.pop();
4590                        self.push_binop_with_overload(BinOp::Sub, a, b, |a, b| {
4591                            if let Some(s) = crate::sketches::try_sketch_binop(
4592                                crate::sketches::SketchOp::Sub,
4593                                a,
4594                                b,
4595                            ) {
4596                                return Ok(s);
4597                            }
4598                            Ok(crate::value::compat_sub(a, b))
4599                        })
4600                    }
4601                    Op::Mul => {
4602                        let b = self.pop();
4603                        let a = self.pop();
4604                        self.push_binop_with_overload(BinOp::Mul, a, b, |a, b| {
4605                            Ok(crate::value::compat_mul(a, b))
4606                        })
4607                    }
4608                    Op::Div => {
4609                        let b = self.pop();
4610                        let a = self.pop();
4611                        let line = self.line();
4612                        self.push_binop_with_overload(BinOp::Div, a, b, |a, b| {
4613                            if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
4614                                if y == 0 {
4615                                    return Err(StrykeError::division_by_zero(
4616                                        "Illegal division by zero",
4617                                        line,
4618                                    ));
4619                                }
4620                                Ok(if x % y == 0 {
4621                                    StrykeValue::integer(x / y)
4622                                } else {
4623                                    StrykeValue::float(x as f64 / y as f64)
4624                                })
4625                            } else {
4626                                let d = b.to_number();
4627                                if d == 0.0 {
4628                                    return Err(StrykeError::division_by_zero(
4629                                        "Illegal division by zero",
4630                                        line,
4631                                    ));
4632                                }
4633                                Ok(StrykeValue::float(a.to_number() / d))
4634                            }
4635                        })
4636                    }
4637                    Op::Mod => {
4638                        let b = self.pop();
4639                        let a = self.pop();
4640                        let line = self.line();
4641                        self.push_binop_with_overload(BinOp::Mod, a, b, |a, b| {
4642                            let b = b.to_int();
4643                            let a = a.to_int();
4644                            if b == 0 {
4645                                return Err(StrykeError::division_by_zero(
4646                                    "Illegal modulus zero",
4647                                    line,
4648                                ));
4649                            }
4650                            Ok(StrykeValue::integer(crate::value::perl_mod_i64(a, b)))
4651                        })
4652                    }
4653                    Op::Pow => {
4654                        let b = self.pop();
4655                        let a = self.pop();
4656                        self.push_binop_with_overload(BinOp::Pow, a, b, |a, b| {
4657                            Ok(crate::value::compat_pow(a, b))
4658                        })
4659                    }
4660                    Op::Negate => {
4661                        let a = self.pop();
4662                        let line = self.line();
4663                        if let Some(exec_res) =
4664                            self.interp.try_overload_unary_dispatch("neg", &a, line)
4665                        {
4666                            self.push(vm_interp_result(exec_res, line)?);
4667                        } else {
4668                            self.push(if let Some(n) = a.as_integer() {
4669                                StrykeValue::integer(-n)
4670                            } else {
4671                                StrykeValue::float(-a.to_number())
4672                            });
4673                        }
4674                        Ok(())
4675                    }
4676                    Op::Inc => {
4677                        let a = self.pop();
4678                        self.push(if let Some(n) = a.as_integer() {
4679                            StrykeValue::integer(n.wrapping_add(1))
4680                        } else {
4681                            StrykeValue::float(a.to_number() + 1.0)
4682                        });
4683                        Ok(())
4684                    }
4685                    Op::Dec => {
4686                        let a = self.pop();
4687                        self.push(if let Some(n) = a.as_integer() {
4688                            StrykeValue::integer(n.wrapping_sub(1))
4689                        } else {
4690                            StrykeValue::float(a.to_number() - 1.0)
4691                        });
4692                        Ok(())
4693                    }
4694
4695                    // ── String ──
4696                    Op::Concat => {
4697                        let b = self.pop();
4698                        let a = self.pop();
4699                        let out = self.concat_stack_values(a, b)?;
4700                        self.push(out);
4701                        Ok(())
4702                    }
4703                    Op::ArrayStringifyListSep => {
4704                        let raw = self.pop();
4705                        let v = self.interp.peel_array_ref_for_list_join(raw);
4706                        let sep = self.interp.list_separator.clone();
4707                        let list = v.to_list();
4708                        let joined = list
4709                            .iter()
4710                            .map(|x| x.to_string())
4711                            .collect::<Vec<_>>()
4712                            .join(&sep);
4713                        self.push(StrykeValue::string(joined));
4714                        Ok(())
4715                    }
4716                    Op::StringRepeat => {
4717                        let n = self.pop().to_int();
4718                        let val = self.pop();
4719                        self.push(StrykeValue::string(val.repeat_value(n)));
4720                        Ok(())
4721                    }
4722                    Op::ListRepeat => {
4723                        let n = self.pop().to_int().max(0) as usize;
4724                        let val = self.pop();
4725                        // Flatten to a Vec<StrykeValue>: an array value gives its
4726                        // items; a scalar (e.g. `(0) x 5` after the LHS evaluates
4727                        // through scalar-collapse paths) wraps as a 1-elt list.
4728                        let items: Vec<StrykeValue> =
4729                            val.as_array_vec().unwrap_or_else(|| vec![val]);
4730                        let mut out = Vec::with_capacity(items.len().saturating_mul(n));
4731                        for _ in 0..n {
4732                            out.extend(items.iter().cloned());
4733                        }
4734                        self.push(StrykeValue::array(out));
4735                        Ok(())
4736                    }
4737                    Op::ProcessCaseEscapes => {
4738                        let val = self.pop();
4739                        let s = val.to_string();
4740                        let processed = VMHelper::process_case_escapes(&s);
4741                        self.push(StrykeValue::string(processed));
4742                        Ok(())
4743                    }
4744
4745                    // ── Numeric comparison ──
4746                    Op::NumEq => {
4747                        let b = self.pop();
4748                        let a = self.pop();
4749                        self.push_binop_with_overload(BinOp::NumEq, a.clone(), b.clone(), |a, b| {
4750                            // Struct equality: compare all fields
4751                            if let (Some(sa), Some(sb)) = (a.as_struct_inst(), b.as_struct_inst()) {
4752                                if sa.def.name != sb.def.name {
4753                                    return Ok(StrykeValue::integer(0));
4754                                }
4755                                let av = sa.get_values();
4756                                let bv = sb.get_values();
4757                                let eq = av.len() == bv.len()
4758                                    && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
4759                                Ok(StrykeValue::integer(if eq { 1 } else { 0 }))
4760                            } else {
4761                                if !crate::compat_mode() && both_non_numeric_strings(a, b) {
4762                                    let sa = a.to_string();
4763                                    let sb = b.to_string();
4764                                    return Ok(StrykeValue::integer(if sa == sb { 1 } else { 0 }));
4765                                }
4766                                Ok(int_cmp(a, b, |x, y| x == y, |x, y| x == y))
4767                            }
4768                        })
4769                    }
4770                    Op::NumNe => {
4771                        let b = self.pop();
4772                        let a = self.pop();
4773                        self.push_binop_with_overload(BinOp::NumNe, a, b, |a, b| {
4774                            // Stryke (non-compat) sugar: when both operands are
4775                            // non-numeric strings, fall back to `ne`. In Perl,
4776                            // `"G" != "T"` is `0 != 0` = false; in stryke we
4777                            // want char/string compare. Compat mode keeps
4778                            // Perl semantics.
4779                            if !crate::compat_mode() && both_non_numeric_strings(a, b) {
4780                                let sa = a.to_string();
4781                                let sb = b.to_string();
4782                                return Ok(StrykeValue::integer(if sa != sb { 1 } else { 0 }));
4783                            }
4784                            Ok(int_cmp(a, b, |x, y| x != y, |x, y| x != y))
4785                        })
4786                    }
4787                    Op::NumLt => {
4788                        let b = self.pop();
4789                        let a = self.pop();
4790                        self.push_binop_with_overload(BinOp::NumLt, a, b, |a, b| {
4791                            Ok(int_cmp(a, b, |x, y| x < y, |x, y| x < y))
4792                        })
4793                    }
4794                    Op::NumGt => {
4795                        let b = self.pop();
4796                        let a = self.pop();
4797                        self.push_binop_with_overload(BinOp::NumGt, a, b, |a, b| {
4798                            Ok(int_cmp(a, b, |x, y| x > y, |x, y| x > y))
4799                        })
4800                    }
4801                    Op::NumLe => {
4802                        let b = self.pop();
4803                        let a = self.pop();
4804                        self.push_binop_with_overload(BinOp::NumLe, a, b, |a, b| {
4805                            Ok(int_cmp(a, b, |x, y| x <= y, |x, y| x <= y))
4806                        })
4807                    }
4808                    Op::NumGe => {
4809                        let b = self.pop();
4810                        let a = self.pop();
4811                        self.push_binop_with_overload(BinOp::NumGe, a, b, |a, b| {
4812                            Ok(int_cmp(a, b, |x, y| x >= y, |x, y| x >= y))
4813                        })
4814                    }
4815                    Op::Spaceship => {
4816                        let b = self.pop();
4817                        let a = self.pop();
4818                        self.push_binop_with_overload(BinOp::Spaceship, a, b, |a, b| {
4819                            Ok(
4820                                if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
4821                                    StrykeValue::integer(if x < y {
4822                                        -1
4823                                    } else if x > y {
4824                                        1
4825                                    } else {
4826                                        0
4827                                    })
4828                                } else {
4829                                    let x = a.to_number();
4830                                    let y = b.to_number();
4831                                    StrykeValue::integer(if x < y {
4832                                        -1
4833                                    } else if x > y {
4834                                        1
4835                                    } else {
4836                                        0
4837                                    })
4838                                },
4839                            )
4840                        })
4841                    }
4842
4843                    // ── String comparison ──
4844                    Op::StrEq => {
4845                        let b = self.pop();
4846                        let a = self.pop();
4847                        self.push_binop_with_overload(BinOp::StrEq, a, b, |a, b| {
4848                            Ok(StrykeValue::integer(if a.str_eq(b) { 1 } else { 0 }))
4849                        })
4850                    }
4851                    Op::StrNe => {
4852                        let b = self.pop();
4853                        let a = self.pop();
4854                        self.push_binop_with_overload(BinOp::StrNe, a, b, |a, b| {
4855                            Ok(StrykeValue::integer(if !a.str_eq(b) { 1 } else { 0 }))
4856                        })
4857                    }
4858                    Op::StrLt => {
4859                        let b = self.pop();
4860                        let a = self.pop();
4861                        self.push_binop_with_overload(BinOp::StrLt, a, b, |a, b| {
4862                            Ok(StrykeValue::integer(
4863                                if a.str_cmp(b) == std::cmp::Ordering::Less {
4864                                    1
4865                                } else {
4866                                    0
4867                                },
4868                            ))
4869                        })
4870                    }
4871                    Op::StrGt => {
4872                        let b = self.pop();
4873                        let a = self.pop();
4874                        self.push_binop_with_overload(BinOp::StrGt, a, b, |a, b| {
4875                            Ok(StrykeValue::integer(
4876                                if a.str_cmp(b) == std::cmp::Ordering::Greater {
4877                                    1
4878                                } else {
4879                                    0
4880                                },
4881                            ))
4882                        })
4883                    }
4884                    Op::StrLe => {
4885                        let b = self.pop();
4886                        let a = self.pop();
4887                        self.push_binop_with_overload(BinOp::StrLe, a, b, |a, b| {
4888                            let o = a.str_cmp(b);
4889                            Ok(StrykeValue::integer(
4890                                if matches!(o, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
4891                                {
4892                                    1
4893                                } else {
4894                                    0
4895                                },
4896                            ))
4897                        })
4898                    }
4899                    Op::StrGe => {
4900                        let b = self.pop();
4901                        let a = self.pop();
4902                        self.push_binop_with_overload(BinOp::StrGe, a, b, |a, b| {
4903                            let o = a.str_cmp(b);
4904                            Ok(StrykeValue::integer(
4905                                if matches!(
4906                                    o,
4907                                    std::cmp::Ordering::Greater | std::cmp::Ordering::Equal
4908                                ) {
4909                                    1
4910                                } else {
4911                                    0
4912                                },
4913                            ))
4914                        })
4915                    }
4916                    Op::StrCmp => {
4917                        let b = self.pop();
4918                        let a = self.pop();
4919                        self.push_binop_with_overload(BinOp::StrCmp, a, b, |a, b| {
4920                            let cmp = a.str_cmp(b);
4921                            Ok(StrykeValue::integer(match cmp {
4922                                std::cmp::Ordering::Less => -1,
4923                                std::cmp::Ordering::Greater => 1,
4924                                std::cmp::Ordering::Equal => 0,
4925                            }))
4926                        })
4927                    }
4928
4929                    // ── Logical / Bitwise ──
4930                    Op::LogNot => {
4931                        let a = self.pop();
4932                        let line = self.line();
4933                        if let Some(exec_res) =
4934                            self.interp.try_overload_unary_dispatch("bool", &a, line)
4935                        {
4936                            let pv = vm_interp_result(exec_res, line)?;
4937                            self.push(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
4938                        } else {
4939                            self.push(StrykeValue::integer(if a.is_true() { 0 } else { 1 }));
4940                        }
4941                        Ok(())
4942                    }
4943                    Op::BitAnd => {
4944                        let rv = self.pop();
4945                        let lv = self.pop();
4946                        if let Some(s) = crate::value::set_intersection(&lv, &rv) {
4947                            self.push(s);
4948                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4949                            crate::sketches::SketchOp::And,
4950                            &lv,
4951                            &rv,
4952                        ) {
4953                            self.push(s);
4954                        } else {
4955                            self.push(StrykeValue::integer(lv.to_int() & rv.to_int()));
4956                        }
4957                        Ok(())
4958                    }
4959                    Op::BitOr => {
4960                        let rv = self.pop();
4961                        let lv = self.pop();
4962                        if let Some(s) = crate::value::set_union(&lv, &rv) {
4963                            self.push(s);
4964                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4965                            crate::sketches::SketchOp::Or,
4966                            &lv,
4967                            &rv,
4968                        ) {
4969                            self.push(s);
4970                        } else {
4971                            self.push(StrykeValue::integer(lv.to_int() | rv.to_int()));
4972                        }
4973                        Ok(())
4974                    }
4975                    Op::BitXor => {
4976                        let rv = self.pop();
4977                        let lv = self.pop();
4978                        if let Some(s) = crate::sketches::try_sketch_binop(
4979                            crate::sketches::SketchOp::Xor,
4980                            &lv,
4981                            &rv,
4982                        ) {
4983                            self.push(s);
4984                        } else {
4985                            self.push(StrykeValue::integer(lv.to_int() ^ rv.to_int()));
4986                        }
4987                        Ok(())
4988                    }
4989                    Op::BitNot => {
4990                        let a = self.pop().to_int();
4991                        self.push(StrykeValue::integer(!a));
4992                        Ok(())
4993                    }
4994                    Op::Shl => {
4995                        let b = self.pop().to_int();
4996                        let a = self.pop().to_int();
4997                        self.push(StrykeValue::integer(perl_shl_i64(a, b)));
4998                        Ok(())
4999                    }
5000                    Op::Shr => {
5001                        let b = self.pop().to_int();
5002                        let a = self.pop().to_int();
5003                        self.push(StrykeValue::integer(perl_shr_i64(a, b)));
5004                        Ok(())
5005                    }
5006
5007                    // ── Control flow ──
5008                    Op::Jump(target) => {
5009                        self.ip = *target;
5010                        Ok(())
5011                    }
5012                    Op::JumpIfTrue(target) => {
5013                        let val = self.pop();
5014                        if val.is_true() {
5015                            self.ip = *target;
5016                        }
5017                        Ok(())
5018                    }
5019                    Op::JumpIfFalse(target) => {
5020                        let val = self.pop();
5021                        if !val.is_true() {
5022                            self.ip = *target;
5023                        }
5024                        Ok(())
5025                    }
5026                    Op::JumpIfFalseKeep(target) => {
5027                        if !self.peek().is_true() {
5028                            self.ip = *target;
5029                        } else {
5030                            self.pop();
5031                        }
5032                        Ok(())
5033                    }
5034                    Op::JumpIfTrueKeep(target) => {
5035                        if self.peek().is_true() {
5036                            self.ip = *target;
5037                        } else {
5038                            self.pop();
5039                        }
5040                        Ok(())
5041                    }
5042                    Op::JumpIfDefinedKeep(target) => {
5043                        if !self.peek().is_undef() {
5044                            self.ip = *target;
5045                        } else {
5046                            self.pop();
5047                        }
5048                        Ok(())
5049                    }
5050
5051                    // ── Increment / Decrement ──
5052                    Op::PreInc(idx) => {
5053                        let n = names[*idx as usize].as_str();
5054                        self.require_scalar_mutable(n)?;
5055                        let en = self.interp.english_scalar_name(n);
5056                        let new_val = self
5057                            .interp
5058                            .scope
5059                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() + 1))
5060                            .map_err(|e| e.at_line(self.line()))?;
5061                        self.push(new_val);
5062                        Ok(())
5063                    }
5064                    Op::PreDec(idx) => {
5065                        let n = names[*idx as usize].as_str();
5066                        self.require_scalar_mutable(n)?;
5067                        let en = self.interp.english_scalar_name(n);
5068                        let new_val = self
5069                            .interp
5070                            .scope
5071                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() - 1))
5072                            .map_err(|e| e.at_line(self.line()))?;
5073                        self.push(new_val);
5074                        Ok(())
5075                    }
5076                    Op::PostInc(idx) => {
5077                        let n = names[*idx as usize].as_str();
5078                        self.require_scalar_mutable(n)?;
5079                        let en = self.interp.english_scalar_name(n);
5080                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5081                            self.interp
5082                                .scope
5083                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
5084                                .map_err(|e| e.at_line(self.line()))?;
5085                            self.ip += 1;
5086                        } else {
5087                            let old = self
5088                                .interp
5089                                .scope
5090                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
5091                                .map_err(|e| e.at_line(self.line()))?;
5092                            self.push(old);
5093                        }
5094                        Ok(())
5095                    }
5096                    Op::PostDec(idx) => {
5097                        let n = names[*idx as usize].as_str();
5098                        self.require_scalar_mutable(n)?;
5099                        let en = self.interp.english_scalar_name(n);
5100                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5101                            self.interp
5102                                .scope
5103                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
5104                                .map_err(|e| e.at_line(self.line()))?;
5105                            self.ip += 1;
5106                        } else {
5107                            let old = self
5108                                .interp
5109                                .scope
5110                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
5111                                .map_err(|e| e.at_line(self.line()))?;
5112                            self.push(old);
5113                        }
5114                        Ok(())
5115                    }
5116                    Op::PreIncSlot(slot) => {
5117                        let cur = self.interp.scope.get_scalar_slot(*slot);
5118                        let new_val = crate::vm_helper::perl_inc(&cur);
5119                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
5120                        self.push(new_val);
5121                        Ok(())
5122                    }
5123                    Op::PreIncSlotVoid(slot) => {
5124                        let cur = self.interp.scope.get_scalar_slot(*slot);
5125                        let new_val = crate::vm_helper::perl_inc(&cur);
5126                        self.interp.scope.set_scalar_slot(*slot, new_val);
5127                        Ok(())
5128                    }
5129                    Op::PreDecSlot(slot) => {
5130                        let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
5131                        let new_val = StrykeValue::integer(val);
5132                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
5133                        self.push(new_val);
5134                        Ok(())
5135                    }
5136                    Op::PostIncSlot(slot) => {
5137                        // Fuse PostIncSlot+Pop: if next op discards the old value, skip stack work.
5138                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5139                            let cur = self.interp.scope.get_scalar_slot(*slot);
5140                            let new_val = crate::vm_helper::perl_inc(&cur);
5141                            self.interp.scope.set_scalar_slot(*slot, new_val);
5142                            self.ip += 1; // skip Pop
5143                        } else {
5144                            let old = self.interp.scope.get_scalar_slot(*slot);
5145                            let new_val = crate::vm_helper::perl_inc(&old);
5146                            self.interp.scope.set_scalar_slot(*slot, new_val);
5147                            self.push(old);
5148                        }
5149                        Ok(())
5150                    }
5151                    Op::PostDecSlot(slot) => {
5152                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5153                            let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
5154                            self.interp
5155                                .scope
5156                                .set_scalar_slot(*slot, StrykeValue::integer(val));
5157                            self.ip += 1;
5158                        } else {
5159                            let old = self.interp.scope.get_scalar_slot(*slot);
5160                            let new_val = StrykeValue::integer(old.to_int() - 1);
5161                            self.interp.scope.set_scalar_slot(*slot, new_val);
5162                            self.push(old);
5163                        }
5164                        Ok(())
5165                    }
5166
5167                    // ── Functions ──
5168                    Op::Call(name_idx, argc, wa) => {
5169                        // A bare callable spelling (`sum`, `set`, `count`, …) routes
5170                        // to the global builtin even when a same-named user sub is
5171                        // registered. There is no shadowing of stryke builtins in
5172                        // default mode: user code can declare `fn sum {}` inside a
5173                        // non-main package, but the only way to reach that user sub
5174                        // is the fully-qualified `Pkg::sum(...)` spelling.
5175                        // `--compat` (full Perl 5 mode) restores classic UDF-wins
5176                        // semantics so unmodified Perl 5 modules keep working.
5177                        let name = &self.names[*name_idx as usize];
5178                        let entry_opt = if !crate::compat_mode()
5179                            && !name.contains("::")
5180                            && crate::builtins::is_callable_spelling(name)
5181                        {
5182                            None
5183                        } else {
5184                            self.find_sub_entry(*name_idx)
5185                        };
5186                        self.vm_dispatch_user_call(*name_idx, entry_opt, *argc, *wa, None)?;
5187                        Ok(())
5188                    }
5189                    Op::CallStaticSubId(sid, name_idx, argc, wa) => {
5190                        let t = self.static_sub_calls.get(*sid as usize).ok_or_else(|| {
5191                            StrykeError::runtime("VM: invalid CallStaticSubId", self.line())
5192                        })?;
5193                        debug_assert_eq!(t.2, *name_idx);
5194                        let closure_sub = self
5195                            .static_sub_closure_subs
5196                            .get(*sid as usize)
5197                            .and_then(|x| x.clone());
5198                        self.vm_dispatch_user_call(
5199                            *name_idx,
5200                            Some((t.0, t.1)),
5201                            *argc,
5202                            *wa,
5203                            closure_sub,
5204                        )?;
5205                        Ok(())
5206                    }
5207                    Op::Return => {
5208                        if let Some(frame) = self.call_stack.pop() {
5209                            if frame.block_region {
5210                                return Err(StrykeError::runtime(
5211                                    "Return in map/grep/sort block bytecode",
5212                                    self.line(),
5213                                ));
5214                            }
5215                            if let Some(t0) = frame.sub_profiler_start {
5216                                if let Some(p) = &mut self.interp.profiler {
5217                                    p.exit_sub(t0.elapsed());
5218                                }
5219                            }
5220                            self.interp.debugger_leave_sub();
5221                            self.interp.wantarray_kind = frame.saved_wantarray;
5222                            self.stack.truncate(frame.stack_base);
5223                            self.interp.pop_scope_to_depth(frame.scope_depth);
5224                            self.interp.current_sub_stack.pop();
5225                            if frame.jit_trampoline_return {
5226                                self.jit_trampoline_out = Some(StrykeValue::UNDEF);
5227                            } else {
5228                                self.push(StrykeValue::UNDEF);
5229                                self.ip = frame.return_ip;
5230                            }
5231                        } else {
5232                            self.exit_main_dispatch = true;
5233                        }
5234                        Ok(())
5235                    }
5236                    Op::ReturnValue => {
5237                        let val = self.pop();
5238                        // Resolve binding refs to real refs before scope cleanup.
5239                        // `\@array` creates a name-based ArrayBindingRef that looks
5240                        // up by name at dereference time.  If the array is a `my`
5241                        // variable, its frame will be destroyed below — so we must
5242                        // snapshot the data into an Arc-based ref now.
5243                        let val = self.resolve_binding_ref(val);
5244                        // Caller-context coercion: `return LIST` from a sub called
5245                        // in scalar context yields the **last** element of the
5246                        // list (Perl wantarray semantics). Without this, the
5247                        // whole list propagates and a `my $x = sub_returning_list()`
5248                        // sees the array stringified rather than its last element.
5249                        // (BUG-010 / BUG-011)
5250                        let val = if matches!(self.interp.wantarray_kind, WantarrayCtx::Scalar) {
5251                            if let Some(items) = val.as_array_vec() {
5252                                items.last().cloned().unwrap_or(StrykeValue::UNDEF)
5253                            } else {
5254                                val
5255                            }
5256                        } else {
5257                            val
5258                        };
5259                        if let Some(frame) = self.call_stack.pop() {
5260                            if frame.block_region {
5261                                return Err(StrykeError::runtime(
5262                                    "Return in map/grep/sort block bytecode",
5263                                    self.line(),
5264                                ));
5265                            }
5266                            if let Some(t0) = frame.sub_profiler_start {
5267                                if let Some(p) = &mut self.interp.profiler {
5268                                    p.exit_sub(t0.elapsed());
5269                                }
5270                            }
5271                            self.interp.debugger_leave_sub();
5272                            self.interp.wantarray_kind = frame.saved_wantarray;
5273                            self.stack.truncate(frame.stack_base);
5274                            self.interp.pop_scope_to_depth(frame.scope_depth);
5275                            self.interp.current_sub_stack.pop();
5276                            if frame.jit_trampoline_return {
5277                                self.jit_trampoline_out = Some(val);
5278                            } else {
5279                                self.push(val);
5280                                self.ip = frame.return_ip;
5281                            }
5282                        } else {
5283                            self.exit_main_dispatch_value = Some(val);
5284                            self.exit_main_dispatch = true;
5285                        }
5286                        Ok(())
5287                    }
5288                    Op::BlockReturnValue => {
5289                        let val = self.pop();
5290                        let val = self.resolve_binding_ref(val);
5291                        if let Some(frame) = self.call_stack.pop() {
5292                            if !frame.block_region {
5293                                return Err(StrykeError::runtime(
5294                                    "BlockReturnValue without map/grep/sort block frame",
5295                                    self.line(),
5296                                ));
5297                            }
5298                            self.interp.wantarray_kind = frame.saved_wantarray;
5299                            self.stack.truncate(frame.stack_base);
5300                            self.interp.pop_scope_to_depth(frame.scope_depth);
5301                            self.block_region_return = Some(val);
5302                            Ok(())
5303                        } else {
5304                            Err(StrykeError::runtime(
5305                                "BlockReturnValue with empty call stack",
5306                                self.line(),
5307                            ))
5308                        }
5309                    }
5310                    Op::BindSubClosure(name_idx) => {
5311                        let n = names[*name_idx as usize].as_str();
5312                        self.interp.rebind_sub_closure(n);
5313                        Ok(())
5314                    }
5315
5316                    // ── Scope ──
5317                    Op::PushFrame => {
5318                        self.interp.scope_push_hook();
5319                        Ok(())
5320                    }
5321                    Op::PopFrame => {
5322                        self.interp.scope_pop_hook();
5323                        Ok(())
5324                    }
5325                    // ── I/O ──
5326                    Op::Print(handle_idx, argc) => {
5327                        let argc = *argc as usize;
5328                        let mut args = Vec::with_capacity(argc);
5329                        for _ in 0..argc {
5330                            args.push(self.pop());
5331                        }
5332                        args.reverse();
5333                        let mut output = String::new();
5334                        if args.is_empty() {
5335                            let topic = self.interp.scope.get_scalar("_").clone();
5336                            let s = match self.interp.stringify_value(topic, self.line()) {
5337                                Ok(s) => s,
5338                                Err(FlowOrError::Error(e)) => return Err(e),
5339                                Err(FlowOrError::Flow(_)) => {
5340                                    return Err(StrykeError::runtime(
5341                                        "print: unexpected control flow",
5342                                        self.line(),
5343                                    ));
5344                                }
5345                            };
5346                            output.push_str(&s);
5347                        } else {
5348                            for (i, arg) in args.iter().enumerate() {
5349                                if i > 0 && !self.interp.ofs.is_empty() {
5350                                    output.push_str(&self.interp.ofs);
5351                                }
5352                                for item in arg.to_list() {
5353                                    let s = match self.interp.stringify_value(item, self.line()) {
5354                                        Ok(s) => s,
5355                                        Err(FlowOrError::Error(e)) => return Err(e),
5356                                        Err(FlowOrError::Flow(_)) => {
5357                                            return Err(StrykeError::runtime(
5358                                                "print: unexpected control flow",
5359                                                self.line(),
5360                                            ));
5361                                        }
5362                                    };
5363                                    output.push_str(&s);
5364                                }
5365                            }
5366                        }
5367                        output.push_str(&self.interp.ors);
5368                        let handle_name = match handle_idx {
5369                            Some(idx) => self.interp.resolve_io_handle_name(
5370                                self.names
5371                                    .get(*idx as usize)
5372                                    .map_or("STDOUT", |s| s.as_str()),
5373                            ),
5374                            None => self
5375                                .interp
5376                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
5377                        };
5378                        self.interp.write_formatted_print(
5379                            handle_name.as_str(),
5380                            &output,
5381                            self.line(),
5382                        )?;
5383                        self.push(StrykeValue::integer(1));
5384                        Ok(())
5385                    }
5386                    Op::Printf(handle_idx, argc) => {
5387                        let argc = *argc as usize;
5388                        let mut args = Vec::with_capacity(argc);
5389                        for _ in 0..argc {
5390                            args.push(self.pop());
5391                        }
5392                        args.reverse();
5393                        let (fmt, rest) = match args.split_first() {
5394                            Some((f, r)) => (f.to_string(), r),
5395                            None => {
5396                                return Err(StrykeError::runtime(
5397                                    "printf requires a format string",
5398                                    self.line(),
5399                                ));
5400                            }
5401                        };
5402                        // sprintf the args, then route through the handle the
5403                        // same way Print does — fixes printf's silent
5404                        // misdirection to STDOUT.
5405                        let mut flat = Vec::new();
5406                        for a in rest {
5407                            if let Some(items) = a.as_array_vec() {
5408                                flat.extend(items);
5409                            } else {
5410                                flat.push(a.clone());
5411                            }
5412                        }
5413                        let s = match self.interp.perl_sprintf_stringify(&fmt, &flat, self.line()) {
5414                            Ok(s) => s,
5415                            Err(FlowOrError::Error(e)) => return Err(e),
5416                            Err(FlowOrError::Flow(_)) => {
5417                                return Err(StrykeError::runtime(
5418                                    "printf: unexpected control flow",
5419                                    self.line(),
5420                                ));
5421                            }
5422                        };
5423                        let handle_name = match handle_idx {
5424                            Some(idx) => self.interp.resolve_io_handle_name(
5425                                self.names
5426                                    .get(*idx as usize)
5427                                    .map_or("STDOUT", |s| s.as_str()),
5428                            ),
5429                            None => self
5430                                .interp
5431                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
5432                        };
5433                        self.interp
5434                            .write_formatted_print(handle_name.as_str(), &s, self.line())?;
5435                        self.push(StrykeValue::integer(1));
5436                        Ok(())
5437                    }
5438                    Op::Say(handle_idx, argc) => {
5439                        if (self.interp.feature_bits & crate::vm_helper::FEAT_SAY) == 0 {
5440                            return Err(StrykeError::runtime(
5441                            "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
5442                            self.line(),
5443                        ));
5444                        }
5445                        let argc = *argc as usize;
5446                        let mut args = Vec::with_capacity(argc);
5447                        for _ in 0..argc {
5448                            args.push(self.pop());
5449                        }
5450                        args.reverse();
5451                        let mut output = String::new();
5452                        if args.is_empty() {
5453                            let topic = self.interp.scope.get_scalar("_").clone();
5454                            let s = match self.interp.stringify_value(topic, self.line()) {
5455                                Ok(s) => s,
5456                                Err(FlowOrError::Error(e)) => return Err(e),
5457                                Err(FlowOrError::Flow(_)) => {
5458                                    return Err(StrykeError::runtime(
5459                                        "say: unexpected control flow",
5460                                        self.line(),
5461                                    ));
5462                                }
5463                            };
5464                            output.push_str(&s);
5465                        } else {
5466                            for (i, arg) in args.iter().enumerate() {
5467                                if i > 0 && !self.interp.ofs.is_empty() {
5468                                    output.push_str(&self.interp.ofs);
5469                                }
5470                                for item in arg.to_list() {
5471                                    let s = match self.interp.stringify_value(item, self.line()) {
5472                                        Ok(s) => s,
5473                                        Err(FlowOrError::Error(e)) => return Err(e),
5474                                        Err(FlowOrError::Flow(_)) => {
5475                                            return Err(StrykeError::runtime(
5476                                                "say: unexpected control flow",
5477                                                self.line(),
5478                                            ));
5479                                        }
5480                                    };
5481                                    output.push_str(&s);
5482                                }
5483                            }
5484                        }
5485                        output.push('\n');
5486                        output.push_str(&self.interp.ors);
5487                        let handle_name = match handle_idx {
5488                            Some(idx) => self.interp.resolve_io_handle_name(
5489                                self.names
5490                                    .get(*idx as usize)
5491                                    .map_or("STDOUT", |s| s.as_str()),
5492                            ),
5493                            None => self
5494                                .interp
5495                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
5496                        };
5497                        self.interp.write_formatted_print(
5498                            handle_name.as_str(),
5499                            &output,
5500                            self.line(),
5501                        )?;
5502                        self.push(StrykeValue::integer(1));
5503                        Ok(())
5504                    }
5505
5506                    // ── Built-in dispatch ──
5507                    Op::CallBuiltin(id, argc) => {
5508                        let argc = *argc as usize;
5509                        let mut args = Vec::with_capacity(argc);
5510                        for _ in 0..argc {
5511                            args.push(self.pop());
5512                        }
5513                        args.reverse();
5514                        let result = self.exec_builtin(*id, args)?;
5515                        self.push(result);
5516                        Ok(())
5517                    }
5518                    Op::WantarrayPush(wa) => {
5519                        self.wantarray_stack.push(self.interp.wantarray_kind);
5520                        self.interp.wantarray_kind = WantarrayCtx::from_byte(*wa);
5521                        Ok(())
5522                    }
5523                    Op::WantarrayPop => {
5524                        self.interp.wantarray_kind =
5525                            self.wantarray_stack.pop().unwrap_or(WantarrayCtx::Scalar);
5526                        Ok(())
5527                    }
5528
5529                    // ── List / Range ──
5530                    Op::MakeArray(n) => {
5531                        let n = *n as usize;
5532                        // Pops are last-to-first on the stack; reverse to source (left-to-right) order,
5533                        // then flatten nested arrays in place (Perl list literal semantics).
5534                        // Hashes flatten to alternating key/value entries — Perl's
5535                        // `(%a, %b)` splat-merge idiom relies on this; without it
5536                        // each hash collapses to its scalar bucket-fill string.
5537                        let mut stack_vals = Vec::with_capacity(n);
5538                        for _ in 0..n {
5539                            stack_vals.push(self.pop());
5540                        }
5541                        stack_vals.reverse();
5542                        let mut arr = Vec::new();
5543                        for v in stack_vals {
5544                            if let Some(items) = v.as_array_vec() {
5545                                arr.extend(items);
5546                            } else if let Some(map) = v.as_hash_map() {
5547                                for (k, vv) in map {
5548                                    arr.push(StrykeValue::string(k));
5549                                    arr.push(vv);
5550                                }
5551                            } else {
5552                                arr.push(v);
5553                            }
5554                        }
5555                        self.push(StrykeValue::array(arr));
5556                        Ok(())
5557                    }
5558                    Op::HashSliceDeref(n) => {
5559                        let n = *n as usize;
5560                        let mut key_vals = Vec::with_capacity(n);
5561                        for _ in 0..n {
5562                            key_vals.push(self.pop());
5563                        }
5564                        key_vals.reverse();
5565                        let container = self.pop();
5566                        let line = self.line();
5567                        let out = vm_interp_result(
5568                            self.interp
5569                                .hash_slice_deref_values(&container, &key_vals, line),
5570                            line,
5571                        )?;
5572                        self.push(out);
5573                        Ok(())
5574                    }
5575                    Op::ArrowArraySlice(n) => {
5576                        let n = *n as usize;
5577                        let idxs = self.pop_flattened_array_slice_specs(n);
5578                        let r = self.pop();
5579                        let line = self.line();
5580                        let out = vm_interp_result(
5581                            self.interp.arrow_array_slice_values(r, &idxs, line),
5582                            line,
5583                        )?;
5584                        self.push(out);
5585                        Ok(())
5586                    }
5587                    Op::SetHashSliceDeref(n) => {
5588                        let n = *n as usize;
5589                        let mut key_vals = Vec::with_capacity(n);
5590                        for _ in 0..n {
5591                            key_vals.push(self.pop());
5592                        }
5593                        key_vals.reverse();
5594                        let container = self.pop();
5595                        let val = self.pop();
5596                        let line = self.line();
5597                        vm_interp_result(
5598                            self.interp
5599                                .assign_hash_slice_deref(container, key_vals, val, line),
5600                            line,
5601                        )?;
5602                        Ok(())
5603                    }
5604                    Op::SetHashSlice(hash_idx, n) => {
5605                        let n = *n as usize;
5606                        let mut key_vals = Vec::with_capacity(n);
5607                        for _ in 0..n {
5608                            key_vals.push(self.pop());
5609                        }
5610                        key_vals.reverse();
5611                        let name = names[*hash_idx as usize].as_str();
5612                        self.require_hash_mutable(name)?;
5613                        let val = self.pop();
5614                        let line = self.line();
5615                        vm_interp_result(
5616                            self.interp
5617                                .assign_named_hash_slice(name, key_vals, val, line),
5618                            line,
5619                        )?;
5620                        Ok(())
5621                    }
5622                    Op::GetHashSlice(hash_idx, n) => {
5623                        let n = *n as usize;
5624                        let mut key_vals = Vec::with_capacity(n);
5625                        for _ in 0..n {
5626                            key_vals.push(self.pop());
5627                        }
5628                        key_vals.reverse();
5629                        let name = names[*hash_idx as usize].as_str();
5630                        let h = self.interp.scope.get_hash(name);
5631                        let mut result = Vec::new();
5632                        for kv in &key_vals {
5633                            // Flatten arrays AND arrayrefs (e.g. `@h{@$kref}`)
5634                            // — both shapes can carry the keys list.
5635                            if let Some(vv) = kv.as_array_vec() {
5636                                for v in vv {
5637                                    let k = v.to_string();
5638                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
5639                                }
5640                            } else if let Some(r) = kv.as_array_ref() {
5641                                for v in r.read().iter() {
5642                                    let k = v.to_string();
5643                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
5644                                }
5645                            } else {
5646                                let k = kv.to_string();
5647                                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
5648                            }
5649                        }
5650                        self.push(StrykeValue::array(result));
5651                        Ok(())
5652                    }
5653                    Op::HashSliceDerefCompound(op_byte, n) => {
5654                        let n = *n as usize;
5655                        let mut key_vals = Vec::with_capacity(n);
5656                        for _ in 0..n {
5657                            key_vals.push(self.pop());
5658                        }
5659                        key_vals.reverse();
5660                        let container = self.pop();
5661                        let rhs = self.pop();
5662                        let line = self.line();
5663                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5664                            .ok_or_else(|| {
5665                                crate::error::StrykeError::runtime(
5666                                    "VM: HashSliceDerefCompound: bad op byte",
5667                                    line,
5668                                )
5669                            })?;
5670                        let new_val = vm_interp_result(
5671                            self.interp.compound_assign_hash_slice_deref(
5672                                container, key_vals, op, rhs, line,
5673                            ),
5674                            line,
5675                        )?;
5676                        self.push(new_val);
5677                        Ok(())
5678                    }
5679                    Op::HashSliceDerefIncDec(kind, n) => {
5680                        let n = *n as usize;
5681                        let mut key_vals = Vec::with_capacity(n);
5682                        for _ in 0..n {
5683                            key_vals.push(self.pop());
5684                        }
5685                        key_vals.reverse();
5686                        let container = self.pop();
5687                        let line = self.line();
5688                        let out = vm_interp_result(
5689                            self.interp
5690                                .hash_slice_deref_inc_dec(container, key_vals, *kind, line),
5691                            line,
5692                        )?;
5693                        self.push(out);
5694                        Ok(())
5695                    }
5696                    Op::NamedHashSliceCompound(op_byte, hash_idx, n) => {
5697                        let n = *n as usize;
5698                        let mut key_vals = Vec::with_capacity(n);
5699                        for _ in 0..n {
5700                            key_vals.push(self.pop());
5701                        }
5702                        key_vals.reverse();
5703                        let name = names[*hash_idx as usize].as_str();
5704                        self.require_hash_mutable(name)?;
5705                        let rhs = self.pop();
5706                        let line = self.line();
5707                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5708                            .ok_or_else(|| {
5709                                crate::error::StrykeError::runtime(
5710                                    "VM: NamedHashSliceCompound: bad op byte",
5711                                    line,
5712                                )
5713                            })?;
5714                        let new_val = vm_interp_result(
5715                            self.interp
5716                                .compound_assign_named_hash_slice(name, key_vals, op, rhs, line),
5717                            line,
5718                        )?;
5719                        self.push(new_val);
5720                        Ok(())
5721                    }
5722                    Op::NamedHashSliceIncDec(kind, hash_idx, n) => {
5723                        let n = *n as usize;
5724                        let mut key_vals = Vec::with_capacity(n);
5725                        for _ in 0..n {
5726                            key_vals.push(self.pop());
5727                        }
5728                        key_vals.reverse();
5729                        let name = names[*hash_idx as usize].as_str();
5730                        self.require_hash_mutable(name)?;
5731                        let line = self.line();
5732                        let out = vm_interp_result(
5733                            self.interp
5734                                .named_hash_slice_inc_dec(name, key_vals, *kind, line),
5735                            line,
5736                        )?;
5737                        self.push(out);
5738                        Ok(())
5739                    }
5740                    Op::NamedHashSlicePeekLast(hash_idx, n) => {
5741                        let n = *n as usize;
5742                        let line = self.line();
5743                        let name = names[*hash_idx as usize].as_str();
5744                        self.require_hash_mutable(name)?;
5745                        let len = self.stack.len();
5746                        if len < n {
5747                            return Err(StrykeError::runtime(
5748                                "VM: NamedHashSlicePeekLast: stack underflow",
5749                                line,
5750                            ));
5751                        }
5752                        let base = len - n;
5753                        let key_vals: Vec<StrykeValue> = self.stack[base..base + n].to_vec();
5754                        let ks = Self::flatten_hash_slice_key_slots(&key_vals);
5755                        let last_k = ks.last().ok_or_else(|| {
5756                            StrykeError::runtime("VM: NamedHashSlicePeekLast: empty key list", line)
5757                        })?;
5758                        self.interp.touch_env_hash(name);
5759                        let cur = self.interp.scope.get_hash_element(name, last_k.as_str());
5760                        self.push(cur);
5761                        Ok(())
5762                    }
5763                    Op::NamedHashSliceDropKeysKeepCur(n) => {
5764                        let n = *n as usize;
5765                        let cur = self.pop();
5766                        for _ in 0..n {
5767                            self.pop();
5768                        }
5769                        self.push(cur);
5770                        Ok(())
5771                    }
5772                    Op::SetNamedHashSliceLastKeep(hash_idx, n) => {
5773                        let n = *n as usize;
5774                        let line = self.line();
5775                        let name = names[*hash_idx as usize].as_str();
5776                        self.require_hash_mutable(name)?;
5777                        let mut key_vals_rev = Vec::with_capacity(n);
5778                        for _ in 0..n {
5779                            key_vals_rev.push(self.pop());
5780                        }
5781                        key_vals_rev.reverse();
5782                        let mut val = self.pop();
5783                        if let Some(av) = val.as_array_vec() {
5784                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5785                        }
5786                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
5787                        let last_k = ks.last().ok_or_else(|| {
5788                            StrykeError::runtime(
5789                                "VM: SetNamedHashSliceLastKeep: empty key list",
5790                                line,
5791                            )
5792                        })?;
5793                        let val_keep = val.clone();
5794                        self.interp.touch_env_hash(name);
5795                        vm_interp_result(
5796                            self.interp
5797                                .scope
5798                                .set_hash_element(name, last_k.as_str(), val)
5799                                .map(|()| StrykeValue::UNDEF)
5800                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
5801                            line,
5802                        )?;
5803                        self.push(val_keep);
5804                        Ok(())
5805                    }
5806                    Op::HashSliceDerefPeekLast(n) => {
5807                        let n = *n as usize;
5808                        let line = self.line();
5809                        let len = self.stack.len();
5810                        if len < n + 1 {
5811                            return Err(StrykeError::runtime(
5812                                "VM: HashSliceDerefPeekLast: stack underflow",
5813                                line,
5814                            ));
5815                        }
5816                        let base = len - n - 1;
5817                        let container = self.stack[base].clone();
5818                        let key_vals: Vec<StrykeValue> =
5819                            self.stack[base + 1..base + 1 + n].to_vec();
5820                        let list = vm_interp_result(
5821                            self.interp
5822                                .hash_slice_deref_values(&container, &key_vals, line),
5823                            line,
5824                        )?;
5825                        let cur = list.to_list().last().cloned().unwrap_or(StrykeValue::UNDEF);
5826                        self.push(cur);
5827                        Ok(())
5828                    }
5829                    Op::HashSliceDerefRollValUnderKeys(n) => {
5830                        let n = *n as usize;
5831                        let val = self.pop();
5832                        let mut keys_rev = Vec::with_capacity(n);
5833                        for _ in 0..n {
5834                            keys_rev.push(self.pop());
5835                        }
5836                        let container = self.pop();
5837                        keys_rev.reverse();
5838                        self.push(val);
5839                        self.push(container);
5840                        for k in keys_rev {
5841                            self.push(k);
5842                        }
5843                        Ok(())
5844                    }
5845                    Op::HashSliceDerefSetLastKeep(n) => {
5846                        let n = *n as usize;
5847                        let line = self.line();
5848                        let mut key_vals_rev = Vec::with_capacity(n);
5849                        for _ in 0..n {
5850                            key_vals_rev.push(self.pop());
5851                        }
5852                        key_vals_rev.reverse();
5853                        let container = self.pop();
5854                        let mut val = self.pop();
5855                        if let Some(av) = val.as_array_vec() {
5856                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5857                        }
5858                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
5859                        let last_k = ks.last().ok_or_else(|| {
5860                            StrykeError::runtime(
5861                                "VM: HashSliceDerefSetLastKeep: empty key list",
5862                                line,
5863                            )
5864                        })?;
5865                        let val_keep = val.clone();
5866                        vm_interp_result(
5867                            self.interp.assign_hash_slice_one_key(
5868                                container,
5869                                last_k.as_str(),
5870                                val,
5871                                line,
5872                            ),
5873                            line,
5874                        )?;
5875                        self.push(val_keep);
5876                        Ok(())
5877                    }
5878                    Op::HashSliceDerefDropKeysKeepCur(n) => {
5879                        let n = *n as usize;
5880                        let cur = self.pop();
5881                        for _ in 0..n {
5882                            self.pop();
5883                        }
5884                        let _container = self.pop();
5885                        self.push(cur);
5886                        Ok(())
5887                    }
5888                    Op::SetArrowArraySlice(n) => {
5889                        let n = *n as usize;
5890                        let idxs = self.pop_flattened_array_slice_specs(n);
5891                        let aref = self.pop();
5892                        let val = self.pop();
5893                        let line = self.line();
5894                        vm_interp_result(
5895                            self.interp.assign_arrow_array_slice(aref, idxs, val, line),
5896                            line,
5897                        )?;
5898                        Ok(())
5899                    }
5900                    Op::ArrowArraySliceCompound(op_byte, n) => {
5901                        let n = *n as usize;
5902                        let idxs = self.pop_flattened_array_slice_specs(n);
5903                        let aref = self.pop();
5904                        let rhs = self.pop();
5905                        let line = self.line();
5906                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5907                            .ok_or_else(|| {
5908                                crate::error::StrykeError::runtime(
5909                                    "VM: ArrowArraySliceCompound: bad op byte",
5910                                    line,
5911                                )
5912                            })?;
5913                        let new_val = vm_interp_result(
5914                            self.interp
5915                                .compound_assign_arrow_array_slice(aref, idxs, op, rhs, line),
5916                            line,
5917                        )?;
5918                        self.push(new_val);
5919                        Ok(())
5920                    }
5921                    Op::ArrowArraySliceIncDec(kind, n) => {
5922                        let n = *n as usize;
5923                        let idxs = self.pop_flattened_array_slice_specs(n);
5924                        let aref = self.pop();
5925                        let line = self.line();
5926                        let out = vm_interp_result(
5927                            self.interp
5928                                .arrow_array_slice_inc_dec(aref, idxs, *kind, line),
5929                            line,
5930                        )?;
5931                        self.push(out);
5932                        Ok(())
5933                    }
5934                    Op::ArrowArraySlicePeekLast(n) => {
5935                        let n = *n as usize;
5936                        let line = self.line();
5937                        let len = self.stack.len();
5938                        if len < n + 1 {
5939                            return Err(StrykeError::runtime(
5940                                "VM: ArrowArraySlicePeekLast: stack underflow",
5941                                line,
5942                            ));
5943                        }
5944                        let base = len - n - 1;
5945                        let aref = self.stack[base].clone();
5946                        let idxs =
5947                            self.flatten_array_slice_specs_ordered_values(&self.stack[base + 1..])?;
5948                        let last = *idxs.last().ok_or_else(|| {
5949                            StrykeError::runtime(
5950                                "VM: ArrowArraySlicePeekLast: empty index list",
5951                                line,
5952                            )
5953                        })?;
5954                        let cur = vm_interp_result(
5955                            self.interp.read_arrow_array_element(aref, last, line),
5956                            line,
5957                        )?;
5958                        self.push(cur);
5959                        Ok(())
5960                    }
5961                    Op::ArrowArraySliceDropKeysKeepCur(n) => {
5962                        let n = *n as usize;
5963                        let cur = self.pop();
5964                        let _idxs = self.pop_flattened_array_slice_specs(n);
5965                        let _aref = self.pop();
5966                        self.push(cur);
5967                        Ok(())
5968                    }
5969                    Op::ArrowArraySliceRollValUnderSpecs(n) => {
5970                        let n = *n as usize;
5971                        let val = self.pop();
5972                        let mut specs_rev = Vec::with_capacity(n);
5973                        for _ in 0..n {
5974                            specs_rev.push(self.pop());
5975                        }
5976                        let aref = self.pop();
5977                        self.push(val);
5978                        self.push(aref);
5979                        for s in specs_rev.into_iter().rev() {
5980                            self.push(s);
5981                        }
5982                        Ok(())
5983                    }
5984                    Op::SetArrowArraySliceLastKeep(n) => {
5985                        let n = *n as usize;
5986                        let line = self.line();
5987                        let idxs = self.pop_flattened_array_slice_specs(n);
5988                        let aref = self.pop();
5989                        let mut val = self.pop();
5990                        // RHS is compiled in list context (`(3,4)` → one array value); Perl assigns
5991                        // only the **last** list element to the last slice index (`||=` / `&&=` / `//=`).
5992                        if let Some(av) = val.as_array_vec() {
5993                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5994                        }
5995                        let last = *idxs.last().ok_or_else(|| {
5996                            StrykeError::runtime(
5997                                "VM: SetArrowArraySliceLastKeep: empty index list",
5998                                line,
5999                            )
6000                        })?;
6001                        let val_keep = val.clone();
6002                        vm_interp_result(
6003                            self.interp.assign_arrow_array_deref(aref, last, val, line),
6004                            line,
6005                        )?;
6006                        self.push(val_keep);
6007                        Ok(())
6008                    }
6009                    Op::NamedArraySliceIncDec(kind, arr_idx, n) => {
6010                        let n = *n as usize;
6011                        let idxs = self.pop_flattened_array_slice_specs(n);
6012                        let name = names[*arr_idx as usize].as_str();
6013                        self.require_array_mutable(name)?;
6014                        let line = self.line();
6015                        let out = vm_interp_result(
6016                            self.interp
6017                                .named_array_slice_inc_dec(name, idxs, *kind, line),
6018                            line,
6019                        )?;
6020                        self.push(out);
6021                        Ok(())
6022                    }
6023                    Op::NamedArraySliceCompound(op_byte, arr_idx, n) => {
6024                        let n = *n as usize;
6025                        let idxs = self.pop_flattened_array_slice_specs(n);
6026                        let name = names[*arr_idx as usize].as_str();
6027                        self.require_array_mutable(name)?;
6028                        let rhs = self.pop();
6029                        let line = self.line();
6030                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
6031                            .ok_or_else(|| {
6032                                crate::error::StrykeError::runtime(
6033                                    "VM: NamedArraySliceCompound: bad op byte",
6034                                    line,
6035                                )
6036                            })?;
6037                        let new_val = vm_interp_result(
6038                            self.interp
6039                                .compound_assign_named_array_slice(name, idxs, op, rhs, line),
6040                            line,
6041                        )?;
6042                        self.push(new_val);
6043                        Ok(())
6044                    }
6045                    Op::NamedArraySlicePeekLast(arr_idx, n) => {
6046                        let n = *n as usize;
6047                        let line = self.line();
6048                        let name = names[*arr_idx as usize].as_str();
6049                        self.require_array_mutable(name)?;
6050                        let len = self.stack.len();
6051                        if len < n {
6052                            return Err(StrykeError::runtime(
6053                                "VM: NamedArraySlicePeekLast: stack underflow",
6054                                line,
6055                            ));
6056                        }
6057                        let base = len - n;
6058                        let idxs =
6059                            self.flatten_array_slice_specs_ordered_values(&self.stack[base..])?;
6060                        let last = *idxs.last().ok_or_else(|| {
6061                            StrykeError::runtime(
6062                                "VM: NamedArraySlicePeekLast: empty index list",
6063                                line,
6064                            )
6065                        })?;
6066                        let cur = self.interp.scope.get_array_element(name, last);
6067                        self.push(cur);
6068                        Ok(())
6069                    }
6070                    Op::NamedArraySliceDropKeysKeepCur(n) => {
6071                        let n = *n as usize;
6072                        let cur = self.pop();
6073                        let _idxs = self.pop_flattened_array_slice_specs(n);
6074                        self.push(cur);
6075                        Ok(())
6076                    }
6077                    Op::NamedArraySliceRollValUnderSpecs(n) => {
6078                        let n = *n as usize;
6079                        let val = self.pop();
6080                        let mut specs_rev = Vec::with_capacity(n);
6081                        for _ in 0..n {
6082                            specs_rev.push(self.pop());
6083                        }
6084                        self.push(val);
6085                        for s in specs_rev.into_iter().rev() {
6086                            self.push(s);
6087                        }
6088                        Ok(())
6089                    }
6090                    Op::SetNamedArraySliceLastKeep(arr_idx, n) => {
6091                        let n = *n as usize;
6092                        let line = self.line();
6093                        let idxs = self.pop_flattened_array_slice_specs(n);
6094                        let name = names[*arr_idx as usize].as_str();
6095                        self.require_array_mutable(name)?;
6096                        let mut val = self.pop();
6097                        if let Some(av) = val.as_array_vec() {
6098                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
6099                        }
6100                        let last = *idxs.last().ok_or_else(|| {
6101                            StrykeError::runtime(
6102                                "VM: SetNamedArraySliceLastKeep: empty index list",
6103                                line,
6104                            )
6105                        })?;
6106                        let val_keep = val.clone();
6107                        vm_interp_result(
6108                            self.interp
6109                                .scope
6110                                .set_array_element(name, last, val)
6111                                .map(|()| StrykeValue::UNDEF)
6112                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
6113                            line,
6114                        )?;
6115                        self.push(val_keep);
6116                        Ok(())
6117                    }
6118                    Op::SetNamedArraySlice(arr_idx, n) => {
6119                        let n = *n as usize;
6120                        let idxs = self.pop_flattened_array_slice_specs(n);
6121                        let name = names[*arr_idx as usize].as_str();
6122                        self.require_array_mutable(name)?;
6123                        let val = self.pop();
6124                        let line = self.line();
6125                        vm_interp_result(
6126                            self.interp.assign_named_array_slice(name, idxs, val, line),
6127                            line,
6128                        )?;
6129                        Ok(())
6130                    }
6131                    Op::MakeHash(n) => {
6132                        let n = *n as usize;
6133                        let mut items = Vec::with_capacity(n);
6134                        for _ in 0..n {
6135                            items.push(self.pop());
6136                        }
6137                        items.reverse();
6138                        let mut map = IndexMap::new();
6139                        let mut i = 0;
6140                        while i + 1 < items.len() {
6141                            map.insert(items[i].to_string(), items[i + 1].clone());
6142                            i += 2;
6143                        }
6144                        self.push(StrykeValue::hash(map));
6145                        Ok(())
6146                    }
6147                    Op::Range => {
6148                        let to = self.pop();
6149                        let from = self.pop();
6150                        let arr = perl_list_range_expand(from, to);
6151                        self.push(StrykeValue::array(arr));
6152                        Ok(())
6153                    }
6154                    Op::RangeStep => {
6155                        let step = self.pop();
6156                        let to = self.pop();
6157                        let from = self.pop();
6158                        let arr = crate::value::perl_list_range_expand_stepped(from, to, step);
6159                        self.push(StrykeValue::array(arr));
6160                        Ok(())
6161                    }
6162                    Op::ArraySliceRange(arr_idx) => {
6163                        let step = self.pop();
6164                        let to = self.pop();
6165                        let from = self.pop();
6166                        let line = self.line();
6167                        let name = names[*arr_idx as usize].as_str();
6168                        // Stryke topic-string slice: `_[from:to:step]` parses
6169                        // to ArraySliceRange on a `__topicstr__N` name.
6170                        if let Some(real) = name.strip_prefix("__topicstr__") {
6171                            let s = self.interp.scope.get_scalar(real).to_string();
6172                            let chars: Vec<char> = s.chars().collect();
6173                            let n = chars.len() as i64;
6174                            let step_i = if step.is_undef() { 1 } else { step.to_int() };
6175                            // Open slices: defaults depend on step direction
6176                            // step > 0: from=0, to=n-1 (forward)
6177                            // step < 0: from=n-1, to=0 (backward)
6178                            let mut from_i = if from.is_undef() {
6179                                if step_i >= 0 {
6180                                    0
6181                                } else {
6182                                    n - 1
6183                                }
6184                            } else {
6185                                from.to_int()
6186                            };
6187                            let mut to_i = if to.is_undef() {
6188                                if step_i >= 0 {
6189                                    n - 1
6190                                } else {
6191                                    0
6192                                }
6193                            } else {
6194                                to.to_int()
6195                            };
6196                            if from_i < 0 {
6197                                from_i += n
6198                            }
6199                            if to_i < 0 {
6200                                to_i += n
6201                            }
6202                            let mut out = String::new();
6203                            if step_i > 0 {
6204                                let mut i = from_i;
6205                                while i <= to_i && i < n {
6206                                    if i >= 0 {
6207                                        out.push(chars[i as usize]);
6208                                    }
6209                                    i += step_i;
6210                                }
6211                            } else if step_i < 0 {
6212                                let mut i = from_i;
6213                                while i >= to_i && i >= 0 {
6214                                    if i < n {
6215                                        out.push(chars[i as usize]);
6216                                    }
6217                                    i += step_i;
6218                                }
6219                            }
6220                            self.push(StrykeValue::string(out));
6221                            return Ok(());
6222                        }
6223                        let arr_len = self.interp.scope.array_len(name) as i64;
6224                        // Stryke string-slice sugar: when `@name` is empty
6225                        // (or doesn't exist) but `$name` is a non-empty
6226                        // string, treat `$name[from:to:step]` as Python-style
6227                        // substring slice. Returns a *string*, not an array.
6228                        if !crate::compat_mode()
6229                            && arr_len == 0
6230                            && self.interp.scope.scalar_binding_exists(name)
6231                        {
6232                            let s = self.interp.scope.get_scalar(name).to_string();
6233                            if !s.is_empty() {
6234                                let chars: Vec<char> = s.chars().collect();
6235                                let n = chars.len() as i64;
6236                                let step_i = if step.is_undef() { 1 } else { step.to_int() };
6237                                // Open slices: defaults depend on step direction
6238                                // step > 0: from=0, to=n-1 (forward)
6239                                // step < 0: from=n-1, to=0 (backward)
6240                                let mut from_i = if from.is_undef() {
6241                                    if step_i >= 0 {
6242                                        0
6243                                    } else {
6244                                        n - 1
6245                                    }
6246                                } else {
6247                                    from.to_int()
6248                                };
6249                                let mut to_i = if to.is_undef() {
6250                                    if step_i >= 0 {
6251                                        n - 1
6252                                    } else {
6253                                        0
6254                                    }
6255                                } else {
6256                                    to.to_int()
6257                                };
6258                                if from_i < 0 {
6259                                    from_i += n
6260                                }
6261                                if to_i < 0 {
6262                                    to_i += n
6263                                }
6264                                let mut out = String::new();
6265                                if step_i > 0 {
6266                                    let mut i = from_i;
6267                                    while i <= to_i && i < n {
6268                                        if i >= 0 {
6269                                            out.push(chars[i as usize]);
6270                                        }
6271                                        i += step_i;
6272                                    }
6273                                } else if step_i < 0 {
6274                                    let mut i = from_i;
6275                                    while i >= to_i && i >= 0 {
6276                                        if i < n {
6277                                            out.push(chars[i as usize]);
6278                                        }
6279                                        i += step_i;
6280                                    }
6281                                }
6282                                self.push(StrykeValue::string(out));
6283                                return Ok(());
6284                            }
6285                        }
6286                        let indices = match crate::value::compute_array_slice_indices(
6287                            arr_len, &from, &to, &step,
6288                        ) {
6289                            Ok(v) => v,
6290                            Err(msg) => {
6291                                return Err(StrykeError::runtime(msg, line));
6292                            }
6293                        };
6294                        let mut out = Vec::with_capacity(indices.len());
6295                        for i in indices {
6296                            out.push(self.interp.scope.get_array_element(name, i));
6297                        }
6298                        self.push(StrykeValue::array(out));
6299                        Ok(())
6300                    }
6301                    Op::HashSliceRange(hash_idx) => {
6302                        let step = self.pop();
6303                        let to = self.pop();
6304                        let from = self.pop();
6305                        let line = self.line();
6306                        let name = names[*hash_idx as usize].as_str();
6307                        let keys = match crate::value::compute_hash_slice_keys(&from, &to, &step) {
6308                            Ok(v) => v,
6309                            Err(msg) => {
6310                                return Err(StrykeError::runtime(msg, line));
6311                            }
6312                        };
6313                        let h = self.interp.scope.get_hash(name);
6314                        let mut out = Vec::with_capacity(keys.len());
6315                        for k in &keys {
6316                            out.push(h.get(k).cloned().unwrap_or(StrykeValue::UNDEF));
6317                        }
6318                        self.push(StrykeValue::array(out));
6319                        Ok(())
6320                    }
6321                    Op::ScalarFlipFlop(slot, exclusive) => {
6322                        let to = self.pop().to_int();
6323                        let from = self.pop().to_int();
6324                        let line = self.line();
6325                        let v = vm_interp_result(
6326                            self.interp
6327                                .scalar_flip_flop_eval(from, to, *slot as usize, *exclusive != 0)
6328                                .map_err(Into::into),
6329                            line,
6330                        )?;
6331                        self.push(v);
6332                        Ok(())
6333                    }
6334                    Op::RegexFlipFlop(slot, exclusive, lp, lf, rp, rf) => {
6335                        let line = self.line();
6336                        let left_pat = constants[*lp as usize].as_str_or_empty();
6337                        let left_flags = constants[*lf as usize].as_str_or_empty();
6338                        let right_pat = constants[*rp as usize].as_str_or_empty();
6339                        let right_flags = constants[*rf as usize].as_str_or_empty();
6340                        let v = vm_interp_result(
6341                            self.interp
6342                                .regex_flip_flop_eval(
6343                                    left_pat.as_str(),
6344                                    left_flags.as_str(),
6345                                    right_pat.as_str(),
6346                                    right_flags.as_str(),
6347                                    *slot as usize,
6348                                    *exclusive != 0,
6349                                    line,
6350                                )
6351                                .map_err(Into::into),
6352                            line,
6353                        )?;
6354                        self.push(v);
6355                        Ok(())
6356                    }
6357                    Op::RegexEofFlipFlop(slot, exclusive, lp, lf) => {
6358                        let line = self.line();
6359                        let left_pat = constants[*lp as usize].as_str_or_empty();
6360                        let left_flags = constants[*lf as usize].as_str_or_empty();
6361                        let v = vm_interp_result(
6362                            self.interp
6363                                .regex_eof_flip_flop_eval(
6364                                    left_pat.as_str(),
6365                                    left_flags.as_str(),
6366                                    *slot as usize,
6367                                    *exclusive != 0,
6368                                    line,
6369                                )
6370                                .map_err(Into::into),
6371                            line,
6372                        )?;
6373                        self.push(v);
6374                        Ok(())
6375                    }
6376                    Op::RegexFlipFlopExprRhs(slot, exclusive, lp, lf, rhs_idx) => {
6377                        let idx = *rhs_idx as usize;
6378                        let line = self.line();
6379                        let right_m = if let Some(&(start, end)) = self
6380                            .regex_flip_flop_rhs_expr_bytecode_ranges
6381                            .get(idx)
6382                            .and_then(|r| r.as_ref())
6383                        {
6384                            let val = self.run_block_region(start, end, op_count)?;
6385                            val.is_true()
6386                        } else {
6387                            let e = &self.regex_flip_flop_rhs_expr_entries[idx];
6388                            match self.interp.eval_boolean_rvalue_condition(e) {
6389                                Ok(b) => b,
6390                                Err(FlowOrError::Error(err)) => return Err(err),
6391                                Err(FlowOrError::Flow(_)) => {
6392                                    return Err(StrykeError::runtime(
6393                                        "unexpected flow in regex flip-flop RHS",
6394                                        line,
6395                                    ))
6396                                }
6397                            }
6398                        };
6399                        let left_pat = constants[*lp as usize].as_str_or_empty();
6400                        let left_flags = constants[*lf as usize].as_str_or_empty();
6401                        let v = vm_interp_result(
6402                            self.interp
6403                                .regex_flip_flop_eval_dynamic_right(
6404                                    left_pat.as_str(),
6405                                    left_flags.as_str(),
6406                                    *slot as usize,
6407                                    *exclusive != 0,
6408                                    line,
6409                                    right_m,
6410                                )
6411                                .map_err(Into::into),
6412                            line,
6413                        )?;
6414                        self.push(v);
6415                        Ok(())
6416                    }
6417                    Op::RegexFlipFlopDotLineRhs(slot, exclusive, lp, lf, line_cidx) => {
6418                        let line = self.line();
6419                        let rhs_line = constants[*line_cidx as usize].to_int();
6420                        let left_pat = constants[*lp as usize].as_str_or_empty();
6421                        let left_flags = constants[*lf as usize].as_str_or_empty();
6422                        let v = vm_interp_result(
6423                            self.interp
6424                                .regex_flip_flop_eval_dot_line_rhs(
6425                                    left_pat.as_str(),
6426                                    left_flags.as_str(),
6427                                    *slot as usize,
6428                                    *exclusive != 0,
6429                                    line,
6430                                    rhs_line,
6431                                )
6432                                .map_err(Into::into),
6433                            line,
6434                        )?;
6435                        self.push(v);
6436                        Ok(())
6437                    }
6438
6439                    // ── Regex ──
6440                    Op::RegexMatch(pat_idx, flags_idx, scalar_g, pos_key_idx) => {
6441                        let val = self.pop();
6442                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
6443                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6444                        let line = self.line();
6445                        if val.is_iterator() {
6446                            let source = crate::map_stream::into_pull_iter(val);
6447                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
6448                                Ok(r) => r,
6449                                Err(FlowOrError::Error(e)) => return Err(e),
6450                                Err(FlowOrError::Flow(_)) => {
6451                                    return Err(StrykeError::runtime(
6452                                        "unexpected flow in regex compile",
6453                                        line,
6454                                    ));
6455                                }
6456                            };
6457                            let global = flags.contains('g');
6458                            if global {
6459                                self.push(StrykeValue::iterator(std::sync::Arc::new(
6460                                    crate::map_stream::MatchGlobalStreamIterator::new(source, re),
6461                                )));
6462                            } else {
6463                                self.push(StrykeValue::iterator(std::sync::Arc::new(
6464                                    crate::map_stream::MatchStreamIterator::new(source, re),
6465                                )));
6466                            }
6467                            return Ok(());
6468                        }
6469                        let string = val.into_string();
6470                        let pos_key_owned = if *pos_key_idx == u16::MAX {
6471                            None
6472                        } else {
6473                            Some(constants[*pos_key_idx as usize].as_str_or_empty())
6474                        };
6475                        let pos_key: &str = pos_key_owned.as_deref().unwrap_or("_");
6476                        match self
6477                            .interp
6478                            .regex_match_execute(string, &pattern, &flags, *scalar_g, pos_key, line)
6479                        {
6480                            Ok(v) => {
6481                                self.push(v);
6482                                Ok(())
6483                            }
6484                            Err(FlowOrError::Error(e)) => Err(e),
6485                            Err(FlowOrError::Flow(_)) => {
6486                                Err(StrykeError::runtime("unexpected flow in regex match", line))
6487                            }
6488                        }
6489                    }
6490                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lvalue_idx) => {
6491                        let val = self.pop();
6492                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
6493                        let replacement = constants[*repl_idx as usize].as_str_or_empty();
6494                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6495                        let line = self.line();
6496                        if val.is_iterator() {
6497                            let source = crate::map_stream::into_pull_iter(val);
6498                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
6499                                Ok(r) => r,
6500                                Err(FlowOrError::Error(e)) => return Err(e),
6501                                Err(FlowOrError::Flow(_)) => {
6502                                    return Err(StrykeError::runtime(
6503                                        "unexpected flow in regex compile",
6504                                        line,
6505                                    ));
6506                                }
6507                            };
6508                            let global = flags.contains('g');
6509                            self.push(StrykeValue::iterator(std::sync::Arc::new(
6510                                crate::map_stream::SubstStreamIterator::new(
6511                                    source,
6512                                    re,
6513                                    crate::vm_helper::normalize_replacement_backrefs(&replacement),
6514                                    global,
6515                                ),
6516                            )));
6517                            return Ok(());
6518                        }
6519                        let string = val.into_string();
6520                        let target = &self.lvalues[*lvalue_idx as usize];
6521                        match self.interp.regex_subst_execute(
6522                            string,
6523                            &pattern,
6524                            &replacement,
6525                            &flags,
6526                            target,
6527                            line,
6528                        ) {
6529                            Ok(v) => {
6530                                self.push(v);
6531                                Ok(())
6532                            }
6533                            Err(FlowOrError::Error(e)) => Err(e),
6534                            Err(FlowOrError::Flow(_)) => {
6535                                Err(StrykeError::runtime("unexpected flow in s///", line))
6536                            }
6537                        }
6538                    }
6539                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lvalue_idx) => {
6540                        let val = self.pop();
6541                        let from = constants[*from_idx as usize].as_str_or_empty();
6542                        let to = constants[*to_idx as usize].as_str_or_empty();
6543                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6544                        let line = self.line();
6545                        if val.is_iterator() {
6546                            let source = crate::map_stream::into_pull_iter(val);
6547                            self.push(StrykeValue::iterator(std::sync::Arc::new(
6548                                crate::map_stream::TransliterateStreamIterator::new(
6549                                    source, &from, &to, &flags,
6550                                ),
6551                            )));
6552                            return Ok(());
6553                        }
6554                        let string = val.into_string();
6555                        let target = &self.lvalues[*lvalue_idx as usize];
6556                        match self
6557                            .interp
6558                            .regex_transliterate_execute(string, &from, &to, &flags, target, line)
6559                        {
6560                            Ok(v) => {
6561                                self.push(v);
6562                                Ok(())
6563                            }
6564                            Err(FlowOrError::Error(e)) => Err(e),
6565                            Err(FlowOrError::Flow(_)) => {
6566                                Err(StrykeError::runtime("unexpected flow in tr///", line))
6567                            }
6568                        }
6569                    }
6570                    Op::RegexMatchDyn(negate) => {
6571                        let rhs = self.pop();
6572                        let s = self.pop().into_string();
6573                        let line = self.line();
6574                        let exec = if let Some((pat, fl)) = rhs.regex_src_and_flags() {
6575                            self.interp
6576                                .regex_match_execute(s, &pat, &fl, false, "_", line)
6577                        } else {
6578                            let pattern = rhs.into_string();
6579                            self.interp
6580                                .regex_match_execute(s, &pattern, "", false, "_", line)
6581                        };
6582                        match exec {
6583                            Ok(v) => {
6584                                let matched = v.is_true();
6585                                let out = if *negate { !matched } else { matched };
6586                                self.push(StrykeValue::integer(if out { 1 } else { 0 }));
6587                            }
6588                            Err(FlowOrError::Error(e)) => return Err(e),
6589                            Err(FlowOrError::Flow(_)) => {
6590                                return Err(StrykeError::runtime("unexpected flow in =~", line));
6591                            }
6592                        }
6593                        Ok(())
6594                    }
6595                    Op::RegexBoolToScalar => {
6596                        let v = self.pop();
6597                        self.push(if v.is_true() {
6598                            StrykeValue::integer(1)
6599                        } else {
6600                            StrykeValue::string(String::new())
6601                        });
6602                        Ok(())
6603                    }
6604                    Op::SetRegexPos => {
6605                        let key = self.pop().to_string();
6606                        let val = self.pop();
6607                        if val.is_undef() {
6608                            self.interp.regex_pos.insert(key, None);
6609                        } else {
6610                            let u = val.to_int().max(0) as usize;
6611                            self.interp.regex_pos.insert(key, Some(u));
6612                        }
6613                        Ok(())
6614                    }
6615                    Op::LoadRegex(pat_idx, flags_idx) => {
6616                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
6617                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6618                        let line = self.line();
6619                        let pattern_owned = pattern.clone();
6620                        let re = match self.interp.compile_regex(&pattern, &flags, line) {
6621                            Ok(r) => r,
6622                            Err(FlowOrError::Error(e)) => return Err(e),
6623                            Err(FlowOrError::Flow(_)) => {
6624                                return Err(StrykeError::runtime(
6625                                    "unexpected flow in qr// compile",
6626                                    line,
6627                                ));
6628                            }
6629                        };
6630                        self.push(StrykeValue::regex(re, pattern_owned, flags.to_string()));
6631                        Ok(())
6632                    }
6633                    Op::ConcatAppend(idx) => {
6634                        let rhs = self.pop();
6635                        let n = names[*idx as usize].as_str();
6636                        let line = self.line();
6637                        let result = self
6638                            .interp
6639                            .scope
6640                            .scalar_concat_inplace(n, &rhs)
6641                            .map_err(|e| e.at_line(line))?;
6642                        self.push(result);
6643                        Ok(())
6644                    }
6645                    Op::ConcatAppendSlot(slot) => {
6646                        let rhs = self.pop();
6647                        let result = self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
6648                        self.push(result);
6649                        Ok(())
6650                    }
6651                    Op::ConcatAppendSlotVoid(slot) => {
6652                        let rhs = self.pop();
6653                        self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
6654                        Ok(())
6655                    }
6656                    Op::SlotLtIntJumpIfFalse(slot, limit, target) => {
6657                        let val = self.interp.scope.get_scalar_slot(*slot);
6658                        let lt = if let Some(i) = val.as_integer() {
6659                            i < *limit as i64
6660                        } else {
6661                            val.to_number() < *limit as f64
6662                        };
6663                        if !lt {
6664                            self.ip = *target;
6665                        }
6666                        Ok(())
6667                    }
6668                    Op::SlotIncLtIntJumpBack(slot, limit, body_target) => {
6669                        // Fused trailing `++$slot; goto top_test` for the bench_loop shape:
6670                        // matches `PreIncSlotVoid` + `Jump` + top `SlotLtIntJumpIfFalse` exactly so
6671                        // coercion, wrap-around, and integer-only write semantics line up byte-for-byte
6672                        // with the un-fused form. Every iteration past the first skips the top check
6673                        // and the unconditional jump entirely.
6674                        let next_i = self
6675                            .interp
6676                            .scope
6677                            .get_scalar_slot(*slot)
6678                            .to_int()
6679                            .wrapping_add(1);
6680                        self.interp
6681                            .scope
6682                            .set_scalar_slot(*slot, StrykeValue::integer(next_i));
6683                        if next_i < *limit as i64 {
6684                            self.ip = *body_target;
6685                        }
6686                        Ok(())
6687                    }
6688                    Op::AccumSumLoop(sum_slot, i_slot, limit) => {
6689                        // Runs the entire counted `while $i < limit { $sum += $i; $i += 1 }` loop in
6690                        // native Rust. The peephole only fires when the body is exactly this one
6691                        // accumulate statement, so every side effect is captured by the final
6692                        // `$sum` and `$i` writes; there is nothing else to do per iteration.
6693                        let mut sum = self.interp.scope.get_scalar_slot(*sum_slot).to_int();
6694                        let mut i = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6695                        let limit = *limit as i64;
6696                        while i < limit {
6697                            sum = sum.wrapping_add(i);
6698                            i = i.wrapping_add(1);
6699                        }
6700                        self.interp
6701                            .scope
6702                            .set_scalar_slot(*sum_slot, StrykeValue::integer(sum));
6703                        self.interp
6704                            .scope
6705                            .set_scalar_slot(*i_slot, StrykeValue::integer(i));
6706                        Ok(())
6707                    }
6708                    Op::AddHashElemPlainKeyToSlot(sum_slot, k_name_idx, h_name_idx) => {
6709                        // `$sum += $h{$k}` — single-dispatch slot += hash[name-scalar] with no
6710                        // VM stack traffic. The key scalar is read via plain (name-based) access
6711                        // because the compiler's `for my $k (keys %h)` lowering currently backs
6712                        // `$k` with a frame scalar, not a slot.
6713                        let k_name = names[*k_name_idx as usize].as_str();
6714                        let h_name = names[*h_name_idx as usize].as_str();
6715                        self.interp.touch_env_hash(h_name);
6716                        let key = self.interp.scope.get_scalar(k_name).to_string();
6717                        let elem = self.interp.scope.get_hash_element(h_name, &key);
6718                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
6719                        let new_v =
6720                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
6721                                StrykeValue::integer(a.wrapping_add(b))
6722                            } else {
6723                                StrykeValue::float(cur.to_number() + elem.to_number())
6724                            };
6725                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
6726                        Ok(())
6727                    }
6728                    Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_name_idx) => {
6729                        // `$sum += $h{$k}` — slot counter, slot key, slot sum. Zero name lookups
6730                        // for `$sum` and `$k`; one frame-walk for `%h` (same as the non-slot form).
6731                        let h_name = names[*h_name_idx as usize].as_str();
6732                        self.interp.touch_env_hash(h_name);
6733                        let key_val = self.interp.scope.get_scalar_slot(*k_slot);
6734                        let key = key_val.to_string();
6735                        let elem = self.interp.scope.get_hash_element(h_name, &key);
6736                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
6737                        let new_v =
6738                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
6739                                StrykeValue::integer(a.wrapping_add(b))
6740                            } else {
6741                                StrykeValue::float(cur.to_number() + elem.to_number())
6742                            };
6743                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
6744                        Ok(())
6745                    }
6746                    Op::SumHashValuesToSlot(sum_slot, h_name_idx) => {
6747                        // `for my $k (keys %h) { $sum += $h{$k} }` fused to a single op that walks
6748                        // `hash.values()` in a tight native loop. No key stringification, no stack
6749                        // traffic, no per-iter dispatch. The foreach body reduced to
6750                        // `AddHashElemSlotKeyToSlot`, so this fusion is correct regardless of `$k`
6751                        // slot assignment — we never read `$k`.
6752                        let h_name = names[*h_name_idx as usize].as_str();
6753                        self.interp.touch_env_hash(h_name);
6754                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
6755                        let mut int_acc: i64 = cur.as_integer().unwrap_or(0);
6756                        let mut float_acc: f64 = 0.0;
6757                        let mut is_int = cur.as_integer().is_some();
6758                        if !is_int {
6759                            float_acc = cur.to_number();
6760                        }
6761                        // Walk the hash via the scope's borrow path without cloning the whole
6762                        // IndexMap. `for_each_hash_value` takes a visitor so the lock (if any) is
6763                        // held once rather than per-element.
6764                        self.interp.scope.for_each_hash_value(h_name, |v| {
6765                            if is_int {
6766                                if let Some(x) = v.as_integer() {
6767                                    int_acc = int_acc.wrapping_add(x);
6768                                    return;
6769                                }
6770                                float_acc = int_acc as f64;
6771                                is_int = false;
6772                            }
6773                            float_acc += v.to_number();
6774                        });
6775                        let new_v = if is_int {
6776                            StrykeValue::integer(int_acc)
6777                        } else {
6778                            StrykeValue::float(float_acc)
6779                        };
6780                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
6781                        Ok(())
6782                    }
6783                    Op::SetHashIntTimesLoop(h_name_idx, i_slot, k, limit) => {
6784                        // Runs the counted `while $i < limit { $h{$i} = $i * k; $i += 1 }` loop
6785                        // natively: the hash is `reserve()`d once, keys are stringified via
6786                        // `itoa` (no `format!` allocation), and values are inserted in a tight
6787                        // Rust loop. `$i` is left at `limit` on exit, matching the un-fused shape.
6788                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6789                        let lim = *limit as i64;
6790                        if i_cur < lim {
6791                            let n = names[*h_name_idx as usize].as_str();
6792                            self.require_hash_mutable(n)?;
6793                            self.interp.touch_env_hash(n);
6794                            let line = self.line();
6795                            self.interp
6796                                .scope
6797                                .set_hash_int_times_range(n, i_cur, lim, *k as i64)
6798                                .map_err(|e| e.at_line(line))?;
6799                        }
6800                        self.interp
6801                            .scope
6802                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6803                        Ok(())
6804                    }
6805                    Op::PushIntRangeToArrayLoop(arr_name_idx, i_slot, limit) => {
6806                        // Runs the entire counted `while $i < limit { push @arr, $i; $i += 1 }`
6807                        // loop in native Rust. The array's `Vec<StrykeValue>` is reserved once and
6808                        // `push(StrykeValue::integer(i))` runs in a tight Rust loop — no per-iter
6809                        // op dispatch, no `require_array_mutable` check per iter.
6810                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6811                        let lim = *limit as i64;
6812                        if i_cur < lim {
6813                            let n = names[*arr_name_idx as usize].as_str();
6814                            self.require_array_mutable(n)?;
6815                            let line = self.line();
6816                            self.interp
6817                                .scope
6818                                .push_int_range_to_array(n, i_cur, lim)
6819                                .map_err(|e| e.at_line(line))?;
6820                        }
6821                        self.interp
6822                            .scope
6823                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6824                        Ok(())
6825                    }
6826                    Op::ConcatConstSlotLoop(const_idx, s_slot, i_slot, limit) => {
6827                        // Runs the entire counted `while $i < limit { $s .= CONST; $i += 1 }` loop
6828                        // in native Rust. We stringify the constant once, reserve `(limit-i_cur) *
6829                        // const.len()` up front so the owning `String` reallocs at most twice, then
6830                        // `push_str` in a tight loop (see `try_concat_repeat_inplace`). Falls back
6831                        // to the per-iteration slow path when the slot is not the sole owner of a
6832                        // heap `String` — `.=` semantics match the un-fused shape byte-for-byte.
6833                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6834                        let lim = *limit as i64;
6835                        if i_cur < lim {
6836                            let n_iters = (lim - i_cur) as usize;
6837                            let rhs = constants[*const_idx as usize].as_str_or_empty();
6838                            if !self
6839                                .interp
6840                                .scope
6841                                .scalar_slot_concat_repeat_inplace(*s_slot, &rhs, n_iters)
6842                            {
6843                                self.interp
6844                                    .scope
6845                                    .scalar_slot_concat_repeat_slow(*s_slot, &rhs, n_iters);
6846                            }
6847                        }
6848                        self.interp
6849                            .scope
6850                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6851                        Ok(())
6852                    }
6853                    Op::AddAssignSlotSlot(dst, src) => {
6854                        let a = self.interp.scope.get_scalar_slot(*dst);
6855                        let b = self.interp.scope.get_scalar_slot(*src);
6856                        let result = crate::value::compat_add(&a, &b);
6857                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6858                        self.push(result);
6859                        Ok(())
6860                    }
6861                    Op::AddAssignSlotSlotVoid(dst, src) => {
6862                        let a = self.interp.scope.get_scalar_slot(*dst);
6863                        let b = self.interp.scope.get_scalar_slot(*src);
6864                        let result = crate::value::compat_add(&a, &b);
6865                        self.interp.scope.set_scalar_slot(*dst, result);
6866                        Ok(())
6867                    }
6868                    Op::SubAssignSlotSlot(dst, src) => {
6869                        let a = self.interp.scope.get_scalar_slot(*dst);
6870                        let b = self.interp.scope.get_scalar_slot(*src);
6871                        let result = crate::value::compat_sub(&a, &b);
6872                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6873                        self.push(result);
6874                        Ok(())
6875                    }
6876                    Op::MulAssignSlotSlot(dst, src) => {
6877                        let a = self.interp.scope.get_scalar_slot(*dst);
6878                        let b = self.interp.scope.get_scalar_slot(*src);
6879                        let result = crate::value::compat_mul(&a, &b);
6880                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6881                        self.push(result);
6882                        Ok(())
6883                    }
6884
6885                    // ── Frame-local scalar slots (O(1), no string lookup) ──
6886                    Op::GetScalarSlot(slot) => {
6887                        let val = self.interp.scope.get_scalar_slot(*slot);
6888                        self.push(val);
6889                        Ok(())
6890                    }
6891                    Op::SetScalarSlot(slot) => {
6892                        let val = self.pop();
6893                        self.interp
6894                            .scope
6895                            .set_scalar_slot_checked(*slot, val, None)
6896                            .map_err(|e| e.at_line(self.line()))?;
6897                        Ok(())
6898                    }
6899                    Op::SetScalarSlotKeep(slot) => {
6900                        let val = self.peek().dup_stack();
6901                        self.interp
6902                            .scope
6903                            .set_scalar_slot_checked(*slot, val, None)
6904                            .map_err(|e| e.at_line(self.line()))?;
6905                        Ok(())
6906                    }
6907                    Op::DeclareScalarSlot(slot, name_idx) => {
6908                        let val = self.pop();
6909                        let name_opt = if *name_idx == u16::MAX {
6910                            None
6911                        } else {
6912                            Some(names[*name_idx as usize].as_str())
6913                        };
6914                        self.interp.scope.declare_scalar_slot(*slot, val, name_opt);
6915                        Ok(())
6916                    }
6917                    Op::GetArg(idx) => {
6918                        // Read argument from caller's stack region without @_ allocation.
6919                        let val = if let Some(frame) = self.call_stack.last() {
6920                            let arg_pos = frame.stack_base + *idx as usize;
6921                            self.stack
6922                                .get(arg_pos)
6923                                .cloned()
6924                                .unwrap_or(StrykeValue::UNDEF)
6925                        } else {
6926                            StrykeValue::UNDEF
6927                        };
6928                        self.push(val);
6929                        Ok(())
6930                    }
6931
6932                    Op::ReadIntoVar(name_idx) => {
6933                        let length = self.pop().to_int() as usize;
6934                        let fh_val = self.pop();
6935                        let name = &names[*name_idx as usize];
6936                        let line = self.line();
6937                        let result = vm_interp_result(
6938                            self.interp.builtin_read_into(fh_val, name, length, line),
6939                            line,
6940                        )?;
6941                        self.push(result);
6942                        Ok(())
6943                    }
6944                    Op::ChompInPlace(lvalue_idx) => {
6945                        let val = self.pop();
6946                        let target = &self.lvalues[*lvalue_idx as usize];
6947                        let line = self.line();
6948                        match self.interp.chomp_inplace_execute(val, target) {
6949                            Ok(v) => self.push(v),
6950                            Err(FlowOrError::Error(e)) => return Err(e),
6951                            Err(FlowOrError::Flow(_)) => {
6952                                return Err(StrykeError::runtime("unexpected flow in chomp", line));
6953                            }
6954                        }
6955                        Ok(())
6956                    }
6957                    Op::ChopInPlace(lvalue_idx) => {
6958                        let val = self.pop();
6959                        let target = &self.lvalues[*lvalue_idx as usize];
6960                        let line = self.line();
6961                        match self.interp.chop_inplace_execute(val, target) {
6962                            Ok(v) => self.push(v),
6963                            Err(FlowOrError::Error(e)) => return Err(e),
6964                            Err(FlowOrError::Flow(_)) => {
6965                                return Err(StrykeError::runtime("unexpected flow in chop", line));
6966                            }
6967                        }
6968                        Ok(())
6969                    }
6970                    Op::SubstrFourArg(idx) => {
6971                        let (string_e, offset_e, length_e, rep_e) =
6972                            &self.substr_four_arg_entries[*idx as usize];
6973                        let v = vm_interp_result(
6974                            self.interp.eval_substr_expr(
6975                                string_e,
6976                                offset_e,
6977                                length_e.as_ref(),
6978                                Some(rep_e),
6979                                self.line(),
6980                            ),
6981                            self.line(),
6982                        )?;
6983                        self.push(v);
6984                        Ok(())
6985                    }
6986                    Op::KeysExpr(idx) => {
6987                        let i = *idx as usize;
6988                        let line = self.line();
6989                        let v = if let Some(&(start, end)) = self
6990                            .keys_expr_bytecode_ranges
6991                            .get(i)
6992                            .and_then(|r| r.as_ref())
6993                        {
6994                            let val = self.run_block_region(start, end, op_count)?;
6995                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
6996                        } else {
6997                            let e = &self.keys_expr_entries[i];
6998                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
6999                        };
7000                        self.push(v);
7001                        Ok(())
7002                    }
7003                    Op::KeysExprScalar(idx) => {
7004                        let i = *idx as usize;
7005                        let line = self.line();
7006                        let v = if let Some(&(start, end)) = self
7007                            .keys_expr_bytecode_ranges
7008                            .get(i)
7009                            .and_then(|r| r.as_ref())
7010                        {
7011                            let val = self.run_block_region(start, end, op_count)?;
7012                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
7013                        } else {
7014                            let e = &self.keys_expr_entries[i];
7015                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
7016                        };
7017                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
7018                        self.push(StrykeValue::integer(n));
7019                        Ok(())
7020                    }
7021                    Op::ValuesExpr(idx) => {
7022                        let i = *idx as usize;
7023                        let line = self.line();
7024                        let v = if let Some(&(start, end)) = self
7025                            .values_expr_bytecode_ranges
7026                            .get(i)
7027                            .and_then(|r| r.as_ref())
7028                        {
7029                            let val = self.run_block_region(start, end, op_count)?;
7030                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
7031                        } else {
7032                            let e = &self.values_expr_entries[i];
7033                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
7034                        };
7035                        self.push(v);
7036                        Ok(())
7037                    }
7038                    Op::ValuesExprScalar(idx) => {
7039                        let i = *idx as usize;
7040                        let line = self.line();
7041                        let v = if let Some(&(start, end)) = self
7042                            .values_expr_bytecode_ranges
7043                            .get(i)
7044                            .and_then(|r| r.as_ref())
7045                        {
7046                            let val = self.run_block_region(start, end, op_count)?;
7047                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
7048                        } else {
7049                            let e = &self.values_expr_entries[i];
7050                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
7051                        };
7052                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
7053                        self.push(StrykeValue::integer(n));
7054                        Ok(())
7055                    }
7056                    Op::DeleteExpr(idx) => {
7057                        let e = &self.delete_expr_entries[*idx as usize];
7058                        let v = vm_interp_result(
7059                            self.interp.eval_delete_operand(e, self.line()),
7060                            self.line(),
7061                        )?;
7062                        self.push(v);
7063                        Ok(())
7064                    }
7065                    Op::ExistsExpr(idx) => {
7066                        let e = &self.exists_expr_entries[*idx as usize];
7067                        let v = vm_interp_result(
7068                            self.interp.eval_exists_operand(e, self.line()),
7069                            self.line(),
7070                        )?;
7071                        self.push(v);
7072                        Ok(())
7073                    }
7074                    Op::PushExpr(idx) => {
7075                        let (array, values) = &self.push_expr_entries[*idx as usize];
7076                        let v = vm_interp_result(
7077                            self.interp
7078                                .eval_push_expr(array, values.as_slice(), self.line()),
7079                            self.line(),
7080                        )?;
7081                        self.push(v);
7082                        Ok(())
7083                    }
7084                    Op::PopExpr(idx) => {
7085                        let e = &self.pop_expr_entries[*idx as usize];
7086                        let v = vm_interp_result(
7087                            self.interp.eval_pop_expr(e, self.line()),
7088                            self.line(),
7089                        )?;
7090                        self.push(v);
7091                        Ok(())
7092                    }
7093                    Op::ShiftExpr(idx) => {
7094                        let e = &self.shift_expr_entries[*idx as usize];
7095                        let v = vm_interp_result(
7096                            self.interp.eval_shift_expr(e, self.line()),
7097                            self.line(),
7098                        )?;
7099                        self.push(v);
7100                        Ok(())
7101                    }
7102                    Op::UnshiftExpr(idx) => {
7103                        let (array, values) = &self.unshift_expr_entries[*idx as usize];
7104                        let v = vm_interp_result(
7105                            self.interp
7106                                .eval_unshift_expr(array, values.as_slice(), self.line()),
7107                            self.line(),
7108                        )?;
7109                        self.push(v);
7110                        Ok(())
7111                    }
7112                    Op::SpliceExpr(idx) => {
7113                        let (array, offset, length, replacement) =
7114                            &self.splice_expr_entries[*idx as usize];
7115                        let v = vm_interp_result(
7116                            self.interp.eval_splice_expr(
7117                                array,
7118                                offset.as_ref(),
7119                                length.as_ref(),
7120                                replacement.as_slice(),
7121                                self.interp.wantarray_kind,
7122                                self.line(),
7123                            ),
7124                            self.line(),
7125                        )?;
7126                        self.push(v);
7127                        Ok(())
7128                    }
7129
7130                    // ── References ──
7131                    Op::MakeScalarRef => {
7132                        let val = self.pop();
7133                        self.push(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))));
7134                        Ok(())
7135                    }
7136                    Op::MakeScalarBindingRef(name_idx) => {
7137                        let name = names[*name_idx as usize].clone();
7138                        self.push(StrykeValue::scalar_binding_ref(name));
7139                        Ok(())
7140                    }
7141                    Op::MakeArrayBindingRef(name_idx) => {
7142                        let name = &names[*name_idx as usize];
7143                        // Promote the scope's array to shared Arc-backed storage.
7144                        // Both the scope and the returned ref share the same Arc,
7145                        // so mutations through either path are visible.
7146                        let arc = self.interp.scope.promote_array_to_shared(name);
7147                        self.push(StrykeValue::array_ref(arc));
7148                        Ok(())
7149                    }
7150                    Op::MakeHashBindingRef(name_idx) => {
7151                        let name = &names[*name_idx as usize];
7152                        // Lazy-init hook: `\%all` / `\%parameters` / `\%main::`
7153                        // bypass `Op::GetHash`, so without this call the
7154                        // reference is taken before the hash is populated and
7155                        // the user gets an empty hashref.
7156                        self.interp.touch_env_hash(name);
7157                        let arc = self.interp.scope.promote_hash_to_shared(name);
7158                        self.push(StrykeValue::hash_ref(arc));
7159                        Ok(())
7160                    }
7161                    Op::MakeArrayRefAlias => {
7162                        let v = self.pop();
7163                        let line = self.line();
7164                        let out =
7165                            vm_interp_result(self.interp.make_array_ref_alias(v, line), line)?;
7166                        self.push(out);
7167                        Ok(())
7168                    }
7169                    Op::MakeHashRefAlias => {
7170                        let v = self.pop();
7171                        let line = self.line();
7172                        let out = vm_interp_result(self.interp.make_hash_ref_alias(v, line), line)?;
7173                        self.push(out);
7174                        Ok(())
7175                    }
7176                    Op::MakeArrayRef => {
7177                        let val = self.pop();
7178                        let val = self.interp.scope.resolve_container_binding_ref(val);
7179                        let arr = if let Some(a) = val.as_array_vec() {
7180                            a
7181                        } else {
7182                            vec![val]
7183                        };
7184                        self.push(StrykeValue::array_ref(Arc::new(RwLock::new(arr))));
7185                        Ok(())
7186                    }
7187                    Op::MakeHashRef => {
7188                        let val = self.pop();
7189                        let map = if let Some(h) = val.as_hash_map() {
7190                            h
7191                        } else {
7192                            let items = val.to_list();
7193                            let mut m = IndexMap::new();
7194                            let mut i = 0;
7195                            while i + 1 < items.len() {
7196                                m.insert(items[i].to_string(), items[i + 1].clone());
7197                                i += 2;
7198                            }
7199                            m
7200                        };
7201                        self.push(StrykeValue::hash_ref(Arc::new(RwLock::new(map))));
7202                        Ok(())
7203                    }
7204                    Op::MakeCodeRef(block_idx, sig_idx) => {
7205                        let block = self.blocks[*block_idx as usize].clone();
7206                        let params = self.code_ref_sigs[*sig_idx as usize].clone();
7207                        let captured = self.interp.scope.capture();
7208                        self.push(StrykeValue::code_ref(Arc::new(crate::value::StrykeSub {
7209                            name: "__ANON__".to_string(),
7210                            params,
7211                            body: block,
7212                            closure_env: Some(captured),
7213                            prototype: None,
7214                            fib_like: None,
7215                        })));
7216                        Ok(())
7217                    }
7218                    Op::LoadNamedSubRef(name_idx) => {
7219                        let name = names[*name_idx as usize].as_str();
7220                        let line = self.line();
7221                        let sub = self.interp.resolve_sub_by_name(name).ok_or_else(|| {
7222                            StrykeError::runtime(
7223                                self.interp.undefined_subroutine_resolve_message(name),
7224                                line,
7225                            )
7226                        })?;
7227                        self.push(StrykeValue::code_ref(sub));
7228                        Ok(())
7229                    }
7230                    Op::LoadDynamicSubRef => {
7231                        let name = self.pop().to_string();
7232                        let line = self.line();
7233                        let sub = self.interp.resolve_sub_by_name(&name).ok_or_else(|| {
7234                            StrykeError::runtime(
7235                                self.interp.undefined_subroutine_resolve_message(&name),
7236                                line,
7237                            )
7238                        })?;
7239                        self.push(StrykeValue::code_ref(sub));
7240                        Ok(())
7241                    }
7242                    Op::LoadDynamicTypeglob => {
7243                        let name = self.pop().to_string();
7244                        let n = self.interp.resolve_io_handle_name(&name);
7245                        self.push(StrykeValue::string(n));
7246                        Ok(())
7247                    }
7248                    Op::CopyTypeglobSlots(lhs_i, rhs_i) => {
7249                        let lhs = self.names[*lhs_i as usize].as_str();
7250                        let rhs = self.names[*rhs_i as usize].as_str();
7251                        let line = self.line();
7252                        self.interp
7253                            .copy_typeglob_slots(lhs, rhs, line)
7254                            .map_err(|e| e.at_line(line))?;
7255                        Ok(())
7256                    }
7257                    Op::TypeglobAssignFromValue(name_idx) => {
7258                        let val = self.pop();
7259                        let name = self.names[*name_idx as usize].as_str();
7260                        let line = self.line();
7261                        vm_interp_result(
7262                            self.interp.assign_typeglob_value(name, val.clone(), line),
7263                            line,
7264                        )?;
7265                        self.push(val);
7266                        Ok(())
7267                    }
7268                    Op::TypeglobAssignFromValueDynamic => {
7269                        let val = self.pop();
7270                        let name = self.pop().to_string();
7271                        let line = self.line();
7272                        vm_interp_result(
7273                            self.interp.assign_typeglob_value(&name, val.clone(), line),
7274                            line,
7275                        )?;
7276                        self.push(val);
7277                        Ok(())
7278                    }
7279                    Op::CopyTypeglobSlotsDynamicLhs(rhs_i) => {
7280                        let lhs = self.pop().to_string();
7281                        let rhs = self.names[*rhs_i as usize].as_str();
7282                        let line = self.line();
7283                        self.interp
7284                            .copy_typeglob_slots(&lhs, rhs, line)
7285                            .map_err(|e| e.at_line(line))?;
7286                        Ok(())
7287                    }
7288                    Op::SymbolicDeref(kind_byte) => {
7289                        let v = self.pop();
7290                        let kind = match *kind_byte {
7291                            0 => Sigil::Scalar,
7292                            1 => Sigil::Array,
7293                            2 => Sigil::Hash,
7294                            3 => Sigil::Typeglob,
7295                            _ => {
7296                                return Err(StrykeError::runtime(
7297                                    "VM: bad SymbolicDeref kind byte",
7298                                    self.line(),
7299                                ));
7300                            }
7301                        };
7302                        let line = self.line();
7303                        let out =
7304                            vm_interp_result(self.interp.symbolic_deref(v, kind, line), line)?;
7305                        self.push(out);
7306                        Ok(())
7307                    }
7308
7309                    // ── Arrow dereference ──
7310                    Op::ArrowArray => {
7311                        let idx = self.pop().to_int();
7312                        let r = self.pop();
7313                        let line = self.line();
7314                        let v = vm_interp_result(
7315                            self.interp.read_arrow_array_element(r, idx, line),
7316                            line,
7317                        )?;
7318                        self.push(v);
7319                        Ok(())
7320                    }
7321                    Op::ArrowHash => {
7322                        let key = self.pop().to_string();
7323                        let r = self.pop();
7324                        let line = self.line();
7325                        let v = vm_interp_result(
7326                            self.interp.read_arrow_hash_element(r, key.as_str(), line),
7327                            line,
7328                        )?;
7329                        self.push(v);
7330                        Ok(())
7331                    }
7332                    Op::SetArrowHash => {
7333                        let key = self.pop().to_string();
7334                        let r = self.pop();
7335                        let val = self.pop();
7336                        let line = self.line();
7337                        vm_interp_result(
7338                            self.interp.assign_arrow_hash_deref(r, key, val, line),
7339                            line,
7340                        )?;
7341                        Ok(())
7342                    }
7343                    Op::SetArrowArray => {
7344                        let idx = self.pop().to_int();
7345                        let r = self.pop();
7346                        let val = self.pop();
7347                        let line = self.line();
7348                        vm_interp_result(
7349                            self.interp.assign_arrow_array_deref(r, idx, val, line),
7350                            line,
7351                        )?;
7352                        Ok(())
7353                    }
7354                    Op::SetArrowArrayKeep => {
7355                        let idx = self.pop().to_int();
7356                        let r = self.pop();
7357                        let val = self.pop();
7358                        let val_keep = val.clone();
7359                        let line = self.line();
7360                        vm_interp_result(
7361                            self.interp.assign_arrow_array_deref(r, idx, val, line),
7362                            line,
7363                        )?;
7364                        self.push(val_keep);
7365                        Ok(())
7366                    }
7367                    Op::SetArrowHashKeep => {
7368                        let key = self.pop().to_string();
7369                        let r = self.pop();
7370                        let val = self.pop();
7371                        let val_keep = val.clone();
7372                        let line = self.line();
7373                        vm_interp_result(
7374                            self.interp.assign_arrow_hash_deref(r, key, val, line),
7375                            line,
7376                        )?;
7377                        self.push(val_keep);
7378                        Ok(())
7379                    }
7380                    Op::ArrowArrayPostfix(b) => {
7381                        let idx = self.pop().to_int();
7382                        let r = self.pop();
7383                        let line = self.line();
7384                        let old = vm_interp_result(
7385                            self.interp.arrow_array_postfix(r, idx, *b == 1, line),
7386                            line,
7387                        )?;
7388                        self.push(old);
7389                        Ok(())
7390                    }
7391                    Op::ArrowHashPostfix(b) => {
7392                        let key = self.pop().to_string();
7393                        let r = self.pop();
7394                        let line = self.line();
7395                        let old = vm_interp_result(
7396                            self.interp.arrow_hash_postfix(r, key, *b == 1, line),
7397                            line,
7398                        )?;
7399                        self.push(old);
7400                        Ok(())
7401                    }
7402                    Op::SetSymbolicScalarRef => {
7403                        let r = self.pop();
7404                        let val = self.pop();
7405                        let line = self.line();
7406                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
7407                        Ok(())
7408                    }
7409                    Op::SetSymbolicScalarRefKeep => {
7410                        let r = self.pop();
7411                        let val = self.pop();
7412                        let val_keep = val.clone();
7413                        let line = self.line();
7414                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
7415                        self.push(val_keep);
7416                        Ok(())
7417                    }
7418                    Op::SetSymbolicArrayRef => {
7419                        let r = self.pop();
7420                        let val = self.pop();
7421                        let line = self.line();
7422                        vm_interp_result(
7423                            self.interp.assign_symbolic_array_ref_deref(r, val, line),
7424                            line,
7425                        )?;
7426                        Ok(())
7427                    }
7428                    Op::SetSymbolicHashRef => {
7429                        let r = self.pop();
7430                        let val = self.pop();
7431                        let line = self.line();
7432                        vm_interp_result(
7433                            self.interp.assign_symbolic_hash_ref_deref(r, val, line),
7434                            line,
7435                        )?;
7436                        Ok(())
7437                    }
7438                    Op::SetSymbolicTypeglobRef => {
7439                        let r = self.pop();
7440                        let val = self.pop();
7441                        let line = self.line();
7442                        vm_interp_result(
7443                            self.interp.assign_symbolic_typeglob_ref_deref(r, val, line),
7444                            line,
7445                        )?;
7446                        Ok(())
7447                    }
7448                    Op::SymbolicScalarRefPostfix(b) => {
7449                        let r = self.pop();
7450                        let line = self.line();
7451                        let old = vm_interp_result(
7452                            self.interp.symbolic_scalar_ref_postfix(r, *b == 1, line),
7453                            line,
7454                        )?;
7455                        self.push(old);
7456                        Ok(())
7457                    }
7458                    Op::ArrowCall(wa) => {
7459                        let want = WantarrayCtx::from_byte(*wa);
7460                        let args_val = self.pop();
7461                        let r = self.pop();
7462                        // Auto-deref ScalarRef so closures that captured $f can call $f->()
7463                        let r = if let Some(inner) = r.as_scalar_ref() {
7464                            inner.read().clone()
7465                        } else {
7466                            r
7467                        };
7468                        let args = args_val.to_list();
7469                        if let Some(sub) = r.as_code_ref() {
7470                            // Higher-order function wrappers (comp, partial, memoize, etc.)
7471                            // have empty bodies + magic closure_env keys. Dispatch them via
7472                            // the interpreter's try_hof_dispatch before falling through to
7473                            // the normal body execution path.
7474                            if let Some(hof_result) =
7475                                self.interp.try_hof_dispatch(&sub, &args, want, self.line())
7476                            {
7477                                let v = vm_interp_result(hof_result, self.line())?;
7478                                self.push(v);
7479                                return Ok(());
7480                            }
7481                            self.interp.current_sub_stack.push(sub.clone());
7482                            let saved_wa = self.interp.wantarray_kind;
7483                            self.interp.wantarray_kind = want;
7484                            self.interp.scope_push_hook();
7485                            self.interp.scope.declare_array("_", args.clone());
7486                            if let Some(ref env) = sub.closure_env {
7487                                self.interp.scope.restore_capture(env);
7488                            }
7489                            let line = self.line();
7490                            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
7491                            self.interp
7492                                .apply_sub_signature(sub.as_ref(), &argv, line)
7493                                .map_err(|e| e.at_line(line))?;
7494                            self.interp.scope.declare_array("_", argv.clone());
7495                            // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
7496                            self.interp.scope.set_closure_args(&argv);
7497                            let result = self.interp.exec_block_no_scope(&sub.body);
7498                            self.interp.wantarray_kind = saved_wa;
7499                            self.interp.scope_pop_hook();
7500                            self.interp.current_sub_stack.pop();
7501                            match result {
7502                                Ok(v) => self.push(v),
7503                                Err(crate::vm_helper::FlowOrError::Flow(
7504                                    crate::vm_helper::Flow::Return(v),
7505                                )) => self.push(v),
7506                                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
7507                                Err(_) => self.push(StrykeValue::UNDEF),
7508                            }
7509                        } else {
7510                            return Err(StrykeError::runtime("Not a code reference", self.line()));
7511                        }
7512                        Ok(())
7513                    }
7514                    Op::IndirectCall(argc, wa, pass_flag) => {
7515                        let want = WantarrayCtx::from_byte(*wa);
7516                        let line = self.line();
7517                        let arg_vals = if *pass_flag != 0 {
7518                            self.interp.scope.get_array("_")
7519                        } else {
7520                            let n = *argc as usize;
7521                            let mut args = Vec::with_capacity(n);
7522                            for _ in 0..n {
7523                                args.push(self.pop());
7524                            }
7525                            args.reverse();
7526                            args
7527                        };
7528                        let target = self.pop();
7529                        // HOF wrapper fast path (comp, partial, memoize, etc.)
7530                        if let Some(sub) = target.as_code_ref() {
7531                            if let Some(hof_result) =
7532                                self.interp.try_hof_dispatch(&sub, &arg_vals, want, line)
7533                            {
7534                                let v = vm_interp_result(hof_result, line)?;
7535                                self.push(v);
7536                                return Ok(());
7537                            }
7538                        }
7539                        let r = self
7540                            .interp
7541                            .dispatch_indirect_call(target, arg_vals, want, line);
7542                        let v = vm_interp_result(r, line)?;
7543                        self.push(v);
7544                        Ok(())
7545                    }
7546
7547                    // ── Method call ──
7548                    Op::MethodCall(name_idx, argc, wa) => {
7549                        self.run_method_op(*name_idx, *argc, *wa, false)?;
7550                        Ok(())
7551                    }
7552                    Op::MethodCallSuper(name_idx, argc, wa) => {
7553                        self.run_method_op(*name_idx, *argc, *wa, true)?;
7554                        Ok(())
7555                    }
7556
7557                    // ── File test ──
7558                    Op::FileTestOp(test) => {
7559                        let path = self.pop().to_string();
7560                        let op = *test as char;
7561                        // -M, -A, -C return fractional days (float)
7562                        if matches!(op, 'M' | 'A' | 'C') {
7563                            #[cfg(unix)]
7564                            {
7565                                let v = match crate::perl_fs::filetest_age_days(&path, op) {
7566                                    Some(days) => StrykeValue::float(days),
7567                                    None => StrykeValue::UNDEF,
7568                                };
7569                                self.push(v);
7570                                return Ok(());
7571                            }
7572                            #[cfg(not(unix))]
7573                            {
7574                                self.push(StrykeValue::UNDEF);
7575                                return Ok(());
7576                            }
7577                        }
7578                        // -s returns file size (integer)
7579                        if op == 's' {
7580                            let v = match std::fs::metadata(&path) {
7581                                Ok(m) => StrykeValue::integer(m.len() as i64),
7582                                Err(_) => StrykeValue::UNDEF,
7583                            };
7584                            self.push(v);
7585                            return Ok(());
7586                        }
7587                        let result = match op {
7588                            'e' => std::path::Path::new(&path).exists(),
7589                            'f' => std::path::Path::new(&path).is_file(),
7590                            'd' => std::path::Path::new(&path).is_dir(),
7591                            'l' => std::path::Path::new(&path).is_symlink(),
7592                            #[cfg(unix)]
7593                            'r' => crate::perl_fs::filetest_effective_access(&path, 4),
7594                            #[cfg(not(unix))]
7595                            'r' => std::fs::metadata(&path).is_ok(),
7596                            #[cfg(unix)]
7597                            'w' => crate::perl_fs::filetest_effective_access(&path, 2),
7598                            #[cfg(not(unix))]
7599                            'w' => std::fs::metadata(&path).is_ok(),
7600                            #[cfg(unix)]
7601                            'x' => crate::perl_fs::filetest_effective_access(&path, 1),
7602                            #[cfg(not(unix))]
7603                            'x' => false,
7604                            #[cfg(unix)]
7605                            'o' => crate::perl_fs::filetest_owned_effective(&path),
7606                            #[cfg(not(unix))]
7607                            'o' => false,
7608                            #[cfg(unix)]
7609                            'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
7610                            #[cfg(not(unix))]
7611                            'R' => false,
7612                            #[cfg(unix)]
7613                            'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
7614                            #[cfg(not(unix))]
7615                            'W' => false,
7616                            #[cfg(unix)]
7617                            'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
7618                            #[cfg(not(unix))]
7619                            'X' => false,
7620                            #[cfg(unix)]
7621                            'O' => crate::perl_fs::filetest_owned_real(&path),
7622                            #[cfg(not(unix))]
7623                            'O' => false,
7624                            'z' => std::fs::metadata(&path)
7625                                .map(|m| m.len() == 0)
7626                                .unwrap_or(true),
7627                            't' => crate::perl_fs::filetest_is_tty(&path),
7628                            #[cfg(unix)]
7629                            'p' => crate::perl_fs::filetest_is_pipe(&path),
7630                            #[cfg(not(unix))]
7631                            'p' => false,
7632                            #[cfg(unix)]
7633                            'S' => crate::perl_fs::filetest_is_socket(&path),
7634                            #[cfg(not(unix))]
7635                            'S' => false,
7636                            #[cfg(unix)]
7637                            'b' => crate::perl_fs::filetest_is_block_device(&path),
7638                            #[cfg(not(unix))]
7639                            'b' => false,
7640                            #[cfg(unix)]
7641                            'c' => crate::perl_fs::filetest_is_char_device(&path),
7642                            #[cfg(not(unix))]
7643                            'c' => false,
7644                            #[cfg(unix)]
7645                            'u' => crate::perl_fs::filetest_is_setuid(&path),
7646                            #[cfg(not(unix))]
7647                            'u' => false,
7648                            #[cfg(unix)]
7649                            'g' => crate::perl_fs::filetest_is_setgid(&path),
7650                            #[cfg(not(unix))]
7651                            'g' => false,
7652                            #[cfg(unix)]
7653                            'k' => crate::perl_fs::filetest_is_sticky(&path),
7654                            #[cfg(not(unix))]
7655                            'k' => false,
7656                            'T' => crate::perl_fs::filetest_is_text(&path),
7657                            'B' => crate::perl_fs::filetest_is_binary(&path),
7658                            _ => false,
7659                        };
7660                        self.push(StrykeValue::integer(if result { 1 } else { 0 }));
7661                        Ok(())
7662                    }
7663
7664                    // ── Map/Grep/Sort with blocks (opcodes when lowered; else AST block fallback) ──
7665                    Op::MapIntMul(k) => {
7666                        let list = self.pop().to_list();
7667                        if list.len() == 1 {
7668                            if let Some(p) = list[0].as_pipeline() {
7669                                let line = self.line();
7670                                let sub = VMHelper::pipeline_int_mul_sub(*k);
7671                                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
7672                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
7673                                return Ok(());
7674                            }
7675                        }
7676                        let mut result = Vec::with_capacity(list.len());
7677                        for item in list {
7678                            let n = item.to_int();
7679                            result.push(StrykeValue::integer(n.wrapping_mul(*k)));
7680                        }
7681                        self.push(StrykeValue::array(result));
7682                        Ok(())
7683                    }
7684                    Op::GrepIntModEq(m, r) => {
7685                        let list = self.pop().to_list();
7686                        let mut result = Vec::new();
7687                        for item in list {
7688                            let n = item.to_int();
7689                            if n % m == *r {
7690                                result.push(item);
7691                            }
7692                        }
7693                        self.push(StrykeValue::array(result));
7694                        Ok(())
7695                    }
7696                    Op::MapWithBlock(block_idx) => {
7697                        let list = self.pop().to_list();
7698                        self.map_with_block_common(list, *block_idx, false, op_count)
7699                    }
7700                    Op::FlatMapWithBlock(block_idx) => {
7701                        let list = self.pop().to_list();
7702                        self.map_with_block_common(list, *block_idx, true, op_count)
7703                    }
7704                    Op::MapWithExpr(expr_idx) => {
7705                        let list = self.pop().to_list();
7706                        self.map_with_expr_common(list, *expr_idx, false, op_count)
7707                    }
7708                    Op::FlatMapWithExpr(expr_idx) => {
7709                        let list = self.pop().to_list();
7710                        self.map_with_expr_common(list, *expr_idx, true, op_count)
7711                    }
7712                    Op::MapsWithBlock(block_idx) => {
7713                        let val = self.pop();
7714                        let block = self.blocks[*block_idx as usize].clone();
7715                        let out =
7716                            self.interp
7717                                .map_stream_block_output(val, &block, false, self.line())?;
7718                        self.push(out);
7719                        Ok(())
7720                    }
7721                    Op::MapsFlatMapWithBlock(block_idx) => {
7722                        let val = self.pop();
7723                        let block = self.blocks[*block_idx as usize].clone();
7724                        let out =
7725                            self.interp
7726                                .map_stream_block_output(val, &block, true, self.line())?;
7727                        self.push(out);
7728                        Ok(())
7729                    }
7730                    Op::MapsWithExpr(expr_idx) => {
7731                        let val = self.pop();
7732                        let idx = *expr_idx as usize;
7733                        let expr = self.map_expr_entries[idx].clone();
7734                        let out =
7735                            self.interp
7736                                .map_stream_expr_output(val, &expr, false, self.line())?;
7737                        self.push(out);
7738                        Ok(())
7739                    }
7740                    Op::MapsFlatMapWithExpr(expr_idx) => {
7741                        let val = self.pop();
7742                        let idx = *expr_idx as usize;
7743                        let expr = self.map_expr_entries[idx].clone();
7744                        let out =
7745                            self.interp
7746                                .map_stream_expr_output(val, &expr, true, self.line())?;
7747                        self.push(out);
7748                        Ok(())
7749                    }
7750                    Op::FilterWithBlock(block_idx) => {
7751                        let val = self.pop();
7752                        let block = self.blocks[*block_idx as usize].clone();
7753                        let out =
7754                            self.interp
7755                                .filter_stream_block_output(val, &block, self.line())?;
7756                        self.push(out);
7757                        Ok(())
7758                    }
7759                    Op::FilterWithExpr(expr_idx) => {
7760                        let val = self.pop();
7761                        let idx = *expr_idx as usize;
7762                        let expr = self.grep_expr_entries[idx].clone();
7763                        let out = self
7764                            .interp
7765                            .filter_stream_expr_output(val, &expr, self.line())?;
7766                        self.push(out);
7767                        Ok(())
7768                    }
7769                    Op::ChunkByWithBlock(block_idx) => {
7770                        let list = self.pop().to_list();
7771                        self.chunk_by_with_block_common(list, *block_idx, op_count)
7772                    }
7773                    Op::ChunkByWithExpr(expr_idx) => {
7774                        let list = self.pop().to_list();
7775                        self.chunk_by_with_expr_common(list, *expr_idx, op_count)
7776                    }
7777                    Op::GrepWithBlock(block_idx) => {
7778                        let list = self.pop().to_list();
7779                        if list.len() == 1 {
7780                            if let Some(p) = list[0].as_pipeline() {
7781                                let idx = *block_idx as usize;
7782                                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
7783                                let line = self.line();
7784                                self.interp
7785                                    .pipeline_push(&p, PipelineOp::Filter(sub), line)?;
7786                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
7787                                return Ok(());
7788                            }
7789                        }
7790                        let idx = *block_idx as usize;
7791                        // Save / restore the topic chain across the iter
7792                        // loop so this grep stage doesn't leak its final
7793                        // `_` (or chain shift) into the enclosing block.
7794                        // Mirror of the map fix above.
7795                        let saved_chain = self.interp.scope.save_topic_chain();
7796                        if let Some(&(start, end)) =
7797                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7798                        {
7799                            let mut result = Vec::new();
7800                            for item in list {
7801                                self.interp.scope.set_topic(item.clone());
7802                                let val = self.run_block_region(start, end, op_count)?;
7803                                // Bare regex → match against $_ (Perl: /pat/ in grep is $_ =~ /pat/)
7804                                let keep = if let Some(re) = val.as_regex() {
7805                                    re.is_match(&item.to_string())
7806                                } else {
7807                                    val.is_true()
7808                                };
7809                                if keep {
7810                                    result.push(item);
7811                                }
7812                            }
7813                            self.interp.scope.restore_topic_chain(saved_chain);
7814                            self.push(StrykeValue::array(result));
7815                            Ok(())
7816                        } else {
7817                            let block = self.blocks[idx].clone();
7818                            let mut result = Vec::new();
7819                            for item in list {
7820                                self.interp.scope.set_topic(item.clone());
7821                                match self.interp.exec_block(&block) {
7822                                    Ok(val) => {
7823                                        let keep = if let Some(re) = val.as_regex() {
7824                                            re.is_match(&item.to_string())
7825                                        } else {
7826                                            val.is_true()
7827                                        };
7828                                        if keep {
7829                                            result.push(item);
7830                                        }
7831                                    }
7832                                    Err(crate::vm_helper::FlowOrError::Error(e)) => {
7833                                        self.interp.scope.restore_topic_chain(saved_chain);
7834                                        return Err(e);
7835                                    }
7836                                    Err(_) => {}
7837                                }
7838                            }
7839                            self.interp.scope.restore_topic_chain(saved_chain);
7840                            self.push(StrykeValue::array(result));
7841                            Ok(())
7842                        }
7843                    }
7844                    Op::ForEachWithBlock(block_idx) => {
7845                        let val = self.pop();
7846                        let idx = *block_idx as usize;
7847                        // Save / restore the topic chain so this foreach
7848                        // doesn't leak its final `_` into the enclosing
7849                        // block (mirror of the map/grep fix above).
7850                        let saved_chain = self.interp.scope.save_topic_chain();
7851                        // Lazy iterator: consume one-at-a-time without materializing.
7852                        if val.is_iterator() {
7853                            let iter = val.into_iterator();
7854                            let mut count = 0i64;
7855                            if let Some(&(start, end)) =
7856                                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7857                            {
7858                                while let Some(item) = iter.next_item() {
7859                                    count += 1;
7860                                    self.interp.scope.set_topic(item);
7861                                    if let Err(e) = self.run_block_region(start, end, op_count) {
7862                                        self.interp.scope.restore_topic_chain(saved_chain);
7863                                        return Err(e);
7864                                    }
7865                                }
7866                            } else {
7867                                let block = self.blocks[idx].clone();
7868                                while let Some(item) = iter.next_item() {
7869                                    count += 1;
7870                                    self.interp.scope.set_topic(item);
7871                                    match self.interp.exec_block(&block) {
7872                                        Ok(_) => {}
7873                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
7874                                            self.interp.scope.restore_topic_chain(saved_chain);
7875                                            return Err(e);
7876                                        }
7877                                        Err(_) => {}
7878                                    }
7879                                }
7880                            }
7881                            self.interp.scope.restore_topic_chain(saved_chain);
7882                            self.push(StrykeValue::integer(count));
7883                            return Ok(());
7884                        }
7885                        let list = val.to_list();
7886                        let count = list.len() as i64;
7887                        if let Some(&(start, end)) =
7888                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7889                        {
7890                            for item in list {
7891                                self.interp.scope.set_topic(item);
7892                                if let Err(e) = self.run_block_region(start, end, op_count) {
7893                                    self.interp.scope.restore_topic_chain(saved_chain);
7894                                    return Err(e);
7895                                }
7896                            }
7897                        } else {
7898                            let block = self.blocks[idx].clone();
7899                            for item in list {
7900                                self.interp.scope.set_topic(item);
7901                                match self.interp.exec_block(&block) {
7902                                    Ok(_) => {}
7903                                    Err(crate::vm_helper::FlowOrError::Error(e)) => {
7904                                        self.interp.scope.restore_topic_chain(saved_chain);
7905                                        return Err(e);
7906                                    }
7907                                    Err(_) => {}
7908                                }
7909                            }
7910                        }
7911                        self.interp.scope.restore_topic_chain(saved_chain);
7912                        self.push(StrykeValue::integer(count));
7913                        Ok(())
7914                    }
7915                    Op::GrepWithExpr(expr_idx) => {
7916                        let list = self.pop().to_list();
7917                        let idx = *expr_idx as usize;
7918                        let dispatch_coderef = !crate::compat_mode();
7919                        // EXPR-form: see `map_with_expr_common` — no `{}` block
7920                        // boundary, so use `set_topic_local` (no chain shift,
7921                        // no slot 1+ zero).
7922                        if let Some(&(start, end)) = self
7923                            .grep_expr_bytecode_ranges
7924                            .get(idx)
7925                            .and_then(|r| r.as_ref())
7926                        {
7927                            let mut result = Vec::new();
7928                            for item in list {
7929                                self.interp.scope.set_topic_local(item.clone());
7930                                let val = self.run_block_region(start, end, op_count)?;
7931                                let val = self.maybe_call_coderef_with_item(
7932                                    val,
7933                                    &item,
7934                                    dispatch_coderef,
7935                                )?;
7936                                let keep = if let Some(re) = val.as_regex() {
7937                                    re.is_match(&item.to_string())
7938                                } else {
7939                                    val.is_true()
7940                                };
7941                                if keep {
7942                                    result.push(item);
7943                                }
7944                            }
7945                            self.push(StrykeValue::array(result));
7946                            Ok(())
7947                        } else {
7948                            let e = self.grep_expr_entries[idx].clone();
7949                            let mut result = Vec::new();
7950                            for item in list {
7951                                self.interp.scope.set_topic_local(item.clone());
7952                                let val = vm_interp_result(self.interp.eval_expr(&e), self.line())?;
7953                                let val = self.maybe_call_coderef_with_item(
7954                                    val,
7955                                    &item,
7956                                    dispatch_coderef,
7957                                )?;
7958                                let keep = if let Some(re) = val.as_regex() {
7959                                    re.is_match(&item.to_string())
7960                                } else {
7961                                    val.is_true()
7962                                };
7963                                if keep {
7964                                    result.push(item);
7965                                }
7966                            }
7967                            self.push(StrykeValue::array(result));
7968                            Ok(())
7969                        }
7970                    }
7971                    Op::SortWithBlock(block_idx) => {
7972                        let mut items = self.pop().to_list();
7973                        let idx = *block_idx as usize;
7974                        // Save the topic chain before sort — set_sort_pair writes to $_
7975                        // which would corrupt _< for subsequent pipeline stages (grep, map).
7976                        let saved_topic = self.interp.scope.save_topic_chain();
7977                        if let Some(&(start, end)) =
7978                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7979                        {
7980                            let mut sort_err: Option<StrykeError> = None;
7981                            items.sort_by(|a, b| {
7982                                if sort_err.is_some() {
7983                                    return std::cmp::Ordering::Equal;
7984                                }
7985                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
7986                                match self.run_block_region(start, end, op_count) {
7987                                    Ok(v) => {
7988                                        let n = v.to_int();
7989                                        if n < 0 {
7990                                            std::cmp::Ordering::Less
7991                                        } else if n > 0 {
7992                                            std::cmp::Ordering::Greater
7993                                        } else {
7994                                            std::cmp::Ordering::Equal
7995                                        }
7996                                    }
7997                                    Err(e) => {
7998                                        sort_err = Some(e);
7999                                        std::cmp::Ordering::Equal
8000                                    }
8001                                }
8002                            });
8003                            self.interp.scope.restore_topic_chain(saved_topic);
8004                            if let Some(e) = sort_err {
8005                                return Err(e);
8006                            }
8007                            self.push(StrykeValue::array(items));
8008                            Ok(())
8009                        } else {
8010                            let block = self.blocks[idx].clone();
8011                            items.sort_by(|a, b| {
8012                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
8013                                match self.interp.exec_block(&block) {
8014                                    Ok(v) => {
8015                                        let n = v.to_int();
8016                                        if n < 0 {
8017                                            std::cmp::Ordering::Less
8018                                        } else if n > 0 {
8019                                            std::cmp::Ordering::Greater
8020                                        } else {
8021                                            std::cmp::Ordering::Equal
8022                                        }
8023                                    }
8024                                    Err(_) => std::cmp::Ordering::Equal,
8025                                }
8026                            });
8027                            self.interp.scope.restore_topic_chain(saved_topic);
8028                            self.push(StrykeValue::array(items));
8029                            Ok(())
8030                        }
8031                    }
8032                    Op::SortWithBlockFast(tag) => {
8033                        let mut items = self.pop().to_list();
8034                        let mode = match *tag {
8035                            0 => SortBlockFast::Numeric,
8036                            1 => SortBlockFast::String,
8037                            2 => SortBlockFast::NumericRev,
8038                            3 => SortBlockFast::StringRev,
8039                            _ => SortBlockFast::Numeric,
8040                        };
8041                        items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
8042                        self.push(StrykeValue::array(items));
8043                        Ok(())
8044                    }
8045                    Op::SortNoBlock => {
8046                        let mut items = self.pop().to_list();
8047                        items.sort_by_key(|a| a.to_string());
8048                        self.push(StrykeValue::array(items));
8049                        Ok(())
8050                    }
8051                    Op::SortWithCodeComparator(wa) => {
8052                        let want = WantarrayCtx::from_byte(*wa);
8053                        let cmp_val = self.pop();
8054                        let mut items = self.pop().to_list();
8055                        let line = self.line();
8056                        let Some(sub) = cmp_val.as_code_ref() else {
8057                            return Err(StrykeError::runtime(
8058                                "sort: comparator must be a code reference",
8059                                line,
8060                            ));
8061                        };
8062                        let interp = &mut self.interp;
8063                        items.sort_by(|a, b| {
8064                            // `set_sort_pair` keeps Perl-style `$a`/`$b` access;
8065                            // positional args let stryke lambdas read via @_.
8066                            interp.scope.set_sort_pair(a.clone(), b.clone());
8067                            match interp.call_sub(
8068                                sub.as_ref(),
8069                                vec![a.clone(), b.clone()],
8070                                want,
8071                                line,
8072                            ) {
8073                                Ok(v) => {
8074                                    let n = v.to_int();
8075                                    if n < 0 {
8076                                        std::cmp::Ordering::Less
8077                                    } else if n > 0 {
8078                                        std::cmp::Ordering::Greater
8079                                    } else {
8080                                        std::cmp::Ordering::Equal
8081                                    }
8082                                }
8083                                Err(_) => std::cmp::Ordering::Equal,
8084                            }
8085                        });
8086                        self.push(StrykeValue::array(items));
8087                        Ok(())
8088                    }
8089                    Op::ReverseListOp => {
8090                        let val = self.pop();
8091                        if val.is_iterator() {
8092                            self.push(StrykeValue::iterator(std::sync::Arc::new(
8093                                crate::value::RevIterator::new(val.into_iterator()),
8094                            )));
8095                        } else {
8096                            let mut items = val.to_list();
8097                            items.reverse();
8098                            self.push(StrykeValue::array(items));
8099                        }
8100                        Ok(())
8101                    }
8102                    Op::ReverseScalarOp => {
8103                        let val = self.pop();
8104                        let items = val.to_list();
8105                        let s: String = items.iter().map(|v| v.to_string()).collect();
8106                        self.push(StrykeValue::string(s.chars().rev().collect()));
8107                        Ok(())
8108                    }
8109                    Op::RevListOp => {
8110                        let val = self.pop();
8111                        if val.is_iterator() {
8112                            // Collect the iterator fully and reverse the list order.
8113                            // RevIterator does per-element char reversal, not list reversal.
8114                            let mut items = val.to_list();
8115                            items.reverse();
8116                            self.push(StrykeValue::array(items));
8117                        } else if let Some(s) = crate::value::set_payload(&val) {
8118                            let mut out = crate::value::PerlSet::new();
8119                            for (k, v) in s.iter().rev() {
8120                                out.insert(k.clone(), v.clone());
8121                            }
8122                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
8123                        } else if let Some(ar) = val.as_array_ref() {
8124                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
8125                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
8126                                parking_lot::RwLock::new(items),
8127                            )));
8128                        } else if let Some(hr) = val.as_hash_ref() {
8129                            let mut out: indexmap::IndexMap<String, StrykeValue> =
8130                                indexmap::IndexMap::new();
8131                            for (k, v) in hr.read().iter() {
8132                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
8133                            }
8134                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
8135                                parking_lot::RwLock::new(out),
8136                            )));
8137                        } else if let Some(hm) = val.as_hash_map() {
8138                            let mut out: indexmap::IndexMap<String, StrykeValue> =
8139                                indexmap::IndexMap::new();
8140                            for (k, v) in hm.iter() {
8141                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
8142                            }
8143                            self.push(StrykeValue::hash(out));
8144                        } else if val.as_array_vec().is_some() {
8145                            let mut items = val.to_list();
8146                            items.reverse();
8147                            self.push(StrykeValue::array(items));
8148                        } else {
8149                            let s = val.to_string();
8150                            self.push(StrykeValue::string(s.chars().rev().collect()));
8151                        }
8152                        Ok(())
8153                    }
8154                    Op::RevScalarOp => {
8155                        let val = self.pop();
8156                        if let Some(s) = crate::value::set_payload(&val) {
8157                            let mut out = crate::value::PerlSet::new();
8158                            for (k, v) in s.iter().rev() {
8159                                out.insert(k.clone(), v.clone());
8160                            }
8161                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
8162                        } else if let Some(ar) = val.as_array_ref() {
8163                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
8164                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
8165                                parking_lot::RwLock::new(items),
8166                            )));
8167                        } else if let Some(hr) = val.as_hash_ref() {
8168                            let mut out: indexmap::IndexMap<String, StrykeValue> =
8169                                indexmap::IndexMap::new();
8170                            for (k, v) in hr.read().iter() {
8171                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
8172                            }
8173                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
8174                                parking_lot::RwLock::new(out),
8175                            )));
8176                        } else {
8177                            let items = val.to_list();
8178                            let s: String = items.iter().map(|v| v.to_string()).collect();
8179                            self.push(StrykeValue::string(s.chars().rev().collect()));
8180                        }
8181                        Ok(())
8182                    }
8183                    Op::StackArrayLen => {
8184                        let v = self.pop();
8185                        self.push(StrykeValue::integer(v.to_list().len() as i64));
8186                        Ok(())
8187                    }
8188                    Op::ListSliceToScalar => {
8189                        let v = self.pop();
8190                        let items = v.to_list();
8191                        self.push(items.last().cloned().unwrap_or(StrykeValue::UNDEF));
8192                        Ok(())
8193                    }
8194
8195                    // ── Eval block ──
8196                    Op::EvalBlock(block_idx, want) => {
8197                        let block = self.blocks[*block_idx as usize].clone();
8198                        let tail = crate::vm_helper::WantarrayCtx::from_byte(*want);
8199                        self.interp.eval_nesting += 1;
8200                        // Use exec_block (with scope frame) so local/my declarations
8201                        // inside the block are properly scoped.
8202                        match self.interp.exec_block_with_tail(&block, tail) {
8203                            Ok(v) => {
8204                                self.interp.clear_eval_error();
8205                                self.push(v);
8206                            }
8207                            Err(crate::vm_helper::FlowOrError::Error(e)) => {
8208                                self.interp.set_eval_error_from_perl_error(&e);
8209                                self.push(StrykeValue::UNDEF);
8210                            }
8211                            Err(_) => self.push(StrykeValue::UNDEF),
8212                        }
8213                        self.interp.eval_nesting -= 1;
8214                        Ok(())
8215                    }
8216                    Op::TraceBlock(block_idx) => {
8217                        let block = self.blocks[*block_idx as usize].clone();
8218                        crate::parallel_trace::trace_enter();
8219                        self.interp.eval_nesting += 1;
8220                        match self.interp.exec_block(&block) {
8221                            Ok(v) => {
8222                                self.interp.clear_eval_error();
8223                                self.push(v);
8224                            }
8225                            Err(FlowOrError::Error(e)) => {
8226                                self.interp.set_eval_error_from_perl_error(&e);
8227                                self.push(StrykeValue::UNDEF);
8228                            }
8229                            Err(_) => self.push(StrykeValue::UNDEF),
8230                        }
8231                        self.interp.eval_nesting -= 1;
8232                        crate::parallel_trace::trace_leave();
8233                        Ok(())
8234                    }
8235                    Op::TimerBlock(block_idx) => {
8236                        let block = self.blocks[*block_idx as usize].clone();
8237                        let start = std::time::Instant::now();
8238                        self.interp.eval_nesting += 1;
8239                        let _ = match self.interp.exec_block(&block) {
8240                            Ok(v) => {
8241                                self.interp.clear_eval_error();
8242                                v
8243                            }
8244                            Err(FlowOrError::Error(e)) => {
8245                                self.interp.set_eval_error_from_perl_error(&e);
8246                                StrykeValue::UNDEF
8247                            }
8248                            Err(_) => StrykeValue::UNDEF,
8249                        };
8250                        self.interp.eval_nesting -= 1;
8251                        let ms = start.elapsed().as_secs_f64() * 1000.0;
8252                        self.push(StrykeValue::float(ms));
8253                        Ok(())
8254                    }
8255                    Op::BenchBlock(block_idx) => {
8256                        let n_i = self.pop().to_int();
8257                        if n_i < 0 {
8258                            return Err(StrykeError::runtime(
8259                                "bench: iteration count must be non-negative",
8260                                self.line(),
8261                            ));
8262                        }
8263                        let n = n_i as usize;
8264                        let block = self.blocks[*block_idx as usize].clone();
8265                        let v = vm_interp_result(
8266                            self.interp.run_bench_block(&block, n, self.line()),
8267                            self.line(),
8268                        )?;
8269                        self.push(v);
8270                        Ok(())
8271                    }
8272                    Op::Given(idx) => {
8273                        let i = *idx as usize;
8274                        let line = self.line();
8275                        let v = if let Some(&(start, end)) = self
8276                            .given_topic_bytecode_ranges
8277                            .get(i)
8278                            .and_then(|r| r.as_ref())
8279                        {
8280                            let topic_val = self.run_block_region(start, end, op_count)?;
8281                            let body = &self.given_entries[i].1;
8282                            vm_interp_result(
8283                                self.interp.exec_given_with_topic_value(topic_val, body),
8284                                line,
8285                            )?
8286                        } else {
8287                            let (topic, body) = &self.given_entries[i];
8288                            vm_interp_result(self.interp.exec_given(topic, body), line)?
8289                        };
8290                        self.push(v);
8291                        Ok(())
8292                    }
8293                    Op::EvalTimeout(idx) => {
8294                        let i = *idx as usize;
8295                        let body = self.eval_timeout_entries[i].1.clone();
8296                        let secs = if let Some(&(start, end)) = self
8297                            .eval_timeout_expr_bytecode_ranges
8298                            .get(i)
8299                            .and_then(|r| r.as_ref())
8300                        {
8301                            self.run_block_region(start, end, op_count)?.to_number()
8302                        } else {
8303                            let timeout_expr = &self.eval_timeout_entries[i].0;
8304                            vm_interp_result(self.interp.eval_expr(timeout_expr), self.line())?
8305                                .to_number()
8306                        };
8307                        let v = vm_interp_result(
8308                            self.interp.eval_timeout_block(&body, secs, self.line()),
8309                            self.line(),
8310                        )?;
8311                        self.push(v);
8312                        Ok(())
8313                    }
8314                    Op::AlgebraicMatch(idx) => {
8315                        let i = *idx as usize;
8316                        let line = self.line();
8317                        let v = if let Some(&(start, end)) = self
8318                            .algebraic_match_subject_bytecode_ranges
8319                            .get(i)
8320                            .and_then(|r| r.as_ref())
8321                        {
8322                            let subject_val = self.run_block_region(start, end, op_count)?;
8323                            let arms = &self.algebraic_match_entries[i].1;
8324                            vm_interp_result(
8325                                self.interp.eval_algebraic_match_with_subject_value(
8326                                    subject_val,
8327                                    arms,
8328                                    line,
8329                                ),
8330                                self.line(),
8331                            )?
8332                        } else {
8333                            let (subject, arms) = &self.algebraic_match_entries[i];
8334                            vm_interp_result(
8335                                self.interp.eval_algebraic_match(subject, arms, line),
8336                                self.line(),
8337                            )?
8338                        };
8339                        self.push(v);
8340                        Ok(())
8341                    }
8342                    Op::ParLines(idx) => {
8343                        let (path, callback, progress) = &self.par_lines_entries[*idx as usize];
8344                        let v = vm_interp_result(
8345                            self.interp.eval_par_lines_expr(
8346                                path,
8347                                callback,
8348                                progress.as_ref(),
8349                                self.line(),
8350                            ),
8351                            self.line(),
8352                        )?;
8353                        self.push(v);
8354                        Ok(())
8355                    }
8356                    Op::ParWalk(idx) => {
8357                        let (path, callback, progress) = &self.par_walk_entries[*idx as usize];
8358                        let v = vm_interp_result(
8359                            self.interp.eval_par_walk_expr(
8360                                path,
8361                                callback,
8362                                progress.as_ref(),
8363                                self.line(),
8364                            ),
8365                            self.line(),
8366                        )?;
8367                        self.push(v);
8368                        Ok(())
8369                    }
8370                    Op::Pwatch(idx) => {
8371                        let (path, callback) = &self.pwatch_entries[*idx as usize];
8372                        let v = vm_interp_result(
8373                            self.interp.eval_pwatch_expr(path, callback, self.line()),
8374                            self.line(),
8375                        )?;
8376                        self.push(v);
8377                        Ok(())
8378                    }
8379
8380                    // ── Parallel operations (rayon) ──
8381                    Op::PMapWithBlock(block_idx) => {
8382                        let list = self.pop().to_list();
8383                        let progress_flag = self.pop().is_true();
8384                        let idx = *block_idx as usize;
8385                        let subs = self.interp.subs.clone();
8386                        let (scope_capture, atomic_arrays, atomic_hashes) =
8387                            self.interp.scope.capture_with_atomics();
8388                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8389                        let n_workers = rayon::current_num_threads();
8390                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8391                            .map(|_| {
8392                                let mut interp = VMHelper::new();
8393                                interp.subs = subs.clone();
8394                                interp.scope.restore_capture(&scope_capture);
8395                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8396                                interp.enable_parallel_guard();
8397                                Mutex::new(interp)
8398                            })
8399                            .collect();
8400                        if let Some(&(start, end)) =
8401                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8402                        {
8403                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8404                            let results: Vec<StrykeValue> = list
8405                                .into_par_iter()
8406                                .map(|item| {
8407                                    let tid =
8408                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8409                                    let mut local_interp = pool[tid].lock();
8410                                    local_interp.scope.set_topic(item);
8411                                    let mut vm = shared.worker_vm(&mut local_interp);
8412                                    let mut op_count = 0u64;
8413                                    let val = match vm.run_block_region(start, end, &mut op_count) {
8414                                        Ok(v) => v,
8415                                        Err(_) => StrykeValue::UNDEF,
8416                                    };
8417                                    pmap_progress.tick();
8418                                    val
8419                                })
8420                                .collect();
8421                            pmap_progress.finish();
8422                            self.push(StrykeValue::array(results));
8423                            Ok(())
8424                        } else {
8425                            let block = self.blocks[idx].clone();
8426                            let results: Vec<StrykeValue> = list
8427                                .into_par_iter()
8428                                .map(|item| {
8429                                    let tid =
8430                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8431                                    let mut local_interp = pool[tid].lock();
8432                                    local_interp.scope.set_topic(item);
8433                                    local_interp.scope_push_hook();
8434                                    let val = match local_interp.exec_block_no_scope(&block) {
8435                                        Ok(val) => val,
8436                                        Err(_) => StrykeValue::UNDEF,
8437                                    };
8438                                    local_interp.scope_pop_hook();
8439                                    pmap_progress.tick();
8440                                    val
8441                                })
8442                                .collect();
8443                            pmap_progress.finish();
8444                            self.push(StrykeValue::array(results));
8445                            Ok(())
8446                        }
8447                    }
8448                    Op::PFlatMapWithBlock(block_idx) => {
8449                        let list = self.pop().to_list();
8450                        let progress_flag = self.pop().is_true();
8451                        let idx = *block_idx as usize;
8452                        let subs = self.interp.subs.clone();
8453                        let (scope_capture, atomic_arrays, atomic_hashes) =
8454                            self.interp.scope.capture_with_atomics();
8455                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8456                        let n_workers = rayon::current_num_threads();
8457                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8458                            .map(|_| {
8459                                let mut interp = VMHelper::new();
8460                                interp.subs = subs.clone();
8461                                interp.scope.restore_capture(&scope_capture);
8462                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8463                                interp.enable_parallel_guard();
8464                                Mutex::new(interp)
8465                            })
8466                            .collect();
8467                        if let Some(&(start, end)) =
8468                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8469                        {
8470                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8471                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
8472                                .into_par_iter()
8473                                .enumerate()
8474                                .map(|(i, item)| {
8475                                    let tid =
8476                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8477                                    let mut local_interp = pool[tid].lock();
8478                                    local_interp.scope.set_topic(item);
8479                                    let mut vm = shared.worker_vm(&mut local_interp);
8480                                    let mut op_count = 0u64;
8481                                    let val = match vm.run_block_region(start, end, &mut op_count) {
8482                                        Ok(v) => v,
8483                                        Err(_) => StrykeValue::UNDEF,
8484                                    };
8485                                    let out = val.map_flatten_outputs(true);
8486                                    pmap_progress.tick();
8487                                    (i, out)
8488                                })
8489                                .collect();
8490                            pmap_progress.finish();
8491                            indexed.sort_by_key(|(i, _)| *i);
8492                            let results: Vec<StrykeValue> =
8493                                indexed.into_iter().flat_map(|(_, v)| v).collect();
8494                            self.push(StrykeValue::array(results));
8495                            Ok(())
8496                        } else {
8497                            let block = self.blocks[idx].clone();
8498                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
8499                                .into_par_iter()
8500                                .enumerate()
8501                                .map(|(i, item)| {
8502                                    let tid =
8503                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8504                                    let mut local_interp = pool[tid].lock();
8505                                    local_interp.scope.set_topic(item);
8506                                    local_interp.scope_push_hook();
8507                                    let val = match local_interp.exec_block_no_scope(&block) {
8508                                        Ok(val) => val,
8509                                        Err(_) => StrykeValue::UNDEF,
8510                                    };
8511                                    local_interp.scope_pop_hook();
8512                                    let out = val.map_flatten_outputs(true);
8513                                    pmap_progress.tick();
8514                                    (i, out)
8515                                })
8516                                .collect();
8517                            pmap_progress.finish();
8518                            indexed.sort_by_key(|(i, _)| *i);
8519                            let results: Vec<StrykeValue> =
8520                                indexed.into_iter().flat_map(|(_, v)| v).collect();
8521                            self.push(StrykeValue::array(results));
8522                            Ok(())
8523                        }
8524                    }
8525                    Op::PMapRemote { block_idx, flat } => {
8526                        let cluster = self.pop();
8527                        let list_pv = self.pop();
8528                        let progress_flag = self.pop().is_true();
8529                        let idx = *block_idx as usize;
8530                        let block = self.blocks[idx].clone();
8531                        let flat_outputs = *flat != 0;
8532                        let v = vm_interp_result(
8533                            self.interp.eval_pmap_remote(
8534                                cluster,
8535                                list_pv,
8536                                progress_flag,
8537                                &block,
8538                                flat_outputs,
8539                                self.line(),
8540                            ),
8541                            self.line(),
8542                        )?;
8543                        self.push(v);
8544                        Ok(())
8545                    }
8546                    Op::Puniq => {
8547                        let list = self.pop().to_list();
8548                        let progress_flag = self.pop().is_true();
8549                        let n_threads = self.interp.parallel_thread_count();
8550                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8551                        let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
8552                        pmap_progress.finish();
8553                        self.push(StrykeValue::array(out));
8554                        Ok(())
8555                    }
8556                    Op::PFirstWithBlock(block_idx) => {
8557                        let list = self.pop().to_list();
8558                        let progress_flag = self.pop().is_true();
8559                        let idx = *block_idx as usize;
8560                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8561                        let subs = self.interp.subs.clone();
8562                        let (scope_capture, atomic_arrays, atomic_hashes) =
8563                            self.interp.scope.capture_with_atomics();
8564                        let out = if let Some(&(start, end)) =
8565                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8566                        {
8567                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8568                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
8569                                let mut local_interp = VMHelper::new();
8570                                local_interp.subs = subs.clone();
8571                                local_interp.scope.restore_capture(&scope_capture);
8572                                local_interp
8573                                    .scope
8574                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8575                                local_interp.enable_parallel_guard();
8576                                local_interp.scope.set_topic(item);
8577                                let mut vm = shared.worker_vm(&mut local_interp);
8578                                let mut op_count = 0u64;
8579                                match vm.run_block_region(start, end, &mut op_count) {
8580                                    Ok(v) => v.is_true(),
8581                                    Err(_) => false,
8582                                }
8583                            })
8584                        } else {
8585                            let block = self.blocks[idx].clone();
8586                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
8587                                let mut local_interp = VMHelper::new();
8588                                local_interp.subs = subs.clone();
8589                                local_interp.scope.restore_capture(&scope_capture);
8590                                local_interp
8591                                    .scope
8592                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8593                                local_interp.enable_parallel_guard();
8594                                local_interp.scope.set_topic(item);
8595                                local_interp.scope_push_hook();
8596                                let ok = match local_interp.exec_block_no_scope(&block) {
8597                                    Ok(v) => v.is_true(),
8598                                    Err(_) => false,
8599                                };
8600                                local_interp.scope_pop_hook();
8601                                ok
8602                            })
8603                        };
8604                        pmap_progress.finish();
8605                        self.push(out.unwrap_or(StrykeValue::UNDEF));
8606                        Ok(())
8607                    }
8608                    Op::PAnyWithBlock(block_idx) => {
8609                        let list = self.pop().to_list();
8610                        let progress_flag = self.pop().is_true();
8611                        let idx = *block_idx as usize;
8612                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8613                        let subs = self.interp.subs.clone();
8614                        let (scope_capture, atomic_arrays, atomic_hashes) =
8615                            self.interp.scope.capture_with_atomics();
8616                        let b = if let Some(&(start, end)) =
8617                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8618                        {
8619                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8620                            crate::par_list::pany_run(list, &pmap_progress, |item| {
8621                                let mut local_interp = VMHelper::new();
8622                                local_interp.subs = subs.clone();
8623                                local_interp.scope.restore_capture(&scope_capture);
8624                                local_interp
8625                                    .scope
8626                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8627                                local_interp.enable_parallel_guard();
8628                                local_interp.scope.set_topic(item);
8629                                let mut vm = shared.worker_vm(&mut local_interp);
8630                                let mut op_count = 0u64;
8631                                match vm.run_block_region(start, end, &mut op_count) {
8632                                    Ok(v) => v.is_true(),
8633                                    Err(_) => false,
8634                                }
8635                            })
8636                        } else {
8637                            let block = self.blocks[idx].clone();
8638                            crate::par_list::pany_run(list, &pmap_progress, |item| {
8639                                let mut local_interp = VMHelper::new();
8640                                local_interp.subs = subs.clone();
8641                                local_interp.scope.restore_capture(&scope_capture);
8642                                local_interp
8643                                    .scope
8644                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8645                                local_interp.enable_parallel_guard();
8646                                local_interp.scope.set_topic(item);
8647                                local_interp.scope_push_hook();
8648                                let ok = match local_interp.exec_block_no_scope(&block) {
8649                                    Ok(v) => v.is_true(),
8650                                    Err(_) => false,
8651                                };
8652                                local_interp.scope_pop_hook();
8653                                ok
8654                            })
8655                        };
8656                        pmap_progress.finish();
8657                        self.push(StrykeValue::integer(if b { 1 } else { 0 }));
8658                        Ok(())
8659                    }
8660                    Op::PMapChunkedWithBlock(block_idx) => {
8661                        let list = self.pop().to_list();
8662                        let chunk_n = self.pop().to_int().max(1) as usize;
8663                        let progress_flag = self.pop().is_true();
8664                        let idx = *block_idx as usize;
8665                        let subs = self.interp.subs.clone();
8666                        let (scope_capture, atomic_arrays, atomic_hashes) =
8667                            self.interp.scope.capture_with_atomics();
8668                        let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = list
8669                            .chunks(chunk_n)
8670                            .enumerate()
8671                            .map(|(i, c)| (i, c.to_vec()))
8672                            .collect();
8673                        let n_chunks = indexed_chunks.len();
8674                        let pmap_progress = PmapProgress::new(progress_flag, n_chunks);
8675                        if let Some(&(start, end)) =
8676                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8677                        {
8678                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8679                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
8680                                .into_par_iter()
8681                                .map(|(chunk_idx, chunk)| {
8682                                    let mut local_interp = VMHelper::new();
8683                                    local_interp.subs = subs.clone();
8684                                    local_interp.scope.restore_capture(&scope_capture);
8685                                    local_interp
8686                                        .scope
8687                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
8688                                    local_interp.enable_parallel_guard();
8689                                    let mut out = Vec::with_capacity(chunk.len());
8690                                    for item in chunk {
8691                                        local_interp.scope.set_topic(item);
8692                                        let mut vm = shared.worker_vm(&mut local_interp);
8693                                        let mut op_count = 0u64;
8694                                        let val =
8695                                            match vm.run_block_region(start, end, &mut op_count) {
8696                                                Ok(v) => v,
8697                                                Err(_) => StrykeValue::UNDEF,
8698                                            };
8699                                        out.push(val);
8700                                    }
8701                                    pmap_progress.tick();
8702                                    (chunk_idx, out)
8703                                })
8704                                .collect();
8705                            pmap_progress.finish();
8706                            chunk_results.sort_by_key(|(i, _)| *i);
8707                            let results: Vec<StrykeValue> =
8708                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
8709                            self.push(StrykeValue::array(results));
8710                            Ok(())
8711                        } else {
8712                            let block = self.blocks[idx].clone();
8713                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
8714                                .into_par_iter()
8715                                .map(|(chunk_idx, chunk)| {
8716                                    let mut local_interp = VMHelper::new();
8717                                    local_interp.subs = subs.clone();
8718                                    local_interp.scope.restore_capture(&scope_capture);
8719                                    local_interp
8720                                        .scope
8721                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
8722                                    local_interp.enable_parallel_guard();
8723                                    let mut out = Vec::with_capacity(chunk.len());
8724                                    for item in chunk {
8725                                        local_interp.scope.set_topic(item);
8726                                        local_interp.scope_push_hook();
8727                                        let val = match local_interp.exec_block_no_scope(&block) {
8728                                            Ok(val) => val,
8729                                            Err(_) => StrykeValue::UNDEF,
8730                                        };
8731                                        local_interp.scope_pop_hook();
8732                                        out.push(val);
8733                                    }
8734                                    pmap_progress.tick();
8735                                    (chunk_idx, out)
8736                                })
8737                                .collect();
8738                            pmap_progress.finish();
8739                            chunk_results.sort_by_key(|(i, _)| *i);
8740                            let results: Vec<StrykeValue> =
8741                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
8742                            self.push(StrykeValue::array(results));
8743                            Ok(())
8744                        }
8745                    }
8746                    Op::ReduceWithBlock(block_idx) => {
8747                        let list = self.pop().to_list();
8748                        let idx = *block_idx as usize;
8749                        let subs = self.interp.subs.clone();
8750                        let scope_capture = self.interp.scope.capture();
8751                        if list.is_empty() {
8752                            self.push(StrykeValue::UNDEF);
8753                            return Ok(());
8754                        }
8755                        if list.len() == 1 {
8756                            self.push(list.into_iter().next().unwrap());
8757                            return Ok(());
8758                        }
8759                        let mut items = list;
8760                        let mut acc = items.remove(0);
8761                        let rest = items;
8762                        if let Some(&(start, end)) =
8763                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8764                        {
8765                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8766                            for b in rest {
8767                                let mut local_interp = VMHelper::new();
8768                                local_interp.subs = subs.clone();
8769                                local_interp.scope.restore_capture(&scope_capture);
8770                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
8771                                let mut vm = shared.worker_vm(&mut local_interp);
8772                                let mut op_count = 0u64;
8773                                acc = match vm.run_block_region(start, end, &mut op_count) {
8774                                    Ok(v) => v,
8775                                    Err(_) => StrykeValue::UNDEF,
8776                                };
8777                            }
8778                        } else {
8779                            let block = self.blocks[idx].clone();
8780                            for b in rest {
8781                                let mut local_interp = VMHelper::new();
8782                                local_interp.subs = subs.clone();
8783                                local_interp.scope.restore_capture(&scope_capture);
8784                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
8785                                acc = match local_interp.exec_block(&block) {
8786                                    Ok(val) => val,
8787                                    Err(_) => StrykeValue::UNDEF,
8788                                };
8789                            }
8790                        }
8791                        self.push(acc);
8792                        Ok(())
8793                    }
8794                    Op::PReduceWithBlock(block_idx) => {
8795                        let list = self.pop().to_list();
8796                        let progress_flag = self.pop().is_true();
8797                        let idx = *block_idx as usize;
8798                        let subs = self.interp.subs.clone();
8799                        let scope_capture = self.interp.scope.capture();
8800                        if list.is_empty() {
8801                            self.push(StrykeValue::UNDEF);
8802                            return Ok(());
8803                        }
8804                        if list.len() == 1 {
8805                            self.push(list.into_iter().next().unwrap());
8806                            return Ok(());
8807                        }
8808                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8809                        if let Some(&(start, end)) =
8810                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8811                        {
8812                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8813                            let result = list
8814                                .into_par_iter()
8815                                .map(|x| {
8816                                    pmap_progress.tick();
8817                                    x
8818                                })
8819                                .reduce_with(|a, b| {
8820                                    let mut local_interp = VMHelper::new();
8821                                    local_interp.subs = subs.clone();
8822                                    local_interp.scope.restore_capture(&scope_capture);
8823                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8824                                    let mut vm = shared.worker_vm(&mut local_interp);
8825                                    let mut op_count = 0u64;
8826                                    match vm.run_block_region(start, end, &mut op_count) {
8827                                        Ok(val) => val,
8828                                        Err(_) => StrykeValue::UNDEF,
8829                                    }
8830                                });
8831                            pmap_progress.finish();
8832                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8833                            Ok(())
8834                        } else {
8835                            let block = self.blocks[idx].clone();
8836                            let result = list
8837                                .into_par_iter()
8838                                .map(|x| {
8839                                    pmap_progress.tick();
8840                                    x
8841                                })
8842                                .reduce_with(|a, b| {
8843                                    let mut local_interp = VMHelper::new();
8844                                    local_interp.subs = subs.clone();
8845                                    local_interp.scope.restore_capture(&scope_capture);
8846                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8847                                    match local_interp.exec_block(&block) {
8848                                        Ok(val) => val,
8849                                        Err(_) => StrykeValue::UNDEF,
8850                                    }
8851                                });
8852                            pmap_progress.finish();
8853                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8854                            Ok(())
8855                        }
8856                    }
8857                    Op::PReduceInitWithBlock(block_idx) => {
8858                        let init_val = self.pop();
8859                        let list = self.pop().to_list();
8860                        let progress_flag = self.pop().is_true();
8861                        let idx = *block_idx as usize;
8862                        let subs = self.interp.subs.clone();
8863                        let scope_capture = self.interp.scope.capture();
8864                        let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
8865                        let block = self.blocks[idx].clone();
8866                        if list.is_empty() {
8867                            self.push(init_val);
8868                            return Ok(());
8869                        }
8870                        if list.len() == 1 {
8871                            let v = fold_preduce_init_step(
8872                                &subs,
8873                                cap,
8874                                &block,
8875                                preduce_init_fold_identity(&init_val),
8876                                list.into_iter().next().unwrap(),
8877                            );
8878                            self.push(v);
8879                            return Ok(());
8880                        }
8881                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8882                        let result = list
8883                            .into_par_iter()
8884                            .fold(
8885                                || preduce_init_fold_identity(&init_val),
8886                                |acc, item| {
8887                                    pmap_progress.tick();
8888                                    fold_preduce_init_step(&subs, cap, &block, acc, item)
8889                                },
8890                            )
8891                            .reduce(
8892                                || preduce_init_fold_identity(&init_val),
8893                                |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
8894                            );
8895                        pmap_progress.finish();
8896                        self.push(result);
8897                        Ok(())
8898                    }
8899                    Op::PMapReduceWithBlocks(map_idx, reduce_idx) => {
8900                        let list = self.pop().to_list();
8901                        let progress_flag = self.pop().is_true();
8902                        let map_i = *map_idx as usize;
8903                        let reduce_i = *reduce_idx as usize;
8904                        let subs = self.interp.subs.clone();
8905                        let scope_capture = self.interp.scope.capture();
8906                        if list.is_empty() {
8907                            self.push(StrykeValue::UNDEF);
8908                            return Ok(());
8909                        }
8910                        if list.len() == 1 {
8911                            let mut local_interp = VMHelper::new();
8912                            local_interp.subs = subs.clone();
8913                            local_interp.scope.restore_capture(&scope_capture);
8914                            local_interp
8915                                .scope
8916                                .set_topic(list.into_iter().next().unwrap());
8917                            let map_block = self.blocks[map_i].clone();
8918                            let v = match local_interp.exec_block_no_scope(&map_block) {
8919                                Ok(v) => v,
8920                                Err(_) => StrykeValue::UNDEF,
8921                            };
8922                            self.push(v);
8923                            return Ok(());
8924                        }
8925                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8926                        let map_range = self
8927                            .block_bytecode_ranges
8928                            .get(map_i)
8929                            .and_then(|r| r.as_ref())
8930                            .copied();
8931                        let reduce_range = self
8932                            .block_bytecode_ranges
8933                            .get(reduce_i)
8934                            .and_then(|r| r.as_ref())
8935                            .copied();
8936                        if let (Some((map_start, map_end)), Some((reduce_start, reduce_end))) =
8937                            (map_range, reduce_range)
8938                        {
8939                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8940                            let result = list
8941                                .into_par_iter()
8942                                .map(|item| {
8943                                    let mut local_interp = VMHelper::new();
8944                                    local_interp.subs = subs.clone();
8945                                    local_interp.scope.restore_capture(&scope_capture);
8946                                    local_interp.scope.set_topic(item);
8947                                    let mut vm = shared.worker_vm(&mut local_interp);
8948                                    let mut op_count = 0u64;
8949                                    let val = match vm.run_block_region(
8950                                        map_start,
8951                                        map_end,
8952                                        &mut op_count,
8953                                    ) {
8954                                        Ok(val) => val,
8955                                        Err(_) => StrykeValue::UNDEF,
8956                                    };
8957                                    pmap_progress.tick();
8958                                    val
8959                                })
8960                                .reduce_with(|a, b| {
8961                                    let mut local_interp = VMHelper::new();
8962                                    local_interp.subs = subs.clone();
8963                                    local_interp.scope.restore_capture(&scope_capture);
8964                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8965                                    let mut vm = shared.worker_vm(&mut local_interp);
8966                                    let mut op_count = 0u64;
8967                                    match vm.run_block_region(
8968                                        reduce_start,
8969                                        reduce_end,
8970                                        &mut op_count,
8971                                    ) {
8972                                        Ok(val) => val,
8973                                        Err(_) => StrykeValue::UNDEF,
8974                                    }
8975                                });
8976                            pmap_progress.finish();
8977                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8978                            Ok(())
8979                        } else {
8980                            let map_block = self.blocks[map_i].clone();
8981                            let reduce_block = self.blocks[reduce_i].clone();
8982                            let result = list
8983                                .into_par_iter()
8984                                .map(|item| {
8985                                    let mut local_interp = VMHelper::new();
8986                                    local_interp.subs = subs.clone();
8987                                    local_interp.scope.restore_capture(&scope_capture);
8988                                    local_interp.scope.set_topic(item);
8989                                    let val = match local_interp.exec_block_no_scope(&map_block) {
8990                                        Ok(val) => val,
8991                                        Err(_) => StrykeValue::UNDEF,
8992                                    };
8993                                    pmap_progress.tick();
8994                                    val
8995                                })
8996                                .reduce_with(|a, b| {
8997                                    let mut local_interp = VMHelper::new();
8998                                    local_interp.subs = subs.clone();
8999                                    local_interp.scope.restore_capture(&scope_capture);
9000                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
9001                                    match local_interp.exec_block_no_scope(&reduce_block) {
9002                                        Ok(val) => val,
9003                                        Err(_) => StrykeValue::UNDEF,
9004                                    }
9005                                });
9006                            pmap_progress.finish();
9007                            self.push(result.unwrap_or(StrykeValue::UNDEF));
9008                            Ok(())
9009                        }
9010                    }
9011                    Op::PcacheWithBlock(block_idx) => {
9012                        let list = self.pop().to_list();
9013                        let progress_flag = self.pop().is_true();
9014                        let idx = *block_idx as usize;
9015                        let subs = self.interp.subs.clone();
9016                        let scope_capture = self.interp.scope.capture();
9017                        let block = self.blocks[idx].clone();
9018                        let cache = &*crate::pcache::GLOBAL_PCACHE;
9019                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
9020                        if let Some(&(start, end)) =
9021                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9022                        {
9023                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9024                            let results: Vec<StrykeValue> = list
9025                                .into_par_iter()
9026                                .map(|item| {
9027                                    let k = crate::pcache::cache_key(&item);
9028                                    if let Some(v) = cache.get(&k) {
9029                                        pmap_progress.tick();
9030                                        return v.clone();
9031                                    }
9032                                    let mut local_interp = VMHelper::new();
9033                                    local_interp.subs = subs.clone();
9034                                    local_interp.scope.restore_capture(&scope_capture);
9035                                    local_interp.scope.set_topic(item.clone());
9036                                    let mut vm = shared.worker_vm(&mut local_interp);
9037                                    let mut op_count = 0u64;
9038                                    let val = match vm.run_block_region(start, end, &mut op_count) {
9039                                        Ok(v) => v,
9040                                        Err(_) => StrykeValue::UNDEF,
9041                                    };
9042                                    cache.insert(k, val.clone());
9043                                    pmap_progress.tick();
9044                                    val
9045                                })
9046                                .collect();
9047                            pmap_progress.finish();
9048                            self.push(StrykeValue::array(results));
9049                            Ok(())
9050                        } else {
9051                            let results: Vec<StrykeValue> = list
9052                                .into_par_iter()
9053                                .map(|item| {
9054                                    let k = crate::pcache::cache_key(&item);
9055                                    if let Some(v) = cache.get(&k) {
9056                                        pmap_progress.tick();
9057                                        return v.clone();
9058                                    }
9059                                    let mut local_interp = VMHelper::new();
9060                                    local_interp.subs = subs.clone();
9061                                    local_interp.scope.restore_capture(&scope_capture);
9062                                    local_interp.scope.set_topic(item.clone());
9063                                    let val = match local_interp.exec_block_no_scope(&block) {
9064                                        Ok(v) => v,
9065                                        Err(_) => StrykeValue::UNDEF,
9066                                    };
9067                                    cache.insert(k, val.clone());
9068                                    pmap_progress.tick();
9069                                    val
9070                                })
9071                                .collect();
9072                            pmap_progress.finish();
9073                            self.push(StrykeValue::array(results));
9074                            Ok(())
9075                        }
9076                    }
9077                    Op::Pselect { n_rx, has_timeout } => {
9078                        let timeout = if *has_timeout {
9079                            let t = self.pop().to_number();
9080                            Some(std::time::Duration::from_secs_f64(t.max(0.0)))
9081                        } else {
9082                            None
9083                        };
9084                        let mut rx_vals = Vec::with_capacity(*n_rx as usize);
9085                        for _ in 0..*n_rx {
9086                            rx_vals.push(self.pop());
9087                        }
9088                        rx_vals.reverse();
9089                        let line = self.line();
9090                        let v = crate::pchannel::pselect_recv_with_optional_timeout(
9091                            &rx_vals, timeout, line,
9092                        )?;
9093                        self.push(v);
9094                        Ok(())
9095                    }
9096                    Op::PGrepWithBlock(block_idx) => {
9097                        let list = self.pop().to_list();
9098                        let progress_flag = self.pop().is_true();
9099                        let idx = *block_idx as usize;
9100                        let subs = self.interp.subs.clone();
9101                        let (scope_capture, atomic_arrays, atomic_hashes) =
9102                            self.interp.scope.capture_with_atomics();
9103                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
9104                        let n_workers = rayon::current_num_threads();
9105                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
9106                            .map(|_| {
9107                                let mut interp = VMHelper::new();
9108                                interp.subs = subs.clone();
9109                                interp.scope.restore_capture(&scope_capture);
9110                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
9111                                interp.enable_parallel_guard();
9112                                Mutex::new(interp)
9113                            })
9114                            .collect();
9115                        if let Some(&(start, end)) =
9116                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9117                        {
9118                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9119                            let results: Vec<StrykeValue> = list
9120                                .into_par_iter()
9121                                .filter_map(|item| {
9122                                    let tid =
9123                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
9124                                    let mut local_interp = pool[tid].lock();
9125                                    local_interp.scope.set_topic(item.clone());
9126                                    let mut vm = shared.worker_vm(&mut local_interp);
9127                                    let mut op_count = 0u64;
9128                                    let keep = match vm.run_block_region(start, end, &mut op_count)
9129                                    {
9130                                        Ok(val) => val.is_true(),
9131                                        Err(_) => false,
9132                                    };
9133                                    pmap_progress.tick();
9134                                    if keep {
9135                                        Some(item)
9136                                    } else {
9137                                        None
9138                                    }
9139                                })
9140                                .collect();
9141                            pmap_progress.finish();
9142                            self.push(StrykeValue::array(results));
9143                            Ok(())
9144                        } else {
9145                            let block = self.blocks[idx].clone();
9146                            let results: Vec<StrykeValue> = list
9147                                .into_par_iter()
9148                                .filter_map(|item| {
9149                                    let tid =
9150                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
9151                                    let mut local_interp = pool[tid].lock();
9152                                    local_interp.scope.set_topic(item.clone());
9153                                    local_interp.scope_push_hook();
9154                                    let keep = match local_interp.exec_block_no_scope(&block) {
9155                                        Ok(val) => val.is_true(),
9156                                        Err(_) => false,
9157                                    };
9158                                    local_interp.scope_pop_hook();
9159                                    pmap_progress.tick();
9160                                    if keep {
9161                                        Some(item)
9162                                    } else {
9163                                        None
9164                                    }
9165                                })
9166                                .collect();
9167                            pmap_progress.finish();
9168                            self.push(StrykeValue::array(results));
9169                            Ok(())
9170                        }
9171                    }
9172                    Op::PMapsWithBlock(block_idx) => {
9173                        let val = self.pop();
9174                        let block = self.blocks[*block_idx as usize].clone();
9175                        let source = crate::map_stream::into_pull_iter(val);
9176                        let sub = self.interp.anon_coderef_from_block(&block);
9177                        let (capture, atomic_arrays, atomic_hashes) =
9178                            self.interp.scope.capture_with_atomics();
9179                        let out = StrykeValue::iterator(Arc::new(
9180                            crate::map_stream::PMapStreamIterator::new(
9181                                source,
9182                                sub,
9183                                self.interp.subs.clone(),
9184                                capture,
9185                                atomic_arrays,
9186                                atomic_hashes,
9187                                false,
9188                            ),
9189                        ));
9190                        self.push(out);
9191                        Ok(())
9192                    }
9193                    Op::PFlatMapsWithBlock(block_idx) => {
9194                        let val = self.pop();
9195                        let block = self.blocks[*block_idx as usize].clone();
9196                        let source = crate::map_stream::into_pull_iter(val);
9197                        let sub = self.interp.anon_coderef_from_block(&block);
9198                        let (capture, atomic_arrays, atomic_hashes) =
9199                            self.interp.scope.capture_with_atomics();
9200                        let out = StrykeValue::iterator(Arc::new(
9201                            crate::map_stream::PMapStreamIterator::new(
9202                                source,
9203                                sub,
9204                                self.interp.subs.clone(),
9205                                capture,
9206                                atomic_arrays,
9207                                atomic_hashes,
9208                                true,
9209                            ),
9210                        ));
9211                        self.push(out);
9212                        Ok(())
9213                    }
9214                    Op::PGrepsWithBlock(block_idx) => {
9215                        let val = self.pop();
9216                        let block = self.blocks[*block_idx as usize].clone();
9217                        let source = crate::map_stream::into_pull_iter(val);
9218                        let sub = self.interp.anon_coderef_from_block(&block);
9219                        let (capture, atomic_arrays, atomic_hashes) =
9220                            self.interp.scope.capture_with_atomics();
9221                        let out = StrykeValue::iterator(Arc::new(
9222                            crate::map_stream::PGrepStreamIterator::new(
9223                                source,
9224                                sub,
9225                                self.interp.subs.clone(),
9226                                capture,
9227                                atomic_arrays,
9228                                atomic_hashes,
9229                            ),
9230                        ));
9231                        self.push(out);
9232                        Ok(())
9233                    }
9234                    Op::PForWithBlock(block_idx) => {
9235                        let line = self.line();
9236                        let list = self.pop().to_list();
9237                        let progress_flag = self.pop().is_true();
9238                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
9239                        let idx = *block_idx as usize;
9240                        let subs = self.interp.subs.clone();
9241                        let (scope_capture, atomic_arrays, atomic_hashes) =
9242                            self.interp.scope.capture_with_atomics();
9243                        let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
9244                        let n_workers = rayon::current_num_threads();
9245                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
9246                            .map(|_| {
9247                                let mut interp = VMHelper::new();
9248                                interp.subs = subs.clone();
9249                                interp.scope.restore_capture(&scope_capture);
9250                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
9251                                interp.enable_parallel_guard();
9252                                Mutex::new(interp)
9253                            })
9254                            .collect();
9255                        if let Some(&(start, end)) =
9256                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9257                        {
9258                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9259                            list.into_par_iter().for_each(|item| {
9260                                if first_err.lock().is_some() {
9261                                    return;
9262                                }
9263                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
9264                                let mut local_interp = pool[tid].lock();
9265                                local_interp.scope.set_topic(item);
9266                                let mut vm = shared.worker_vm(&mut local_interp);
9267                                let mut op_count = 0u64;
9268                                match vm.run_block_region(start, end, &mut op_count) {
9269                                    Ok(_) => {}
9270                                    Err(e) => {
9271                                        let mut g = first_err.lock();
9272                                        if g.is_none() {
9273                                            *g = Some(e);
9274                                        }
9275                                    }
9276                                }
9277                                pmap_progress.tick();
9278                            });
9279                        } else {
9280                            let block = self.blocks[idx].clone();
9281                            list.into_par_iter().for_each(|item| {
9282                                if first_err.lock().is_some() {
9283                                    return;
9284                                }
9285                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
9286                                let mut local_interp = pool[tid].lock();
9287                                local_interp.scope.set_topic(item);
9288                                local_interp.scope_push_hook();
9289                                match local_interp.exec_block_no_scope(&block) {
9290                                    Ok(_) => {}
9291                                    Err(e) => {
9292                                        let stryke = match e {
9293                                            FlowOrError::Error(stryke) => stryke,
9294                                            FlowOrError::Flow(_) => StrykeError::runtime(
9295                                                "return/last/next/redo not supported inside pfor block",
9296                                                line,
9297                                            ),
9298                                        };
9299                                        let mut g = first_err.lock();
9300                                        if g.is_none() {
9301                                            *g = Some(stryke);
9302                                        }
9303                                    }
9304                                }
9305                                local_interp.scope_pop_hook();
9306                                pmap_progress.tick();
9307                            });
9308                        }
9309                        pmap_progress.finish();
9310                        if let Some(e) = first_err.lock().take() {
9311                            return Err(e);
9312                        }
9313                        self.push(StrykeValue::UNDEF);
9314                        Ok(())
9315                    }
9316                    Op::PSortWithBlock(block_idx) => {
9317                        let mut items = self.pop().to_list();
9318                        let progress_flag = self.pop().is_true();
9319                        let pmap_progress = PmapProgress::new(progress_flag, 2);
9320                        pmap_progress.tick();
9321                        let idx = *block_idx as usize;
9322                        let subs = self.interp.subs.clone();
9323                        let (scope_capture, atomic_arrays, atomic_hashes) =
9324                            self.interp.scope.capture_with_atomics();
9325                        if let Some(&(start, end)) =
9326                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9327                        {
9328                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9329                            items.par_sort_by(|a, b| {
9330                                let mut local_interp = VMHelper::new();
9331                                local_interp.subs = subs.clone();
9332                                local_interp.scope.restore_capture(&scope_capture);
9333                                local_interp
9334                                    .scope
9335                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
9336                                local_interp.enable_parallel_guard();
9337                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
9338                                // Populate slot-based positional args so the
9339                                // bytecode block can read `$_0`/`$_1` (and the
9340                                // bareword `_0`/`_1`) through the slot fast
9341                                // path. `set_sort_pair` only sets the named
9342                                // scalars; without slots, an `$_0` reference
9343                                // resolves to undef in worker bytecode.
9344                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
9345                                let mut vm = shared.worker_vm(&mut local_interp);
9346                                let mut op_count = 0u64;
9347                                match vm.run_block_region(start, end, &mut op_count) {
9348                                    Ok(v) => {
9349                                        let n = v.to_int();
9350                                        if n < 0 {
9351                                            std::cmp::Ordering::Less
9352                                        } else if n > 0 {
9353                                            std::cmp::Ordering::Greater
9354                                        } else {
9355                                            std::cmp::Ordering::Equal
9356                                        }
9357                                    }
9358                                    Err(_) => std::cmp::Ordering::Equal,
9359                                }
9360                            });
9361                        } else {
9362                            let block = self.blocks[idx].clone();
9363                            items.par_sort_by(|a, b| {
9364                                let mut local_interp = VMHelper::new();
9365                                local_interp.subs = subs.clone();
9366                                local_interp.scope.restore_capture(&scope_capture);
9367                                local_interp
9368                                    .scope
9369                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
9370                                local_interp.enable_parallel_guard();
9371                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
9372                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
9373                                local_interp.scope_push_hook();
9374                                let ord = match local_interp.exec_block_no_scope(&block) {
9375                                    Ok(v) => {
9376                                        let n = v.to_int();
9377                                        if n < 0 {
9378                                            std::cmp::Ordering::Less
9379                                        } else if n > 0 {
9380                                            std::cmp::Ordering::Greater
9381                                        } else {
9382                                            std::cmp::Ordering::Equal
9383                                        }
9384                                    }
9385                                    Err(_) => std::cmp::Ordering::Equal,
9386                                };
9387                                local_interp.scope_pop_hook();
9388                                ord
9389                            });
9390                        }
9391                        pmap_progress.tick();
9392                        pmap_progress.finish();
9393                        self.push(StrykeValue::array(items));
9394                        Ok(())
9395                    }
9396                    Op::PSortWithBlockFast(tag) => {
9397                        let mut items = self.pop().to_list();
9398                        let progress_flag = self.pop().is_true();
9399                        let pmap_progress = PmapProgress::new(progress_flag, 2);
9400                        pmap_progress.tick();
9401                        let mode = match *tag {
9402                            0 => SortBlockFast::Numeric,
9403                            1 => SortBlockFast::String,
9404                            2 => SortBlockFast::NumericRev,
9405                            3 => SortBlockFast::StringRev,
9406                            _ => SortBlockFast::Numeric,
9407                        };
9408                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
9409                        pmap_progress.tick();
9410                        pmap_progress.finish();
9411                        self.push(StrykeValue::array(items));
9412                        Ok(())
9413                    }
9414                    Op::PSortNoBlockParallel => {
9415                        let mut items = self.pop().to_list();
9416                        let progress_flag = self.pop().is_true();
9417                        let pmap_progress = PmapProgress::new(progress_flag, 2);
9418                        pmap_progress.tick();
9419                        items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
9420                        pmap_progress.tick();
9421                        pmap_progress.finish();
9422                        self.push(StrykeValue::array(items));
9423                        Ok(())
9424                    }
9425                    Op::FanWithBlock(block_idx) => {
9426                        let line = self.line();
9427                        let n = self.pop().to_int().max(0) as usize;
9428                        let progress_flag = self.pop().is_true();
9429                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
9430                        Ok(())
9431                    }
9432                    Op::FanWithBlockAuto(block_idx) => {
9433                        let line = self.line();
9434                        let n = self.interp.parallel_thread_count();
9435                        let progress_flag = self.pop().is_true();
9436                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
9437                        Ok(())
9438                    }
9439                    Op::FanCapWithBlock(block_idx) => {
9440                        let line = self.line();
9441                        let n = self.pop().to_int().max(0) as usize;
9442                        let progress_flag = self.pop().is_true();
9443                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
9444                        Ok(())
9445                    }
9446                    Op::FanCapWithBlockAuto(block_idx) => {
9447                        let line = self.line();
9448                        let n = self.interp.parallel_thread_count();
9449                        let progress_flag = self.pop().is_true();
9450                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
9451                        Ok(())
9452                    }
9453
9454                    Op::AsyncBlock(block_idx) => {
9455                        let block = self.blocks[*block_idx as usize].clone();
9456                        let subs = self.interp.subs.clone();
9457                        let (scope_capture, atomic_arrays, atomic_hashes) =
9458                            self.interp.scope.capture_with_atomics();
9459                        let result_slot: Arc<Mutex<Option<StrykeResult<StrykeValue>>>> =
9460                            Arc::new(Mutex::new(None));
9461                        let join_slot: Arc<Mutex<Option<std::thread::JoinHandle<()>>>> =
9462                            Arc::new(Mutex::new(None));
9463                        let rs = Arc::clone(&result_slot);
9464                        let h = std::thread::spawn(move || {
9465                            let mut local_interp = VMHelper::new();
9466                            local_interp.subs = subs;
9467                            local_interp.scope.restore_capture(&scope_capture);
9468                            local_interp
9469                                .scope
9470                                .restore_atomics(&atomic_arrays, &atomic_hashes);
9471                            local_interp.enable_parallel_guard();
9472                            local_interp.scope_push_hook();
9473                            let out = match local_interp.exec_block_no_scope(&block) {
9474                                Ok(v) => Ok(v),
9475                                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
9476                                Err(FlowOrError::Error(e)) => Err(e),
9477                                Err(_) => Ok(StrykeValue::UNDEF),
9478                            };
9479                            local_interp.scope_pop_hook();
9480                            *rs.lock() = Some(out);
9481                        });
9482                        *join_slot.lock() = Some(h);
9483                        self.push(StrykeValue::async_task(Arc::new(StrykeAsyncTask {
9484                            result: result_slot,
9485                            join: join_slot,
9486                        })));
9487                        Ok(())
9488                    }
9489                    Op::Await => {
9490                        let v = self.pop();
9491                        if let Some(t) = v.as_async_task() {
9492                            let r = t.await_result();
9493                            self.push(r?);
9494                        } else {
9495                            self.push(v);
9496                        }
9497                        Ok(())
9498                    }
9499
9500                    Op::LoadCurrentSub => {
9501                        if let Some(sub) = self.interp.current_sub_stack.last().cloned() {
9502                            self.push(StrykeValue::code_ref(sub));
9503                        } else {
9504                            self.push(StrykeValue::UNDEF);
9505                        }
9506                        Ok(())
9507                    }
9508
9509                    Op::DeferBlock => {
9510                        let coderef = self.pop();
9511                        self.interp.scope.push_defer(coderef);
9512                        Ok(())
9513                    }
9514
9515                    // ── try / catch / finally ──
9516                    Op::TryPush { .. } => {
9517                        self.try_stack.push(TryFrame {
9518                            try_push_op_idx: self.ip - 1,
9519                            state: TryState::Trying,
9520                            deferred_error: None,
9521                        });
9522                        Ok(())
9523                    }
9524                    Op::TryContinueNormal => {
9525                        let frame = self.try_stack.last().ok_or_else(|| {
9526                            StrykeError::runtime(
9527                                "TryContinueNormal without active try",
9528                                self.line(),
9529                            )
9530                        })?;
9531                        let Op::TryPush {
9532                            finally_ip,
9533                            after_ip,
9534                            ..
9535                        } = &self.ops[frame.try_push_op_idx]
9536                        else {
9537                            return Err(StrykeError::runtime(
9538                                "TryContinueNormal: corrupt try frame",
9539                                self.line(),
9540                            ));
9541                        };
9542                        if let Some(fin_ip) = *finally_ip {
9543                            self.ip = fin_ip;
9544                            Ok(())
9545                        } else {
9546                            self.try_stack.pop();
9547                            self.ip = *after_ip;
9548                            Ok(())
9549                        }
9550                    }
9551                    Op::TryFinallyEnd => {
9552                        let frame = self.try_stack.pop().ok_or_else(|| {
9553                            StrykeError::runtime("TryFinallyEnd without active try", self.line())
9554                        })?;
9555                        // If `catch` threw and we ran `finally` to clean up, re-raise the
9556                        // deferred error now that finally has completed.
9557                        if let Some(deferred) = frame.deferred_error {
9558                            return Err(deferred);
9559                        }
9560                        let Op::TryPush { after_ip, .. } = &self.ops[frame.try_push_op_idx] else {
9561                            return Err(StrykeError::runtime(
9562                                "TryFinallyEnd: corrupt try frame",
9563                                self.line(),
9564                            ));
9565                        };
9566                        self.ip = *after_ip;
9567                        Ok(())
9568                    }
9569                    Op::CatchReceive(idx) => {
9570                        let val = self.pending_catch_error.take().ok_or_else(|| {
9571                            StrykeError::runtime(
9572                                "CatchReceive without pending exception",
9573                                self.line(),
9574                            )
9575                        })?;
9576                        let n = names[*idx as usize].as_str();
9577                        self.interp.scope_pop_hook();
9578                        self.interp.scope_push_hook();
9579                        self.interp.scope.declare_scalar(n, val);
9580                        self.interp.english_note_lexical_scalar(n);
9581                        Ok(())
9582                    }
9583
9584                    Op::DeclareMySyncScalar(name_idx) => {
9585                        let val = self.pop();
9586                        let n = names[*name_idx as usize].as_str();
9587                        let stored = if val.is_mysync_deque_or_heap() {
9588                            val
9589                        } else {
9590                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
9591                        };
9592                        self.interp.scope.declare_scalar(n, stored);
9593                        Ok(())
9594                    }
9595                    Op::DeclareMySyncArray(name_idx) => {
9596                        let val = self.pop();
9597                        let n = names[*name_idx as usize].as_str();
9598                        self.interp.scope.declare_atomic_array(n, val.to_list());
9599                        Ok(())
9600                    }
9601                    Op::DeclareMySyncHash(name_idx) => {
9602                        let val = self.pop();
9603                        let n = names[*name_idx as usize].as_str();
9604                        let items = val.to_list();
9605                        let mut map = IndexMap::new();
9606                        let mut i = 0usize;
9607                        while i + 1 < items.len() {
9608                            map.insert(items[i].to_string(), items[i + 1].clone());
9609                            i += 2;
9610                        }
9611                        self.interp.scope.declare_atomic_hash(n, map);
9612                        Ok(())
9613                    }
9614                    Op::DeclareOurSyncScalar(name_idx) => {
9615                        let val = self.pop();
9616                        let n = names[*name_idx as usize].as_str();
9617                        let stored = if val.is_mysync_deque_or_heap() {
9618                            val
9619                        } else {
9620                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
9621                        };
9622                        self.interp.scope.declare_scalar(n, stored);
9623                        // Register the bare name (everything after `Pkg::`) in the
9624                        // tree-walker tracking sets so worker `$x` reads inside fan/pmap
9625                        // bodies (which run via `exec_block_no_scope`, not bytecode)
9626                        // rewrite to `Pkg::x` and find the shared cell.
9627                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
9628                        self.interp.english_note_lexical_scalar_pub(&bare);
9629                        self.interp.note_our_scalar_pub(&bare);
9630                        Ok(())
9631                    }
9632                    Op::DeclareOurSyncArray(name_idx) => {
9633                        let val = self.pop();
9634                        let n = names[*name_idx as usize].as_str();
9635                        self.interp.scope.declare_atomic_array(n, val.to_list());
9636                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
9637                        self.interp.english_note_lexical_scalar_pub(&bare);
9638                        self.interp.note_our_scalar_pub(&bare);
9639                        Ok(())
9640                    }
9641                    Op::DeclareOurSyncHash(name_idx) => {
9642                        let val = self.pop();
9643                        let n = names[*name_idx as usize].as_str();
9644                        let items = val.to_list();
9645                        let mut map = IndexMap::new();
9646                        let mut i = 0usize;
9647                        while i + 1 < items.len() {
9648                            map.insert(items[i].to_string(), items[i + 1].clone());
9649                            i += 2;
9650                        }
9651                        self.interp.scope.declare_atomic_hash(n, map);
9652                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
9653                        self.interp.english_note_lexical_scalar_pub(&bare);
9654                        self.interp.note_our_scalar_pub(&bare);
9655                        Ok(())
9656                    }
9657                    Op::RuntimeSubDecl(idx) => {
9658                        let rs = &self.runtime_sub_decls[*idx as usize];
9659                        let key = self.interp.qualify_sub_key(&rs.name);
9660                        let captured = self.interp.scope.capture();
9661                        let closure_env = if captured.is_empty() {
9662                            None
9663                        } else {
9664                            Some(captured)
9665                        };
9666                        let mut sub = StrykeSub {
9667                            name: rs.name.clone(),
9668                            params: rs.params.clone(),
9669                            body: rs.body.clone(),
9670                            closure_env,
9671                            prototype: rs.prototype.clone(),
9672                            fib_like: None,
9673                        };
9674                        sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
9675                        self.interp.subs.insert(key, Arc::new(sub));
9676                        Ok(())
9677                    }
9678                    Op::RegisterAdvice(idx) => {
9679                        let rd = &self.runtime_advice_decls[*idx as usize];
9680                        let id = self.interp.next_intercept_id;
9681                        self.interp.next_intercept_id = id.saturating_add(1);
9682                        self.interp.intercepts.push(crate::aop::Intercept {
9683                            id,
9684                            kind: rd.kind,
9685                            pattern: rd.pattern.clone(),
9686                            body: rd.body.clone(),
9687                            body_block_idx: rd.body_block_idx,
9688                        });
9689                        Ok(())
9690                    }
9691                    Op::Tie {
9692                        target_kind,
9693                        name_idx,
9694                        argc,
9695                    } => {
9696                        let argc = *argc as usize;
9697                        let mut stack_vals = Vec::with_capacity(argc);
9698                        for _ in 0..argc {
9699                            stack_vals.push(self.pop());
9700                        }
9701                        stack_vals.reverse();
9702                        let name = names[*name_idx as usize].as_str();
9703                        let line = self.line();
9704                        self.interp
9705                            .tie_execute(*target_kind, name, stack_vals, line)
9706                            .map_err(|e| e.at_line(line))?;
9707                        Ok(())
9708                    }
9709                    Op::FormatDecl(idx) => {
9710                        let (basename, lines) = &self.format_decls[*idx as usize];
9711                        let line = self.line();
9712                        self.interp
9713                            .install_format_decl(basename.as_str(), lines, line)
9714                            .map_err(|e| e.at_line(line))?;
9715                        Ok(())
9716                    }
9717                    Op::UseOverload(idx) => {
9718                        let pairs = &self.use_overload_entries[*idx as usize];
9719                        self.interp.install_use_overload_pairs(pairs);
9720                        Ok(())
9721                    }
9722                    Op::ScalarCompoundAssign { name_idx, op: op_b } => {
9723                        let rhs = self.pop();
9724                        let n = names[*name_idx as usize].as_str();
9725                        let op = scalar_compound_op_from_byte(*op_b).ok_or_else(|| {
9726                            StrykeError::runtime(
9727                                "ScalarCompoundAssign: invalid op byte",
9728                                self.line(),
9729                            )
9730                        })?;
9731                        let en = self.interp.english_scalar_name(n);
9732                        let val = self
9733                            .interp
9734                            .scalar_compound_assign_scalar_target(en, op, rhs)
9735                            .map_err(|e| e.at_line(self.line()))?;
9736                        self.push(val);
9737                        Ok(())
9738                    }
9739
9740                    Op::SetGlobalPhase(phase) => {
9741                        let s = match *phase {
9742                            crate::bytecode::GP_START => "START",
9743                            crate::bytecode::GP_UNITCHECK => "UNITCHECK",
9744                            crate::bytecode::GP_CHECK => "CHECK",
9745                            crate::bytecode::GP_INIT => "INIT",
9746                            crate::bytecode::GP_RUN => "RUN",
9747                            crate::bytecode::GP_END => "END",
9748                            _ => {
9749                                return Err(StrykeError::runtime(
9750                                    format!("SetGlobalPhase: invalid phase byte {}", phase),
9751                                    self.line(),
9752                                ));
9753                            }
9754                        };
9755                        self.interp.global_phase = s.to_string();
9756                        Ok(())
9757                    }
9758
9759                    // ── Halt ──
9760                    Op::Halt => {
9761                        self.halt = true;
9762                        Ok(())
9763                    }
9764                    Op::EvalAstExpr(idx) => {
9765                        let expr = &self.ast_eval_exprs[*idx as usize];
9766                        let val = match self.interp.eval_expr_ctx(expr, self.interp.wantarray_kind)
9767                        {
9768                            Ok(v) => v,
9769                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
9770                            Err(crate::vm_helper::FlowOrError::Flow(f)) => {
9771                                return Err(StrykeError::runtime(
9772                                    format!("unexpected flow control in EvalAstExpr: {:?}", f),
9773                                    self.line(),
9774                                ));
9775                            }
9776                        };
9777                        self.push(val);
9778                        Ok(())
9779                    }
9780                }
9781            })();
9782            if let (Some(prof), Some(t0)) = (&mut self.interp.profiler, op_prof_t0) {
9783                prof.on_line(&self.interp.file, line, t0.elapsed());
9784            }
9785            if let Err(e) = __op_res {
9786                if self.try_recover_from_exception(&e)? {
9787                    continue;
9788                }
9789                return Err(e);
9790            }
9791            // Blessed refcount drops enqueue from `StrykeValue::drop`; drain before the next opcode
9792            // so `$x = undef; f()` runs `DESTROY` before `f` (Perl semantics).
9793            if crate::pending_destroy::pending_destroy_vm_sync_needed() {
9794                self.interp.drain_pending_destroys(line)?;
9795            }
9796            if self.exit_main_dispatch {
9797                if let Some(v) = self.exit_main_dispatch_value.take() {
9798                    last = v;
9799                }
9800                break;
9801            }
9802            if self.halt {
9803                break;
9804            }
9805        }
9806
9807        if !self.stack.is_empty() {
9808            last = self.stack.last().cloned().unwrap_or(StrykeValue::UNDEF);
9809            // Drain iterators left on the stack so side effects fire
9810            // (e.g. `pmaps { system(...) } @list` with no consumer).
9811            if last.is_iterator() {
9812                let iter = last.clone().into_iterator();
9813                while iter.next_item().is_some() {}
9814                last = StrykeValue::UNDEF;
9815            }
9816        }
9817
9818        Ok(last)
9819    }
9820
9821    /// Called from Cranelift (`stryke_jit_call_sub`) to run a compiled sub by bytecode IP with `i64` args.
9822    pub(crate) fn jit_trampoline_run_sub(
9823        &mut self,
9824        entry_ip: usize,
9825        want: WantarrayCtx,
9826        args: &[i64],
9827    ) -> StrykeResult<StrykeValue> {
9828        let saved_wa = self.interp.wantarray_kind;
9829        for a in args {
9830            self.push(StrykeValue::integer(*a));
9831        }
9832        let stack_base = self.stack.len() - args.len();
9833        let mut sub_prof_t0 = None;
9834        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
9835            sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
9836            let nm_owned = self.names[nidx as usize].to_string();
9837            if let Some(p) = &mut self.interp.profiler {
9838                p.enter_sub(nm_owned.as_str());
9839            }
9840            self.interp.debugger_enter_sub(nm_owned.as_str());
9841        }
9842        self.call_stack.push(CallFrame {
9843            return_ip: 0,
9844            stack_base,
9845            scope_depth: self.interp.scope.depth(),
9846            saved_wantarray: saved_wa,
9847            jit_trampoline_return: true,
9848            block_region: false,
9849            sub_profiler_start: sub_prof_t0,
9850        });
9851        self.interp.wantarray_kind = want;
9852        self.interp.scope_push_hook();
9853        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
9854            let nm = self.names[nidx as usize].as_str();
9855            if let Some(sub) = self.interp.subs.get(nm).cloned() {
9856                if let Some(ref env) = sub.closure_env {
9857                    self.interp.scope.restore_capture(env);
9858                }
9859            }
9860        }
9861        self.ip = entry_ip;
9862        self.jit_trampoline_out = None;
9863        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_add(1);
9864        let mut op_count = 0u64;
9865        let last = StrykeValue::UNDEF;
9866        let r = self.run_main_dispatch_loop(last, &mut op_count, true);
9867        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_sub(1);
9868        r?;
9869        self.jit_trampoline_out.take().ok_or_else(|| {
9870            StrykeError::runtime("JIT trampoline: subroutine did not return", self.line())
9871        })
9872    }
9873
9874    #[inline]
9875    fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
9876        self.sub_entry_by_name.get(&name_idx).copied()
9877    }
9878
9879    /// Name pool index for a compiled sub entry IP (for closure env + JIT trampoline).
9880    fn sub_entry_name_idx(&self, entry_ip: usize) -> Option<u16> {
9881        for &(n, ip, _) in &self.sub_entries {
9882            if ip == entry_ip {
9883                return Some(n);
9884            }
9885        }
9886        None
9887    }
9888
9889    fn exec_builtin(&mut self, id: u16, args: Vec<StrykeValue>) -> StrykeResult<StrykeValue> {
9890        let line = self.line();
9891        let bid = BuiltinId::from_u16(id);
9892        match bid {
9893            Some(BuiltinId::Length) => {
9894                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9895                Ok(StrykeValue::integer(val.length_value(self.interp.utf8_pragma)))
9896            }
9897            Some(BuiltinId::Defined) => {
9898                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9899                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
9900            }
9901            Some(BuiltinId::Abs) => {
9902                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9903                Ok(StrykeValue::float(val.to_number().abs()))
9904            }
9905            Some(BuiltinId::Int) => {
9906                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9907                Ok(StrykeValue::integer(val.to_number() as i64))
9908            }
9909            Some(BuiltinId::Sqrt) => {
9910                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9911                Ok(StrykeValue::float(val.to_number().sqrt()))
9912            }
9913            Some(BuiltinId::Sin) => {
9914                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9915                Ok(StrykeValue::float(val.to_number().sin()))
9916            }
9917            Some(BuiltinId::Cos) => {
9918                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9919                Ok(StrykeValue::float(val.to_number().cos()))
9920            }
9921            Some(BuiltinId::Atan2) => {
9922                let mut it = args.into_iter();
9923                let y = it.next().unwrap_or(StrykeValue::UNDEF);
9924                let x = it.next().unwrap_or(StrykeValue::UNDEF);
9925                Ok(StrykeValue::float(y.to_number().atan2(x.to_number())))
9926            }
9927            Some(BuiltinId::Exp) => {
9928                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9929                Ok(StrykeValue::float(val.to_number().exp()))
9930            }
9931            Some(BuiltinId::Log) => {
9932                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9933                Ok(StrykeValue::float(val.to_number().ln()))
9934            }
9935            Some(BuiltinId::Rand) => {
9936                let upper = match args.len() {
9937                    0 => 1.0,
9938                    _ => args[0].to_number(),
9939                };
9940                Ok(StrykeValue::float(self.interp.perl_rand(upper)))
9941            }
9942            Some(BuiltinId::Srand) => {
9943                let seed = match args.len() {
9944                    0 => None,
9945                    _ => Some(args[0].to_number()),
9946                };
9947                Ok(StrykeValue::integer(self.interp.perl_srand(seed)))
9948            }
9949            Some(BuiltinId::Crypt) => {
9950                let mut it = args.into_iter();
9951                let p = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
9952                let salt = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
9953                Ok(StrykeValue::string(crate::crypt_util::perl_crypt(
9954                    &p, &salt,
9955                )))
9956            }
9957            Some(BuiltinId::Fc) => {
9958                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9959                Ok(StrykeValue::string(s.fc_value()))
9960            }
9961            Some(BuiltinId::Quotemeta) => {
9962                let s = args
9963                    .into_iter()
9964                    .next()
9965                    .map(|v| v.to_string())
9966                    .unwrap_or_default();
9967                Ok(StrykeValue::string(crate::perl_regex::perl_quotemeta(&s)))
9968            }
9969            Some(BuiltinId::Tan) => Ok(StrykeValue::float(
9970                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().tan(),
9971            )),
9972            Some(BuiltinId::Asin) => Ok(StrykeValue::float(
9973                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().asin(),
9974            )),
9975            Some(BuiltinId::Acos) => Ok(StrykeValue::float(
9976                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().acos(),
9977            )),
9978            Some(BuiltinId::Atan) => Ok(StrykeValue::float(
9979                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().atan(),
9980            )),
9981            Some(BuiltinId::Sinh) => Ok(StrykeValue::float(
9982                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().sinh(),
9983            )),
9984            Some(BuiltinId::Cosh) => Ok(StrykeValue::float(
9985                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().cosh(),
9986            )),
9987            Some(BuiltinId::Tanh) => Ok(StrykeValue::float(
9988                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().tanh(),
9989            )),
9990            Some(BuiltinId::Log2) => Ok(StrykeValue::float(
9991                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().log2(),
9992            )),
9993            Some(BuiltinId::Log10) => Ok(StrykeValue::float(
9994                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().log10(),
9995            )),
9996            Some(BuiltinId::Ceil) => Ok(StrykeValue::integer(
9997                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().ceil() as i64,
9998            )),
9999            Some(BuiltinId::Floor) => Ok(StrykeValue::integer(
10000                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().floor() as i64,
10001            )),
10002            // Round: 1-arg form returns Int (ties away from zero); 2-arg form
10003            // (round to N places) defers to the named-dispatch path which is
10004            // still in `builtins.rs`. CallBuiltin only emits the 1-arg form.
10005            Some(BuiltinId::Round) => Ok(StrykeValue::integer(
10006                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().round() as i64,
10007            )),
10008            Some(BuiltinId::Pos) => {
10009                let key = if args.is_empty() {
10010                    "_".to_string()
10011                } else {
10012                    args[0].to_string()
10013                };
10014                Ok(self
10015                    .interp
10016                    .regex_pos
10017                    .get(&key)
10018                    .copied()
10019                    .flatten()
10020                    .map(|n| StrykeValue::integer(n as i64))
10021                    .unwrap_or(StrykeValue::UNDEF))
10022            }
10023            Some(BuiltinId::Study) => {
10024                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10025                Ok(VMHelper::study_return_value(&s.to_string()))
10026            }
10027            Some(BuiltinId::Chr) => {
10028                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10029                Ok(StrykeValue::string(val.chr_value()))
10030            }
10031            Some(BuiltinId::Ord) => {
10032                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10033                Ok(StrykeValue::integer(val.ord_value()))
10034            }
10035            Some(BuiltinId::Hex) => {
10036                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10037                Ok(StrykeValue::integer(val.hex_value()))
10038            }
10039            Some(BuiltinId::Oct) => {
10040                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10041                Ok(StrykeValue::integer(val.oct_value()))
10042            }
10043            Some(BuiltinId::Uc) => {
10044                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10045                Ok(StrykeValue::string(s.uc_value()))
10046            }
10047            Some(BuiltinId::Lc) => {
10048                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10049                Ok(StrykeValue::string(s.lc_value()))
10050            }
10051            Some(BuiltinId::Ref) => {
10052                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10053                Ok(val.ref_type())
10054            }
10055            Some(BuiltinId::Scalar) => {
10056                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10057                Ok(val.scalar_context())
10058            }
10059            Some(BuiltinId::Join) => {
10060                let mut iter = args.into_iter();
10061                let sep = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
10062                let list = iter.next().unwrap_or(StrykeValue::UNDEF).to_list();
10063                let mut strs = Vec::with_capacity(list.len());
10064                for v in list {
10065                    let s = match self.interp.stringify_value(v, line) {
10066                        Ok(s) => s,
10067                        Err(FlowOrError::Error(e)) => return Err(e),
10068                        Err(FlowOrError::Flow(_)) => {
10069                            return Err(StrykeError::runtime(
10070                                "join: unexpected control flow",
10071                                line,
10072                            ));
10073                        }
10074                    };
10075                    strs.push(s);
10076                }
10077                Ok(StrykeValue::string(strs.join(&sep)))
10078            }
10079            Some(BuiltinId::Split) => {
10080                let mut iter = args.into_iter();
10081                let pat_val = iter.next().unwrap_or(StrykeValue::string(" ".into()));
10082                // Prefer the regex source over the Display form: `qr//`'s Display is
10083                // `(?:)` (matches everywhere), which is NOT the same as Perl's empty-
10084                // pattern semantics ("split between every character"). Pulling the
10085                // source out via `regex_src_and_flags` lets us treat `//` as truly
10086                // empty so the char-split branch fires.
10087                let pat = pat_val
10088                    .regex_src_and_flags()
10089                    .map(|(s, _)| s)
10090                    .unwrap_or_else(|| pat_val.to_string());
10091                let s = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
10092                // Perl 5: splitting the empty string yields the empty list for any
10093                // pattern / limit (regex `split` on `""` would otherwise leave one field).
10094                if s.is_empty() {
10095                    return Ok(StrykeValue::array(vec![]));
10096                }
10097                // Perl LIMIT semantics:
10098                //   omitted / 0  → no truncation, strip trailing empties.
10099                //   > 0          → at most LIMIT fields, keep empties up to limit.
10100                //   < 0          → no truncation, keep all empties.
10101                let lim_signed: Option<i64> = iter.next().map(|v| v.to_int());
10102
10103                let mut parts: Vec<String> = if pat.is_empty() {
10104                    // Empty pattern → "split between every character" (Perl). The
10105                    // regex engine would also match at the boundaries, producing
10106                    // spurious empties; `s.chars()` is the right primitive.
10107                    let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
10108                    match lim_signed {
10109                        // LIMIT > 0 (Perl):
10110                        //   n < |chars|        → first n-1 chars then the tail in one field
10111                        //                        (`split //, "abcde", 3` → ("a","b","cde")).
10112                        //   n == |chars|       → chars exactly, no trailing empty.
10113                        //   n > |chars|        → chars + "" (Perl emits the end-of-string
10114                        //                        match as a final empty when LIMIT permits).
10115                        Some(l) if l > 0 => {
10116                            let n = l as usize;
10117                            if n < chars.len() {
10118                                let mut head: Vec<String> =
10119                                    chars.iter().take(n.saturating_sub(1)).cloned().collect();
10120                                let tail: String = s.chars().skip(n.saturating_sub(1)).collect();
10121                                head.push(tail);
10122                                head
10123                            } else if n == chars.len() {
10124                                chars
10125                            } else {
10126                                let mut v = chars;
10127                                v.push(String::new());
10128                                v
10129                            }
10130                        }
10131                        // LIMIT < 0 → chars + trailing empty.
10132                        Some(l) if l < 0 => {
10133                            let mut v = chars;
10134                            v.push(String::new());
10135                            v
10136                        }
10137                        // No limit / 0 → just the chars; the trailing-empty strip
10138                        // below is a no-op (`chars()` never emits one).
10139                        _ => chars,
10140                    }
10141                } else {
10142                    let re =
10143                        regex::Regex::new(&pat).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
10144                    match lim_signed {
10145                        Some(l) if l > 0 => {
10146                            re.splitn(&s, l as usize).map(|p| p.to_string()).collect()
10147                        }
10148                        _ => re.split(&s).map(|p| p.to_string()).collect(),
10149                    }
10150                };
10151
10152                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted or
10153                // zero. Positive LIMIT keeps trailing empties (capped at LIMIT).
10154                // Negative LIMIT also keeps them.
10155                let strip_trailing = matches!(lim_signed, None | Some(0));
10156                if strip_trailing {
10157                    while parts.last().is_some_and(|p| p.is_empty()) {
10158                        parts.pop();
10159                    }
10160                }
10161
10162                Ok(StrykeValue::array(
10163                    parts.into_iter().map(StrykeValue::string).collect(),
10164                ))
10165            }
10166            Some(BuiltinId::Sprintf) => {
10167                // sprintf arg list is Perl list context; flatten ranges / arrays / reverse
10168                // output into individual format arguments (same splatting as printf).
10169                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
10170                for a in args.into_iter() {
10171                    if let Some(items) = a.as_array_vec() {
10172                        flat.extend(items);
10173                    } else {
10174                        flat.push(a);
10175                    }
10176                }
10177                let args = flat;
10178                if args.is_empty() {
10179                    return Ok(StrykeValue::string(String::new()));
10180                }
10181                let fmt = args[0].to_string();
10182                let rest = &args[1..];
10183                match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
10184                    Ok(s) => Ok(StrykeValue::string(s)),
10185                    Err(FlowOrError::Error(e)) => Err(e),
10186                    Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
10187                        "sprintf: unexpected control flow",
10188                        line,
10189                    )),
10190                }
10191            }
10192            Some(BuiltinId::Reverse) => {
10193                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10194                Ok(if let Some(mut a) = val.as_array_vec() {
10195                    a.reverse();
10196                    StrykeValue::array(a)
10197                } else if let Some(s) = val.as_str() {
10198                    StrykeValue::string(s.chars().rev().collect())
10199                } else {
10200                    StrykeValue::string(val.to_string().chars().rev().collect())
10201                })
10202            }
10203            Some(BuiltinId::Die) => {
10204                // Single-ref arg: preserve the original value (hash/array/code/blessed ref)
10205                // so `$@` and `try/catch` see the ref, not a stringification.
10206                if args.len() == 1 {
10207                    let v = &args[0];
10208                    if v.as_hash_ref().is_some()
10209                        || v.as_blessed_ref().is_some()
10210                        || v.as_array_ref().is_some()
10211                        || v.as_code_ref().is_some()
10212                    {
10213                        let msg = v.to_string();
10214                        self.interp.fire_pseudosig_die(&msg, line)?;
10215                        return Err(StrykeError::die_with_value(v.clone(), msg, line));
10216                    }
10217                }
10218                let mut msg = String::new();
10219                for a in &args {
10220                    msg.push_str(&a.to_string());
10221                }
10222                if msg.is_empty() {
10223                    msg = "Died".to_string();
10224                }
10225                if !msg.ends_with('\n') {
10226                    msg.push_str(&self.interp.die_warn_at_suffix(line));
10227                    msg.push('\n');
10228                }
10229                self.interp.fire_pseudosig_die(&msg, line)?;
10230                Err(StrykeError::die(msg, line))
10231            }
10232            Some(BuiltinId::Warn) => {
10233                let mut msg = String::new();
10234                for a in &args {
10235                    msg.push_str(&a.to_string());
10236                }
10237                if msg.is_empty() {
10238                    msg = "Warning: something's wrong".to_string();
10239                }
10240                if !msg.ends_with('\n') {
10241                    msg.push_str(&self.interp.die_warn_at_suffix(line));
10242                    msg.push('\n');
10243                }
10244                self.interp.fire_pseudosig_warn(&msg, line)?;
10245                Ok(StrykeValue::integer(1))
10246            }
10247            Some(BuiltinId::Exit) => {
10248                let code = args
10249                    .into_iter()
10250                    .next()
10251                    .map(|v| v.to_int() as i32)
10252                    .unwrap_or(0);
10253                Err(StrykeError::new(
10254                    ErrorKind::Exit(code),
10255                    "",
10256                    line,
10257                    &self.interp.file,
10258                ))
10259            }
10260            Some(BuiltinId::System) => {
10261                // Perl's `system`:
10262                //   - `system "cmd args"` (single string)  → `sh -c "cmd args"`
10263                //   - `system "cmd", "arg1", "arg2", ...`  → exec the program
10264                //     directly with the trailing args as argv (no shell).
10265                // Return value is the encoded `$?` status word (exit_code << 8
10266                // on a clean exit; raw signal number for signals), not the bare
10267                // exit code, so `$rc == 0` <=> clean success and bit-twiddles
10268                // like `($? >> 8)` work on the return value too.
10269                let strs: Vec<String> = args.iter().map(|a| a.to_string()).collect();
10270                if strs.is_empty() {
10271                    self.interp.child_exit_status = -1;
10272                    return Ok(StrykeValue::integer(-1));
10273                }
10274                let status = if strs.len() == 1 {
10275                    std::process::Command::new("sh")
10276                        .arg("-c")
10277                        .arg(&strs[0])
10278                        .status()
10279                } else {
10280                    std::process::Command::new(&strs[0])
10281                        .args(&strs[1..])
10282                        .status()
10283                };
10284                match status {
10285                    Ok(s) => {
10286                        self.interp.record_child_exit_status(s);
10287                        Ok(StrykeValue::integer(self.interp.child_exit_status))
10288                    }
10289                    Err(e) => {
10290                        self.interp.errno = e.to_string();
10291                        self.interp.child_exit_status = -1;
10292                        Ok(StrykeValue::integer(-1))
10293                    }
10294                }
10295            }
10296            Some(BuiltinId::Ssh) => self.interp.ssh_builtin_execute(&args),
10297            Some(BuiltinId::Chomp) => {
10298                // Chomp modifies the variable in-place — but in CallBuiltin we get the value, not a reference.
10299                // Return the number of chars removed (like Perl).
10300                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10301                let s = val.to_string();
10302                Ok(StrykeValue::integer(if s.ends_with('\n') { 1 } else { 0 }))
10303            }
10304            Some(BuiltinId::Chop) => {
10305                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10306                let s = val.to_string();
10307                Ok(s.chars()
10308                    .last()
10309                    .map(|c| StrykeValue::string(c.to_string()))
10310                    .unwrap_or(StrykeValue::UNDEF))
10311            }
10312            Some(BuiltinId::Substr) => {
10313                if args.len() < 3 {
10314                    let s = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10315                    let off = args.get(1).map(|v| v.to_int()).unwrap_or(0);
10316                    return Ok(StrykeValue::string(s.substr2_value(off)));
10317                }
10318                let s = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10319                let off = args.get(1).map(|v| v.to_int()).unwrap_or(0);
10320                let len = args.get(2).map(|v| v.to_int()).unwrap_or(0);
10321                Ok(StrykeValue::string(s.substr3_value(off, len)))
10322            }
10323            Some(BuiltinId::Index) => {
10324                let s = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10325                let sub = args.get(1).cloned().unwrap_or(StrykeValue::UNDEF);
10326                if args.len() < 3 {
10327                    return Ok(StrykeValue::integer(s.index_value(&sub)));
10328                }
10329                let s = s.to_string();
10330                let sub = sub.to_string();
10331                // Perl: negative POS clamps to 0; POS past end returns -1
10332                // (or, for empty needle, returns POS clamped to len).
10333                let pos_raw = args.get(2).map(|v| v.to_int()).unwrap_or(0);
10334                let pos = if pos_raw < 0 {
10335                    0usize
10336                } else {
10337                    (pos_raw as usize).min(s.len())
10338                };
10339                Ok(StrykeValue::integer(
10340                    s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1),
10341                ))
10342            }
10343            Some(BuiltinId::Rindex) => {
10344                let sv = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10345                let subv = args.get(1).cloned().unwrap_or(StrykeValue::UNDEF);
10346                if args.len() < 3 {
10347                    return Ok(StrykeValue::integer(sv.rindex_value(&subv)));
10348                }
10349                let s = sv.to_string();
10350                let sub = subv.to_string();
10351                // Perl: negative POS means "search must end at or before POS";
10352                // any negative value past -1 implies no possible match.
10353                let result = match args.get(2) {
10354                    Some(v) => {
10355                        let p = v.to_int();
10356                        if p < 0 {
10357                            -1
10358                        } else {
10359                            let end = (p as usize).saturating_add(sub.len()).min(s.len());
10360                            s[..end].rfind(&sub).map(|i| i as i64).unwrap_or(-1)
10361                        }
10362                    }
10363                    None => s.rfind(&sub).map(|i| i as i64).unwrap_or(-1),
10364                };
10365                Ok(StrykeValue::integer(result))
10366            }
10367            Some(BuiltinId::Ucfirst) => {
10368                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10369                Ok(StrykeValue::string(s.ucfirst_value()))
10370            }
10371            Some(BuiltinId::Lcfirst) => {
10372                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10373                Ok(StrykeValue::string(s.lcfirst_value()))
10374            }
10375            Some(BuiltinId::Splice) => self.interp.splice_builtin_execute(&args, line),
10376            Some(BuiltinId::Unshift) => self.interp.unshift_builtin_execute(&args, line),
10377            Some(BuiltinId::Printf) => {
10378                // Flatten list-context operands (ranges, arrays, `reverse`, …) so format
10379                // placeholders line up with individual values instead of an array reference.
10380                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
10381                for a in args.into_iter() {
10382                    if let Some(items) = a.as_array_vec() {
10383                        flat.extend(items);
10384                    } else {
10385                        flat.push(a);
10386                    }
10387                }
10388                let args = flat;
10389                let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
10390                    let s = match self
10391                        .interp
10392                        .stringify_value(self.interp.scope.get_scalar("_").clone(), line)
10393                    {
10394                        Ok(s) => s,
10395                        Err(FlowOrError::Error(e)) => return Err(e),
10396                        Err(FlowOrError::Flow(_)) => {
10397                            return Err(StrykeError::runtime(
10398                                "printf: unexpected control flow",
10399                                line,
10400                            ));
10401                        }
10402                    };
10403                    (s, &[])
10404                } else {
10405                    (args[0].to_string(), &args[1..])
10406                };
10407                let out = match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
10408                    Ok(s) => s,
10409                    Err(FlowOrError::Error(e)) => return Err(e),
10410                    Err(FlowOrError::Flow(_)) => {
10411                        return Err(StrykeError::runtime(
10412                            "printf: unexpected control flow",
10413                            line,
10414                        ));
10415                    }
10416                };
10417                print!("{}", out);
10418                if self.interp.output_autoflush {
10419                    let _ = io::stdout().flush();
10420                }
10421                Ok(StrykeValue::integer(1))
10422            }
10423            Some(BuiltinId::Open) => {
10424                if args.len() < 2 {
10425                    return Err(StrykeError::runtime(
10426                        "open requires at least 2 arguments",
10427                        line,
10428                    ));
10429                }
10430                let handle_name = args[0].to_string();
10431                let mode_s = args[1].to_string();
10432                let file_opt = args.get(2).map(|v| v.to_string());
10433                self.interp
10434                    .open_builtin_execute(handle_name, mode_s, file_opt, line)
10435            }
10436            Some(BuiltinId::Close) => {
10437                let name = args
10438                    .into_iter()
10439                    .next()
10440                    .unwrap_or(StrykeValue::UNDEF)
10441                    .to_string();
10442                self.interp.close_builtin_execute(name)
10443            }
10444            Some(BuiltinId::Eof) => self.interp.eof_builtin_execute(&args, line),
10445            Some(BuiltinId::ReadLine) => {
10446                let h = if args.is_empty() {
10447                    None
10448                } else {
10449                    Some(args[0].to_string())
10450                };
10451                self.interp.readline_builtin_execute(h.as_deref())
10452            }
10453            Some(BuiltinId::ReadLineList) => {
10454                let h = if args.is_empty() {
10455                    None
10456                } else {
10457                    Some(args[0].to_string())
10458                };
10459                self.interp.readline_builtin_execute_list(h.as_deref())
10460            }
10461            Some(BuiltinId::Exec) => {
10462                let cmd = args
10463                    .iter()
10464                    .map(|a| a.to_string())
10465                    .collect::<Vec<_>>()
10466                    .join(" ");
10467                let status = std::process::Command::new("sh")
10468                    .arg("-c")
10469                    .arg(&cmd)
10470                    .status();
10471                std::process::exit(status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1));
10472            }
10473            Some(BuiltinId::Chdir) => {
10474                let path = args
10475                    .into_iter()
10476                    .next()
10477                    .unwrap_or(StrykeValue::UNDEF)
10478                    .to_string();
10479                if std::env::set_current_dir(&path).is_ok() {
10480                    if let Ok(c) = std::env::current_dir() {
10481                        self.interp.stryke_pwd = std::fs::canonicalize(&c).unwrap_or(c);
10482                    }
10483                    Ok(StrykeValue::integer(1))
10484                } else {
10485                    Ok(StrykeValue::integer(0))
10486                }
10487            }
10488            Some(BuiltinId::Mkdir) => {
10489                let path = args.first().map(|v| v.to_string()).unwrap_or_default();
10490                let path = self.interp.resolve_stryke_path_string(&path);
10491                Ok(StrykeValue::integer(
10492                    if std::fs::create_dir(&path).is_ok() {
10493                        1
10494                    } else {
10495                        0
10496                    },
10497                ))
10498            }
10499            Some(BuiltinId::Unlink) => {
10500                let mut count = 0i64;
10501                for a in &args {
10502                    let p = self.interp.resolve_stryke_path_string(&a.to_string());
10503                    if std::fs::remove_file(&p).is_ok() {
10504                        count += 1;
10505                    }
10506                }
10507                Ok(StrykeValue::integer(count))
10508            }
10509            Some(BuiltinId::Rmdir) => self.interp.builtin_rmdir_execute(&args, line),
10510            Some(BuiltinId::Utime) => self.interp.builtin_utime_execute(&args, line),
10511            Some(BuiltinId::Umask) => self.interp.builtin_umask_execute(&args, line),
10512            Some(BuiltinId::Getcwd) => self.interp.builtin_getcwd_execute(&args, line),
10513            Some(BuiltinId::Pipe) => self.interp.builtin_pipe_execute(&args, line),
10514            Some(BuiltinId::Rename) => {
10515                let old = self.interp.resolve_stryke_path_string(
10516                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10517                );
10518                let new = self.interp.resolve_stryke_path_string(
10519                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
10520                );
10521                Ok(crate::perl_fs::rename_paths(&old, &new))
10522            }
10523            Some(BuiltinId::Chmod) => {
10524                if args.is_empty() {
10525                    return Ok(StrykeValue::integer(0));
10526                }
10527                let mode = args[0].to_int();
10528                let paths: Vec<String> = args
10529                    .iter()
10530                    .skip(1)
10531                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10532                    .collect();
10533                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
10534                    &paths, mode,
10535                )))
10536            }
10537            Some(BuiltinId::Chown) => {
10538                if args.len() < 3 {
10539                    return Ok(StrykeValue::integer(0));
10540                }
10541                let uid = args[0].to_int();
10542                let gid = args[1].to_int();
10543                let paths: Vec<String> = args
10544                    .iter()
10545                    .skip(2)
10546                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10547                    .collect();
10548                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
10549                    &paths, uid, gid,
10550                )))
10551            }
10552            Some(BuiltinId::Stat) => {
10553                let path = self.interp.resolve_stryke_path_string(
10554                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10555                );
10556                Ok(crate::perl_fs::stat_path(&path, false))
10557            }
10558            Some(BuiltinId::Lstat) => {
10559                let path = self.interp.resolve_stryke_path_string(
10560                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10561                );
10562                Ok(crate::perl_fs::stat_path(&path, true))
10563            }
10564            Some(BuiltinId::Link) => {
10565                let old = self.interp.resolve_stryke_path_string(
10566                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10567                );
10568                let new = self.interp.resolve_stryke_path_string(
10569                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
10570                );
10571                Ok(crate::perl_fs::link_hard(&old, &new))
10572            }
10573            Some(BuiltinId::Symlink) => {
10574                let old = args.first().map(|v| v.to_string()).unwrap_or_default();
10575                let new = self.interp.resolve_stryke_path_string(
10576                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
10577                );
10578                Ok(crate::perl_fs::link_sym(&old, &new))
10579            }
10580            Some(BuiltinId::Readlink) => {
10581                let path = self.interp.resolve_stryke_path_string(
10582                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10583                );
10584                Ok(crate::perl_fs::read_link(&path))
10585            }
10586            Some(BuiltinId::Glob) => {
10587                // Pass user patterns through verbatim: zsh::glob runs from OS cwd,
10588                // which `chdir` keeps in sync with `stryke_pwd`. Absolutising the
10589                // pattern up front would turn relative-pattern results into
10590                // absolute paths (breaking `glob("**(/)")` → "sub" contract,
10591                // pinned in tests/suite/glob_zsh_qualifiers.rs).
10592                let pats: Vec<String> = args.iter().map(|v| v.to_string()).collect();
10593                Ok(crate::perl_fs::glob_patterns(&pats))
10594            }
10595            Some(BuiltinId::Files) => {
10596                let dir = if args.is_empty() {
10597                    self.interp.resolve_stryke_path_string(".")
10598                } else {
10599                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10600                };
10601                Ok(crate::perl_fs::list_files(&dir))
10602            }
10603            Some(BuiltinId::Filesf) => {
10604                let dir = if args.is_empty() {
10605                    self.interp.resolve_stryke_path_string(".")
10606                } else {
10607                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10608                };
10609                Ok(crate::perl_fs::list_filesf(&dir))
10610            }
10611            Some(BuiltinId::FilesfRecursive) => {
10612                let dir = if args.is_empty() {
10613                    self.interp.resolve_stryke_path_string(".")
10614                } else {
10615                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10616                };
10617                Ok(StrykeValue::iterator(std::sync::Arc::new(
10618                    crate::value::FsWalkIterator::new(&dir, true),
10619                )))
10620            }
10621            Some(BuiltinId::Dirs) => {
10622                let dir = if args.is_empty() {
10623                    self.interp.resolve_stryke_path_string(".")
10624                } else {
10625                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10626                };
10627                Ok(crate::perl_fs::list_dirs(&dir))
10628            }
10629            Some(BuiltinId::DirsRecursive) => {
10630                let dir = if args.is_empty() {
10631                    self.interp.resolve_stryke_path_string(".")
10632                } else {
10633                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10634                };
10635                Ok(StrykeValue::iterator(std::sync::Arc::new(
10636                    crate::value::FsWalkIterator::new(&dir, false),
10637                )))
10638            }
10639            Some(BuiltinId::SymLinks) => {
10640                let dir = if args.is_empty() {
10641                    self.interp.resolve_stryke_path_string(".")
10642                } else {
10643                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10644                };
10645                Ok(crate::perl_fs::list_sym_links(&dir))
10646            }
10647            Some(BuiltinId::Sockets) => {
10648                let dir = if args.is_empty() {
10649                    self.interp.resolve_stryke_path_string(".")
10650                } else {
10651                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10652                };
10653                Ok(crate::perl_fs::list_sockets(&dir))
10654            }
10655            Some(BuiltinId::Pipes) => {
10656                let dir = if args.is_empty() {
10657                    self.interp.resolve_stryke_path_string(".")
10658                } else {
10659                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10660                };
10661                Ok(crate::perl_fs::list_pipes(&dir))
10662            }
10663            Some(BuiltinId::BlockDevices) => {
10664                let dir = if args.is_empty() {
10665                    self.interp.resolve_stryke_path_string(".")
10666                } else {
10667                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10668                };
10669                Ok(crate::perl_fs::list_block_devices(&dir))
10670            }
10671            Some(BuiltinId::CharDevices) => {
10672                let dir = if args.is_empty() {
10673                    self.interp.resolve_stryke_path_string(".")
10674                } else {
10675                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10676                };
10677                Ok(crate::perl_fs::list_char_devices(&dir))
10678            }
10679            Some(BuiltinId::Executables) => {
10680                let dir = if args.is_empty() {
10681                    self.interp.resolve_stryke_path_string(".")
10682                } else {
10683                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10684                };
10685                Ok(crate::perl_fs::list_executables(&dir))
10686            }
10687            Some(BuiltinId::GlobPar) => {
10688                let pats: Vec<String> = args
10689                    .iter()
10690                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10691                    .collect();
10692                Ok(crate::perl_fs::glob_par_patterns(&pats))
10693            }
10694            Some(BuiltinId::GlobParProgress) => {
10695                let progress = args.last().map(|v| v.is_true()).unwrap_or(false);
10696                let pats: Vec<String> = args[..args.len().saturating_sub(1)]
10697                    .iter()
10698                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10699                    .collect();
10700                Ok(crate::perl_fs::glob_par_patterns_with_progress(
10701                    &pats, progress,
10702                ))
10703            }
10704            Some(BuiltinId::ParSed) => self.interp.builtin_par_sed(&args, line, false),
10705            Some(BuiltinId::ParSedProgress) => self.interp.builtin_par_sed(&args, line, true),
10706            Some(BuiltinId::Opendir) => {
10707                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10708                let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
10709                Ok(self.interp.opendir_handle(&handle, &path))
10710            }
10711            Some(BuiltinId::Readdir) => {
10712                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10713                Ok(self.interp.readdir_handle(&handle))
10714            }
10715            Some(BuiltinId::ReaddirList) => {
10716                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10717                Ok(self.interp.readdir_handle_list(&handle))
10718            }
10719            Some(BuiltinId::Closedir) => {
10720                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10721                Ok(self.interp.closedir_handle(&handle))
10722            }
10723            Some(BuiltinId::Rewinddir) => {
10724                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10725                Ok(self.interp.rewinddir_handle(&handle))
10726            }
10727            Some(BuiltinId::Telldir) => {
10728                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10729                Ok(self.interp.telldir_handle(&handle))
10730            }
10731            Some(BuiltinId::Seekdir) => {
10732                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10733                let pos = args.get(1).map(|v| v.to_int().max(0) as usize).unwrap_or(0);
10734                Ok(self.interp.seekdir_handle(&handle, pos))
10735            }
10736            Some(BuiltinId::Slurp) => {
10737                let path = args
10738                    .into_iter()
10739                    .next()
10740                    .unwrap_or(StrykeValue::UNDEF)
10741                    .to_string();
10742                let path = self.interp.resolve_stryke_path_string(&path);
10743                crate::perl_fs::read_bytes_or_glob(&path)
10744                    .map(StrykeValue::bytes)
10745                    .map_err(|e| StrykeError::runtime(format!("slurp: {}", e), line))
10746            }
10747            Some(BuiltinId::Swallow) => {
10748                let path = args
10749                    .into_iter()
10750                    .next()
10751                    .unwrap_or(StrykeValue::UNDEF)
10752                    .to_string();
10753                let path = self.interp.resolve_stryke_path_string(&path);
10754                crate::perl_fs::swallow_to_hash(&path)
10755                    .map_err(|e| StrykeError::runtime(format!("swallow: {}", e), line))
10756            }
10757            Some(BuiltinId::Ingest) => {
10758                let path = args
10759                    .into_iter()
10760                    .next()
10761                    .unwrap_or(StrykeValue::UNDEF)
10762                    .to_string();
10763                let path = self.interp.resolve_stryke_path_string(&path);
10764                crate::perl_fs::ingest_iterator(&path)
10765                    .map_err(|e| StrykeError::runtime(format!("ingest: {}", e), line))
10766            }
10767            Some(BuiltinId::Burp) => {
10768                let v = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10769                crate::perl_fs::burp_hash_to_disk(&v)
10770                    .map(StrykeValue::integer)
10771                    .map_err(|e| StrykeError::runtime(format!("burp: {}", e), line))
10772            }
10773            Some(BuiltinId::God) => {
10774                let v = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10775                Ok(StrykeValue::string(crate::god::god_dump(&v)))
10776            }
10777            Some(BuiltinId::Capture) => {
10778                let cmd = args
10779                    .into_iter()
10780                    .next()
10781                    .unwrap_or(StrykeValue::UNDEF)
10782                    .to_string();
10783                crate::capture::run_capture(self.interp, &cmd, line)
10784            }
10785            Some(BuiltinId::Ppool) => {
10786                let n = args
10787                    .first()
10788                    .map(|v| v.to_int().max(0) as usize)
10789                    .unwrap_or(1);
10790                crate::ppool::create_pool(n)
10791            }
10792            Some(BuiltinId::Wantarray) => Ok(match self.interp.wantarray_kind {
10793                crate::vm_helper::WantarrayCtx::Void => StrykeValue::UNDEF,
10794                crate::vm_helper::WantarrayCtx::Scalar => StrykeValue::integer(0),
10795                crate::vm_helper::WantarrayCtx::List => StrykeValue::integer(1),
10796            }),
10797            Some(BuiltinId::FetchUrl) => {
10798                let url = args
10799                    .into_iter()
10800                    .next()
10801                    .unwrap_or(StrykeValue::UNDEF)
10802                    .to_string();
10803                ureq::get(&url)
10804                    .call()
10805                    .map_err(|e| StrykeError::runtime(format!("fetch_url: {}", e), line))
10806                    .and_then(|r| {
10807                        r.into_string()
10808                            .map(StrykeValue::string)
10809                            .map_err(|e| StrykeError::runtime(format!("fetch_url: {}", e), line))
10810                    })
10811            }
10812            Some(BuiltinId::Pchannel) => {
10813                if args.is_empty() {
10814                    Ok(crate::pchannel::create_pair())
10815                } else if args.len() == 1 {
10816                    let n = args[0].to_int().max(1) as usize;
10817                    Ok(crate::pchannel::create_bounded_pair(n))
10818                } else {
10819                    Err(StrykeError::runtime(
10820                        "pchannel() takes 0 or 1 arguments (capacity)",
10821                        line,
10822                    ))
10823                }
10824            }
10825            Some(BuiltinId::Pselect) => crate::pchannel::pselect_recv(&args, line),
10826            Some(BuiltinId::DequeNew) => {
10827                if !args.is_empty() {
10828                    return Err(StrykeError::runtime("deque() takes no arguments", line));
10829                }
10830                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
10831            }
10832            Some(BuiltinId::HeapNew) => {
10833                if args.len() != 1 {
10834                    return Err(StrykeError::runtime(
10835                        "heap() expects one comparator sub",
10836                        line,
10837                    ));
10838                }
10839                let a0 = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10840                if let Some(sub) = a0.as_code_ref() {
10841                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
10842                        items: Vec::new(),
10843                        cmp: Arc::clone(&sub),
10844                    }))))
10845                } else {
10846                    Err(StrykeError::runtime(
10847                        "heap() requires a code reference",
10848                        line,
10849                    ))
10850                }
10851            }
10852            Some(BuiltinId::BarrierNew) => {
10853                let n = args
10854                    .first()
10855                    .map(|v| v.to_int().max(1) as usize)
10856                    .unwrap_or(1);
10857                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
10858            }
10859            Some(BuiltinId::ClusterNew) => {
10860                // `cluster(HOST...)` — accepts one operand (flattened) or
10861                // multiple (each is a slot spec). Same surface as the
10862                // tree-walker arm in `vm_helper.rs` `call_named_sub`'s
10863                // "cluster" case so `pmap_on` / `~d>` see identical
10864                // `RemoteCluster` values from either dispatch path.
10865                let items = if args.len() == 1 {
10866                    args[0].to_list()
10867                } else {
10868                    args.clone()
10869                };
10870                let c = crate::value::RemoteCluster::from_list_args(&items)
10871                    .map_err(|msg| StrykeError::runtime(msg, line))?;
10872                Ok(StrykeValue::remote_cluster(std::sync::Arc::new(c)))
10873            }
10874            Some(BuiltinId::Pipeline) => {
10875                let mut items = Vec::new();
10876                for v in args {
10877                    if let Some(a) = v.as_array_vec() {
10878                        items.extend(a);
10879                    } else {
10880                        items.push(v);
10881                    }
10882                }
10883                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
10884                    source: items,
10885                    ops: Vec::new(),
10886                    has_scalar_terminal: false,
10887                    par_stream: false,
10888                    streaming: false,
10889                    streaming_workers: 0,
10890                    streaming_buffer: 256,
10891                }))))
10892            }
10893            Some(BuiltinId::ParPipeline) => {
10894                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
10895                    return crate::par_pipeline::run_par_pipeline(self.interp, &args, line);
10896                }
10897                let mut items = Vec::new();
10898                for v in args {
10899                    if let Some(a) = v.as_array_vec() {
10900                        items.extend(a);
10901                    } else {
10902                        items.push(v);
10903                    }
10904                }
10905                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
10906                    source: items,
10907                    ops: Vec::new(),
10908                    has_scalar_terminal: false,
10909                    par_stream: true,
10910                    streaming: false,
10911                    streaming_workers: 0,
10912                    streaming_buffer: 256,
10913                }))))
10914            }
10915            Some(BuiltinId::ParPipelineStream) => {
10916                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
10917                    return crate::par_pipeline::run_par_pipeline_streaming(
10918                        self.interp,
10919                        &args,
10920                        line,
10921                    );
10922                }
10923                self.interp.builtin_par_pipeline_stream_new(&args, line)
10924            }
10925            Some(BuiltinId::Each) => {
10926                let _arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10927                Ok(StrykeValue::array(vec![]))
10928            }
10929            Some(BuiltinId::Readpipe) => {
10930                let cmd = args
10931                    .into_iter()
10932                    .next()
10933                    .unwrap_or(StrykeValue::UNDEF)
10934                    .to_string();
10935                crate::capture::run_readpipe(self.interp, &cmd, line)
10936            }
10937            Some(BuiltinId::ReadpipeList) => {
10938                let cmd = args
10939                    .into_iter()
10940                    .next()
10941                    .unwrap_or(StrykeValue::UNDEF)
10942                    .to_string();
10943                let v = crate::capture::run_readpipe(self.interp, &cmd, line)?;
10944                let s = v.to_string();
10945                if s.is_empty() {
10946                    return Ok(StrykeValue::array(Vec::new()));
10947                }
10948                let mut lines = Vec::new();
10949                let mut buf = String::new();
10950                for c in s.chars() {
10951                    buf.push(c);
10952                    if c == '\n' {
10953                        lines.push(StrykeValue::string(std::mem::take(&mut buf)));
10954                    }
10955                }
10956                if !buf.is_empty() {
10957                    lines.push(StrykeValue::string(buf));
10958                }
10959                Ok(StrykeValue::array(lines))
10960            }
10961            Some(BuiltinId::Eval) => {
10962                let arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10963                self.interp.eval_nesting += 1;
10964                let out = if let Some(sub) = arg.as_code_ref() {
10965                    match self.interp.exec_block(&sub.body) {
10966                        Ok(v) => {
10967                            self.interp.clear_eval_error();
10968                            Ok(v)
10969                        }
10970                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
10971                            self.interp.set_eval_error_from_perl_error(&e);
10972                            Ok(StrykeValue::UNDEF)
10973                        }
10974                        Err(crate::vm_helper::FlowOrError::Flow(_)) => {
10975                            self.interp.clear_eval_error();
10976                            Ok(StrykeValue::UNDEF)
10977                        }
10978                    }
10979                } else {
10980                    let code = arg.to_string();
10981                    match crate::parse_and_run_string(&code, self.interp) {
10982                        Ok(v) => {
10983                            self.interp.clear_eval_error();
10984                            Ok(v)
10985                        }
10986                        Err(e) => {
10987                            self.interp.set_eval_error_from_perl_error(&e);
10988                            Ok(StrykeValue::UNDEF)
10989                        }
10990                    }
10991                };
10992                self.interp.eval_nesting -= 1;
10993                out
10994            }
10995            Some(BuiltinId::Do) => {
10996                let filename = args
10997                    .into_iter()
10998                    .next()
10999                    .unwrap_or(StrykeValue::UNDEF)
11000                    .to_string();
11001                match read_file_text_perl_compat(&filename) {
11002                    Ok(code) => {
11003                        let code = crate::data_section::strip_perl_end_marker(&code);
11004                        crate::parse_and_run_string_in_file(code, self.interp, &filename)
11005                            .or(Ok(StrykeValue::UNDEF))
11006                    }
11007                    Err(_) => Ok(StrykeValue::UNDEF),
11008                }
11009            }
11010            Some(BuiltinId::Require) => {
11011                let name = args
11012                    .into_iter()
11013                    .next()
11014                    .unwrap_or(StrykeValue::UNDEF)
11015                    .to_string();
11016                self.interp.require_execute(&name, line)
11017            }
11018            Some(BuiltinId::Bless) => {
11019                let ref_val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
11020                let class = args
11021                    .get(1)
11022                    .map(|v| v.to_string())
11023                    .unwrap_or_else(|| self.interp.scope.get_scalar("__PACKAGE__").to_string());
11024                Ok(StrykeValue::blessed(Arc::new(
11025                    crate::value::BlessedRef::new_blessed(class, ref_val),
11026                )))
11027            }
11028            Some(BuiltinId::Caller) => {
11029                // Simplified caller frame: (package, file, line, subname).
11030                // The sub name is the fully-qualified name of the currently
11031                // executing sub so logger / decorator patterns work.
11032                let sub_name = self
11033                    .interp
11034                    .current_sub_stack
11035                    .last()
11036                    .map(|s| StrykeValue::string(s.name.clone()))
11037                    .unwrap_or(StrykeValue::UNDEF);
11038                let pkg = self.interp.current_package();
11039                Ok(StrykeValue::array(vec![
11040                    StrykeValue::string(pkg),
11041                    StrykeValue::string(self.interp.file.clone()),
11042                    StrykeValue::integer(line as i64),
11043                    sub_name,
11044                ]))
11045            }
11046            // Parallel ops (shouldn't reach here — handled by block ops)
11047            Some(BuiltinId::PMap)
11048            | Some(BuiltinId::PGrep)
11049            | Some(BuiltinId::PFor)
11050            | Some(BuiltinId::PSort)
11051            | Some(BuiltinId::Fan)
11052            | Some(BuiltinId::MapBlock)
11053            | Some(BuiltinId::GrepBlock)
11054            | Some(BuiltinId::SortBlock)
11055            | Some(BuiltinId::Sort) => Ok(StrykeValue::UNDEF),
11056            _ => Err(StrykeError::runtime(
11057                format!("Unimplemented builtin {:?}", bid),
11058                line,
11059            )),
11060        }
11061    }
11062}
11063
11064/// Integer fast-path comparison helper.
11065#[inline]
11066/// True when both values are non-numeric strings — used by `==` / `!=` in
11067/// stryke non-compat mode to decide whether to fall back to string compare.
11068/// "Numeric string" matches `looks_like_number` semantics (digits, optional
11069/// sign, optional decimal/exponent). Non-string values (refs, undef) are
11070/// excluded so `==` on objects keeps its overload-driven behavior.
11071fn both_non_numeric_strings(a: &StrykeValue, b: &StrykeValue) -> bool {
11072    if !a.is_string_like() || !b.is_string_like() {
11073        return false;
11074    }
11075    let sa = a.to_string();
11076    let sb = b.to_string();
11077    !looks_numeric(&sa) && !looks_numeric(&sb)
11078}
11079
11080#[inline]
11081fn looks_numeric(s: &str) -> bool {
11082    let t = s.trim();
11083    if t.is_empty() {
11084        return false;
11085    }
11086    t.parse::<f64>().is_ok()
11087}
11088
11089fn int_cmp(
11090    a: &StrykeValue,
11091    b: &StrykeValue,
11092    int_op: fn(&i64, &i64) -> bool,
11093    float_op: fn(f64, f64) -> bool,
11094) -> StrykeValue {
11095    if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
11096        StrykeValue::integer(if int_op(&x, &y) { 1 } else { 0 })
11097    } else {
11098        StrykeValue::integer(if float_op(a.to_number(), b.to_number()) {
11099            1
11100        } else {
11101            0
11102        })
11103    }
11104}
11105
11106/// Block JIT hook: string concat with `use overload` / `""` stringify (matches [`Op::Concat`]).
11107///
11108/// # Safety
11109///
11110/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call.
11111#[no_mangle]
11112pub unsafe extern "C" fn stryke_jit_concat_vm(vm: *mut std::ffi::c_void, a: i64, b: i64) -> i64 {
11113    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
11114    let pa = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(a));
11115    let pb = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(b));
11116    match vm.concat_stack_values(pa, pb) {
11117        Ok(pv) => pv.raw_bits() as i64,
11118        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
11119    }
11120}
11121
11122/// Cranelift host hook: re-enter the VM for [`Op::Call`] to a compiled sub (stack-args, scalar `i64` args).
11123/// `sub_ip`, `argc`, `wa` are passed as `i64` for a uniform Cranelift signature.
11124///
11125/// # Safety
11126///
11127/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call (JIT only
11128/// invokes this while the VM is executing).
11129#[no_mangle]
11130pub unsafe extern "C" fn stryke_jit_call_sub(
11131    vm: *mut std::ffi::c_void,
11132    sub_ip: i64,
11133    argc: i64,
11134    wa: i64,
11135    a0: i64,
11136    a1: i64,
11137    a2: i64,
11138    a3: i64,
11139    a4: i64,
11140    a5: i64,
11141    a6: i64,
11142    a7: i64,
11143) -> i64 {
11144    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
11145    let want = WantarrayCtx::from_byte(wa as u8);
11146    if want != WantarrayCtx::Scalar {
11147        return StrykeValue::UNDEF.raw_bits() as i64;
11148    }
11149    let argc = argc.clamp(0, 8) as usize;
11150    let args = [a0, a1, a2, a3, a4, a5, a6, a7];
11151    let args = &args[..argc];
11152    match vm.jit_trampoline_run_sub(sub_ip as usize, want, args) {
11153        Ok(pv) => {
11154            if let Some(n) = pv.as_integer() {
11155                n
11156            } else {
11157                pv.raw_bits() as i64
11158            }
11159        }
11160        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
11161    }
11162}
11163
11164#[cfg(test)]
11165mod tests {
11166    use super::*;
11167    use crate::bytecode::{Chunk, Op};
11168    use crate::value::StrykeValue;
11169
11170    fn run_chunk(chunk: &Chunk) -> StrykeResult<StrykeValue> {
11171        let mut interp = VMHelper::new();
11172        let mut vm = VM::new(chunk, &mut interp);
11173        vm.execute()
11174    }
11175
11176    /// Block-JIT-eligible loop: `for ($i=0; $i<limit; $i++) { $sum += $i }` — sum 0..limit-1.
11177    fn block_jit_sum_chunk(limit: i64) -> Chunk {
11178        let mut c = Chunk::new();
11179        let ni = c.intern_name("i");
11180        let ns = c.intern_name("sum");
11181        c.emit(Op::LoadInt(0), 1);
11182        c.emit(Op::DeclareScalarSlot(0, ni), 1);
11183        c.emit(Op::LoadInt(0), 1);
11184        c.emit(Op::DeclareScalarSlot(1, ns), 1);
11185        c.emit(Op::GetScalarSlot(0), 1);
11186        c.emit(Op::LoadInt(limit), 1);
11187        c.emit(Op::NumLt, 1);
11188        c.emit(Op::JumpIfFalse(15), 1);
11189        c.emit(Op::GetScalarSlot(1), 1);
11190        c.emit(Op::GetScalarSlot(0), 1);
11191        c.emit(Op::Add, 1);
11192        c.emit(Op::SetScalarSlot(1), 1);
11193        c.emit(Op::PostIncSlot(0), 1);
11194        c.emit(Op::Pop, 1);
11195        c.emit(Op::Jump(4), 1);
11196        c.emit(Op::GetScalarSlot(1), 1);
11197        c.emit(Op::Halt, 1);
11198        c
11199    }
11200
11201    #[test]
11202    fn jit_disabled_same_result_as_jit_block_loop() {
11203        let limit = 500i64;
11204        let chunk = block_jit_sum_chunk(limit);
11205        let expect = limit * (limit - 1) / 2;
11206
11207        let mut interp_on = VMHelper::new();
11208        let mut vm_on = VM::new(&chunk, &mut interp_on);
11209        assert_eq!(vm_on.execute().expect("vm").to_int(), expect);
11210
11211        let mut interp_off = VMHelper::new();
11212        let mut vm_off = VM::new(&chunk, &mut interp_off);
11213        vm_off.set_jit_enabled(false);
11214        assert_eq!(vm_off.execute().expect("vm").to_int(), expect);
11215    }
11216
11217    #[test]
11218    fn vm_add_two_integers() {
11219        let mut c = Chunk::new();
11220        c.emit(Op::LoadInt(2), 1);
11221        c.emit(Op::LoadInt(3), 1);
11222        c.emit(Op::Add, 1);
11223        c.emit(Op::Halt, 1);
11224        let v = run_chunk(&c).expect("vm");
11225        assert_eq!(v.to_int(), 5);
11226    }
11227
11228    #[test]
11229    fn vm_sub_mul_div() {
11230        let mut c = Chunk::new();
11231        c.emit(Op::LoadInt(10), 1);
11232        c.emit(Op::LoadInt(3), 1);
11233        c.emit(Op::Sub, 1);
11234        c.emit(Op::Halt, 1);
11235        assert_eq!(run_chunk(&c).expect("vm").to_int(), 7);
11236
11237        let mut c = Chunk::new();
11238        c.emit(Op::LoadInt(6), 1);
11239        c.emit(Op::LoadInt(7), 1);
11240        c.emit(Op::Mul, 1);
11241        c.emit(Op::Halt, 1);
11242        assert_eq!(run_chunk(&c).expect("vm").to_int(), 42);
11243
11244        let mut c = Chunk::new();
11245        c.emit(Op::LoadInt(20), 1);
11246        c.emit(Op::LoadInt(4), 1);
11247        c.emit(Op::Div, 1);
11248        c.emit(Op::Halt, 1);
11249        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
11250    }
11251
11252    #[test]
11253    fn vm_mod_and_pow() {
11254        let mut c = Chunk::new();
11255        c.emit(Op::LoadInt(17), 1);
11256        c.emit(Op::LoadInt(5), 1);
11257        c.emit(Op::Mod, 1);
11258        c.emit(Op::Halt, 1);
11259        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11260
11261        let mut c = Chunk::new();
11262        c.emit(Op::LoadInt(2), 1);
11263        c.emit(Op::LoadInt(3), 1);
11264        c.emit(Op::Pow, 1);
11265        c.emit(Op::Halt, 1);
11266        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
11267    }
11268
11269    #[test]
11270    fn vm_negate() {
11271        let mut c = Chunk::new();
11272        c.emit(Op::LoadInt(7), 1);
11273        c.emit(Op::Negate, 1);
11274        c.emit(Op::Halt, 1);
11275        assert_eq!(run_chunk(&c).expect("vm").to_int(), -7);
11276    }
11277
11278    #[test]
11279    fn vm_dup_and_pop() {
11280        let mut c = Chunk::new();
11281        c.emit(Op::LoadInt(1), 1);
11282        c.emit(Op::Dup, 1);
11283        c.emit(Op::Add, 1);
11284        c.emit(Op::Halt, 1);
11285        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11286
11287        let mut c = Chunk::new();
11288        c.emit(Op::LoadInt(1), 1);
11289        c.emit(Op::LoadInt(2), 1);
11290        c.emit(Op::Pop, 1);
11291        c.emit(Op::Halt, 1);
11292        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11293    }
11294
11295    #[test]
11296    fn vm_set_get_scalar() {
11297        let mut c = Chunk::new();
11298        let i = c.intern_name("v");
11299        c.emit(Op::LoadInt(99), 1);
11300        c.emit(Op::SetScalar(i), 1);
11301        c.emit(Op::GetScalar(i), 1);
11302        c.emit(Op::Halt, 1);
11303        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
11304    }
11305
11306    #[test]
11307    fn vm_scalar_plain_roundtrip_and_keep() {
11308        let mut c = Chunk::new();
11309        let i = c.intern_name("plainvar");
11310        c.emit(Op::LoadInt(99), 1);
11311        c.emit(Op::SetScalarPlain(i), 1);
11312        c.emit(Op::GetScalarPlain(i), 1);
11313        c.emit(Op::Halt, 1);
11314        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
11315
11316        let mut c = Chunk::new();
11317        let k = c.intern_name("keepme");
11318        c.emit(Op::LoadInt(5), 1);
11319        c.emit(Op::SetScalarKeepPlain(k), 1);
11320        c.emit(Op::Halt, 1);
11321        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
11322    }
11323
11324    #[test]
11325    fn vm_get_scalar_plain_skips_special_global_zero() {
11326        let mut c = Chunk::new();
11327        let idx = c.intern_name("0");
11328        c.emit(Op::GetScalar(idx), 1);
11329        c.emit(Op::Halt, 1);
11330        assert_eq!(run_chunk(&c).expect("vm").to_string(), "stryke");
11331
11332        let mut c = Chunk::new();
11333        let idx = c.intern_name("0");
11334        c.emit(Op::GetScalarPlain(idx), 1);
11335        c.emit(Op::Halt, 1);
11336        assert!(run_chunk(&c).expect("vm").is_undef());
11337    }
11338
11339    #[test]
11340    fn vm_slot_pre_post_inc_dec() {
11341        let mut c = Chunk::new();
11342        c.emit(Op::LoadInt(10), 1);
11343        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11344        c.emit(Op::PostIncSlot(0), 1);
11345        c.emit(Op::Pop, 1);
11346        c.emit(Op::GetScalarSlot(0), 1);
11347        c.emit(Op::Halt, 1);
11348        assert_eq!(run_chunk(&c).expect("vm").to_int(), 11);
11349
11350        let mut c = Chunk::new();
11351        c.emit(Op::LoadInt(0), 1);
11352        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11353        c.emit(Op::PreIncSlot(0), 1);
11354        c.emit(Op::Halt, 1);
11355        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11356
11357        let mut c = Chunk::new();
11358        c.emit(Op::LoadInt(5), 1);
11359        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11360        c.emit(Op::PreDecSlot(0), 1);
11361        c.emit(Op::Halt, 1);
11362        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
11363
11364        let mut c = Chunk::new();
11365        c.emit(Op::LoadInt(3), 1);
11366        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11367        c.emit(Op::PostDecSlot(0), 1);
11368        c.emit(Op::Pop, 1);
11369        c.emit(Op::GetScalarSlot(0), 1);
11370        c.emit(Op::Halt, 1);
11371        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11372    }
11373
11374    #[test]
11375    fn vm_str_eq_ne_heap_strings() {
11376        let mut c = Chunk::new();
11377        let a = c.add_constant(StrykeValue::string("same".into()));
11378        let b = c.add_constant(StrykeValue::string("same".into()));
11379        c.emit(Op::LoadConst(a), 1);
11380        c.emit(Op::LoadConst(b), 1);
11381        c.emit(Op::StrEq, 1);
11382        c.emit(Op::Halt, 1);
11383        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11384
11385        let mut c = Chunk::new();
11386        let a = c.add_constant(StrykeValue::string("a".into()));
11387        let b = c.add_constant(StrykeValue::string("b".into()));
11388        c.emit(Op::LoadConst(a), 1);
11389        c.emit(Op::LoadConst(b), 1);
11390        c.emit(Op::StrNe, 1);
11391        c.emit(Op::Halt, 1);
11392        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11393    }
11394
11395    #[test]
11396    fn vm_num_eq_ine() {
11397        let mut c = Chunk::new();
11398        c.emit(Op::LoadInt(1), 1);
11399        c.emit(Op::LoadInt(1), 1);
11400        c.emit(Op::NumEq, 1);
11401        c.emit(Op::Halt, 1);
11402        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11403
11404        let mut c = Chunk::new();
11405        c.emit(Op::LoadInt(1), 1);
11406        c.emit(Op::LoadInt(2), 1);
11407        c.emit(Op::NumNe, 1);
11408        c.emit(Op::Halt, 1);
11409        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11410    }
11411
11412    #[test]
11413    fn vm_num_ordering() {
11414        for (a, b, op, want) in [
11415            (1i64, 2i64, Op::NumLt, 1),
11416            (3i64, 2i64, Op::NumGt, 1),
11417            (2i64, 2i64, Op::NumLe, 1),
11418            (2i64, 2i64, Op::NumGe, 1),
11419        ] {
11420            let mut c = Chunk::new();
11421            c.emit(Op::LoadInt(a), 1);
11422            c.emit(Op::LoadInt(b), 1);
11423            c.emit(op, 1);
11424            c.emit(Op::Halt, 1);
11425            assert_eq!(run_chunk(&c).expect("vm").to_int(), want);
11426        }
11427    }
11428
11429    #[test]
11430    fn vm_concat_and_str_cmp() {
11431        let mut c = Chunk::new();
11432        let i1 = c.add_constant(StrykeValue::string("a".into()));
11433        let i2 = c.add_constant(StrykeValue::string("b".into()));
11434        c.emit(Op::LoadConst(i1), 1);
11435        c.emit(Op::LoadConst(i2), 1);
11436        c.emit(Op::Concat, 1);
11437        c.emit(Op::Halt, 1);
11438        assert_eq!(run_chunk(&c).expect("vm").to_string(), "ab");
11439
11440        let mut c = Chunk::new();
11441        let i1 = c.add_constant(StrykeValue::string("a".into()));
11442        let i2 = c.add_constant(StrykeValue::string("b".into()));
11443        c.emit(Op::LoadConst(i1), 1);
11444        c.emit(Op::LoadConst(i2), 1);
11445        c.emit(Op::StrCmp, 1);
11446        c.emit(Op::Halt, 1);
11447        let v = run_chunk(&c).expect("vm");
11448        assert!(v.to_int() < 0);
11449    }
11450
11451    #[test]
11452    fn vm_log_not() {
11453        let mut c = Chunk::new();
11454        c.emit(Op::LoadInt(0), 1);
11455        c.emit(Op::LogNot, 1);
11456        c.emit(Op::Halt, 1);
11457        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11458    }
11459
11460    #[test]
11461    fn vm_bit_and_or_xor_not() {
11462        let mut c = Chunk::new();
11463        c.emit(Op::LoadInt(0b1100), 1);
11464        c.emit(Op::LoadInt(0b1010), 1);
11465        c.emit(Op::BitAnd, 1);
11466        c.emit(Op::Halt, 1);
11467        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1000);
11468
11469        let mut c = Chunk::new();
11470        c.emit(Op::LoadInt(0b1100), 1);
11471        c.emit(Op::LoadInt(0b1010), 1);
11472        c.emit(Op::BitOr, 1);
11473        c.emit(Op::Halt, 1);
11474        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1110);
11475
11476        let mut c = Chunk::new();
11477        c.emit(Op::LoadInt(0b1100), 1);
11478        c.emit(Op::LoadInt(0b1010), 1);
11479        c.emit(Op::BitXor, 1);
11480        c.emit(Op::Halt, 1);
11481        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b0110);
11482
11483        let mut c = Chunk::new();
11484        c.emit(Op::LoadInt(0), 1);
11485        c.emit(Op::BitNot, 1);
11486        c.emit(Op::Halt, 1);
11487        assert!((run_chunk(&c).expect("vm").to_int() & 0xFF) != 0);
11488    }
11489
11490    #[test]
11491    fn vm_shl_shr() {
11492        let mut c = Chunk::new();
11493        c.emit(Op::LoadInt(1), 1);
11494        c.emit(Op::LoadInt(3), 1);
11495        c.emit(Op::Shl, 1);
11496        c.emit(Op::Halt, 1);
11497        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
11498
11499        let mut c = Chunk::new();
11500        c.emit(Op::LoadInt(16), 1);
11501        c.emit(Op::LoadInt(2), 1);
11502        c.emit(Op::Shr, 1);
11503        c.emit(Op::Halt, 1);
11504        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
11505    }
11506
11507    #[test]
11508    fn vm_load_undef_float_constant() {
11509        let mut c = Chunk::new();
11510        c.emit(Op::LoadUndef, 1);
11511        c.emit(Op::Halt, 1);
11512        assert!(run_chunk(&c).expect("vm").is_undef());
11513
11514        let mut c = Chunk::new();
11515        c.emit(Op::LoadFloat(2.5), 1);
11516        c.emit(Op::Halt, 1);
11517        assert!((run_chunk(&c).expect("vm").to_number() - 2.5).abs() < 1e-9);
11518    }
11519
11520    #[test]
11521    fn vm_jump_skips_ops() {
11522        let mut c = Chunk::new();
11523        let j = c.emit(Op::Jump(0), 1);
11524        c.emit(Op::LoadInt(1), 1);
11525        c.emit(Op::LoadInt(2), 1);
11526        c.emit(Op::Add, 1);
11527        c.patch_jump_here(j);
11528        c.emit(Op::LoadInt(40), 1);
11529        c.emit(Op::Halt, 1);
11530        assert_eq!(run_chunk(&c).expect("vm").to_int(), 40);
11531    }
11532
11533    #[test]
11534    fn vm_jump_if_false() {
11535        let mut c = Chunk::new();
11536        c.emit(Op::LoadInt(0), 1);
11537        let j = c.emit(Op::JumpIfFalse(0), 1);
11538        c.emit(Op::LoadInt(1), 1);
11539        c.emit(Op::Halt, 1);
11540        c.patch_jump_here(j);
11541        c.emit(Op::LoadInt(2), 1);
11542        c.emit(Op::Halt, 1);
11543        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11544    }
11545
11546    #[test]
11547    fn vm_call_builtin_defined() {
11548        let mut c = Chunk::new();
11549        c.emit(Op::LoadUndef, 1);
11550        c.emit(Op::CallBuiltin(BuiltinId::Defined as u16, 1), 1);
11551        c.emit(Op::Halt, 1);
11552        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0);
11553    }
11554
11555    #[test]
11556    fn vm_call_builtin_length_string() {
11557        let mut c = Chunk::new();
11558        let idx = c.add_constant(StrykeValue::string("abc".into()));
11559        c.emit(Op::LoadConst(idx), 1);
11560        c.emit(Op::CallBuiltin(BuiltinId::Length as u16, 1), 1);
11561        c.emit(Op::Halt, 1);
11562        assert_eq!(run_chunk(&c).expect("vm").to_int(), 3);
11563    }
11564
11565    #[test]
11566    fn vm_make_array_two() {
11567        let mut c = Chunk::new();
11568        c.emit(Op::LoadInt(1), 1);
11569        c.emit(Op::LoadInt(2), 1);
11570        c.emit(Op::MakeArray(2), 1);
11571        c.emit(Op::Halt, 1);
11572        let v = run_chunk(&c).expect("vm");
11573        let a = v.as_array_vec().expect("array");
11574        assert_eq!(a.len(), 2);
11575        assert_eq!(a[0].to_int(), 1);
11576        assert_eq!(a[1].to_int(), 2);
11577    }
11578
11579    #[test]
11580    fn vm_spaceship() {
11581        let mut c = Chunk::new();
11582        c.emit(Op::LoadInt(1), 1);
11583        c.emit(Op::LoadInt(2), 1);
11584        c.emit(Op::Spaceship, 1);
11585        c.emit(Op::Halt, 1);
11586        assert_eq!(run_chunk(&c).expect("vm").to_int(), -1);
11587    }
11588
11589    #[test]
11590    fn compiled_try_catch_catches_die_via_vm() {
11591        let program = crate::parse(
11592            r#"
11593        try {
11594            die "boom";
11595        } catch ($err) {
11596            42;
11597        }
11598    "#,
11599        )
11600        .expect("parse");
11601        let chunk = crate::compiler::Compiler::new()
11602            .compile_program(&program)
11603            .expect("compile");
11604        let tp = chunk
11605            .ops
11606            .iter()
11607            .position(|o| matches!(o, Op::TryPush { .. }))
11608            .expect("TryPush op");
11609        match &chunk.ops[tp] {
11610            Op::TryPush {
11611                catch_ip, after_ip, ..
11612            } => {
11613                assert_ne!(*catch_ip, 0, "catch_ip must be patched");
11614                assert_ne!(*after_ip, 0, "after_ip must be patched");
11615            }
11616            _ => unreachable!(),
11617        }
11618        let mut interp = VMHelper::new();
11619        let mut vm = VM::new(&chunk, &mut interp);
11620        vm.set_jit_enabled(false);
11621        let v = vm.execute().expect("vm should catch die");
11622        assert_eq!(v.to_int(), 42);
11623    }
11624}