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                // 2-arg `index` / `rindex` over a captured outer-scope string
1635                // have the same i64-seeding hazard as StrEq/StrNe above. The
1636                // bridge has a `STK_STR_INDEX` host helper that reconstructs
1637                // both operands from raw bits — fine when the slot's raw_bits
1638                // is a live `StrykeValue` handle. But the plain-remap path
1639                // seeds plain-bound captured scalars as `raw_bits() as i64`,
1640                // and for any non-trivial captured string (or a derived value
1641                // like `my $lower = lc($s)`) the JIT produced -1 for every
1642                // call regardless of whether the substring was present —
1643                // making `grep { index($s, _) < 0 }` keep *every* element
1644                // (-1 < 0 is true). The pangram rosetta test surfaced this:
1645                // `grep { index($lower, _) < 0 } 'a'..'z'` returned all 26
1646                // letters for the canonical "the quick brown fox …" string
1647                // (pangram ⇒ should return zero), and `Standard pangram`
1648                // failed across the rosetta suite. Conservatively bail; the
1649                // bridge can still lower grep bodies whose `index` operands
1650                // are constants or topic-only (the `index(LITERAL, _) < N`
1651                // form continues to JIT because plain-remap isn't engaged).
1652                Op::CallBuiltin(id, 2)
1653                    if *id == crate::bytecode::BuiltinId::Index as u16
1654                        || *id == crate::bytecode::BuiltinId::Rindex as u16 =>
1655                {
1656                    return Ok(None)
1657                }
1658                _ => {}
1659            }
1660        }
1661
1662        // Same plain-scalar-read remap as sig subs: rewrite GetScalarPlain(name)
1663        // → GetScalarSlot(synth) so the bridge can seed `$_`/outer-scope reads
1664        // through its slot mechanism. Bails on any named-scalar WRITE.
1665        let base_slot = crate::jit::linear_slot_ops_max_index_seq(full_seg)
1666            .map(|m| m as usize + 1)
1667            .unwrap_or(0);
1668        let plain_remap: Option<(Vec<Op>, Vec<(u8, u16)>)> = if base_slot <= u8::MAX as usize {
1669            crate::jit::plain_scalar_read_names(full_seg).and_then(|pnames| {
1670                if base_slot + pnames.len() <= u8::MAX as usize + 1 {
1671                    Some(crate::jit::remap_plain_reads_to_slots(
1672                        full_seg,
1673                        &pnames,
1674                        base_slot as u8,
1675                    ))
1676                } else {
1677                    None
1678                }
1679            })
1680        } else {
1681            None
1682        };
1683        let (seg, plain_binds): (&[Op], &[(u8, u16)]) = match &plain_remap {
1684            Some((normalized, binds)) => (normalized.as_slice(), binds.as_slice()),
1685            None => (full_seg, &[][..]),
1686        };
1687        let seg_ip = start;
1688
1689        // Eligibility cache (same Vec used by try_fusevm_subroutine — IPs
1690        // don't collide between sub-entries and block-region starts).
1691        let cache_ip = start;
1692        if cache_ip >= self.sub_fusevm_meta.len() {
1693            self.sub_fusevm_meta.resize(cache_ip + 1, None);
1694        }
1695        match &self.sub_fusevm_meta[cache_ip] {
1696            Some(None) => return Ok(None),
1697            Some(Some(_)) => {}
1698            None => {
1699                let computed = compute_fusevm_elig(seg, seg_ip);
1700                self.sub_fusevm_meta[cache_ip] = Some(computed);
1701                if matches!(&self.sub_fusevm_meta[cache_ip], Some(None)) {
1702                    return Ok(None);
1703                }
1704            }
1705        }
1706        let (dispatch, str_handle_slot) = {
1707            let e = self.sub_fusevm_meta[cache_ip]
1708                .as_ref()
1709                .and_then(|x| x.as_ref())
1710                .expect("populated above");
1711            (e.dispatch, e.str_handle_slot)
1712        };
1713        let str_ok = matches!(dispatch, FusevmDispatch::Str);
1714        let lit_str_sprintf_ok = matches!(dispatch, FusevmDispatch::LitStrSprintf);
1715        let val_unary_ok = matches!(dispatch, FusevmDispatch::ValUnary);
1716
1717        // Resolve each plain-remapped synthetic slot to its current scope
1718        // value. For grep `{ length($_) > 1 }` this reads `$_` (the topic,
1719        // set by the caller before each iteration) into one synth slot.
1720        let plain_vals: Vec<(u8, StrykeValue)> = plain_binds
1721            .iter()
1722            .map(|(slot, name_idx)| {
1723                let nm = self.names[*name_idx as usize].clone();
1724                (*slot, self.interp.scope.get_scalar(&nm))
1725            })
1726            .collect();
1727        let plain_of_slot = |slot: u8| -> Option<&StrykeValue> {
1728            plain_vals.iter().find(|(s, _)| *s == slot).map(|(_, v)| v)
1729        };
1730
1731        // Seeder. Same per-slot kind matrix as try_fusevm_subroutine, minus
1732        // the @_ arg-bound branch (blocks have no args). Body-local slots
1733        // get 0 (write-before-read) or are read from the current scope.
1734        let mut slot_n = 0usize;
1735        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1736            let n = max as usize + 1;
1737            self.jit_buf_slot.resize(n, 0);
1738            let str_slot_kinds: Option<Vec<bool>> = if str_ok {
1739                crate::fusevm_bridge::string_bearing_int_result_slot_kinds(seg, seg_ip)
1740            } else {
1741                None
1742            };
1743            let wants_string = |i: u8| -> bool {
1744                match str_handle_slot {
1745                    Some(str_slot) => i == str_slot,
1746                    None => match &str_slot_kinds {
1747                        Some(kinds) => kinds.get(i as usize).copied().unwrap_or(false),
1748                        None => str_ok || val_unary_ok || lit_str_sprintf_ok,
1749                    },
1750                }
1751            };
1752            // Block regions bypass the `is_string_like` gate unconditionally:
1753            // block items (`$_` from grep/map/sort) can be any type — numbers,
1754            // strings, refs — and Perl-style stringification is the standard
1755            // (`length(42)` == 2, `42 eq "42"`, etc.). The bridge's str
1756            // helpers call `as_str()` / `length_value()` / `ord_value()` /
1757            // etc. which all stringify internally, matching the interpreter's
1758            // semantics for non-overloaded values. The narrow risk is an
1759            // operator-overload (`use overload '""'`) producing a different
1760            // string than the helpers' naive stringification — for grep/map
1761            // bodies the convention is to use unblessed scalars, so this
1762            // tradeoff is the right default.
1763            //
1764            // For ValUnary / LitStrSprintf the gate-bypass was already needed
1765            // for sub-call dispatch (`defined`/`ref`/`sprintf` accept any
1766            // type); the unconditional `true` here covers both those cases
1767            // and the general block-region case.
1768            let bypass_type_gate = true;
1769            for i in 0..=max {
1770                self.jit_buf_slot[i as usize] = if let Some(pv) = plain_of_slot(i) {
1771                    if wants_string(i) {
1772                        if !bypass_type_gate && !pv.is_string_like() {
1773                            return Ok(None);
1774                        }
1775                        pv.raw_bits() as i64
1776                    } else {
1777                        match pv.as_integer() {
1778                            Some(v) => v,
1779                            None => return Ok(None),
1780                        }
1781                    }
1782                } else if wants_string(i) {
1783                    let v = self.interp.scope.get_scalar_slot(i);
1784                    if !bypass_type_gate && !v.is_string_like() {
1785                        return Ok(None);
1786                    }
1787                    v.raw_bits() as i64
1788                } else if crate::jit::slot_undef_prefill_ok_seq(seg, i) {
1789                    0
1790                } else {
1791                    match self.interp.scope.get_scalar_slot(i).as_integer() {
1792                        Some(v) => v,
1793                        None => return Ok(None),
1794                    }
1795                };
1796            }
1797            // Silence unused-variable warnings for the val_*_ok flags now
1798            // that bypass_type_gate is unconditionally true.
1799            let _ = (val_unary_ok, lit_str_sprintf_ok);
1800            slot_n = n;
1801        }
1802
1803        crate::fusevm_bridge::set_utf8_pragma(self.interp.utf8_pragma);
1804        let cached_chunk_arc: Option<std::sync::Arc<fusevm::Chunk>> = self
1805            .sub_fusevm_meta
1806            .get(cache_ip)
1807            .and_then(|x| x.as_ref())
1808            .and_then(|x| x.as_ref())
1809            .and_then(|e| e.cached_chunk.get().cloned());
1810        let (result_opt, fresh_chunk) = crate::fusevm_bridge::run_linear_segment_cached(
1811            seg,
1812            seg_ip,
1813            &mut self.jit_buf_slot[..slot_n],
1814            crate::jit::SubTerminator::Value,
1815            &self.constants,
1816            cached_chunk_arc.as_ref(),
1817        );
1818        if let Some(fresh) = fresh_chunk {
1819            if let Some(e) = self
1820                .sub_fusevm_meta
1821                .get(cache_ip)
1822                .and_then(|x| x.as_ref())
1823                .and_then(|x| x.as_ref())
1824            {
1825                let _ = e.cached_chunk.set(fresh);
1826            }
1827        }
1828        Ok(result_opt)
1829    }
1830
1831    /// Cranelift linear JIT for a subroutine body when `ip` is a compiled sub entry (see `Chunk::sub_entries`).
1832    /// Returns `Ok(true)` when the sub was executed natively and the VM should continue at `return_ip`.
1833    fn try_jit_subroutine_linear(&mut self) -> Result<bool, StrykeError> {
1834        let ip = self.ip;
1835        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1836        if self.sub_jit_skip_linear_test(ip) {
1837            return Ok(false);
1838        }
1839        let ops: &Vec<Op> = &self.ops;
1840        let ops = ops as *const Vec<Op>;
1841        let ops = unsafe { &*ops };
1842        let constants: &Vec<StrykeValue> = &self.constants;
1843        let constants = constants as *const Vec<StrykeValue>;
1844        let constants = unsafe { &*constants };
1845        let names: &Vec<String> = &self.names;
1846        let names = names as *const Vec<String>;
1847        let names = unsafe { &*names };
1848        let Some((seg, _)) = crate::jit::sub_entry_segment(ops, ip) else {
1849            return Ok(false);
1850        };
1851        // `try_run_linear_sub` rejects these segments without compiling — skip expensive work before
1852        // resize/fill of reusable scratch buffers (`jit_buf_*`).
1853        if crate::jit::segment_blocks_subroutine_linear_jit(seg, &self.sub_entries) {
1854            self.sub_jit_skip_linear_mark(ip);
1855            return Ok(false);
1856        }
1857        let mut slot_len: Option<usize> = None;
1858        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1859            let n = max as usize + 1;
1860            self.jit_buf_slot.resize(n, 0);
1861            let mut ok = true;
1862            for i in 0..=max {
1863                let pv = self.interp.scope.get_scalar_slot(i);
1864                self.jit_buf_slot[i as usize] = match pv.as_integer() {
1865                    Some(v) => v,
1866                    None if pv.is_undef() && crate::jit::slot_undef_prefill_ok_seq(seg, i) => 0,
1867                    None => {
1868                        ok = false;
1869                        break;
1870                    }
1871                };
1872            }
1873            if ok {
1874                slot_len = Some(n);
1875            }
1876        }
1877        let mut plain_len: Option<usize> = None;
1878        if let Some(max) = crate::jit::linear_plain_ops_max_index_seq(seg) {
1879            if (max as usize) < names.len() {
1880                let n = max as usize + 1;
1881                self.jit_buf_plain.resize(n, 0);
1882                let mut ok = true;
1883                for i in 0..=max {
1884                    let nm = names[i as usize].as_str();
1885                    match self.interp.scope.get_scalar(nm).as_integer() {
1886                        Some(v) => self.jit_buf_plain[i as usize] = v,
1887                        None => {
1888                            ok = false;
1889                            break;
1890                        }
1891                    }
1892                }
1893                if ok {
1894                    plain_len = Some(n);
1895                }
1896            }
1897        }
1898        let mut arg_len: Option<usize> = None;
1899        if let Some(max) = crate::jit::linear_arg_ops_max_index_seq(seg) {
1900            if let Some(frame) = self.call_stack.last() {
1901                let base = frame.stack_base;
1902                let n = max as usize + 1;
1903                self.jit_buf_arg.resize(n, 0);
1904                let mut ok = true;
1905                for i in 0..=max {
1906                    let pos = base + i as usize;
1907                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1908                    match pv.as_integer() {
1909                        Some(v) => self.jit_buf_arg[i as usize] = v,
1910                        None => {
1911                            ok = false;
1912                            break;
1913                        }
1914                    }
1915                }
1916                if ok {
1917                    arg_len = Some(n);
1918                }
1919            }
1920        }
1921        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1922        let slot_buf = slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1923        let plain_buf = plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1924        let arg_buf = arg_len.map(|n| &self.jit_buf_arg[..n]);
1925        let Some(v) = crate::jit::try_run_linear_sub(
1926            ops,
1927            ip,
1928            slot_buf,
1929            plain_buf,
1930            arg_buf,
1931            constants,
1932            &self.sub_entries,
1933            vm_ptr,
1934        ) else {
1935            return Ok(false);
1936        };
1937        if let Some(n) = slot_len {
1938            let buf = &self.jit_buf_slot[..n];
1939            for idx in crate::jit::linear_slot_ops_written_indices_seq(seg) {
1940                self.interp
1941                    .scope
1942                    .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
1943            }
1944        }
1945        if let Some(n) = plain_len {
1946            let buf = &self.jit_buf_plain[..n];
1947            for idx in crate::jit::linear_plain_ops_written_indices_seq(seg) {
1948                let name = names[idx as usize].as_str();
1949                self.interp
1950                    .scope
1951                    .set_scalar(name, StrykeValue::integer(buf[idx as usize]))
1952                    .map_err(|e| e.at_line(self.line()))?;
1953            }
1954        }
1955        if let Some(frame) = self.call_stack.pop() {
1956            self.interp.wantarray_kind = frame.saved_wantarray;
1957            self.stack.truncate(frame.stack_base);
1958            self.interp.pop_scope_to_depth(frame.scope_depth);
1959            if frame.jit_trampoline_return {
1960                self.jit_trampoline_out = Some(v);
1961            } else {
1962                self.push(v);
1963                self.ip = frame.return_ip;
1964            }
1965        }
1966        Ok(true)
1967    }
1968
1969    /// Cranelift block JIT for a subroutine with control flow (see [`crate::jit::block_jit_validate_sub`]).
1970    fn try_jit_subroutine_block(&mut self) -> Result<bool, StrykeError> {
1971        let ip = self.ip;
1972        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1973        if self.sub_jit_skip_block_test(ip) {
1974            return Ok(false);
1975        }
1976        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1977        let ops: &Vec<Op> = &self.ops;
1978        let constants: &Vec<StrykeValue> = &self.constants;
1979        let names: &Vec<String> = &self.names;
1980        let Some((full_body, term)) = crate::jit::sub_full_body(ops, ip) else {
1981            return Ok(false);
1982        };
1983        if crate::jit::sub_body_blocks_subroutine_block_jit(full_body) {
1984            self.sub_jit_skip_block_mark(ip);
1985            return Ok(false);
1986        }
1987        let Some(validated) =
1988            crate::jit::block_jit_validate_sub(full_body, constants, term, &self.sub_entries)
1989        else {
1990            self.sub_jit_skip_block_mark(ip);
1991            return Ok(false);
1992        };
1993        let block_buf_mode = validated.buffer_mode();
1994
1995        let mut b_slot_len: Option<usize> = None;
1996        if let Some(max) = crate::jit::block_slot_ops_max_index(full_body) {
1997            let n = max as usize + 1;
1998            self.jit_buf_slot.resize(n, 0);
1999            let mut ok = true;
2000            for i in 0..=max {
2001                let pv = self.interp.scope.get_scalar_slot(i);
2002                self.jit_buf_slot[i as usize] = match block_buf_mode {
2003                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => pv.raw_bits() as i64,
2004                    crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2005                        Some(v) => v,
2006                        None if pv.is_undef()
2007                            && crate::jit::block_slot_undef_prefill_ok(full_body, i) =>
2008                        {
2009                            0
2010                        }
2011                        None => {
2012                            ok = false;
2013                            break;
2014                        }
2015                    },
2016                };
2017            }
2018            if ok {
2019                b_slot_len = Some(n);
2020            }
2021        }
2022
2023        let mut b_plain_len: Option<usize> = None;
2024        if let Some(max) = crate::jit::block_plain_ops_max_index(full_body) {
2025            if (max as usize) < names.len() {
2026                let n = max as usize + 1;
2027                self.jit_buf_plain.resize(n, 0);
2028                let mut ok = true;
2029                for i in 0..=max {
2030                    let nm = names[i as usize].as_str();
2031                    let pv = self.interp.scope.get_scalar(nm);
2032                    self.jit_buf_plain[i as usize] = match block_buf_mode {
2033                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2034                            pv.raw_bits() as i64
2035                        }
2036                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2037                            Some(v) => v,
2038                            None => {
2039                                ok = false;
2040                                break;
2041                            }
2042                        },
2043                    };
2044                }
2045                if ok {
2046                    b_plain_len = Some(n);
2047                }
2048            }
2049        }
2050
2051        let mut b_arg_len: Option<usize> = None;
2052        if let Some(max) = crate::jit::block_arg_ops_max_index(full_body) {
2053            if let Some(frame) = self.call_stack.last() {
2054                let base = frame.stack_base;
2055                let n = max as usize + 1;
2056                self.jit_buf_arg.resize(n, 0);
2057                let mut ok = true;
2058                for i in 0..=max {
2059                    let pos = base + i as usize;
2060                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
2061                    self.jit_buf_arg[i as usize] = match block_buf_mode {
2062                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2063                            pv.raw_bits() as i64
2064                        }
2065                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2066                            Some(v) => v,
2067                            None => {
2068                                ok = false;
2069                                break;
2070                            }
2071                        },
2072                    };
2073                }
2074                if ok {
2075                    b_arg_len = Some(n);
2076                }
2077            }
2078        }
2079
2080        let block_slot_buf = b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
2081        let block_plain_buf = b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
2082        let block_arg_buf = b_arg_len.map(|n| &self.jit_buf_arg[..n]);
2083
2084        let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
2085            full_body,
2086            block_slot_buf,
2087            block_plain_buf,
2088            block_arg_buf,
2089            constants,
2090            Some(validated),
2091            vm_ptr,
2092            &self.sub_entries,
2093        ) else {
2094            self.sub_jit_skip_block_mark(ip);
2095            return Ok(false);
2096        };
2097
2098        if let Some(n) = b_slot_len {
2099            let buf = &self.jit_buf_slot[..n];
2100            for idx in crate::jit::block_slot_ops_written_indices(full_body) {
2101                let bits = buf[idx as usize] as u64;
2102                let pv = match buf_mode {
2103                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2104                        StrykeValue::from_raw_bits(bits)
2105                    }
2106                    crate::jit::BlockJitBufferMode::I64AsInteger => {
2107                        StrykeValue::integer(buf[idx as usize])
2108                    }
2109                };
2110                self.interp.scope.set_scalar_slot(idx, pv);
2111            }
2112        }
2113        if let Some(n) = b_plain_len {
2114            let buf = &self.jit_buf_plain[..n];
2115            for idx in crate::jit::block_plain_ops_written_indices(full_body) {
2116                let name = names[idx as usize].as_str();
2117                let bits = buf[idx as usize] as u64;
2118                let pv = match buf_mode {
2119                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2120                        StrykeValue::from_raw_bits(bits)
2121                    }
2122                    crate::jit::BlockJitBufferMode::I64AsInteger => {
2123                        StrykeValue::integer(buf[idx as usize])
2124                    }
2125                };
2126                self.interp
2127                    .scope
2128                    .set_scalar(name, pv)
2129                    .map_err(|e| e.at_line(self.line()))?;
2130            }
2131        }
2132        if let Some(frame) = self.call_stack.pop() {
2133            self.interp.wantarray_kind = frame.saved_wantarray;
2134            self.stack.truncate(frame.stack_base);
2135            self.interp.pop_scope_to_depth(frame.scope_depth);
2136            if frame.jit_trampoline_return {
2137                self.jit_trampoline_out = Some(v);
2138            } else {
2139                self.push(v);
2140                self.ip = frame.return_ip;
2141            }
2142        }
2143        Ok(true)
2144    }
2145
2146    fn run_method_op(
2147        &mut self,
2148        name_idx: u16,
2149        argc: u8,
2150        wa: u8,
2151        super_call: bool,
2152    ) -> StrykeResult<()> {
2153        let method_owned = self.names[name_idx as usize].clone();
2154        let argc = argc as usize;
2155        let want = WantarrayCtx::from_byte(wa);
2156        let mut args = Vec::with_capacity(argc);
2157        for _ in 0..argc {
2158            args.push(self.pop());
2159        }
2160        args.reverse();
2161        let obj = self.pop();
2162        let method = method_owned.as_str();
2163        if let Some(r) = crate::pchannel::dispatch_method(&obj, method, &args, self.line()) {
2164            self.push(r?);
2165            return Ok(());
2166        }
2167        if let Some(r) = self
2168            .interp
2169            .try_native_method(&obj, method, &args, self.line())
2170        {
2171            self.push(r?);
2172            return Ok(());
2173        }
2174        let class = if let Some(b) = obj.as_blessed_ref() {
2175            b.class.clone()
2176        } else if let Some(s) = obj.as_str() {
2177            s
2178        } else {
2179            return Err(StrykeError::runtime(
2180                "Can't call method on non-object",
2181                self.line(),
2182            ));
2183        };
2184        if method == "VERSION" && !super_call {
2185            if let Some(ver) = self.interp.package_version_scalar(class.as_str())? {
2186                self.push(ver);
2187                return Ok(());
2188            }
2189        }
2190        // UNIVERSAL methods: isa, can, DOES
2191        if !super_call {
2192            match method {
2193                "isa" => {
2194                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
2195                    let mro = self.interp.mro_linearize(&class);
2196                    let result = mro.iter().any(|c| c == &target);
2197                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
2198                    return Ok(());
2199                }
2200                "can" => {
2201                    let target_method = args.first().map(|v| v.to_string()).unwrap_or_default();
2202                    let found = self
2203                        .interp
2204                        .resolve_method_full_name(&class, &target_method, false)
2205                        .and_then(|fq| self.interp.subs.get(&fq))
2206                        .is_some();
2207                    if found {
2208                        self.push(StrykeValue::code_ref(std::sync::Arc::new(
2209                            crate::value::StrykeSub {
2210                                name: target_method,
2211                                params: vec![],
2212                                body: vec![],
2213                                closure_env: None,
2214                                prototype: None,
2215                                fib_like: None,
2216                            },
2217                        )));
2218                    } else {
2219                        self.push(StrykeValue::UNDEF);
2220                    }
2221                    return Ok(());
2222                }
2223                "DOES" => {
2224                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
2225                    let mro = self.interp.mro_linearize(&class);
2226                    let result = mro.iter().any(|c| c == &target);
2227                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
2228                    return Ok(());
2229                }
2230                _ => {}
2231            }
2232        }
2233        let mut all_args = vec![obj];
2234        all_args.extend(args);
2235        let full_name = match self
2236            .interp
2237            .resolve_method_full_name(&class, method, super_call)
2238        {
2239            Some(f) => f,
2240            None => {
2241                return Err(StrykeError::runtime(
2242                    format!(
2243                        "Can't locate method \"{}\" via inheritance (invocant \"{}\")",
2244                        method, class
2245                    ),
2246                    self.line(),
2247                ));
2248            }
2249        };
2250        if let Some(sub) = self.interp.subs.get(&full_name).cloned() {
2251            let saved_wa = self.interp.wantarray_kind;
2252            self.interp.wantarray_kind = want;
2253            self.interp.scope_push_hook();
2254            self.interp.scope.declare_array("_", all_args);
2255            if let Some(ref env) = sub.closure_env {
2256                self.interp.scope.restore_capture(env);
2257            }
2258            let line = self.line();
2259            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
2260            self.interp
2261                .apply_sub_signature(sub.as_ref(), &argv, line)
2262                .map_err(|e| e.at_line(line))?;
2263            self.interp.scope.declare_array("_", argv);
2264            let result = self.interp.exec_block_no_scope(&sub.body);
2265            self.interp.wantarray_kind = saved_wa;
2266            self.interp.scope_pop_hook();
2267            match result {
2268                Ok(v) => self.push(v),
2269                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
2270                    self.push(v)
2271                }
2272                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2273                Err(_) => self.push(StrykeValue::UNDEF),
2274            }
2275        } else if method == "new" && !super_call {
2276            if class == "Set" {
2277                self.push(crate::value::set_from_elements(
2278                    all_args.into_iter().skip(1),
2279                ));
2280            } else if let Some(def) = self.interp.struct_defs.get(&class).cloned() {
2281                let line = self.line();
2282                let mut provided = Vec::new();
2283                let mut i = 1;
2284                while i + 1 < all_args.len() {
2285                    let k = all_args[i].to_string();
2286                    let v = all_args[i + 1].clone();
2287                    provided.push((k, v));
2288                    i += 2;
2289                }
2290                let mut defaults = Vec::with_capacity(def.fields.len());
2291                for field in &def.fields {
2292                    if let Some(ref expr) = field.default {
2293                        let val = self.interp.eval_expr(expr).map_err(|e| match e {
2294                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
2295                            _ => StrykeError::runtime("default evaluation flow", line),
2296                        })?;
2297                        defaults.push(Some(val));
2298                    } else {
2299                        defaults.push(None);
2300                    }
2301                }
2302                let v =
2303                    crate::native_data::struct_new_with_defaults(&def, &provided, &defaults, line)?;
2304                self.push(v);
2305            } else if let Some(def) = self.interp.class_defs.get(&class).cloned() {
2306                // Stryke `class` declarations route through `class_construct`
2307                // so the result is a real `ClassInstance` (typed-my checks,
2308                // isa walk, BUILD hooks). Without this the bytecode path
2309                // fell through to the default Perl-style blessed-hashref
2310                // below, breaking method dispatch for `$self` binding.
2311                // Mirrors the tree-walker fix in `vm_helper::builtin_new`.
2312                // Skip `all_args[0]` (the class-name receiver) since
2313                // `class_construct` expects user args only.
2314                let line = self.line();
2315                let user_args: Vec<StrykeValue> = all_args.into_iter().skip(1).collect();
2316                let v =
2317                    self.interp
2318                        .class_construct(&def, user_args, line)
2319                        .map_err(|e| match e {
2320                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
2321                            _ => StrykeError::runtime("class_construct flow", line),
2322                        })?;
2323                self.push(v);
2324            } else {
2325                let mut map = IndexMap::new();
2326                let mut i = 1;
2327                while i + 1 < all_args.len() {
2328                    map.insert(all_args[i].to_string(), all_args[i + 1].clone());
2329                    i += 2;
2330                }
2331                self.push(StrykeValue::blessed(Arc::new(
2332                    crate::value::BlessedRef::new_blessed(class, StrykeValue::hash(map)),
2333                )));
2334            }
2335        } else if let Some(result) =
2336            self.interp
2337                .try_autoload_call(&full_name, all_args, self.line(), want, Some(&class))
2338        {
2339            match result {
2340                Ok(v) => self.push(v),
2341                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
2342                    self.push(v)
2343                }
2344                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2345                Err(_) => self.push(StrykeValue::UNDEF),
2346            }
2347        } else {
2348            return Err(StrykeError::runtime(
2349                format!(
2350                    "Can't locate method \"{}\" in package \"{}\"",
2351                    method, class
2352                ),
2353                self.line(),
2354            ));
2355        }
2356        Ok(())
2357    }
2358
2359    fn run_fan_block(
2360        &mut self,
2361        block_idx: u16,
2362        n: usize,
2363        line: usize,
2364        progress: bool,
2365    ) -> StrykeResult<()> {
2366        let block = self.blocks[block_idx as usize].clone();
2367        let subs = self.interp.subs.clone();
2368        let (scope_capture, atomic_arrays, atomic_hashes) =
2369            self.interp.scope.capture_with_atomics();
2370        // Worker bodies execute via the tree walker (`exec_block_no_scope`) which uses
2371        // `tree_scalar_storage_name` to rewrite `$x` → `Pkg::x`. That helper consults
2372        // `english_lexical_scalars` + `our_lexical_scalars` — empty in a fresh worker —
2373        // so without copying the parent's sets, `our` / `oursync` reads see UNDEF.
2374        let lex_scalars = self.interp.english_lexical_scalars_clone();
2375        let our_scalars = self.interp.our_lexical_scalars_clone();
2376        let fan_progress = FanProgress::new(progress, n);
2377        let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
2378        (0..n).into_par_iter().for_each(|i| {
2379            if first_err.lock().is_some() {
2380                return;
2381            }
2382            fan_progress.start_worker(i);
2383            let mut local_interp = VMHelper::new();
2384            local_interp.subs = subs.clone();
2385            local_interp.suppress_stdout = progress;
2386            local_interp.scope.restore_capture(&scope_capture);
2387            local_interp
2388                .scope
2389                .restore_atomics(&atomic_arrays, &atomic_hashes);
2390            local_interp.set_english_lexical_scalars(lex_scalars.clone());
2391            local_interp.set_our_lexical_scalars(our_scalars.clone());
2392            local_interp.enable_parallel_guard();
2393            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
2394            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
2395            local_interp.scope_push_hook();
2396            match local_interp.exec_block_no_scope(&block) {
2397                Ok(_) => {}
2398                Err(e) => {
2399                    let stryke = match e {
2400                        FlowOrError::Error(stryke) => stryke,
2401                        FlowOrError::Flow(_) => StrykeError::runtime(
2402                            "return/last/next/redo not supported inside fan block",
2403                            line,
2404                        ),
2405                    };
2406                    let mut g = first_err.lock();
2407                    if g.is_none() {
2408                        *g = Some(stryke);
2409                    }
2410                }
2411            }
2412            local_interp.scope_pop_hook();
2413            crate::parallel_trace::fan_worker_set_index(None);
2414            fan_progress.finish_worker(i);
2415        });
2416        fan_progress.finish();
2417        if let Some(e) = first_err.lock().take() {
2418            return Err(e);
2419        }
2420        self.push(StrykeValue::UNDEF);
2421        Ok(())
2422    }
2423
2424    fn run_fan_cap_block(
2425        &mut self,
2426        block_idx: u16,
2427        n: usize,
2428        line: usize,
2429        progress: bool,
2430    ) -> StrykeResult<()> {
2431        let block = self.blocks[block_idx as usize].clone();
2432        let subs = self.interp.subs.clone();
2433        let (scope_capture, atomic_arrays, atomic_hashes) =
2434            self.interp.scope.capture_with_atomics();
2435        // See run_fan_block for why we copy lexical-scalar tracking sets.
2436        let lex_scalars = self.interp.english_lexical_scalars_clone();
2437        let our_scalars = self.interp.our_lexical_scalars_clone();
2438        let fan_progress = FanProgress::new(progress, n);
2439        let pairs: Vec<(usize, Result<StrykeValue, FlowOrError>)> = (0..n)
2440            .into_par_iter()
2441            .map(|i| {
2442                fan_progress.start_worker(i);
2443                let mut local_interp = VMHelper::new();
2444                local_interp.subs = subs.clone();
2445                local_interp.suppress_stdout = progress;
2446                local_interp.scope.restore_capture(&scope_capture);
2447                local_interp
2448                    .scope
2449                    .restore_atomics(&atomic_arrays, &atomic_hashes);
2450                local_interp.set_english_lexical_scalars(lex_scalars.clone());
2451                local_interp.set_our_lexical_scalars(our_scalars.clone());
2452                local_interp.enable_parallel_guard();
2453                local_interp.scope.set_topic(StrykeValue::integer(i as i64));
2454                crate::parallel_trace::fan_worker_set_index(Some(i as i64));
2455                local_interp.scope_push_hook();
2456                let res = local_interp.exec_block_no_scope(&block);
2457                local_interp.scope_pop_hook();
2458                crate::parallel_trace::fan_worker_set_index(None);
2459                fan_progress.finish_worker(i);
2460                (i, res)
2461            })
2462            .collect();
2463        fan_progress.finish();
2464        let mut pairs = pairs;
2465        pairs.sort_by_key(|(i, _)| *i);
2466        let mut out = Vec::with_capacity(n);
2467        for (_, r) in pairs {
2468            match r {
2469                Ok(v) => out.push(v),
2470                Err(e) => {
2471                    let stryke = match e {
2472                        FlowOrError::Error(stryke) => stryke,
2473                        FlowOrError::Flow(_) => StrykeError::runtime(
2474                            "return/last/next/redo not supported inside fan_cap block",
2475                            line,
2476                        ),
2477                    };
2478                    return Err(stryke);
2479                }
2480            }
2481        }
2482        self.push(StrykeValue::array(out));
2483        Ok(())
2484    }
2485
2486    fn require_scalar_mutable(&self, name: &str) -> StrykeResult<()> {
2487        if self.interp.scope.is_scalar_frozen(name) {
2488            return Err(StrykeError::syntax(
2489                format!("cannot assign to frozen variable `${}`", name),
2490                self.line(),
2491            ));
2492        }
2493        Ok(())
2494    }
2495
2496    fn require_array_mutable(&self, name: &str) -> StrykeResult<()> {
2497        if self.interp.scope.is_array_frozen(name) {
2498            return Err(StrykeError::syntax(
2499                format!("cannot modify frozen array `@{}`", name),
2500                self.line(),
2501            ));
2502        }
2503        Ok(())
2504    }
2505
2506    fn require_hash_mutable(&self, name: &str) -> StrykeResult<()> {
2507        if self.interp.scope.is_hash_frozen(name) || Self::is_reflection_hash(name) {
2508            return Err(StrykeError::syntax(
2509                format!("cannot modify frozen hash `%{}`", name),
2510                self.line(),
2511            ));
2512        }
2513        Ok(())
2514    }
2515
2516    /// Reflection hashes are frozen builtins even before lazy init.
2517    fn is_reflection_hash(name: &str) -> bool {
2518        matches!(name, "b" | "pc" | "e" | "a" | "d" | "c" | "p" | "all")
2519            || name.starts_with("stryke::")
2520    }
2521
2522    /// Run bytecode: first attempts Cranelift method JIT for eligible numeric fragments (unless
2523    /// [`VM::set_jit_enabled`] disabled it). For block JIT, `block_jit_validate` runs once per attempt;
2524    /// buffers may use `StrykeValue::raw_bits` for `defined`-style control flow. Then the main opcode
2525    /// interpreter loop.
2526    pub fn execute(&mut self) -> StrykeResult<StrykeValue> {
2527        let ops_ref: &Vec<Op> = &self.ops;
2528        let ops = ops_ref as *const Vec<Op>;
2529        // SAFETY: ops doesn't change during execution; pointer avoids borrow on self
2530        let ops = unsafe { &*ops };
2531        let names_ref: &Vec<String> = &self.names;
2532        let names = names_ref as *const Vec<String>;
2533        // SAFETY: names doesn't change during execution; pointer avoids borrow on self
2534        let names = unsafe { &*names };
2535        let constants_ref: &Vec<StrykeValue> = &self.constants;
2536        let constants = constants_ref as *const Vec<StrykeValue>;
2537        // SAFETY: constants doesn't change during execution; pointer avoids borrow on self
2538        let constants = unsafe { &*constants };
2539        let mut last = StrykeValue::UNDEF;
2540        // Safety limit: [`run_main_dispatch_loop`] counts ops (1B cap).
2541        let mut op_count: u64 = 0;
2542
2543        // Match Perl signal delivery: deliver `%SIG` and set `$^C` latch (Unix).
2544        crate::perl_signal::poll(self.interp)?;
2545        if self.jit_enabled {
2546            let mut top_slot_len: Option<usize> = None;
2547            if let Some(max) = crate::jit::linear_slot_ops_max_index(ops) {
2548                let n = max as usize + 1;
2549                self.jit_buf_slot.resize(n, 0);
2550                let mut ok = true;
2551                for i in 0..=max {
2552                    let pv = self.interp.scope.get_scalar_slot(i);
2553                    self.jit_buf_slot[i as usize] = match pv.as_integer() {
2554                        Some(v) => v,
2555                        None if pv.is_undef() && crate::jit::slot_undef_prefill_ok(ops, i) => 0,
2556                        None => {
2557                            ok = false;
2558                            break;
2559                        }
2560                    };
2561                }
2562                if ok {
2563                    top_slot_len = Some(n);
2564                }
2565            }
2566
2567            let mut top_plain_len: Option<usize> = None;
2568            if let Some(max) = crate::jit::linear_plain_ops_max_index(ops) {
2569                if (max as usize) < names.len() {
2570                    let n = max as usize + 1;
2571                    self.jit_buf_plain.resize(n, 0);
2572                    let mut ok = true;
2573                    for i in 0..=max {
2574                        let nm = names[i as usize].as_str();
2575                        match self.interp.scope.get_scalar(nm).as_integer() {
2576                            Some(v) => self.jit_buf_plain[i as usize] = v,
2577                            None => {
2578                                ok = false;
2579                                break;
2580                            }
2581                        }
2582                    }
2583                    if ok {
2584                        top_plain_len = Some(n);
2585                    }
2586                }
2587            }
2588
2589            let mut top_arg_len: Option<usize> = None;
2590            if let Some(max) = crate::jit::linear_arg_ops_max_index(ops) {
2591                if let Some(frame) = self.call_stack.last() {
2592                    let base = frame.stack_base;
2593                    let n = max as usize + 1;
2594                    self.jit_buf_arg.resize(n, 0);
2595                    let mut ok = true;
2596                    for i in 0..=max {
2597                        let pos = base + i as usize;
2598                        let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
2599                        match pv.as_integer() {
2600                            Some(v) => self.jit_buf_arg[i as usize] = v,
2601                            None => {
2602                                ok = false;
2603                                break;
2604                            }
2605                        }
2606                    }
2607                    if ok {
2608                        top_arg_len = Some(n);
2609                    }
2610                }
2611            }
2612
2613            let slot_buf = top_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
2614            let plain_buf = top_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
2615            let arg_buf = top_arg_len.map(|n| &self.jit_buf_arg[..n]);
2616
2617            if let Some(v) =
2618                crate::jit::try_run_linear_ops(ops, slot_buf, plain_buf, arg_buf, constants)
2619            {
2620                if let Some(n) = top_slot_len {
2621                    let buf = &self.jit_buf_slot[..n];
2622                    for idx in crate::jit::linear_slot_ops_written_indices(ops) {
2623                        self.interp
2624                            .scope
2625                            .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
2626                    }
2627                }
2628                if let Some(n) = top_plain_len {
2629                    let buf = &self.jit_buf_plain[..n];
2630                    for idx in crate::jit::linear_plain_ops_written_indices(ops) {
2631                        let name = names[idx as usize].as_str();
2632                        self.interp
2633                            .scope
2634                            .set_scalar(name, StrykeValue::integer(buf[idx as usize]))?;
2635                    }
2636                }
2637                return Ok(v);
2638            }
2639
2640            // ── Block JIT: try to compile sequences with control flow (loops, conditionals). ──
2641            if let Some(validated) =
2642                crate::jit::block_jit_validate(ops, constants, &self.sub_entries)
2643            {
2644                let block_buf_mode = validated.buffer_mode();
2645
2646                let mut top_b_slot_len: Option<usize> = None;
2647                if let Some(max) = crate::jit::block_slot_ops_max_index(ops) {
2648                    let n = max as usize + 1;
2649                    self.jit_buf_slot.resize(n, 0);
2650                    let mut ok = true;
2651                    for i in 0..=max {
2652                        let pv = self.interp.scope.get_scalar_slot(i);
2653                        self.jit_buf_slot[i as usize] = match block_buf_mode {
2654                            crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2655                                pv.raw_bits() as i64
2656                            }
2657                            crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
2658                                Some(v) => v,
2659                                None if pv.is_undef()
2660                                    && crate::jit::block_slot_undef_prefill_ok(ops, i) =>
2661                                {
2662                                    0
2663                                }
2664                                None => {
2665                                    ok = false;
2666                                    break;
2667                                }
2668                            },
2669                        };
2670                    }
2671                    if ok {
2672                        top_b_slot_len = Some(n);
2673                    }
2674                }
2675
2676                let mut top_b_plain_len: Option<usize> = None;
2677                if let Some(max) = crate::jit::block_plain_ops_max_index(ops) {
2678                    if (max as usize) < names.len() {
2679                        let n = max as usize + 1;
2680                        self.jit_buf_plain.resize(n, 0);
2681                        let mut ok = true;
2682                        for i in 0..=max {
2683                            let nm = names[i as usize].as_str();
2684                            let pv = self.interp.scope.get_scalar(nm);
2685                            self.jit_buf_plain[i as usize] = match block_buf_mode {
2686                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2687                                    pv.raw_bits() as i64
2688                                }
2689                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2690                                    match pv.as_integer() {
2691                                        Some(v) => v,
2692                                        None => {
2693                                            ok = false;
2694                                            break;
2695                                        }
2696                                    }
2697                                }
2698                            };
2699                        }
2700                        if ok {
2701                            top_b_plain_len = Some(n);
2702                        }
2703                    }
2704                }
2705
2706                let mut top_b_arg_len: Option<usize> = None;
2707                if let Some(max) = crate::jit::block_arg_ops_max_index(ops) {
2708                    if let Some(frame) = self.call_stack.last() {
2709                        let base = frame.stack_base;
2710                        let n = max as usize + 1;
2711                        self.jit_buf_arg.resize(n, 0);
2712                        let mut ok = true;
2713                        for i in 0..=max {
2714                            let pos = base + i as usize;
2715                            let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
2716                            self.jit_buf_arg[i as usize] = match block_buf_mode {
2717                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2718                                    pv.raw_bits() as i64
2719                                }
2720                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2721                                    match pv.as_integer() {
2722                                        Some(v) => v,
2723                                        None => {
2724                                            ok = false;
2725                                            break;
2726                                        }
2727                                    }
2728                                }
2729                            };
2730                        }
2731                        if ok {
2732                            top_b_arg_len = Some(n);
2733                        }
2734                    }
2735                }
2736
2737                let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
2738                let block_slot_buf = top_b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
2739                let block_plain_buf = top_b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
2740                let block_arg_buf = top_b_arg_len.map(|n| &self.jit_buf_arg[..n]);
2741
2742                if let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
2743                    ops,
2744                    block_slot_buf,
2745                    block_plain_buf,
2746                    block_arg_buf,
2747                    constants,
2748                    Some(validated),
2749                    vm_ptr,
2750                    &self.sub_entries,
2751                ) {
2752                    if let Some(n) = top_b_slot_len {
2753                        let buf = &self.jit_buf_slot[..n];
2754                        for idx in crate::jit::block_slot_ops_written_indices(ops) {
2755                            let bits = buf[idx as usize] as u64;
2756                            let pv = match buf_mode {
2757                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2758                                    StrykeValue::from_raw_bits(bits)
2759                                }
2760                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2761                                    StrykeValue::integer(buf[idx as usize])
2762                                }
2763                            };
2764                            self.interp.scope.set_scalar_slot(idx, pv);
2765                        }
2766                    }
2767                    if let Some(n) = top_b_plain_len {
2768                        let buf = &self.jit_buf_plain[..n];
2769                        for idx in crate::jit::block_plain_ops_written_indices(ops) {
2770                            let name = names[idx as usize].as_str();
2771                            let bits = buf[idx as usize] as u64;
2772                            let pv = match buf_mode {
2773                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
2774                                    StrykeValue::from_raw_bits(bits)
2775                                }
2776                                crate::jit::BlockJitBufferMode::I64AsInteger => {
2777                                    StrykeValue::integer(buf[idx as usize])
2778                                }
2779                            };
2780                            self.interp.scope.set_scalar(name, pv)?;
2781                        }
2782                    }
2783                    return Ok(v);
2784                }
2785            }
2786        }
2787
2788        last = self.run_main_dispatch_loop(last, &mut op_count, true)?;
2789
2790        Ok(last)
2791    }
2792
2793    /// `die` / runtime errors inside `try` jump to `catch_ip` unless the error is [`ErrorKind::Exit`].
2794    ///
2795    /// Walks the try stack top-down looking for a frame still in `Trying` state. Frames in
2796    /// `Catching` / `Finalizing` are skipped (and possibly popped) so that re-raising from
2797    /// inside `catch` or `finally` propagates outward instead of re-entering the same handler.
2798    /// If a `Catching` frame has a `finally`, that finally still runs (with the new error
2799    /// deferred) before the propagation continues.
2800    fn try_recover_from_exception(&mut self, e: &StrykeError) -> StrykeResult<bool> {
2801        if matches!(e.kind, ErrorKind::Exit(_)) {
2802            return Ok(false);
2803        }
2804        loop {
2805            let Some(frame) = self.try_stack.last() else {
2806                return Ok(false);
2807            };
2808            let op_idx = frame.try_push_op_idx;
2809            let Op::TryPush {
2810                catch_ip,
2811                finally_ip,
2812                ..
2813            } = &self.ops[op_idx]
2814            else {
2815                return Ok(false);
2816            };
2817            let catch_ip = *catch_ip;
2818            let finally_ip = *finally_ip;
2819            match frame.state {
2820                TryState::Trying => {
2821                    let val = e
2822                        .die_value
2823                        .clone()
2824                        .unwrap_or_else(|| StrykeValue::string(e.to_string()));
2825                    self.pending_catch_error = Some(val);
2826                    if let Some(top) = self.try_stack.last_mut() {
2827                        top.state = TryState::Catching;
2828                    }
2829                    self.ip = catch_ip;
2830                    return Ok(true);
2831                }
2832                TryState::Catching => {
2833                    if let Some(fin_ip) = finally_ip {
2834                        if let Some(top) = self.try_stack.last_mut() {
2835                            top.state = TryState::Finalizing;
2836                            top.deferred_error = Some(e.clone());
2837                        }
2838                        self.ip = fin_ip;
2839                        return Ok(true);
2840                    }
2841                    self.try_stack.pop();
2842                }
2843                TryState::Finalizing => {
2844                    // Finally itself threw — drop deferred (if any) and keep propagating.
2845                    self.try_stack.pop();
2846                }
2847            }
2848        }
2849    }
2850
2851    /// Stash lookup only (qualified key from compiler); avoids `resolve_sub_by_name`'s package fallback on hot calls.
2852    #[inline]
2853    fn sub_for_closure_restore(&self, name: &str) -> Option<Arc<StrykeSub>> {
2854        self.interp.subs.get(name).cloned()
2855    }
2856
2857    /// AOP: run before-advice → original (or around) → after-advice for `name`.
2858    /// Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). Args are popped synchronously
2859    /// off the stack; the original is invoked via `Interpreter::call_sub` so the retval is
2860    /// available to after-advice and to the around block (via `proceed`).
2861    #[cold]
2862    fn dispatch_with_advice(
2863        &mut self,
2864        name: &str,
2865        closure_sub_hint: Option<Arc<StrykeSub>>,
2866        argc: usize,
2867        want: WantarrayCtx,
2868        preserve_arrays: bool,
2869    ) -> StrykeResult<()> {
2870        use crate::ast::AdviceKind;
2871
2872        let line = self.line();
2873
2874        let args = if preserve_arrays {
2875            self.pop_call_operands_preserved(argc)
2876        } else {
2877            self.pop_call_operands_flattened(argc)
2878        };
2879
2880        let sub_opt = closure_sub_hint.or_else(|| self.interp.resolve_sub_by_name(name));
2881
2882        let matching: Vec<crate::aop::Intercept> = self
2883            .interp
2884            .intercepts
2885            .iter()
2886            .filter(|i| crate::aop::glob_match(&i.pattern, name))
2887            .cloned()
2888            .collect();
2889
2890        // Context vars visible to advice bodies (mirrors zshrs INTERCEPT_NAME / INTERCEPT_ARGS).
2891        self.interp
2892            .scope
2893            .declare_scalar("INTERCEPT_NAME", StrykeValue::string(name.to_string()));
2894        self.interp
2895            .scope
2896            .declare_array("INTERCEPT_ARGS", args.clone());
2897
2898        self.interp.intercept_active_names.push(name.to_string());
2899
2900        // Run all matching `before` advices via the bytecode VM (`run_block_region`).
2901        // We never fall back to `interp.exec_block` here — the advice body must use the
2902        // same name resolution as the surrounding bytecode (see the source-level test
2903        // in `tests/tree_walker_absent_aop.rs`).
2904        for adv in matching
2905            .iter()
2906            .filter(|i| matches!(i.kind, AdviceKind::Before))
2907        {
2908            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2909                self.interp.intercept_active_names.pop();
2910                return Err(e);
2911            }
2912        }
2913
2914        let around = matching
2915            .iter()
2916            .find(|i| matches!(i.kind, AdviceKind::Around));
2917
2918        let t0 = std::time::Instant::now();
2919        let retval = if let Some(around) = around {
2920            self.interp
2921                .intercept_ctx_stack
2922                .push(crate::aop::InterceptCtx {
2923                    name: name.to_string(),
2924                    args: args.clone(),
2925                    proceeded: false,
2926                    retval: StrykeValue::UNDEF,
2927                });
2928            let exec_res = self.run_advice_body_bytecode(around, line);
2929            let _ctx = self.interp.intercept_ctx_stack.pop();
2930            // AspectJ-style: the around block's evaluated value is the call's return.
2931            // If the user wants to forward the original's value, they say `proceed()`
2932            // as the last expression; if they want to transform, `proceed() + 1`; if
2933            // they want to replace, just emit a value without calling proceed.
2934            match exec_res {
2935                Ok(v) => v,
2936                Err(e) => {
2937                    self.interp.intercept_active_names.pop();
2938                    return Err(e);
2939                }
2940            }
2941        } else if let Some(sub) = sub_opt {
2942            match self.interp.call_sub(&sub, args.clone(), want, line) {
2943                Ok(v) => v,
2944                Err(FlowOrError::Flow(Flow::Return(v))) => v,
2945                Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2946                Err(FlowOrError::Error(e)) => {
2947                    self.interp.intercept_active_names.pop();
2948                    return Err(e.at_line(line));
2949                }
2950            }
2951        } else {
2952            // Sub not resolvable — fall back to builtins (matches the non-advice fallback).
2953            let saved_wa_call = self.interp.wantarray_kind;
2954            self.interp.wantarray_kind = want;
2955            let r = crate::builtins::try_builtin(self.interp, name, &args, line);
2956            self.interp.wantarray_kind = saved_wa_call;
2957            match r {
2958                Some(Ok(v)) => v,
2959                Some(Err(e)) => {
2960                    self.interp.intercept_active_names.pop();
2961                    return Err(e.at_line(line));
2962                }
2963                None => {
2964                    self.interp.intercept_active_names.pop();
2965                    return Err(StrykeError::runtime(
2966                        format!("undefined sub `{}` (advice fallback)", name),
2967                        line,
2968                    ));
2969                }
2970            }
2971        };
2972        let elapsed = t0.elapsed();
2973
2974        // Timing context vars for after-advice (matches zshrs INTERCEPT_MS / INTERCEPT_US).
2975        self.interp.scope.declare_scalar(
2976            "INTERCEPT_MS",
2977            StrykeValue::float(elapsed.as_secs_f64() * 1000.0),
2978        );
2979        self.interp.scope.declare_scalar(
2980            "INTERCEPT_US",
2981            StrykeValue::integer(elapsed.as_micros() as i64),
2982        );
2983        self.interp
2984            .scope
2985            .declare_scalar("INTERCEPT_RESULT", retval.clone());
2986
2987        for adv in matching
2988            .iter()
2989            .filter(|i| matches!(i.kind, AdviceKind::After))
2990        {
2991            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2992                self.interp.intercept_active_names.pop();
2993                return Err(e);
2994            }
2995        }
2996
2997        self.interp.intercept_active_names.pop();
2998        self.push(retval);
2999        Ok(())
3000    }
3001
3002    /// Dispatch one advice body through the VM bytecode helper (`run_block_region`),
3003    /// the same path used by `map { }` / `grep { }` blocks. Always returns the body's
3004    /// final value on success. The body is required to have a lowered bytecode region
3005    /// (`Chunk::block_bytecode_ranges[idx]`) — the compiler's fourth pass populates
3006    /// this for every chunk block, so the only reason it would be missing is if the
3007    /// body contains a construct the lowering rejects (e.g. a literal `return`); in
3008    /// that case we error out loudly rather than silently fall back to the
3009    /// tree-walker. See `tests/tree_walker_absent_aop.rs`.
3010    #[inline]
3011    fn run_advice_body_bytecode(
3012        &mut self,
3013        adv: &crate::aop::Intercept,
3014        line: usize,
3015    ) -> StrykeResult<StrykeValue> {
3016        let idx = adv.body_block_idx as usize;
3017        let range = self
3018            .block_bytecode_ranges
3019            .get(idx)
3020            .copied()
3021            .flatten()
3022            .ok_or_else(|| {
3023                StrykeError::runtime(
3024                    format!(
3025                        "AOP {} advice body for `{}` could not be lowered to bytecode \
3026                         (likely contains a construct unsupported by block lowering, \
3027                         e.g. a literal `return`); rewrite the body without it",
3028                        match adv.kind {
3029                            crate::ast::AdviceKind::Before => "before",
3030                            crate::ast::AdviceKind::After => "after",
3031                            crate::ast::AdviceKind::Around => "around",
3032                        },
3033                        adv.pattern,
3034                    ),
3035                    line,
3036                )
3037            })?;
3038        let mut op_count: u64 = 0;
3039        self.run_block_region(range.0, range.1, &mut op_count)
3040    }
3041
3042    fn vm_dispatch_user_call(
3043        &mut self,
3044        name_idx: u16,
3045        entry_opt: Option<(usize, bool)>,
3046        argc_u8: u8,
3047        wa_byte: u8,
3048        // Pre-resolved sub for `Op::CallStaticSubId` (stash lookup once in `VM::new`).
3049        closure_sub_hint: Option<Arc<StrykeSub>>,
3050    ) -> StrykeResult<()> {
3051        let name_owned = self.names[name_idx as usize].clone();
3052        let name = name_owned.as_str();
3053        let argc = argc_u8 as usize;
3054        let want = WantarrayCtx::from_byte(wa_byte);
3055
3056        // AOP advice path: at least one matching intercept and no re-entrancy guard for `name`.
3057        // Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). The fast-path skip below is the
3058        // common case (no intercepts registered); when the registry is non-empty we still bail
3059        // out cheaply unless a glob actually matches.
3060        if !self.interp.intercepts.is_empty()
3061            && !self.interp.intercept_active_names.iter().any(|n| n == name)
3062            && self
3063                .interp
3064                .intercepts
3065                .iter()
3066                .any(|i| crate::aop::glob_match(&i.pattern, name))
3067        {
3068            let preserve = Self::call_preserve_operand_arrays(name);
3069            return self.dispatch_with_advice(&name_owned, closure_sub_hint, argc, want, preserve);
3070        }
3071
3072        if let Some((entry_ip, stack_args)) = entry_opt {
3073            let saved_wa = self.interp.wantarray_kind;
3074            let sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3075            if let Some(p) = &mut self.interp.profiler {
3076                p.enter_sub(name);
3077            }
3078            self.interp.debugger_enter_sub(name);
3079
3080            // Fib-shaped recursive-add fast path: if the target sub is tagged with a
3081            // `fib_like` pattern (detected at sub-registration time in the compiler and
3082            // cached in `static_sub_closure_subs`), skip frame setup entirely and
3083            // evaluate the closed-form-ish iterative version. `bench_fib` collapses from
3084            // ~2.7M recursive VM calls to a single `while` loop.
3085            let fib_sub: Option<Arc<StrykeSub>> = closure_sub_hint
3086                .clone()
3087                .or_else(|| self.sub_for_closure_restore(name));
3088            if let Some(ref sub_arc) = fib_sub {
3089                if let Some(pat) = sub_arc.fib_like.as_ref() {
3090                    // stack_args path pushes exactly `argc` ints; non-stack_args pops them
3091                    // off the stack into @_. Only the argc==1 / integer case qualifies.
3092                    if argc == 1 {
3093                        let top_idx = self.stack.len().saturating_sub(1);
3094                        if let Some(n0) = self.stack.get(top_idx).and_then(|v| v.as_integer()) {
3095                            let result = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
3096                            // Drop the arg, push the result, keep wantarray as the caller had it.
3097                            self.stack.truncate(top_idx);
3098                            self.push(StrykeValue::integer(result));
3099                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, sub_prof_t0) {
3100                                p.exit_sub(t0.elapsed());
3101                            }
3102                            self.interp.debugger_leave_sub();
3103                            self.interp.wantarray_kind = saved_wa;
3104                            return Ok(());
3105                        }
3106                    }
3107                }
3108            }
3109
3110            if stack_args {
3111                let eff_argc = if argc == 0 {
3112                    self.push(self.interp.scope.get_scalar("_").clone());
3113                    1
3114                } else {
3115                    argc
3116                };
3117                let stack_base = self.stack.len() - eff_argc;
3118                self.call_stack.push(CallFrame {
3119                    return_ip: self.ip,
3120                    stack_base,
3121                    scope_depth: self.interp.scope.depth(),
3122                    saved_wantarray: saved_wa,
3123                    jit_trampoline_return: false,
3124                    block_region: false,
3125                    sub_profiler_start: sub_prof_t0,
3126                });
3127                self.interp.wantarray_kind = want;
3128                self.interp.scope_push_hook();
3129                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
3130                if let Some(ref sub) = closure_sub {
3131                    if let Some(ref env) = sub.closure_env {
3132                        self.interp.scope.restore_capture(env);
3133                    }
3134                    self.interp.current_sub_stack.push(sub.clone());
3135                }
3136                self.ip = entry_ip;
3137            } else {
3138                let args = if Self::call_preserve_operand_arrays(name) {
3139                    self.pop_call_operands_preserved(argc)
3140                } else {
3141                    self.pop_call_operands_flattened(argc)
3142                };
3143                // Only substitute $_ when the call site has no syntactic arguments (argc == 0).
3144                // When argc > 0 but args is empty (e.g., passing an empty array), keep args empty.
3145                let args = if argc == 0 {
3146                    self.interp.with_topic_default_args(args)
3147                } else {
3148                    args
3149                };
3150                self.call_stack.push(CallFrame {
3151                    return_ip: self.ip,
3152                    stack_base: self.stack.len(),
3153                    scope_depth: self.interp.scope.depth(),
3154                    saved_wantarray: saved_wa,
3155                    jit_trampoline_return: false,
3156                    block_region: false,
3157                    sub_profiler_start: sub_prof_t0,
3158                });
3159                self.interp.wantarray_kind = want;
3160                self.interp.scope_push_hook();
3161                self.interp.scope.declare_array("_", args);
3162                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
3163                if let Some(ref sub) = closure_sub {
3164                    if let Some(ref env) = sub.closure_env {
3165                        self.interp.scope.restore_capture(env);
3166                    }
3167                    let line = self.line();
3168                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
3169                    self.interp
3170                        .apply_sub_signature(sub.as_ref(), &argv, line)
3171                        .map_err(|e| e.at_line(line))?;
3172                    self.interp.scope.declare_array("_", argv.clone());
3173                    self.interp.scope.set_closure_args(&argv);
3174                    self.interp.current_sub_stack.push(sub.clone());
3175                }
3176                self.ip = entry_ip;
3177            }
3178        } else {
3179            let args = if Self::call_preserve_operand_arrays(name) {
3180                self.pop_call_operands_preserved(argc)
3181            } else {
3182                self.pop_call_operands_flattened(argc)
3183            };
3184
3185            let saved_wa_call = self.interp.wantarray_kind;
3186            self.interp.wantarray_kind = want;
3187            // Bare callable spelling: builtins always win in default mode.
3188            // Skip the user-sub resolve below so `fn sum {}` declared in a
3189            // non-main package never shadows the global `sum` on a bare
3190            // call. `--compat` (Perl 5 mode) restores UDF-wins semantics.
3191            let is_bare_builtin = !crate::compat_mode()
3192                && !name.contains("::")
3193                && crate::builtins::is_callable_spelling(name);
3194            if let Some(r) = crate::builtins::try_builtin(self.interp, name, &args, self.line()) {
3195                self.interp.wantarray_kind = saved_wa_call;
3196                self.push(r?);
3197            } else {
3198                self.interp.wantarray_kind = saved_wa_call;
3199                let maybe_sub = if is_bare_builtin {
3200                    None
3201                } else {
3202                    self.interp.resolve_sub_by_name(name)
3203                };
3204                if let Some(sub) = maybe_sub {
3205                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3206                    if let Some(p) = &mut self.interp.profiler {
3207                        p.enter_sub(name);
3208                    }
3209                    self.interp.debugger_enter_sub(name);
3210                    // Only substitute $_ when argc == 0; passing an empty array keeps args empty.
3211                    let args = if argc == 0 {
3212                        self.interp.with_topic_default_args(args)
3213                    } else {
3214                        args
3215                    };
3216                    let saved_wa = self.interp.wantarray_kind;
3217                    self.interp.wantarray_kind = want;
3218                    self.interp.scope_push_hook();
3219                    self.interp.scope.declare_array("_", args);
3220                    if let Some(ref env) = sub.closure_env {
3221                        self.interp.scope.restore_capture(env);
3222                    }
3223                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
3224                    let line = self.line();
3225                    self.interp
3226                        .apply_sub_signature(&sub, &argv, line)
3227                        .map_err(|e| e.at_line(line))?;
3228                    let result = {
3229                        self.interp.scope.declare_array("_", argv.clone());
3230                        self.interp.scope.set_closure_args(&argv);
3231                        self.interp
3232                            .exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List)
3233                    };
3234                    self.interp.wantarray_kind = saved_wa;
3235                    self.interp.scope_pop_hook();
3236                    match result {
3237                        Ok(v) => self.push(v),
3238                        Err(crate::vm_helper::FlowOrError::Flow(
3239                            crate::vm_helper::Flow::Return(v),
3240                        )) => self.push(v),
3241                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3242                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3243                                p.exit_sub(t0.elapsed());
3244                            }
3245                            self.interp.debugger_leave_sub();
3246                            return Err(e);
3247                        }
3248                        Err(_) => self.push(StrykeValue::UNDEF),
3249                    }
3250                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3251                        p.exit_sub(t0.elapsed());
3252                    }
3253                    self.interp.debugger_leave_sub();
3254                } else if !name.contains("::")
3255                    && matches!(
3256                        name,
3257                        "uniq"
3258                            | "distinct"
3259                            | "uniqstr"
3260                            | "uniqint"
3261                            | "uniqnum"
3262                            | "shuffle"
3263                            | "sample"
3264                            | "chunked"
3265                            | "windowed"
3266                            | "zip"
3267                            | "zip_shortest"
3268                            | "zip_longest"
3269                            | "mesh"
3270                            | "mesh_shortest"
3271                            | "mesh_longest"
3272                            | "any"
3273                            | "all"
3274                            | "none"
3275                            | "notall"
3276                            | "first"
3277                            | "find_index"
3278                            | "firstidx"
3279                            | "first_index"
3280                            | "reduce"
3281                            | "reductions"
3282                            | "sum"
3283                            | "sum0"
3284                            | "product"
3285                            | "min"
3286                            | "max"
3287                            | "minstr"
3288                            | "maxstr"
3289                            | "mean"
3290                            | "median"
3291                            | "mode"
3292                            | "stddev"
3293                            | "variance"
3294                            | "pairs"
3295                            | "unpairs"
3296                            | "pairkeys"
3297                            | "pairvalues"
3298                            | "pairgrep"
3299                            | "pairmap"
3300                            | "pairfirst"
3301                            // Scalar/Sub/utf8-utility bare builtins (no module — direct names)
3302                            | "blessed"
3303                            | "refaddr"
3304                            | "reftype"
3305                            | "looks_like_number"
3306                            | "weaken"
3307                            | "unweaken"
3308                            | "isweak"
3309                            | "set_subname"
3310                            | "subname"
3311                            | "unicode_to_native"
3312                    )
3313                {
3314                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3315                    if let Some(p) = &mut self.interp.profiler {
3316                        p.enter_sub(name);
3317                    }
3318                    self.interp.debugger_enter_sub(name);
3319                    let saved_wa = self.interp.wantarray_kind;
3320                    self.interp.wantarray_kind = want;
3321                    let out = self
3322                        .interp
3323                        .call_bare_list_builtin(name, args, self.line(), want);
3324                    self.interp.wantarray_kind = saved_wa;
3325                    match out {
3326                        Ok(v) => self.push(v),
3327                        Err(crate::vm_helper::FlowOrError::Flow(
3328                            crate::vm_helper::Flow::Return(v),
3329                        )) => self.push(v),
3330                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3331                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3332                                p.exit_sub(t0.elapsed());
3333                            }
3334                            self.interp.debugger_leave_sub();
3335                            return Err(e);
3336                        }
3337                        Err(_) => self.push(StrykeValue::UNDEF),
3338                    }
3339                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3340                        p.exit_sub(t0.elapsed());
3341                    }
3342                    self.interp.debugger_leave_sub();
3343                } else if let Some(result) = self.interp.try_autoload_call(
3344                    name,
3345                    if argc == 0 {
3346                        self.interp.with_topic_default_args(args.clone())
3347                    } else {
3348                        args.clone()
3349                    },
3350                    self.line(),
3351                    want,
3352                    None,
3353                ) {
3354                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3355                    if let Some(p) = &mut self.interp.profiler {
3356                        p.enter_sub(name);
3357                    }
3358                    self.interp.debugger_enter_sub(name);
3359                    match result {
3360                        Ok(v) => self.push(v),
3361                        Err(crate::vm_helper::FlowOrError::Flow(
3362                            crate::vm_helper::Flow::Return(v),
3363                        )) => self.push(v),
3364                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3365                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3366                                p.exit_sub(t0.elapsed());
3367                            }
3368                            self.interp.debugger_leave_sub();
3369                            return Err(e);
3370                        }
3371                        Err(_) => self.push(StrykeValue::UNDEF),
3372                    }
3373                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
3374                        p.exit_sub(t0.elapsed());
3375                    }
3376                    self.interp.debugger_leave_sub();
3377                } else if let Some(def) = self.interp.struct_defs.get(name).cloned() {
3378                    // Struct constructor: Point(x => 1, y => 2) or Point(1, 2)
3379                    let result = self.interp.struct_construct(&def, args, self.line());
3380                    match result {
3381                        Ok(v) => self.push(v),
3382                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
3383                        _ => self.push(StrykeValue::UNDEF),
3384                    }
3385                } else if let Some(def) = self.interp.class_defs.get(name).cloned() {
3386                    // Class constructor: Dog(name => "Rex") or Dog("Rex", 5)
3387                    let result = self.interp.class_construct(&def, args, self.line());
3388                    match result {
3389                        Ok(v) => self.push(v),
3390                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
3391                        _ => self.push(StrykeValue::UNDEF),
3392                    }
3393                } else if let Some((prefix, suffix)) = name.rsplit_once("::") {
3394                    // Enum variant constructor: Color::Red or Maybe::Some(value)
3395                    if let Some(def) = self.interp.enum_defs.get(prefix).cloned() {
3396                        let result = self.interp.enum_construct(&def, suffix, args, self.line());
3397                        match result {
3398                            Ok(v) => self.push(v),
3399                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
3400                            _ => self.push(StrykeValue::UNDEF),
3401                        }
3402                    // Static class method: Math::add(...)
3403                    } else if let Some(def) = self.interp.class_defs.get(prefix).cloned() {
3404                        if let Some(m) = def.method(suffix) {
3405                            if m.is_static {
3406                                if let Some(ref body) = m.body {
3407                                    let params = m.params.clone();
3408                                    match self.interp.call_static_class_method(
3409                                        body,
3410                                        &params,
3411                                        args.clone(),
3412                                        self.line(),
3413                                    ) {
3414                                        Ok(v) => self.push(v),
3415                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
3416                                            return Err(e)
3417                                        }
3418                                        Err(crate::vm_helper::FlowOrError::Flow(
3419                                            crate::vm_helper::Flow::Return(v),
3420                                        )) => self.push(v),
3421                                        _ => self.push(StrykeValue::UNDEF),
3422                                    }
3423                                } else {
3424                                    self.push(StrykeValue::UNDEF);
3425                                }
3426                            } else {
3427                                return Err(StrykeError::runtime(
3428                                    format!("method `{}` is not static", suffix),
3429                                    self.line(),
3430                                ));
3431                            }
3432                        } else if def.static_fields.iter().any(|sf| sf.name == suffix) {
3433                            // Static field access: getter (0 args) or setter (1 arg)
3434                            let key = format!("{}::{}", prefix, suffix);
3435                            match args.len() {
3436                                0 => {
3437                                    let val = self.interp.scope.get_scalar(&key);
3438                                    self.push(val);
3439                                }
3440                                1 => {
3441                                    let _ = self.interp.scope.set_scalar(&key, args[0].clone());
3442                                    self.push(args[0].clone());
3443                                }
3444                                _ => {
3445                                    return Err(StrykeError::runtime(
3446                                        format!(
3447                                            "static field `{}::{}` takes 0 or 1 arguments",
3448                                            prefix, suffix
3449                                        ),
3450                                        self.line(),
3451                                    ));
3452                                }
3453                            }
3454                        } else {
3455                            return Err(StrykeError::runtime(
3456                                self.interp.undefined_subroutine_call_message(name),
3457                                self.line(),
3458                            ));
3459                        }
3460                    } else {
3461                        return Err(StrykeError::runtime(
3462                            self.interp.undefined_subroutine_call_message(name),
3463                            self.line(),
3464                        ));
3465                    }
3466                } else {
3467                    return Err(StrykeError::runtime(
3468                        self.interp.undefined_subroutine_call_message(name),
3469                        self.line(),
3470                    ));
3471                }
3472            }
3473        }
3474        Ok(())
3475    }
3476
3477    #[inline]
3478    fn push_binop_with_overload<F>(
3479        &mut self,
3480        op: BinOp,
3481        a: StrykeValue,
3482        b: StrykeValue,
3483        default: F,
3484    ) -> StrykeResult<()>
3485    where
3486        F: FnOnce(&StrykeValue, &StrykeValue) -> StrykeResult<StrykeValue>,
3487    {
3488        let line = self.line();
3489        if let Some(exec_res) = self.interp.try_overload_binop(op, &a, &b, line) {
3490            self.push(vm_interp_result(exec_res, line)?);
3491        } else {
3492            self.push(default(&a, &b)?);
3493        }
3494        Ok(())
3495    }
3496
3497    pub(crate) fn concat_stack_values(
3498        &mut self,
3499        a: StrykeValue,
3500        b: StrykeValue,
3501    ) -> StrykeResult<StrykeValue> {
3502        let line = self.line();
3503        if let Some(exec_res) = self.interp.try_overload_binop(BinOp::Concat, &a, &b, line) {
3504            vm_interp_result(exec_res, line)
3505        } else {
3506            let sa = match self.interp.stringify_value(a, line) {
3507                Ok(s) => s,
3508                Err(FlowOrError::Error(e)) => return Err(e),
3509                Err(FlowOrError::Flow(_)) => {
3510                    return Err(StrykeError::runtime(
3511                        "concat: unexpected control flow",
3512                        line,
3513                    ));
3514                }
3515            };
3516            let sb = match self.interp.stringify_value(b, line) {
3517                Ok(s) => s,
3518                Err(FlowOrError::Error(e)) => return Err(e),
3519                Err(FlowOrError::Flow(_)) => {
3520                    return Err(StrykeError::runtime(
3521                        "concat: unexpected control flow",
3522                        line,
3523                    ));
3524                }
3525            };
3526            let mut s = sa;
3527            s.push_str(&sb);
3528            Ok(StrykeValue::string(s))
3529        }
3530    }
3531
3532    fn run_main_dispatch_loop(
3533        &mut self,
3534        mut last: StrykeValue,
3535        op_count: &mut u64,
3536        init_dispatch: bool,
3537    ) -> StrykeResult<StrykeValue> {
3538        if init_dispatch {
3539            self.halt = false;
3540            self.exit_main_dispatch = false;
3541            self.exit_main_dispatch_value = None;
3542        }
3543        let ops_ref: &Vec<Op> = &self.ops;
3544        let ops = ops_ref as *const Vec<Op>;
3545        let ops = unsafe { &*ops };
3546        let names_ref: &Vec<String> = &self.names;
3547        let names = names_ref as *const Vec<String>;
3548        let names = unsafe { &*names };
3549        let constants_ref: &Vec<StrykeValue> = &self.constants;
3550        let constants = constants_ref as *const Vec<StrykeValue>;
3551        let constants = unsafe { &*constants };
3552        let len = ops.len();
3553        const MAX_OPS: u64 = 1_000_000_000;
3554        loop {
3555            if self.jit_trampoline_depth > 0 && self.jit_trampoline_out.is_some() {
3556                break;
3557            }
3558            if self.block_region_return.is_some() {
3559                break;
3560            }
3561            if self.block_region_mode && self.ip >= self.block_region_end {
3562                return Err(StrykeError::runtime(
3563                    "block bytecode region fell through without BlockReturnValue",
3564                    self.line(),
3565                ));
3566            }
3567            if self.ip >= len {
3568                break;
3569            }
3570
3571            if !self.block_region_mode
3572                && self.jit_enabled
3573                && self.sub_entry_at_ip.get(self.ip).copied().unwrap_or(false)
3574            {
3575                let sub_ip = self.ip;
3576                if sub_ip >= self.sub_entry_invoke_count.len() {
3577                    self.sub_entry_invoke_count.resize(sub_ip + 1, 0);
3578                }
3579                let c = &mut self.sub_entry_invoke_count[sub_ip];
3580                if *c <= self.jit_sub_invoke_threshold {
3581                    *c = c.saturating_add(1);
3582                }
3583                let should_try_jit = *c > self.jit_sub_invoke_threshold
3584                    && (!self.sub_jit_skip_linear_test(sub_ip)
3585                        || !self.sub_jit_skip_block_test(sub_ip));
3586                if should_try_jit {
3587                    // Tier 0: shared fusevm runtime. Falls through to strykelang's
3588                    // own JIT below when the segment isn't in the universal-integer
3589                    // subset fusevm handles.
3590                    if self.try_fusevm_subroutine()? {
3591                        continue;
3592                    }
3593                    if !self.sub_jit_skip_linear_test(sub_ip) && self.try_jit_subroutine_linear()? {
3594                        continue;
3595                    }
3596                    if !self.sub_jit_skip_block_test(sub_ip) && self.try_jit_subroutine_block()? {
3597                        continue;
3598                    }
3599                }
3600            }
3601
3602            *op_count += 1;
3603            // `%SIG` delivery and the execution cap: same cadence as the old per-op poll (signals
3604            // remain responsive; hot loops avoid a syscall/atomic path every opcode).
3605            if (*op_count & 0x3FF) == 0 {
3606                crate::perl_signal::poll(self.interp)?;
3607                if *op_count > MAX_OPS {
3608                    return Err(StrykeError::runtime(
3609                        "VM execution limit exceeded (possible infinite loop)",
3610                        self.line(),
3611                    ));
3612                }
3613            }
3614
3615            let ip_before = self.ip;
3616            let line = self.lines.get(ip_before).copied().unwrap_or(0);
3617            let op = &ops[self.ip];
3618            self.ip += 1;
3619
3620            // Debugger hook: check if we should stop at this line
3621            if let Some(ref mut dbg) = self.interp.debugger {
3622                if dbg.should_stop(line) {
3623                    let call_stack = self.interp.debug_call_stack.clone();
3624                    match dbg.prompt(line, &self.interp.scope, &call_stack) {
3625                        crate::debugger::DebugAction::Quit => {
3626                            return Err(StrykeError::runtime("debugger: quit", line));
3627                        }
3628                        crate::debugger::DebugAction::Continue => {}
3629                        crate::debugger::DebugAction::Prompt => {}
3630                    }
3631                }
3632            }
3633
3634            let op_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
3635            // Closure: `?` / `return Err` inside `match op` must not return from
3636            // `run_main_dispatch_loop` — they must become `__op_res` so `try_recover_from_exception`
3637            // can run before propagating.
3638            let __op_res: StrykeResult<()> = (|| -> StrykeResult<()> {
3639                match op {
3640                    Op::Nop => Ok(()),
3641                    // ── Constants ──
3642                    Op::LoadInt(n) => {
3643                        self.push(StrykeValue::integer(*n));
3644                        Ok(())
3645                    }
3646                    Op::LoadFloat(f) => {
3647                        self.push(StrykeValue::float(*f));
3648                        Ok(())
3649                    }
3650                    Op::LoadConst(idx) => {
3651                        self.push(self.constant(*idx).clone());
3652                        Ok(())
3653                    }
3654                    Op::LoadUndef => {
3655                        self.push(StrykeValue::UNDEF);
3656                        Ok(())
3657                    }
3658                    Op::RuntimeErrorConst(idx) => {
3659                        let msg = self.constant(*idx).to_string();
3660                        let line = self.line();
3661                        Err(crate::error::StrykeError::runtime(msg, line))
3662                    }
3663                    Op::BarewordRvalue(name_idx) => {
3664                        let name = names[*name_idx as usize].clone();
3665                        let line = self.line();
3666                        let out = vm_interp_result(
3667                            self.interp.resolve_bareword_rvalue(
3668                                &name,
3669                                crate::vm_helper::WantarrayCtx::Scalar,
3670                                line,
3671                            ),
3672                            line,
3673                        )?;
3674                        self.push(out);
3675                        Ok(())
3676                    }
3677
3678                    // ── Stack ──
3679                    Op::Pop => {
3680                        let v = self.pop();
3681                        // Drain iterators used as void statements so side effects fire.
3682                        if v.is_iterator() {
3683                            let iter = v.into_iterator();
3684                            while iter.next_item().is_some() {}
3685                        }
3686                        Ok(())
3687                    }
3688                    Op::Dup => {
3689                        let v = self.peek().dup_stack();
3690                        self.push(v);
3691                        Ok(())
3692                    }
3693                    Op::Dup2 => {
3694                        let b = self.pop();
3695                        let a = self.pop();
3696                        self.push(a.dup_stack());
3697                        self.push(b.dup_stack());
3698                        self.push(a);
3699                        self.push(b);
3700                        Ok(())
3701                    }
3702                    Op::Swap => {
3703                        let top = self.pop();
3704                        let below = self.pop();
3705                        self.push(top);
3706                        self.push(below);
3707                        Ok(())
3708                    }
3709                    Op::Rot => {
3710                        let c = self.pop();
3711                        let b = self.pop();
3712                        let a = self.pop();
3713                        self.push(b);
3714                        self.push(c);
3715                        self.push(a);
3716                        Ok(())
3717                    }
3718                    Op::ValueScalarContext => {
3719                        let v = self.pop();
3720                        self.push(v.scalar_context());
3721                        Ok(())
3722                    }
3723                    Op::ListFirst => {
3724                        let v = self.pop();
3725                        let first = if let Some(arr) = v.as_array_vec() {
3726                            arr.first().cloned().unwrap_or(StrykeValue::UNDEF)
3727                        } else {
3728                            v
3729                        };
3730                        self.push(first);
3731                        Ok(())
3732                    }
3733
3734                    // ── Scalars ──
3735                    Op::GetScalar(idx) => {
3736                        let n = names[*idx as usize].as_str();
3737                        let val = self.interp.get_special_var(n);
3738                        self.push(val);
3739                        Ok(())
3740                    }
3741                    Op::GetScalarPlain(idx) => {
3742                        let n = names[*idx as usize].as_str();
3743                        let val = self.interp.scope.get_scalar(n);
3744                        self.push(val);
3745                        Ok(())
3746                    }
3747                    Op::SetScalar(idx) => {
3748                        let val = self.pop();
3749                        let n = names[*idx as usize].as_str();
3750                        self.require_scalar_mutable(n)?;
3751                        self.interp.maybe_invalidate_regex_capture_memo(n);
3752                        self.interp
3753                            .set_special_var(n, &val)
3754                            .map_err(|e| e.at_line(self.line()))?;
3755                        Ok(())
3756                    }
3757                    Op::SetScalarPlain(idx) => {
3758                        let val = self.pop();
3759                        let n = names[*idx as usize].as_str();
3760                        self.require_scalar_mutable(n)?;
3761                        self.interp.maybe_invalidate_regex_capture_memo(n);
3762                        self.interp
3763                            .scope
3764                            .set_scalar(n, val)
3765                            .map_err(|e| e.at_line(self.line()))?;
3766                        Ok(())
3767                    }
3768                    Op::SetScalarKeep(idx) => {
3769                        let val = self.peek().dup_stack();
3770                        let n = names[*idx as usize].as_str();
3771                        self.require_scalar_mutable(n)?;
3772                        self.interp.maybe_invalidate_regex_capture_memo(n);
3773                        self.interp
3774                            .set_special_var(n, &val)
3775                            .map_err(|e| e.at_line(self.line()))?;
3776                        Ok(())
3777                    }
3778                    Op::SetScalarKeepPlain(idx) => {
3779                        let val = self.peek().dup_stack();
3780                        let n = names[*idx as usize].as_str();
3781                        self.require_scalar_mutable(n)?;
3782                        self.interp.maybe_invalidate_regex_capture_memo(n);
3783                        self.interp
3784                            .scope
3785                            .set_scalar(n, val)
3786                            .map_err(|e| e.at_line(self.line()))?;
3787                        Ok(())
3788                    }
3789                    Op::DeclareScalar(idx) => {
3790                        let val = self.pop();
3791                        let n = names[*idx as usize].as_str();
3792                        self.interp
3793                            .scope
3794                            .declare_scalar_frozen(n, val, false, None)
3795                            .map_err(|e| e.at_line(self.line()))?;
3796                        Ok(())
3797                    }
3798                    Op::DeclareScalarFrozen(idx) => {
3799                        let val = self.pop();
3800                        let n = names[*idx as usize].as_str();
3801                        self.interp
3802                            .scope
3803                            .declare_scalar_frozen(n, val, true, None)
3804                            .map_err(|e| e.at_line(self.line()))?;
3805                        Ok(())
3806                    }
3807                    Op::DeclareScalarTyped(idx, tyb) => {
3808                        let val = self.pop();
3809                        let n = names[*idx as usize].as_str();
3810                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
3811                            StrykeError::runtime(
3812                                format!("invalid typed scalar type byte {}", tyb),
3813                                self.line(),
3814                            )
3815                        })?;
3816                        self.interp
3817                            .scope
3818                            .declare_scalar_frozen(n, val, false, Some(ty))
3819                            .map_err(|e| e.at_line(self.line()))?;
3820                        Ok(())
3821                    }
3822                    Op::DeclareScalarTypedFrozen(idx, tyb) => {
3823                        let val = self.pop();
3824                        let n = names[*idx as usize].as_str();
3825                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
3826                            StrykeError::runtime(
3827                                format!("invalid typed scalar type byte {}", tyb),
3828                                self.line(),
3829                            )
3830                        })?;
3831                        self.interp
3832                            .scope
3833                            .declare_scalar_frozen(n, val, true, Some(ty))
3834                            .map_err(|e| e.at_line(self.line()))?;
3835                        Ok(())
3836                    }
3837                    Op::DeclareScalarTypedUser(name_idx, type_idx, flag) => {
3838                        let val = self.pop();
3839                        let n = names[*name_idx as usize].as_str();
3840                        let type_name = names[*type_idx as usize].clone();
3841                        let is_enum = (flag & 0b01) != 0;
3842                        let is_frozen = (flag & 0b10) != 0;
3843                        let ty = if is_enum {
3844                            PerlTypeName::Enum(type_name)
3845                        } else {
3846                            // Struct variant covers struct, class, and any
3847                            // user-defined nominal type — `check_value` for
3848                            // `Struct(name)` already accepts class instances
3849                            // via `c.isa(name)`.
3850                            PerlTypeName::Struct(type_name)
3851                        };
3852                        self.interp
3853                            .scope
3854                            .declare_scalar_frozen(n, val, is_frozen, Some(ty))
3855                            .map_err(|e| e.at_line(self.line()))?;
3856                        Ok(())
3857                    }
3858
3859                    // ── State variables (persist across calls) ──
3860                    Op::DeclareStateScalar(idx) => {
3861                        let init_val = self.pop();
3862                        let n = names[*idx as usize].as_str();
3863                        // Key by source line + name (matches interpreter's state_key format)
3864                        let state_key = format!("{}:{}", self.line(), n);
3865                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3866                            prev.clone()
3867                        } else {
3868                            self.interp
3869                                .state_vars
3870                                .insert(state_key.clone(), init_val.clone());
3871                            init_val
3872                        };
3873                        self.interp
3874                            .scope
3875                            .declare_scalar_frozen(n, val, false, None)
3876                            .map_err(|e| e.at_line(self.line()))?;
3877                        // Register for save-back when scope pops
3878                        if let Some(frame) = self.interp.state_bindings_stack.last_mut() {
3879                            frame.push((n.to_string(), state_key));
3880                        }
3881                        Ok(())
3882                    }
3883                    Op::DeclareStateArray(idx) => {
3884                        let init_val = self.pop();
3885                        let n = names[*idx as usize].as_str();
3886                        let state_key = format!("{}:{}", self.line(), n);
3887                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3888                            prev.clone()
3889                        } else {
3890                            self.interp
3891                                .state_vars
3892                                .insert(state_key.clone(), init_val.clone());
3893                            init_val
3894                        };
3895                        self.interp.scope.declare_array(n, val.to_list());
3896                        Ok(())
3897                    }
3898                    Op::DeclareStateHash(idx) => {
3899                        let init_val = self.pop();
3900                        let n = names[*idx as usize].as_str();
3901                        let state_key = format!("{}:{}", self.line(), n);
3902                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3903                            prev.clone()
3904                        } else {
3905                            self.interp
3906                                .state_vars
3907                                .insert(state_key.clone(), init_val.clone());
3908                            init_val
3909                        };
3910                        let items = val.to_list();
3911                        let mut map = IndexMap::new();
3912                        let mut i = 0;
3913                        while i + 1 < items.len() {
3914                            map.insert(items[i].to_string(), items[i + 1].clone());
3915                            i += 2;
3916                        }
3917                        self.interp.scope.declare_hash(n, map);
3918                        Ok(())
3919                    }
3920
3921                    // ── Arrays ──
3922                    Op::GetArray(idx) => {
3923                        let n = names[*idx as usize].as_str();
3924                        let arr = self.interp.scope.get_array(n);
3925                        self.push(StrykeValue::array(arr));
3926                        Ok(())
3927                    }
3928                    Op::SetArray(idx) => {
3929                        let val = self.pop();
3930                        let n = names[*idx as usize].as_str();
3931                        self.require_array_mutable(n)?;
3932                        self.interp
3933                            .scope
3934                            .set_array(n, val.to_list())
3935                            .map_err(|e| e.at_line(self.line()))?;
3936                        Ok(())
3937                    }
3938                    Op::DeclareArray(idx) => {
3939                        let val = self.pop();
3940                        let n = names[*idx as usize].as_str();
3941                        self.interp.scope.declare_array(n, val.to_list());
3942                        Ok(())
3943                    }
3944                    Op::DeclareArrayFrozen(idx) => {
3945                        let val = self.pop();
3946                        let n = names[*idx as usize].as_str();
3947                        self.interp
3948                            .scope
3949                            .declare_array_frozen(n, val.to_list(), true);
3950                        Ok(())
3951                    }
3952                    Op::GetArrayElem(idx) => {
3953                        let index = self.pop().to_int();
3954                        let n = names[*idx as usize].as_str();
3955                        // Stryke string-index sugar: bareword `_[N]` parses
3956                        // to a `__topicstr__N` synthetic name. Index the
3957                        // scalar (`$_` / `$_N`) by char.
3958                        if let Some(real) = n.strip_prefix("__topicstr__") {
3959                            let s = self.interp.scope.get_scalar(real).to_string();
3960                            let cnt = s.chars().count() as i64;
3961                            let i = if index < 0 { index + cnt } else { index };
3962                            let v = if i >= 0 && i < cnt {
3963                                s.chars()
3964                                    .nth(i as usize)
3965                                    .map(|c| StrykeValue::string(c.to_string()))
3966                                    .unwrap_or(StrykeValue::UNDEF)
3967                            } else {
3968                                StrykeValue::UNDEF
3969                            };
3970                            self.push(v);
3971                            return Ok(());
3972                        }
3973                        // Stryke (non-compat) sugar: `$s[i]` indexes by
3974                        // Unicode char when `@s` is missing or empty but
3975                        // `$s` is a non-empty string. NB: `$_[0]` keeps
3976                        // Perl's `@_`-access semantics because `@_` is
3977                        // populated inside any sub; the bareword `_[0]`
3978                        // parses to the same AST so it behaves identically.
3979                        // Use `substr(_, 0, 1)` for char-of-topic inside
3980                        // a sub. Compat mode = Perl semantics.
3981                        if !crate::compat_mode() && self.interp.scope.scalar_binding_exists(n) {
3982                            let prefer_scalar = self.interp.scope.get_array(n).is_empty();
3983                            if prefer_scalar {
3984                                let s = self.interp.scope.get_scalar(n).to_string();
3985                                if !s.is_empty() {
3986                                    let cnt = s.chars().count() as i64;
3987                                    let i = if index < 0 { index + cnt } else { index };
3988                                    let v = if i >= 0 && i < cnt {
3989                                        s.chars()
3990                                            .nth(i as usize)
3991                                            .map(|c| StrykeValue::string(c.to_string()))
3992                                            .unwrap_or(StrykeValue::UNDEF)
3993                                    } else {
3994                                        StrykeValue::UNDEF
3995                                    };
3996                                    self.push(v);
3997                                    return Ok(());
3998                                }
3999                            }
4000                        }
4001                        let val = self.interp.scope.get_array_element(n, index);
4002                        self.push(val);
4003                        Ok(())
4004                    }
4005                    Op::ExistsArrayElem(idx) => {
4006                        let index = self.pop().to_int();
4007                        let n = names[*idx as usize].as_str();
4008                        let yes = self.interp.scope.exists_array_element(n, index);
4009                        self.push(StrykeValue::integer(if yes { 1 } else { 0 }));
4010                        Ok(())
4011                    }
4012                    Op::DeleteArrayElem(idx) => {
4013                        let index = self.pop().to_int();
4014                        let n = names[*idx as usize].as_str();
4015                        self.require_array_mutable(n)?;
4016                        let v = self
4017                            .interp
4018                            .scope
4019                            .delete_array_element(n, index)
4020                            .map_err(|e| e.at_line(self.line()))?;
4021                        self.push(v);
4022                        Ok(())
4023                    }
4024                    Op::SetArrayElem(idx) => {
4025                        let index = self.pop().to_int();
4026                        let val = self.pop();
4027                        let n = names[*idx as usize].as_str();
4028                        self.require_array_mutable(n)?;
4029                        self.interp
4030                            .scope
4031                            .set_array_element(n, index, val)
4032                            .map_err(|e| e.at_line(self.line()))?;
4033                        Ok(())
4034                    }
4035                    Op::SetArrayElemKeep(idx) => {
4036                        let index = self.pop().to_int();
4037                        let val = self.pop();
4038                        let val_keep = val.clone();
4039                        let n = names[*idx as usize].as_str();
4040                        self.require_array_mutable(n)?;
4041                        let line = self.line();
4042                        self.interp
4043                            .scope
4044                            .set_array_element(n, index, val)
4045                            .map_err(|e| e.at_line(line))?;
4046                        self.push(val_keep);
4047                        Ok(())
4048                    }
4049                    Op::PushArray(idx) => {
4050                        let val = self.pop();
4051                        let n = names[*idx as usize].as_str();
4052                        self.require_array_mutable(n)?;
4053                        let line = self.line();
4054                        if let Some(items) = val.as_array_vec() {
4055                            for item in items {
4056                                self.interp
4057                                    .scope
4058                                    .push_to_array(n, item)
4059                                    .map_err(|e| e.at_line(line))?;
4060                            }
4061                        } else {
4062                            self.interp
4063                                .scope
4064                                .push_to_array(n, val)
4065                                .map_err(|e| e.at_line(line))?;
4066                        }
4067                        Ok(())
4068                    }
4069                    Op::PopArray(idx) => {
4070                        let n = names[*idx as usize].as_str();
4071                        self.require_array_mutable(n)?;
4072                        let line = self.line();
4073                        let val = self
4074                            .interp
4075                            .scope
4076                            .pop_from_array(n)
4077                            .map_err(|e| e.at_line(line))?;
4078                        self.push(val);
4079                        Ok(())
4080                    }
4081                    Op::ShiftArray(idx) => {
4082                        let n = names[*idx as usize].as_str();
4083                        self.require_array_mutable(n)?;
4084                        let line = self.line();
4085                        let val = self
4086                            .interp
4087                            .scope
4088                            .shift_from_array(n)
4089                            .map_err(|e| e.at_line(line))?;
4090                        self.push(val);
4091                        Ok(())
4092                    }
4093                    Op::PushArrayDeref => {
4094                        let val = self.pop();
4095                        let r = self.pop();
4096                        let line = self.line();
4097                        vm_interp_result(
4098                            self.interp
4099                                .push_array_deref_value(r.clone(), val, line)
4100                                .map(|_| StrykeValue::UNDEF),
4101                            line,
4102                        )?;
4103                        self.push(r);
4104                        Ok(())
4105                    }
4106                    Op::ArrayDerefLen => {
4107                        let r = self.pop();
4108                        let line = self.line();
4109                        let n = match self.interp.array_deref_len(r, line) {
4110                            Ok(n) => n,
4111                            Err(FlowOrError::Error(e)) => return Err(e),
4112                            Err(FlowOrError::Flow(_)) => {
4113                                return Err(StrykeError::runtime(
4114                                    "unexpected flow in tree-assisted opcode",
4115                                    line,
4116                                ));
4117                            }
4118                        };
4119                        self.push(StrykeValue::integer(n));
4120                        Ok(())
4121                    }
4122                    Op::PopArrayDeref => {
4123                        let r = self.pop();
4124                        let line = self.line();
4125                        let v = vm_interp_result(self.interp.pop_array_deref(r, line), line)?;
4126                        self.push(v);
4127                        Ok(())
4128                    }
4129                    Op::ShiftArrayDeref => {
4130                        let r = self.pop();
4131                        let line = self.line();
4132                        let v = vm_interp_result(self.interp.shift_array_deref(r, line), line)?;
4133                        self.push(v);
4134                        Ok(())
4135                    }
4136                    Op::UnshiftArrayDeref(n_extra) => {
4137                        let n = *n_extra as usize;
4138                        let mut vals: Vec<StrykeValue> = Vec::with_capacity(n);
4139                        for _ in 0..n {
4140                            vals.push(self.pop());
4141                        }
4142                        vals.reverse();
4143                        let r = self.pop();
4144                        let line = self.line();
4145                        let len = match self.interp.unshift_array_deref_multi(r, vals, line) {
4146                            Ok(n) => n,
4147                            Err(FlowOrError::Error(e)) => return Err(e),
4148                            Err(FlowOrError::Flow(_)) => {
4149                                return Err(StrykeError::runtime(
4150                                    "unexpected flow in tree-assisted opcode",
4151                                    line,
4152                                ));
4153                            }
4154                        };
4155                        self.push(StrykeValue::integer(len));
4156                        Ok(())
4157                    }
4158                    Op::SpliceArrayDeref(n_rep) => {
4159                        let n = *n_rep as usize;
4160                        let mut rep_vals: Vec<StrykeValue> = Vec::with_capacity(n);
4161                        for _ in 0..n {
4162                            rep_vals.push(self.pop());
4163                        }
4164                        rep_vals.reverse();
4165                        let length_val = self.pop();
4166                        let offset_val = self.pop();
4167                        let aref = self.pop();
4168                        let line = self.line();
4169                        let v = vm_interp_result(
4170                            self.interp
4171                                .splice_array_deref(aref, offset_val, length_val, rep_vals, line),
4172                            line,
4173                        )?;
4174                        self.push(v);
4175                        Ok(())
4176                    }
4177                    Op::ArrayLen(idx) => {
4178                        let len = self.interp.scope.array_len(&self.names[*idx as usize]);
4179                        self.push(StrykeValue::integer(len as i64));
4180                        Ok(())
4181                    }
4182                    Op::ArraySlicePart(idx) => {
4183                        let spec = self.pop();
4184                        let n = names[*idx as usize].as_str();
4185                        let mut out = Vec::new();
4186                        if let Some(indices) = spec.as_array_vec() {
4187                            for pv in indices {
4188                                out.push(self.interp.scope.get_array_element(n, pv.to_int()));
4189                            }
4190                        } else {
4191                            out.push(self.interp.scope.get_array_element(n, spec.to_int()));
4192                        }
4193                        self.push(StrykeValue::array(out));
4194                        Ok(())
4195                    }
4196                    Op::GetArrayFromIndex(idx, start) => {
4197                        let n = names[*idx as usize].as_str();
4198                        let arr = self.interp.scope.get_array(n);
4199                        let start = *start as usize;
4200                        let out: Vec<StrykeValue> = if start >= arr.len() {
4201                            Vec::new()
4202                        } else {
4203                            arr[start..].to_vec()
4204                        };
4205                        self.push(StrykeValue::array(out));
4206                        Ok(())
4207                    }
4208                    Op::ArrayConcatTwo => {
4209                        let b = self.pop();
4210                        let a = self.pop();
4211                        let mut av = a.as_array_vec().unwrap_or_else(|| vec![a]);
4212                        let bv = b.as_array_vec().unwrap_or_else(|| vec![b]);
4213                        av.extend(bv);
4214                        self.push(StrykeValue::array(av));
4215                        Ok(())
4216                    }
4217
4218                    // ── Hashes ──
4219                    Op::GetHash(idx) => {
4220                        let n = names[*idx as usize].as_str();
4221                        self.interp.touch_env_hash(n);
4222                        let h = self.interp.scope.get_hash(n);
4223                        self.push(StrykeValue::hash(h));
4224                        Ok(())
4225                    }
4226                    Op::SetHash(idx) => {
4227                        let val = self.pop();
4228                        let items = val.to_list();
4229                        let mut map = IndexMap::new();
4230                        let mut i = 0;
4231                        while i + 1 < items.len() {
4232                            map.insert(items[i].to_string(), items[i + 1].clone());
4233                            i += 2;
4234                        }
4235                        let n = names[*idx as usize].as_str();
4236                        self.require_hash_mutable(n)?;
4237                        self.interp
4238                            .scope
4239                            .set_hash(n, map)
4240                            .map_err(|e| e.at_line(self.line()))?;
4241                        Ok(())
4242                    }
4243                    Op::DeclareHash(idx) => {
4244                        let val = self.pop();
4245                        let n = names[*idx as usize].as_str();
4246                        // `our %h;` (no initializer) compiles as
4247                        // LoadUndef + DeclareHash. For package-qualified
4248                        // names (the `our` form), we must NOT clobber
4249                        // existing data — re-declaring in a subsequent
4250                        // EVAL on the same persistent VMHelper should
4251                        // preserve cross-EVAL state. For lexical names
4252                        // (the `my` form, no `::` qualifier), the
4253                        // declare-only path SHOULD initialize to empty
4254                        // every time (a fresh `my %h;` inside a loop
4255                        // must reset per iteration; preserving prior
4256                        // data would silently leak state across loops
4257                        // and break demos like de_bruijn_sequence).
4258                        // Bug fix 2026-05-27, refined to gate on
4259                        // package-qualification after de_bruijn regression.
4260                        if val.is_undef() && n.contains("::") {
4261                            let existing = self.interp.scope.get_hash(n);
4262                            self.interp.scope.declare_hash(n, existing);
4263                        } else {
4264                            let items = val.to_list();
4265                            let mut map = IndexMap::new();
4266                            let mut i = 0;
4267                            while i + 1 < items.len() {
4268                                map.insert(items[i].to_string(), items[i + 1].clone());
4269                                i += 2;
4270                            }
4271                            self.interp.scope.declare_hash(n, map);
4272                        }
4273                        Ok(())
4274                    }
4275                    Op::DeclareHashFrozen(idx) => {
4276                        let val = self.pop();
4277                        let items = val.to_list();
4278                        let mut map = IndexMap::new();
4279                        let mut i = 0;
4280                        while i + 1 < items.len() {
4281                            map.insert(items[i].to_string(), items[i + 1].clone());
4282                            i += 2;
4283                        }
4284                        let n = names[*idx as usize].as_str();
4285                        self.interp.scope.declare_hash_frozen(n, map, true);
4286                        Ok(())
4287                    }
4288                    Op::LocalDeclareScalar(idx) => {
4289                        let val = self.pop();
4290                        let n = names[*idx as usize].as_str();
4291                        // `local $X` on a special var (`$/`, `$\`, `$,`, `$"`, …) — see
4292                        // Perl's `local` handler. Save prior value to
4293                        // the interpreter's `special_var_restore_frames` so `scope_pop_hook`
4294                        // restores the backing field on block exit.
4295                        if VMHelper::is_special_scalar_name_for_set(n) {
4296                            let old = self.interp.get_special_var(n);
4297                            if let Some(frame) = self.interp.special_var_restore_frames.last_mut() {
4298                                frame.push((n.to_string(), old));
4299                            }
4300                            let line = self.line();
4301                            self.interp
4302                                .set_special_var(n, &val)
4303                                .map_err(|e| e.at_line(line))?;
4304                        }
4305                        self.interp
4306                            .scope
4307                            .local_set_scalar(n, val.clone())
4308                            .map_err(|e| e.at_line(self.line()))?;
4309                        self.push(val);
4310                        Ok(())
4311                    }
4312                    Op::LocalDeclareArray(idx) => {
4313                        let val = self.pop();
4314                        let n = names[*idx as usize].as_str();
4315                        self.interp
4316                            .scope
4317                            .local_set_array(n, val.to_list())
4318                            .map_err(|e| e.at_line(self.line()))?;
4319                        self.push(val);
4320                        Ok(())
4321                    }
4322                    Op::LocalDeclareHash(idx) => {
4323                        let val = self.pop();
4324                        let items = val.to_list();
4325                        let mut map = IndexMap::new();
4326                        let mut i = 0;
4327                        while i + 1 < items.len() {
4328                            map.insert(items[i].to_string(), items[i + 1].clone());
4329                            i += 2;
4330                        }
4331                        let n = names[*idx as usize].as_str();
4332                        self.interp.touch_env_hash(n);
4333                        self.interp
4334                            .scope
4335                            .local_set_hash(n, map)
4336                            .map_err(|e| e.at_line(self.line()))?;
4337                        self.push(val);
4338                        Ok(())
4339                    }
4340                    Op::LocalDeclareHashElement(idx) => {
4341                        let key = self.pop().to_string();
4342                        let val = self.pop();
4343                        let n = names[*idx as usize].as_str();
4344                        self.interp.touch_env_hash(n);
4345                        self.interp
4346                            .scope
4347                            .local_set_hash_element(n, key.as_str(), val.clone())
4348                            .map_err(|e| e.at_line(self.line()))?;
4349                        self.push(val);
4350                        Ok(())
4351                    }
4352                    Op::LocalDeclareArrayElement(idx) => {
4353                        let index = self.pop().to_int();
4354                        let val = self.pop();
4355                        let n = names[*idx as usize].as_str();
4356                        self.require_array_mutable(n)?;
4357                        self.interp
4358                            .scope
4359                            .local_set_array_element(n, index, val.clone())
4360                            .map_err(|e| e.at_line(self.line()))?;
4361                        self.push(val);
4362                        Ok(())
4363                    }
4364                    Op::LocalDeclareTypeglob(lhs_i, rhs_opt) => {
4365                        let lhs = names[*lhs_i as usize].as_str();
4366                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
4367                        let line = self.line();
4368                        self.interp
4369                            .local_declare_typeglob(lhs, rhs, line)
4370                            .map_err(|e| e.at_line(line))?;
4371                        Ok(())
4372                    }
4373                    Op::LocalDeclareTypeglobDynamic(rhs_opt) => {
4374                        let lhs = self.pop().to_string();
4375                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
4376                        let line = self.line();
4377                        self.interp
4378                            .local_declare_typeglob(lhs.as_str(), rhs, line)
4379                            .map_err(|e| e.at_line(line))?;
4380                        Ok(())
4381                    }
4382                    Op::GetHashElem(idx) => {
4383                        let key = self.pop().to_string();
4384                        let n = names[*idx as usize].as_str();
4385                        self.interp.touch_env_hash(n);
4386                        let val = self.interp.scope.get_hash_element(n, &key);
4387                        self.push(val);
4388                        Ok(())
4389                    }
4390                    Op::SetHashElem(idx) => {
4391                        let key = self.pop().to_string();
4392                        let val = self.pop();
4393                        let n = names[*idx as usize].as_str();
4394                        self.require_hash_mutable(n)?;
4395                        self.interp.touch_env_hash(n);
4396                        self.interp
4397                            .scope
4398                            .set_hash_element(n, &key, val)
4399                            .map_err(|e| e.at_line(self.line()))?;
4400                        Ok(())
4401                    }
4402                    Op::SetHashElemKeep(idx) => {
4403                        let key = self.pop().to_string();
4404                        let val = self.pop();
4405                        let val_keep = val.clone();
4406                        let n = names[*idx as usize].as_str();
4407                        self.require_hash_mutable(n)?;
4408                        self.interp.touch_env_hash(n);
4409                        let line = self.line();
4410                        self.interp
4411                            .scope
4412                            .set_hash_element(n, &key, val)
4413                            .map_err(|e| e.at_line(line))?;
4414                        self.push(val_keep);
4415                        Ok(())
4416                    }
4417                    Op::DeleteHashElem(idx) => {
4418                        let key = self.pop().to_string();
4419                        let n = names[*idx as usize].as_str();
4420                        self.require_hash_mutable(n)?;
4421                        self.interp.touch_env_hash(n);
4422                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
4423                            let class = obj
4424                                .as_blessed_ref()
4425                                .map(|b| b.class.clone())
4426                                .unwrap_or_default();
4427                            let full = format!("{}::DELETE", class);
4428                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
4429                                let line = self.line();
4430                                let v = vm_interp_result(
4431                                    self.interp.call_sub(
4432                                        &sub,
4433                                        vec![obj, StrykeValue::string(key)],
4434                                        WantarrayCtx::Scalar,
4435                                        line,
4436                                    ),
4437                                    line,
4438                                )?;
4439                                self.push(v);
4440                                return Ok(());
4441                            }
4442                        }
4443                        let val = self
4444                            .interp
4445                            .scope
4446                            .delete_hash_element(n, &key)
4447                            .map_err(|e| e.at_line(self.line()))?;
4448                        self.push(val);
4449                        Ok(())
4450                    }
4451                    Op::ExistsHashElem(idx) => {
4452                        let key = self.pop().to_string();
4453                        let n = names[*idx as usize].as_str();
4454                        self.interp.touch_env_hash(n);
4455                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
4456                            let class = obj
4457                                .as_blessed_ref()
4458                                .map(|b| b.class.clone())
4459                                .unwrap_or_default();
4460                            let full = format!("{}::EXISTS", class);
4461                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
4462                                let line = self.line();
4463                                let v = vm_interp_result(
4464                                    self.interp.call_sub(
4465                                        &sub,
4466                                        vec![obj, StrykeValue::string(key)],
4467                                        WantarrayCtx::Scalar,
4468                                        line,
4469                                    ),
4470                                    line,
4471                                )?;
4472                                self.push(v);
4473                                return Ok(());
4474                            }
4475                        }
4476                        let exists = self.interp.scope.exists_hash_element(n, &key);
4477                        self.push(StrykeValue::integer(if exists { 1 } else { 0 }));
4478                        Ok(())
4479                    }
4480                    Op::ExistsArrowHashElem => {
4481                        let key = self.pop().to_string();
4482                        let container = self.pop();
4483                        let line = self.line();
4484                        let yes = vm_interp_result(
4485                            self.interp
4486                                .exists_arrow_hash_element(container, &key, line)
4487                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
4488                                .map_err(FlowOrError::Error),
4489                            line,
4490                        )?;
4491                        self.push(yes);
4492                        Ok(())
4493                    }
4494                    Op::DeleteArrowHashElem => {
4495                        let key = self.pop().to_string();
4496                        let container = self.pop();
4497                        let line = self.line();
4498                        let v = vm_interp_result(
4499                            self.interp
4500                                .delete_arrow_hash_element(container, &key, line)
4501                                .map_err(FlowOrError::Error),
4502                            line,
4503                        )?;
4504                        self.push(v);
4505                        Ok(())
4506                    }
4507                    Op::ExistsArrowArrayElem => {
4508                        let idx = self.pop().to_int();
4509                        let container = self.pop();
4510                        let line = self.line();
4511                        let yes = vm_interp_result(
4512                            self.interp
4513                                .exists_arrow_array_element(container, idx, line)
4514                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
4515                                .map_err(FlowOrError::Error),
4516                            line,
4517                        )?;
4518                        self.push(yes);
4519                        Ok(())
4520                    }
4521                    Op::DeleteArrowArrayElem => {
4522                        let idx = self.pop().to_int();
4523                        let container = self.pop();
4524                        let line = self.line();
4525                        let v = vm_interp_result(
4526                            self.interp
4527                                .delete_arrow_array_element(container, idx, line)
4528                                .map_err(FlowOrError::Error),
4529                            line,
4530                        )?;
4531                        self.push(v);
4532                        Ok(())
4533                    }
4534                    Op::HashKeys(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                        let keys: Vec<StrykeValue> =
4539                            h.keys().map(|k| StrykeValue::string(k.clone())).collect();
4540                        self.push(StrykeValue::array(keys));
4541                        Ok(())
4542                    }
4543                    Op::HashKeysScalar(idx) => {
4544                        let n = names[*idx as usize].as_str();
4545                        self.interp.touch_env_hash(n);
4546                        let h = self.interp.scope.get_hash(n);
4547                        self.push(StrykeValue::integer(h.len() as i64));
4548                        Ok(())
4549                    }
4550                    Op::HashValues(idx) => {
4551                        let n = names[*idx as usize].as_str();
4552                        self.interp.touch_env_hash(n);
4553                        let h = self.interp.scope.get_hash(n);
4554                        let vals: Vec<StrykeValue> = h.values().cloned().collect();
4555                        self.push(StrykeValue::array(vals));
4556                        Ok(())
4557                    }
4558                    Op::HashValuesScalar(idx) => {
4559                        let n = names[*idx as usize].as_str();
4560                        self.interp.touch_env_hash(n);
4561                        let h = self.interp.scope.get_hash(n);
4562                        self.push(StrykeValue::integer(h.len() as i64));
4563                        Ok(())
4564                    }
4565                    Op::KeysFromValue => {
4566                        let val = self.pop();
4567                        let line = self.line();
4568                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
4569                        self.push(v);
4570                        Ok(())
4571                    }
4572                    Op::KeysFromValueScalar => {
4573                        let val = self.pop();
4574                        let line = self.line();
4575                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
4576                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
4577                        self.push(StrykeValue::integer(n));
4578                        Ok(())
4579                    }
4580                    Op::ValuesFromValue => {
4581                        let val = self.pop();
4582                        let line = self.line();
4583                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
4584                        self.push(v);
4585                        Ok(())
4586                    }
4587                    Op::ValuesFromValueScalar => {
4588                        let val = self.pop();
4589                        let line = self.line();
4590                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
4591                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
4592                        self.push(StrykeValue::integer(n));
4593                        Ok(())
4594                    }
4595
4596                    // ── Arithmetic (integer fast paths) ──
4597                    Op::Add => {
4598                        let b = self.pop();
4599                        let a = self.pop();
4600                        self.push_binop_with_overload(BinOp::Add, a, b, |a, b| {
4601                            if let Some(s) = crate::sketches::try_sketch_binop(
4602                                crate::sketches::SketchOp::Add,
4603                                a,
4604                                b,
4605                            ) {
4606                                return Ok(s);
4607                            }
4608                            Ok(crate::value::compat_add(a, b))
4609                        })
4610                    }
4611                    Op::Sub => {
4612                        let b = self.pop();
4613                        let a = self.pop();
4614                        self.push_binop_with_overload(BinOp::Sub, a, b, |a, b| {
4615                            if let Some(s) = crate::sketches::try_sketch_binop(
4616                                crate::sketches::SketchOp::Sub,
4617                                a,
4618                                b,
4619                            ) {
4620                                return Ok(s);
4621                            }
4622                            Ok(crate::value::compat_sub(a, b))
4623                        })
4624                    }
4625                    Op::Mul => {
4626                        let b = self.pop();
4627                        let a = self.pop();
4628                        self.push_binop_with_overload(BinOp::Mul, a, b, |a, b| {
4629                            Ok(crate::value::compat_mul(a, b))
4630                        })
4631                    }
4632                    Op::Div => {
4633                        let b = self.pop();
4634                        let a = self.pop();
4635                        let line = self.line();
4636                        self.push_binop_with_overload(BinOp::Div, a, b, |a, b| {
4637                            if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
4638                                if y == 0 {
4639                                    return Err(StrykeError::division_by_zero(
4640                                        "Illegal division by zero",
4641                                        line,
4642                                    ));
4643                                }
4644                                Ok(if x % y == 0 {
4645                                    StrykeValue::integer(x / y)
4646                                } else {
4647                                    StrykeValue::float(x as f64 / y as f64)
4648                                })
4649                            } else {
4650                                let d = b.to_number();
4651                                if d == 0.0 {
4652                                    return Err(StrykeError::division_by_zero(
4653                                        "Illegal division by zero",
4654                                        line,
4655                                    ));
4656                                }
4657                                Ok(StrykeValue::float(a.to_number() / d))
4658                            }
4659                        })
4660                    }
4661                    Op::Mod => {
4662                        let b = self.pop();
4663                        let a = self.pop();
4664                        let line = self.line();
4665                        self.push_binop_with_overload(BinOp::Mod, a, b, |a, b| {
4666                            let b = b.to_int();
4667                            let a = a.to_int();
4668                            if b == 0 {
4669                                return Err(StrykeError::division_by_zero(
4670                                    "Illegal modulus zero",
4671                                    line,
4672                                ));
4673                            }
4674                            Ok(StrykeValue::integer(crate::value::perl_mod_i64(a, b)))
4675                        })
4676                    }
4677                    Op::Pow => {
4678                        let b = self.pop();
4679                        let a = self.pop();
4680                        self.push_binop_with_overload(BinOp::Pow, a, b, |a, b| {
4681                            Ok(crate::value::compat_pow(a, b))
4682                        })
4683                    }
4684                    Op::Negate => {
4685                        let a = self.pop();
4686                        let line = self.line();
4687                        if let Some(exec_res) =
4688                            self.interp.try_overload_unary_dispatch("neg", &a, line)
4689                        {
4690                            self.push(vm_interp_result(exec_res, line)?);
4691                        } else {
4692                            self.push(if let Some(n) = a.as_integer() {
4693                                StrykeValue::integer(-n)
4694                            } else {
4695                                StrykeValue::float(-a.to_number())
4696                            });
4697                        }
4698                        Ok(())
4699                    }
4700                    Op::Inc => {
4701                        let a = self.pop();
4702                        self.push(if let Some(n) = a.as_integer() {
4703                            StrykeValue::integer(n.wrapping_add(1))
4704                        } else {
4705                            StrykeValue::float(a.to_number() + 1.0)
4706                        });
4707                        Ok(())
4708                    }
4709                    Op::Dec => {
4710                        let a = self.pop();
4711                        self.push(if let Some(n) = a.as_integer() {
4712                            StrykeValue::integer(n.wrapping_sub(1))
4713                        } else {
4714                            StrykeValue::float(a.to_number() - 1.0)
4715                        });
4716                        Ok(())
4717                    }
4718
4719                    // ── String ──
4720                    Op::Concat => {
4721                        let b = self.pop();
4722                        let a = self.pop();
4723                        let out = self.concat_stack_values(a, b)?;
4724                        self.push(out);
4725                        Ok(())
4726                    }
4727                    Op::ArrayStringifyListSep => {
4728                        let raw = self.pop();
4729                        let v = self.interp.peel_array_ref_for_list_join(raw);
4730                        let sep = self.interp.list_separator.clone();
4731                        let list = v.to_list();
4732                        let joined = list
4733                            .iter()
4734                            .map(|x| x.to_string())
4735                            .collect::<Vec<_>>()
4736                            .join(&sep);
4737                        self.push(StrykeValue::string(joined));
4738                        Ok(())
4739                    }
4740                    Op::StringRepeat => {
4741                        let n = self.pop().to_int();
4742                        let val = self.pop();
4743                        self.push(StrykeValue::string(val.repeat_value(n)));
4744                        Ok(())
4745                    }
4746                    Op::ListRepeat => {
4747                        let n = self.pop().to_int().max(0) as usize;
4748                        let val = self.pop();
4749                        // Flatten to a Vec<StrykeValue>: an array value gives its
4750                        // items; a scalar (e.g. `(0) x 5` after the LHS evaluates
4751                        // through scalar-collapse paths) wraps as a 1-elt list.
4752                        let items: Vec<StrykeValue> =
4753                            val.as_array_vec().unwrap_or_else(|| vec![val]);
4754                        let mut out = Vec::with_capacity(items.len().saturating_mul(n));
4755                        for _ in 0..n {
4756                            out.extend(items.iter().cloned());
4757                        }
4758                        self.push(StrykeValue::array(out));
4759                        Ok(())
4760                    }
4761                    Op::ProcessCaseEscapes => {
4762                        let val = self.pop();
4763                        let s = val.to_string();
4764                        let processed = VMHelper::process_case_escapes(&s);
4765                        self.push(StrykeValue::string(processed));
4766                        Ok(())
4767                    }
4768
4769                    // ── Numeric comparison ──
4770                    Op::NumEq => {
4771                        let b = self.pop();
4772                        let a = self.pop();
4773                        self.push_binop_with_overload(BinOp::NumEq, a.clone(), b.clone(), |a, b| {
4774                            // Struct equality: compare all fields
4775                            if let (Some(sa), Some(sb)) = (a.as_struct_inst(), b.as_struct_inst()) {
4776                                if sa.def.name != sb.def.name {
4777                                    return Ok(StrykeValue::integer(0));
4778                                }
4779                                let av = sa.get_values();
4780                                let bv = sb.get_values();
4781                                let eq = av.len() == bv.len()
4782                                    && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
4783                                Ok(StrykeValue::integer(if eq { 1 } else { 0 }))
4784                            } else {
4785                                if !crate::compat_mode() && both_non_numeric_strings(a, b) {
4786                                    let sa = a.to_string();
4787                                    let sb = b.to_string();
4788                                    return Ok(StrykeValue::integer(if sa == sb { 1 } else { 0 }));
4789                                }
4790                                Ok(int_cmp(a, b, |x, y| x == y, |x, y| x == y))
4791                            }
4792                        })
4793                    }
4794                    Op::NumNe => {
4795                        let b = self.pop();
4796                        let a = self.pop();
4797                        self.push_binop_with_overload(BinOp::NumNe, a, b, |a, b| {
4798                            // Stryke (non-compat) sugar: when both operands are
4799                            // non-numeric strings, fall back to `ne`. In Perl,
4800                            // `"G" != "T"` is `0 != 0` = false; in stryke we
4801                            // want char/string compare. Compat mode keeps
4802                            // Perl semantics.
4803                            if !crate::compat_mode() && both_non_numeric_strings(a, b) {
4804                                let sa = a.to_string();
4805                                let sb = b.to_string();
4806                                return Ok(StrykeValue::integer(if sa != sb { 1 } else { 0 }));
4807                            }
4808                            Ok(int_cmp(a, b, |x, y| x != y, |x, y| x != y))
4809                        })
4810                    }
4811                    Op::NumLt => {
4812                        let b = self.pop();
4813                        let a = self.pop();
4814                        self.push_binop_with_overload(BinOp::NumLt, a, b, |a, b| {
4815                            Ok(int_cmp(a, b, |x, y| x < y, |x, y| x < y))
4816                        })
4817                    }
4818                    Op::NumGt => {
4819                        let b = self.pop();
4820                        let a = self.pop();
4821                        self.push_binop_with_overload(BinOp::NumGt, a, b, |a, b| {
4822                            Ok(int_cmp(a, b, |x, y| x > y, |x, y| x > y))
4823                        })
4824                    }
4825                    Op::NumLe => {
4826                        let b = self.pop();
4827                        let a = self.pop();
4828                        self.push_binop_with_overload(BinOp::NumLe, a, b, |a, b| {
4829                            Ok(int_cmp(a, b, |x, y| x <= y, |x, y| x <= y))
4830                        })
4831                    }
4832                    Op::NumGe => {
4833                        let b = self.pop();
4834                        let a = self.pop();
4835                        self.push_binop_with_overload(BinOp::NumGe, a, b, |a, b| {
4836                            Ok(int_cmp(a, b, |x, y| x >= y, |x, y| x >= y))
4837                        })
4838                    }
4839                    Op::Spaceship => {
4840                        let b = self.pop();
4841                        let a = self.pop();
4842                        self.push_binop_with_overload(BinOp::Spaceship, a, b, |a, b| {
4843                            Ok(
4844                                if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
4845                                    StrykeValue::integer(if x < y {
4846                                        -1
4847                                    } else if x > y {
4848                                        1
4849                                    } else {
4850                                        0
4851                                    })
4852                                } else {
4853                                    let x = a.to_number();
4854                                    let y = b.to_number();
4855                                    StrykeValue::integer(if x < y {
4856                                        -1
4857                                    } else if x > y {
4858                                        1
4859                                    } else {
4860                                        0
4861                                    })
4862                                },
4863                            )
4864                        })
4865                    }
4866
4867                    // ── String comparison ──
4868                    Op::StrEq => {
4869                        let b = self.pop();
4870                        let a = self.pop();
4871                        self.push_binop_with_overload(BinOp::StrEq, a, b, |a, b| {
4872                            Ok(StrykeValue::integer(if a.str_eq(b) { 1 } else { 0 }))
4873                        })
4874                    }
4875                    Op::StrNe => {
4876                        let b = self.pop();
4877                        let a = self.pop();
4878                        self.push_binop_with_overload(BinOp::StrNe, a, b, |a, b| {
4879                            Ok(StrykeValue::integer(if !a.str_eq(b) { 1 } else { 0 }))
4880                        })
4881                    }
4882                    Op::StrLt => {
4883                        let b = self.pop();
4884                        let a = self.pop();
4885                        self.push_binop_with_overload(BinOp::StrLt, a, b, |a, b| {
4886                            Ok(StrykeValue::integer(
4887                                if a.str_cmp(b) == std::cmp::Ordering::Less {
4888                                    1
4889                                } else {
4890                                    0
4891                                },
4892                            ))
4893                        })
4894                    }
4895                    Op::StrGt => {
4896                        let b = self.pop();
4897                        let a = self.pop();
4898                        self.push_binop_with_overload(BinOp::StrGt, a, b, |a, b| {
4899                            Ok(StrykeValue::integer(
4900                                if a.str_cmp(b) == std::cmp::Ordering::Greater {
4901                                    1
4902                                } else {
4903                                    0
4904                                },
4905                            ))
4906                        })
4907                    }
4908                    Op::StrLe => {
4909                        let b = self.pop();
4910                        let a = self.pop();
4911                        self.push_binop_with_overload(BinOp::StrLe, a, b, |a, b| {
4912                            let o = a.str_cmp(b);
4913                            Ok(StrykeValue::integer(
4914                                if matches!(o, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
4915                                {
4916                                    1
4917                                } else {
4918                                    0
4919                                },
4920                            ))
4921                        })
4922                    }
4923                    Op::StrGe => {
4924                        let b = self.pop();
4925                        let a = self.pop();
4926                        self.push_binop_with_overload(BinOp::StrGe, a, b, |a, b| {
4927                            let o = a.str_cmp(b);
4928                            Ok(StrykeValue::integer(
4929                                if matches!(
4930                                    o,
4931                                    std::cmp::Ordering::Greater | std::cmp::Ordering::Equal
4932                                ) {
4933                                    1
4934                                } else {
4935                                    0
4936                                },
4937                            ))
4938                        })
4939                    }
4940                    Op::StrCmp => {
4941                        let b = self.pop();
4942                        let a = self.pop();
4943                        self.push_binop_with_overload(BinOp::StrCmp, a, b, |a, b| {
4944                            let cmp = a.str_cmp(b);
4945                            Ok(StrykeValue::integer(match cmp {
4946                                std::cmp::Ordering::Less => -1,
4947                                std::cmp::Ordering::Greater => 1,
4948                                std::cmp::Ordering::Equal => 0,
4949                            }))
4950                        })
4951                    }
4952
4953                    // ── Logical / Bitwise ──
4954                    Op::LogNot => {
4955                        let a = self.pop();
4956                        let line = self.line();
4957                        if let Some(exec_res) =
4958                            self.interp.try_overload_unary_dispatch("bool", &a, line)
4959                        {
4960                            let pv = vm_interp_result(exec_res, line)?;
4961                            self.push(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
4962                        } else {
4963                            self.push(StrykeValue::integer(if a.is_true() { 0 } else { 1 }));
4964                        }
4965                        Ok(())
4966                    }
4967                    Op::BitAnd => {
4968                        let rv = self.pop();
4969                        let lv = self.pop();
4970                        if let Some(s) = crate::value::set_intersection(&lv, &rv) {
4971                            self.push(s);
4972                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4973                            crate::sketches::SketchOp::And,
4974                            &lv,
4975                            &rv,
4976                        ) {
4977                            self.push(s);
4978                        } else {
4979                            self.push(StrykeValue::integer(lv.to_int() & rv.to_int()));
4980                        }
4981                        Ok(())
4982                    }
4983                    Op::BitOr => {
4984                        let rv = self.pop();
4985                        let lv = self.pop();
4986                        if let Some(s) = crate::value::set_union(&lv, &rv) {
4987                            self.push(s);
4988                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4989                            crate::sketches::SketchOp::Or,
4990                            &lv,
4991                            &rv,
4992                        ) {
4993                            self.push(s);
4994                        } else {
4995                            self.push(StrykeValue::integer(lv.to_int() | rv.to_int()));
4996                        }
4997                        Ok(())
4998                    }
4999                    Op::BitXor => {
5000                        let rv = self.pop();
5001                        let lv = self.pop();
5002                        if let Some(s) = crate::sketches::try_sketch_binop(
5003                            crate::sketches::SketchOp::Xor,
5004                            &lv,
5005                            &rv,
5006                        ) {
5007                            self.push(s);
5008                        } else {
5009                            self.push(StrykeValue::integer(lv.to_int() ^ rv.to_int()));
5010                        }
5011                        Ok(())
5012                    }
5013                    Op::BitNot => {
5014                        let a = self.pop().to_int();
5015                        self.push(StrykeValue::integer(!a));
5016                        Ok(())
5017                    }
5018                    Op::Shl => {
5019                        let b = self.pop().to_int();
5020                        let a = self.pop().to_int();
5021                        self.push(StrykeValue::integer(perl_shl_i64(a, b)));
5022                        Ok(())
5023                    }
5024                    Op::Shr => {
5025                        let b = self.pop().to_int();
5026                        let a = self.pop().to_int();
5027                        self.push(StrykeValue::integer(perl_shr_i64(a, b)));
5028                        Ok(())
5029                    }
5030
5031                    // ── Control flow ──
5032                    Op::Jump(target) => {
5033                        self.ip = *target;
5034                        Ok(())
5035                    }
5036                    Op::JumpIfTrue(target) => {
5037                        let val = self.pop();
5038                        if val.is_true() {
5039                            self.ip = *target;
5040                        }
5041                        Ok(())
5042                    }
5043                    Op::JumpIfFalse(target) => {
5044                        let val = self.pop();
5045                        if !val.is_true() {
5046                            self.ip = *target;
5047                        }
5048                        Ok(())
5049                    }
5050                    Op::JumpIfFalseKeep(target) => {
5051                        if !self.peek().is_true() {
5052                            self.ip = *target;
5053                        } else {
5054                            self.pop();
5055                        }
5056                        Ok(())
5057                    }
5058                    Op::JumpIfTrueKeep(target) => {
5059                        if self.peek().is_true() {
5060                            self.ip = *target;
5061                        } else {
5062                            self.pop();
5063                        }
5064                        Ok(())
5065                    }
5066                    Op::JumpIfDefinedKeep(target) => {
5067                        if !self.peek().is_undef() {
5068                            self.ip = *target;
5069                        } else {
5070                            self.pop();
5071                        }
5072                        Ok(())
5073                    }
5074
5075                    // ── Increment / Decrement ──
5076                    Op::PreInc(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                        let new_val = self
5081                            .interp
5082                            .scope
5083                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() + 1))
5084                            .map_err(|e| e.at_line(self.line()))?;
5085                        self.push(new_val);
5086                        Ok(())
5087                    }
5088                    Op::PreDec(idx) => {
5089                        let n = names[*idx as usize].as_str();
5090                        self.require_scalar_mutable(n)?;
5091                        let en = self.interp.english_scalar_name(n);
5092                        let new_val = self
5093                            .interp
5094                            .scope
5095                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() - 1))
5096                            .map_err(|e| e.at_line(self.line()))?;
5097                        self.push(new_val);
5098                        Ok(())
5099                    }
5100                    Op::PostInc(idx) => {
5101                        let n = names[*idx as usize].as_str();
5102                        self.require_scalar_mutable(n)?;
5103                        let en = self.interp.english_scalar_name(n);
5104                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5105                            self.interp
5106                                .scope
5107                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
5108                                .map_err(|e| e.at_line(self.line()))?;
5109                            self.ip += 1;
5110                        } else {
5111                            let old = self
5112                                .interp
5113                                .scope
5114                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
5115                                .map_err(|e| e.at_line(self.line()))?;
5116                            self.push(old);
5117                        }
5118                        Ok(())
5119                    }
5120                    Op::PostDec(idx) => {
5121                        let n = names[*idx as usize].as_str();
5122                        self.require_scalar_mutable(n)?;
5123                        let en = self.interp.english_scalar_name(n);
5124                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5125                            self.interp
5126                                .scope
5127                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
5128                                .map_err(|e| e.at_line(self.line()))?;
5129                            self.ip += 1;
5130                        } else {
5131                            let old = self
5132                                .interp
5133                                .scope
5134                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
5135                                .map_err(|e| e.at_line(self.line()))?;
5136                            self.push(old);
5137                        }
5138                        Ok(())
5139                    }
5140                    Op::PreIncSlot(slot) => {
5141                        let cur = self.interp.scope.get_scalar_slot(*slot);
5142                        let new_val = crate::vm_helper::perl_inc(&cur);
5143                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
5144                        self.push(new_val);
5145                        Ok(())
5146                    }
5147                    Op::PreIncSlotVoid(slot) => {
5148                        let cur = self.interp.scope.get_scalar_slot(*slot);
5149                        let new_val = crate::vm_helper::perl_inc(&cur);
5150                        self.interp.scope.set_scalar_slot(*slot, new_val);
5151                        Ok(())
5152                    }
5153                    Op::PreDecSlot(slot) => {
5154                        let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
5155                        let new_val = StrykeValue::integer(val);
5156                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
5157                        self.push(new_val);
5158                        Ok(())
5159                    }
5160                    Op::PostIncSlot(slot) => {
5161                        // Fuse PostIncSlot+Pop: if next op discards the old value, skip stack work.
5162                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5163                            let cur = self.interp.scope.get_scalar_slot(*slot);
5164                            let new_val = crate::vm_helper::perl_inc(&cur);
5165                            self.interp.scope.set_scalar_slot(*slot, new_val);
5166                            self.ip += 1; // skip Pop
5167                        } else {
5168                            let old = self.interp.scope.get_scalar_slot(*slot);
5169                            let new_val = crate::vm_helper::perl_inc(&old);
5170                            self.interp.scope.set_scalar_slot(*slot, new_val);
5171                            self.push(old);
5172                        }
5173                        Ok(())
5174                    }
5175                    Op::PostDecSlot(slot) => {
5176                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
5177                            let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
5178                            self.interp
5179                                .scope
5180                                .set_scalar_slot(*slot, StrykeValue::integer(val));
5181                            self.ip += 1;
5182                        } else {
5183                            let old = self.interp.scope.get_scalar_slot(*slot);
5184                            let new_val = StrykeValue::integer(old.to_int() - 1);
5185                            self.interp.scope.set_scalar_slot(*slot, new_val);
5186                            self.push(old);
5187                        }
5188                        Ok(())
5189                    }
5190
5191                    // ── Functions ──
5192                    Op::Call(name_idx, argc, wa) => {
5193                        // A bare callable spelling (`sum`, `set`, `count`, …) routes
5194                        // to the global builtin even when a same-named user sub is
5195                        // registered. There is no shadowing of stryke builtins in
5196                        // default mode: user code can declare `fn sum {}` inside a
5197                        // non-main package, but the only way to reach that user sub
5198                        // is the fully-qualified `Pkg::sum(...)` spelling.
5199                        // `--compat` (full Perl 5 mode) restores classic UDF-wins
5200                        // semantics so unmodified Perl 5 modules keep working.
5201                        let name = &self.names[*name_idx as usize];
5202                        let entry_opt = if !crate::compat_mode()
5203                            && !name.contains("::")
5204                            && crate::builtins::is_callable_spelling(name)
5205                        {
5206                            None
5207                        } else {
5208                            self.find_sub_entry(*name_idx)
5209                        };
5210                        self.vm_dispatch_user_call(*name_idx, entry_opt, *argc, *wa, None)?;
5211                        Ok(())
5212                    }
5213                    Op::CallStaticSubId(sid, name_idx, argc, wa) => {
5214                        let t = self.static_sub_calls.get(*sid as usize).ok_or_else(|| {
5215                            StrykeError::runtime("VM: invalid CallStaticSubId", self.line())
5216                        })?;
5217                        debug_assert_eq!(t.2, *name_idx);
5218                        let closure_sub = self
5219                            .static_sub_closure_subs
5220                            .get(*sid as usize)
5221                            .and_then(|x| x.clone());
5222                        self.vm_dispatch_user_call(
5223                            *name_idx,
5224                            Some((t.0, t.1)),
5225                            *argc,
5226                            *wa,
5227                            closure_sub,
5228                        )?;
5229                        Ok(())
5230                    }
5231                    Op::Return => {
5232                        if let Some(frame) = self.call_stack.pop() {
5233                            if frame.block_region {
5234                                return Err(StrykeError::runtime(
5235                                    "Return in map/grep/sort block bytecode",
5236                                    self.line(),
5237                                ));
5238                            }
5239                            if let Some(t0) = frame.sub_profiler_start {
5240                                if let Some(p) = &mut self.interp.profiler {
5241                                    p.exit_sub(t0.elapsed());
5242                                }
5243                            }
5244                            self.interp.debugger_leave_sub();
5245                            self.interp.wantarray_kind = frame.saved_wantarray;
5246                            self.stack.truncate(frame.stack_base);
5247                            self.interp.pop_scope_to_depth(frame.scope_depth);
5248                            self.interp.current_sub_stack.pop();
5249                            if frame.jit_trampoline_return {
5250                                self.jit_trampoline_out = Some(StrykeValue::UNDEF);
5251                            } else {
5252                                self.push(StrykeValue::UNDEF);
5253                                self.ip = frame.return_ip;
5254                            }
5255                        } else {
5256                            self.exit_main_dispatch = true;
5257                        }
5258                        Ok(())
5259                    }
5260                    Op::ReturnValue => {
5261                        let val = self.pop();
5262                        // Resolve binding refs to real refs before scope cleanup.
5263                        // `\@array` creates a name-based ArrayBindingRef that looks
5264                        // up by name at dereference time.  If the array is a `my`
5265                        // variable, its frame will be destroyed below — so we must
5266                        // snapshot the data into an Arc-based ref now.
5267                        let val = self.resolve_binding_ref(val);
5268                        // Caller-context coercion: `return LIST` from a sub called
5269                        // in scalar context yields the **last** element of the
5270                        // list (Perl wantarray semantics). Without this, the
5271                        // whole list propagates and a `my $x = sub_returning_list()`
5272                        // sees the array stringified rather than its last element.
5273                        // (BUG-010 / BUG-011)
5274                        let val = if matches!(self.interp.wantarray_kind, WantarrayCtx::Scalar) {
5275                            if let Some(items) = val.as_array_vec() {
5276                                items.last().cloned().unwrap_or(StrykeValue::UNDEF)
5277                            } else {
5278                                val
5279                            }
5280                        } else {
5281                            val
5282                        };
5283                        if let Some(frame) = self.call_stack.pop() {
5284                            if frame.block_region {
5285                                return Err(StrykeError::runtime(
5286                                    "Return in map/grep/sort block bytecode",
5287                                    self.line(),
5288                                ));
5289                            }
5290                            if let Some(t0) = frame.sub_profiler_start {
5291                                if let Some(p) = &mut self.interp.profiler {
5292                                    p.exit_sub(t0.elapsed());
5293                                }
5294                            }
5295                            self.interp.debugger_leave_sub();
5296                            self.interp.wantarray_kind = frame.saved_wantarray;
5297                            self.stack.truncate(frame.stack_base);
5298                            self.interp.pop_scope_to_depth(frame.scope_depth);
5299                            self.interp.current_sub_stack.pop();
5300                            if frame.jit_trampoline_return {
5301                                self.jit_trampoline_out = Some(val);
5302                            } else {
5303                                self.push(val);
5304                                self.ip = frame.return_ip;
5305                            }
5306                        } else {
5307                            self.exit_main_dispatch_value = Some(val);
5308                            self.exit_main_dispatch = true;
5309                        }
5310                        Ok(())
5311                    }
5312                    Op::BlockReturnValue => {
5313                        let val = self.pop();
5314                        let val = self.resolve_binding_ref(val);
5315                        if let Some(frame) = self.call_stack.pop() {
5316                            if !frame.block_region {
5317                                return Err(StrykeError::runtime(
5318                                    "BlockReturnValue without map/grep/sort block frame",
5319                                    self.line(),
5320                                ));
5321                            }
5322                            self.interp.wantarray_kind = frame.saved_wantarray;
5323                            self.stack.truncate(frame.stack_base);
5324                            self.interp.pop_scope_to_depth(frame.scope_depth);
5325                            self.block_region_return = Some(val);
5326                            Ok(())
5327                        } else {
5328                            Err(StrykeError::runtime(
5329                                "BlockReturnValue with empty call stack",
5330                                self.line(),
5331                            ))
5332                        }
5333                    }
5334                    Op::BindSubClosure(name_idx) => {
5335                        let n = names[*name_idx as usize].as_str();
5336                        self.interp.rebind_sub_closure(n);
5337                        Ok(())
5338                    }
5339
5340                    // ── Scope ──
5341                    Op::PushFrame => {
5342                        self.interp.scope_push_hook();
5343                        Ok(())
5344                    }
5345                    Op::PopFrame => {
5346                        self.interp.scope_pop_hook();
5347                        Ok(())
5348                    }
5349                    // ── I/O ──
5350                    Op::Print(handle_idx, argc) => {
5351                        let argc = *argc as usize;
5352                        let mut args = Vec::with_capacity(argc);
5353                        for _ in 0..argc {
5354                            args.push(self.pop());
5355                        }
5356                        args.reverse();
5357                        let mut output = String::new();
5358                        if args.is_empty() {
5359                            let topic = self.interp.scope.get_scalar("_").clone();
5360                            let s = match self.interp.stringify_value(topic, self.line()) {
5361                                Ok(s) => s,
5362                                Err(FlowOrError::Error(e)) => return Err(e),
5363                                Err(FlowOrError::Flow(_)) => {
5364                                    return Err(StrykeError::runtime(
5365                                        "print: unexpected control flow",
5366                                        self.line(),
5367                                    ));
5368                                }
5369                            };
5370                            output.push_str(&s);
5371                        } else {
5372                            for (i, arg) in args.iter().enumerate() {
5373                                if i > 0 && !self.interp.ofs.is_empty() {
5374                                    output.push_str(&self.interp.ofs);
5375                                }
5376                                for item in arg.to_list() {
5377                                    let s = match self.interp.stringify_value(item, self.line()) {
5378                                        Ok(s) => s,
5379                                        Err(FlowOrError::Error(e)) => return Err(e),
5380                                        Err(FlowOrError::Flow(_)) => {
5381                                            return Err(StrykeError::runtime(
5382                                                "print: unexpected control flow",
5383                                                self.line(),
5384                                            ));
5385                                        }
5386                                    };
5387                                    output.push_str(&s);
5388                                }
5389                            }
5390                        }
5391                        output.push_str(&self.interp.ors);
5392                        let handle_name = match handle_idx {
5393                            Some(idx) => self.interp.resolve_io_handle_name(
5394                                self.names
5395                                    .get(*idx as usize)
5396                                    .map_or("STDOUT", |s| s.as_str()),
5397                            ),
5398                            None => self
5399                                .interp
5400                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
5401                        };
5402                        self.interp.write_formatted_print(
5403                            handle_name.as_str(),
5404                            &output,
5405                            self.line(),
5406                        )?;
5407                        self.push(StrykeValue::integer(1));
5408                        Ok(())
5409                    }
5410                    Op::Printf(handle_idx, argc) => {
5411                        let argc = *argc as usize;
5412                        let mut args = Vec::with_capacity(argc);
5413                        for _ in 0..argc {
5414                            args.push(self.pop());
5415                        }
5416                        args.reverse();
5417                        let (fmt, rest) = match args.split_first() {
5418                            Some((f, r)) => (f.to_string(), r),
5419                            None => {
5420                                return Err(StrykeError::runtime(
5421                                    "printf requires a format string",
5422                                    self.line(),
5423                                ));
5424                            }
5425                        };
5426                        // sprintf the args, then route through the handle the
5427                        // same way Print does — fixes printf's silent
5428                        // misdirection to STDOUT.
5429                        let mut flat = Vec::new();
5430                        for a in rest {
5431                            if let Some(items) = a.as_array_vec() {
5432                                flat.extend(items);
5433                            } else {
5434                                flat.push(a.clone());
5435                            }
5436                        }
5437                        let s = match self.interp.perl_sprintf_stringify(&fmt, &flat, self.line()) {
5438                            Ok(s) => s,
5439                            Err(FlowOrError::Error(e)) => return Err(e),
5440                            Err(FlowOrError::Flow(_)) => {
5441                                return Err(StrykeError::runtime(
5442                                    "printf: unexpected control flow",
5443                                    self.line(),
5444                                ));
5445                            }
5446                        };
5447                        let handle_name = match handle_idx {
5448                            Some(idx) => self.interp.resolve_io_handle_name(
5449                                self.names
5450                                    .get(*idx as usize)
5451                                    .map_or("STDOUT", |s| s.as_str()),
5452                            ),
5453                            None => self
5454                                .interp
5455                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
5456                        };
5457                        self.interp
5458                            .write_formatted_print(handle_name.as_str(), &s, self.line())?;
5459                        self.push(StrykeValue::integer(1));
5460                        Ok(())
5461                    }
5462                    Op::Say(handle_idx, argc) => {
5463                        if (self.interp.feature_bits & crate::vm_helper::FEAT_SAY) == 0 {
5464                            return Err(StrykeError::runtime(
5465                            "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
5466                            self.line(),
5467                        ));
5468                        }
5469                        let argc = *argc as usize;
5470                        let mut args = Vec::with_capacity(argc);
5471                        for _ in 0..argc {
5472                            args.push(self.pop());
5473                        }
5474                        args.reverse();
5475                        let mut output = String::new();
5476                        if args.is_empty() {
5477                            let topic = self.interp.scope.get_scalar("_").clone();
5478                            let s = match self.interp.stringify_value(topic, self.line()) {
5479                                Ok(s) => s,
5480                                Err(FlowOrError::Error(e)) => return Err(e),
5481                                Err(FlowOrError::Flow(_)) => {
5482                                    return Err(StrykeError::runtime(
5483                                        "say: unexpected control flow",
5484                                        self.line(),
5485                                    ));
5486                                }
5487                            };
5488                            output.push_str(&s);
5489                        } else {
5490                            for (i, arg) in args.iter().enumerate() {
5491                                if i > 0 && !self.interp.ofs.is_empty() {
5492                                    output.push_str(&self.interp.ofs);
5493                                }
5494                                for item in arg.to_list() {
5495                                    let s = match self.interp.stringify_value(item, self.line()) {
5496                                        Ok(s) => s,
5497                                        Err(FlowOrError::Error(e)) => return Err(e),
5498                                        Err(FlowOrError::Flow(_)) => {
5499                                            return Err(StrykeError::runtime(
5500                                                "say: unexpected control flow",
5501                                                self.line(),
5502                                            ));
5503                                        }
5504                                    };
5505                                    output.push_str(&s);
5506                                }
5507                            }
5508                        }
5509                        output.push('\n');
5510                        output.push_str(&self.interp.ors);
5511                        let handle_name = match handle_idx {
5512                            Some(idx) => self.interp.resolve_io_handle_name(
5513                                self.names
5514                                    .get(*idx as usize)
5515                                    .map_or("STDOUT", |s| s.as_str()),
5516                            ),
5517                            None => self
5518                                .interp
5519                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
5520                        };
5521                        self.interp.write_formatted_print(
5522                            handle_name.as_str(),
5523                            &output,
5524                            self.line(),
5525                        )?;
5526                        self.push(StrykeValue::integer(1));
5527                        Ok(())
5528                    }
5529
5530                    // ── Built-in dispatch ──
5531                    Op::CallBuiltin(id, argc) => {
5532                        let argc = *argc as usize;
5533                        let mut args = Vec::with_capacity(argc);
5534                        for _ in 0..argc {
5535                            args.push(self.pop());
5536                        }
5537                        args.reverse();
5538                        let result = self.exec_builtin(*id, args)?;
5539                        self.push(result);
5540                        Ok(())
5541                    }
5542                    Op::WantarrayPush(wa) => {
5543                        self.wantarray_stack.push(self.interp.wantarray_kind);
5544                        self.interp.wantarray_kind = WantarrayCtx::from_byte(*wa);
5545                        Ok(())
5546                    }
5547                    Op::WantarrayPop => {
5548                        self.interp.wantarray_kind =
5549                            self.wantarray_stack.pop().unwrap_or(WantarrayCtx::Scalar);
5550                        Ok(())
5551                    }
5552
5553                    // ── List / Range ──
5554                    Op::MakeArray(n) => {
5555                        let n = *n as usize;
5556                        // Pops are last-to-first on the stack; reverse to source (left-to-right) order,
5557                        // then flatten nested arrays in place (Perl list literal semantics).
5558                        // Hashes flatten to alternating key/value entries — Perl's
5559                        // `(%a, %b)` splat-merge idiom relies on this; without it
5560                        // each hash collapses to its scalar bucket-fill string.
5561                        let mut stack_vals = Vec::with_capacity(n);
5562                        for _ in 0..n {
5563                            stack_vals.push(self.pop());
5564                        }
5565                        stack_vals.reverse();
5566                        let mut arr = Vec::new();
5567                        for v in stack_vals {
5568                            if let Some(items) = v.as_array_vec() {
5569                                arr.extend(items);
5570                            } else if let Some(map) = v.as_hash_map() {
5571                                for (k, vv) in map {
5572                                    arr.push(StrykeValue::string(k));
5573                                    arr.push(vv);
5574                                }
5575                            } else {
5576                                arr.push(v);
5577                            }
5578                        }
5579                        self.push(StrykeValue::array(arr));
5580                        Ok(())
5581                    }
5582                    Op::HashSliceDeref(n) => {
5583                        let n = *n as usize;
5584                        let mut key_vals = Vec::with_capacity(n);
5585                        for _ in 0..n {
5586                            key_vals.push(self.pop());
5587                        }
5588                        key_vals.reverse();
5589                        let container = self.pop();
5590                        let line = self.line();
5591                        let out = vm_interp_result(
5592                            self.interp
5593                                .hash_slice_deref_values(&container, &key_vals, line),
5594                            line,
5595                        )?;
5596                        self.push(out);
5597                        Ok(())
5598                    }
5599                    Op::ArrowArraySlice(n) => {
5600                        let n = *n as usize;
5601                        let idxs = self.pop_flattened_array_slice_specs(n);
5602                        let r = self.pop();
5603                        let line = self.line();
5604                        let out = vm_interp_result(
5605                            self.interp.arrow_array_slice_values(r, &idxs, line),
5606                            line,
5607                        )?;
5608                        self.push(out);
5609                        Ok(())
5610                    }
5611                    Op::SetHashSliceDeref(n) => {
5612                        let n = *n as usize;
5613                        let mut key_vals = Vec::with_capacity(n);
5614                        for _ in 0..n {
5615                            key_vals.push(self.pop());
5616                        }
5617                        key_vals.reverse();
5618                        let container = self.pop();
5619                        let val = self.pop();
5620                        let line = self.line();
5621                        vm_interp_result(
5622                            self.interp
5623                                .assign_hash_slice_deref(container, key_vals, val, line),
5624                            line,
5625                        )?;
5626                        Ok(())
5627                    }
5628                    Op::SetHashSlice(hash_idx, n) => {
5629                        let n = *n as usize;
5630                        let mut key_vals = Vec::with_capacity(n);
5631                        for _ in 0..n {
5632                            key_vals.push(self.pop());
5633                        }
5634                        key_vals.reverse();
5635                        let name = names[*hash_idx as usize].as_str();
5636                        self.require_hash_mutable(name)?;
5637                        let val = self.pop();
5638                        let line = self.line();
5639                        vm_interp_result(
5640                            self.interp
5641                                .assign_named_hash_slice(name, key_vals, val, line),
5642                            line,
5643                        )?;
5644                        Ok(())
5645                    }
5646                    Op::GetHashSlice(hash_idx, n) => {
5647                        let n = *n as usize;
5648                        let mut key_vals = Vec::with_capacity(n);
5649                        for _ in 0..n {
5650                            key_vals.push(self.pop());
5651                        }
5652                        key_vals.reverse();
5653                        let name = names[*hash_idx as usize].as_str();
5654                        let h = self.interp.scope.get_hash(name);
5655                        let mut result = Vec::new();
5656                        for kv in &key_vals {
5657                            // Flatten arrays AND arrayrefs (e.g. `@h{@$kref}`)
5658                            // — both shapes can carry the keys list.
5659                            if let Some(vv) = kv.as_array_vec() {
5660                                for v in vv {
5661                                    let k = v.to_string();
5662                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
5663                                }
5664                            } else if let Some(r) = kv.as_array_ref() {
5665                                for v in r.read().iter() {
5666                                    let k = v.to_string();
5667                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
5668                                }
5669                            } else {
5670                                let k = kv.to_string();
5671                                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
5672                            }
5673                        }
5674                        self.push(StrykeValue::array(result));
5675                        Ok(())
5676                    }
5677                    Op::HashSliceDerefCompound(op_byte, n) => {
5678                        let n = *n as usize;
5679                        let mut key_vals = Vec::with_capacity(n);
5680                        for _ in 0..n {
5681                            key_vals.push(self.pop());
5682                        }
5683                        key_vals.reverse();
5684                        let container = self.pop();
5685                        let rhs = self.pop();
5686                        let line = self.line();
5687                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5688                            .ok_or_else(|| {
5689                                crate::error::StrykeError::runtime(
5690                                    "VM: HashSliceDerefCompound: bad op byte",
5691                                    line,
5692                                )
5693                            })?;
5694                        let new_val = vm_interp_result(
5695                            self.interp.compound_assign_hash_slice_deref(
5696                                container, key_vals, op, rhs, line,
5697                            ),
5698                            line,
5699                        )?;
5700                        self.push(new_val);
5701                        Ok(())
5702                    }
5703                    Op::HashSliceDerefIncDec(kind, n) => {
5704                        let n = *n as usize;
5705                        let mut key_vals = Vec::with_capacity(n);
5706                        for _ in 0..n {
5707                            key_vals.push(self.pop());
5708                        }
5709                        key_vals.reverse();
5710                        let container = self.pop();
5711                        let line = self.line();
5712                        let out = vm_interp_result(
5713                            self.interp
5714                                .hash_slice_deref_inc_dec(container, key_vals, *kind, line),
5715                            line,
5716                        )?;
5717                        self.push(out);
5718                        Ok(())
5719                    }
5720                    Op::NamedHashSliceCompound(op_byte, hash_idx, n) => {
5721                        let n = *n as usize;
5722                        let mut key_vals = Vec::with_capacity(n);
5723                        for _ in 0..n {
5724                            key_vals.push(self.pop());
5725                        }
5726                        key_vals.reverse();
5727                        let name = names[*hash_idx as usize].as_str();
5728                        self.require_hash_mutable(name)?;
5729                        let rhs = self.pop();
5730                        let line = self.line();
5731                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5732                            .ok_or_else(|| {
5733                                crate::error::StrykeError::runtime(
5734                                    "VM: NamedHashSliceCompound: bad op byte",
5735                                    line,
5736                                )
5737                            })?;
5738                        let new_val = vm_interp_result(
5739                            self.interp
5740                                .compound_assign_named_hash_slice(name, key_vals, op, rhs, line),
5741                            line,
5742                        )?;
5743                        self.push(new_val);
5744                        Ok(())
5745                    }
5746                    Op::NamedHashSliceIncDec(kind, hash_idx, n) => {
5747                        let n = *n as usize;
5748                        let mut key_vals = Vec::with_capacity(n);
5749                        for _ in 0..n {
5750                            key_vals.push(self.pop());
5751                        }
5752                        key_vals.reverse();
5753                        let name = names[*hash_idx as usize].as_str();
5754                        self.require_hash_mutable(name)?;
5755                        let line = self.line();
5756                        let out = vm_interp_result(
5757                            self.interp
5758                                .named_hash_slice_inc_dec(name, key_vals, *kind, line),
5759                            line,
5760                        )?;
5761                        self.push(out);
5762                        Ok(())
5763                    }
5764                    Op::NamedHashSlicePeekLast(hash_idx, n) => {
5765                        let n = *n as usize;
5766                        let line = self.line();
5767                        let name = names[*hash_idx as usize].as_str();
5768                        self.require_hash_mutable(name)?;
5769                        let len = self.stack.len();
5770                        if len < n {
5771                            return Err(StrykeError::runtime(
5772                                "VM: NamedHashSlicePeekLast: stack underflow",
5773                                line,
5774                            ));
5775                        }
5776                        let base = len - n;
5777                        let key_vals: Vec<StrykeValue> = self.stack[base..base + n].to_vec();
5778                        let ks = Self::flatten_hash_slice_key_slots(&key_vals);
5779                        let last_k = ks.last().ok_or_else(|| {
5780                            StrykeError::runtime("VM: NamedHashSlicePeekLast: empty key list", line)
5781                        })?;
5782                        self.interp.touch_env_hash(name);
5783                        let cur = self.interp.scope.get_hash_element(name, last_k.as_str());
5784                        self.push(cur);
5785                        Ok(())
5786                    }
5787                    Op::NamedHashSliceDropKeysKeepCur(n) => {
5788                        let n = *n as usize;
5789                        let cur = self.pop();
5790                        for _ in 0..n {
5791                            self.pop();
5792                        }
5793                        self.push(cur);
5794                        Ok(())
5795                    }
5796                    Op::SetNamedHashSliceLastKeep(hash_idx, n) => {
5797                        let n = *n as usize;
5798                        let line = self.line();
5799                        let name = names[*hash_idx as usize].as_str();
5800                        self.require_hash_mutable(name)?;
5801                        let mut key_vals_rev = Vec::with_capacity(n);
5802                        for _ in 0..n {
5803                            key_vals_rev.push(self.pop());
5804                        }
5805                        key_vals_rev.reverse();
5806                        let mut val = self.pop();
5807                        if let Some(av) = val.as_array_vec() {
5808                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5809                        }
5810                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
5811                        let last_k = ks.last().ok_or_else(|| {
5812                            StrykeError::runtime(
5813                                "VM: SetNamedHashSliceLastKeep: empty key list",
5814                                line,
5815                            )
5816                        })?;
5817                        let val_keep = val.clone();
5818                        self.interp.touch_env_hash(name);
5819                        vm_interp_result(
5820                            self.interp
5821                                .scope
5822                                .set_hash_element(name, last_k.as_str(), val)
5823                                .map(|()| StrykeValue::UNDEF)
5824                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
5825                            line,
5826                        )?;
5827                        self.push(val_keep);
5828                        Ok(())
5829                    }
5830                    Op::HashSliceDerefPeekLast(n) => {
5831                        let n = *n as usize;
5832                        let line = self.line();
5833                        let len = self.stack.len();
5834                        if len < n + 1 {
5835                            return Err(StrykeError::runtime(
5836                                "VM: HashSliceDerefPeekLast: stack underflow",
5837                                line,
5838                            ));
5839                        }
5840                        let base = len - n - 1;
5841                        let container = self.stack[base].clone();
5842                        let key_vals: Vec<StrykeValue> =
5843                            self.stack[base + 1..base + 1 + n].to_vec();
5844                        let list = vm_interp_result(
5845                            self.interp
5846                                .hash_slice_deref_values(&container, &key_vals, line),
5847                            line,
5848                        )?;
5849                        let cur = list.to_list().last().cloned().unwrap_or(StrykeValue::UNDEF);
5850                        self.push(cur);
5851                        Ok(())
5852                    }
5853                    Op::HashSliceDerefRollValUnderKeys(n) => {
5854                        let n = *n as usize;
5855                        let val = self.pop();
5856                        let mut keys_rev = Vec::with_capacity(n);
5857                        for _ in 0..n {
5858                            keys_rev.push(self.pop());
5859                        }
5860                        let container = self.pop();
5861                        keys_rev.reverse();
5862                        self.push(val);
5863                        self.push(container);
5864                        for k in keys_rev {
5865                            self.push(k);
5866                        }
5867                        Ok(())
5868                    }
5869                    Op::HashSliceDerefSetLastKeep(n) => {
5870                        let n = *n as usize;
5871                        let line = self.line();
5872                        let mut key_vals_rev = Vec::with_capacity(n);
5873                        for _ in 0..n {
5874                            key_vals_rev.push(self.pop());
5875                        }
5876                        key_vals_rev.reverse();
5877                        let container = self.pop();
5878                        let mut val = self.pop();
5879                        if let Some(av) = val.as_array_vec() {
5880                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5881                        }
5882                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
5883                        let last_k = ks.last().ok_or_else(|| {
5884                            StrykeError::runtime(
5885                                "VM: HashSliceDerefSetLastKeep: empty key list",
5886                                line,
5887                            )
5888                        })?;
5889                        let val_keep = val.clone();
5890                        vm_interp_result(
5891                            self.interp.assign_hash_slice_one_key(
5892                                container,
5893                                last_k.as_str(),
5894                                val,
5895                                line,
5896                            ),
5897                            line,
5898                        )?;
5899                        self.push(val_keep);
5900                        Ok(())
5901                    }
5902                    Op::HashSliceDerefDropKeysKeepCur(n) => {
5903                        let n = *n as usize;
5904                        let cur = self.pop();
5905                        for _ in 0..n {
5906                            self.pop();
5907                        }
5908                        let _container = self.pop();
5909                        self.push(cur);
5910                        Ok(())
5911                    }
5912                    Op::SetArrowArraySlice(n) => {
5913                        let n = *n as usize;
5914                        let idxs = self.pop_flattened_array_slice_specs(n);
5915                        let aref = self.pop();
5916                        let val = self.pop();
5917                        let line = self.line();
5918                        vm_interp_result(
5919                            self.interp.assign_arrow_array_slice(aref, idxs, val, line),
5920                            line,
5921                        )?;
5922                        Ok(())
5923                    }
5924                    Op::ArrowArraySliceCompound(op_byte, n) => {
5925                        let n = *n as usize;
5926                        let idxs = self.pop_flattened_array_slice_specs(n);
5927                        let aref = self.pop();
5928                        let rhs = self.pop();
5929                        let line = self.line();
5930                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5931                            .ok_or_else(|| {
5932                                crate::error::StrykeError::runtime(
5933                                    "VM: ArrowArraySliceCompound: bad op byte",
5934                                    line,
5935                                )
5936                            })?;
5937                        let new_val = vm_interp_result(
5938                            self.interp
5939                                .compound_assign_arrow_array_slice(aref, idxs, op, rhs, line),
5940                            line,
5941                        )?;
5942                        self.push(new_val);
5943                        Ok(())
5944                    }
5945                    Op::ArrowArraySliceIncDec(kind, n) => {
5946                        let n = *n as usize;
5947                        let idxs = self.pop_flattened_array_slice_specs(n);
5948                        let aref = self.pop();
5949                        let line = self.line();
5950                        let out = vm_interp_result(
5951                            self.interp
5952                                .arrow_array_slice_inc_dec(aref, idxs, *kind, line),
5953                            line,
5954                        )?;
5955                        self.push(out);
5956                        Ok(())
5957                    }
5958                    Op::ArrowArraySlicePeekLast(n) => {
5959                        let n = *n as usize;
5960                        let line = self.line();
5961                        let len = self.stack.len();
5962                        if len < n + 1 {
5963                            return Err(StrykeError::runtime(
5964                                "VM: ArrowArraySlicePeekLast: stack underflow",
5965                                line,
5966                            ));
5967                        }
5968                        let base = len - n - 1;
5969                        let aref = self.stack[base].clone();
5970                        let idxs =
5971                            self.flatten_array_slice_specs_ordered_values(&self.stack[base + 1..])?;
5972                        let last = *idxs.last().ok_or_else(|| {
5973                            StrykeError::runtime(
5974                                "VM: ArrowArraySlicePeekLast: empty index list",
5975                                line,
5976                            )
5977                        })?;
5978                        let cur = vm_interp_result(
5979                            self.interp.read_arrow_array_element(aref, last, line),
5980                            line,
5981                        )?;
5982                        self.push(cur);
5983                        Ok(())
5984                    }
5985                    Op::ArrowArraySliceDropKeysKeepCur(n) => {
5986                        let n = *n as usize;
5987                        let cur = self.pop();
5988                        let _idxs = self.pop_flattened_array_slice_specs(n);
5989                        let _aref = self.pop();
5990                        self.push(cur);
5991                        Ok(())
5992                    }
5993                    Op::ArrowArraySliceRollValUnderSpecs(n) => {
5994                        let n = *n as usize;
5995                        let val = self.pop();
5996                        let mut specs_rev = Vec::with_capacity(n);
5997                        for _ in 0..n {
5998                            specs_rev.push(self.pop());
5999                        }
6000                        let aref = self.pop();
6001                        self.push(val);
6002                        self.push(aref);
6003                        for s in specs_rev.into_iter().rev() {
6004                            self.push(s);
6005                        }
6006                        Ok(())
6007                    }
6008                    Op::SetArrowArraySliceLastKeep(n) => {
6009                        let n = *n as usize;
6010                        let line = self.line();
6011                        let idxs = self.pop_flattened_array_slice_specs(n);
6012                        let aref = self.pop();
6013                        let mut val = self.pop();
6014                        // RHS is compiled in list context (`(3,4)` → one array value); Perl assigns
6015                        // only the **last** list element to the last slice index (`||=` / `&&=` / `//=`).
6016                        if let Some(av) = val.as_array_vec() {
6017                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
6018                        }
6019                        let last = *idxs.last().ok_or_else(|| {
6020                            StrykeError::runtime(
6021                                "VM: SetArrowArraySliceLastKeep: empty index list",
6022                                line,
6023                            )
6024                        })?;
6025                        let val_keep = val.clone();
6026                        vm_interp_result(
6027                            self.interp.assign_arrow_array_deref(aref, last, val, line),
6028                            line,
6029                        )?;
6030                        self.push(val_keep);
6031                        Ok(())
6032                    }
6033                    Op::NamedArraySliceIncDec(kind, arr_idx, n) => {
6034                        let n = *n as usize;
6035                        let idxs = self.pop_flattened_array_slice_specs(n);
6036                        let name = names[*arr_idx as usize].as_str();
6037                        self.require_array_mutable(name)?;
6038                        let line = self.line();
6039                        let out = vm_interp_result(
6040                            self.interp
6041                                .named_array_slice_inc_dec(name, idxs, *kind, line),
6042                            line,
6043                        )?;
6044                        self.push(out);
6045                        Ok(())
6046                    }
6047                    Op::NamedArraySliceCompound(op_byte, arr_idx, n) => {
6048                        let n = *n as usize;
6049                        let idxs = self.pop_flattened_array_slice_specs(n);
6050                        let name = names[*arr_idx as usize].as_str();
6051                        self.require_array_mutable(name)?;
6052                        let rhs = self.pop();
6053                        let line = self.line();
6054                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
6055                            .ok_or_else(|| {
6056                                crate::error::StrykeError::runtime(
6057                                    "VM: NamedArraySliceCompound: bad op byte",
6058                                    line,
6059                                )
6060                            })?;
6061                        let new_val = vm_interp_result(
6062                            self.interp
6063                                .compound_assign_named_array_slice(name, idxs, op, rhs, line),
6064                            line,
6065                        )?;
6066                        self.push(new_val);
6067                        Ok(())
6068                    }
6069                    Op::NamedArraySlicePeekLast(arr_idx, n) => {
6070                        let n = *n as usize;
6071                        let line = self.line();
6072                        let name = names[*arr_idx as usize].as_str();
6073                        self.require_array_mutable(name)?;
6074                        let len = self.stack.len();
6075                        if len < n {
6076                            return Err(StrykeError::runtime(
6077                                "VM: NamedArraySlicePeekLast: stack underflow",
6078                                line,
6079                            ));
6080                        }
6081                        let base = len - n;
6082                        let idxs =
6083                            self.flatten_array_slice_specs_ordered_values(&self.stack[base..])?;
6084                        let last = *idxs.last().ok_or_else(|| {
6085                            StrykeError::runtime(
6086                                "VM: NamedArraySlicePeekLast: empty index list",
6087                                line,
6088                            )
6089                        })?;
6090                        let cur = self.interp.scope.get_array_element(name, last);
6091                        self.push(cur);
6092                        Ok(())
6093                    }
6094                    Op::NamedArraySliceDropKeysKeepCur(n) => {
6095                        let n = *n as usize;
6096                        let cur = self.pop();
6097                        let _idxs = self.pop_flattened_array_slice_specs(n);
6098                        self.push(cur);
6099                        Ok(())
6100                    }
6101                    Op::NamedArraySliceRollValUnderSpecs(n) => {
6102                        let n = *n as usize;
6103                        let val = self.pop();
6104                        let mut specs_rev = Vec::with_capacity(n);
6105                        for _ in 0..n {
6106                            specs_rev.push(self.pop());
6107                        }
6108                        self.push(val);
6109                        for s in specs_rev.into_iter().rev() {
6110                            self.push(s);
6111                        }
6112                        Ok(())
6113                    }
6114                    Op::SetNamedArraySliceLastKeep(arr_idx, n) => {
6115                        let n = *n as usize;
6116                        let line = self.line();
6117                        let idxs = self.pop_flattened_array_slice_specs(n);
6118                        let name = names[*arr_idx as usize].as_str();
6119                        self.require_array_mutable(name)?;
6120                        let mut val = self.pop();
6121                        if let Some(av) = val.as_array_vec() {
6122                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
6123                        }
6124                        let last = *idxs.last().ok_or_else(|| {
6125                            StrykeError::runtime(
6126                                "VM: SetNamedArraySliceLastKeep: empty index list",
6127                                line,
6128                            )
6129                        })?;
6130                        let val_keep = val.clone();
6131                        vm_interp_result(
6132                            self.interp
6133                                .scope
6134                                .set_array_element(name, last, val)
6135                                .map(|()| StrykeValue::UNDEF)
6136                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
6137                            line,
6138                        )?;
6139                        self.push(val_keep);
6140                        Ok(())
6141                    }
6142                    Op::SetNamedArraySlice(arr_idx, n) => {
6143                        let n = *n as usize;
6144                        let idxs = self.pop_flattened_array_slice_specs(n);
6145                        let name = names[*arr_idx as usize].as_str();
6146                        self.require_array_mutable(name)?;
6147                        let val = self.pop();
6148                        let line = self.line();
6149                        vm_interp_result(
6150                            self.interp.assign_named_array_slice(name, idxs, val, line),
6151                            line,
6152                        )?;
6153                        Ok(())
6154                    }
6155                    Op::MakeHash(n) => {
6156                        let n = *n as usize;
6157                        let mut items = Vec::with_capacity(n);
6158                        for _ in 0..n {
6159                            items.push(self.pop());
6160                        }
6161                        items.reverse();
6162                        let mut map = IndexMap::new();
6163                        let mut i = 0;
6164                        while i + 1 < items.len() {
6165                            map.insert(items[i].to_string(), items[i + 1].clone());
6166                            i += 2;
6167                        }
6168                        self.push(StrykeValue::hash(map));
6169                        Ok(())
6170                    }
6171                    Op::Range => {
6172                        let to = self.pop();
6173                        let from = self.pop();
6174                        let arr = perl_list_range_expand(from, to);
6175                        self.push(StrykeValue::array(arr));
6176                        Ok(())
6177                    }
6178                    Op::RangeStep => {
6179                        let step = self.pop();
6180                        let to = self.pop();
6181                        let from = self.pop();
6182                        let arr = crate::value::perl_list_range_expand_stepped(from, to, step);
6183                        self.push(StrykeValue::array(arr));
6184                        Ok(())
6185                    }
6186                    Op::ArraySliceRange(arr_idx) => {
6187                        let step = self.pop();
6188                        let to = self.pop();
6189                        let from = self.pop();
6190                        let line = self.line();
6191                        let name = names[*arr_idx as usize].as_str();
6192                        // Stryke topic-string slice: `_[from:to:step]` parses
6193                        // to ArraySliceRange on a `__topicstr__N` name.
6194                        if let Some(real) = name.strip_prefix("__topicstr__") {
6195                            let s = self.interp.scope.get_scalar(real).to_string();
6196                            let chars: Vec<char> = s.chars().collect();
6197                            let n = chars.len() as i64;
6198                            let step_i = if step.is_undef() { 1 } else { step.to_int() };
6199                            // Open slices: defaults depend on step direction
6200                            // step > 0: from=0, to=n-1 (forward)
6201                            // step < 0: from=n-1, to=0 (backward)
6202                            let mut from_i = if from.is_undef() {
6203                                if step_i >= 0 {
6204                                    0
6205                                } else {
6206                                    n - 1
6207                                }
6208                            } else {
6209                                from.to_int()
6210                            };
6211                            let mut to_i = if to.is_undef() {
6212                                if step_i >= 0 {
6213                                    n - 1
6214                                } else {
6215                                    0
6216                                }
6217                            } else {
6218                                to.to_int()
6219                            };
6220                            if from_i < 0 {
6221                                from_i += n
6222                            }
6223                            if to_i < 0 {
6224                                to_i += n
6225                            }
6226                            let mut out = String::new();
6227                            if step_i > 0 {
6228                                let mut i = from_i;
6229                                while i <= to_i && i < n {
6230                                    if i >= 0 {
6231                                        out.push(chars[i as usize]);
6232                                    }
6233                                    i += step_i;
6234                                }
6235                            } else if step_i < 0 {
6236                                let mut i = from_i;
6237                                while i >= to_i && i >= 0 {
6238                                    if i < n {
6239                                        out.push(chars[i as usize]);
6240                                    }
6241                                    i += step_i;
6242                                }
6243                            }
6244                            self.push(StrykeValue::string(out));
6245                            return Ok(());
6246                        }
6247                        let arr_len = self.interp.scope.array_len(name) as i64;
6248                        // Stryke string-slice sugar: when `@name` is empty
6249                        // (or doesn't exist) but `$name` is a non-empty
6250                        // string, treat `$name[from:to:step]` as Python-style
6251                        // substring slice. Returns a *string*, not an array.
6252                        if !crate::compat_mode()
6253                            && arr_len == 0
6254                            && self.interp.scope.scalar_binding_exists(name)
6255                        {
6256                            let s = self.interp.scope.get_scalar(name).to_string();
6257                            if !s.is_empty() {
6258                                let chars: Vec<char> = s.chars().collect();
6259                                let n = chars.len() as i64;
6260                                let step_i = if step.is_undef() { 1 } else { step.to_int() };
6261                                // Open slices: defaults depend on step direction
6262                                // step > 0: from=0, to=n-1 (forward)
6263                                // step < 0: from=n-1, to=0 (backward)
6264                                let mut from_i = if from.is_undef() {
6265                                    if step_i >= 0 {
6266                                        0
6267                                    } else {
6268                                        n - 1
6269                                    }
6270                                } else {
6271                                    from.to_int()
6272                                };
6273                                let mut to_i = if to.is_undef() {
6274                                    if step_i >= 0 {
6275                                        n - 1
6276                                    } else {
6277                                        0
6278                                    }
6279                                } else {
6280                                    to.to_int()
6281                                };
6282                                if from_i < 0 {
6283                                    from_i += n
6284                                }
6285                                if to_i < 0 {
6286                                    to_i += n
6287                                }
6288                                let mut out = String::new();
6289                                if step_i > 0 {
6290                                    let mut i = from_i;
6291                                    while i <= to_i && i < n {
6292                                        if i >= 0 {
6293                                            out.push(chars[i as usize]);
6294                                        }
6295                                        i += step_i;
6296                                    }
6297                                } else if step_i < 0 {
6298                                    let mut i = from_i;
6299                                    while i >= to_i && i >= 0 {
6300                                        if i < n {
6301                                            out.push(chars[i as usize]);
6302                                        }
6303                                        i += step_i;
6304                                    }
6305                                }
6306                                self.push(StrykeValue::string(out));
6307                                return Ok(());
6308                            }
6309                        }
6310                        let indices = match crate::value::compute_array_slice_indices(
6311                            arr_len, &from, &to, &step,
6312                        ) {
6313                            Ok(v) => v,
6314                            Err(msg) => {
6315                                return Err(StrykeError::runtime(msg, line));
6316                            }
6317                        };
6318                        let mut out = Vec::with_capacity(indices.len());
6319                        for i in indices {
6320                            out.push(self.interp.scope.get_array_element(name, i));
6321                        }
6322                        self.push(StrykeValue::array(out));
6323                        Ok(())
6324                    }
6325                    Op::HashSliceRange(hash_idx) => {
6326                        let step = self.pop();
6327                        let to = self.pop();
6328                        let from = self.pop();
6329                        let line = self.line();
6330                        let name = names[*hash_idx as usize].as_str();
6331                        let keys = match crate::value::compute_hash_slice_keys(&from, &to, &step) {
6332                            Ok(v) => v,
6333                            Err(msg) => {
6334                                return Err(StrykeError::runtime(msg, line));
6335                            }
6336                        };
6337                        let h = self.interp.scope.get_hash(name);
6338                        let mut out = Vec::with_capacity(keys.len());
6339                        for k in &keys {
6340                            out.push(h.get(k).cloned().unwrap_or(StrykeValue::UNDEF));
6341                        }
6342                        self.push(StrykeValue::array(out));
6343                        Ok(())
6344                    }
6345                    Op::ScalarFlipFlop(slot, exclusive) => {
6346                        let to = self.pop().to_int();
6347                        let from = self.pop().to_int();
6348                        let line = self.line();
6349                        let v = vm_interp_result(
6350                            self.interp
6351                                .scalar_flip_flop_eval(from, to, *slot as usize, *exclusive != 0)
6352                                .map_err(Into::into),
6353                            line,
6354                        )?;
6355                        self.push(v);
6356                        Ok(())
6357                    }
6358                    Op::RegexFlipFlop(slot, exclusive, lp, lf, rp, rf) => {
6359                        let line = self.line();
6360                        let left_pat = constants[*lp as usize].as_str_or_empty();
6361                        let left_flags = constants[*lf as usize].as_str_or_empty();
6362                        let right_pat = constants[*rp as usize].as_str_or_empty();
6363                        let right_flags = constants[*rf as usize].as_str_or_empty();
6364                        let v = vm_interp_result(
6365                            self.interp
6366                                .regex_flip_flop_eval(
6367                                    left_pat.as_str(),
6368                                    left_flags.as_str(),
6369                                    right_pat.as_str(),
6370                                    right_flags.as_str(),
6371                                    *slot as usize,
6372                                    *exclusive != 0,
6373                                    line,
6374                                )
6375                                .map_err(Into::into),
6376                            line,
6377                        )?;
6378                        self.push(v);
6379                        Ok(())
6380                    }
6381                    Op::RegexEofFlipFlop(slot, exclusive, lp, lf) => {
6382                        let line = self.line();
6383                        let left_pat = constants[*lp as usize].as_str_or_empty();
6384                        let left_flags = constants[*lf as usize].as_str_or_empty();
6385                        let v = vm_interp_result(
6386                            self.interp
6387                                .regex_eof_flip_flop_eval(
6388                                    left_pat.as_str(),
6389                                    left_flags.as_str(),
6390                                    *slot as usize,
6391                                    *exclusive != 0,
6392                                    line,
6393                                )
6394                                .map_err(Into::into),
6395                            line,
6396                        )?;
6397                        self.push(v);
6398                        Ok(())
6399                    }
6400                    Op::RegexFlipFlopExprRhs(slot, exclusive, lp, lf, rhs_idx) => {
6401                        let idx = *rhs_idx as usize;
6402                        let line = self.line();
6403                        let right_m = if let Some(&(start, end)) = self
6404                            .regex_flip_flop_rhs_expr_bytecode_ranges
6405                            .get(idx)
6406                            .and_then(|r| r.as_ref())
6407                        {
6408                            let val = self.run_block_region(start, end, op_count)?;
6409                            val.is_true()
6410                        } else {
6411                            let e = &self.regex_flip_flop_rhs_expr_entries[idx];
6412                            match self.interp.eval_boolean_rvalue_condition(e) {
6413                                Ok(b) => b,
6414                                Err(FlowOrError::Error(err)) => return Err(err),
6415                                Err(FlowOrError::Flow(_)) => {
6416                                    return Err(StrykeError::runtime(
6417                                        "unexpected flow in regex flip-flop RHS",
6418                                        line,
6419                                    ))
6420                                }
6421                            }
6422                        };
6423                        let left_pat = constants[*lp as usize].as_str_or_empty();
6424                        let left_flags = constants[*lf as usize].as_str_or_empty();
6425                        let v = vm_interp_result(
6426                            self.interp
6427                                .regex_flip_flop_eval_dynamic_right(
6428                                    left_pat.as_str(),
6429                                    left_flags.as_str(),
6430                                    *slot as usize,
6431                                    *exclusive != 0,
6432                                    line,
6433                                    right_m,
6434                                )
6435                                .map_err(Into::into),
6436                            line,
6437                        )?;
6438                        self.push(v);
6439                        Ok(())
6440                    }
6441                    Op::RegexFlipFlopDotLineRhs(slot, exclusive, lp, lf, line_cidx) => {
6442                        let line = self.line();
6443                        let rhs_line = constants[*line_cidx as usize].to_int();
6444                        let left_pat = constants[*lp as usize].as_str_or_empty();
6445                        let left_flags = constants[*lf as usize].as_str_or_empty();
6446                        let v = vm_interp_result(
6447                            self.interp
6448                                .regex_flip_flop_eval_dot_line_rhs(
6449                                    left_pat.as_str(),
6450                                    left_flags.as_str(),
6451                                    *slot as usize,
6452                                    *exclusive != 0,
6453                                    line,
6454                                    rhs_line,
6455                                )
6456                                .map_err(Into::into),
6457                            line,
6458                        )?;
6459                        self.push(v);
6460                        Ok(())
6461                    }
6462
6463                    // ── Regex ──
6464                    Op::RegexMatch(pat_idx, flags_idx, scalar_g, pos_key_idx) => {
6465                        let val = self.pop();
6466                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
6467                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6468                        let line = self.line();
6469                        if val.is_iterator() {
6470                            let source = crate::map_stream::into_pull_iter(val);
6471                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
6472                                Ok(r) => r,
6473                                Err(FlowOrError::Error(e)) => return Err(e),
6474                                Err(FlowOrError::Flow(_)) => {
6475                                    return Err(StrykeError::runtime(
6476                                        "unexpected flow in regex compile",
6477                                        line,
6478                                    ));
6479                                }
6480                            };
6481                            let global = flags.contains('g');
6482                            if global {
6483                                self.push(StrykeValue::iterator(std::sync::Arc::new(
6484                                    crate::map_stream::MatchGlobalStreamIterator::new(source, re),
6485                                )));
6486                            } else {
6487                                self.push(StrykeValue::iterator(std::sync::Arc::new(
6488                                    crate::map_stream::MatchStreamIterator::new(source, re),
6489                                )));
6490                            }
6491                            return Ok(());
6492                        }
6493                        let string = val.into_string();
6494                        let pos_key_owned = if *pos_key_idx == u16::MAX {
6495                            None
6496                        } else {
6497                            Some(constants[*pos_key_idx as usize].as_str_or_empty())
6498                        };
6499                        let pos_key: &str = pos_key_owned.as_deref().unwrap_or("_");
6500                        match self
6501                            .interp
6502                            .regex_match_execute(string, &pattern, &flags, *scalar_g, pos_key, line)
6503                        {
6504                            Ok(v) => {
6505                                self.push(v);
6506                                Ok(())
6507                            }
6508                            Err(FlowOrError::Error(e)) => Err(e),
6509                            Err(FlowOrError::Flow(_)) => {
6510                                Err(StrykeError::runtime("unexpected flow in regex match", line))
6511                            }
6512                        }
6513                    }
6514                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lvalue_idx) => {
6515                        let val = self.pop();
6516                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
6517                        let replacement = constants[*repl_idx as usize].as_str_or_empty();
6518                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6519                        let line = self.line();
6520                        if val.is_iterator() {
6521                            let source = crate::map_stream::into_pull_iter(val);
6522                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
6523                                Ok(r) => r,
6524                                Err(FlowOrError::Error(e)) => return Err(e),
6525                                Err(FlowOrError::Flow(_)) => {
6526                                    return Err(StrykeError::runtime(
6527                                        "unexpected flow in regex compile",
6528                                        line,
6529                                    ));
6530                                }
6531                            };
6532                            let global = flags.contains('g');
6533                            self.push(StrykeValue::iterator(std::sync::Arc::new(
6534                                crate::map_stream::SubstStreamIterator::new(
6535                                    source,
6536                                    re,
6537                                    crate::vm_helper::normalize_replacement_backrefs(&replacement),
6538                                    global,
6539                                ),
6540                            )));
6541                            return Ok(());
6542                        }
6543                        let string = val.into_string();
6544                        let target = &self.lvalues[*lvalue_idx as usize];
6545                        match self.interp.regex_subst_execute(
6546                            string,
6547                            &pattern,
6548                            &replacement,
6549                            &flags,
6550                            target,
6551                            line,
6552                        ) {
6553                            Ok(v) => {
6554                                self.push(v);
6555                                Ok(())
6556                            }
6557                            Err(FlowOrError::Error(e)) => Err(e),
6558                            Err(FlowOrError::Flow(_)) => {
6559                                Err(StrykeError::runtime("unexpected flow in s///", line))
6560                            }
6561                        }
6562                    }
6563                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lvalue_idx) => {
6564                        let val = self.pop();
6565                        let from = constants[*from_idx as usize].as_str_or_empty();
6566                        let to = constants[*to_idx as usize].as_str_or_empty();
6567                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6568                        let line = self.line();
6569                        if val.is_iterator() {
6570                            let source = crate::map_stream::into_pull_iter(val);
6571                            self.push(StrykeValue::iterator(std::sync::Arc::new(
6572                                crate::map_stream::TransliterateStreamIterator::new(
6573                                    source, &from, &to, &flags,
6574                                ),
6575                            )));
6576                            return Ok(());
6577                        }
6578                        let string = val.into_string();
6579                        let target = &self.lvalues[*lvalue_idx as usize];
6580                        match self
6581                            .interp
6582                            .regex_transliterate_execute(string, &from, &to, &flags, target, line)
6583                        {
6584                            Ok(v) => {
6585                                self.push(v);
6586                                Ok(())
6587                            }
6588                            Err(FlowOrError::Error(e)) => Err(e),
6589                            Err(FlowOrError::Flow(_)) => {
6590                                Err(StrykeError::runtime("unexpected flow in tr///", line))
6591                            }
6592                        }
6593                    }
6594                    Op::RegexMatchDyn(negate) => {
6595                        let rhs = self.pop();
6596                        let s = self.pop().into_string();
6597                        let line = self.line();
6598                        let exec = if let Some((pat, fl)) = rhs.regex_src_and_flags() {
6599                            self.interp
6600                                .regex_match_execute(s, &pat, &fl, false, "_", line)
6601                        } else {
6602                            let pattern = rhs.into_string();
6603                            self.interp
6604                                .regex_match_execute(s, &pattern, "", false, "_", line)
6605                        };
6606                        match exec {
6607                            Ok(v) => {
6608                                let matched = v.is_true();
6609                                let out = if *negate { !matched } else { matched };
6610                                self.push(StrykeValue::integer(if out { 1 } else { 0 }));
6611                            }
6612                            Err(FlowOrError::Error(e)) => return Err(e),
6613                            Err(FlowOrError::Flow(_)) => {
6614                                return Err(StrykeError::runtime("unexpected flow in =~", line));
6615                            }
6616                        }
6617                        Ok(())
6618                    }
6619                    Op::RegexBoolToScalar => {
6620                        let v = self.pop();
6621                        self.push(if v.is_true() {
6622                            StrykeValue::integer(1)
6623                        } else {
6624                            StrykeValue::string(String::new())
6625                        });
6626                        Ok(())
6627                    }
6628                    Op::SetRegexPos => {
6629                        let key = self.pop().to_string();
6630                        let val = self.pop();
6631                        if val.is_undef() {
6632                            self.interp.regex_pos.insert(key, None);
6633                        } else {
6634                            let u = val.to_int().max(0) as usize;
6635                            self.interp.regex_pos.insert(key, Some(u));
6636                        }
6637                        Ok(())
6638                    }
6639                    Op::LoadRegex(pat_idx, flags_idx) => {
6640                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
6641                        let flags = constants[*flags_idx as usize].as_str_or_empty();
6642                        let line = self.line();
6643                        let pattern_owned = pattern.clone();
6644                        let re = match self.interp.compile_regex(&pattern, &flags, line) {
6645                            Ok(r) => r,
6646                            Err(FlowOrError::Error(e)) => return Err(e),
6647                            Err(FlowOrError::Flow(_)) => {
6648                                return Err(StrykeError::runtime(
6649                                    "unexpected flow in qr// compile",
6650                                    line,
6651                                ));
6652                            }
6653                        };
6654                        self.push(StrykeValue::regex(re, pattern_owned, flags.to_string()));
6655                        Ok(())
6656                    }
6657                    Op::ConcatAppend(idx) => {
6658                        let rhs = self.pop();
6659                        let n = names[*idx as usize].as_str();
6660                        let line = self.line();
6661                        let result = self
6662                            .interp
6663                            .scope
6664                            .scalar_concat_inplace(n, &rhs)
6665                            .map_err(|e| e.at_line(line))?;
6666                        self.push(result);
6667                        Ok(())
6668                    }
6669                    Op::ConcatAppendSlot(slot) => {
6670                        let rhs = self.pop();
6671                        let result = self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
6672                        self.push(result);
6673                        Ok(())
6674                    }
6675                    Op::ConcatAppendSlotVoid(slot) => {
6676                        let rhs = self.pop();
6677                        self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
6678                        Ok(())
6679                    }
6680                    Op::SlotLtIntJumpIfFalse(slot, limit, target) => {
6681                        let val = self.interp.scope.get_scalar_slot(*slot);
6682                        let lt = if let Some(i) = val.as_integer() {
6683                            i < *limit as i64
6684                        } else {
6685                            val.to_number() < *limit as f64
6686                        };
6687                        if !lt {
6688                            self.ip = *target;
6689                        }
6690                        Ok(())
6691                    }
6692                    Op::SlotIncLtIntJumpBack(slot, limit, body_target) => {
6693                        // Fused trailing `++$slot; goto top_test` for the bench_loop shape:
6694                        // matches `PreIncSlotVoid` + `Jump` + top `SlotLtIntJumpIfFalse` exactly so
6695                        // coercion, wrap-around, and integer-only write semantics line up byte-for-byte
6696                        // with the un-fused form. Every iteration past the first skips the top check
6697                        // and the unconditional jump entirely.
6698                        let next_i = self
6699                            .interp
6700                            .scope
6701                            .get_scalar_slot(*slot)
6702                            .to_int()
6703                            .wrapping_add(1);
6704                        self.interp
6705                            .scope
6706                            .set_scalar_slot(*slot, StrykeValue::integer(next_i));
6707                        if next_i < *limit as i64 {
6708                            self.ip = *body_target;
6709                        }
6710                        Ok(())
6711                    }
6712                    Op::AccumSumLoop(sum_slot, i_slot, limit) => {
6713                        // Runs the entire counted `while $i < limit { $sum += $i; $i += 1 }` loop in
6714                        // native Rust. The peephole only fires when the body is exactly this one
6715                        // accumulate statement, so every side effect is captured by the final
6716                        // `$sum` and `$i` writes; there is nothing else to do per iteration.
6717                        let mut sum = self.interp.scope.get_scalar_slot(*sum_slot).to_int();
6718                        let mut i = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6719                        let limit = *limit as i64;
6720                        while i < limit {
6721                            sum = sum.wrapping_add(i);
6722                            i = i.wrapping_add(1);
6723                        }
6724                        self.interp
6725                            .scope
6726                            .set_scalar_slot(*sum_slot, StrykeValue::integer(sum));
6727                        self.interp
6728                            .scope
6729                            .set_scalar_slot(*i_slot, StrykeValue::integer(i));
6730                        Ok(())
6731                    }
6732                    Op::AddHashElemPlainKeyToSlot(sum_slot, k_name_idx, h_name_idx) => {
6733                        // `$sum += $h{$k}` — single-dispatch slot += hash[name-scalar] with no
6734                        // VM stack traffic. The key scalar is read via plain (name-based) access
6735                        // because the compiler's `for my $k (keys %h)` lowering currently backs
6736                        // `$k` with a frame scalar, not a slot.
6737                        let k_name = names[*k_name_idx as usize].as_str();
6738                        let h_name = names[*h_name_idx as usize].as_str();
6739                        self.interp.touch_env_hash(h_name);
6740                        let key = self.interp.scope.get_scalar(k_name).to_string();
6741                        let elem = self.interp.scope.get_hash_element(h_name, &key);
6742                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
6743                        let new_v =
6744                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
6745                                StrykeValue::integer(a.wrapping_add(b))
6746                            } else {
6747                                StrykeValue::float(cur.to_number() + elem.to_number())
6748                            };
6749                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
6750                        Ok(())
6751                    }
6752                    Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_name_idx) => {
6753                        // `$sum += $h{$k}` — slot counter, slot key, slot sum. Zero name lookups
6754                        // for `$sum` and `$k`; one frame-walk for `%h` (same as the non-slot form).
6755                        let h_name = names[*h_name_idx as usize].as_str();
6756                        self.interp.touch_env_hash(h_name);
6757                        let key_val = self.interp.scope.get_scalar_slot(*k_slot);
6758                        let key = key_val.to_string();
6759                        let elem = self.interp.scope.get_hash_element(h_name, &key);
6760                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
6761                        let new_v =
6762                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
6763                                StrykeValue::integer(a.wrapping_add(b))
6764                            } else {
6765                                StrykeValue::float(cur.to_number() + elem.to_number())
6766                            };
6767                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
6768                        Ok(())
6769                    }
6770                    Op::SumHashValuesToSlot(sum_slot, h_name_idx) => {
6771                        // `for my $k (keys %h) { $sum += $h{$k} }` fused to a single op that walks
6772                        // `hash.values()` in a tight native loop. No key stringification, no stack
6773                        // traffic, no per-iter dispatch. The foreach body reduced to
6774                        // `AddHashElemSlotKeyToSlot`, so this fusion is correct regardless of `$k`
6775                        // slot assignment — we never read `$k`.
6776                        let h_name = names[*h_name_idx as usize].as_str();
6777                        self.interp.touch_env_hash(h_name);
6778                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
6779                        let mut int_acc: i64 = cur.as_integer().unwrap_or(0);
6780                        let mut float_acc: f64 = 0.0;
6781                        let mut is_int = cur.as_integer().is_some();
6782                        if !is_int {
6783                            float_acc = cur.to_number();
6784                        }
6785                        // Walk the hash via the scope's borrow path without cloning the whole
6786                        // IndexMap. `for_each_hash_value` takes a visitor so the lock (if any) is
6787                        // held once rather than per-element.
6788                        self.interp.scope.for_each_hash_value(h_name, |v| {
6789                            if is_int {
6790                                if let Some(x) = v.as_integer() {
6791                                    int_acc = int_acc.wrapping_add(x);
6792                                    return;
6793                                }
6794                                float_acc = int_acc as f64;
6795                                is_int = false;
6796                            }
6797                            float_acc += v.to_number();
6798                        });
6799                        let new_v = if is_int {
6800                            StrykeValue::integer(int_acc)
6801                        } else {
6802                            StrykeValue::float(float_acc)
6803                        };
6804                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
6805                        Ok(())
6806                    }
6807                    Op::SetHashIntTimesLoop(h_name_idx, i_slot, k, limit) => {
6808                        // Runs the counted `while $i < limit { $h{$i} = $i * k; $i += 1 }` loop
6809                        // natively: the hash is `reserve()`d once, keys are stringified via
6810                        // `itoa` (no `format!` allocation), and values are inserted in a tight
6811                        // Rust loop. `$i` is left at `limit` on exit, matching the un-fused shape.
6812                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6813                        let lim = *limit as i64;
6814                        if i_cur < lim {
6815                            let n = names[*h_name_idx as usize].as_str();
6816                            self.require_hash_mutable(n)?;
6817                            self.interp.touch_env_hash(n);
6818                            let line = self.line();
6819                            self.interp
6820                                .scope
6821                                .set_hash_int_times_range(n, i_cur, lim, *k as i64)
6822                                .map_err(|e| e.at_line(line))?;
6823                        }
6824                        self.interp
6825                            .scope
6826                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6827                        Ok(())
6828                    }
6829                    Op::PushIntRangeToArrayLoop(arr_name_idx, i_slot, limit) => {
6830                        // Runs the entire counted `while $i < limit { push @arr, $i; $i += 1 }`
6831                        // loop in native Rust. The array's `Vec<StrykeValue>` is reserved once and
6832                        // `push(StrykeValue::integer(i))` runs in a tight Rust loop — no per-iter
6833                        // op dispatch, no `require_array_mutable` check per iter.
6834                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6835                        let lim = *limit as i64;
6836                        if i_cur < lim {
6837                            let n = names[*arr_name_idx as usize].as_str();
6838                            self.require_array_mutable(n)?;
6839                            let line = self.line();
6840                            self.interp
6841                                .scope
6842                                .push_int_range_to_array(n, i_cur, lim)
6843                                .map_err(|e| e.at_line(line))?;
6844                        }
6845                        self.interp
6846                            .scope
6847                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6848                        Ok(())
6849                    }
6850                    Op::ConcatConstSlotLoop(const_idx, s_slot, i_slot, limit) => {
6851                        // Runs the entire counted `while $i < limit { $s .= CONST; $i += 1 }` loop
6852                        // in native Rust. We stringify the constant once, reserve `(limit-i_cur) *
6853                        // const.len()` up front so the owning `String` reallocs at most twice, then
6854                        // `push_str` in a tight loop (see `try_concat_repeat_inplace`). Falls back
6855                        // to the per-iteration slow path when the slot is not the sole owner of a
6856                        // heap `String` — `.=` semantics match the un-fused shape byte-for-byte.
6857                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6858                        let lim = *limit as i64;
6859                        if i_cur < lim {
6860                            let n_iters = (lim - i_cur) as usize;
6861                            let rhs = constants[*const_idx as usize].as_str_or_empty();
6862                            if !self
6863                                .interp
6864                                .scope
6865                                .scalar_slot_concat_repeat_inplace(*s_slot, &rhs, n_iters)
6866                            {
6867                                self.interp
6868                                    .scope
6869                                    .scalar_slot_concat_repeat_slow(*s_slot, &rhs, n_iters);
6870                            }
6871                        }
6872                        self.interp
6873                            .scope
6874                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6875                        Ok(())
6876                    }
6877                    Op::AddAssignSlotSlot(dst, src) => {
6878                        let a = self.interp.scope.get_scalar_slot(*dst);
6879                        let b = self.interp.scope.get_scalar_slot(*src);
6880                        let result = crate::value::compat_add(&a, &b);
6881                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6882                        self.push(result);
6883                        Ok(())
6884                    }
6885                    Op::AddAssignSlotSlotVoid(dst, src) => {
6886                        let a = self.interp.scope.get_scalar_slot(*dst);
6887                        let b = self.interp.scope.get_scalar_slot(*src);
6888                        let result = crate::value::compat_add(&a, &b);
6889                        self.interp.scope.set_scalar_slot(*dst, result);
6890                        Ok(())
6891                    }
6892                    Op::SubAssignSlotSlot(dst, src) => {
6893                        let a = self.interp.scope.get_scalar_slot(*dst);
6894                        let b = self.interp.scope.get_scalar_slot(*src);
6895                        let result = crate::value::compat_sub(&a, &b);
6896                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6897                        self.push(result);
6898                        Ok(())
6899                    }
6900                    Op::MulAssignSlotSlot(dst, src) => {
6901                        let a = self.interp.scope.get_scalar_slot(*dst);
6902                        let b = self.interp.scope.get_scalar_slot(*src);
6903                        let result = crate::value::compat_mul(&a, &b);
6904                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6905                        self.push(result);
6906                        Ok(())
6907                    }
6908
6909                    // ── Frame-local scalar slots (O(1), no string lookup) ──
6910                    Op::GetScalarSlot(slot) => {
6911                        let val = self.interp.scope.get_scalar_slot(*slot);
6912                        self.push(val);
6913                        Ok(())
6914                    }
6915                    Op::SetScalarSlot(slot) => {
6916                        let val = self.pop();
6917                        self.interp
6918                            .scope
6919                            .set_scalar_slot_checked(*slot, val, None)
6920                            .map_err(|e| e.at_line(self.line()))?;
6921                        Ok(())
6922                    }
6923                    Op::SetScalarSlotKeep(slot) => {
6924                        let val = self.peek().dup_stack();
6925                        self.interp
6926                            .scope
6927                            .set_scalar_slot_checked(*slot, val, None)
6928                            .map_err(|e| e.at_line(self.line()))?;
6929                        Ok(())
6930                    }
6931                    Op::DeclareScalarSlot(slot, name_idx) => {
6932                        let val = self.pop();
6933                        let name_opt = if *name_idx == u16::MAX {
6934                            None
6935                        } else {
6936                            Some(names[*name_idx as usize].as_str())
6937                        };
6938                        self.interp.scope.declare_scalar_slot(*slot, val, name_opt);
6939                        Ok(())
6940                    }
6941                    Op::GetArg(idx) => {
6942                        // Read argument from caller's stack region without @_ allocation.
6943                        let val = if let Some(frame) = self.call_stack.last() {
6944                            let arg_pos = frame.stack_base + *idx as usize;
6945                            self.stack
6946                                .get(arg_pos)
6947                                .cloned()
6948                                .unwrap_or(StrykeValue::UNDEF)
6949                        } else {
6950                            StrykeValue::UNDEF
6951                        };
6952                        self.push(val);
6953                        Ok(())
6954                    }
6955
6956                    Op::ReadIntoVar(name_idx) => {
6957                        let length = self.pop().to_int() as usize;
6958                        let fh_val = self.pop();
6959                        let name = &names[*name_idx as usize];
6960                        let line = self.line();
6961                        let result = vm_interp_result(
6962                            self.interp.builtin_read_into(fh_val, name, length, line),
6963                            line,
6964                        )?;
6965                        self.push(result);
6966                        Ok(())
6967                    }
6968                    Op::ChompInPlace(lvalue_idx) => {
6969                        let val = self.pop();
6970                        let target = &self.lvalues[*lvalue_idx as usize];
6971                        let line = self.line();
6972                        match self.interp.chomp_inplace_execute(val, target) {
6973                            Ok(v) => self.push(v),
6974                            Err(FlowOrError::Error(e)) => return Err(e),
6975                            Err(FlowOrError::Flow(_)) => {
6976                                return Err(StrykeError::runtime("unexpected flow in chomp", line));
6977                            }
6978                        }
6979                        Ok(())
6980                    }
6981                    Op::ChopInPlace(lvalue_idx) => {
6982                        let val = self.pop();
6983                        let target = &self.lvalues[*lvalue_idx as usize];
6984                        let line = self.line();
6985                        match self.interp.chop_inplace_execute(val, target) {
6986                            Ok(v) => self.push(v),
6987                            Err(FlowOrError::Error(e)) => return Err(e),
6988                            Err(FlowOrError::Flow(_)) => {
6989                                return Err(StrykeError::runtime("unexpected flow in chop", line));
6990                            }
6991                        }
6992                        Ok(())
6993                    }
6994                    Op::SubstrFourArg(idx) => {
6995                        let (string_e, offset_e, length_e, rep_e) =
6996                            &self.substr_four_arg_entries[*idx as usize];
6997                        let v = vm_interp_result(
6998                            self.interp.eval_substr_expr(
6999                                string_e,
7000                                offset_e,
7001                                length_e.as_ref(),
7002                                Some(rep_e),
7003                                self.line(),
7004                            ),
7005                            self.line(),
7006                        )?;
7007                        self.push(v);
7008                        Ok(())
7009                    }
7010                    Op::KeysExpr(idx) => {
7011                        let i = *idx as usize;
7012                        let line = self.line();
7013                        let v = if let Some(&(start, end)) = self
7014                            .keys_expr_bytecode_ranges
7015                            .get(i)
7016                            .and_then(|r| r.as_ref())
7017                        {
7018                            let val = self.run_block_region(start, end, op_count)?;
7019                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
7020                        } else {
7021                            let e = &self.keys_expr_entries[i];
7022                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
7023                        };
7024                        self.push(v);
7025                        Ok(())
7026                    }
7027                    Op::KeysExprScalar(idx) => {
7028                        let i = *idx as usize;
7029                        let line = self.line();
7030                        let v = if let Some(&(start, end)) = self
7031                            .keys_expr_bytecode_ranges
7032                            .get(i)
7033                            .and_then(|r| r.as_ref())
7034                        {
7035                            let val = self.run_block_region(start, end, op_count)?;
7036                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
7037                        } else {
7038                            let e = &self.keys_expr_entries[i];
7039                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
7040                        };
7041                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
7042                        self.push(StrykeValue::integer(n));
7043                        Ok(())
7044                    }
7045                    Op::ValuesExpr(idx) => {
7046                        let i = *idx as usize;
7047                        let line = self.line();
7048                        let v = if let Some(&(start, end)) = self
7049                            .values_expr_bytecode_ranges
7050                            .get(i)
7051                            .and_then(|r| r.as_ref())
7052                        {
7053                            let val = self.run_block_region(start, end, op_count)?;
7054                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
7055                        } else {
7056                            let e = &self.values_expr_entries[i];
7057                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
7058                        };
7059                        self.push(v);
7060                        Ok(())
7061                    }
7062                    Op::ValuesExprScalar(idx) => {
7063                        let i = *idx as usize;
7064                        let line = self.line();
7065                        let v = if let Some(&(start, end)) = self
7066                            .values_expr_bytecode_ranges
7067                            .get(i)
7068                            .and_then(|r| r.as_ref())
7069                        {
7070                            let val = self.run_block_region(start, end, op_count)?;
7071                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
7072                        } else {
7073                            let e = &self.values_expr_entries[i];
7074                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
7075                        };
7076                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
7077                        self.push(StrykeValue::integer(n));
7078                        Ok(())
7079                    }
7080                    Op::DeleteExpr(idx) => {
7081                        let e = &self.delete_expr_entries[*idx as usize];
7082                        let v = vm_interp_result(
7083                            self.interp.eval_delete_operand(e, self.line()),
7084                            self.line(),
7085                        )?;
7086                        self.push(v);
7087                        Ok(())
7088                    }
7089                    Op::ExistsExpr(idx) => {
7090                        let e = &self.exists_expr_entries[*idx as usize];
7091                        let v = vm_interp_result(
7092                            self.interp.eval_exists_operand(e, self.line()),
7093                            self.line(),
7094                        )?;
7095                        self.push(v);
7096                        Ok(())
7097                    }
7098                    Op::PushExpr(idx) => {
7099                        let (array, values) = &self.push_expr_entries[*idx as usize];
7100                        let v = vm_interp_result(
7101                            self.interp
7102                                .eval_push_expr(array, values.as_slice(), self.line()),
7103                            self.line(),
7104                        )?;
7105                        self.push(v);
7106                        Ok(())
7107                    }
7108                    Op::PopExpr(idx) => {
7109                        let e = &self.pop_expr_entries[*idx as usize];
7110                        let v = vm_interp_result(
7111                            self.interp.eval_pop_expr(e, self.line()),
7112                            self.line(),
7113                        )?;
7114                        self.push(v);
7115                        Ok(())
7116                    }
7117                    Op::ShiftExpr(idx) => {
7118                        let e = &self.shift_expr_entries[*idx as usize];
7119                        let v = vm_interp_result(
7120                            self.interp.eval_shift_expr(e, self.line()),
7121                            self.line(),
7122                        )?;
7123                        self.push(v);
7124                        Ok(())
7125                    }
7126                    Op::UnshiftExpr(idx) => {
7127                        let (array, values) = &self.unshift_expr_entries[*idx as usize];
7128                        let v = vm_interp_result(
7129                            self.interp
7130                                .eval_unshift_expr(array, values.as_slice(), self.line()),
7131                            self.line(),
7132                        )?;
7133                        self.push(v);
7134                        Ok(())
7135                    }
7136                    Op::SpliceExpr(idx) => {
7137                        let (array, offset, length, replacement) =
7138                            &self.splice_expr_entries[*idx as usize];
7139                        let v = vm_interp_result(
7140                            self.interp.eval_splice_expr(
7141                                array,
7142                                offset.as_ref(),
7143                                length.as_ref(),
7144                                replacement.as_slice(),
7145                                self.interp.wantarray_kind,
7146                                self.line(),
7147                            ),
7148                            self.line(),
7149                        )?;
7150                        self.push(v);
7151                        Ok(())
7152                    }
7153
7154                    // ── References ──
7155                    Op::MakeScalarRef => {
7156                        let val = self.pop();
7157                        self.push(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))));
7158                        Ok(())
7159                    }
7160                    Op::MakeScalarBindingRef(name_idx) => {
7161                        let name = names[*name_idx as usize].clone();
7162                        self.push(StrykeValue::scalar_binding_ref(name));
7163                        Ok(())
7164                    }
7165                    Op::MakeArrayBindingRef(name_idx) => {
7166                        let name = &names[*name_idx as usize];
7167                        // Promote the scope's array to shared Arc-backed storage.
7168                        // Both the scope and the returned ref share the same Arc,
7169                        // so mutations through either path are visible.
7170                        let arc = self.interp.scope.promote_array_to_shared(name);
7171                        self.push(StrykeValue::array_ref(arc));
7172                        Ok(())
7173                    }
7174                    Op::MakeHashBindingRef(name_idx) => {
7175                        let name = &names[*name_idx as usize];
7176                        // Lazy-init hook: `\%all` / `\%parameters` / `\%main::`
7177                        // bypass `Op::GetHash`, so without this call the
7178                        // reference is taken before the hash is populated and
7179                        // the user gets an empty hashref.
7180                        self.interp.touch_env_hash(name);
7181                        let arc = self.interp.scope.promote_hash_to_shared(name);
7182                        self.push(StrykeValue::hash_ref(arc));
7183                        Ok(())
7184                    }
7185                    Op::MakeArrayRefAlias => {
7186                        let v = self.pop();
7187                        let line = self.line();
7188                        let out =
7189                            vm_interp_result(self.interp.make_array_ref_alias(v, line), line)?;
7190                        self.push(out);
7191                        Ok(())
7192                    }
7193                    Op::MakeHashRefAlias => {
7194                        let v = self.pop();
7195                        let line = self.line();
7196                        let out = vm_interp_result(self.interp.make_hash_ref_alias(v, line), line)?;
7197                        self.push(out);
7198                        Ok(())
7199                    }
7200                    Op::MakeArrayRef => {
7201                        let val = self.pop();
7202                        let val = self.interp.scope.resolve_container_binding_ref(val);
7203                        let arr = if let Some(a) = val.as_array_vec() {
7204                            a
7205                        } else {
7206                            vec![val]
7207                        };
7208                        self.push(StrykeValue::array_ref(Arc::new(RwLock::new(arr))));
7209                        Ok(())
7210                    }
7211                    Op::MakeHashRef => {
7212                        let val = self.pop();
7213                        let map = if let Some(h) = val.as_hash_map() {
7214                            h
7215                        } else {
7216                            let items = val.to_list();
7217                            let mut m = IndexMap::new();
7218                            let mut i = 0;
7219                            while i + 1 < items.len() {
7220                                m.insert(items[i].to_string(), items[i + 1].clone());
7221                                i += 2;
7222                            }
7223                            m
7224                        };
7225                        self.push(StrykeValue::hash_ref(Arc::new(RwLock::new(map))));
7226                        Ok(())
7227                    }
7228                    Op::MakeCodeRef(block_idx, sig_idx) => {
7229                        let block = self.blocks[*block_idx as usize].clone();
7230                        let params = self.code_ref_sigs[*sig_idx as usize].clone();
7231                        let captured = self.interp.scope.capture();
7232                        self.push(StrykeValue::code_ref(Arc::new(crate::value::StrykeSub {
7233                            name: "__ANON__".to_string(),
7234                            params,
7235                            body: block,
7236                            closure_env: Some(captured),
7237                            prototype: None,
7238                            fib_like: None,
7239                        })));
7240                        Ok(())
7241                    }
7242                    Op::LoadNamedSubRef(name_idx) => {
7243                        let name = names[*name_idx as usize].as_str();
7244                        let line = self.line();
7245                        let sub = self.interp.resolve_sub_by_name(name).ok_or_else(|| {
7246                            StrykeError::runtime(
7247                                self.interp.undefined_subroutine_resolve_message(name),
7248                                line,
7249                            )
7250                        })?;
7251                        self.push(StrykeValue::code_ref(sub));
7252                        Ok(())
7253                    }
7254                    Op::LoadDynamicSubRef => {
7255                        let name = self.pop().to_string();
7256                        let line = self.line();
7257                        let sub = self.interp.resolve_sub_by_name(&name).ok_or_else(|| {
7258                            StrykeError::runtime(
7259                                self.interp.undefined_subroutine_resolve_message(&name),
7260                                line,
7261                            )
7262                        })?;
7263                        self.push(StrykeValue::code_ref(sub));
7264                        Ok(())
7265                    }
7266                    Op::LoadDynamicTypeglob => {
7267                        let name = self.pop().to_string();
7268                        let n = self.interp.resolve_io_handle_name(&name);
7269                        self.push(StrykeValue::string(n));
7270                        Ok(())
7271                    }
7272                    Op::CopyTypeglobSlots(lhs_i, rhs_i) => {
7273                        let lhs = self.names[*lhs_i as usize].as_str();
7274                        let rhs = self.names[*rhs_i as usize].as_str();
7275                        let line = self.line();
7276                        self.interp
7277                            .copy_typeglob_slots(lhs, rhs, line)
7278                            .map_err(|e| e.at_line(line))?;
7279                        Ok(())
7280                    }
7281                    Op::TypeglobAssignFromValue(name_idx) => {
7282                        let val = self.pop();
7283                        let name = self.names[*name_idx as usize].as_str();
7284                        let line = self.line();
7285                        vm_interp_result(
7286                            self.interp.assign_typeglob_value(name, val.clone(), line),
7287                            line,
7288                        )?;
7289                        self.push(val);
7290                        Ok(())
7291                    }
7292                    Op::TypeglobAssignFromValueDynamic => {
7293                        let val = self.pop();
7294                        let name = self.pop().to_string();
7295                        let line = self.line();
7296                        vm_interp_result(
7297                            self.interp.assign_typeglob_value(&name, val.clone(), line),
7298                            line,
7299                        )?;
7300                        self.push(val);
7301                        Ok(())
7302                    }
7303                    Op::CopyTypeglobSlotsDynamicLhs(rhs_i) => {
7304                        let lhs = self.pop().to_string();
7305                        let rhs = self.names[*rhs_i as usize].as_str();
7306                        let line = self.line();
7307                        self.interp
7308                            .copy_typeglob_slots(&lhs, rhs, line)
7309                            .map_err(|e| e.at_line(line))?;
7310                        Ok(())
7311                    }
7312                    Op::SymbolicDeref(kind_byte) => {
7313                        let v = self.pop();
7314                        let kind = match *kind_byte {
7315                            0 => Sigil::Scalar,
7316                            1 => Sigil::Array,
7317                            2 => Sigil::Hash,
7318                            3 => Sigil::Typeglob,
7319                            _ => {
7320                                return Err(StrykeError::runtime(
7321                                    "VM: bad SymbolicDeref kind byte",
7322                                    self.line(),
7323                                ));
7324                            }
7325                        };
7326                        let line = self.line();
7327                        let out =
7328                            vm_interp_result(self.interp.symbolic_deref(v, kind, line), line)?;
7329                        self.push(out);
7330                        Ok(())
7331                    }
7332
7333                    // ── Arrow dereference ──
7334                    Op::ArrowArray => {
7335                        let idx = self.pop().to_int();
7336                        let r = self.pop();
7337                        let line = self.line();
7338                        let v = vm_interp_result(
7339                            self.interp.read_arrow_array_element(r, idx, line),
7340                            line,
7341                        )?;
7342                        self.push(v);
7343                        Ok(())
7344                    }
7345                    Op::ArrowHash => {
7346                        let key = self.pop().to_string();
7347                        let r = self.pop();
7348                        let line = self.line();
7349                        let v = vm_interp_result(
7350                            self.interp.read_arrow_hash_element(r, key.as_str(), line),
7351                            line,
7352                        )?;
7353                        self.push(v);
7354                        Ok(())
7355                    }
7356                    Op::SetArrowHash => {
7357                        let key = self.pop().to_string();
7358                        let r = self.pop();
7359                        let val = self.pop();
7360                        let line = self.line();
7361                        vm_interp_result(
7362                            self.interp.assign_arrow_hash_deref(r, key, val, line),
7363                            line,
7364                        )?;
7365                        Ok(())
7366                    }
7367                    Op::SetArrowArray => {
7368                        let idx = self.pop().to_int();
7369                        let r = self.pop();
7370                        let val = self.pop();
7371                        let line = self.line();
7372                        vm_interp_result(
7373                            self.interp.assign_arrow_array_deref(r, idx, val, line),
7374                            line,
7375                        )?;
7376                        Ok(())
7377                    }
7378                    Op::SetArrowArrayKeep => {
7379                        let idx = self.pop().to_int();
7380                        let r = self.pop();
7381                        let val = self.pop();
7382                        let val_keep = val.clone();
7383                        let line = self.line();
7384                        vm_interp_result(
7385                            self.interp.assign_arrow_array_deref(r, idx, val, line),
7386                            line,
7387                        )?;
7388                        self.push(val_keep);
7389                        Ok(())
7390                    }
7391                    Op::SetArrowHashKeep => {
7392                        let key = self.pop().to_string();
7393                        let r = self.pop();
7394                        let val = self.pop();
7395                        let val_keep = val.clone();
7396                        let line = self.line();
7397                        vm_interp_result(
7398                            self.interp.assign_arrow_hash_deref(r, key, val, line),
7399                            line,
7400                        )?;
7401                        self.push(val_keep);
7402                        Ok(())
7403                    }
7404                    Op::ArrowArrayPostfix(b) => {
7405                        let idx = self.pop().to_int();
7406                        let r = self.pop();
7407                        let line = self.line();
7408                        let old = vm_interp_result(
7409                            self.interp.arrow_array_postfix(r, idx, *b == 1, line),
7410                            line,
7411                        )?;
7412                        self.push(old);
7413                        Ok(())
7414                    }
7415                    Op::ArrowHashPostfix(b) => {
7416                        let key = self.pop().to_string();
7417                        let r = self.pop();
7418                        let line = self.line();
7419                        let old = vm_interp_result(
7420                            self.interp.arrow_hash_postfix(r, key, *b == 1, line),
7421                            line,
7422                        )?;
7423                        self.push(old);
7424                        Ok(())
7425                    }
7426                    Op::SetSymbolicScalarRef => {
7427                        let r = self.pop();
7428                        let val = self.pop();
7429                        let line = self.line();
7430                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
7431                        Ok(())
7432                    }
7433                    Op::SetSymbolicScalarRefKeep => {
7434                        let r = self.pop();
7435                        let val = self.pop();
7436                        let val_keep = val.clone();
7437                        let line = self.line();
7438                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
7439                        self.push(val_keep);
7440                        Ok(())
7441                    }
7442                    Op::SetSymbolicArrayRef => {
7443                        let r = self.pop();
7444                        let val = self.pop();
7445                        let line = self.line();
7446                        vm_interp_result(
7447                            self.interp.assign_symbolic_array_ref_deref(r, val, line),
7448                            line,
7449                        )?;
7450                        Ok(())
7451                    }
7452                    Op::SetSymbolicHashRef => {
7453                        let r = self.pop();
7454                        let val = self.pop();
7455                        let line = self.line();
7456                        vm_interp_result(
7457                            self.interp.assign_symbolic_hash_ref_deref(r, val, line),
7458                            line,
7459                        )?;
7460                        Ok(())
7461                    }
7462                    Op::SetSymbolicTypeglobRef => {
7463                        let r = self.pop();
7464                        let val = self.pop();
7465                        let line = self.line();
7466                        vm_interp_result(
7467                            self.interp.assign_symbolic_typeglob_ref_deref(r, val, line),
7468                            line,
7469                        )?;
7470                        Ok(())
7471                    }
7472                    Op::SymbolicScalarRefPostfix(b) => {
7473                        let r = self.pop();
7474                        let line = self.line();
7475                        let old = vm_interp_result(
7476                            self.interp.symbolic_scalar_ref_postfix(r, *b == 1, line),
7477                            line,
7478                        )?;
7479                        self.push(old);
7480                        Ok(())
7481                    }
7482                    Op::ArrowCall(wa) => {
7483                        let want = WantarrayCtx::from_byte(*wa);
7484                        let args_val = self.pop();
7485                        let r = self.pop();
7486                        // Auto-deref ScalarRef so closures that captured $f can call $f->()
7487                        let r = if let Some(inner) = r.as_scalar_ref() {
7488                            inner.read().clone()
7489                        } else {
7490                            r
7491                        };
7492                        let args = args_val.to_list();
7493                        if let Some(sub) = r.as_code_ref() {
7494                            // Higher-order function wrappers (comp, partial, memoize, etc.)
7495                            // have empty bodies + magic closure_env keys. Dispatch them via
7496                            // the interpreter's try_hof_dispatch before falling through to
7497                            // the normal body execution path.
7498                            if let Some(hof_result) =
7499                                self.interp.try_hof_dispatch(&sub, &args, want, self.line())
7500                            {
7501                                let v = vm_interp_result(hof_result, self.line())?;
7502                                self.push(v);
7503                                return Ok(());
7504                            }
7505                            self.interp.current_sub_stack.push(sub.clone());
7506                            let saved_wa = self.interp.wantarray_kind;
7507                            self.interp.wantarray_kind = want;
7508                            self.interp.scope_push_hook();
7509                            self.interp.scope.declare_array("_", args.clone());
7510                            if let Some(ref env) = sub.closure_env {
7511                                self.interp.scope.restore_capture(env);
7512                            }
7513                            let line = self.line();
7514                            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
7515                            self.interp
7516                                .apply_sub_signature(sub.as_ref(), &argv, line)
7517                                .map_err(|e| e.at_line(line))?;
7518                            self.interp.scope.declare_array("_", argv.clone());
7519                            // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
7520                            self.interp.scope.set_closure_args(&argv);
7521                            let result = self.interp.exec_block_no_scope(&sub.body);
7522                            self.interp.wantarray_kind = saved_wa;
7523                            self.interp.scope_pop_hook();
7524                            self.interp.current_sub_stack.pop();
7525                            match result {
7526                                Ok(v) => self.push(v),
7527                                Err(crate::vm_helper::FlowOrError::Flow(
7528                                    crate::vm_helper::Flow::Return(v),
7529                                )) => self.push(v),
7530                                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
7531                                Err(_) => self.push(StrykeValue::UNDEF),
7532                            }
7533                        } else {
7534                            return Err(StrykeError::runtime("Not a code reference", self.line()));
7535                        }
7536                        Ok(())
7537                    }
7538                    Op::IndirectCall(argc, wa, pass_flag) => {
7539                        let want = WantarrayCtx::from_byte(*wa);
7540                        let line = self.line();
7541                        let arg_vals = if *pass_flag != 0 {
7542                            self.interp.scope.get_array("_")
7543                        } else {
7544                            let n = *argc as usize;
7545                            let mut args = Vec::with_capacity(n);
7546                            for _ in 0..n {
7547                                args.push(self.pop());
7548                            }
7549                            args.reverse();
7550                            args
7551                        };
7552                        let target = self.pop();
7553                        // HOF wrapper fast path (comp, partial, memoize, etc.)
7554                        if let Some(sub) = target.as_code_ref() {
7555                            if let Some(hof_result) =
7556                                self.interp.try_hof_dispatch(&sub, &arg_vals, want, line)
7557                            {
7558                                let v = vm_interp_result(hof_result, line)?;
7559                                self.push(v);
7560                                return Ok(());
7561                            }
7562                        }
7563                        let r = self
7564                            .interp
7565                            .dispatch_indirect_call(target, arg_vals, want, line);
7566                        let v = vm_interp_result(r, line)?;
7567                        self.push(v);
7568                        Ok(())
7569                    }
7570
7571                    // ── Method call ──
7572                    Op::MethodCall(name_idx, argc, wa) => {
7573                        self.run_method_op(*name_idx, *argc, *wa, false)?;
7574                        Ok(())
7575                    }
7576                    Op::MethodCallSuper(name_idx, argc, wa) => {
7577                        self.run_method_op(*name_idx, *argc, *wa, true)?;
7578                        Ok(())
7579                    }
7580
7581                    // ── File test ──
7582                    Op::FileTestOp(test) => {
7583                        let path = self.pop().to_string();
7584                        let op = *test as char;
7585                        // -M, -A, -C return fractional days (float)
7586                        if matches!(op, 'M' | 'A' | 'C') {
7587                            #[cfg(unix)]
7588                            {
7589                                let v = match crate::perl_fs::filetest_age_days(&path, op) {
7590                                    Some(days) => StrykeValue::float(days),
7591                                    None => StrykeValue::UNDEF,
7592                                };
7593                                self.push(v);
7594                                return Ok(());
7595                            }
7596                            #[cfg(not(unix))]
7597                            {
7598                                self.push(StrykeValue::UNDEF);
7599                                return Ok(());
7600                            }
7601                        }
7602                        // -s returns file size (integer)
7603                        if op == 's' {
7604                            let v = match std::fs::metadata(&path) {
7605                                Ok(m) => StrykeValue::integer(m.len() as i64),
7606                                Err(_) => StrykeValue::UNDEF,
7607                            };
7608                            self.push(v);
7609                            return Ok(());
7610                        }
7611                        let result = match op {
7612                            'e' => std::path::Path::new(&path).exists(),
7613                            'f' => std::path::Path::new(&path).is_file(),
7614                            'd' => std::path::Path::new(&path).is_dir(),
7615                            'l' => std::path::Path::new(&path).is_symlink(),
7616                            #[cfg(unix)]
7617                            'r' => crate::perl_fs::filetest_effective_access(&path, 4),
7618                            #[cfg(not(unix))]
7619                            'r' => std::fs::metadata(&path).is_ok(),
7620                            #[cfg(unix)]
7621                            'w' => crate::perl_fs::filetest_effective_access(&path, 2),
7622                            #[cfg(not(unix))]
7623                            'w' => std::fs::metadata(&path).is_ok(),
7624                            #[cfg(unix)]
7625                            'x' => crate::perl_fs::filetest_effective_access(&path, 1),
7626                            #[cfg(not(unix))]
7627                            'x' => false,
7628                            #[cfg(unix)]
7629                            'o' => crate::perl_fs::filetest_owned_effective(&path),
7630                            #[cfg(not(unix))]
7631                            'o' => false,
7632                            #[cfg(unix)]
7633                            'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
7634                            #[cfg(not(unix))]
7635                            'R' => false,
7636                            #[cfg(unix)]
7637                            'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
7638                            #[cfg(not(unix))]
7639                            'W' => false,
7640                            #[cfg(unix)]
7641                            'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
7642                            #[cfg(not(unix))]
7643                            'X' => false,
7644                            #[cfg(unix)]
7645                            'O' => crate::perl_fs::filetest_owned_real(&path),
7646                            #[cfg(not(unix))]
7647                            'O' => false,
7648                            'z' => std::fs::metadata(&path)
7649                                .map(|m| m.len() == 0)
7650                                .unwrap_or(true),
7651                            't' => crate::perl_fs::filetest_is_tty(&path),
7652                            #[cfg(unix)]
7653                            'p' => crate::perl_fs::filetest_is_pipe(&path),
7654                            #[cfg(not(unix))]
7655                            'p' => false,
7656                            #[cfg(unix)]
7657                            'S' => crate::perl_fs::filetest_is_socket(&path),
7658                            #[cfg(not(unix))]
7659                            'S' => false,
7660                            #[cfg(unix)]
7661                            'b' => crate::perl_fs::filetest_is_block_device(&path),
7662                            #[cfg(not(unix))]
7663                            'b' => false,
7664                            #[cfg(unix)]
7665                            'c' => crate::perl_fs::filetest_is_char_device(&path),
7666                            #[cfg(not(unix))]
7667                            'c' => false,
7668                            #[cfg(unix)]
7669                            'u' => crate::perl_fs::filetest_is_setuid(&path),
7670                            #[cfg(not(unix))]
7671                            'u' => false,
7672                            #[cfg(unix)]
7673                            'g' => crate::perl_fs::filetest_is_setgid(&path),
7674                            #[cfg(not(unix))]
7675                            'g' => false,
7676                            #[cfg(unix)]
7677                            'k' => crate::perl_fs::filetest_is_sticky(&path),
7678                            #[cfg(not(unix))]
7679                            'k' => false,
7680                            'T' => crate::perl_fs::filetest_is_text(&path),
7681                            'B' => crate::perl_fs::filetest_is_binary(&path),
7682                            _ => false,
7683                        };
7684                        self.push(StrykeValue::integer(if result { 1 } else { 0 }));
7685                        Ok(())
7686                    }
7687
7688                    // ── Map/Grep/Sort with blocks (opcodes when lowered; else AST block fallback) ──
7689                    Op::MapIntMul(k) => {
7690                        let list = self.pop().to_list();
7691                        if list.len() == 1 {
7692                            if let Some(p) = list[0].as_pipeline() {
7693                                let line = self.line();
7694                                let sub = VMHelper::pipeline_int_mul_sub(*k);
7695                                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
7696                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
7697                                return Ok(());
7698                            }
7699                        }
7700                        let mut result = Vec::with_capacity(list.len());
7701                        for item in list {
7702                            let n = item.to_int();
7703                            result.push(StrykeValue::integer(n.wrapping_mul(*k)));
7704                        }
7705                        self.push(StrykeValue::array(result));
7706                        Ok(())
7707                    }
7708                    Op::GrepIntModEq(m, r) => {
7709                        let list = self.pop().to_list();
7710                        let mut result = Vec::new();
7711                        for item in list {
7712                            let n = item.to_int();
7713                            if n % m == *r {
7714                                result.push(item);
7715                            }
7716                        }
7717                        self.push(StrykeValue::array(result));
7718                        Ok(())
7719                    }
7720                    Op::MapWithBlock(block_idx) => {
7721                        let list = self.pop().to_list();
7722                        self.map_with_block_common(list, *block_idx, false, op_count)
7723                    }
7724                    Op::FlatMapWithBlock(block_idx) => {
7725                        let list = self.pop().to_list();
7726                        self.map_with_block_common(list, *block_idx, true, op_count)
7727                    }
7728                    Op::MapWithExpr(expr_idx) => {
7729                        let list = self.pop().to_list();
7730                        self.map_with_expr_common(list, *expr_idx, false, op_count)
7731                    }
7732                    Op::FlatMapWithExpr(expr_idx) => {
7733                        let list = self.pop().to_list();
7734                        self.map_with_expr_common(list, *expr_idx, true, op_count)
7735                    }
7736                    Op::MapsWithBlock(block_idx) => {
7737                        let val = self.pop();
7738                        let block = self.blocks[*block_idx as usize].clone();
7739                        let out =
7740                            self.interp
7741                                .map_stream_block_output(val, &block, false, self.line())?;
7742                        self.push(out);
7743                        Ok(())
7744                    }
7745                    Op::MapsFlatMapWithBlock(block_idx) => {
7746                        let val = self.pop();
7747                        let block = self.blocks[*block_idx as usize].clone();
7748                        let out =
7749                            self.interp
7750                                .map_stream_block_output(val, &block, true, self.line())?;
7751                        self.push(out);
7752                        Ok(())
7753                    }
7754                    Op::MapsWithExpr(expr_idx) => {
7755                        let val = self.pop();
7756                        let idx = *expr_idx as usize;
7757                        let expr = self.map_expr_entries[idx].clone();
7758                        let out =
7759                            self.interp
7760                                .map_stream_expr_output(val, &expr, false, self.line())?;
7761                        self.push(out);
7762                        Ok(())
7763                    }
7764                    Op::MapsFlatMapWithExpr(expr_idx) => {
7765                        let val = self.pop();
7766                        let idx = *expr_idx as usize;
7767                        let expr = self.map_expr_entries[idx].clone();
7768                        let out =
7769                            self.interp
7770                                .map_stream_expr_output(val, &expr, true, self.line())?;
7771                        self.push(out);
7772                        Ok(())
7773                    }
7774                    Op::FilterWithBlock(block_idx) => {
7775                        let val = self.pop();
7776                        let block = self.blocks[*block_idx as usize].clone();
7777                        let out =
7778                            self.interp
7779                                .filter_stream_block_output(val, &block, self.line())?;
7780                        self.push(out);
7781                        Ok(())
7782                    }
7783                    Op::FilterWithExpr(expr_idx) => {
7784                        let val = self.pop();
7785                        let idx = *expr_idx as usize;
7786                        let expr = self.grep_expr_entries[idx].clone();
7787                        let out = self
7788                            .interp
7789                            .filter_stream_expr_output(val, &expr, self.line())?;
7790                        self.push(out);
7791                        Ok(())
7792                    }
7793                    Op::ChunkByWithBlock(block_idx) => {
7794                        let list = self.pop().to_list();
7795                        self.chunk_by_with_block_common(list, *block_idx, op_count)
7796                    }
7797                    Op::ChunkByWithExpr(expr_idx) => {
7798                        let list = self.pop().to_list();
7799                        self.chunk_by_with_expr_common(list, *expr_idx, op_count)
7800                    }
7801                    Op::GrepWithBlock(block_idx) => {
7802                        let list = self.pop().to_list();
7803                        if list.len() == 1 {
7804                            if let Some(p) = list[0].as_pipeline() {
7805                                let idx = *block_idx as usize;
7806                                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
7807                                let line = self.line();
7808                                self.interp
7809                                    .pipeline_push(&p, PipelineOp::Filter(sub), line)?;
7810                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
7811                                return Ok(());
7812                            }
7813                        }
7814                        let idx = *block_idx as usize;
7815                        // Save / restore the topic chain across the iter
7816                        // loop so this grep stage doesn't leak its final
7817                        // `_` (or chain shift) into the enclosing block.
7818                        // Mirror of the map fix above.
7819                        let saved_chain = self.interp.scope.save_topic_chain();
7820                        if let Some(&(start, end)) =
7821                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7822                        {
7823                            let mut result = Vec::new();
7824                            for item in list {
7825                                self.interp.scope.set_topic(item.clone());
7826                                let val = self.run_block_region(start, end, op_count)?;
7827                                // Bare regex → match against $_ (Perl: /pat/ in grep is $_ =~ /pat/)
7828                                let keep = if let Some(re) = val.as_regex() {
7829                                    re.is_match(&item.to_string())
7830                                } else {
7831                                    val.is_true()
7832                                };
7833                                if keep {
7834                                    result.push(item);
7835                                }
7836                            }
7837                            self.interp.scope.restore_topic_chain(saved_chain);
7838                            self.push(StrykeValue::array(result));
7839                            Ok(())
7840                        } else {
7841                            let block = self.blocks[idx].clone();
7842                            let mut result = Vec::new();
7843                            for item in list {
7844                                self.interp.scope.set_topic(item.clone());
7845                                match self.interp.exec_block(&block) {
7846                                    Ok(val) => {
7847                                        let keep = if let Some(re) = val.as_regex() {
7848                                            re.is_match(&item.to_string())
7849                                        } else {
7850                                            val.is_true()
7851                                        };
7852                                        if keep {
7853                                            result.push(item);
7854                                        }
7855                                    }
7856                                    Err(crate::vm_helper::FlowOrError::Error(e)) => {
7857                                        self.interp.scope.restore_topic_chain(saved_chain);
7858                                        return Err(e);
7859                                    }
7860                                    Err(_) => {}
7861                                }
7862                            }
7863                            self.interp.scope.restore_topic_chain(saved_chain);
7864                            self.push(StrykeValue::array(result));
7865                            Ok(())
7866                        }
7867                    }
7868                    Op::ForEachWithBlock(block_idx) => {
7869                        let val = self.pop();
7870                        let idx = *block_idx as usize;
7871                        // Save / restore the topic chain so this foreach
7872                        // doesn't leak its final `_` into the enclosing
7873                        // block (mirror of the map/grep fix above).
7874                        let saved_chain = self.interp.scope.save_topic_chain();
7875                        // Lazy iterator: consume one-at-a-time without materializing.
7876                        if val.is_iterator() {
7877                            let iter = val.into_iterator();
7878                            let mut count = 0i64;
7879                            if let Some(&(start, end)) =
7880                                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7881                            {
7882                                while let Some(item) = iter.next_item() {
7883                                    count += 1;
7884                                    self.interp.scope.set_topic(item);
7885                                    if let Err(e) = self.run_block_region(start, end, op_count) {
7886                                        self.interp.scope.restore_topic_chain(saved_chain);
7887                                        return Err(e);
7888                                    }
7889                                }
7890                            } else {
7891                                let block = self.blocks[idx].clone();
7892                                while let Some(item) = iter.next_item() {
7893                                    count += 1;
7894                                    self.interp.scope.set_topic(item);
7895                                    match self.interp.exec_block(&block) {
7896                                        Ok(_) => {}
7897                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
7898                                            self.interp.scope.restore_topic_chain(saved_chain);
7899                                            return Err(e);
7900                                        }
7901                                        Err(_) => {}
7902                                    }
7903                                }
7904                            }
7905                            self.interp.scope.restore_topic_chain(saved_chain);
7906                            self.push(StrykeValue::integer(count));
7907                            return Ok(());
7908                        }
7909                        let list = val.to_list();
7910                        let count = list.len() as i64;
7911                        if let Some(&(start, end)) =
7912                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7913                        {
7914                            for item in list {
7915                                self.interp.scope.set_topic(item);
7916                                if let Err(e) = self.run_block_region(start, end, op_count) {
7917                                    self.interp.scope.restore_topic_chain(saved_chain);
7918                                    return Err(e);
7919                                }
7920                            }
7921                        } else {
7922                            let block = self.blocks[idx].clone();
7923                            for item in list {
7924                                self.interp.scope.set_topic(item);
7925                                match self.interp.exec_block(&block) {
7926                                    Ok(_) => {}
7927                                    Err(crate::vm_helper::FlowOrError::Error(e)) => {
7928                                        self.interp.scope.restore_topic_chain(saved_chain);
7929                                        return Err(e);
7930                                    }
7931                                    Err(_) => {}
7932                                }
7933                            }
7934                        }
7935                        self.interp.scope.restore_topic_chain(saved_chain);
7936                        self.push(StrykeValue::integer(count));
7937                        Ok(())
7938                    }
7939                    Op::GrepWithExpr(expr_idx) => {
7940                        let list = self.pop().to_list();
7941                        let idx = *expr_idx as usize;
7942                        let dispatch_coderef = !crate::compat_mode();
7943                        // EXPR-form: see `map_with_expr_common` — no `{}` block
7944                        // boundary, so use `set_topic_local` (no chain shift,
7945                        // no slot 1+ zero).
7946                        if let Some(&(start, end)) = self
7947                            .grep_expr_bytecode_ranges
7948                            .get(idx)
7949                            .and_then(|r| r.as_ref())
7950                        {
7951                            let mut result = Vec::new();
7952                            for item in list {
7953                                self.interp.scope.set_topic_local(item.clone());
7954                                let val = self.run_block_region(start, end, op_count)?;
7955                                let val = self.maybe_call_coderef_with_item(
7956                                    val,
7957                                    &item,
7958                                    dispatch_coderef,
7959                                )?;
7960                                let keep = if let Some(re) = val.as_regex() {
7961                                    re.is_match(&item.to_string())
7962                                } else {
7963                                    val.is_true()
7964                                };
7965                                if keep {
7966                                    result.push(item);
7967                                }
7968                            }
7969                            self.push(StrykeValue::array(result));
7970                            Ok(())
7971                        } else {
7972                            let e = self.grep_expr_entries[idx].clone();
7973                            let mut result = Vec::new();
7974                            for item in list {
7975                                self.interp.scope.set_topic_local(item.clone());
7976                                let val = vm_interp_result(self.interp.eval_expr(&e), self.line())?;
7977                                let val = self.maybe_call_coderef_with_item(
7978                                    val,
7979                                    &item,
7980                                    dispatch_coderef,
7981                                )?;
7982                                let keep = if let Some(re) = val.as_regex() {
7983                                    re.is_match(&item.to_string())
7984                                } else {
7985                                    val.is_true()
7986                                };
7987                                if keep {
7988                                    result.push(item);
7989                                }
7990                            }
7991                            self.push(StrykeValue::array(result));
7992                            Ok(())
7993                        }
7994                    }
7995                    Op::SortWithBlock(block_idx) => {
7996                        let mut items = self.pop().to_list();
7997                        let idx = *block_idx as usize;
7998                        // Save the topic chain before sort — set_sort_pair writes to $_
7999                        // which would corrupt _< for subsequent pipeline stages (grep, map).
8000                        let saved_topic = self.interp.scope.save_topic_chain();
8001                        if let Some(&(start, end)) =
8002                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8003                        {
8004                            let mut sort_err: Option<StrykeError> = None;
8005                            items.sort_by(|a, b| {
8006                                if sort_err.is_some() {
8007                                    return std::cmp::Ordering::Equal;
8008                                }
8009                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
8010                                match self.run_block_region(start, end, op_count) {
8011                                    Ok(v) => {
8012                                        let n = v.to_int();
8013                                        if n < 0 {
8014                                            std::cmp::Ordering::Less
8015                                        } else if n > 0 {
8016                                            std::cmp::Ordering::Greater
8017                                        } else {
8018                                            std::cmp::Ordering::Equal
8019                                        }
8020                                    }
8021                                    Err(e) => {
8022                                        sort_err = Some(e);
8023                                        std::cmp::Ordering::Equal
8024                                    }
8025                                }
8026                            });
8027                            self.interp.scope.restore_topic_chain(saved_topic);
8028                            if let Some(e) = sort_err {
8029                                return Err(e);
8030                            }
8031                            self.push(StrykeValue::array(items));
8032                            Ok(())
8033                        } else {
8034                            let block = self.blocks[idx].clone();
8035                            items.sort_by(|a, b| {
8036                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
8037                                match self.interp.exec_block(&block) {
8038                                    Ok(v) => {
8039                                        let n = v.to_int();
8040                                        if n < 0 {
8041                                            std::cmp::Ordering::Less
8042                                        } else if n > 0 {
8043                                            std::cmp::Ordering::Greater
8044                                        } else {
8045                                            std::cmp::Ordering::Equal
8046                                        }
8047                                    }
8048                                    Err(_) => std::cmp::Ordering::Equal,
8049                                }
8050                            });
8051                            self.interp.scope.restore_topic_chain(saved_topic);
8052                            self.push(StrykeValue::array(items));
8053                            Ok(())
8054                        }
8055                    }
8056                    Op::SortWithBlockFast(tag) => {
8057                        let mut items = self.pop().to_list();
8058                        let mode = match *tag {
8059                            0 => SortBlockFast::Numeric,
8060                            1 => SortBlockFast::String,
8061                            2 => SortBlockFast::NumericRev,
8062                            3 => SortBlockFast::StringRev,
8063                            _ => SortBlockFast::Numeric,
8064                        };
8065                        items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
8066                        self.push(StrykeValue::array(items));
8067                        Ok(())
8068                    }
8069                    Op::SortNoBlock => {
8070                        let mut items = self.pop().to_list();
8071                        items.sort_by_key(|a| a.to_string());
8072                        self.push(StrykeValue::array(items));
8073                        Ok(())
8074                    }
8075                    Op::SortWithCodeComparator(wa) => {
8076                        let want = WantarrayCtx::from_byte(*wa);
8077                        let cmp_val = self.pop();
8078                        let mut items = self.pop().to_list();
8079                        let line = self.line();
8080                        let Some(sub) = cmp_val.as_code_ref() else {
8081                            return Err(StrykeError::runtime(
8082                                "sort: comparator must be a code reference",
8083                                line,
8084                            ));
8085                        };
8086                        let interp = &mut self.interp;
8087                        items.sort_by(|a, b| {
8088                            // `set_sort_pair` keeps Perl-style `$a`/`$b` access;
8089                            // positional args let stryke lambdas read via @_.
8090                            interp.scope.set_sort_pair(a.clone(), b.clone());
8091                            match interp.call_sub(
8092                                sub.as_ref(),
8093                                vec![a.clone(), b.clone()],
8094                                want,
8095                                line,
8096                            ) {
8097                                Ok(v) => {
8098                                    let n = v.to_int();
8099                                    if n < 0 {
8100                                        std::cmp::Ordering::Less
8101                                    } else if n > 0 {
8102                                        std::cmp::Ordering::Greater
8103                                    } else {
8104                                        std::cmp::Ordering::Equal
8105                                    }
8106                                }
8107                                Err(_) => std::cmp::Ordering::Equal,
8108                            }
8109                        });
8110                        self.push(StrykeValue::array(items));
8111                        Ok(())
8112                    }
8113                    Op::ReverseListOp => {
8114                        let val = self.pop();
8115                        if val.is_iterator() {
8116                            self.push(StrykeValue::iterator(std::sync::Arc::new(
8117                                crate::value::RevIterator::new(val.into_iterator()),
8118                            )));
8119                        } else {
8120                            let mut items = val.to_list();
8121                            items.reverse();
8122                            self.push(StrykeValue::array(items));
8123                        }
8124                        Ok(())
8125                    }
8126                    Op::ReverseScalarOp => {
8127                        let val = self.pop();
8128                        let items = val.to_list();
8129                        let s: String = items.iter().map(|v| v.to_string()).collect();
8130                        self.push(StrykeValue::string(s.chars().rev().collect()));
8131                        Ok(())
8132                    }
8133                    Op::RevListOp => {
8134                        let val = self.pop();
8135                        if val.is_iterator() {
8136                            // Collect the iterator fully and reverse the list order.
8137                            // RevIterator does per-element char reversal, not list reversal.
8138                            let mut items = val.to_list();
8139                            items.reverse();
8140                            self.push(StrykeValue::array(items));
8141                        } else if let Some(s) = crate::value::set_payload(&val) {
8142                            let mut out = crate::value::PerlSet::new();
8143                            for (k, v) in s.iter().rev() {
8144                                out.insert(k.clone(), v.clone());
8145                            }
8146                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
8147                        } else if let Some(ar) = val.as_array_ref() {
8148                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
8149                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
8150                                parking_lot::RwLock::new(items),
8151                            )));
8152                        } else if let Some(hr) = val.as_hash_ref() {
8153                            let mut out: indexmap::IndexMap<String, StrykeValue> =
8154                                indexmap::IndexMap::new();
8155                            for (k, v) in hr.read().iter() {
8156                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
8157                            }
8158                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
8159                                parking_lot::RwLock::new(out),
8160                            )));
8161                        } else if let Some(hm) = val.as_hash_map() {
8162                            let mut out: indexmap::IndexMap<String, StrykeValue> =
8163                                indexmap::IndexMap::new();
8164                            for (k, v) in hm.iter() {
8165                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
8166                            }
8167                            self.push(StrykeValue::hash(out));
8168                        } else if val.as_array_vec().is_some() {
8169                            let mut items = val.to_list();
8170                            items.reverse();
8171                            self.push(StrykeValue::array(items));
8172                        } else {
8173                            let s = val.to_string();
8174                            self.push(StrykeValue::string(s.chars().rev().collect()));
8175                        }
8176                        Ok(())
8177                    }
8178                    Op::RevScalarOp => {
8179                        let val = self.pop();
8180                        if let Some(s) = crate::value::set_payload(&val) {
8181                            let mut out = crate::value::PerlSet::new();
8182                            for (k, v) in s.iter().rev() {
8183                                out.insert(k.clone(), v.clone());
8184                            }
8185                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
8186                        } else if let Some(ar) = val.as_array_ref() {
8187                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
8188                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
8189                                parking_lot::RwLock::new(items),
8190                            )));
8191                        } else if let Some(hr) = val.as_hash_ref() {
8192                            let mut out: indexmap::IndexMap<String, StrykeValue> =
8193                                indexmap::IndexMap::new();
8194                            for (k, v) in hr.read().iter() {
8195                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
8196                            }
8197                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
8198                                parking_lot::RwLock::new(out),
8199                            )));
8200                        } else {
8201                            let items = val.to_list();
8202                            let s: String = items.iter().map(|v| v.to_string()).collect();
8203                            self.push(StrykeValue::string(s.chars().rev().collect()));
8204                        }
8205                        Ok(())
8206                    }
8207                    Op::StackArrayLen => {
8208                        let v = self.pop();
8209                        self.push(StrykeValue::integer(v.to_list().len() as i64));
8210                        Ok(())
8211                    }
8212                    Op::ListSliceToScalar => {
8213                        let v = self.pop();
8214                        let items = v.to_list();
8215                        self.push(items.last().cloned().unwrap_or(StrykeValue::UNDEF));
8216                        Ok(())
8217                    }
8218
8219                    // ── Eval block ──
8220                    Op::EvalBlock(block_idx, want) => {
8221                        let block = self.blocks[*block_idx as usize].clone();
8222                        let tail = crate::vm_helper::WantarrayCtx::from_byte(*want);
8223                        self.interp.eval_nesting += 1;
8224                        // Use exec_block (with scope frame) so local/my declarations
8225                        // inside the block are properly scoped.
8226                        match self.interp.exec_block_with_tail(&block, tail) {
8227                            Ok(v) => {
8228                                self.interp.clear_eval_error();
8229                                self.push(v);
8230                            }
8231                            Err(crate::vm_helper::FlowOrError::Error(e)) => {
8232                                self.interp.set_eval_error_from_perl_error(&e);
8233                                self.push(StrykeValue::UNDEF);
8234                            }
8235                            Err(_) => self.push(StrykeValue::UNDEF),
8236                        }
8237                        self.interp.eval_nesting -= 1;
8238                        Ok(())
8239                    }
8240                    Op::TraceBlock(block_idx) => {
8241                        let block = self.blocks[*block_idx as usize].clone();
8242                        crate::parallel_trace::trace_enter();
8243                        self.interp.eval_nesting += 1;
8244                        match self.interp.exec_block(&block) {
8245                            Ok(v) => {
8246                                self.interp.clear_eval_error();
8247                                self.push(v);
8248                            }
8249                            Err(FlowOrError::Error(e)) => {
8250                                self.interp.set_eval_error_from_perl_error(&e);
8251                                self.push(StrykeValue::UNDEF);
8252                            }
8253                            Err(_) => self.push(StrykeValue::UNDEF),
8254                        }
8255                        self.interp.eval_nesting -= 1;
8256                        crate::parallel_trace::trace_leave();
8257                        Ok(())
8258                    }
8259                    Op::TimerBlock(block_idx) => {
8260                        let block = self.blocks[*block_idx as usize].clone();
8261                        let start = std::time::Instant::now();
8262                        self.interp.eval_nesting += 1;
8263                        let _ = match self.interp.exec_block(&block) {
8264                            Ok(v) => {
8265                                self.interp.clear_eval_error();
8266                                v
8267                            }
8268                            Err(FlowOrError::Error(e)) => {
8269                                self.interp.set_eval_error_from_perl_error(&e);
8270                                StrykeValue::UNDEF
8271                            }
8272                            Err(_) => StrykeValue::UNDEF,
8273                        };
8274                        self.interp.eval_nesting -= 1;
8275                        let ms = start.elapsed().as_secs_f64() * 1000.0;
8276                        self.push(StrykeValue::float(ms));
8277                        Ok(())
8278                    }
8279                    Op::BenchBlock(block_idx) => {
8280                        let n_i = self.pop().to_int();
8281                        if n_i < 0 {
8282                            return Err(StrykeError::runtime(
8283                                "bench: iteration count must be non-negative",
8284                                self.line(),
8285                            ));
8286                        }
8287                        let n = n_i as usize;
8288                        let block = self.blocks[*block_idx as usize].clone();
8289                        let v = vm_interp_result(
8290                            self.interp.run_bench_block(&block, n, self.line()),
8291                            self.line(),
8292                        )?;
8293                        self.push(v);
8294                        Ok(())
8295                    }
8296                    Op::Given(idx) => {
8297                        let i = *idx as usize;
8298                        let line = self.line();
8299                        let v = if let Some(&(start, end)) = self
8300                            .given_topic_bytecode_ranges
8301                            .get(i)
8302                            .and_then(|r| r.as_ref())
8303                        {
8304                            let topic_val = self.run_block_region(start, end, op_count)?;
8305                            let body = &self.given_entries[i].1;
8306                            vm_interp_result(
8307                                self.interp.exec_given_with_topic_value(topic_val, body),
8308                                line,
8309                            )?
8310                        } else {
8311                            let (topic, body) = &self.given_entries[i];
8312                            vm_interp_result(self.interp.exec_given(topic, body), line)?
8313                        };
8314                        self.push(v);
8315                        Ok(())
8316                    }
8317                    Op::EvalTimeout(idx) => {
8318                        let i = *idx as usize;
8319                        let body = self.eval_timeout_entries[i].1.clone();
8320                        let secs = if let Some(&(start, end)) = self
8321                            .eval_timeout_expr_bytecode_ranges
8322                            .get(i)
8323                            .and_then(|r| r.as_ref())
8324                        {
8325                            self.run_block_region(start, end, op_count)?.to_number()
8326                        } else {
8327                            let timeout_expr = &self.eval_timeout_entries[i].0;
8328                            vm_interp_result(self.interp.eval_expr(timeout_expr), self.line())?
8329                                .to_number()
8330                        };
8331                        let v = vm_interp_result(
8332                            self.interp.eval_timeout_block(&body, secs, self.line()),
8333                            self.line(),
8334                        )?;
8335                        self.push(v);
8336                        Ok(())
8337                    }
8338                    Op::AlgebraicMatch(idx) => {
8339                        let i = *idx as usize;
8340                        let line = self.line();
8341                        let v = if let Some(&(start, end)) = self
8342                            .algebraic_match_subject_bytecode_ranges
8343                            .get(i)
8344                            .and_then(|r| r.as_ref())
8345                        {
8346                            let subject_val = self.run_block_region(start, end, op_count)?;
8347                            let arms = &self.algebraic_match_entries[i].1;
8348                            vm_interp_result(
8349                                self.interp.eval_algebraic_match_with_subject_value(
8350                                    subject_val,
8351                                    arms,
8352                                    line,
8353                                ),
8354                                self.line(),
8355                            )?
8356                        } else {
8357                            let (subject, arms) = &self.algebraic_match_entries[i];
8358                            vm_interp_result(
8359                                self.interp.eval_algebraic_match(subject, arms, line),
8360                                self.line(),
8361                            )?
8362                        };
8363                        self.push(v);
8364                        Ok(())
8365                    }
8366                    Op::ParLines(idx) => {
8367                        let (path, callback, progress) = &self.par_lines_entries[*idx as usize];
8368                        let v = vm_interp_result(
8369                            self.interp.eval_par_lines_expr(
8370                                path,
8371                                callback,
8372                                progress.as_ref(),
8373                                self.line(),
8374                            ),
8375                            self.line(),
8376                        )?;
8377                        self.push(v);
8378                        Ok(())
8379                    }
8380                    Op::ParWalk(idx) => {
8381                        let (path, callback, progress) = &self.par_walk_entries[*idx as usize];
8382                        let v = vm_interp_result(
8383                            self.interp.eval_par_walk_expr(
8384                                path,
8385                                callback,
8386                                progress.as_ref(),
8387                                self.line(),
8388                            ),
8389                            self.line(),
8390                        )?;
8391                        self.push(v);
8392                        Ok(())
8393                    }
8394                    Op::Pwatch(idx) => {
8395                        let (path, callback) = &self.pwatch_entries[*idx as usize];
8396                        let v = vm_interp_result(
8397                            self.interp.eval_pwatch_expr(path, callback, self.line()),
8398                            self.line(),
8399                        )?;
8400                        self.push(v);
8401                        Ok(())
8402                    }
8403
8404                    // ── Parallel operations (rayon) ──
8405                    Op::PMapWithBlock(block_idx) => {
8406                        let list = self.pop().to_list();
8407                        let progress_flag = self.pop().is_true();
8408                        let idx = *block_idx as usize;
8409                        let subs = self.interp.subs.clone();
8410                        let (scope_capture, atomic_arrays, atomic_hashes) =
8411                            self.interp.scope.capture_with_atomics();
8412                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8413                        let n_workers = rayon::current_num_threads();
8414                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8415                            .map(|_| {
8416                                let mut interp = VMHelper::new();
8417                                interp.subs = subs.clone();
8418                                interp.scope.restore_capture(&scope_capture);
8419                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8420                                interp.enable_parallel_guard();
8421                                Mutex::new(interp)
8422                            })
8423                            .collect();
8424                        if let Some(&(start, end)) =
8425                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8426                        {
8427                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8428                            let results: Vec<StrykeValue> = list
8429                                .into_par_iter()
8430                                .map(|item| {
8431                                    let tid =
8432                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8433                                    let mut local_interp = pool[tid].lock();
8434                                    local_interp.scope.set_topic(item);
8435                                    let mut vm = shared.worker_vm(&mut local_interp);
8436                                    let mut op_count = 0u64;
8437                                    let val = match vm.run_block_region(start, end, &mut op_count) {
8438                                        Ok(v) => v,
8439                                        Err(_) => StrykeValue::UNDEF,
8440                                    };
8441                                    pmap_progress.tick();
8442                                    val
8443                                })
8444                                .collect();
8445                            pmap_progress.finish();
8446                            self.push(StrykeValue::array(results));
8447                            Ok(())
8448                        } else {
8449                            let block = self.blocks[idx].clone();
8450                            let results: Vec<StrykeValue> = list
8451                                .into_par_iter()
8452                                .map(|item| {
8453                                    let tid =
8454                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8455                                    let mut local_interp = pool[tid].lock();
8456                                    local_interp.scope.set_topic(item);
8457                                    local_interp.scope_push_hook();
8458                                    let val = match local_interp.exec_block_no_scope(&block) {
8459                                        Ok(val) => val,
8460                                        Err(_) => StrykeValue::UNDEF,
8461                                    };
8462                                    local_interp.scope_pop_hook();
8463                                    pmap_progress.tick();
8464                                    val
8465                                })
8466                                .collect();
8467                            pmap_progress.finish();
8468                            self.push(StrykeValue::array(results));
8469                            Ok(())
8470                        }
8471                    }
8472                    Op::PFlatMapWithBlock(block_idx) => {
8473                        let list = self.pop().to_list();
8474                        let progress_flag = self.pop().is_true();
8475                        let idx = *block_idx as usize;
8476                        let subs = self.interp.subs.clone();
8477                        let (scope_capture, atomic_arrays, atomic_hashes) =
8478                            self.interp.scope.capture_with_atomics();
8479                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8480                        let n_workers = rayon::current_num_threads();
8481                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8482                            .map(|_| {
8483                                let mut interp = VMHelper::new();
8484                                interp.subs = subs.clone();
8485                                interp.scope.restore_capture(&scope_capture);
8486                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8487                                interp.enable_parallel_guard();
8488                                Mutex::new(interp)
8489                            })
8490                            .collect();
8491                        if let Some(&(start, end)) =
8492                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8493                        {
8494                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8495                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
8496                                .into_par_iter()
8497                                .enumerate()
8498                                .map(|(i, item)| {
8499                                    let tid =
8500                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8501                                    let mut local_interp = pool[tid].lock();
8502                                    local_interp.scope.set_topic(item);
8503                                    let mut vm = shared.worker_vm(&mut local_interp);
8504                                    let mut op_count = 0u64;
8505                                    let val = match vm.run_block_region(start, end, &mut op_count) {
8506                                        Ok(v) => v,
8507                                        Err(_) => StrykeValue::UNDEF,
8508                                    };
8509                                    let out = val.map_flatten_outputs(true);
8510                                    pmap_progress.tick();
8511                                    (i, out)
8512                                })
8513                                .collect();
8514                            pmap_progress.finish();
8515                            indexed.sort_by_key(|(i, _)| *i);
8516                            let results: Vec<StrykeValue> =
8517                                indexed.into_iter().flat_map(|(_, v)| v).collect();
8518                            self.push(StrykeValue::array(results));
8519                            Ok(())
8520                        } else {
8521                            let block = self.blocks[idx].clone();
8522                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
8523                                .into_par_iter()
8524                                .enumerate()
8525                                .map(|(i, item)| {
8526                                    let tid =
8527                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8528                                    let mut local_interp = pool[tid].lock();
8529                                    local_interp.scope.set_topic(item);
8530                                    local_interp.scope_push_hook();
8531                                    let val = match local_interp.exec_block_no_scope(&block) {
8532                                        Ok(val) => val,
8533                                        Err(_) => StrykeValue::UNDEF,
8534                                    };
8535                                    local_interp.scope_pop_hook();
8536                                    let out = val.map_flatten_outputs(true);
8537                                    pmap_progress.tick();
8538                                    (i, out)
8539                                })
8540                                .collect();
8541                            pmap_progress.finish();
8542                            indexed.sort_by_key(|(i, _)| *i);
8543                            let results: Vec<StrykeValue> =
8544                                indexed.into_iter().flat_map(|(_, v)| v).collect();
8545                            self.push(StrykeValue::array(results));
8546                            Ok(())
8547                        }
8548                    }
8549                    Op::PMapRemote { block_idx, flat } => {
8550                        let cluster = self.pop();
8551                        let list_pv = self.pop();
8552                        let progress_flag = self.pop().is_true();
8553                        let idx = *block_idx as usize;
8554                        let block = self.blocks[idx].clone();
8555                        let flat_outputs = *flat != 0;
8556                        let v = vm_interp_result(
8557                            self.interp.eval_pmap_remote(
8558                                cluster,
8559                                list_pv,
8560                                progress_flag,
8561                                &block,
8562                                flat_outputs,
8563                                self.line(),
8564                            ),
8565                            self.line(),
8566                        )?;
8567                        self.push(v);
8568                        Ok(())
8569                    }
8570                    Op::Puniq => {
8571                        let list = self.pop().to_list();
8572                        let progress_flag = self.pop().is_true();
8573                        let n_threads = self.interp.parallel_thread_count();
8574                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8575                        let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
8576                        pmap_progress.finish();
8577                        self.push(StrykeValue::array(out));
8578                        Ok(())
8579                    }
8580                    Op::PFirstWithBlock(block_idx) => {
8581                        let list = self.pop().to_list();
8582                        let progress_flag = self.pop().is_true();
8583                        let idx = *block_idx as usize;
8584                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8585                        let subs = self.interp.subs.clone();
8586                        let (scope_capture, atomic_arrays, atomic_hashes) =
8587                            self.interp.scope.capture_with_atomics();
8588                        let out = if let Some(&(start, end)) =
8589                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8590                        {
8591                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8592                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
8593                                let mut local_interp = VMHelper::new();
8594                                local_interp.subs = subs.clone();
8595                                local_interp.scope.restore_capture(&scope_capture);
8596                                local_interp
8597                                    .scope
8598                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8599                                local_interp.enable_parallel_guard();
8600                                local_interp.scope.set_topic(item);
8601                                let mut vm = shared.worker_vm(&mut local_interp);
8602                                let mut op_count = 0u64;
8603                                match vm.run_block_region(start, end, &mut op_count) {
8604                                    Ok(v) => v.is_true(),
8605                                    Err(_) => false,
8606                                }
8607                            })
8608                        } else {
8609                            let block = self.blocks[idx].clone();
8610                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
8611                                let mut local_interp = VMHelper::new();
8612                                local_interp.subs = subs.clone();
8613                                local_interp.scope.restore_capture(&scope_capture);
8614                                local_interp
8615                                    .scope
8616                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8617                                local_interp.enable_parallel_guard();
8618                                local_interp.scope.set_topic(item);
8619                                local_interp.scope_push_hook();
8620                                let ok = match local_interp.exec_block_no_scope(&block) {
8621                                    Ok(v) => v.is_true(),
8622                                    Err(_) => false,
8623                                };
8624                                local_interp.scope_pop_hook();
8625                                ok
8626                            })
8627                        };
8628                        pmap_progress.finish();
8629                        self.push(out.unwrap_or(StrykeValue::UNDEF));
8630                        Ok(())
8631                    }
8632                    Op::PAnyWithBlock(block_idx) => {
8633                        let list = self.pop().to_list();
8634                        let progress_flag = self.pop().is_true();
8635                        let idx = *block_idx as usize;
8636                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8637                        let subs = self.interp.subs.clone();
8638                        let (scope_capture, atomic_arrays, atomic_hashes) =
8639                            self.interp.scope.capture_with_atomics();
8640                        let b = if let Some(&(start, end)) =
8641                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8642                        {
8643                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8644                            crate::par_list::pany_run(list, &pmap_progress, |item| {
8645                                let mut local_interp = VMHelper::new();
8646                                local_interp.subs = subs.clone();
8647                                local_interp.scope.restore_capture(&scope_capture);
8648                                local_interp
8649                                    .scope
8650                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8651                                local_interp.enable_parallel_guard();
8652                                local_interp.scope.set_topic(item);
8653                                let mut vm = shared.worker_vm(&mut local_interp);
8654                                let mut op_count = 0u64;
8655                                match vm.run_block_region(start, end, &mut op_count) {
8656                                    Ok(v) => v.is_true(),
8657                                    Err(_) => false,
8658                                }
8659                            })
8660                        } else {
8661                            let block = self.blocks[idx].clone();
8662                            crate::par_list::pany_run(list, &pmap_progress, |item| {
8663                                let mut local_interp = VMHelper::new();
8664                                local_interp.subs = subs.clone();
8665                                local_interp.scope.restore_capture(&scope_capture);
8666                                local_interp
8667                                    .scope
8668                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8669                                local_interp.enable_parallel_guard();
8670                                local_interp.scope.set_topic(item);
8671                                local_interp.scope_push_hook();
8672                                let ok = match local_interp.exec_block_no_scope(&block) {
8673                                    Ok(v) => v.is_true(),
8674                                    Err(_) => false,
8675                                };
8676                                local_interp.scope_pop_hook();
8677                                ok
8678                            })
8679                        };
8680                        pmap_progress.finish();
8681                        self.push(StrykeValue::integer(if b { 1 } else { 0 }));
8682                        Ok(())
8683                    }
8684                    Op::PMapChunkedWithBlock(block_idx) => {
8685                        let list = self.pop().to_list();
8686                        let chunk_n = self.pop().to_int().max(1) as usize;
8687                        let progress_flag = self.pop().is_true();
8688                        let idx = *block_idx as usize;
8689                        let subs = self.interp.subs.clone();
8690                        let (scope_capture, atomic_arrays, atomic_hashes) =
8691                            self.interp.scope.capture_with_atomics();
8692                        let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = list
8693                            .chunks(chunk_n)
8694                            .enumerate()
8695                            .map(|(i, c)| (i, c.to_vec()))
8696                            .collect();
8697                        let n_chunks = indexed_chunks.len();
8698                        let pmap_progress = PmapProgress::new(progress_flag, n_chunks);
8699                        if let Some(&(start, end)) =
8700                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8701                        {
8702                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8703                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
8704                                .into_par_iter()
8705                                .map(|(chunk_idx, chunk)| {
8706                                    let mut local_interp = VMHelper::new();
8707                                    local_interp.subs = subs.clone();
8708                                    local_interp.scope.restore_capture(&scope_capture);
8709                                    local_interp
8710                                        .scope
8711                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
8712                                    local_interp.enable_parallel_guard();
8713                                    let mut out = Vec::with_capacity(chunk.len());
8714                                    for item in chunk {
8715                                        local_interp.scope.set_topic(item);
8716                                        let mut vm = shared.worker_vm(&mut local_interp);
8717                                        let mut op_count = 0u64;
8718                                        let val =
8719                                            match vm.run_block_region(start, end, &mut op_count) {
8720                                                Ok(v) => v,
8721                                                Err(_) => StrykeValue::UNDEF,
8722                                            };
8723                                        out.push(val);
8724                                    }
8725                                    pmap_progress.tick();
8726                                    (chunk_idx, out)
8727                                })
8728                                .collect();
8729                            pmap_progress.finish();
8730                            chunk_results.sort_by_key(|(i, _)| *i);
8731                            let results: Vec<StrykeValue> =
8732                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
8733                            self.push(StrykeValue::array(results));
8734                            Ok(())
8735                        } else {
8736                            let block = self.blocks[idx].clone();
8737                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
8738                                .into_par_iter()
8739                                .map(|(chunk_idx, chunk)| {
8740                                    let mut local_interp = VMHelper::new();
8741                                    local_interp.subs = subs.clone();
8742                                    local_interp.scope.restore_capture(&scope_capture);
8743                                    local_interp
8744                                        .scope
8745                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
8746                                    local_interp.enable_parallel_guard();
8747                                    let mut out = Vec::with_capacity(chunk.len());
8748                                    for item in chunk {
8749                                        local_interp.scope.set_topic(item);
8750                                        local_interp.scope_push_hook();
8751                                        let val = match local_interp.exec_block_no_scope(&block) {
8752                                            Ok(val) => val,
8753                                            Err(_) => StrykeValue::UNDEF,
8754                                        };
8755                                        local_interp.scope_pop_hook();
8756                                        out.push(val);
8757                                    }
8758                                    pmap_progress.tick();
8759                                    (chunk_idx, out)
8760                                })
8761                                .collect();
8762                            pmap_progress.finish();
8763                            chunk_results.sort_by_key(|(i, _)| *i);
8764                            let results: Vec<StrykeValue> =
8765                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
8766                            self.push(StrykeValue::array(results));
8767                            Ok(())
8768                        }
8769                    }
8770                    Op::ReduceWithBlock(block_idx) => {
8771                        let list = self.pop().to_list();
8772                        let idx = *block_idx as usize;
8773                        let subs = self.interp.subs.clone();
8774                        let scope_capture = self.interp.scope.capture();
8775                        if list.is_empty() {
8776                            self.push(StrykeValue::UNDEF);
8777                            return Ok(());
8778                        }
8779                        if list.len() == 1 {
8780                            self.push(list.into_iter().next().unwrap());
8781                            return Ok(());
8782                        }
8783                        let mut items = list;
8784                        let mut acc = items.remove(0);
8785                        let rest = items;
8786                        if let Some(&(start, end)) =
8787                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8788                        {
8789                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8790                            for b in rest {
8791                                let mut local_interp = VMHelper::new();
8792                                local_interp.subs = subs.clone();
8793                                local_interp.scope.restore_capture(&scope_capture);
8794                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
8795                                let mut vm = shared.worker_vm(&mut local_interp);
8796                                let mut op_count = 0u64;
8797                                acc = match vm.run_block_region(start, end, &mut op_count) {
8798                                    Ok(v) => v,
8799                                    Err(_) => StrykeValue::UNDEF,
8800                                };
8801                            }
8802                        } else {
8803                            let block = self.blocks[idx].clone();
8804                            for b in rest {
8805                                let mut local_interp = VMHelper::new();
8806                                local_interp.subs = subs.clone();
8807                                local_interp.scope.restore_capture(&scope_capture);
8808                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
8809                                acc = match local_interp.exec_block(&block) {
8810                                    Ok(val) => val,
8811                                    Err(_) => StrykeValue::UNDEF,
8812                                };
8813                            }
8814                        }
8815                        self.push(acc);
8816                        Ok(())
8817                    }
8818                    Op::PReduceWithBlock(block_idx) => {
8819                        let list = self.pop().to_list();
8820                        let progress_flag = self.pop().is_true();
8821                        let idx = *block_idx as usize;
8822                        let subs = self.interp.subs.clone();
8823                        let scope_capture = self.interp.scope.capture();
8824                        if list.is_empty() {
8825                            self.push(StrykeValue::UNDEF);
8826                            return Ok(());
8827                        }
8828                        if list.len() == 1 {
8829                            self.push(list.into_iter().next().unwrap());
8830                            return Ok(());
8831                        }
8832                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8833                        if let Some(&(start, end)) =
8834                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8835                        {
8836                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8837                            let result = list
8838                                .into_par_iter()
8839                                .map(|x| {
8840                                    pmap_progress.tick();
8841                                    x
8842                                })
8843                                .reduce_with(|a, b| {
8844                                    let mut local_interp = VMHelper::new();
8845                                    local_interp.subs = subs.clone();
8846                                    local_interp.scope.restore_capture(&scope_capture);
8847                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8848                                    let mut vm = shared.worker_vm(&mut local_interp);
8849                                    let mut op_count = 0u64;
8850                                    match vm.run_block_region(start, end, &mut op_count) {
8851                                        Ok(val) => val,
8852                                        Err(_) => StrykeValue::UNDEF,
8853                                    }
8854                                });
8855                            pmap_progress.finish();
8856                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8857                            Ok(())
8858                        } else {
8859                            let block = self.blocks[idx].clone();
8860                            let result = list
8861                                .into_par_iter()
8862                                .map(|x| {
8863                                    pmap_progress.tick();
8864                                    x
8865                                })
8866                                .reduce_with(|a, b| {
8867                                    let mut local_interp = VMHelper::new();
8868                                    local_interp.subs = subs.clone();
8869                                    local_interp.scope.restore_capture(&scope_capture);
8870                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8871                                    match local_interp.exec_block(&block) {
8872                                        Ok(val) => val,
8873                                        Err(_) => StrykeValue::UNDEF,
8874                                    }
8875                                });
8876                            pmap_progress.finish();
8877                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8878                            Ok(())
8879                        }
8880                    }
8881                    Op::PReduceInitWithBlock(block_idx) => {
8882                        let init_val = self.pop();
8883                        let list = self.pop().to_list();
8884                        let progress_flag = self.pop().is_true();
8885                        let idx = *block_idx as usize;
8886                        let subs = self.interp.subs.clone();
8887                        let scope_capture = self.interp.scope.capture();
8888                        let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
8889                        let block = self.blocks[idx].clone();
8890                        if list.is_empty() {
8891                            self.push(init_val);
8892                            return Ok(());
8893                        }
8894                        if list.len() == 1 {
8895                            let v = fold_preduce_init_step(
8896                                &subs,
8897                                cap,
8898                                &block,
8899                                preduce_init_fold_identity(&init_val),
8900                                list.into_iter().next().unwrap(),
8901                            );
8902                            self.push(v);
8903                            return Ok(());
8904                        }
8905                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8906                        let result = list
8907                            .into_par_iter()
8908                            .fold(
8909                                || preduce_init_fold_identity(&init_val),
8910                                |acc, item| {
8911                                    pmap_progress.tick();
8912                                    fold_preduce_init_step(&subs, cap, &block, acc, item)
8913                                },
8914                            )
8915                            .reduce(
8916                                || preduce_init_fold_identity(&init_val),
8917                                |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
8918                            );
8919                        pmap_progress.finish();
8920                        self.push(result);
8921                        Ok(())
8922                    }
8923                    Op::PMapReduceWithBlocks(map_idx, reduce_idx) => {
8924                        let list = self.pop().to_list();
8925                        let progress_flag = self.pop().is_true();
8926                        let map_i = *map_idx as usize;
8927                        let reduce_i = *reduce_idx as usize;
8928                        let subs = self.interp.subs.clone();
8929                        let scope_capture = self.interp.scope.capture();
8930                        if list.is_empty() {
8931                            self.push(StrykeValue::UNDEF);
8932                            return Ok(());
8933                        }
8934                        if list.len() == 1 {
8935                            let mut local_interp = VMHelper::new();
8936                            local_interp.subs = subs.clone();
8937                            local_interp.scope.restore_capture(&scope_capture);
8938                            local_interp
8939                                .scope
8940                                .set_topic(list.into_iter().next().unwrap());
8941                            let map_block = self.blocks[map_i].clone();
8942                            let v = match local_interp.exec_block_no_scope(&map_block) {
8943                                Ok(v) => v,
8944                                Err(_) => StrykeValue::UNDEF,
8945                            };
8946                            self.push(v);
8947                            return Ok(());
8948                        }
8949                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8950                        let map_range = self
8951                            .block_bytecode_ranges
8952                            .get(map_i)
8953                            .and_then(|r| r.as_ref())
8954                            .copied();
8955                        let reduce_range = self
8956                            .block_bytecode_ranges
8957                            .get(reduce_i)
8958                            .and_then(|r| r.as_ref())
8959                            .copied();
8960                        if let (Some((map_start, map_end)), Some((reduce_start, reduce_end))) =
8961                            (map_range, reduce_range)
8962                        {
8963                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8964                            let result = list
8965                                .into_par_iter()
8966                                .map(|item| {
8967                                    let mut local_interp = VMHelper::new();
8968                                    local_interp.subs = subs.clone();
8969                                    local_interp.scope.restore_capture(&scope_capture);
8970                                    local_interp.scope.set_topic(item);
8971                                    let mut vm = shared.worker_vm(&mut local_interp);
8972                                    let mut op_count = 0u64;
8973                                    let val = match vm.run_block_region(
8974                                        map_start,
8975                                        map_end,
8976                                        &mut op_count,
8977                                    ) {
8978                                        Ok(val) => val,
8979                                        Err(_) => StrykeValue::UNDEF,
8980                                    };
8981                                    pmap_progress.tick();
8982                                    val
8983                                })
8984                                .reduce_with(|a, b| {
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_sort_pair(a.clone(), b.clone());
8989                                    let mut vm = shared.worker_vm(&mut local_interp);
8990                                    let mut op_count = 0u64;
8991                                    match vm.run_block_region(
8992                                        reduce_start,
8993                                        reduce_end,
8994                                        &mut op_count,
8995                                    ) {
8996                                        Ok(val) => val,
8997                                        Err(_) => StrykeValue::UNDEF,
8998                                    }
8999                                });
9000                            pmap_progress.finish();
9001                            self.push(result.unwrap_or(StrykeValue::UNDEF));
9002                            Ok(())
9003                        } else {
9004                            let map_block = self.blocks[map_i].clone();
9005                            let reduce_block = self.blocks[reduce_i].clone();
9006                            let result = list
9007                                .into_par_iter()
9008                                .map(|item| {
9009                                    let mut local_interp = VMHelper::new();
9010                                    local_interp.subs = subs.clone();
9011                                    local_interp.scope.restore_capture(&scope_capture);
9012                                    local_interp.scope.set_topic(item);
9013                                    let val = match local_interp.exec_block_no_scope(&map_block) {
9014                                        Ok(val) => val,
9015                                        Err(_) => StrykeValue::UNDEF,
9016                                    };
9017                                    pmap_progress.tick();
9018                                    val
9019                                })
9020                                .reduce_with(|a, b| {
9021                                    let mut local_interp = VMHelper::new();
9022                                    local_interp.subs = subs.clone();
9023                                    local_interp.scope.restore_capture(&scope_capture);
9024                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
9025                                    match local_interp.exec_block_no_scope(&reduce_block) {
9026                                        Ok(val) => val,
9027                                        Err(_) => StrykeValue::UNDEF,
9028                                    }
9029                                });
9030                            pmap_progress.finish();
9031                            self.push(result.unwrap_or(StrykeValue::UNDEF));
9032                            Ok(())
9033                        }
9034                    }
9035                    Op::PcacheWithBlock(block_idx) => {
9036                        let list = self.pop().to_list();
9037                        let progress_flag = self.pop().is_true();
9038                        let idx = *block_idx as usize;
9039                        let subs = self.interp.subs.clone();
9040                        let scope_capture = self.interp.scope.capture();
9041                        let block = self.blocks[idx].clone();
9042                        let cache = &*crate::pcache::GLOBAL_PCACHE;
9043                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
9044                        if let Some(&(start, end)) =
9045                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9046                        {
9047                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9048                            let results: Vec<StrykeValue> = list
9049                                .into_par_iter()
9050                                .map(|item| {
9051                                    let k = crate::pcache::cache_key(&item);
9052                                    if let Some(v) = cache.get(&k) {
9053                                        pmap_progress.tick();
9054                                        return v.clone();
9055                                    }
9056                                    let mut local_interp = VMHelper::new();
9057                                    local_interp.subs = subs.clone();
9058                                    local_interp.scope.restore_capture(&scope_capture);
9059                                    local_interp.scope.set_topic(item.clone());
9060                                    let mut vm = shared.worker_vm(&mut local_interp);
9061                                    let mut op_count = 0u64;
9062                                    let val = match vm.run_block_region(start, end, &mut op_count) {
9063                                        Ok(v) => v,
9064                                        Err(_) => StrykeValue::UNDEF,
9065                                    };
9066                                    cache.insert(k, val.clone());
9067                                    pmap_progress.tick();
9068                                    val
9069                                })
9070                                .collect();
9071                            pmap_progress.finish();
9072                            self.push(StrykeValue::array(results));
9073                            Ok(())
9074                        } else {
9075                            let results: Vec<StrykeValue> = list
9076                                .into_par_iter()
9077                                .map(|item| {
9078                                    let k = crate::pcache::cache_key(&item);
9079                                    if let Some(v) = cache.get(&k) {
9080                                        pmap_progress.tick();
9081                                        return v.clone();
9082                                    }
9083                                    let mut local_interp = VMHelper::new();
9084                                    local_interp.subs = subs.clone();
9085                                    local_interp.scope.restore_capture(&scope_capture);
9086                                    local_interp.scope.set_topic(item.clone());
9087                                    let val = match local_interp.exec_block_no_scope(&block) {
9088                                        Ok(v) => v,
9089                                        Err(_) => StrykeValue::UNDEF,
9090                                    };
9091                                    cache.insert(k, val.clone());
9092                                    pmap_progress.tick();
9093                                    val
9094                                })
9095                                .collect();
9096                            pmap_progress.finish();
9097                            self.push(StrykeValue::array(results));
9098                            Ok(())
9099                        }
9100                    }
9101                    Op::Pselect { n_rx, has_timeout } => {
9102                        let timeout = if *has_timeout {
9103                            let t = self.pop().to_number();
9104                            Some(std::time::Duration::from_secs_f64(t.max(0.0)))
9105                        } else {
9106                            None
9107                        };
9108                        let mut rx_vals = Vec::with_capacity(*n_rx as usize);
9109                        for _ in 0..*n_rx {
9110                            rx_vals.push(self.pop());
9111                        }
9112                        rx_vals.reverse();
9113                        let line = self.line();
9114                        let v = crate::pchannel::pselect_recv_with_optional_timeout(
9115                            &rx_vals, timeout, line,
9116                        )?;
9117                        self.push(v);
9118                        Ok(())
9119                    }
9120                    Op::PGrepWithBlock(block_idx) => {
9121                        let list = self.pop().to_list();
9122                        let progress_flag = self.pop().is_true();
9123                        let idx = *block_idx as usize;
9124                        let subs = self.interp.subs.clone();
9125                        let (scope_capture, atomic_arrays, atomic_hashes) =
9126                            self.interp.scope.capture_with_atomics();
9127                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
9128                        let n_workers = rayon::current_num_threads();
9129                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
9130                            .map(|_| {
9131                                let mut interp = VMHelper::new();
9132                                interp.subs = subs.clone();
9133                                interp.scope.restore_capture(&scope_capture);
9134                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
9135                                interp.enable_parallel_guard();
9136                                Mutex::new(interp)
9137                            })
9138                            .collect();
9139                        if let Some(&(start, end)) =
9140                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9141                        {
9142                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9143                            let results: Vec<StrykeValue> = list
9144                                .into_par_iter()
9145                                .filter_map(|item| {
9146                                    let tid =
9147                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
9148                                    let mut local_interp = pool[tid].lock();
9149                                    local_interp.scope.set_topic(item.clone());
9150                                    let mut vm = shared.worker_vm(&mut local_interp);
9151                                    let mut op_count = 0u64;
9152                                    let keep = match vm.run_block_region(start, end, &mut op_count)
9153                                    {
9154                                        Ok(val) => val.is_true(),
9155                                        Err(_) => false,
9156                                    };
9157                                    pmap_progress.tick();
9158                                    if keep {
9159                                        Some(item)
9160                                    } else {
9161                                        None
9162                                    }
9163                                })
9164                                .collect();
9165                            pmap_progress.finish();
9166                            self.push(StrykeValue::array(results));
9167                            Ok(())
9168                        } else {
9169                            let block = self.blocks[idx].clone();
9170                            let results: Vec<StrykeValue> = list
9171                                .into_par_iter()
9172                                .filter_map(|item| {
9173                                    let tid =
9174                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
9175                                    let mut local_interp = pool[tid].lock();
9176                                    local_interp.scope.set_topic(item.clone());
9177                                    local_interp.scope_push_hook();
9178                                    let keep = match local_interp.exec_block_no_scope(&block) {
9179                                        Ok(val) => val.is_true(),
9180                                        Err(_) => false,
9181                                    };
9182                                    local_interp.scope_pop_hook();
9183                                    pmap_progress.tick();
9184                                    if keep {
9185                                        Some(item)
9186                                    } else {
9187                                        None
9188                                    }
9189                                })
9190                                .collect();
9191                            pmap_progress.finish();
9192                            self.push(StrykeValue::array(results));
9193                            Ok(())
9194                        }
9195                    }
9196                    Op::PMapsWithBlock(block_idx) => {
9197                        let val = self.pop();
9198                        let block = self.blocks[*block_idx as usize].clone();
9199                        let source = crate::map_stream::into_pull_iter(val);
9200                        let sub = self.interp.anon_coderef_from_block(&block);
9201                        let (capture, atomic_arrays, atomic_hashes) =
9202                            self.interp.scope.capture_with_atomics();
9203                        let out = StrykeValue::iterator(Arc::new(
9204                            crate::map_stream::PMapStreamIterator::new(
9205                                source,
9206                                sub,
9207                                self.interp.subs.clone(),
9208                                capture,
9209                                atomic_arrays,
9210                                atomic_hashes,
9211                                false,
9212                            ),
9213                        ));
9214                        self.push(out);
9215                        Ok(())
9216                    }
9217                    Op::PFlatMapsWithBlock(block_idx) => {
9218                        let val = self.pop();
9219                        let block = self.blocks[*block_idx as usize].clone();
9220                        let source = crate::map_stream::into_pull_iter(val);
9221                        let sub = self.interp.anon_coderef_from_block(&block);
9222                        let (capture, atomic_arrays, atomic_hashes) =
9223                            self.interp.scope.capture_with_atomics();
9224                        let out = StrykeValue::iterator(Arc::new(
9225                            crate::map_stream::PMapStreamIterator::new(
9226                                source,
9227                                sub,
9228                                self.interp.subs.clone(),
9229                                capture,
9230                                atomic_arrays,
9231                                atomic_hashes,
9232                                true,
9233                            ),
9234                        ));
9235                        self.push(out);
9236                        Ok(())
9237                    }
9238                    Op::PGrepsWithBlock(block_idx) => {
9239                        let val = self.pop();
9240                        let block = self.blocks[*block_idx as usize].clone();
9241                        let source = crate::map_stream::into_pull_iter(val);
9242                        let sub = self.interp.anon_coderef_from_block(&block);
9243                        let (capture, atomic_arrays, atomic_hashes) =
9244                            self.interp.scope.capture_with_atomics();
9245                        let out = StrykeValue::iterator(Arc::new(
9246                            crate::map_stream::PGrepStreamIterator::new(
9247                                source,
9248                                sub,
9249                                self.interp.subs.clone(),
9250                                capture,
9251                                atomic_arrays,
9252                                atomic_hashes,
9253                            ),
9254                        ));
9255                        self.push(out);
9256                        Ok(())
9257                    }
9258                    Op::PForWithBlock(block_idx) => {
9259                        let line = self.line();
9260                        let list = self.pop().to_list();
9261                        let progress_flag = self.pop().is_true();
9262                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
9263                        let idx = *block_idx as usize;
9264                        let subs = self.interp.subs.clone();
9265                        let (scope_capture, atomic_arrays, atomic_hashes) =
9266                            self.interp.scope.capture_with_atomics();
9267                        let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
9268                        let n_workers = rayon::current_num_threads();
9269                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
9270                            .map(|_| {
9271                                let mut interp = VMHelper::new();
9272                                interp.subs = subs.clone();
9273                                interp.scope.restore_capture(&scope_capture);
9274                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
9275                                interp.enable_parallel_guard();
9276                                Mutex::new(interp)
9277                            })
9278                            .collect();
9279                        if let Some(&(start, end)) =
9280                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9281                        {
9282                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9283                            list.into_par_iter().for_each(|item| {
9284                                if first_err.lock().is_some() {
9285                                    return;
9286                                }
9287                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
9288                                let mut local_interp = pool[tid].lock();
9289                                local_interp.scope.set_topic(item);
9290                                let mut vm = shared.worker_vm(&mut local_interp);
9291                                let mut op_count = 0u64;
9292                                match vm.run_block_region(start, end, &mut op_count) {
9293                                    Ok(_) => {}
9294                                    Err(e) => {
9295                                        let mut g = first_err.lock();
9296                                        if g.is_none() {
9297                                            *g = Some(e);
9298                                        }
9299                                    }
9300                                }
9301                                pmap_progress.tick();
9302                            });
9303                        } else {
9304                            let block = self.blocks[idx].clone();
9305                            list.into_par_iter().for_each(|item| {
9306                                if first_err.lock().is_some() {
9307                                    return;
9308                                }
9309                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
9310                                let mut local_interp = pool[tid].lock();
9311                                local_interp.scope.set_topic(item);
9312                                local_interp.scope_push_hook();
9313                                match local_interp.exec_block_no_scope(&block) {
9314                                    Ok(_) => {}
9315                                    Err(e) => {
9316                                        let stryke = match e {
9317                                            FlowOrError::Error(stryke) => stryke,
9318                                            FlowOrError::Flow(_) => StrykeError::runtime(
9319                                                "return/last/next/redo not supported inside pfor block",
9320                                                line,
9321                                            ),
9322                                        };
9323                                        let mut g = first_err.lock();
9324                                        if g.is_none() {
9325                                            *g = Some(stryke);
9326                                        }
9327                                    }
9328                                }
9329                                local_interp.scope_pop_hook();
9330                                pmap_progress.tick();
9331                            });
9332                        }
9333                        pmap_progress.finish();
9334                        if let Some(e) = first_err.lock().take() {
9335                            return Err(e);
9336                        }
9337                        self.push(StrykeValue::UNDEF);
9338                        Ok(())
9339                    }
9340                    Op::PSortWithBlock(block_idx) => {
9341                        let mut items = self.pop().to_list();
9342                        let progress_flag = self.pop().is_true();
9343                        let pmap_progress = PmapProgress::new(progress_flag, 2);
9344                        pmap_progress.tick();
9345                        let idx = *block_idx as usize;
9346                        let subs = self.interp.subs.clone();
9347                        let (scope_capture, atomic_arrays, atomic_hashes) =
9348                            self.interp.scope.capture_with_atomics();
9349                        if let Some(&(start, end)) =
9350                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
9351                        {
9352                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
9353                            items.par_sort_by(|a, b| {
9354                                let mut local_interp = VMHelper::new();
9355                                local_interp.subs = subs.clone();
9356                                local_interp.scope.restore_capture(&scope_capture);
9357                                local_interp
9358                                    .scope
9359                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
9360                                local_interp.enable_parallel_guard();
9361                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
9362                                // Populate slot-based positional args so the
9363                                // bytecode block can read `$_0`/`$_1` (and the
9364                                // bareword `_0`/`_1`) through the slot fast
9365                                // path. `set_sort_pair` only sets the named
9366                                // scalars; without slots, an `$_0` reference
9367                                // resolves to undef in worker bytecode.
9368                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
9369                                let mut vm = shared.worker_vm(&mut local_interp);
9370                                let mut op_count = 0u64;
9371                                match vm.run_block_region(start, end, &mut op_count) {
9372                                    Ok(v) => {
9373                                        let n = v.to_int();
9374                                        if n < 0 {
9375                                            std::cmp::Ordering::Less
9376                                        } else if n > 0 {
9377                                            std::cmp::Ordering::Greater
9378                                        } else {
9379                                            std::cmp::Ordering::Equal
9380                                        }
9381                                    }
9382                                    Err(_) => std::cmp::Ordering::Equal,
9383                                }
9384                            });
9385                        } else {
9386                            let block = self.blocks[idx].clone();
9387                            items.par_sort_by(|a, b| {
9388                                let mut local_interp = VMHelper::new();
9389                                local_interp.subs = subs.clone();
9390                                local_interp.scope.restore_capture(&scope_capture);
9391                                local_interp
9392                                    .scope
9393                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
9394                                local_interp.enable_parallel_guard();
9395                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
9396                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
9397                                local_interp.scope_push_hook();
9398                                let ord = match local_interp.exec_block_no_scope(&block) {
9399                                    Ok(v) => {
9400                                        let n = v.to_int();
9401                                        if n < 0 {
9402                                            std::cmp::Ordering::Less
9403                                        } else if n > 0 {
9404                                            std::cmp::Ordering::Greater
9405                                        } else {
9406                                            std::cmp::Ordering::Equal
9407                                        }
9408                                    }
9409                                    Err(_) => std::cmp::Ordering::Equal,
9410                                };
9411                                local_interp.scope_pop_hook();
9412                                ord
9413                            });
9414                        }
9415                        pmap_progress.tick();
9416                        pmap_progress.finish();
9417                        self.push(StrykeValue::array(items));
9418                        Ok(())
9419                    }
9420                    Op::PSortWithBlockFast(tag) => {
9421                        let mut items = self.pop().to_list();
9422                        let progress_flag = self.pop().is_true();
9423                        let pmap_progress = PmapProgress::new(progress_flag, 2);
9424                        pmap_progress.tick();
9425                        let mode = match *tag {
9426                            0 => SortBlockFast::Numeric,
9427                            1 => SortBlockFast::String,
9428                            2 => SortBlockFast::NumericRev,
9429                            3 => SortBlockFast::StringRev,
9430                            _ => SortBlockFast::Numeric,
9431                        };
9432                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
9433                        pmap_progress.tick();
9434                        pmap_progress.finish();
9435                        self.push(StrykeValue::array(items));
9436                        Ok(())
9437                    }
9438                    Op::PSortNoBlockParallel => {
9439                        let mut items = self.pop().to_list();
9440                        let progress_flag = self.pop().is_true();
9441                        let pmap_progress = PmapProgress::new(progress_flag, 2);
9442                        pmap_progress.tick();
9443                        items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
9444                        pmap_progress.tick();
9445                        pmap_progress.finish();
9446                        self.push(StrykeValue::array(items));
9447                        Ok(())
9448                    }
9449                    Op::FanWithBlock(block_idx) => {
9450                        let line = self.line();
9451                        let n = self.pop().to_int().max(0) as usize;
9452                        let progress_flag = self.pop().is_true();
9453                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
9454                        Ok(())
9455                    }
9456                    Op::FanWithBlockAuto(block_idx) => {
9457                        let line = self.line();
9458                        let n = self.interp.parallel_thread_count();
9459                        let progress_flag = self.pop().is_true();
9460                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
9461                        Ok(())
9462                    }
9463                    Op::FanCapWithBlock(block_idx) => {
9464                        let line = self.line();
9465                        let n = self.pop().to_int().max(0) as usize;
9466                        let progress_flag = self.pop().is_true();
9467                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
9468                        Ok(())
9469                    }
9470                    Op::FanCapWithBlockAuto(block_idx) => {
9471                        let line = self.line();
9472                        let n = self.interp.parallel_thread_count();
9473                        let progress_flag = self.pop().is_true();
9474                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
9475                        Ok(())
9476                    }
9477
9478                    Op::AsyncBlock(block_idx) => {
9479                        let block = self.blocks[*block_idx as usize].clone();
9480                        let subs = self.interp.subs.clone();
9481                        let (scope_capture, atomic_arrays, atomic_hashes) =
9482                            self.interp.scope.capture_with_atomics();
9483                        let result_slot: Arc<Mutex<Option<StrykeResult<StrykeValue>>>> =
9484                            Arc::new(Mutex::new(None));
9485                        let join_slot: Arc<Mutex<Option<std::thread::JoinHandle<()>>>> =
9486                            Arc::new(Mutex::new(None));
9487                        let rs = Arc::clone(&result_slot);
9488                        let h = std::thread::spawn(move || {
9489                            let mut local_interp = VMHelper::new();
9490                            local_interp.subs = subs;
9491                            local_interp.scope.restore_capture(&scope_capture);
9492                            local_interp
9493                                .scope
9494                                .restore_atomics(&atomic_arrays, &atomic_hashes);
9495                            local_interp.enable_parallel_guard();
9496                            local_interp.scope_push_hook();
9497                            let out = match local_interp.exec_block_no_scope(&block) {
9498                                Ok(v) => Ok(v),
9499                                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
9500                                Err(FlowOrError::Error(e)) => Err(e),
9501                                Err(_) => Ok(StrykeValue::UNDEF),
9502                            };
9503                            local_interp.scope_pop_hook();
9504                            *rs.lock() = Some(out);
9505                        });
9506                        *join_slot.lock() = Some(h);
9507                        self.push(StrykeValue::async_task(Arc::new(StrykeAsyncTask {
9508                            result: result_slot,
9509                            join: join_slot,
9510                        })));
9511                        Ok(())
9512                    }
9513                    Op::Await => {
9514                        let v = self.pop();
9515                        if let Some(t) = v.as_async_task() {
9516                            let r = t.await_result();
9517                            self.push(r?);
9518                        } else {
9519                            self.push(v);
9520                        }
9521                        Ok(())
9522                    }
9523
9524                    Op::LoadCurrentSub => {
9525                        if let Some(sub) = self.interp.current_sub_stack.last().cloned() {
9526                            self.push(StrykeValue::code_ref(sub));
9527                        } else {
9528                            self.push(StrykeValue::UNDEF);
9529                        }
9530                        Ok(())
9531                    }
9532
9533                    Op::DeferBlock => {
9534                        let coderef = self.pop();
9535                        self.interp.scope.push_defer(coderef);
9536                        Ok(())
9537                    }
9538
9539                    // ── try / catch / finally ──
9540                    Op::TryPush { .. } => {
9541                        self.try_stack.push(TryFrame {
9542                            try_push_op_idx: self.ip - 1,
9543                            state: TryState::Trying,
9544                            deferred_error: None,
9545                        });
9546                        Ok(())
9547                    }
9548                    Op::TryContinueNormal => {
9549                        let frame = self.try_stack.last().ok_or_else(|| {
9550                            StrykeError::runtime(
9551                                "TryContinueNormal without active try",
9552                                self.line(),
9553                            )
9554                        })?;
9555                        let Op::TryPush {
9556                            finally_ip,
9557                            after_ip,
9558                            ..
9559                        } = &self.ops[frame.try_push_op_idx]
9560                        else {
9561                            return Err(StrykeError::runtime(
9562                                "TryContinueNormal: corrupt try frame",
9563                                self.line(),
9564                            ));
9565                        };
9566                        if let Some(fin_ip) = *finally_ip {
9567                            self.ip = fin_ip;
9568                            Ok(())
9569                        } else {
9570                            self.try_stack.pop();
9571                            self.ip = *after_ip;
9572                            Ok(())
9573                        }
9574                    }
9575                    Op::TryFinallyEnd => {
9576                        let frame = self.try_stack.pop().ok_or_else(|| {
9577                            StrykeError::runtime("TryFinallyEnd without active try", self.line())
9578                        })?;
9579                        // If `catch` threw and we ran `finally` to clean up, re-raise the
9580                        // deferred error now that finally has completed.
9581                        if let Some(deferred) = frame.deferred_error {
9582                            return Err(deferred);
9583                        }
9584                        let Op::TryPush { after_ip, .. } = &self.ops[frame.try_push_op_idx] else {
9585                            return Err(StrykeError::runtime(
9586                                "TryFinallyEnd: corrupt try frame",
9587                                self.line(),
9588                            ));
9589                        };
9590                        self.ip = *after_ip;
9591                        Ok(())
9592                    }
9593                    Op::CatchReceive(idx) => {
9594                        let val = self.pending_catch_error.take().ok_or_else(|| {
9595                            StrykeError::runtime(
9596                                "CatchReceive without pending exception",
9597                                self.line(),
9598                            )
9599                        })?;
9600                        let n = names[*idx as usize].as_str();
9601                        self.interp.scope_pop_hook();
9602                        self.interp.scope_push_hook();
9603                        self.interp.scope.declare_scalar(n, val);
9604                        self.interp.english_note_lexical_scalar(n);
9605                        Ok(())
9606                    }
9607
9608                    Op::DeclareMySyncScalar(name_idx) => {
9609                        let val = self.pop();
9610                        let n = names[*name_idx as usize].as_str();
9611                        let stored = if val.is_mysync_deque_or_heap() {
9612                            val
9613                        } else {
9614                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
9615                        };
9616                        self.interp.scope.declare_scalar(n, stored);
9617                        Ok(())
9618                    }
9619                    Op::DeclareMySyncArray(name_idx) => {
9620                        let val = self.pop();
9621                        let n = names[*name_idx as usize].as_str();
9622                        self.interp.scope.declare_atomic_array(n, val.to_list());
9623                        Ok(())
9624                    }
9625                    Op::DeclareMySyncHash(name_idx) => {
9626                        let val = self.pop();
9627                        let n = names[*name_idx as usize].as_str();
9628                        let items = val.to_list();
9629                        let mut map = IndexMap::new();
9630                        let mut i = 0usize;
9631                        while i + 1 < items.len() {
9632                            map.insert(items[i].to_string(), items[i + 1].clone());
9633                            i += 2;
9634                        }
9635                        self.interp.scope.declare_atomic_hash(n, map);
9636                        Ok(())
9637                    }
9638                    Op::DeclareOurSyncScalar(name_idx) => {
9639                        let val = self.pop();
9640                        let n = names[*name_idx as usize].as_str();
9641                        let stored = if val.is_mysync_deque_or_heap() {
9642                            val
9643                        } else {
9644                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
9645                        };
9646                        self.interp.scope.declare_scalar(n, stored);
9647                        // Register the bare name (everything after `Pkg::`) in the
9648                        // tree-walker tracking sets so worker `$x` reads inside fan/pmap
9649                        // bodies (which run via `exec_block_no_scope`, not bytecode)
9650                        // rewrite to `Pkg::x` and find the shared cell.
9651                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
9652                        self.interp.english_note_lexical_scalar_pub(&bare);
9653                        self.interp.note_our_scalar_pub(&bare);
9654                        Ok(())
9655                    }
9656                    Op::DeclareOurSyncArray(name_idx) => {
9657                        let val = self.pop();
9658                        let n = names[*name_idx as usize].as_str();
9659                        self.interp.scope.declare_atomic_array(n, val.to_list());
9660                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
9661                        self.interp.english_note_lexical_scalar_pub(&bare);
9662                        self.interp.note_our_scalar_pub(&bare);
9663                        Ok(())
9664                    }
9665                    Op::DeclareOurSyncHash(name_idx) => {
9666                        let val = self.pop();
9667                        let n = names[*name_idx as usize].as_str();
9668                        let items = val.to_list();
9669                        let mut map = IndexMap::new();
9670                        let mut i = 0usize;
9671                        while i + 1 < items.len() {
9672                            map.insert(items[i].to_string(), items[i + 1].clone());
9673                            i += 2;
9674                        }
9675                        self.interp.scope.declare_atomic_hash(n, map);
9676                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
9677                        self.interp.english_note_lexical_scalar_pub(&bare);
9678                        self.interp.note_our_scalar_pub(&bare);
9679                        Ok(())
9680                    }
9681                    Op::RuntimeSubDecl(idx) => {
9682                        let rs = &self.runtime_sub_decls[*idx as usize];
9683                        let key = self.interp.qualify_sub_key(&rs.name);
9684                        let captured = self.interp.scope.capture();
9685                        let closure_env = if captured.is_empty() {
9686                            None
9687                        } else {
9688                            Some(captured)
9689                        };
9690                        let mut sub = StrykeSub {
9691                            name: rs.name.clone(),
9692                            params: rs.params.clone(),
9693                            body: rs.body.clone(),
9694                            closure_env,
9695                            prototype: rs.prototype.clone(),
9696                            fib_like: None,
9697                        };
9698                        sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
9699                        self.interp.subs.insert(key, Arc::new(sub));
9700                        Ok(())
9701                    }
9702                    Op::RegisterAdvice(idx) => {
9703                        let rd = &self.runtime_advice_decls[*idx as usize];
9704                        let id = self.interp.next_intercept_id;
9705                        self.interp.next_intercept_id = id.saturating_add(1);
9706                        self.interp.intercepts.push(crate::aop::Intercept {
9707                            id,
9708                            kind: rd.kind,
9709                            pattern: rd.pattern.clone(),
9710                            body: rd.body.clone(),
9711                            body_block_idx: rd.body_block_idx,
9712                        });
9713                        Ok(())
9714                    }
9715                    Op::Tie {
9716                        target_kind,
9717                        name_idx,
9718                        argc,
9719                    } => {
9720                        let argc = *argc as usize;
9721                        let mut stack_vals = Vec::with_capacity(argc);
9722                        for _ in 0..argc {
9723                            stack_vals.push(self.pop());
9724                        }
9725                        stack_vals.reverse();
9726                        let name = names[*name_idx as usize].as_str();
9727                        let line = self.line();
9728                        self.interp
9729                            .tie_execute(*target_kind, name, stack_vals, line)
9730                            .map_err(|e| e.at_line(line))?;
9731                        Ok(())
9732                    }
9733                    Op::FormatDecl(idx) => {
9734                        let (basename, lines) = &self.format_decls[*idx as usize];
9735                        let line = self.line();
9736                        self.interp
9737                            .install_format_decl(basename.as_str(), lines, line)
9738                            .map_err(|e| e.at_line(line))?;
9739                        Ok(())
9740                    }
9741                    Op::UseOverload(idx) => {
9742                        let pairs = &self.use_overload_entries[*idx as usize];
9743                        self.interp.install_use_overload_pairs(pairs);
9744                        Ok(())
9745                    }
9746                    Op::ScalarCompoundAssign { name_idx, op: op_b } => {
9747                        let rhs = self.pop();
9748                        let n = names[*name_idx as usize].as_str();
9749                        let op = scalar_compound_op_from_byte(*op_b).ok_or_else(|| {
9750                            StrykeError::runtime(
9751                                "ScalarCompoundAssign: invalid op byte",
9752                                self.line(),
9753                            )
9754                        })?;
9755                        let en = self.interp.english_scalar_name(n);
9756                        let val = self
9757                            .interp
9758                            .scalar_compound_assign_scalar_target(en, op, rhs)
9759                            .map_err(|e| e.at_line(self.line()))?;
9760                        self.push(val);
9761                        Ok(())
9762                    }
9763
9764                    Op::SetGlobalPhase(phase) => {
9765                        let s = match *phase {
9766                            crate::bytecode::GP_START => "START",
9767                            crate::bytecode::GP_UNITCHECK => "UNITCHECK",
9768                            crate::bytecode::GP_CHECK => "CHECK",
9769                            crate::bytecode::GP_INIT => "INIT",
9770                            crate::bytecode::GP_RUN => "RUN",
9771                            crate::bytecode::GP_END => "END",
9772                            _ => {
9773                                return Err(StrykeError::runtime(
9774                                    format!("SetGlobalPhase: invalid phase byte {}", phase),
9775                                    self.line(),
9776                                ));
9777                            }
9778                        };
9779                        self.interp.global_phase = s.to_string();
9780                        Ok(())
9781                    }
9782
9783                    // ── Halt ──
9784                    Op::Halt => {
9785                        self.halt = true;
9786                        Ok(())
9787                    }
9788                    Op::EvalAstExpr(idx) => {
9789                        let expr = &self.ast_eval_exprs[*idx as usize];
9790                        let val = match self.interp.eval_expr_ctx(expr, self.interp.wantarray_kind)
9791                        {
9792                            Ok(v) => v,
9793                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
9794                            Err(crate::vm_helper::FlowOrError::Flow(f)) => {
9795                                return Err(StrykeError::runtime(
9796                                    format!("unexpected flow control in EvalAstExpr: {:?}", f),
9797                                    self.line(),
9798                                ));
9799                            }
9800                        };
9801                        self.push(val);
9802                        Ok(())
9803                    }
9804                }
9805            })();
9806            if let (Some(prof), Some(t0)) = (&mut self.interp.profiler, op_prof_t0) {
9807                prof.on_line(&self.interp.file, line, t0.elapsed());
9808            }
9809            if let Err(e) = __op_res {
9810                if self.try_recover_from_exception(&e)? {
9811                    continue;
9812                }
9813                return Err(e);
9814            }
9815            // Blessed refcount drops enqueue from `StrykeValue::drop`; drain before the next opcode
9816            // so `$x = undef; f()` runs `DESTROY` before `f` (Perl semantics).
9817            if crate::pending_destroy::pending_destroy_vm_sync_needed() {
9818                self.interp.drain_pending_destroys(line)?;
9819            }
9820            if self.exit_main_dispatch {
9821                if let Some(v) = self.exit_main_dispatch_value.take() {
9822                    last = v;
9823                }
9824                break;
9825            }
9826            if self.halt {
9827                break;
9828            }
9829        }
9830
9831        if !self.stack.is_empty() {
9832            last = self.stack.last().cloned().unwrap_or(StrykeValue::UNDEF);
9833            // Drain iterators left on the stack so side effects fire
9834            // (e.g. `pmaps { system(...) } @list` with no consumer).
9835            if last.is_iterator() {
9836                let iter = last.clone().into_iterator();
9837                while iter.next_item().is_some() {}
9838                last = StrykeValue::UNDEF;
9839            }
9840        }
9841
9842        Ok(last)
9843    }
9844
9845    /// Called from Cranelift (`stryke_jit_call_sub`) to run a compiled sub by bytecode IP with `i64` args.
9846    pub(crate) fn jit_trampoline_run_sub(
9847        &mut self,
9848        entry_ip: usize,
9849        want: WantarrayCtx,
9850        args: &[i64],
9851    ) -> StrykeResult<StrykeValue> {
9852        let saved_wa = self.interp.wantarray_kind;
9853        for a in args {
9854            self.push(StrykeValue::integer(*a));
9855        }
9856        let stack_base = self.stack.len() - args.len();
9857        let mut sub_prof_t0 = None;
9858        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
9859            sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
9860            let nm_owned = self.names[nidx as usize].to_string();
9861            if let Some(p) = &mut self.interp.profiler {
9862                p.enter_sub(nm_owned.as_str());
9863            }
9864            self.interp.debugger_enter_sub(nm_owned.as_str());
9865        }
9866        self.call_stack.push(CallFrame {
9867            return_ip: 0,
9868            stack_base,
9869            scope_depth: self.interp.scope.depth(),
9870            saved_wantarray: saved_wa,
9871            jit_trampoline_return: true,
9872            block_region: false,
9873            sub_profiler_start: sub_prof_t0,
9874        });
9875        self.interp.wantarray_kind = want;
9876        self.interp.scope_push_hook();
9877        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
9878            let nm = self.names[nidx as usize].as_str();
9879            if let Some(sub) = self.interp.subs.get(nm).cloned() {
9880                if let Some(ref env) = sub.closure_env {
9881                    self.interp.scope.restore_capture(env);
9882                }
9883            }
9884        }
9885        self.ip = entry_ip;
9886        self.jit_trampoline_out = None;
9887        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_add(1);
9888        let mut op_count = 0u64;
9889        let last = StrykeValue::UNDEF;
9890        let r = self.run_main_dispatch_loop(last, &mut op_count, true);
9891        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_sub(1);
9892        r?;
9893        self.jit_trampoline_out.take().ok_or_else(|| {
9894            StrykeError::runtime("JIT trampoline: subroutine did not return", self.line())
9895        })
9896    }
9897
9898    #[inline]
9899    fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
9900        self.sub_entry_by_name.get(&name_idx).copied()
9901    }
9902
9903    /// Name pool index for a compiled sub entry IP (for closure env + JIT trampoline).
9904    fn sub_entry_name_idx(&self, entry_ip: usize) -> Option<u16> {
9905        for &(n, ip, _) in &self.sub_entries {
9906            if ip == entry_ip {
9907                return Some(n);
9908            }
9909        }
9910        None
9911    }
9912
9913    fn exec_builtin(&mut self, id: u16, args: Vec<StrykeValue>) -> StrykeResult<StrykeValue> {
9914        let line = self.line();
9915        let bid = BuiltinId::from_u16(id);
9916        match bid {
9917            Some(BuiltinId::Length) => {
9918                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9919                Ok(StrykeValue::integer(val.length_value(self.interp.utf8_pragma)))
9920            }
9921            Some(BuiltinId::Defined) => {
9922                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9923                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
9924            }
9925            Some(BuiltinId::Abs) => {
9926                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9927                Ok(StrykeValue::float(val.to_number().abs()))
9928            }
9929            Some(BuiltinId::Int) => {
9930                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9931                Ok(StrykeValue::integer(val.to_number() as i64))
9932            }
9933            Some(BuiltinId::Sqrt) => {
9934                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9935                Ok(StrykeValue::float(val.to_number().sqrt()))
9936            }
9937            Some(BuiltinId::Sin) => {
9938                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9939                Ok(StrykeValue::float(val.to_number().sin()))
9940            }
9941            Some(BuiltinId::Cos) => {
9942                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9943                Ok(StrykeValue::float(val.to_number().cos()))
9944            }
9945            Some(BuiltinId::Atan2) => {
9946                let mut it = args.into_iter();
9947                let y = it.next().unwrap_or(StrykeValue::UNDEF);
9948                let x = it.next().unwrap_or(StrykeValue::UNDEF);
9949                Ok(StrykeValue::float(y.to_number().atan2(x.to_number())))
9950            }
9951            Some(BuiltinId::Exp) => {
9952                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9953                Ok(StrykeValue::float(val.to_number().exp()))
9954            }
9955            Some(BuiltinId::Log) => {
9956                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9957                Ok(StrykeValue::float(val.to_number().ln()))
9958            }
9959            Some(BuiltinId::Rand) => {
9960                let upper = match args.len() {
9961                    0 => 1.0,
9962                    _ => args[0].to_number(),
9963                };
9964                Ok(StrykeValue::float(self.interp.perl_rand(upper)))
9965            }
9966            Some(BuiltinId::Srand) => {
9967                let seed = match args.len() {
9968                    0 => None,
9969                    _ => Some(args[0].to_number()),
9970                };
9971                Ok(StrykeValue::integer(self.interp.perl_srand(seed)))
9972            }
9973            Some(BuiltinId::Crypt) => {
9974                let mut it = args.into_iter();
9975                let p = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
9976                let salt = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
9977                Ok(StrykeValue::string(crate::crypt_util::perl_crypt(
9978                    &p, &salt,
9979                )))
9980            }
9981            Some(BuiltinId::Fc) => {
9982                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9983                Ok(StrykeValue::string(s.fc_value()))
9984            }
9985            Some(BuiltinId::Quotemeta) => {
9986                let s = args
9987                    .into_iter()
9988                    .next()
9989                    .map(|v| v.to_string())
9990                    .unwrap_or_default();
9991                Ok(StrykeValue::string(crate::perl_regex::perl_quotemeta(&s)))
9992            }
9993            Some(BuiltinId::Tan) => Ok(StrykeValue::float(
9994                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().tan(),
9995            )),
9996            Some(BuiltinId::Asin) => Ok(StrykeValue::float(
9997                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().asin(),
9998            )),
9999            Some(BuiltinId::Acos) => Ok(StrykeValue::float(
10000                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().acos(),
10001            )),
10002            Some(BuiltinId::Atan) => Ok(StrykeValue::float(
10003                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().atan(),
10004            )),
10005            Some(BuiltinId::Sinh) => Ok(StrykeValue::float(
10006                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().sinh(),
10007            )),
10008            Some(BuiltinId::Cosh) => Ok(StrykeValue::float(
10009                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().cosh(),
10010            )),
10011            Some(BuiltinId::Tanh) => Ok(StrykeValue::float(
10012                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().tanh(),
10013            )),
10014            Some(BuiltinId::Log2) => Ok(StrykeValue::float(
10015                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().log2(),
10016            )),
10017            Some(BuiltinId::Log10) => Ok(StrykeValue::float(
10018                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().log10(),
10019            )),
10020            Some(BuiltinId::Ceil) => Ok(StrykeValue::integer(
10021                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().ceil() as i64,
10022            )),
10023            Some(BuiltinId::Floor) => Ok(StrykeValue::integer(
10024                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().floor() as i64,
10025            )),
10026            // Round: 1-arg form returns Int (ties away from zero); 2-arg form
10027            // (round to N places) defers to the named-dispatch path which is
10028            // still in `builtins.rs`. CallBuiltin only emits the 1-arg form.
10029            Some(BuiltinId::Round) => Ok(StrykeValue::integer(
10030                args.into_iter().next().unwrap_or(StrykeValue::UNDEF).to_number().round() as i64,
10031            )),
10032            Some(BuiltinId::Pos) => {
10033                let key = if args.is_empty() {
10034                    "_".to_string()
10035                } else {
10036                    args[0].to_string()
10037                };
10038                Ok(self
10039                    .interp
10040                    .regex_pos
10041                    .get(&key)
10042                    .copied()
10043                    .flatten()
10044                    .map(|n| StrykeValue::integer(n as i64))
10045                    .unwrap_or(StrykeValue::UNDEF))
10046            }
10047            Some(BuiltinId::Study) => {
10048                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10049                Ok(VMHelper::study_return_value(&s.to_string()))
10050            }
10051            Some(BuiltinId::Chr) => {
10052                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10053                Ok(StrykeValue::string(val.chr_value()))
10054            }
10055            Some(BuiltinId::Ord) => {
10056                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10057                Ok(StrykeValue::integer(val.ord_value()))
10058            }
10059            Some(BuiltinId::Hex) => {
10060                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10061                Ok(StrykeValue::integer(val.hex_value()))
10062            }
10063            Some(BuiltinId::Oct) => {
10064                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10065                Ok(StrykeValue::integer(val.oct_value()))
10066            }
10067            Some(BuiltinId::Uc) => {
10068                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10069                Ok(StrykeValue::string(s.uc_value()))
10070            }
10071            Some(BuiltinId::Lc) => {
10072                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10073                Ok(StrykeValue::string(s.lc_value()))
10074            }
10075            Some(BuiltinId::Ref) => {
10076                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10077                Ok(val.ref_type())
10078            }
10079            Some(BuiltinId::Scalar) => {
10080                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10081                Ok(val.scalar_context())
10082            }
10083            Some(BuiltinId::Join) => {
10084                let mut iter = args.into_iter();
10085                let sep = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
10086                let list = iter.next().unwrap_or(StrykeValue::UNDEF).to_list();
10087                let mut strs = Vec::with_capacity(list.len());
10088                for v in list {
10089                    let s = match self.interp.stringify_value(v, line) {
10090                        Ok(s) => s,
10091                        Err(FlowOrError::Error(e)) => return Err(e),
10092                        Err(FlowOrError::Flow(_)) => {
10093                            return Err(StrykeError::runtime(
10094                                "join: unexpected control flow",
10095                                line,
10096                            ));
10097                        }
10098                    };
10099                    strs.push(s);
10100                }
10101                Ok(StrykeValue::string(strs.join(&sep)))
10102            }
10103            Some(BuiltinId::Split) => {
10104                let mut iter = args.into_iter();
10105                let pat_val = iter.next().unwrap_or(StrykeValue::string(" ".into()));
10106                // Prefer the regex source over the Display form: `qr//`'s Display is
10107                // `(?:)` (matches everywhere), which is NOT the same as Perl's empty-
10108                // pattern semantics ("split between every character"). Pulling the
10109                // source out via `regex_src_and_flags` lets us treat `//` as truly
10110                // empty so the char-split branch fires.
10111                let pat = pat_val
10112                    .regex_src_and_flags()
10113                    .map(|(s, _)| s)
10114                    .unwrap_or_else(|| pat_val.to_string());
10115                let s = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
10116                // Perl 5: splitting the empty string yields the empty list for any
10117                // pattern / limit (regex `split` on `""` would otherwise leave one field).
10118                if s.is_empty() {
10119                    return Ok(StrykeValue::array(vec![]));
10120                }
10121                // Perl LIMIT semantics:
10122                //   omitted / 0  → no truncation, strip trailing empties.
10123                //   > 0          → at most LIMIT fields, keep empties up to limit.
10124                //   < 0          → no truncation, keep all empties.
10125                let lim_signed: Option<i64> = iter.next().map(|v| v.to_int());
10126
10127                let mut parts: Vec<String> = if pat.is_empty() {
10128                    // Empty pattern → "split between every character" (Perl). The
10129                    // regex engine would also match at the boundaries, producing
10130                    // spurious empties; `s.chars()` is the right primitive.
10131                    let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
10132                    match lim_signed {
10133                        // LIMIT > 0 (Perl):
10134                        //   n < |chars|        → first n-1 chars then the tail in one field
10135                        //                        (`split //, "abcde", 3` → ("a","b","cde")).
10136                        //   n == |chars|       → chars exactly, no trailing empty.
10137                        //   n > |chars|        → chars + "" (Perl emits the end-of-string
10138                        //                        match as a final empty when LIMIT permits).
10139                        Some(l) if l > 0 => {
10140                            let n = l as usize;
10141                            if n < chars.len() {
10142                                let mut head: Vec<String> =
10143                                    chars.iter().take(n.saturating_sub(1)).cloned().collect();
10144                                let tail: String = s.chars().skip(n.saturating_sub(1)).collect();
10145                                head.push(tail);
10146                                head
10147                            } else if n == chars.len() {
10148                                chars
10149                            } else {
10150                                let mut v = chars;
10151                                v.push(String::new());
10152                                v
10153                            }
10154                        }
10155                        // LIMIT < 0 → chars + trailing empty.
10156                        Some(l) if l < 0 => {
10157                            let mut v = chars;
10158                            v.push(String::new());
10159                            v
10160                        }
10161                        // No limit / 0 → just the chars; the trailing-empty strip
10162                        // below is a no-op (`chars()` never emits one).
10163                        _ => chars,
10164                    }
10165                } else {
10166                    let re =
10167                        regex::Regex::new(&pat).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
10168                    match lim_signed {
10169                        Some(l) if l > 0 => {
10170                            re.splitn(&s, l as usize).map(|p| p.to_string()).collect()
10171                        }
10172                        _ => re.split(&s).map(|p| p.to_string()).collect(),
10173                    }
10174                };
10175
10176                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted or
10177                // zero. Positive LIMIT keeps trailing empties (capped at LIMIT).
10178                // Negative LIMIT also keeps them.
10179                let strip_trailing = matches!(lim_signed, None | Some(0));
10180                if strip_trailing {
10181                    while parts.last().is_some_and(|p| p.is_empty()) {
10182                        parts.pop();
10183                    }
10184                }
10185
10186                Ok(StrykeValue::array(
10187                    parts.into_iter().map(StrykeValue::string).collect(),
10188                ))
10189            }
10190            Some(BuiltinId::Sprintf) => {
10191                // sprintf arg list is Perl list context; flatten ranges / arrays / reverse
10192                // output into individual format arguments (same splatting as printf).
10193                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
10194                for a in args.into_iter() {
10195                    if let Some(items) = a.as_array_vec() {
10196                        flat.extend(items);
10197                    } else {
10198                        flat.push(a);
10199                    }
10200                }
10201                let args = flat;
10202                if args.is_empty() {
10203                    return Ok(StrykeValue::string(String::new()));
10204                }
10205                let fmt = args[0].to_string();
10206                let rest = &args[1..];
10207                match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
10208                    Ok(s) => Ok(StrykeValue::string(s)),
10209                    Err(FlowOrError::Error(e)) => Err(e),
10210                    Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
10211                        "sprintf: unexpected control flow",
10212                        line,
10213                    )),
10214                }
10215            }
10216            Some(BuiltinId::Reverse) => {
10217                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10218                Ok(if let Some(mut a) = val.as_array_vec() {
10219                    a.reverse();
10220                    StrykeValue::array(a)
10221                } else if let Some(s) = val.as_str() {
10222                    StrykeValue::string(s.chars().rev().collect())
10223                } else {
10224                    StrykeValue::string(val.to_string().chars().rev().collect())
10225                })
10226            }
10227            Some(BuiltinId::Die) => {
10228                // Single-ref arg: preserve the original value (hash/array/code/blessed ref)
10229                // so `$@` and `try/catch` see the ref, not a stringification.
10230                if args.len() == 1 {
10231                    let v = &args[0];
10232                    if v.as_hash_ref().is_some()
10233                        || v.as_blessed_ref().is_some()
10234                        || v.as_array_ref().is_some()
10235                        || v.as_code_ref().is_some()
10236                    {
10237                        let msg = v.to_string();
10238                        self.interp.fire_pseudosig_die(&msg, line)?;
10239                        return Err(StrykeError::die_with_value(v.clone(), msg, line));
10240                    }
10241                }
10242                let mut msg = String::new();
10243                for a in &args {
10244                    msg.push_str(&a.to_string());
10245                }
10246                if msg.is_empty() {
10247                    msg = "Died".to_string();
10248                }
10249                if !msg.ends_with('\n') {
10250                    msg.push_str(&self.interp.die_warn_at_suffix(line));
10251                    msg.push('\n');
10252                }
10253                self.interp.fire_pseudosig_die(&msg, line)?;
10254                Err(StrykeError::die(msg, line))
10255            }
10256            Some(BuiltinId::Warn) => {
10257                let mut msg = String::new();
10258                for a in &args {
10259                    msg.push_str(&a.to_string());
10260                }
10261                if msg.is_empty() {
10262                    msg = "Warning: something's wrong".to_string();
10263                }
10264                if !msg.ends_with('\n') {
10265                    msg.push_str(&self.interp.die_warn_at_suffix(line));
10266                    msg.push('\n');
10267                }
10268                self.interp.fire_pseudosig_warn(&msg, line)?;
10269                Ok(StrykeValue::integer(1))
10270            }
10271            Some(BuiltinId::Exit) => {
10272                let code = args
10273                    .into_iter()
10274                    .next()
10275                    .map(|v| v.to_int() as i32)
10276                    .unwrap_or(0);
10277                Err(StrykeError::new(
10278                    ErrorKind::Exit(code),
10279                    "",
10280                    line,
10281                    &self.interp.file,
10282                ))
10283            }
10284            Some(BuiltinId::System) => {
10285                // Perl's `system`:
10286                //   - `system "cmd args"` (single string)  → `sh -c "cmd args"`
10287                //   - `system "cmd", "arg1", "arg2", ...`  → exec the program
10288                //     directly with the trailing args as argv (no shell).
10289                // Return value is the encoded `$?` status word (exit_code << 8
10290                // on a clean exit; raw signal number for signals), not the bare
10291                // exit code, so `$rc == 0` <=> clean success and bit-twiddles
10292                // like `($? >> 8)` work on the return value too.
10293                let strs: Vec<String> = args.iter().map(|a| a.to_string()).collect();
10294                if strs.is_empty() {
10295                    self.interp.child_exit_status = -1;
10296                    return Ok(StrykeValue::integer(-1));
10297                }
10298                let status = if strs.len() == 1 {
10299                    std::process::Command::new("sh")
10300                        .arg("-c")
10301                        .arg(&strs[0])
10302                        .status()
10303                } else {
10304                    std::process::Command::new(&strs[0])
10305                        .args(&strs[1..])
10306                        .status()
10307                };
10308                match status {
10309                    Ok(s) => {
10310                        self.interp.record_child_exit_status(s);
10311                        Ok(StrykeValue::integer(self.interp.child_exit_status))
10312                    }
10313                    Err(e) => {
10314                        self.interp.errno = e.to_string();
10315                        self.interp.child_exit_status = -1;
10316                        Ok(StrykeValue::integer(-1))
10317                    }
10318                }
10319            }
10320            Some(BuiltinId::Ssh) => self.interp.ssh_builtin_execute(&args),
10321            Some(BuiltinId::Chomp) => {
10322                // Chomp modifies the variable in-place — but in CallBuiltin we get the value, not a reference.
10323                // Return the number of chars removed (like Perl).
10324                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10325                let s = val.to_string();
10326                Ok(StrykeValue::integer(if s.ends_with('\n') { 1 } else { 0 }))
10327            }
10328            Some(BuiltinId::Chop) => {
10329                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10330                let s = val.to_string();
10331                Ok(s.chars()
10332                    .last()
10333                    .map(|c| StrykeValue::string(c.to_string()))
10334                    .unwrap_or(StrykeValue::UNDEF))
10335            }
10336            Some(BuiltinId::Substr) => {
10337                if args.len() < 3 {
10338                    let s = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10339                    let off = args.get(1).map(|v| v.to_int()).unwrap_or(0);
10340                    return Ok(StrykeValue::string(s.substr2_value(off)));
10341                }
10342                let s = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10343                let off = args.get(1).map(|v| v.to_int()).unwrap_or(0);
10344                let len = args.get(2).map(|v| v.to_int()).unwrap_or(0);
10345                Ok(StrykeValue::string(s.substr3_value(off, len)))
10346            }
10347            Some(BuiltinId::Index) => {
10348                let s = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10349                let sub = args.get(1).cloned().unwrap_or(StrykeValue::UNDEF);
10350                if args.len() < 3 {
10351                    return Ok(StrykeValue::integer(s.index_value(&sub)));
10352                }
10353                let s = s.to_string();
10354                let sub = sub.to_string();
10355                // Perl: negative POS clamps to 0; POS past end returns -1
10356                // (or, for empty needle, returns POS clamped to len).
10357                let pos_raw = args.get(2).map(|v| v.to_int()).unwrap_or(0);
10358                let pos = if pos_raw < 0 {
10359                    0usize
10360                } else {
10361                    (pos_raw as usize).min(s.len())
10362                };
10363                Ok(StrykeValue::integer(
10364                    s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1),
10365                ))
10366            }
10367            Some(BuiltinId::Rindex) => {
10368                let sv = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10369                let subv = args.get(1).cloned().unwrap_or(StrykeValue::UNDEF);
10370                if args.len() < 3 {
10371                    return Ok(StrykeValue::integer(sv.rindex_value(&subv)));
10372                }
10373                let s = sv.to_string();
10374                let sub = subv.to_string();
10375                // Perl: negative POS means "search must end at or before POS";
10376                // any negative value past -1 implies no possible match.
10377                let result = match args.get(2) {
10378                    Some(v) => {
10379                        let p = v.to_int();
10380                        if p < 0 {
10381                            -1
10382                        } else {
10383                            let end = (p as usize).saturating_add(sub.len()).min(s.len());
10384                            s[..end].rfind(&sub).map(|i| i as i64).unwrap_or(-1)
10385                        }
10386                    }
10387                    None => s.rfind(&sub).map(|i| i as i64).unwrap_or(-1),
10388                };
10389                Ok(StrykeValue::integer(result))
10390            }
10391            Some(BuiltinId::Ucfirst) => {
10392                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10393                Ok(StrykeValue::string(s.ucfirst_value()))
10394            }
10395            Some(BuiltinId::Lcfirst) => {
10396                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10397                Ok(StrykeValue::string(s.lcfirst_value()))
10398            }
10399            Some(BuiltinId::Splice) => self.interp.splice_builtin_execute(&args, line),
10400            Some(BuiltinId::Unshift) => self.interp.unshift_builtin_execute(&args, line),
10401            Some(BuiltinId::Printf) => {
10402                // Flatten list-context operands (ranges, arrays, `reverse`, …) so format
10403                // placeholders line up with individual values instead of an array reference.
10404                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
10405                for a in args.into_iter() {
10406                    if let Some(items) = a.as_array_vec() {
10407                        flat.extend(items);
10408                    } else {
10409                        flat.push(a);
10410                    }
10411                }
10412                let args = flat;
10413                let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
10414                    let s = match self
10415                        .interp
10416                        .stringify_value(self.interp.scope.get_scalar("_").clone(), line)
10417                    {
10418                        Ok(s) => s,
10419                        Err(FlowOrError::Error(e)) => return Err(e),
10420                        Err(FlowOrError::Flow(_)) => {
10421                            return Err(StrykeError::runtime(
10422                                "printf: unexpected control flow",
10423                                line,
10424                            ));
10425                        }
10426                    };
10427                    (s, &[])
10428                } else {
10429                    (args[0].to_string(), &args[1..])
10430                };
10431                let out = match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
10432                    Ok(s) => s,
10433                    Err(FlowOrError::Error(e)) => return Err(e),
10434                    Err(FlowOrError::Flow(_)) => {
10435                        return Err(StrykeError::runtime(
10436                            "printf: unexpected control flow",
10437                            line,
10438                        ));
10439                    }
10440                };
10441                print!("{}", out);
10442                if self.interp.output_autoflush {
10443                    let _ = io::stdout().flush();
10444                }
10445                Ok(StrykeValue::integer(1))
10446            }
10447            Some(BuiltinId::Open) => {
10448                if args.len() < 2 {
10449                    return Err(StrykeError::runtime(
10450                        "open requires at least 2 arguments",
10451                        line,
10452                    ));
10453                }
10454                let handle_name = args[0].to_string();
10455                let mode_s = args[1].to_string();
10456                let file_opt = args.get(2).map(|v| v.to_string());
10457                self.interp
10458                    .open_builtin_execute(handle_name, mode_s, file_opt, line)
10459            }
10460            Some(BuiltinId::Close) => {
10461                let name = args
10462                    .into_iter()
10463                    .next()
10464                    .unwrap_or(StrykeValue::UNDEF)
10465                    .to_string();
10466                self.interp.close_builtin_execute(name)
10467            }
10468            Some(BuiltinId::Eof) => self.interp.eof_builtin_execute(&args, line),
10469            Some(BuiltinId::ReadLine) => {
10470                let h = if args.is_empty() {
10471                    None
10472                } else {
10473                    Some(args[0].to_string())
10474                };
10475                self.interp.readline_builtin_execute(h.as_deref())
10476            }
10477            Some(BuiltinId::ReadLineList) => {
10478                let h = if args.is_empty() {
10479                    None
10480                } else {
10481                    Some(args[0].to_string())
10482                };
10483                self.interp.readline_builtin_execute_list(h.as_deref())
10484            }
10485            Some(BuiltinId::Exec) => {
10486                let cmd = args
10487                    .iter()
10488                    .map(|a| a.to_string())
10489                    .collect::<Vec<_>>()
10490                    .join(" ");
10491                let status = std::process::Command::new("sh")
10492                    .arg("-c")
10493                    .arg(&cmd)
10494                    .status();
10495                std::process::exit(status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1));
10496            }
10497            Some(BuiltinId::Chdir) => {
10498                let path = args
10499                    .into_iter()
10500                    .next()
10501                    .unwrap_or(StrykeValue::UNDEF)
10502                    .to_string();
10503                if std::env::set_current_dir(&path).is_ok() {
10504                    if let Ok(c) = std::env::current_dir() {
10505                        self.interp.stryke_pwd = std::fs::canonicalize(&c).unwrap_or(c);
10506                    }
10507                    Ok(StrykeValue::integer(1))
10508                } else {
10509                    Ok(StrykeValue::integer(0))
10510                }
10511            }
10512            Some(BuiltinId::Mkdir) => {
10513                let path = args.first().map(|v| v.to_string()).unwrap_or_default();
10514                let path = self.interp.resolve_stryke_path_string(&path);
10515                Ok(StrykeValue::integer(
10516                    if std::fs::create_dir(&path).is_ok() {
10517                        1
10518                    } else {
10519                        0
10520                    },
10521                ))
10522            }
10523            Some(BuiltinId::Unlink) => {
10524                let mut count = 0i64;
10525                for a in &args {
10526                    let p = self.interp.resolve_stryke_path_string(&a.to_string());
10527                    if std::fs::remove_file(&p).is_ok() {
10528                        count += 1;
10529                    }
10530                }
10531                Ok(StrykeValue::integer(count))
10532            }
10533            Some(BuiltinId::Rmdir) => self.interp.builtin_rmdir_execute(&args, line),
10534            Some(BuiltinId::Utime) => self.interp.builtin_utime_execute(&args, line),
10535            Some(BuiltinId::Umask) => self.interp.builtin_umask_execute(&args, line),
10536            Some(BuiltinId::Getcwd) => self.interp.builtin_getcwd_execute(&args, line),
10537            Some(BuiltinId::Pipe) => self.interp.builtin_pipe_execute(&args, line),
10538            Some(BuiltinId::Rename) => {
10539                let old = self.interp.resolve_stryke_path_string(
10540                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10541                );
10542                let new = self.interp.resolve_stryke_path_string(
10543                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
10544                );
10545                Ok(crate::perl_fs::rename_paths(&old, &new))
10546            }
10547            Some(BuiltinId::Chmod) => {
10548                if args.is_empty() {
10549                    return Ok(StrykeValue::integer(0));
10550                }
10551                let mode = args[0].to_int();
10552                let paths: Vec<String> = args
10553                    .iter()
10554                    .skip(1)
10555                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10556                    .collect();
10557                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
10558                    &paths, mode,
10559                )))
10560            }
10561            Some(BuiltinId::Chown) => {
10562                if args.len() < 3 {
10563                    return Ok(StrykeValue::integer(0));
10564                }
10565                let uid = args[0].to_int();
10566                let gid = args[1].to_int();
10567                let paths: Vec<String> = args
10568                    .iter()
10569                    .skip(2)
10570                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10571                    .collect();
10572                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
10573                    &paths, uid, gid,
10574                )))
10575            }
10576            Some(BuiltinId::Stat) => {
10577                let path = self.interp.resolve_stryke_path_string(
10578                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10579                );
10580                Ok(crate::perl_fs::stat_path(&path, false))
10581            }
10582            Some(BuiltinId::Lstat) => {
10583                let path = self.interp.resolve_stryke_path_string(
10584                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10585                );
10586                Ok(crate::perl_fs::stat_path(&path, true))
10587            }
10588            Some(BuiltinId::Link) => {
10589                let old = self.interp.resolve_stryke_path_string(
10590                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10591                );
10592                let new = self.interp.resolve_stryke_path_string(
10593                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
10594                );
10595                Ok(crate::perl_fs::link_hard(&old, &new))
10596            }
10597            Some(BuiltinId::Symlink) => {
10598                let old = args.first().map(|v| v.to_string()).unwrap_or_default();
10599                let new = self.interp.resolve_stryke_path_string(
10600                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
10601                );
10602                Ok(crate::perl_fs::link_sym(&old, &new))
10603            }
10604            Some(BuiltinId::Readlink) => {
10605                let path = self.interp.resolve_stryke_path_string(
10606                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
10607                );
10608                Ok(crate::perl_fs::read_link(&path))
10609            }
10610            Some(BuiltinId::Glob) => {
10611                // Pass user patterns through verbatim: zsh::glob runs from OS cwd,
10612                // which `chdir` keeps in sync with `stryke_pwd`. Absolutising the
10613                // pattern up front would turn relative-pattern results into
10614                // absolute paths (breaking `glob("**(/)")` → "sub" contract,
10615                // pinned in tests/suite/glob_zsh_qualifiers.rs).
10616                let pats: Vec<String> = args.iter().map(|v| v.to_string()).collect();
10617                Ok(crate::perl_fs::glob_patterns(&pats))
10618            }
10619            Some(BuiltinId::Files) => {
10620                let dir = if args.is_empty() {
10621                    self.interp.resolve_stryke_path_string(".")
10622                } else {
10623                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10624                };
10625                Ok(crate::perl_fs::list_files(&dir))
10626            }
10627            Some(BuiltinId::Filesf) => {
10628                let dir = if args.is_empty() {
10629                    self.interp.resolve_stryke_path_string(".")
10630                } else {
10631                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10632                };
10633                Ok(crate::perl_fs::list_filesf(&dir))
10634            }
10635            Some(BuiltinId::FilesfRecursive) => {
10636                let dir = if args.is_empty() {
10637                    self.interp.resolve_stryke_path_string(".")
10638                } else {
10639                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10640                };
10641                Ok(StrykeValue::iterator(std::sync::Arc::new(
10642                    crate::value::FsWalkIterator::new(&dir, true),
10643                )))
10644            }
10645            Some(BuiltinId::Dirs) => {
10646                let dir = if args.is_empty() {
10647                    self.interp.resolve_stryke_path_string(".")
10648                } else {
10649                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10650                };
10651                Ok(crate::perl_fs::list_dirs(&dir))
10652            }
10653            Some(BuiltinId::DirsRecursive) => {
10654                let dir = if args.is_empty() {
10655                    self.interp.resolve_stryke_path_string(".")
10656                } else {
10657                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10658                };
10659                Ok(StrykeValue::iterator(std::sync::Arc::new(
10660                    crate::value::FsWalkIterator::new(&dir, false),
10661                )))
10662            }
10663            Some(BuiltinId::SymLinks) => {
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_sym_links(&dir))
10670            }
10671            Some(BuiltinId::Sockets) => {
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_sockets(&dir))
10678            }
10679            Some(BuiltinId::Pipes) => {
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_pipes(&dir))
10686            }
10687            Some(BuiltinId::BlockDevices) => {
10688                let dir = if args.is_empty() {
10689                    self.interp.resolve_stryke_path_string(".")
10690                } else {
10691                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10692                };
10693                Ok(crate::perl_fs::list_block_devices(&dir))
10694            }
10695            Some(BuiltinId::CharDevices) => {
10696                let dir = if args.is_empty() {
10697                    self.interp.resolve_stryke_path_string(".")
10698                } else {
10699                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10700                };
10701                Ok(crate::perl_fs::list_char_devices(&dir))
10702            }
10703            Some(BuiltinId::Executables) => {
10704                let dir = if args.is_empty() {
10705                    self.interp.resolve_stryke_path_string(".")
10706                } else {
10707                    self.interp.resolve_stryke_path_string(&args[0].to_string())
10708                };
10709                Ok(crate::perl_fs::list_executables(&dir))
10710            }
10711            Some(BuiltinId::GlobPar) => {
10712                let pats: Vec<String> = args
10713                    .iter()
10714                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10715                    .collect();
10716                Ok(crate::perl_fs::glob_par_patterns(&pats))
10717            }
10718            Some(BuiltinId::GlobParProgress) => {
10719                let progress = args.last().map(|v| v.is_true()).unwrap_or(false);
10720                let pats: Vec<String> = args[..args.len().saturating_sub(1)]
10721                    .iter()
10722                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
10723                    .collect();
10724                Ok(crate::perl_fs::glob_par_patterns_with_progress(
10725                    &pats, progress,
10726                ))
10727            }
10728            Some(BuiltinId::ParSed) => self.interp.builtin_par_sed(&args, line, false),
10729            Some(BuiltinId::ParSedProgress) => self.interp.builtin_par_sed(&args, line, true),
10730            Some(BuiltinId::Opendir) => {
10731                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10732                let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
10733                Ok(self.interp.opendir_handle(&handle, &path))
10734            }
10735            Some(BuiltinId::Readdir) => {
10736                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10737                Ok(self.interp.readdir_handle(&handle))
10738            }
10739            Some(BuiltinId::ReaddirList) => {
10740                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10741                Ok(self.interp.readdir_handle_list(&handle))
10742            }
10743            Some(BuiltinId::Closedir) => {
10744                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10745                Ok(self.interp.closedir_handle(&handle))
10746            }
10747            Some(BuiltinId::Rewinddir) => {
10748                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10749                Ok(self.interp.rewinddir_handle(&handle))
10750            }
10751            Some(BuiltinId::Telldir) => {
10752                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10753                Ok(self.interp.telldir_handle(&handle))
10754            }
10755            Some(BuiltinId::Seekdir) => {
10756                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
10757                let pos = args.get(1).map(|v| v.to_int().max(0) as usize).unwrap_or(0);
10758                Ok(self.interp.seekdir_handle(&handle, pos))
10759            }
10760            Some(BuiltinId::Slurp) => {
10761                let path = args
10762                    .into_iter()
10763                    .next()
10764                    .unwrap_or(StrykeValue::UNDEF)
10765                    .to_string();
10766                let path = self.interp.resolve_stryke_path_string(&path);
10767                crate::perl_fs::read_bytes_or_glob(&path)
10768                    .map(StrykeValue::bytes)
10769                    .map_err(|e| StrykeError::runtime(format!("slurp: {}", e), line))
10770            }
10771            Some(BuiltinId::Swallow) => {
10772                let path = args
10773                    .into_iter()
10774                    .next()
10775                    .unwrap_or(StrykeValue::UNDEF)
10776                    .to_string();
10777                let path = self.interp.resolve_stryke_path_string(&path);
10778                crate::perl_fs::swallow_to_hash(&path)
10779                    .map_err(|e| StrykeError::runtime(format!("swallow: {}", e), line))
10780            }
10781            Some(BuiltinId::Ingest) => {
10782                let path = args
10783                    .into_iter()
10784                    .next()
10785                    .unwrap_or(StrykeValue::UNDEF)
10786                    .to_string();
10787                let path = self.interp.resolve_stryke_path_string(&path);
10788                crate::perl_fs::ingest_iterator(&path)
10789                    .map_err(|e| StrykeError::runtime(format!("ingest: {}", e), line))
10790            }
10791            Some(BuiltinId::Burp) => {
10792                let v = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10793                crate::perl_fs::burp_hash_to_disk(&v)
10794                    .map(StrykeValue::integer)
10795                    .map_err(|e| StrykeError::runtime(format!("burp: {}", e), line))
10796            }
10797            Some(BuiltinId::God) => {
10798                let v = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10799                Ok(StrykeValue::string(crate::god::god_dump(&v)))
10800            }
10801            Some(BuiltinId::Capture) => {
10802                let cmd = args
10803                    .into_iter()
10804                    .next()
10805                    .unwrap_or(StrykeValue::UNDEF)
10806                    .to_string();
10807                crate::capture::run_capture(self.interp, &cmd, line)
10808            }
10809            Some(BuiltinId::Ppool) => {
10810                let n = args
10811                    .first()
10812                    .map(|v| v.to_int().max(0) as usize)
10813                    .unwrap_or(1);
10814                crate::ppool::create_pool(n)
10815            }
10816            Some(BuiltinId::Wantarray) => Ok(match self.interp.wantarray_kind {
10817                crate::vm_helper::WantarrayCtx::Void => StrykeValue::UNDEF,
10818                crate::vm_helper::WantarrayCtx::Scalar => StrykeValue::integer(0),
10819                crate::vm_helper::WantarrayCtx::List => StrykeValue::integer(1),
10820            }),
10821            Some(BuiltinId::FetchUrl) => {
10822                let url = args
10823                    .into_iter()
10824                    .next()
10825                    .unwrap_or(StrykeValue::UNDEF)
10826                    .to_string();
10827                ureq::get(&url)
10828                    .call()
10829                    .map_err(|e| StrykeError::runtime(format!("fetch_url: {}", e), line))
10830                    .and_then(|r| {
10831                        r.into_string()
10832                            .map(StrykeValue::string)
10833                            .map_err(|e| StrykeError::runtime(format!("fetch_url: {}", e), line))
10834                    })
10835            }
10836            Some(BuiltinId::Pchannel) => {
10837                if args.is_empty() {
10838                    Ok(crate::pchannel::create_pair())
10839                } else if args.len() == 1 {
10840                    let n = args[0].to_int().max(1) as usize;
10841                    Ok(crate::pchannel::create_bounded_pair(n))
10842                } else {
10843                    Err(StrykeError::runtime(
10844                        "pchannel() takes 0 or 1 arguments (capacity)",
10845                        line,
10846                    ))
10847                }
10848            }
10849            Some(BuiltinId::Pselect) => crate::pchannel::pselect_recv(&args, line),
10850            Some(BuiltinId::DequeNew) => {
10851                if !args.is_empty() {
10852                    return Err(StrykeError::runtime("deque() takes no arguments", line));
10853                }
10854                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
10855            }
10856            Some(BuiltinId::HeapNew) => {
10857                if args.len() != 1 {
10858                    return Err(StrykeError::runtime(
10859                        "heap() expects one comparator sub",
10860                        line,
10861                    ));
10862                }
10863                let a0 = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10864                if let Some(sub) = a0.as_code_ref() {
10865                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
10866                        items: Vec::new(),
10867                        cmp: Arc::clone(&sub),
10868                    }))))
10869                } else {
10870                    Err(StrykeError::runtime(
10871                        "heap() requires a code reference",
10872                        line,
10873                    ))
10874                }
10875            }
10876            Some(BuiltinId::BarrierNew) => {
10877                let n = args
10878                    .first()
10879                    .map(|v| v.to_int().max(1) as usize)
10880                    .unwrap_or(1);
10881                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
10882            }
10883            Some(BuiltinId::ClusterNew) => {
10884                // `cluster(HOST...)` — accepts one operand (flattened) or
10885                // multiple (each is a slot spec). Same surface as the
10886                // tree-walker arm in `vm_helper.rs` `call_named_sub`'s
10887                // "cluster" case so `pmap_on` / `~d>` see identical
10888                // `RemoteCluster` values from either dispatch path.
10889                let items = if args.len() == 1 {
10890                    args[0].to_list()
10891                } else {
10892                    args.clone()
10893                };
10894                let c = crate::value::RemoteCluster::from_list_args(&items)
10895                    .map_err(|msg| StrykeError::runtime(msg, line))?;
10896                Ok(StrykeValue::remote_cluster(std::sync::Arc::new(c)))
10897            }
10898            Some(BuiltinId::Pipeline) => {
10899                let mut items = Vec::new();
10900                for v in args {
10901                    if let Some(a) = v.as_array_vec() {
10902                        items.extend(a);
10903                    } else {
10904                        items.push(v);
10905                    }
10906                }
10907                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
10908                    source: items,
10909                    ops: Vec::new(),
10910                    has_scalar_terminal: false,
10911                    par_stream: false,
10912                    streaming: false,
10913                    streaming_workers: 0,
10914                    streaming_buffer: 256,
10915                }))))
10916            }
10917            Some(BuiltinId::ParPipeline) => {
10918                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
10919                    return crate::par_pipeline::run_par_pipeline(self.interp, &args, line);
10920                }
10921                let mut items = Vec::new();
10922                for v in args {
10923                    if let Some(a) = v.as_array_vec() {
10924                        items.extend(a);
10925                    } else {
10926                        items.push(v);
10927                    }
10928                }
10929                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
10930                    source: items,
10931                    ops: Vec::new(),
10932                    has_scalar_terminal: false,
10933                    par_stream: true,
10934                    streaming: false,
10935                    streaming_workers: 0,
10936                    streaming_buffer: 256,
10937                }))))
10938            }
10939            Some(BuiltinId::ParPipelineStream) => {
10940                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
10941                    return crate::par_pipeline::run_par_pipeline_streaming(
10942                        self.interp,
10943                        &args,
10944                        line,
10945                    );
10946                }
10947                self.interp.builtin_par_pipeline_stream_new(&args, line)
10948            }
10949            Some(BuiltinId::Each) => {
10950                let _arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10951                Ok(StrykeValue::array(vec![]))
10952            }
10953            Some(BuiltinId::Readpipe) => {
10954                let cmd = args
10955                    .into_iter()
10956                    .next()
10957                    .unwrap_or(StrykeValue::UNDEF)
10958                    .to_string();
10959                crate::capture::run_readpipe(self.interp, &cmd, line)
10960            }
10961            Some(BuiltinId::ReadpipeList) => {
10962                let cmd = args
10963                    .into_iter()
10964                    .next()
10965                    .unwrap_or(StrykeValue::UNDEF)
10966                    .to_string();
10967                let v = crate::capture::run_readpipe(self.interp, &cmd, line)?;
10968                let s = v.to_string();
10969                if s.is_empty() {
10970                    return Ok(StrykeValue::array(Vec::new()));
10971                }
10972                let mut lines = Vec::new();
10973                let mut buf = String::new();
10974                for c in s.chars() {
10975                    buf.push(c);
10976                    if c == '\n' {
10977                        lines.push(StrykeValue::string(std::mem::take(&mut buf)));
10978                    }
10979                }
10980                if !buf.is_empty() {
10981                    lines.push(StrykeValue::string(buf));
10982                }
10983                Ok(StrykeValue::array(lines))
10984            }
10985            Some(BuiltinId::Eval) => {
10986                let arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10987                self.interp.eval_nesting += 1;
10988                let out = if let Some(sub) = arg.as_code_ref() {
10989                    match self.interp.exec_block(&sub.body) {
10990                        Ok(v) => {
10991                            self.interp.clear_eval_error();
10992                            Ok(v)
10993                        }
10994                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
10995                            self.interp.set_eval_error_from_perl_error(&e);
10996                            Ok(StrykeValue::UNDEF)
10997                        }
10998                        Err(crate::vm_helper::FlowOrError::Flow(_)) => {
10999                            self.interp.clear_eval_error();
11000                            Ok(StrykeValue::UNDEF)
11001                        }
11002                    }
11003                } else {
11004                    let code = arg.to_string();
11005                    match crate::parse_and_run_string(&code, self.interp) {
11006                        Ok(v) => {
11007                            self.interp.clear_eval_error();
11008                            Ok(v)
11009                        }
11010                        Err(e) => {
11011                            self.interp.set_eval_error_from_perl_error(&e);
11012                            Ok(StrykeValue::UNDEF)
11013                        }
11014                    }
11015                };
11016                self.interp.eval_nesting -= 1;
11017                out
11018            }
11019            Some(BuiltinId::Do) => {
11020                let filename = args
11021                    .into_iter()
11022                    .next()
11023                    .unwrap_or(StrykeValue::UNDEF)
11024                    .to_string();
11025                match read_file_text_perl_compat(&filename) {
11026                    Ok(code) => {
11027                        let code = crate::data_section::strip_perl_end_marker(&code);
11028                        crate::parse_and_run_string_in_file(code, self.interp, &filename)
11029                            .or(Ok(StrykeValue::UNDEF))
11030                    }
11031                    Err(_) => Ok(StrykeValue::UNDEF),
11032                }
11033            }
11034            Some(BuiltinId::Require) => {
11035                let name = args
11036                    .into_iter()
11037                    .next()
11038                    .unwrap_or(StrykeValue::UNDEF)
11039                    .to_string();
11040                self.interp.require_execute(&name, line)
11041            }
11042            Some(BuiltinId::Bless) => {
11043                let ref_val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
11044                let class = args
11045                    .get(1)
11046                    .map(|v| v.to_string())
11047                    .unwrap_or_else(|| self.interp.scope.get_scalar("__PACKAGE__").to_string());
11048                Ok(StrykeValue::blessed(Arc::new(
11049                    crate::value::BlessedRef::new_blessed(class, ref_val),
11050                )))
11051            }
11052            Some(BuiltinId::Caller) => {
11053                // Simplified caller frame: (package, file, line, subname).
11054                // The sub name is the fully-qualified name of the currently
11055                // executing sub so logger / decorator patterns work.
11056                let sub_name = self
11057                    .interp
11058                    .current_sub_stack
11059                    .last()
11060                    .map(|s| StrykeValue::string(s.name.clone()))
11061                    .unwrap_or(StrykeValue::UNDEF);
11062                let pkg = self.interp.current_package();
11063                Ok(StrykeValue::array(vec![
11064                    StrykeValue::string(pkg),
11065                    StrykeValue::string(self.interp.file.clone()),
11066                    StrykeValue::integer(line as i64),
11067                    sub_name,
11068                ]))
11069            }
11070            // Parallel ops (shouldn't reach here — handled by block ops)
11071            Some(BuiltinId::PMap)
11072            | Some(BuiltinId::PGrep)
11073            | Some(BuiltinId::PFor)
11074            | Some(BuiltinId::PSort)
11075            | Some(BuiltinId::Fan)
11076            | Some(BuiltinId::MapBlock)
11077            | Some(BuiltinId::GrepBlock)
11078            | Some(BuiltinId::SortBlock)
11079            | Some(BuiltinId::Sort) => Ok(StrykeValue::UNDEF),
11080            _ => Err(StrykeError::runtime(
11081                format!("Unimplemented builtin {:?}", bid),
11082                line,
11083            )),
11084        }
11085    }
11086}
11087
11088/// Integer fast-path comparison helper.
11089#[inline]
11090/// True when both values are non-numeric strings — used by `==` / `!=` in
11091/// stryke non-compat mode to decide whether to fall back to string compare.
11092/// "Numeric string" matches `looks_like_number` semantics (digits, optional
11093/// sign, optional decimal/exponent). Non-string values (refs, undef) are
11094/// excluded so `==` on objects keeps its overload-driven behavior.
11095fn both_non_numeric_strings(a: &StrykeValue, b: &StrykeValue) -> bool {
11096    if !a.is_string_like() || !b.is_string_like() {
11097        return false;
11098    }
11099    let sa = a.to_string();
11100    let sb = b.to_string();
11101    !looks_numeric(&sa) && !looks_numeric(&sb)
11102}
11103
11104#[inline]
11105fn looks_numeric(s: &str) -> bool {
11106    let t = s.trim();
11107    if t.is_empty() {
11108        return false;
11109    }
11110    t.parse::<f64>().is_ok()
11111}
11112
11113fn int_cmp(
11114    a: &StrykeValue,
11115    b: &StrykeValue,
11116    int_op: fn(&i64, &i64) -> bool,
11117    float_op: fn(f64, f64) -> bool,
11118) -> StrykeValue {
11119    if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
11120        StrykeValue::integer(if int_op(&x, &y) { 1 } else { 0 })
11121    } else {
11122        StrykeValue::integer(if float_op(a.to_number(), b.to_number()) {
11123            1
11124        } else {
11125            0
11126        })
11127    }
11128}
11129
11130/// Block JIT hook: string concat with `use overload` / `""` stringify (matches [`Op::Concat`]).
11131///
11132/// # Safety
11133///
11134/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call.
11135#[no_mangle]
11136pub unsafe extern "C" fn stryke_jit_concat_vm(vm: *mut std::ffi::c_void, a: i64, b: i64) -> i64 {
11137    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
11138    let pa = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(a));
11139    let pb = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(b));
11140    match vm.concat_stack_values(pa, pb) {
11141        Ok(pv) => pv.raw_bits() as i64,
11142        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
11143    }
11144}
11145
11146/// Cranelift host hook: re-enter the VM for [`Op::Call`] to a compiled sub (stack-args, scalar `i64` args).
11147/// `sub_ip`, `argc`, `wa` are passed as `i64` for a uniform Cranelift signature.
11148///
11149/// # Safety
11150///
11151/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call (JIT only
11152/// invokes this while the VM is executing).
11153#[no_mangle]
11154pub unsafe extern "C" fn stryke_jit_call_sub(
11155    vm: *mut std::ffi::c_void,
11156    sub_ip: i64,
11157    argc: i64,
11158    wa: i64,
11159    a0: i64,
11160    a1: i64,
11161    a2: i64,
11162    a3: i64,
11163    a4: i64,
11164    a5: i64,
11165    a6: i64,
11166    a7: i64,
11167) -> i64 {
11168    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
11169    let want = WantarrayCtx::from_byte(wa as u8);
11170    if want != WantarrayCtx::Scalar {
11171        return StrykeValue::UNDEF.raw_bits() as i64;
11172    }
11173    let argc = argc.clamp(0, 8) as usize;
11174    let args = [a0, a1, a2, a3, a4, a5, a6, a7];
11175    let args = &args[..argc];
11176    match vm.jit_trampoline_run_sub(sub_ip as usize, want, args) {
11177        Ok(pv) => {
11178            if let Some(n) = pv.as_integer() {
11179                n
11180            } else {
11181                pv.raw_bits() as i64
11182            }
11183        }
11184        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
11185    }
11186}
11187
11188#[cfg(test)]
11189mod tests {
11190    use super::*;
11191    use crate::bytecode::{Chunk, Op};
11192    use crate::value::StrykeValue;
11193
11194    fn run_chunk(chunk: &Chunk) -> StrykeResult<StrykeValue> {
11195        let mut interp = VMHelper::new();
11196        let mut vm = VM::new(chunk, &mut interp);
11197        vm.execute()
11198    }
11199
11200    /// Block-JIT-eligible loop: `for ($i=0; $i<limit; $i++) { $sum += $i }` — sum 0..limit-1.
11201    fn block_jit_sum_chunk(limit: i64) -> Chunk {
11202        let mut c = Chunk::new();
11203        let ni = c.intern_name("i");
11204        let ns = c.intern_name("sum");
11205        c.emit(Op::LoadInt(0), 1);
11206        c.emit(Op::DeclareScalarSlot(0, ni), 1);
11207        c.emit(Op::LoadInt(0), 1);
11208        c.emit(Op::DeclareScalarSlot(1, ns), 1);
11209        c.emit(Op::GetScalarSlot(0), 1);
11210        c.emit(Op::LoadInt(limit), 1);
11211        c.emit(Op::NumLt, 1);
11212        c.emit(Op::JumpIfFalse(15), 1);
11213        c.emit(Op::GetScalarSlot(1), 1);
11214        c.emit(Op::GetScalarSlot(0), 1);
11215        c.emit(Op::Add, 1);
11216        c.emit(Op::SetScalarSlot(1), 1);
11217        c.emit(Op::PostIncSlot(0), 1);
11218        c.emit(Op::Pop, 1);
11219        c.emit(Op::Jump(4), 1);
11220        c.emit(Op::GetScalarSlot(1), 1);
11221        c.emit(Op::Halt, 1);
11222        c
11223    }
11224
11225    #[test]
11226    fn jit_disabled_same_result_as_jit_block_loop() {
11227        let limit = 500i64;
11228        let chunk = block_jit_sum_chunk(limit);
11229        let expect = limit * (limit - 1) / 2;
11230
11231        let mut interp_on = VMHelper::new();
11232        let mut vm_on = VM::new(&chunk, &mut interp_on);
11233        assert_eq!(vm_on.execute().expect("vm").to_int(), expect);
11234
11235        let mut interp_off = VMHelper::new();
11236        let mut vm_off = VM::new(&chunk, &mut interp_off);
11237        vm_off.set_jit_enabled(false);
11238        assert_eq!(vm_off.execute().expect("vm").to_int(), expect);
11239    }
11240
11241    #[test]
11242    fn vm_add_two_integers() {
11243        let mut c = Chunk::new();
11244        c.emit(Op::LoadInt(2), 1);
11245        c.emit(Op::LoadInt(3), 1);
11246        c.emit(Op::Add, 1);
11247        c.emit(Op::Halt, 1);
11248        let v = run_chunk(&c).expect("vm");
11249        assert_eq!(v.to_int(), 5);
11250    }
11251
11252    #[test]
11253    fn vm_sub_mul_div() {
11254        let mut c = Chunk::new();
11255        c.emit(Op::LoadInt(10), 1);
11256        c.emit(Op::LoadInt(3), 1);
11257        c.emit(Op::Sub, 1);
11258        c.emit(Op::Halt, 1);
11259        assert_eq!(run_chunk(&c).expect("vm").to_int(), 7);
11260
11261        let mut c = Chunk::new();
11262        c.emit(Op::LoadInt(6), 1);
11263        c.emit(Op::LoadInt(7), 1);
11264        c.emit(Op::Mul, 1);
11265        c.emit(Op::Halt, 1);
11266        assert_eq!(run_chunk(&c).expect("vm").to_int(), 42);
11267
11268        let mut c = Chunk::new();
11269        c.emit(Op::LoadInt(20), 1);
11270        c.emit(Op::LoadInt(4), 1);
11271        c.emit(Op::Div, 1);
11272        c.emit(Op::Halt, 1);
11273        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
11274    }
11275
11276    #[test]
11277    fn vm_mod_and_pow() {
11278        let mut c = Chunk::new();
11279        c.emit(Op::LoadInt(17), 1);
11280        c.emit(Op::LoadInt(5), 1);
11281        c.emit(Op::Mod, 1);
11282        c.emit(Op::Halt, 1);
11283        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11284
11285        let mut c = Chunk::new();
11286        c.emit(Op::LoadInt(2), 1);
11287        c.emit(Op::LoadInt(3), 1);
11288        c.emit(Op::Pow, 1);
11289        c.emit(Op::Halt, 1);
11290        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
11291    }
11292
11293    #[test]
11294    fn vm_negate() {
11295        let mut c = Chunk::new();
11296        c.emit(Op::LoadInt(7), 1);
11297        c.emit(Op::Negate, 1);
11298        c.emit(Op::Halt, 1);
11299        assert_eq!(run_chunk(&c).expect("vm").to_int(), -7);
11300    }
11301
11302    #[test]
11303    fn vm_dup_and_pop() {
11304        let mut c = Chunk::new();
11305        c.emit(Op::LoadInt(1), 1);
11306        c.emit(Op::Dup, 1);
11307        c.emit(Op::Add, 1);
11308        c.emit(Op::Halt, 1);
11309        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11310
11311        let mut c = Chunk::new();
11312        c.emit(Op::LoadInt(1), 1);
11313        c.emit(Op::LoadInt(2), 1);
11314        c.emit(Op::Pop, 1);
11315        c.emit(Op::Halt, 1);
11316        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11317    }
11318
11319    #[test]
11320    fn vm_set_get_scalar() {
11321        let mut c = Chunk::new();
11322        let i = c.intern_name("v");
11323        c.emit(Op::LoadInt(99), 1);
11324        c.emit(Op::SetScalar(i), 1);
11325        c.emit(Op::GetScalar(i), 1);
11326        c.emit(Op::Halt, 1);
11327        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
11328    }
11329
11330    #[test]
11331    fn vm_scalar_plain_roundtrip_and_keep() {
11332        let mut c = Chunk::new();
11333        let i = c.intern_name("plainvar");
11334        c.emit(Op::LoadInt(99), 1);
11335        c.emit(Op::SetScalarPlain(i), 1);
11336        c.emit(Op::GetScalarPlain(i), 1);
11337        c.emit(Op::Halt, 1);
11338        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
11339
11340        let mut c = Chunk::new();
11341        let k = c.intern_name("keepme");
11342        c.emit(Op::LoadInt(5), 1);
11343        c.emit(Op::SetScalarKeepPlain(k), 1);
11344        c.emit(Op::Halt, 1);
11345        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
11346    }
11347
11348    #[test]
11349    fn vm_get_scalar_plain_skips_special_global_zero() {
11350        let mut c = Chunk::new();
11351        let idx = c.intern_name("0");
11352        c.emit(Op::GetScalar(idx), 1);
11353        c.emit(Op::Halt, 1);
11354        assert_eq!(run_chunk(&c).expect("vm").to_string(), "stryke");
11355
11356        let mut c = Chunk::new();
11357        let idx = c.intern_name("0");
11358        c.emit(Op::GetScalarPlain(idx), 1);
11359        c.emit(Op::Halt, 1);
11360        assert!(run_chunk(&c).expect("vm").is_undef());
11361    }
11362
11363    #[test]
11364    fn vm_slot_pre_post_inc_dec() {
11365        let mut c = Chunk::new();
11366        c.emit(Op::LoadInt(10), 1);
11367        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11368        c.emit(Op::PostIncSlot(0), 1);
11369        c.emit(Op::Pop, 1);
11370        c.emit(Op::GetScalarSlot(0), 1);
11371        c.emit(Op::Halt, 1);
11372        assert_eq!(run_chunk(&c).expect("vm").to_int(), 11);
11373
11374        let mut c = Chunk::new();
11375        c.emit(Op::LoadInt(0), 1);
11376        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11377        c.emit(Op::PreIncSlot(0), 1);
11378        c.emit(Op::Halt, 1);
11379        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11380
11381        let mut c = Chunk::new();
11382        c.emit(Op::LoadInt(5), 1);
11383        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11384        c.emit(Op::PreDecSlot(0), 1);
11385        c.emit(Op::Halt, 1);
11386        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
11387
11388        let mut c = Chunk::new();
11389        c.emit(Op::LoadInt(3), 1);
11390        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
11391        c.emit(Op::PostDecSlot(0), 1);
11392        c.emit(Op::Pop, 1);
11393        c.emit(Op::GetScalarSlot(0), 1);
11394        c.emit(Op::Halt, 1);
11395        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11396    }
11397
11398    #[test]
11399    fn vm_str_eq_ne_heap_strings() {
11400        let mut c = Chunk::new();
11401        let a = c.add_constant(StrykeValue::string("same".into()));
11402        let b = c.add_constant(StrykeValue::string("same".into()));
11403        c.emit(Op::LoadConst(a), 1);
11404        c.emit(Op::LoadConst(b), 1);
11405        c.emit(Op::StrEq, 1);
11406        c.emit(Op::Halt, 1);
11407        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11408
11409        let mut c = Chunk::new();
11410        let a = c.add_constant(StrykeValue::string("a".into()));
11411        let b = c.add_constant(StrykeValue::string("b".into()));
11412        c.emit(Op::LoadConst(a), 1);
11413        c.emit(Op::LoadConst(b), 1);
11414        c.emit(Op::StrNe, 1);
11415        c.emit(Op::Halt, 1);
11416        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11417    }
11418
11419    #[test]
11420    fn vm_num_eq_ine() {
11421        let mut c = Chunk::new();
11422        c.emit(Op::LoadInt(1), 1);
11423        c.emit(Op::LoadInt(1), 1);
11424        c.emit(Op::NumEq, 1);
11425        c.emit(Op::Halt, 1);
11426        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11427
11428        let mut c = Chunk::new();
11429        c.emit(Op::LoadInt(1), 1);
11430        c.emit(Op::LoadInt(2), 1);
11431        c.emit(Op::NumNe, 1);
11432        c.emit(Op::Halt, 1);
11433        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11434    }
11435
11436    #[test]
11437    fn vm_num_ordering() {
11438        for (a, b, op, want) in [
11439            (1i64, 2i64, Op::NumLt, 1),
11440            (3i64, 2i64, Op::NumGt, 1),
11441            (2i64, 2i64, Op::NumLe, 1),
11442            (2i64, 2i64, Op::NumGe, 1),
11443        ] {
11444            let mut c = Chunk::new();
11445            c.emit(Op::LoadInt(a), 1);
11446            c.emit(Op::LoadInt(b), 1);
11447            c.emit(op, 1);
11448            c.emit(Op::Halt, 1);
11449            assert_eq!(run_chunk(&c).expect("vm").to_int(), want);
11450        }
11451    }
11452
11453    #[test]
11454    fn vm_concat_and_str_cmp() {
11455        let mut c = Chunk::new();
11456        let i1 = c.add_constant(StrykeValue::string("a".into()));
11457        let i2 = c.add_constant(StrykeValue::string("b".into()));
11458        c.emit(Op::LoadConst(i1), 1);
11459        c.emit(Op::LoadConst(i2), 1);
11460        c.emit(Op::Concat, 1);
11461        c.emit(Op::Halt, 1);
11462        assert_eq!(run_chunk(&c).expect("vm").to_string(), "ab");
11463
11464        let mut c = Chunk::new();
11465        let i1 = c.add_constant(StrykeValue::string("a".into()));
11466        let i2 = c.add_constant(StrykeValue::string("b".into()));
11467        c.emit(Op::LoadConst(i1), 1);
11468        c.emit(Op::LoadConst(i2), 1);
11469        c.emit(Op::StrCmp, 1);
11470        c.emit(Op::Halt, 1);
11471        let v = run_chunk(&c).expect("vm");
11472        assert!(v.to_int() < 0);
11473    }
11474
11475    #[test]
11476    fn vm_log_not() {
11477        let mut c = Chunk::new();
11478        c.emit(Op::LoadInt(0), 1);
11479        c.emit(Op::LogNot, 1);
11480        c.emit(Op::Halt, 1);
11481        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
11482    }
11483
11484    #[test]
11485    fn vm_bit_and_or_xor_not() {
11486        let mut c = Chunk::new();
11487        c.emit(Op::LoadInt(0b1100), 1);
11488        c.emit(Op::LoadInt(0b1010), 1);
11489        c.emit(Op::BitAnd, 1);
11490        c.emit(Op::Halt, 1);
11491        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1000);
11492
11493        let mut c = Chunk::new();
11494        c.emit(Op::LoadInt(0b1100), 1);
11495        c.emit(Op::LoadInt(0b1010), 1);
11496        c.emit(Op::BitOr, 1);
11497        c.emit(Op::Halt, 1);
11498        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1110);
11499
11500        let mut c = Chunk::new();
11501        c.emit(Op::LoadInt(0b1100), 1);
11502        c.emit(Op::LoadInt(0b1010), 1);
11503        c.emit(Op::BitXor, 1);
11504        c.emit(Op::Halt, 1);
11505        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b0110);
11506
11507        let mut c = Chunk::new();
11508        c.emit(Op::LoadInt(0), 1);
11509        c.emit(Op::BitNot, 1);
11510        c.emit(Op::Halt, 1);
11511        assert!((run_chunk(&c).expect("vm").to_int() & 0xFF) != 0);
11512    }
11513
11514    #[test]
11515    fn vm_shl_shr() {
11516        let mut c = Chunk::new();
11517        c.emit(Op::LoadInt(1), 1);
11518        c.emit(Op::LoadInt(3), 1);
11519        c.emit(Op::Shl, 1);
11520        c.emit(Op::Halt, 1);
11521        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
11522
11523        let mut c = Chunk::new();
11524        c.emit(Op::LoadInt(16), 1);
11525        c.emit(Op::LoadInt(2), 1);
11526        c.emit(Op::Shr, 1);
11527        c.emit(Op::Halt, 1);
11528        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
11529    }
11530
11531    #[test]
11532    fn vm_load_undef_float_constant() {
11533        let mut c = Chunk::new();
11534        c.emit(Op::LoadUndef, 1);
11535        c.emit(Op::Halt, 1);
11536        assert!(run_chunk(&c).expect("vm").is_undef());
11537
11538        let mut c = Chunk::new();
11539        c.emit(Op::LoadFloat(2.5), 1);
11540        c.emit(Op::Halt, 1);
11541        assert!((run_chunk(&c).expect("vm").to_number() - 2.5).abs() < 1e-9);
11542    }
11543
11544    #[test]
11545    fn vm_jump_skips_ops() {
11546        let mut c = Chunk::new();
11547        let j = c.emit(Op::Jump(0), 1);
11548        c.emit(Op::LoadInt(1), 1);
11549        c.emit(Op::LoadInt(2), 1);
11550        c.emit(Op::Add, 1);
11551        c.patch_jump_here(j);
11552        c.emit(Op::LoadInt(40), 1);
11553        c.emit(Op::Halt, 1);
11554        assert_eq!(run_chunk(&c).expect("vm").to_int(), 40);
11555    }
11556
11557    #[test]
11558    fn vm_jump_if_false() {
11559        let mut c = Chunk::new();
11560        c.emit(Op::LoadInt(0), 1);
11561        let j = c.emit(Op::JumpIfFalse(0), 1);
11562        c.emit(Op::LoadInt(1), 1);
11563        c.emit(Op::Halt, 1);
11564        c.patch_jump_here(j);
11565        c.emit(Op::LoadInt(2), 1);
11566        c.emit(Op::Halt, 1);
11567        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
11568    }
11569
11570    #[test]
11571    fn vm_call_builtin_defined() {
11572        let mut c = Chunk::new();
11573        c.emit(Op::LoadUndef, 1);
11574        c.emit(Op::CallBuiltin(BuiltinId::Defined as u16, 1), 1);
11575        c.emit(Op::Halt, 1);
11576        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0);
11577    }
11578
11579    #[test]
11580    fn vm_call_builtin_length_string() {
11581        let mut c = Chunk::new();
11582        let idx = c.add_constant(StrykeValue::string("abc".into()));
11583        c.emit(Op::LoadConst(idx), 1);
11584        c.emit(Op::CallBuiltin(BuiltinId::Length as u16, 1), 1);
11585        c.emit(Op::Halt, 1);
11586        assert_eq!(run_chunk(&c).expect("vm").to_int(), 3);
11587    }
11588
11589    #[test]
11590    fn vm_make_array_two() {
11591        let mut c = Chunk::new();
11592        c.emit(Op::LoadInt(1), 1);
11593        c.emit(Op::LoadInt(2), 1);
11594        c.emit(Op::MakeArray(2), 1);
11595        c.emit(Op::Halt, 1);
11596        let v = run_chunk(&c).expect("vm");
11597        let a = v.as_array_vec().expect("array");
11598        assert_eq!(a.len(), 2);
11599        assert_eq!(a[0].to_int(), 1);
11600        assert_eq!(a[1].to_int(), 2);
11601    }
11602
11603    #[test]
11604    fn vm_spaceship() {
11605        let mut c = Chunk::new();
11606        c.emit(Op::LoadInt(1), 1);
11607        c.emit(Op::LoadInt(2), 1);
11608        c.emit(Op::Spaceship, 1);
11609        c.emit(Op::Halt, 1);
11610        assert_eq!(run_chunk(&c).expect("vm").to_int(), -1);
11611    }
11612
11613    #[test]
11614    fn compiled_try_catch_catches_die_via_vm() {
11615        let program = crate::parse(
11616            r#"
11617        try {
11618            die "boom";
11619        } catch ($err) {
11620            42;
11621        }
11622    "#,
11623        )
11624        .expect("parse");
11625        let chunk = crate::compiler::Compiler::new()
11626            .compile_program(&program)
11627            .expect("compile");
11628        let tp = chunk
11629            .ops
11630            .iter()
11631            .position(|o| matches!(o, Op::TryPush { .. }))
11632            .expect("TryPush op");
11633        match &chunk.ops[tp] {
11634            Op::TryPush {
11635                catch_ip, after_ip, ..
11636            } => {
11637                assert_ne!(*catch_ip, 0, "catch_ip must be patched");
11638                assert_ne!(*after_ip, 0, "after_ip must be patched");
11639            }
11640            _ => unreachable!(),
11641        }
11642        let mut interp = VMHelper::new();
11643        let mut vm = VM::new(&chunk, &mut interp);
11644        vm.set_jit_enabled(false);
11645        let v = vm.execute().expect("vm should catch die");
11646        assert_eq!(v.to_int(), 42);
11647    }
11648}