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