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
9use caseless::default_case_fold_str;
10
11use crate::ast::{BinOp, Block, Expr, MatchArm, PerlTypeName, Sigil, SubSigParam};
12use crate::bytecode::{BuiltinId, Chunk, Op, RuntimeSubDecl, SpliceExprEntry};
13use crate::compiler::scalar_compound_op_from_byte;
14use crate::error::{ErrorKind, StrykeError, StrykeResult};
15use crate::perl_fs::read_file_text_perl_compat;
16use crate::pmap_progress::{FanProgress, PmapProgress};
17use crate::sort_fast::{sort_magic_cmp, SortBlockFast};
18use crate::value::{
19    perl_list_range_expand, perl_shl_i64, perl_shr_i64, PerlBarrier, PerlHeap, PipelineInner,
20    PipelineOp, StrykeAsyncTask, StrykeSub, StrykeValue,
21};
22use crate::vm_helper::{
23    fold_preduce_init_step, merge_preduce_init_partials, preduce_init_fold_identity, Flow,
24    FlowOrError, VMHelper, WantarrayCtx,
25};
26use parking_lot::Mutex;
27use std::sync::Barrier;
28
29/// Stable reference for empty-stack [`VM::peek`] (not a temporary `&StrykeValue::UNDEF`).
30static PEEK_UNDEF: StrykeValue = StrykeValue::UNDEF;
31
32/// Immutable snapshot of [`VM`] pools for rayon workers (cheap `Arc` clones; no `&mut VM` in closures).
33struct ParallelBlockVmShared {
34    ops: Arc<Vec<Op>>,
35    names: Arc<Vec<String>>,
36    constants: Arc<Vec<StrykeValue>>,
37    lines: Arc<Vec<usize>>,
38    sub_entries: Vec<(u16, usize, bool)>,
39    static_sub_calls: Vec<(usize, bool, u16)>,
40    blocks: Vec<Block>,
41    code_ref_sigs: Vec<Vec<SubSigParam>>,
42    block_bytecode_ranges: Vec<Option<(usize, usize)>>,
43    map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
44    grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
45    regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
46    given_entries: Vec<(Expr, Block)>,
47    given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
48    eval_timeout_entries: Vec<(Expr, Block)>,
49    eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
50    algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
51    algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
52    par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
53    par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
54    pwatch_entries: Vec<(Expr, Expr)>,
55    substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
56    keys_expr_entries: Vec<Expr>,
57    keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
58    map_expr_entries: Vec<Expr>,
59    grep_expr_entries: Vec<Expr>,
60    regex_flip_flop_rhs_expr_entries: Vec<Expr>,
61    values_expr_entries: Vec<Expr>,
62    values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
63    delete_expr_entries: Vec<Expr>,
64    exists_expr_entries: Vec<Expr>,
65    push_expr_entries: Vec<(Expr, Vec<Expr>)>,
66    pop_expr_entries: Vec<Expr>,
67    shift_expr_entries: Vec<Expr>,
68    unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
69    splice_expr_entries: Vec<SpliceExprEntry>,
70    lvalues: Vec<Expr>,
71    ast_eval_exprs: Vec<Expr>,
72    format_decls: Vec<(String, Vec<String>)>,
73    use_overload_entries: Vec<Vec<(String, String)>>,
74    runtime_sub_decls: Arc<Vec<RuntimeSubDecl>>,
75    runtime_advice_decls: Arc<Vec<crate::bytecode::RuntimeAdviceDecl>>,
76    jit_sub_invoke_threshold: u32,
77    op_len_plus_one: usize,
78    static_sub_closure_subs: Vec<Option<Arc<StrykeSub>>>,
79    sub_entry_by_name: HashMap<u16, (usize, bool)>,
80}
81
82impl ParallelBlockVmShared {
83    fn from_vm(vm: &VM<'_>) -> Self {
84        let n = vm.ops.len().saturating_add(1);
85        Self {
86            ops: Arc::clone(&vm.ops),
87            names: Arc::clone(&vm.names),
88            constants: Arc::clone(&vm.constants),
89            lines: Arc::clone(&vm.lines),
90            sub_entries: vm.sub_entries.clone(),
91            static_sub_calls: vm.static_sub_calls.clone(),
92            blocks: vm.blocks.clone(),
93            code_ref_sigs: vm.code_ref_sigs.clone(),
94            block_bytecode_ranges: vm.block_bytecode_ranges.clone(),
95            map_expr_bytecode_ranges: vm.map_expr_bytecode_ranges.clone(),
96            grep_expr_bytecode_ranges: vm.grep_expr_bytecode_ranges.clone(),
97            regex_flip_flop_rhs_expr_bytecode_ranges: vm
98                .regex_flip_flop_rhs_expr_bytecode_ranges
99                .clone(),
100            given_entries: vm.given_entries.clone(),
101            given_topic_bytecode_ranges: vm.given_topic_bytecode_ranges.clone(),
102            eval_timeout_entries: vm.eval_timeout_entries.clone(),
103            eval_timeout_expr_bytecode_ranges: vm.eval_timeout_expr_bytecode_ranges.clone(),
104            algebraic_match_entries: vm.algebraic_match_entries.clone(),
105            algebraic_match_subject_bytecode_ranges: vm
106                .algebraic_match_subject_bytecode_ranges
107                .clone(),
108            par_lines_entries: vm.par_lines_entries.clone(),
109            par_walk_entries: vm.par_walk_entries.clone(),
110            pwatch_entries: vm.pwatch_entries.clone(),
111            substr_four_arg_entries: vm.substr_four_arg_entries.clone(),
112            keys_expr_entries: vm.keys_expr_entries.clone(),
113            keys_expr_bytecode_ranges: vm.keys_expr_bytecode_ranges.clone(),
114            map_expr_entries: vm.map_expr_entries.clone(),
115            grep_expr_entries: vm.grep_expr_entries.clone(),
116            regex_flip_flop_rhs_expr_entries: vm.regex_flip_flop_rhs_expr_entries.clone(),
117            values_expr_entries: vm.values_expr_entries.clone(),
118            values_expr_bytecode_ranges: vm.values_expr_bytecode_ranges.clone(),
119            delete_expr_entries: vm.delete_expr_entries.clone(),
120            exists_expr_entries: vm.exists_expr_entries.clone(),
121            push_expr_entries: vm.push_expr_entries.clone(),
122            pop_expr_entries: vm.pop_expr_entries.clone(),
123            shift_expr_entries: vm.shift_expr_entries.clone(),
124            unshift_expr_entries: vm.unshift_expr_entries.clone(),
125            splice_expr_entries: vm.splice_expr_entries.clone(),
126            lvalues: vm.lvalues.clone(),
127            ast_eval_exprs: vm.ast_eval_exprs.clone(),
128            format_decls: vm.format_decls.clone(),
129            use_overload_entries: vm.use_overload_entries.clone(),
130            runtime_sub_decls: Arc::clone(&vm.runtime_sub_decls),
131            runtime_advice_decls: Arc::clone(&vm.runtime_advice_decls),
132            jit_sub_invoke_threshold: vm.jit_sub_invoke_threshold,
133            op_len_plus_one: n,
134            static_sub_closure_subs: vm.static_sub_closure_subs.clone(),
135            sub_entry_by_name: vm.sub_entry_by_name.clone(),
136        }
137    }
138
139    fn worker_vm<'a>(&self, interp: &'a mut VMHelper) -> VM<'a> {
140        let n = self.op_len_plus_one;
141        VM {
142            names: Arc::clone(&self.names),
143            constants: Arc::clone(&self.constants),
144            ops: Arc::clone(&self.ops),
145            lines: Arc::clone(&self.lines),
146            sub_entries: self.sub_entries.clone(),
147            static_sub_calls: self.static_sub_calls.clone(),
148            blocks: self.blocks.clone(),
149            code_ref_sigs: self.code_ref_sigs.clone(),
150            block_bytecode_ranges: self.block_bytecode_ranges.clone(),
151            map_expr_bytecode_ranges: self.map_expr_bytecode_ranges.clone(),
152            grep_expr_bytecode_ranges: self.grep_expr_bytecode_ranges.clone(),
153            regex_flip_flop_rhs_expr_bytecode_ranges: self
154                .regex_flip_flop_rhs_expr_bytecode_ranges
155                .clone(),
156            given_entries: self.given_entries.clone(),
157            given_topic_bytecode_ranges: self.given_topic_bytecode_ranges.clone(),
158            eval_timeout_entries: self.eval_timeout_entries.clone(),
159            eval_timeout_expr_bytecode_ranges: self.eval_timeout_expr_bytecode_ranges.clone(),
160            algebraic_match_entries: self.algebraic_match_entries.clone(),
161            algebraic_match_subject_bytecode_ranges: self
162                .algebraic_match_subject_bytecode_ranges
163                .clone(),
164            par_lines_entries: self.par_lines_entries.clone(),
165            par_walk_entries: self.par_walk_entries.clone(),
166            pwatch_entries: self.pwatch_entries.clone(),
167            substr_four_arg_entries: self.substr_four_arg_entries.clone(),
168            keys_expr_entries: self.keys_expr_entries.clone(),
169            keys_expr_bytecode_ranges: self.keys_expr_bytecode_ranges.clone(),
170            map_expr_entries: self.map_expr_entries.clone(),
171            grep_expr_entries: self.grep_expr_entries.clone(),
172            regex_flip_flop_rhs_expr_entries: self.regex_flip_flop_rhs_expr_entries.clone(),
173            values_expr_entries: self.values_expr_entries.clone(),
174            values_expr_bytecode_ranges: self.values_expr_bytecode_ranges.clone(),
175            delete_expr_entries: self.delete_expr_entries.clone(),
176            exists_expr_entries: self.exists_expr_entries.clone(),
177            push_expr_entries: self.push_expr_entries.clone(),
178            pop_expr_entries: self.pop_expr_entries.clone(),
179            shift_expr_entries: self.shift_expr_entries.clone(),
180            unshift_expr_entries: self.unshift_expr_entries.clone(),
181            splice_expr_entries: self.splice_expr_entries.clone(),
182            lvalues: self.lvalues.clone(),
183            ast_eval_exprs: self.ast_eval_exprs.clone(),
184            format_decls: self.format_decls.clone(),
185            use_overload_entries: self.use_overload_entries.clone(),
186            runtime_sub_decls: Arc::clone(&self.runtime_sub_decls),
187            runtime_advice_decls: Arc::clone(&self.runtime_advice_decls),
188            ip: 0,
189            stack: Vec::with_capacity(256),
190            call_stack: Vec::with_capacity(32),
191            wantarray_stack: Vec::with_capacity(8),
192            interp,
193            jit_enabled: false,
194            sub_jit_skip_linear: vec![false; n],
195            sub_jit_skip_block: vec![false; n],
196            sub_entry_at_ip: {
197                let mut v = vec![false; n];
198                for (_, e, _) in &self.sub_entries {
199                    if *e < v.len() {
200                        v[*e] = true;
201                    }
202                }
203                v
204            },
205            sub_entry_invoke_count: vec![0; n],
206            jit_sub_invoke_threshold: self.jit_sub_invoke_threshold,
207            jit_buf_slot: Vec::new(),
208            jit_buf_plain: Vec::new(),
209            jit_buf_arg: Vec::new(),
210            jit_trampoline_out: None,
211            jit_trampoline_depth: 0,
212            halt: false,
213            try_stack: Vec::new(),
214            pending_catch_error: None,
215            exit_main_dispatch: false,
216            exit_main_dispatch_value: None,
217            static_sub_closure_subs: self.static_sub_closure_subs.clone(),
218            sub_entry_by_name: self.sub_entry_by_name.clone(),
219            block_region_mode: false,
220            block_region_end: 0,
221            block_region_return: None,
222        }
223    }
224}
225
226#[inline]
227fn vm_interp_result(r: Result<StrykeValue, FlowOrError>, line: usize) -> StrykeResult<StrykeValue> {
228    match r {
229        Ok(v) => Ok(v),
230        Err(FlowOrError::Error(e)) => Err(e),
231        Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
232            "unexpected control flow in tree-assisted opcode",
233            line,
234        )),
235    }
236}
237
238/// Saved state for `try { } catch (…) { } finally { }`.
239/// Jump targets live in [`Op::TryPush`] and are patched after emission; we only store the op index.
240#[derive(Debug, Clone, PartialEq)]
241pub(crate) enum TryState {
242    /// Executing the `try` body — die here jumps to `catch`.
243    Trying,
244    /// Executing the `catch` body — die here runs `finally` (if present) then propagates outward.
245    Catching,
246    /// Executing the `finally` body — die here overrides any deferred error and propagates outward.
247    Finalizing,
248}
249
250#[derive(Debug, Clone)]
251pub(crate) struct TryFrame {
252    pub(crate) try_push_op_idx: usize,
253    pub(crate) state: TryState,
254    /// When `catch` itself throws and a `finally` exists, the new error is parked here so
255    /// `TryFinallyEnd` can re-raise it after `finally` runs.
256    pub(crate) deferred_error: Option<StrykeError>,
257}
258
259/// Saved state when entering a function call.
260#[derive(Debug)]
261struct CallFrame {
262    return_ip: usize,
263    stack_base: usize,
264    scope_depth: usize,
265    saved_wantarray: WantarrayCtx,
266    /// [`stryke_jit_call_sub`] — no bytecode resume; result stored in [`VM::jit_trampoline_out`].
267    jit_trampoline_return: bool,
268    /// Synthetic frame for [`Op::BlockReturnValue`] (`map`/`grep`/`sort` block bytecode), paired with
269    /// `scope_push_hook` at [`VM::run_block_region`] entry (not a sub call; no closure capture).
270    block_region: bool,
271    /// Wall-clock start for [`crate::profiler::Profiler::exit_sub`] (paired with `enter_sub` on `Call`).
272    sub_profiler_start: Option<std::time::Instant>,
273}
274
275/// Stack-based bytecode virtual machine.
276pub struct VM<'a> {
277    /// Shared with parallel workers via [`Self::new_parallel_worker`] (cheap `Arc` clones).
278    names: Arc<Vec<String>>,
279    constants: Arc<Vec<StrykeValue>>,
280    ops: Arc<Vec<Op>>,
281    lines: Arc<Vec<usize>>,
282    sub_entries: Vec<(u16, usize, bool)>,
283    /// See [`Chunk::static_sub_calls`] (`Op::CallStaticSubId`).
284    static_sub_calls: Vec<(usize, bool, u16)>,
285    blocks: Vec<Block>,
286    code_ref_sigs: Vec<Vec<SubSigParam>>,
287    /// Optional `ops[start..end]` lowering for [`Self::blocks`] (see [`Chunk::block_bytecode_ranges`]).
288    block_bytecode_ranges: Vec<Option<(usize, usize)>>,
289    /// Optional lowering for [`Chunk::map_expr_entries`] (see [`Chunk::map_expr_bytecode_ranges`]).
290    map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
291    /// Optional lowering for [`Chunk::grep_expr_entries`] (see [`Chunk::grep_expr_bytecode_ranges`]).
292    grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
293    given_entries: Vec<(Expr, Block)>,
294    given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
295    eval_timeout_entries: Vec<(Expr, Block)>,
296    eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
297    algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
298    algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
299    par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
300    par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
301    pwatch_entries: Vec<(Expr, Expr)>,
302    substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
303    keys_expr_entries: Vec<Expr>,
304    keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
305    map_expr_entries: Vec<Expr>,
306    grep_expr_entries: Vec<Expr>,
307    regex_flip_flop_rhs_expr_entries: Vec<Expr>,
308    regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
309    values_expr_entries: Vec<Expr>,
310    values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
311    delete_expr_entries: Vec<Expr>,
312    exists_expr_entries: Vec<Expr>,
313    push_expr_entries: Vec<(Expr, Vec<Expr>)>,
314    pop_expr_entries: Vec<Expr>,
315    shift_expr_entries: Vec<Expr>,
316    unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
317    splice_expr_entries: Vec<SpliceExprEntry>,
318    lvalues: Vec<Expr>,
319    ast_eval_exprs: Vec<Expr>,
320    format_decls: Vec<(String, Vec<String>)>,
321    use_overload_entries: Vec<Vec<(String, String)>>,
322    runtime_sub_decls: Arc<Vec<RuntimeSubDecl>>,
323    runtime_advice_decls: Arc<Vec<crate::bytecode::RuntimeAdviceDecl>>,
324    pub(crate) ip: usize,
325    stack: Vec<StrykeValue>,
326    call_stack: Vec<CallFrame>,
327    /// Paired with [`Op::WantarrayPush`] / [`Op::WantarrayPop`] (e.g. `splice` list vs scalar return).
328    wantarray_stack: Vec<WantarrayCtx>,
329    interp: &'a mut VMHelper,
330    /// When `false`, [`VM::execute`] skips Cranelift JIT (linear, block, and subroutine linear) and
331    /// uses only the opcode interpreter. Default `true`.
332    jit_enabled: bool,
333    /// `sub_jit_skip_linear[ip]` — true when linear sub-JIT cannot apply (control flow / calls).
334    /// Indexed by IP for O(1) lookup instead of hashing (recursive subs like fib hit this millions of times).
335    sub_jit_skip_linear: Vec<bool>,
336    /// `sub_jit_skip_block[ip]` — true when block sub-JIT cannot apply.
337    sub_jit_skip_block: Vec<bool>,
338    /// `sub_entry_at_ip[ip]` — faster than hashing on every opcode (recursive subs dispatch millions of ops).
339    sub_entry_at_ip: Vec<bool>,
340    /// Invocations per sub-entry IP (tiered JIT: interpreter until count exceeds threshold).
341    sub_entry_invoke_count: Vec<u32>,
342    /// Minimum invocations before attempting subroutine JIT. Override with `STRYKE_JIT_SUB_INVOKES` (default 50).
343    jit_sub_invoke_threshold: u32,
344    /// Reused `i64` tables for sub-JIT / top-level JIT attempts (avoids `vec![0; n]` on every try).
345    jit_buf_slot: Vec<i64>,
346    jit_buf_plain: Vec<i64>,
347    jit_buf_arg: Vec<i64>,
348    /// Set when running [`VM::jit_trampoline_run_sub`]; [`Op::ReturnValue`] stores here and exits dispatch.
349    jit_trampoline_out: Option<StrykeValue>,
350    /// Nesting depth for [`Self::jit_trampoline_run_sub`]; dispatch breaks on [`Self::jit_trampoline_out`] only when `> 0`.
351    jit_trampoline_depth: u32,
352    /// Set by [`Op::Halt`]; outer loop exits after handling [`Self::try_recover_from_exception`].
353    halt: bool,
354    /// Stack of active `try` regions (LIFO).
355    try_stack: Vec<TryFrame>,
356    /// Value to bind in the next [`Op::CatchReceive`] (set before jumping to `catch_ip`).
357    /// Carries the original `die`-value when one was supplied (preserves hash/array refs);
358    /// otherwise a string copy of the formatted error message.
359    pub(crate) pending_catch_error: Option<StrykeValue>,
360    /// [`Op::Return`] / [`Op::ReturnValue`] with no caller frame: exit the main dispatch loop (was `break`).
361    exit_main_dispatch: bool,
362    /// Top-level [`Op::ReturnValue`] with no frame: value for implicit return (was `last = val; break`).
363    exit_main_dispatch_value: Option<StrykeValue>,
364    /// [`Chunk::static_sub_calls`] index → pre-resolved [`StrykeSub`] for closure restore (stash key lookup once at VM build).
365    static_sub_closure_subs: Vec<Option<Arc<StrykeSub>>>,
366    /// O(1) [`Chunk::sub_entries`] lookup (same first-wins semantics as the old linear scan).
367    sub_entry_by_name: HashMap<u16, (usize, bool)>,
368    /// When executing [`Chunk::block_bytecode_ranges`] via [`Self::run_block_region`].
369    block_region_mode: bool,
370    block_region_end: usize,
371    block_region_return: Option<StrykeValue>,
372}
373
374impl<'a> VM<'a> {
375    pub fn new(chunk: &Chunk, interp: &'a mut VMHelper) -> Self {
376        let static_sub_closure_subs: Vec<Option<Arc<StrykeSub>>> = chunk
377            .static_sub_calls
378            .iter()
379            .map(|(_, _, name_idx)| {
380                let nm = chunk.names[*name_idx as usize].as_str();
381                interp.subs.get(nm).cloned()
382            })
383            .collect();
384        let mut sub_entry_by_name = HashMap::with_capacity(chunk.sub_entries.len());
385        for &(n, ip, sa) in &chunk.sub_entries {
386            sub_entry_by_name.entry(n).or_insert((ip, sa));
387        }
388        Self {
389            names: Arc::new(chunk.names.clone()),
390            constants: Arc::new(chunk.constants.clone()),
391            ops: Arc::new(chunk.ops.clone()),
392            lines: Arc::new(chunk.lines.clone()),
393            sub_entries: chunk.sub_entries.clone(),
394            static_sub_calls: chunk.static_sub_calls.clone(),
395            blocks: chunk.blocks.clone(),
396            code_ref_sigs: chunk.code_ref_sigs.clone(),
397            block_bytecode_ranges: chunk.block_bytecode_ranges.clone(),
398            map_expr_bytecode_ranges: chunk.map_expr_bytecode_ranges.clone(),
399            grep_expr_bytecode_ranges: chunk.grep_expr_bytecode_ranges.clone(),
400            regex_flip_flop_rhs_expr_bytecode_ranges: chunk
401                .regex_flip_flop_rhs_expr_bytecode_ranges
402                .clone(),
403            given_entries: chunk.given_entries.clone(),
404            given_topic_bytecode_ranges: chunk.given_topic_bytecode_ranges.clone(),
405            eval_timeout_entries: chunk.eval_timeout_entries.clone(),
406            eval_timeout_expr_bytecode_ranges: chunk.eval_timeout_expr_bytecode_ranges.clone(),
407            algebraic_match_entries: chunk.algebraic_match_entries.clone(),
408            algebraic_match_subject_bytecode_ranges: chunk
409                .algebraic_match_subject_bytecode_ranges
410                .clone(),
411            par_lines_entries: chunk.par_lines_entries.clone(),
412            par_walk_entries: chunk.par_walk_entries.clone(),
413            pwatch_entries: chunk.pwatch_entries.clone(),
414            substr_four_arg_entries: chunk.substr_four_arg_entries.clone(),
415            keys_expr_entries: chunk.keys_expr_entries.clone(),
416            keys_expr_bytecode_ranges: chunk.keys_expr_bytecode_ranges.clone(),
417            map_expr_entries: chunk.map_expr_entries.clone(),
418            grep_expr_entries: chunk.grep_expr_entries.clone(),
419            regex_flip_flop_rhs_expr_entries: chunk.regex_flip_flop_rhs_expr_entries.clone(),
420            values_expr_entries: chunk.values_expr_entries.clone(),
421            values_expr_bytecode_ranges: chunk.values_expr_bytecode_ranges.clone(),
422            delete_expr_entries: chunk.delete_expr_entries.clone(),
423            exists_expr_entries: chunk.exists_expr_entries.clone(),
424            push_expr_entries: chunk.push_expr_entries.clone(),
425            pop_expr_entries: chunk.pop_expr_entries.clone(),
426            shift_expr_entries: chunk.shift_expr_entries.clone(),
427            unshift_expr_entries: chunk.unshift_expr_entries.clone(),
428            splice_expr_entries: chunk.splice_expr_entries.clone(),
429            lvalues: chunk.lvalues.clone(),
430            ast_eval_exprs: chunk.ast_eval_exprs.clone(),
431            format_decls: chunk.format_decls.clone(),
432            use_overload_entries: chunk.use_overload_entries.clone(),
433            runtime_sub_decls: Arc::new(chunk.runtime_sub_decls.clone()),
434            runtime_advice_decls: Arc::new(chunk.runtime_advice_decls.clone()),
435            ip: 0,
436            stack: Vec::with_capacity(256),
437            call_stack: Vec::with_capacity(32),
438            wantarray_stack: Vec::with_capacity(8),
439            interp,
440            jit_enabled: true,
441            sub_jit_skip_linear: vec![false; chunk.ops.len().saturating_add(1)],
442            sub_jit_skip_block: vec![false; chunk.ops.len().saturating_add(1)],
443            sub_entry_at_ip: {
444                let mut v = vec![false; chunk.ops.len().saturating_add(1)];
445                for (_, e, _) in &chunk.sub_entries {
446                    if *e < v.len() {
447                        v[*e] = true;
448                    }
449                }
450                v
451            },
452            sub_entry_invoke_count: vec![0; chunk.ops.len().saturating_add(1)],
453            jit_sub_invoke_threshold: std::env::var("STRYKE_JIT_SUB_INVOKES")
454                .ok()
455                .and_then(|s| s.parse().ok())
456                .unwrap_or(50),
457            jit_buf_slot: Vec::new(),
458            jit_buf_plain: Vec::new(),
459            jit_buf_arg: Vec::new(),
460            jit_trampoline_out: None,
461            jit_trampoline_depth: 0,
462            halt: false,
463            try_stack: Vec::new(),
464            pending_catch_error: None,
465            exit_main_dispatch: false,
466            exit_main_dispatch_value: None,
467            static_sub_closure_subs,
468            sub_entry_by_name,
469            block_region_mode: false,
470            block_region_end: 0,
471            block_region_return: None,
472        }
473    }
474
475    /// Pop a synthetic [`CallFrame::block_region`] frame if dispatch exited before
476    /// [`Op::BlockReturnValue`] (error or fallthrough), restoring stack and scope.
477    fn unwind_stale_block_region_frame(&mut self) {
478        if let Some(frame) = self.call_stack.pop() {
479            if frame.block_region {
480                self.interp.wantarray_kind = frame.saved_wantarray;
481                self.stack.truncate(frame.stack_base);
482                self.interp.pop_scope_to_depth(frame.scope_depth);
483            } else {
484                self.call_stack.push(frame);
485            }
486        }
487    }
488
489    /// Run `ops[start..end]` (exclusive) for a compiled `map`/`grep`/`sort` block body.
490    ///
491    /// Matches [`VMHelper::exec_block`]: `$_` / `$a` / `$b` are set in the caller before each
492    /// iteration; then one block-local scope frame is pushed (no closure capture) and the body runs
493    /// inline. [`Op::BlockReturnValue`] unwinds that frame via [`Self::unwind_stale_block_region_frame`]
494    /// on error paths here.
495    fn run_block_region(
496        &mut self,
497        start: usize,
498        end: usize,
499        op_count: &mut u64,
500    ) -> StrykeResult<StrykeValue> {
501        let resume_ip = self.ip;
502        let saved_mode = self.block_region_mode;
503        let saved_end = self.block_region_end;
504        let saved_ret = self.block_region_return.take();
505
506        let scope_depth_before = self.interp.scope.depth();
507        let saved_wa = self.interp.wantarray_kind;
508
509        self.call_stack.push(CallFrame {
510            return_ip: 0,
511            stack_base: self.stack.len(),
512            scope_depth: scope_depth_before,
513            saved_wantarray: saved_wa,
514            jit_trampoline_return: false,
515            block_region: true,
516            sub_profiler_start: None,
517        });
518        self.interp.scope_push_hook();
519        self.interp.wantarray_kind = WantarrayCtx::Scalar;
520        self.ip = start;
521        self.block_region_mode = true;
522        self.block_region_end = end;
523        self.block_region_return = None;
524
525        let r = self.run_main_dispatch_loop(StrykeValue::UNDEF, op_count, false);
526        let out = self.block_region_return.take();
527
528        self.block_region_return = saved_ret;
529        self.block_region_mode = saved_mode;
530        self.block_region_end = saved_end;
531        self.ip = resume_ip;
532
533        match r {
534            Ok(_) => {
535                if let Some(val) = out {
536                    Ok(val)
537                } else {
538                    self.unwind_stale_block_region_frame();
539                    Err(StrykeError::runtime(
540                        "block bytecode region did not finish with BlockReturnValue",
541                        self.line(),
542                    ))
543                }
544            }
545            Err(e) => {
546                self.unwind_stale_block_region_frame();
547                Err(e)
548            }
549        }
550    }
551
552    #[inline]
553    fn extend_map_outputs(dst: &mut Vec<StrykeValue>, val: StrykeValue, peel_array_ref: bool) {
554        dst.extend(val.map_flatten_outputs(peel_array_ref));
555    }
556
557    fn map_with_block_common(
558        &mut self,
559        list: Vec<StrykeValue>,
560        block_idx: u16,
561        peel_array_ref: bool,
562        op_count: &mut u64,
563    ) -> StrykeResult<()> {
564        if list.len() == 1 {
565            if let Some(p) = list[0].as_pipeline() {
566                if peel_array_ref {
567                    return Err(StrykeError::runtime(
568                        "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
569                        self.line(),
570                    ));
571                }
572                let idx = block_idx as usize;
573                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
574                let line = self.line();
575                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
576                self.push(StrykeValue::pipeline(Arc::clone(&p)));
577                return Ok(());
578            }
579        }
580        let idx = block_idx as usize;
581        // map's BLOCK is list context. The shared block bytecode region is compiled with a
582        // scalar-context tail (grep/sort consumers need that), so when the block's tail is
583        // list-sensitive (`($_, $_*10)`, `1..$_`, `reverse …`, an array variable, …) fall
584        // back to the interpreter's list-tail [`Interpreter::exec_block_with_tail`]. For
585        // plain scalar tails (`$_ * 2`, `f($_)`, string ops) the bytecode region produces
586        // the same value in either context, so keep using it for speed.
587        let block_tail_is_list_sensitive = self
588            .blocks
589            .get(idx)
590            .and_then(|b| b.last())
591            .map(|stmt| match &stmt.kind {
592                crate::ast::StmtKind::Expression(expr) => {
593                    crate::compiler::expr_tail_is_list_sensitive(expr)
594                }
595                _ => true,
596            })
597            .unwrap_or(true);
598        if !block_tail_is_list_sensitive {
599            if let Some(&(start, end)) =
600                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
601            {
602                let mut result = Vec::new();
603                for item in list {
604                    self.interp.scope.set_topic(item);
605                    let val = self.run_block_region(start, end, op_count)?;
606                    Self::extend_map_outputs(&mut result, val, peel_array_ref);
607                }
608                self.push(StrykeValue::array(result));
609                return Ok(());
610            }
611        }
612        let block = self.blocks[idx].clone();
613        let mut result = Vec::new();
614        for item in list {
615            self.interp.scope.set_topic(item);
616            match self.interp.exec_block_with_tail(&block, WantarrayCtx::List) {
617                Ok(val) => Self::extend_map_outputs(&mut result, val, peel_array_ref),
618                Err(FlowOrError::Error(e)) => return Err(e),
619                Err(_) => {}
620            }
621        }
622        self.push(StrykeValue::array(result));
623        Ok(())
624    }
625
626    fn map_with_expr_common(
627        &mut self,
628        list: Vec<StrykeValue>,
629        expr_idx: u16,
630        peel_array_ref: bool,
631        op_count: &mut u64,
632    ) -> StrykeResult<()> {
633        let idx = expr_idx as usize;
634        let dispatch_coderef = !crate::compat_mode();
635        // EXPR-form `map EXPR, LIST`: no block boundary, so use
636        // `set_topic_local` (rebinds `_`/`_0` only, no chain shift, no
637        // slot 1+ zero). Block-form `map { ... }` goes through a
638        // separate dispatch path that uses full `set_topic`.
639        if let Some(&(start, end)) = self
640            .map_expr_bytecode_ranges
641            .get(idx)
642            .and_then(|r| r.as_ref())
643        {
644            let mut result = Vec::new();
645            for item in list {
646                self.interp.scope.set_topic_local(item.clone());
647                let val = self.run_block_region(start, end, op_count)?;
648                let val = self.maybe_call_coderef_with_item(val, &item, dispatch_coderef)?;
649                Self::extend_map_outputs(&mut result, val, peel_array_ref);
650            }
651            self.push(StrykeValue::array(result));
652        } else {
653            let e = self.map_expr_entries[idx].clone();
654            let mut result = Vec::new();
655            for item in list {
656                self.interp.scope.set_topic_local(item.clone());
657                let val = vm_interp_result(
658                    self.interp.eval_expr_ctx(&e, WantarrayCtx::List),
659                    self.line(),
660                )?;
661                let val = self.maybe_call_coderef_with_item(val, &item, dispatch_coderef)?;
662                Self::extend_map_outputs(&mut result, val, peel_array_ref);
663            }
664            self.push(StrykeValue::array(result));
665        }
666        Ok(())
667    }
668
669    /// If `val` is a code reference and `dispatch` is true (i.e. not in
670    /// `--compat` mode), call it with `item` as the sole argument and
671    /// return the call result. Otherwise return `val` unchanged. Powers
672    /// the "coderef-in-expr-position" feature for `grep $f, @l`,
673    /// `map $f, @l`, and pipe-forward `|> grep $f`.
674    fn maybe_call_coderef_with_item(
675        &mut self,
676        val: StrykeValue,
677        item: &StrykeValue,
678        dispatch: bool,
679    ) -> StrykeResult<StrykeValue> {
680        if !dispatch {
681            return Ok(val);
682        }
683        if let Some(sub) = val.as_code_ref() {
684            let sub = sub.clone();
685            let line = self.line();
686            return vm_interp_result(
687                self.interp
688                    .call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line),
689                line,
690            );
691        }
692        Ok(val)
693    }
694
695    /// Consecutive groups: key from block with `$_`; keys compared with [`StrykeValue::str_eq`].
696    fn chunk_by_with_block_common(
697        &mut self,
698        list: Vec<StrykeValue>,
699        block_idx: u16,
700        op_count: &mut u64,
701    ) -> StrykeResult<()> {
702        if list.is_empty() {
703            self.push(StrykeValue::array(vec![]));
704            return Ok(());
705        }
706        let idx = block_idx as usize;
707        let mut chunks: Vec<StrykeValue> = Vec::new();
708        let mut run: Vec<StrykeValue> = Vec::new();
709        let mut prev_key: Option<StrykeValue> = None;
710
711        let eval_key =
712            |vm: &mut VM, item: StrykeValue, op_count: &mut u64| -> StrykeResult<StrykeValue> {
713                vm.interp.scope.set_topic(item);
714                if let Some(&(start, end)) =
715                    vm.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
716                {
717                    vm.run_block_region(start, end, op_count)
718                } else {
719                    let block = vm.blocks[idx].clone();
720                    match vm.interp.exec_block(&block) {
721                        Ok(val) => Ok(val),
722                        Err(FlowOrError::Error(e)) => Err(e),
723                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
724                        Err(_) => Ok(StrykeValue::UNDEF),
725                    }
726                }
727            };
728
729        for item in list {
730            let key = eval_key(self, item.clone(), op_count)?;
731            match &prev_key {
732                None => {
733                    run.push(item);
734                    prev_key = Some(key);
735                }
736                Some(pk) => {
737                    if key.str_eq(pk) {
738                        run.push(item);
739                    } else {
740                        chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
741                            std::mem::take(&mut run),
742                        ))));
743                        run.push(item);
744                        prev_key = Some(key);
745                    }
746                }
747            }
748        }
749        if !run.is_empty() {
750            chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
751        }
752        self.push(StrykeValue::array(chunks));
753        Ok(())
754    }
755
756    fn chunk_by_with_expr_common(
757        &mut self,
758        list: Vec<StrykeValue>,
759        expr_idx: u16,
760        op_count: &mut u64,
761    ) -> StrykeResult<()> {
762        if list.is_empty() {
763            self.push(StrykeValue::array(vec![]));
764            return Ok(());
765        }
766        let idx = expr_idx as usize;
767        let mut chunks: Vec<StrykeValue> = Vec::new();
768        let mut run: Vec<StrykeValue> = Vec::new();
769        let mut prev_key: Option<StrykeValue> = None;
770        for item in list {
771            self.interp.scope.set_topic(item.clone());
772            let key = if let Some(&(start, end)) = self
773                .map_expr_bytecode_ranges
774                .get(idx)
775                .and_then(|r| r.as_ref())
776            {
777                self.run_block_region(start, end, op_count)?
778            } else {
779                let e = &self.map_expr_entries[idx];
780                vm_interp_result(
781                    self.interp.eval_expr_ctx(e, WantarrayCtx::Scalar),
782                    self.line(),
783                )?
784            };
785            match &prev_key {
786                None => {
787                    run.push(item);
788                    prev_key = Some(key);
789                }
790                Some(pk) => {
791                    if key.str_eq(pk) {
792                        run.push(item);
793                    } else {
794                        chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
795                            std::mem::take(&mut run),
796                        ))));
797                        run.push(item);
798                        prev_key = Some(key);
799                    }
800                }
801            }
802        }
803        if !run.is_empty() {
804            chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
805        }
806        self.push(StrykeValue::array(chunks));
807        Ok(())
808    }
809
810    #[inline]
811    fn sub_jit_skip_linear_test(&self, ip: usize) -> bool {
812        self.sub_jit_skip_linear.get(ip).copied().unwrap_or(false)
813    }
814
815    #[inline]
816    fn sub_jit_skip_linear_mark(&mut self, ip: usize) {
817        if ip >= self.sub_jit_skip_linear.len() {
818            self.sub_jit_skip_linear.resize(ip + 1, false);
819        }
820        self.sub_jit_skip_linear[ip] = true;
821    }
822
823    #[inline]
824    fn sub_jit_skip_block_test(&self, ip: usize) -> bool {
825        self.sub_jit_skip_block.get(ip).copied().unwrap_or(false)
826    }
827
828    #[inline]
829    fn sub_jit_skip_block_mark(&mut self, ip: usize) {
830        if ip >= self.sub_jit_skip_block.len() {
831            self.sub_jit_skip_block.resize(ip + 1, false);
832        }
833        self.sub_jit_skip_block[ip] = true;
834    }
835
836    /// Enable or disable Cranelift JIT for this execution. Disabling skips compilation and buffer
837    /// prefetch for JIT paths (pure interpreter).
838    pub fn set_jit_enabled(&mut self, enabled: bool) {
839        self.jit_enabled = enabled;
840    }
841
842    #[inline]
843    fn push(&mut self, val: StrykeValue) {
844        self.stack.push(val);
845    }
846
847    #[inline]
848    fn pop(&mut self) -> StrykeValue {
849        self.stack.pop().unwrap_or(StrykeValue::UNDEF)
850    }
851
852    /// Convert a name-based binding ref (`\@array`, `\%hash`, `\$scalar`) into a
853    /// real `Arc`-based ref by snapshotting the current scope data.  This must be
854    /// called before the declaring scope is destroyed (e.g. on function return)
855    /// so the ref survives scope exit — matching Perl 5's refcount semantics.
856    fn resolve_binding_ref(&self, val: StrykeValue) -> StrykeValue {
857        if let Some(name) = val.as_array_binding_name() {
858            let data = self.interp.scope.get_array(&name);
859            return StrykeValue::array_ref(Arc::new(RwLock::new(data)));
860        }
861        if let Some(name) = val.as_hash_binding_name() {
862            let data = self.interp.scope.get_hash(&name);
863            return StrykeValue::hash_ref(Arc::new(RwLock::new(data)));
864        }
865        if let Some(name) = val.as_scalar_binding_name() {
866            let data = self.interp.scope.get_scalar(&name);
867            return StrykeValue::scalar_ref(Arc::new(RwLock::new(data)));
868        }
869        val
870    }
871
872    /// Pop `n` array-slice index specs (TOS = last spec). Each spec is a scalar index or an array
873    /// of indices (list-context `..`, `qw/.../`, parenthesized list), matching
874    /// [`crate::compiler::Compiler::compile_array_slice_index_expr`]. Returns flattened indices in
875    /// source order (first spec’s indices first).
876    fn pop_flattened_array_slice_specs(&mut self, n: usize) -> Vec<i64> {
877        let mut chunks: Vec<Vec<i64>> = Vec::with_capacity(n);
878        for _ in 0..n {
879            let spec = self.pop();
880            let mut flat = Vec::new();
881            if let Some(av) = spec.as_array_vec() {
882                for pv in av.iter() {
883                    flat.push(pv.to_int());
884                }
885            } else {
886                flat.push(spec.to_int());
887            }
888            chunks.push(flat);
889        }
890        chunks.reverse();
891        chunks.into_iter().flatten().collect()
892    }
893
894    /// Call operands are pushed so the rightmost syntactic argument is on top. Restore
895    /// left-to-right order, then flatten list-valued operands (`qw/.../`, list literals, hashes)
896    /// into successive scalars — matching Perl's argument list for simple calls. Reversing after
897    /// flattening would incorrectly reverse elements inside expanded lists.
898    fn pop_call_operands_flattened(&mut self, argc: usize) -> Vec<StrykeValue> {
899        let mut slots = Vec::with_capacity(argc);
900        for _ in 0..argc {
901            slots.push(self.pop());
902        }
903        slots.reverse();
904        let mut out = Vec::new();
905        for v in slots {
906            if let Some(items) = v.as_array_vec() {
907                out.extend(items);
908            } else if let Some(h) = v.as_hash_map() {
909                for (k, val) in h {
910                    out.push(StrykeValue::string(k));
911                    out.push(val);
912                }
913            } else {
914                out.push(v);
915            }
916        }
917        out
918    }
919
920    /// Like [`Self::pop_call_operands_flattened`], but each syntactic argument stays one
921    /// [`StrykeValue`] (`zip` / `mesh` need full lists per operand, not Perl's flattened `@_`).
922    fn pop_call_operands_preserved(&mut self, argc: usize) -> Vec<StrykeValue> {
923        let mut slots = Vec::with_capacity(argc);
924        for _ in 0..argc {
925            slots.push(self.pop());
926        }
927        slots.reverse();
928        slots
929    }
930
931    #[inline]
932    fn call_preserve_operand_arrays(name: &str) -> bool {
933        // Stryke builtins are unprefixed; `CORE::` callers route to bare names.
934        let name = name.strip_prefix("CORE::").unwrap_or(name);
935        matches!(
936            name,
937            "zip"
938                | "zip_longest"
939                | "zip_shortest"
940                | "mesh"
941                | "mesh_longest"
942                | "mesh_shortest"
943                | "take"
944                | "head"
945                | "tail"
946                | "drop"
947                // `len` / `count` / … must receive list-valued operands as **one** value.
948                // Otherwise `len stat $path` flattens `@_`: empty stat → 0 args → `$_` fallback
949                // (wrong), success → 13 args → list_count semantics (wrong for `len`).
950                | "len"
951                | "cnt"
952                | "count"
953                | "list_count"
954                | "list_size"
955        )
956    }
957
958    fn flatten_array_slice_specs_ordered_values(
959        &self,
960        specs: &[StrykeValue],
961    ) -> Result<Vec<i64>, StrykeError> {
962        let mut out = Vec::new();
963        for spec in specs {
964            if let Some(av) = spec.as_array_vec() {
965                for pv in av.iter() {
966                    out.push(pv.to_int());
967                }
968            } else {
969                out.push(spec.to_int());
970            }
971        }
972        Ok(out)
973    }
974
975    /// Hash `{…}` slice key slots in source order (each slot may expand to many string keys).
976    fn flatten_hash_slice_key_slots(key_vals: &[StrykeValue]) -> Vec<String> {
977        let mut ks = Vec::new();
978        for kv in key_vals {
979            if let Some(vv) = kv.as_array_vec() {
980                ks.extend(vv.iter().map(|x| x.to_string()));
981            } else {
982                ks.push(kv.to_string());
983            }
984        }
985        ks
986    }
987
988    #[inline]
989    fn peek(&self) -> &StrykeValue {
990        self.stack.last().unwrap_or(&PEEK_UNDEF)
991    }
992
993    #[inline]
994    fn constant(&self, idx: u16) -> &StrykeValue {
995        &self.constants[idx as usize]
996    }
997
998    fn line(&self) -> usize {
999        self.lines
1000            .get(self.ip.saturating_sub(1))
1001            .copied()
1002            .unwrap_or(0)
1003    }
1004
1005    /// Cranelift linear JIT for a subroutine body when `ip` is a compiled sub entry (see `Chunk::sub_entries`).
1006    /// Returns `Ok(true)` when the sub was executed natively and the VM should continue at `return_ip`.
1007    fn try_jit_subroutine_linear(&mut self) -> Result<bool, StrykeError> {
1008        let ip = self.ip;
1009        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1010        if self.sub_jit_skip_linear_test(ip) {
1011            return Ok(false);
1012        }
1013        let ops: &Vec<Op> = &self.ops;
1014        let ops = ops as *const Vec<Op>;
1015        let ops = unsafe { &*ops };
1016        let constants: &Vec<StrykeValue> = &self.constants;
1017        let constants = constants as *const Vec<StrykeValue>;
1018        let constants = unsafe { &*constants };
1019        let names: &Vec<String> = &self.names;
1020        let names = names as *const Vec<String>;
1021        let names = unsafe { &*names };
1022        let Some((seg, _)) = crate::jit::sub_entry_segment(ops, ip) else {
1023            return Ok(false);
1024        };
1025        // `try_run_linear_sub` rejects these segments without compiling — skip expensive work before
1026        // resize/fill of reusable scratch buffers (`jit_buf_*`).
1027        if crate::jit::segment_blocks_subroutine_linear_jit(seg, &self.sub_entries) {
1028            self.sub_jit_skip_linear_mark(ip);
1029            return Ok(false);
1030        }
1031        let mut slot_len: Option<usize> = None;
1032        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1033            let n = max as usize + 1;
1034            self.jit_buf_slot.resize(n, 0);
1035            let mut ok = true;
1036            for i in 0..=max {
1037                let pv = self.interp.scope.get_scalar_slot(i);
1038                self.jit_buf_slot[i as usize] = match pv.as_integer() {
1039                    Some(v) => v,
1040                    None if pv.is_undef() && crate::jit::slot_undef_prefill_ok_seq(seg, i) => 0,
1041                    None => {
1042                        ok = false;
1043                        break;
1044                    }
1045                };
1046            }
1047            if ok {
1048                slot_len = Some(n);
1049            }
1050        }
1051        let mut plain_len: Option<usize> = None;
1052        if let Some(max) = crate::jit::linear_plain_ops_max_index_seq(seg) {
1053            if (max as usize) < names.len() {
1054                let n = max as usize + 1;
1055                self.jit_buf_plain.resize(n, 0);
1056                let mut ok = true;
1057                for i in 0..=max {
1058                    let nm = names[i as usize].as_str();
1059                    match self.interp.scope.get_scalar(nm).as_integer() {
1060                        Some(v) => self.jit_buf_plain[i as usize] = v,
1061                        None => {
1062                            ok = false;
1063                            break;
1064                        }
1065                    }
1066                }
1067                if ok {
1068                    plain_len = Some(n);
1069                }
1070            }
1071        }
1072        let mut arg_len: Option<usize> = None;
1073        if let Some(max) = crate::jit::linear_arg_ops_max_index_seq(seg) {
1074            if let Some(frame) = self.call_stack.last() {
1075                let base = frame.stack_base;
1076                let n = max as usize + 1;
1077                self.jit_buf_arg.resize(n, 0);
1078                let mut ok = true;
1079                for i in 0..=max {
1080                    let pos = base + i as usize;
1081                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1082                    match pv.as_integer() {
1083                        Some(v) => self.jit_buf_arg[i as usize] = v,
1084                        None => {
1085                            ok = false;
1086                            break;
1087                        }
1088                    }
1089                }
1090                if ok {
1091                    arg_len = Some(n);
1092                }
1093            }
1094        }
1095        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1096        let slot_buf = slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1097        let plain_buf = plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1098        let arg_buf = arg_len.map(|n| &self.jit_buf_arg[..n]);
1099        let Some(v) = crate::jit::try_run_linear_sub(
1100            ops,
1101            ip,
1102            slot_buf,
1103            plain_buf,
1104            arg_buf,
1105            constants,
1106            &self.sub_entries,
1107            vm_ptr,
1108        ) else {
1109            return Ok(false);
1110        };
1111        if let Some(n) = slot_len {
1112            let buf = &self.jit_buf_slot[..n];
1113            for idx in crate::jit::linear_slot_ops_written_indices_seq(seg) {
1114                self.interp
1115                    .scope
1116                    .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
1117            }
1118        }
1119        if let Some(n) = plain_len {
1120            let buf = &self.jit_buf_plain[..n];
1121            for idx in crate::jit::linear_plain_ops_written_indices_seq(seg) {
1122                let name = names[idx as usize].as_str();
1123                self.interp
1124                    .scope
1125                    .set_scalar(name, StrykeValue::integer(buf[idx as usize]))
1126                    .map_err(|e| e.at_line(self.line()))?;
1127            }
1128        }
1129        if let Some(frame) = self.call_stack.pop() {
1130            self.interp.wantarray_kind = frame.saved_wantarray;
1131            self.stack.truncate(frame.stack_base);
1132            self.interp.pop_scope_to_depth(frame.scope_depth);
1133            if frame.jit_trampoline_return {
1134                self.jit_trampoline_out = Some(v);
1135            } else {
1136                self.push(v);
1137                self.ip = frame.return_ip;
1138            }
1139        }
1140        Ok(true)
1141    }
1142
1143    /// Cranelift block JIT for a subroutine with control flow (see [`crate::jit::block_jit_validate_sub`]).
1144    fn try_jit_subroutine_block(&mut self) -> Result<bool, StrykeError> {
1145        let ip = self.ip;
1146        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1147        if self.sub_jit_skip_block_test(ip) {
1148            return Ok(false);
1149        }
1150        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1151        let ops: &Vec<Op> = &self.ops;
1152        let constants: &Vec<StrykeValue> = &self.constants;
1153        let names: &Vec<String> = &self.names;
1154        let Some((full_body, term)) = crate::jit::sub_full_body(ops, ip) else {
1155            return Ok(false);
1156        };
1157        if crate::jit::sub_body_blocks_subroutine_block_jit(full_body) {
1158            self.sub_jit_skip_block_mark(ip);
1159            return Ok(false);
1160        }
1161        let Some(validated) =
1162            crate::jit::block_jit_validate_sub(full_body, constants, term, &self.sub_entries)
1163        else {
1164            self.sub_jit_skip_block_mark(ip);
1165            return Ok(false);
1166        };
1167        let block_buf_mode = validated.buffer_mode();
1168
1169        let mut b_slot_len: Option<usize> = None;
1170        if let Some(max) = crate::jit::block_slot_ops_max_index(full_body) {
1171            let n = max as usize + 1;
1172            self.jit_buf_slot.resize(n, 0);
1173            let mut ok = true;
1174            for i in 0..=max {
1175                let pv = self.interp.scope.get_scalar_slot(i);
1176                self.jit_buf_slot[i as usize] = match block_buf_mode {
1177                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => pv.raw_bits() as i64,
1178                    crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1179                        Some(v) => v,
1180                        None if pv.is_undef()
1181                            && crate::jit::block_slot_undef_prefill_ok(full_body, i) =>
1182                        {
1183                            0
1184                        }
1185                        None => {
1186                            ok = false;
1187                            break;
1188                        }
1189                    },
1190                };
1191            }
1192            if ok {
1193                b_slot_len = Some(n);
1194            }
1195        }
1196
1197        let mut b_plain_len: Option<usize> = None;
1198        if let Some(max) = crate::jit::block_plain_ops_max_index(full_body) {
1199            if (max as usize) < names.len() {
1200                let n = max as usize + 1;
1201                self.jit_buf_plain.resize(n, 0);
1202                let mut ok = true;
1203                for i in 0..=max {
1204                    let nm = names[i as usize].as_str();
1205                    let pv = self.interp.scope.get_scalar(nm);
1206                    self.jit_buf_plain[i as usize] = match block_buf_mode {
1207                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1208                            pv.raw_bits() as i64
1209                        }
1210                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1211                            Some(v) => v,
1212                            None => {
1213                                ok = false;
1214                                break;
1215                            }
1216                        },
1217                    };
1218                }
1219                if ok {
1220                    b_plain_len = Some(n);
1221                }
1222            }
1223        }
1224
1225        let mut b_arg_len: Option<usize> = None;
1226        if let Some(max) = crate::jit::block_arg_ops_max_index(full_body) {
1227            if let Some(frame) = self.call_stack.last() {
1228                let base = frame.stack_base;
1229                let n = max as usize + 1;
1230                self.jit_buf_arg.resize(n, 0);
1231                let mut ok = true;
1232                for i in 0..=max {
1233                    let pos = base + i as usize;
1234                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1235                    self.jit_buf_arg[i as usize] = match block_buf_mode {
1236                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1237                            pv.raw_bits() as i64
1238                        }
1239                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1240                            Some(v) => v,
1241                            None => {
1242                                ok = false;
1243                                break;
1244                            }
1245                        },
1246                    };
1247                }
1248                if ok {
1249                    b_arg_len = Some(n);
1250                }
1251            }
1252        }
1253
1254        let block_slot_buf = b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1255        let block_plain_buf = b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1256        let block_arg_buf = b_arg_len.map(|n| &self.jit_buf_arg[..n]);
1257
1258        let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
1259            full_body,
1260            block_slot_buf,
1261            block_plain_buf,
1262            block_arg_buf,
1263            constants,
1264            Some(validated),
1265            vm_ptr,
1266            &self.sub_entries,
1267        ) else {
1268            self.sub_jit_skip_block_mark(ip);
1269            return Ok(false);
1270        };
1271
1272        if let Some(n) = b_slot_len {
1273            let buf = &self.jit_buf_slot[..n];
1274            for idx in crate::jit::block_slot_ops_written_indices(full_body) {
1275                let bits = buf[idx as usize] as u64;
1276                let pv = match buf_mode {
1277                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1278                        StrykeValue::from_raw_bits(bits)
1279                    }
1280                    crate::jit::BlockJitBufferMode::I64AsInteger => {
1281                        StrykeValue::integer(buf[idx as usize])
1282                    }
1283                };
1284                self.interp.scope.set_scalar_slot(idx, pv);
1285            }
1286        }
1287        if let Some(n) = b_plain_len {
1288            let buf = &self.jit_buf_plain[..n];
1289            for idx in crate::jit::block_plain_ops_written_indices(full_body) {
1290                let name = names[idx as usize].as_str();
1291                let bits = buf[idx as usize] as u64;
1292                let pv = match buf_mode {
1293                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1294                        StrykeValue::from_raw_bits(bits)
1295                    }
1296                    crate::jit::BlockJitBufferMode::I64AsInteger => {
1297                        StrykeValue::integer(buf[idx as usize])
1298                    }
1299                };
1300                self.interp
1301                    .scope
1302                    .set_scalar(name, pv)
1303                    .map_err(|e| e.at_line(self.line()))?;
1304            }
1305        }
1306        if let Some(frame) = self.call_stack.pop() {
1307            self.interp.wantarray_kind = frame.saved_wantarray;
1308            self.stack.truncate(frame.stack_base);
1309            self.interp.pop_scope_to_depth(frame.scope_depth);
1310            if frame.jit_trampoline_return {
1311                self.jit_trampoline_out = Some(v);
1312            } else {
1313                self.push(v);
1314                self.ip = frame.return_ip;
1315            }
1316        }
1317        Ok(true)
1318    }
1319
1320    fn run_method_op(
1321        &mut self,
1322        name_idx: u16,
1323        argc: u8,
1324        wa: u8,
1325        super_call: bool,
1326    ) -> StrykeResult<()> {
1327        let method_owned = self.names[name_idx as usize].clone();
1328        let argc = argc as usize;
1329        let want = WantarrayCtx::from_byte(wa);
1330        let mut args = Vec::with_capacity(argc);
1331        for _ in 0..argc {
1332            args.push(self.pop());
1333        }
1334        args.reverse();
1335        let obj = self.pop();
1336        let method = method_owned.as_str();
1337        if let Some(r) = crate::pchannel::dispatch_method(&obj, method, &args, self.line()) {
1338            self.push(r?);
1339            return Ok(());
1340        }
1341        if let Some(r) = self
1342            .interp
1343            .try_native_method(&obj, method, &args, self.line())
1344        {
1345            self.push(r?);
1346            return Ok(());
1347        }
1348        let class = if let Some(b) = obj.as_blessed_ref() {
1349            b.class.clone()
1350        } else if let Some(s) = obj.as_str() {
1351            s
1352        } else {
1353            return Err(StrykeError::runtime(
1354                "Can't call method on non-object",
1355                self.line(),
1356            ));
1357        };
1358        if method == "VERSION" && !super_call {
1359            if let Some(ver) = self.interp.package_version_scalar(class.as_str())? {
1360                self.push(ver);
1361                return Ok(());
1362            }
1363        }
1364        // UNIVERSAL methods: isa, can, DOES
1365        if !super_call {
1366            match method {
1367                "isa" => {
1368                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
1369                    let mro = self.interp.mro_linearize(&class);
1370                    let result = mro.iter().any(|c| c == &target);
1371                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
1372                    return Ok(());
1373                }
1374                "can" => {
1375                    let target_method = args.first().map(|v| v.to_string()).unwrap_or_default();
1376                    let found = self
1377                        .interp
1378                        .resolve_method_full_name(&class, &target_method, false)
1379                        .and_then(|fq| self.interp.subs.get(&fq))
1380                        .is_some();
1381                    if found {
1382                        self.push(StrykeValue::code_ref(std::sync::Arc::new(
1383                            crate::value::StrykeSub {
1384                                name: target_method,
1385                                params: vec![],
1386                                body: vec![],
1387                                closure_env: None,
1388                                prototype: None,
1389                                fib_like: None,
1390                            },
1391                        )));
1392                    } else {
1393                        self.push(StrykeValue::UNDEF);
1394                    }
1395                    return Ok(());
1396                }
1397                "DOES" => {
1398                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
1399                    let mro = self.interp.mro_linearize(&class);
1400                    let result = mro.iter().any(|c| c == &target);
1401                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
1402                    return Ok(());
1403                }
1404                _ => {}
1405            }
1406        }
1407        let mut all_args = vec![obj];
1408        all_args.extend(args);
1409        let full_name = match self
1410            .interp
1411            .resolve_method_full_name(&class, method, super_call)
1412        {
1413            Some(f) => f,
1414            None => {
1415                return Err(StrykeError::runtime(
1416                    format!(
1417                        "Can't locate method \"{}\" via inheritance (invocant \"{}\")",
1418                        method, class
1419                    ),
1420                    self.line(),
1421                ));
1422            }
1423        };
1424        if let Some(sub) = self.interp.subs.get(&full_name).cloned() {
1425            let saved_wa = self.interp.wantarray_kind;
1426            self.interp.wantarray_kind = want;
1427            self.interp.scope_push_hook();
1428            self.interp.scope.declare_array("_", all_args);
1429            if let Some(ref env) = sub.closure_env {
1430                self.interp.scope.restore_capture(env);
1431            }
1432            let line = self.line();
1433            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
1434            self.interp
1435                .apply_sub_signature(sub.as_ref(), &argv, line)
1436                .map_err(|e| e.at_line(line))?;
1437            self.interp.scope.declare_array("_", argv);
1438            let result = self.interp.exec_block_no_scope(&sub.body);
1439            self.interp.wantarray_kind = saved_wa;
1440            self.interp.scope_pop_hook();
1441            match result {
1442                Ok(v) => self.push(v),
1443                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
1444                    self.push(v)
1445                }
1446                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
1447                Err(_) => self.push(StrykeValue::UNDEF),
1448            }
1449        } else if method == "new" && !super_call {
1450            if class == "Set" {
1451                self.push(crate::value::set_from_elements(
1452                    all_args.into_iter().skip(1),
1453                ));
1454            } else if let Some(def) = self.interp.struct_defs.get(&class).cloned() {
1455                let line = self.line();
1456                let mut provided = Vec::new();
1457                let mut i = 1;
1458                while i + 1 < all_args.len() {
1459                    let k = all_args[i].to_string();
1460                    let v = all_args[i + 1].clone();
1461                    provided.push((k, v));
1462                    i += 2;
1463                }
1464                let mut defaults = Vec::with_capacity(def.fields.len());
1465                for field in &def.fields {
1466                    if let Some(ref expr) = field.default {
1467                        let val = self.interp.eval_expr(expr).map_err(|e| match e {
1468                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
1469                            _ => StrykeError::runtime("default evaluation flow", line),
1470                        })?;
1471                        defaults.push(Some(val));
1472                    } else {
1473                        defaults.push(None);
1474                    }
1475                }
1476                let v =
1477                    crate::native_data::struct_new_with_defaults(&def, &provided, &defaults, line)?;
1478                self.push(v);
1479            } else if let Some(def) = self.interp.class_defs.get(&class).cloned() {
1480                // Stryke `class` declarations route through `class_construct`
1481                // so the result is a real `ClassInstance` (typed-my checks,
1482                // isa walk, BUILD hooks). Without this the bytecode path
1483                // fell through to the default Perl-style blessed-hashref
1484                // below, breaking method dispatch for `$self` binding.
1485                // Mirrors the tree-walker fix in `vm_helper::builtin_new`.
1486                // Skip `all_args[0]` (the class-name receiver) since
1487                // `class_construct` expects user args only.
1488                let line = self.line();
1489                let user_args: Vec<StrykeValue> = all_args.into_iter().skip(1).collect();
1490                let v =
1491                    self.interp
1492                        .class_construct(&def, user_args, line)
1493                        .map_err(|e| match e {
1494                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
1495                            _ => StrykeError::runtime("class_construct flow", line),
1496                        })?;
1497                self.push(v);
1498            } else {
1499                let mut map = IndexMap::new();
1500                let mut i = 1;
1501                while i + 1 < all_args.len() {
1502                    map.insert(all_args[i].to_string(), all_args[i + 1].clone());
1503                    i += 2;
1504                }
1505                self.push(StrykeValue::blessed(Arc::new(
1506                    crate::value::BlessedRef::new_blessed(class, StrykeValue::hash(map)),
1507                )));
1508            }
1509        } else if let Some(result) =
1510            self.interp
1511                .try_autoload_call(&full_name, all_args, self.line(), want, Some(&class))
1512        {
1513            match result {
1514                Ok(v) => self.push(v),
1515                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
1516                    self.push(v)
1517                }
1518                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
1519                Err(_) => self.push(StrykeValue::UNDEF),
1520            }
1521        } else {
1522            return Err(StrykeError::runtime(
1523                format!(
1524                    "Can't locate method \"{}\" in package \"{}\"",
1525                    method, class
1526                ),
1527                self.line(),
1528            ));
1529        }
1530        Ok(())
1531    }
1532
1533    fn run_fan_block(
1534        &mut self,
1535        block_idx: u16,
1536        n: usize,
1537        line: usize,
1538        progress: bool,
1539    ) -> StrykeResult<()> {
1540        let block = self.blocks[block_idx as usize].clone();
1541        let subs = self.interp.subs.clone();
1542        let (scope_capture, atomic_arrays, atomic_hashes) =
1543            self.interp.scope.capture_with_atomics();
1544        // Worker bodies execute via the tree walker (`exec_block_no_scope`) which uses
1545        // `tree_scalar_storage_name` to rewrite `$x` → `Pkg::x`. That helper consults
1546        // `english_lexical_scalars` + `our_lexical_scalars` — empty in a fresh worker —
1547        // so without copying the parent's sets, `our` / `oursync` reads see UNDEF.
1548        let lex_scalars = self.interp.english_lexical_scalars_clone();
1549        let our_scalars = self.interp.our_lexical_scalars_clone();
1550        let fan_progress = FanProgress::new(progress, n);
1551        let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
1552        (0..n).into_par_iter().for_each(|i| {
1553            if first_err.lock().is_some() {
1554                return;
1555            }
1556            fan_progress.start_worker(i);
1557            let mut local_interp = VMHelper::new();
1558            local_interp.subs = subs.clone();
1559            local_interp.suppress_stdout = progress;
1560            local_interp.scope.restore_capture(&scope_capture);
1561            local_interp
1562                .scope
1563                .restore_atomics(&atomic_arrays, &atomic_hashes);
1564            local_interp.set_english_lexical_scalars(lex_scalars.clone());
1565            local_interp.set_our_lexical_scalars(our_scalars.clone());
1566            local_interp.enable_parallel_guard();
1567            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
1568            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
1569            local_interp.scope_push_hook();
1570            match local_interp.exec_block_no_scope(&block) {
1571                Ok(_) => {}
1572                Err(e) => {
1573                    let stryke = match e {
1574                        FlowOrError::Error(stryke) => stryke,
1575                        FlowOrError::Flow(_) => StrykeError::runtime(
1576                            "return/last/next/redo not supported inside fan block",
1577                            line,
1578                        ),
1579                    };
1580                    let mut g = first_err.lock();
1581                    if g.is_none() {
1582                        *g = Some(stryke);
1583                    }
1584                }
1585            }
1586            local_interp.scope_pop_hook();
1587            crate::parallel_trace::fan_worker_set_index(None);
1588            fan_progress.finish_worker(i);
1589        });
1590        fan_progress.finish();
1591        if let Some(e) = first_err.lock().take() {
1592            return Err(e);
1593        }
1594        self.push(StrykeValue::UNDEF);
1595        Ok(())
1596    }
1597
1598    fn run_fan_cap_block(
1599        &mut self,
1600        block_idx: u16,
1601        n: usize,
1602        line: usize,
1603        progress: bool,
1604    ) -> StrykeResult<()> {
1605        let block = self.blocks[block_idx as usize].clone();
1606        let subs = self.interp.subs.clone();
1607        let (scope_capture, atomic_arrays, atomic_hashes) =
1608            self.interp.scope.capture_with_atomics();
1609        // See run_fan_block for why we copy lexical-scalar tracking sets.
1610        let lex_scalars = self.interp.english_lexical_scalars_clone();
1611        let our_scalars = self.interp.our_lexical_scalars_clone();
1612        let fan_progress = FanProgress::new(progress, n);
1613        let pairs: Vec<(usize, Result<StrykeValue, FlowOrError>)> = (0..n)
1614            .into_par_iter()
1615            .map(|i| {
1616                fan_progress.start_worker(i);
1617                let mut local_interp = VMHelper::new();
1618                local_interp.subs = subs.clone();
1619                local_interp.suppress_stdout = progress;
1620                local_interp.scope.restore_capture(&scope_capture);
1621                local_interp
1622                    .scope
1623                    .restore_atomics(&atomic_arrays, &atomic_hashes);
1624                local_interp.set_english_lexical_scalars(lex_scalars.clone());
1625                local_interp.set_our_lexical_scalars(our_scalars.clone());
1626                local_interp.enable_parallel_guard();
1627                local_interp.scope.set_topic(StrykeValue::integer(i as i64));
1628                crate::parallel_trace::fan_worker_set_index(Some(i as i64));
1629                local_interp.scope_push_hook();
1630                let res = local_interp.exec_block_no_scope(&block);
1631                local_interp.scope_pop_hook();
1632                crate::parallel_trace::fan_worker_set_index(None);
1633                fan_progress.finish_worker(i);
1634                (i, res)
1635            })
1636            .collect();
1637        fan_progress.finish();
1638        let mut pairs = pairs;
1639        pairs.sort_by_key(|(i, _)| *i);
1640        let mut out = Vec::with_capacity(n);
1641        for (_, r) in pairs {
1642            match r {
1643                Ok(v) => out.push(v),
1644                Err(e) => {
1645                    let stryke = match e {
1646                        FlowOrError::Error(stryke) => stryke,
1647                        FlowOrError::Flow(_) => StrykeError::runtime(
1648                            "return/last/next/redo not supported inside fan_cap block",
1649                            line,
1650                        ),
1651                    };
1652                    return Err(stryke);
1653                }
1654            }
1655        }
1656        self.push(StrykeValue::array(out));
1657        Ok(())
1658    }
1659
1660    fn require_scalar_mutable(&self, name: &str) -> StrykeResult<()> {
1661        if self.interp.scope.is_scalar_frozen(name) {
1662            return Err(StrykeError::syntax(
1663                format!("cannot assign to frozen variable `${}`", name),
1664                self.line(),
1665            ));
1666        }
1667        Ok(())
1668    }
1669
1670    fn require_array_mutable(&self, name: &str) -> StrykeResult<()> {
1671        if self.interp.scope.is_array_frozen(name) {
1672            return Err(StrykeError::syntax(
1673                format!("cannot modify frozen array `@{}`", name),
1674                self.line(),
1675            ));
1676        }
1677        Ok(())
1678    }
1679
1680    fn require_hash_mutable(&self, name: &str) -> StrykeResult<()> {
1681        if self.interp.scope.is_hash_frozen(name) || Self::is_reflection_hash(name) {
1682            return Err(StrykeError::syntax(
1683                format!("cannot modify frozen hash `%{}`", name),
1684                self.line(),
1685            ));
1686        }
1687        Ok(())
1688    }
1689
1690    /// Reflection hashes are frozen builtins even before lazy init.
1691    fn is_reflection_hash(name: &str) -> bool {
1692        matches!(name, "b" | "pc" | "e" | "a" | "d" | "c" | "p" | "all")
1693            || name.starts_with("stryke::")
1694    }
1695
1696    /// Run bytecode: first attempts Cranelift method JIT for eligible numeric fragments (unless
1697    /// [`VM::set_jit_enabled`] disabled it). For block JIT, `block_jit_validate` runs once per attempt;
1698    /// buffers may use `StrykeValue::raw_bits` for `defined`-style control flow. Then the main opcode
1699    /// interpreter loop.
1700    pub fn execute(&mut self) -> StrykeResult<StrykeValue> {
1701        let ops_ref: &Vec<Op> = &self.ops;
1702        let ops = ops_ref as *const Vec<Op>;
1703        // SAFETY: ops doesn't change during execution; pointer avoids borrow on self
1704        let ops = unsafe { &*ops };
1705        let names_ref: &Vec<String> = &self.names;
1706        let names = names_ref as *const Vec<String>;
1707        // SAFETY: names doesn't change during execution; pointer avoids borrow on self
1708        let names = unsafe { &*names };
1709        let constants_ref: &Vec<StrykeValue> = &self.constants;
1710        let constants = constants_ref as *const Vec<StrykeValue>;
1711        // SAFETY: constants doesn't change during execution; pointer avoids borrow on self
1712        let constants = unsafe { &*constants };
1713        let mut last = StrykeValue::UNDEF;
1714        // Safety limit: [`run_main_dispatch_loop`] counts ops (1B cap).
1715        let mut op_count: u64 = 0;
1716
1717        // Match Perl signal delivery: deliver `%SIG` and set `$^C` latch (Unix).
1718        crate::perl_signal::poll(self.interp)?;
1719        if self.jit_enabled {
1720            let mut top_slot_len: Option<usize> = None;
1721            if let Some(max) = crate::jit::linear_slot_ops_max_index(ops) {
1722                let n = max as usize + 1;
1723                self.jit_buf_slot.resize(n, 0);
1724                let mut ok = true;
1725                for i in 0..=max {
1726                    let pv = self.interp.scope.get_scalar_slot(i);
1727                    self.jit_buf_slot[i as usize] = match pv.as_integer() {
1728                        Some(v) => v,
1729                        None if pv.is_undef() && crate::jit::slot_undef_prefill_ok(ops, i) => 0,
1730                        None => {
1731                            ok = false;
1732                            break;
1733                        }
1734                    };
1735                }
1736                if ok {
1737                    top_slot_len = Some(n);
1738                }
1739            }
1740
1741            let mut top_plain_len: Option<usize> = None;
1742            if let Some(max) = crate::jit::linear_plain_ops_max_index(ops) {
1743                if (max as usize) < names.len() {
1744                    let n = max as usize + 1;
1745                    self.jit_buf_plain.resize(n, 0);
1746                    let mut ok = true;
1747                    for i in 0..=max {
1748                        let nm = names[i as usize].as_str();
1749                        match self.interp.scope.get_scalar(nm).as_integer() {
1750                            Some(v) => self.jit_buf_plain[i as usize] = v,
1751                            None => {
1752                                ok = false;
1753                                break;
1754                            }
1755                        }
1756                    }
1757                    if ok {
1758                        top_plain_len = Some(n);
1759                    }
1760                }
1761            }
1762
1763            let mut top_arg_len: Option<usize> = None;
1764            if let Some(max) = crate::jit::linear_arg_ops_max_index(ops) {
1765                if let Some(frame) = self.call_stack.last() {
1766                    let base = frame.stack_base;
1767                    let n = max as usize + 1;
1768                    self.jit_buf_arg.resize(n, 0);
1769                    let mut ok = true;
1770                    for i in 0..=max {
1771                        let pos = base + i as usize;
1772                        let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1773                        match pv.as_integer() {
1774                            Some(v) => self.jit_buf_arg[i as usize] = v,
1775                            None => {
1776                                ok = false;
1777                                break;
1778                            }
1779                        }
1780                    }
1781                    if ok {
1782                        top_arg_len = Some(n);
1783                    }
1784                }
1785            }
1786
1787            let slot_buf = top_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1788            let plain_buf = top_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1789            let arg_buf = top_arg_len.map(|n| &self.jit_buf_arg[..n]);
1790
1791            if let Some(v) =
1792                crate::jit::try_run_linear_ops(ops, slot_buf, plain_buf, arg_buf, constants)
1793            {
1794                if let Some(n) = top_slot_len {
1795                    let buf = &self.jit_buf_slot[..n];
1796                    for idx in crate::jit::linear_slot_ops_written_indices(ops) {
1797                        self.interp
1798                            .scope
1799                            .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
1800                    }
1801                }
1802                if let Some(n) = top_plain_len {
1803                    let buf = &self.jit_buf_plain[..n];
1804                    for idx in crate::jit::linear_plain_ops_written_indices(ops) {
1805                        let name = names[idx as usize].as_str();
1806                        self.interp
1807                            .scope
1808                            .set_scalar(name, StrykeValue::integer(buf[idx as usize]))?;
1809                    }
1810                }
1811                return Ok(v);
1812            }
1813
1814            // ── Block JIT: try to compile sequences with control flow (loops, conditionals). ──
1815            if let Some(validated) =
1816                crate::jit::block_jit_validate(ops, constants, &self.sub_entries)
1817            {
1818                let block_buf_mode = validated.buffer_mode();
1819
1820                let mut top_b_slot_len: Option<usize> = None;
1821                if let Some(max) = crate::jit::block_slot_ops_max_index(ops) {
1822                    let n = max as usize + 1;
1823                    self.jit_buf_slot.resize(n, 0);
1824                    let mut ok = true;
1825                    for i in 0..=max {
1826                        let pv = self.interp.scope.get_scalar_slot(i);
1827                        self.jit_buf_slot[i as usize] = match block_buf_mode {
1828                            crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1829                                pv.raw_bits() as i64
1830                            }
1831                            crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1832                                Some(v) => v,
1833                                None if pv.is_undef()
1834                                    && crate::jit::block_slot_undef_prefill_ok(ops, i) =>
1835                                {
1836                                    0
1837                                }
1838                                None => {
1839                                    ok = false;
1840                                    break;
1841                                }
1842                            },
1843                        };
1844                    }
1845                    if ok {
1846                        top_b_slot_len = Some(n);
1847                    }
1848                }
1849
1850                let mut top_b_plain_len: Option<usize> = None;
1851                if let Some(max) = crate::jit::block_plain_ops_max_index(ops) {
1852                    if (max as usize) < names.len() {
1853                        let n = max as usize + 1;
1854                        self.jit_buf_plain.resize(n, 0);
1855                        let mut ok = true;
1856                        for i in 0..=max {
1857                            let nm = names[i as usize].as_str();
1858                            let pv = self.interp.scope.get_scalar(nm);
1859                            self.jit_buf_plain[i as usize] = match block_buf_mode {
1860                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1861                                    pv.raw_bits() as i64
1862                                }
1863                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1864                                    match pv.as_integer() {
1865                                        Some(v) => v,
1866                                        None => {
1867                                            ok = false;
1868                                            break;
1869                                        }
1870                                    }
1871                                }
1872                            };
1873                        }
1874                        if ok {
1875                            top_b_plain_len = Some(n);
1876                        }
1877                    }
1878                }
1879
1880                let mut top_b_arg_len: Option<usize> = None;
1881                if let Some(max) = crate::jit::block_arg_ops_max_index(ops) {
1882                    if let Some(frame) = self.call_stack.last() {
1883                        let base = frame.stack_base;
1884                        let n = max as usize + 1;
1885                        self.jit_buf_arg.resize(n, 0);
1886                        let mut ok = true;
1887                        for i in 0..=max {
1888                            let pos = base + i as usize;
1889                            let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1890                            self.jit_buf_arg[i as usize] = match block_buf_mode {
1891                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1892                                    pv.raw_bits() as i64
1893                                }
1894                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1895                                    match pv.as_integer() {
1896                                        Some(v) => v,
1897                                        None => {
1898                                            ok = false;
1899                                            break;
1900                                        }
1901                                    }
1902                                }
1903                            };
1904                        }
1905                        if ok {
1906                            top_b_arg_len = Some(n);
1907                        }
1908                    }
1909                }
1910
1911                let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1912                let block_slot_buf = top_b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1913                let block_plain_buf = top_b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1914                let block_arg_buf = top_b_arg_len.map(|n| &self.jit_buf_arg[..n]);
1915
1916                if let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
1917                    ops,
1918                    block_slot_buf,
1919                    block_plain_buf,
1920                    block_arg_buf,
1921                    constants,
1922                    Some(validated),
1923                    vm_ptr,
1924                    &self.sub_entries,
1925                ) {
1926                    if let Some(n) = top_b_slot_len {
1927                        let buf = &self.jit_buf_slot[..n];
1928                        for idx in crate::jit::block_slot_ops_written_indices(ops) {
1929                            let bits = buf[idx as usize] as u64;
1930                            let pv = match buf_mode {
1931                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1932                                    StrykeValue::from_raw_bits(bits)
1933                                }
1934                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1935                                    StrykeValue::integer(buf[idx as usize])
1936                                }
1937                            };
1938                            self.interp.scope.set_scalar_slot(idx, pv);
1939                        }
1940                    }
1941                    if let Some(n) = top_b_plain_len {
1942                        let buf = &self.jit_buf_plain[..n];
1943                        for idx in crate::jit::block_plain_ops_written_indices(ops) {
1944                            let name = names[idx as usize].as_str();
1945                            let bits = buf[idx as usize] as u64;
1946                            let pv = match buf_mode {
1947                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1948                                    StrykeValue::from_raw_bits(bits)
1949                                }
1950                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1951                                    StrykeValue::integer(buf[idx as usize])
1952                                }
1953                            };
1954                            self.interp.scope.set_scalar(name, pv)?;
1955                        }
1956                    }
1957                    return Ok(v);
1958                }
1959            }
1960        }
1961
1962        last = self.run_main_dispatch_loop(last, &mut op_count, true)?;
1963
1964        Ok(last)
1965    }
1966
1967    /// `die` / runtime errors inside `try` jump to `catch_ip` unless the error is [`ErrorKind::Exit`].
1968    ///
1969    /// Walks the try stack top-down looking for a frame still in `Trying` state. Frames in
1970    /// `Catching` / `Finalizing` are skipped (and possibly popped) so that re-raising from
1971    /// inside `catch` or `finally` propagates outward instead of re-entering the same handler.
1972    /// If a `Catching` frame has a `finally`, that finally still runs (with the new error
1973    /// deferred) before the propagation continues.
1974    fn try_recover_from_exception(&mut self, e: &StrykeError) -> StrykeResult<bool> {
1975        if matches!(e.kind, ErrorKind::Exit(_)) {
1976            return Ok(false);
1977        }
1978        loop {
1979            let Some(frame) = self.try_stack.last() else {
1980                return Ok(false);
1981            };
1982            let op_idx = frame.try_push_op_idx;
1983            let Op::TryPush {
1984                catch_ip,
1985                finally_ip,
1986                ..
1987            } = &self.ops[op_idx]
1988            else {
1989                return Ok(false);
1990            };
1991            let catch_ip = *catch_ip;
1992            let finally_ip = *finally_ip;
1993            match frame.state {
1994                TryState::Trying => {
1995                    let val = e
1996                        .die_value
1997                        .clone()
1998                        .unwrap_or_else(|| StrykeValue::string(e.to_string()));
1999                    self.pending_catch_error = Some(val);
2000                    if let Some(top) = self.try_stack.last_mut() {
2001                        top.state = TryState::Catching;
2002                    }
2003                    self.ip = catch_ip;
2004                    return Ok(true);
2005                }
2006                TryState::Catching => {
2007                    if let Some(fin_ip) = finally_ip {
2008                        if let Some(top) = self.try_stack.last_mut() {
2009                            top.state = TryState::Finalizing;
2010                            top.deferred_error = Some(e.clone());
2011                        }
2012                        self.ip = fin_ip;
2013                        return Ok(true);
2014                    }
2015                    self.try_stack.pop();
2016                }
2017                TryState::Finalizing => {
2018                    // Finally itself threw — drop deferred (if any) and keep propagating.
2019                    self.try_stack.pop();
2020                }
2021            }
2022        }
2023    }
2024
2025    /// Stash lookup only (qualified key from compiler); avoids `resolve_sub_by_name`'s package fallback on hot calls.
2026    #[inline]
2027    fn sub_for_closure_restore(&self, name: &str) -> Option<Arc<StrykeSub>> {
2028        self.interp.subs.get(name).cloned()
2029    }
2030
2031    /// AOP: run before-advice → original (or around) → after-advice for `name`.
2032    /// Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). Args are popped synchronously
2033    /// off the stack; the original is invoked via `Interpreter::call_sub` so the retval is
2034    /// available to after-advice and to the around block (via `proceed`).
2035    #[cold]
2036    fn dispatch_with_advice(
2037        &mut self,
2038        name: &str,
2039        closure_sub_hint: Option<Arc<StrykeSub>>,
2040        argc: usize,
2041        want: WantarrayCtx,
2042        preserve_arrays: bool,
2043    ) -> StrykeResult<()> {
2044        use crate::ast::AdviceKind;
2045
2046        let line = self.line();
2047
2048        let args = if preserve_arrays {
2049            self.pop_call_operands_preserved(argc)
2050        } else {
2051            self.pop_call_operands_flattened(argc)
2052        };
2053
2054        let sub_opt = closure_sub_hint.or_else(|| self.interp.resolve_sub_by_name(name));
2055
2056        let matching: Vec<crate::aop::Intercept> = self
2057            .interp
2058            .intercepts
2059            .iter()
2060            .filter(|i| crate::aop::glob_match(&i.pattern, name))
2061            .cloned()
2062            .collect();
2063
2064        // Context vars visible to advice bodies (mirrors zshrs INTERCEPT_NAME / INTERCEPT_ARGS).
2065        self.interp
2066            .scope
2067            .declare_scalar("INTERCEPT_NAME", StrykeValue::string(name.to_string()));
2068        self.interp
2069            .scope
2070            .declare_array("INTERCEPT_ARGS", args.clone());
2071
2072        self.interp.intercept_active_names.push(name.to_string());
2073
2074        // Run all matching `before` advices via the bytecode VM (`run_block_region`).
2075        // We never fall back to `interp.exec_block` here — the advice body must use the
2076        // same name resolution as the surrounding bytecode (see the source-level test
2077        // in `tests/tree_walker_absent_aop.rs`).
2078        for adv in matching
2079            .iter()
2080            .filter(|i| matches!(i.kind, AdviceKind::Before))
2081        {
2082            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2083                self.interp.intercept_active_names.pop();
2084                return Err(e);
2085            }
2086        }
2087
2088        let around = matching
2089            .iter()
2090            .find(|i| matches!(i.kind, AdviceKind::Around));
2091
2092        let t0 = std::time::Instant::now();
2093        let retval = if let Some(around) = around {
2094            self.interp
2095                .intercept_ctx_stack
2096                .push(crate::aop::InterceptCtx {
2097                    name: name.to_string(),
2098                    args: args.clone(),
2099                    proceeded: false,
2100                    retval: StrykeValue::UNDEF,
2101                });
2102            let exec_res = self.run_advice_body_bytecode(around, line);
2103            let _ctx = self.interp.intercept_ctx_stack.pop();
2104            // AspectJ-style: the around block's evaluated value is the call's return.
2105            // If the user wants to forward the original's value, they say `proceed()`
2106            // as the last expression; if they want to transform, `proceed() + 1`; if
2107            // they want to replace, just emit a value without calling proceed.
2108            match exec_res {
2109                Ok(v) => v,
2110                Err(e) => {
2111                    self.interp.intercept_active_names.pop();
2112                    return Err(e);
2113                }
2114            }
2115        } else if let Some(sub) = sub_opt {
2116            match self.interp.call_sub(&sub, args.clone(), want, line) {
2117                Ok(v) => v,
2118                Err(FlowOrError::Flow(Flow::Return(v))) => v,
2119                Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2120                Err(FlowOrError::Error(e)) => {
2121                    self.interp.intercept_active_names.pop();
2122                    return Err(e.at_line(line));
2123                }
2124            }
2125        } else {
2126            // Sub not resolvable — fall back to builtins (matches the non-advice fallback).
2127            let saved_wa_call = self.interp.wantarray_kind;
2128            self.interp.wantarray_kind = want;
2129            let r = crate::builtins::try_builtin(self.interp, name, &args, line);
2130            self.interp.wantarray_kind = saved_wa_call;
2131            match r {
2132                Some(Ok(v)) => v,
2133                Some(Err(e)) => {
2134                    self.interp.intercept_active_names.pop();
2135                    return Err(e.at_line(line));
2136                }
2137                None => {
2138                    self.interp.intercept_active_names.pop();
2139                    return Err(StrykeError::runtime(
2140                        format!("undefined sub `{}` (advice fallback)", name),
2141                        line,
2142                    ));
2143                }
2144            }
2145        };
2146        let elapsed = t0.elapsed();
2147
2148        // Timing context vars for after-advice (matches zshrs INTERCEPT_MS / INTERCEPT_US).
2149        self.interp.scope.declare_scalar(
2150            "INTERCEPT_MS",
2151            StrykeValue::float(elapsed.as_secs_f64() * 1000.0),
2152        );
2153        self.interp.scope.declare_scalar(
2154            "INTERCEPT_US",
2155            StrykeValue::integer(elapsed.as_micros() as i64),
2156        );
2157        self.interp
2158            .scope
2159            .declare_scalar("INTERCEPT_RESULT", retval.clone());
2160
2161        for adv in matching
2162            .iter()
2163            .filter(|i| matches!(i.kind, AdviceKind::After))
2164        {
2165            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2166                self.interp.intercept_active_names.pop();
2167                return Err(e);
2168            }
2169        }
2170
2171        self.interp.intercept_active_names.pop();
2172        self.push(retval);
2173        Ok(())
2174    }
2175
2176    /// Dispatch one advice body through the VM bytecode helper (`run_block_region`),
2177    /// the same path used by `map { }` / `grep { }` blocks. Always returns the body's
2178    /// final value on success. The body is required to have a lowered bytecode region
2179    /// (`Chunk::block_bytecode_ranges[idx]`) — the compiler's fourth pass populates
2180    /// this for every chunk block, so the only reason it would be missing is if the
2181    /// body contains a construct the lowering rejects (e.g. a literal `return`); in
2182    /// that case we error out loudly rather than silently fall back to the
2183    /// tree-walker. See `tests/tree_walker_absent_aop.rs`.
2184    #[inline]
2185    fn run_advice_body_bytecode(
2186        &mut self,
2187        adv: &crate::aop::Intercept,
2188        line: usize,
2189    ) -> StrykeResult<StrykeValue> {
2190        let idx = adv.body_block_idx as usize;
2191        let range = self
2192            .block_bytecode_ranges
2193            .get(idx)
2194            .copied()
2195            .flatten()
2196            .ok_or_else(|| {
2197                StrykeError::runtime(
2198                    format!(
2199                        "AOP {} advice body for `{}` could not be lowered to bytecode \
2200                         (likely contains a construct unsupported by block lowering, \
2201                         e.g. a literal `return`); rewrite the body without it",
2202                        match adv.kind {
2203                            crate::ast::AdviceKind::Before => "before",
2204                            crate::ast::AdviceKind::After => "after",
2205                            crate::ast::AdviceKind::Around => "around",
2206                        },
2207                        adv.pattern,
2208                    ),
2209                    line,
2210                )
2211            })?;
2212        let mut op_count: u64 = 0;
2213        self.run_block_region(range.0, range.1, &mut op_count)
2214    }
2215
2216    fn vm_dispatch_user_call(
2217        &mut self,
2218        name_idx: u16,
2219        entry_opt: Option<(usize, bool)>,
2220        argc_u8: u8,
2221        wa_byte: u8,
2222        // Pre-resolved sub for `Op::CallStaticSubId` (stash lookup once in `VM::new`).
2223        closure_sub_hint: Option<Arc<StrykeSub>>,
2224    ) -> StrykeResult<()> {
2225        let name_owned = self.names[name_idx as usize].clone();
2226        let name = name_owned.as_str();
2227        let argc = argc_u8 as usize;
2228        let want = WantarrayCtx::from_byte(wa_byte);
2229
2230        // AOP advice path: at least one matching intercept and no re-entrancy guard for `name`.
2231        // Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). The fast-path skip below is the
2232        // common case (no intercepts registered); when the registry is non-empty we still bail
2233        // out cheaply unless a glob actually matches.
2234        if !self.interp.intercepts.is_empty()
2235            && !self.interp.intercept_active_names.iter().any(|n| n == name)
2236            && self
2237                .interp
2238                .intercepts
2239                .iter()
2240                .any(|i| crate::aop::glob_match(&i.pattern, name))
2241        {
2242            let preserve = Self::call_preserve_operand_arrays(name);
2243            return self.dispatch_with_advice(&name_owned, closure_sub_hint, argc, want, preserve);
2244        }
2245
2246        if let Some((entry_ip, stack_args)) = entry_opt {
2247            let saved_wa = self.interp.wantarray_kind;
2248            let sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2249            if let Some(p) = &mut self.interp.profiler {
2250                p.enter_sub(name);
2251            }
2252            self.interp.debugger_enter_sub(name);
2253
2254            // Fib-shaped recursive-add fast path: if the target sub is tagged with a
2255            // `fib_like` pattern (detected at sub-registration time in the compiler and
2256            // cached in `static_sub_closure_subs`), skip frame setup entirely and
2257            // evaluate the closed-form-ish iterative version. `bench_fib` collapses from
2258            // ~2.7M recursive VM calls to a single `while` loop.
2259            let fib_sub: Option<Arc<StrykeSub>> = closure_sub_hint
2260                .clone()
2261                .or_else(|| self.sub_for_closure_restore(name));
2262            if let Some(ref sub_arc) = fib_sub {
2263                if let Some(pat) = sub_arc.fib_like.as_ref() {
2264                    // stack_args path pushes exactly `argc` ints; non-stack_args pops them
2265                    // off the stack into @_. Only the argc==1 / integer case qualifies.
2266                    if argc == 1 {
2267                        let top_idx = self.stack.len().saturating_sub(1);
2268                        if let Some(n0) = self.stack.get(top_idx).and_then(|v| v.as_integer()) {
2269                            let result = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
2270                            // Drop the arg, push the result, keep wantarray as the caller had it.
2271                            self.stack.truncate(top_idx);
2272                            self.push(StrykeValue::integer(result));
2273                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, sub_prof_t0) {
2274                                p.exit_sub(t0.elapsed());
2275                            }
2276                            self.interp.debugger_leave_sub();
2277                            self.interp.wantarray_kind = saved_wa;
2278                            return Ok(());
2279                        }
2280                    }
2281                }
2282            }
2283
2284            if stack_args {
2285                let eff_argc = if argc == 0 {
2286                    self.push(self.interp.scope.get_scalar("_").clone());
2287                    1
2288                } else {
2289                    argc
2290                };
2291                let stack_base = self.stack.len() - eff_argc;
2292                self.call_stack.push(CallFrame {
2293                    return_ip: self.ip,
2294                    stack_base,
2295                    scope_depth: self.interp.scope.depth(),
2296                    saved_wantarray: saved_wa,
2297                    jit_trampoline_return: false,
2298                    block_region: false,
2299                    sub_profiler_start: sub_prof_t0,
2300                });
2301                self.interp.wantarray_kind = want;
2302                self.interp.scope_push_hook();
2303                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
2304                if let Some(ref sub) = closure_sub {
2305                    if let Some(ref env) = sub.closure_env {
2306                        self.interp.scope.restore_capture(env);
2307                    }
2308                    self.interp.current_sub_stack.push(sub.clone());
2309                }
2310                self.ip = entry_ip;
2311            } else {
2312                let args = if Self::call_preserve_operand_arrays(name) {
2313                    self.pop_call_operands_preserved(argc)
2314                } else {
2315                    self.pop_call_operands_flattened(argc)
2316                };
2317                // Only substitute $_ when the call site has no syntactic arguments (argc == 0).
2318                // When argc > 0 but args is empty (e.g., passing an empty array), keep args empty.
2319                let args = if argc == 0 {
2320                    self.interp.with_topic_default_args(args)
2321                } else {
2322                    args
2323                };
2324                self.call_stack.push(CallFrame {
2325                    return_ip: self.ip,
2326                    stack_base: self.stack.len(),
2327                    scope_depth: self.interp.scope.depth(),
2328                    saved_wantarray: saved_wa,
2329                    jit_trampoline_return: false,
2330                    block_region: false,
2331                    sub_profiler_start: sub_prof_t0,
2332                });
2333                self.interp.wantarray_kind = want;
2334                self.interp.scope_push_hook();
2335                self.interp.scope.declare_array("_", args);
2336                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
2337                if let Some(ref sub) = closure_sub {
2338                    if let Some(ref env) = sub.closure_env {
2339                        self.interp.scope.restore_capture(env);
2340                    }
2341                    let line = self.line();
2342                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
2343                    self.interp
2344                        .apply_sub_signature(sub.as_ref(), &argv, line)
2345                        .map_err(|e| e.at_line(line))?;
2346                    self.interp.scope.declare_array("_", argv.clone());
2347                    self.interp.scope.set_closure_args(&argv);
2348                    self.interp.current_sub_stack.push(sub.clone());
2349                }
2350                self.ip = entry_ip;
2351            }
2352        } else {
2353            let args = if Self::call_preserve_operand_arrays(name) {
2354                self.pop_call_operands_preserved(argc)
2355            } else {
2356                self.pop_call_operands_flattened(argc)
2357            };
2358
2359            let saved_wa_call = self.interp.wantarray_kind;
2360            self.interp.wantarray_kind = want;
2361            // Bare callable spelling: builtins always win in default mode.
2362            // Skip the user-sub resolve below so `fn sum {}` declared in a
2363            // non-main package never shadows the global `sum` on a bare
2364            // call. `--compat` (Perl 5 mode) restores UDF-wins semantics.
2365            let is_bare_builtin = !crate::compat_mode()
2366                && !name.contains("::")
2367                && crate::builtins::is_callable_spelling(name);
2368            if let Some(r) = crate::builtins::try_builtin(self.interp, name, &args, self.line()) {
2369                self.interp.wantarray_kind = saved_wa_call;
2370                self.push(r?);
2371            } else {
2372                self.interp.wantarray_kind = saved_wa_call;
2373                let maybe_sub = if is_bare_builtin {
2374                    None
2375                } else {
2376                    self.interp.resolve_sub_by_name(name)
2377                };
2378                if let Some(sub) = maybe_sub {
2379                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2380                    if let Some(p) = &mut self.interp.profiler {
2381                        p.enter_sub(name);
2382                    }
2383                    self.interp.debugger_enter_sub(name);
2384                    // Only substitute $_ when argc == 0; passing an empty array keeps args empty.
2385                    let args = if argc == 0 {
2386                        self.interp.with_topic_default_args(args)
2387                    } else {
2388                        args
2389                    };
2390                    let saved_wa = self.interp.wantarray_kind;
2391                    self.interp.wantarray_kind = want;
2392                    self.interp.scope_push_hook();
2393                    self.interp.scope.declare_array("_", args);
2394                    if let Some(ref env) = sub.closure_env {
2395                        self.interp.scope.restore_capture(env);
2396                    }
2397                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
2398                    let line = self.line();
2399                    self.interp
2400                        .apply_sub_signature(&sub, &argv, line)
2401                        .map_err(|e| e.at_line(line))?;
2402                    let result = {
2403                        self.interp.scope.declare_array("_", argv.clone());
2404                        self.interp.scope.set_closure_args(&argv);
2405                        self.interp
2406                            .exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List)
2407                    };
2408                    self.interp.wantarray_kind = saved_wa;
2409                    self.interp.scope_pop_hook();
2410                    match result {
2411                        Ok(v) => self.push(v),
2412                        Err(crate::vm_helper::FlowOrError::Flow(
2413                            crate::vm_helper::Flow::Return(v),
2414                        )) => self.push(v),
2415                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2416                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2417                                p.exit_sub(t0.elapsed());
2418                            }
2419                            self.interp.debugger_leave_sub();
2420                            return Err(e);
2421                        }
2422                        Err(_) => self.push(StrykeValue::UNDEF),
2423                    }
2424                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2425                        p.exit_sub(t0.elapsed());
2426                    }
2427                    self.interp.debugger_leave_sub();
2428                } else if !name.contains("::")
2429                    && matches!(
2430                        name,
2431                        "uniq"
2432                            | "distinct"
2433                            | "uniqstr"
2434                            | "uniqint"
2435                            | "uniqnum"
2436                            | "shuffle"
2437                            | "sample"
2438                            | "chunked"
2439                            | "windowed"
2440                            | "zip"
2441                            | "zip_shortest"
2442                            | "zip_longest"
2443                            | "mesh"
2444                            | "mesh_shortest"
2445                            | "mesh_longest"
2446                            | "any"
2447                            | "all"
2448                            | "none"
2449                            | "notall"
2450                            | "first"
2451                            | "find_index"
2452                            | "firstidx"
2453                            | "first_index"
2454                            | "reduce"
2455                            | "reductions"
2456                            | "sum"
2457                            | "sum0"
2458                            | "product"
2459                            | "min"
2460                            | "max"
2461                            | "minstr"
2462                            | "maxstr"
2463                            | "mean"
2464                            | "median"
2465                            | "mode"
2466                            | "stddev"
2467                            | "variance"
2468                            | "pairs"
2469                            | "unpairs"
2470                            | "pairkeys"
2471                            | "pairvalues"
2472                            | "pairgrep"
2473                            | "pairmap"
2474                            | "pairfirst"
2475                            // Scalar/Sub/utf8-utility bare builtins (no module — direct names)
2476                            | "blessed"
2477                            | "refaddr"
2478                            | "reftype"
2479                            | "looks_like_number"
2480                            | "weaken"
2481                            | "unweaken"
2482                            | "isweak"
2483                            | "set_subname"
2484                            | "subname"
2485                            | "unicode_to_native"
2486                    )
2487                {
2488                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2489                    if let Some(p) = &mut self.interp.profiler {
2490                        p.enter_sub(name);
2491                    }
2492                    self.interp.debugger_enter_sub(name);
2493                    let saved_wa = self.interp.wantarray_kind;
2494                    self.interp.wantarray_kind = want;
2495                    let out = self
2496                        .interp
2497                        .call_bare_list_builtin(name, args, self.line(), want);
2498                    self.interp.wantarray_kind = saved_wa;
2499                    match out {
2500                        Ok(v) => self.push(v),
2501                        Err(crate::vm_helper::FlowOrError::Flow(
2502                            crate::vm_helper::Flow::Return(v),
2503                        )) => self.push(v),
2504                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2505                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2506                                p.exit_sub(t0.elapsed());
2507                            }
2508                            self.interp.debugger_leave_sub();
2509                            return Err(e);
2510                        }
2511                        Err(_) => self.push(StrykeValue::UNDEF),
2512                    }
2513                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2514                        p.exit_sub(t0.elapsed());
2515                    }
2516                    self.interp.debugger_leave_sub();
2517                } else if let Some(result) = self.interp.try_autoload_call(
2518                    name,
2519                    if argc == 0 {
2520                        self.interp.with_topic_default_args(args.clone())
2521                    } else {
2522                        args.clone()
2523                    },
2524                    self.line(),
2525                    want,
2526                    None,
2527                ) {
2528                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2529                    if let Some(p) = &mut self.interp.profiler {
2530                        p.enter_sub(name);
2531                    }
2532                    self.interp.debugger_enter_sub(name);
2533                    match result {
2534                        Ok(v) => self.push(v),
2535                        Err(crate::vm_helper::FlowOrError::Flow(
2536                            crate::vm_helper::Flow::Return(v),
2537                        )) => self.push(v),
2538                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2539                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2540                                p.exit_sub(t0.elapsed());
2541                            }
2542                            self.interp.debugger_leave_sub();
2543                            return Err(e);
2544                        }
2545                        Err(_) => self.push(StrykeValue::UNDEF),
2546                    }
2547                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2548                        p.exit_sub(t0.elapsed());
2549                    }
2550                    self.interp.debugger_leave_sub();
2551                } else if let Some(def) = self.interp.struct_defs.get(name).cloned() {
2552                    // Struct constructor: Point(x => 1, y => 2) or Point(1, 2)
2553                    let result = self.interp.struct_construct(&def, args, self.line());
2554                    match result {
2555                        Ok(v) => self.push(v),
2556                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2557                        _ => self.push(StrykeValue::UNDEF),
2558                    }
2559                } else if let Some(def) = self.interp.class_defs.get(name).cloned() {
2560                    // Class constructor: Dog(name => "Rex") or Dog("Rex", 5)
2561                    let result = self.interp.class_construct(&def, args, self.line());
2562                    match result {
2563                        Ok(v) => self.push(v),
2564                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2565                        _ => self.push(StrykeValue::UNDEF),
2566                    }
2567                } else if let Some((prefix, suffix)) = name.rsplit_once("::") {
2568                    // Enum variant constructor: Color::Red or Maybe::Some(value)
2569                    if let Some(def) = self.interp.enum_defs.get(prefix).cloned() {
2570                        let result = self.interp.enum_construct(&def, suffix, args, self.line());
2571                        match result {
2572                            Ok(v) => self.push(v),
2573                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2574                            _ => self.push(StrykeValue::UNDEF),
2575                        }
2576                    // Static class method: Math::add(...)
2577                    } else if let Some(def) = self.interp.class_defs.get(prefix).cloned() {
2578                        if let Some(m) = def.method(suffix) {
2579                            if m.is_static {
2580                                if let Some(ref body) = m.body {
2581                                    let params = m.params.clone();
2582                                    match self.interp.call_static_class_method(
2583                                        body,
2584                                        &params,
2585                                        args.clone(),
2586                                        self.line(),
2587                                    ) {
2588                                        Ok(v) => self.push(v),
2589                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2590                                            return Err(e)
2591                                        }
2592                                        Err(crate::vm_helper::FlowOrError::Flow(
2593                                            crate::vm_helper::Flow::Return(v),
2594                                        )) => self.push(v),
2595                                        _ => self.push(StrykeValue::UNDEF),
2596                                    }
2597                                } else {
2598                                    self.push(StrykeValue::UNDEF);
2599                                }
2600                            } else {
2601                                return Err(StrykeError::runtime(
2602                                    format!("method `{}` is not static", suffix),
2603                                    self.line(),
2604                                ));
2605                            }
2606                        } else if def.static_fields.iter().any(|sf| sf.name == suffix) {
2607                            // Static field access: getter (0 args) or setter (1 arg)
2608                            let key = format!("{}::{}", prefix, suffix);
2609                            match args.len() {
2610                                0 => {
2611                                    let val = self.interp.scope.get_scalar(&key);
2612                                    self.push(val);
2613                                }
2614                                1 => {
2615                                    let _ = self.interp.scope.set_scalar(&key, args[0].clone());
2616                                    self.push(args[0].clone());
2617                                }
2618                                _ => {
2619                                    return Err(StrykeError::runtime(
2620                                        format!(
2621                                            "static field `{}::{}` takes 0 or 1 arguments",
2622                                            prefix, suffix
2623                                        ),
2624                                        self.line(),
2625                                    ));
2626                                }
2627                            }
2628                        } else {
2629                            return Err(StrykeError::runtime(
2630                                self.interp.undefined_subroutine_call_message(name),
2631                                self.line(),
2632                            ));
2633                        }
2634                    } else {
2635                        return Err(StrykeError::runtime(
2636                            self.interp.undefined_subroutine_call_message(name),
2637                            self.line(),
2638                        ));
2639                    }
2640                } else {
2641                    return Err(StrykeError::runtime(
2642                        self.interp.undefined_subroutine_call_message(name),
2643                        self.line(),
2644                    ));
2645                }
2646            }
2647        }
2648        Ok(())
2649    }
2650
2651    #[inline]
2652    fn push_binop_with_overload<F>(
2653        &mut self,
2654        op: BinOp,
2655        a: StrykeValue,
2656        b: StrykeValue,
2657        default: F,
2658    ) -> StrykeResult<()>
2659    where
2660        F: FnOnce(&StrykeValue, &StrykeValue) -> StrykeResult<StrykeValue>,
2661    {
2662        let line = self.line();
2663        if let Some(exec_res) = self.interp.try_overload_binop(op, &a, &b, line) {
2664            self.push(vm_interp_result(exec_res, line)?);
2665        } else {
2666            self.push(default(&a, &b)?);
2667        }
2668        Ok(())
2669    }
2670
2671    pub(crate) fn concat_stack_values(
2672        &mut self,
2673        a: StrykeValue,
2674        b: StrykeValue,
2675    ) -> StrykeResult<StrykeValue> {
2676        let line = self.line();
2677        if let Some(exec_res) = self.interp.try_overload_binop(BinOp::Concat, &a, &b, line) {
2678            vm_interp_result(exec_res, line)
2679        } else {
2680            let sa = match self.interp.stringify_value(a, line) {
2681                Ok(s) => s,
2682                Err(FlowOrError::Error(e)) => return Err(e),
2683                Err(FlowOrError::Flow(_)) => {
2684                    return Err(StrykeError::runtime(
2685                        "concat: unexpected control flow",
2686                        line,
2687                    ));
2688                }
2689            };
2690            let sb = match self.interp.stringify_value(b, line) {
2691                Ok(s) => s,
2692                Err(FlowOrError::Error(e)) => return Err(e),
2693                Err(FlowOrError::Flow(_)) => {
2694                    return Err(StrykeError::runtime(
2695                        "concat: unexpected control flow",
2696                        line,
2697                    ));
2698                }
2699            };
2700            let mut s = sa;
2701            s.push_str(&sb);
2702            Ok(StrykeValue::string(s))
2703        }
2704    }
2705
2706    fn run_main_dispatch_loop(
2707        &mut self,
2708        mut last: StrykeValue,
2709        op_count: &mut u64,
2710        init_dispatch: bool,
2711    ) -> StrykeResult<StrykeValue> {
2712        if init_dispatch {
2713            self.halt = false;
2714            self.exit_main_dispatch = false;
2715            self.exit_main_dispatch_value = None;
2716        }
2717        let ops_ref: &Vec<Op> = &self.ops;
2718        let ops = ops_ref as *const Vec<Op>;
2719        let ops = unsafe { &*ops };
2720        let names_ref: &Vec<String> = &self.names;
2721        let names = names_ref as *const Vec<String>;
2722        let names = unsafe { &*names };
2723        let constants_ref: &Vec<StrykeValue> = &self.constants;
2724        let constants = constants_ref as *const Vec<StrykeValue>;
2725        let constants = unsafe { &*constants };
2726        let len = ops.len();
2727        const MAX_OPS: u64 = 1_000_000_000;
2728        loop {
2729            if self.jit_trampoline_depth > 0 && self.jit_trampoline_out.is_some() {
2730                break;
2731            }
2732            if self.block_region_return.is_some() {
2733                break;
2734            }
2735            if self.block_region_mode && self.ip >= self.block_region_end {
2736                return Err(StrykeError::runtime(
2737                    "block bytecode region fell through without BlockReturnValue",
2738                    self.line(),
2739                ));
2740            }
2741            if self.ip >= len {
2742                break;
2743            }
2744
2745            if !self.block_region_mode
2746                && self.jit_enabled
2747                && self.sub_entry_at_ip.get(self.ip).copied().unwrap_or(false)
2748            {
2749                let sub_ip = self.ip;
2750                if sub_ip >= self.sub_entry_invoke_count.len() {
2751                    self.sub_entry_invoke_count.resize(sub_ip + 1, 0);
2752                }
2753                let c = &mut self.sub_entry_invoke_count[sub_ip];
2754                if *c <= self.jit_sub_invoke_threshold {
2755                    *c = c.saturating_add(1);
2756                }
2757                let should_try_jit = *c > self.jit_sub_invoke_threshold
2758                    && (!self.sub_jit_skip_linear_test(sub_ip)
2759                        || !self.sub_jit_skip_block_test(sub_ip));
2760                if should_try_jit {
2761                    if !self.sub_jit_skip_linear_test(sub_ip) && self.try_jit_subroutine_linear()? {
2762                        continue;
2763                    }
2764                    if !self.sub_jit_skip_block_test(sub_ip) && self.try_jit_subroutine_block()? {
2765                        continue;
2766                    }
2767                }
2768            }
2769
2770            *op_count += 1;
2771            // `%SIG` delivery and the execution cap: same cadence as the old per-op poll (signals
2772            // remain responsive; hot loops avoid a syscall/atomic path every opcode).
2773            if (*op_count & 0x3FF) == 0 {
2774                crate::perl_signal::poll(self.interp)?;
2775                if *op_count > MAX_OPS {
2776                    return Err(StrykeError::runtime(
2777                        "VM execution limit exceeded (possible infinite loop)",
2778                        self.line(),
2779                    ));
2780                }
2781            }
2782
2783            let ip_before = self.ip;
2784            let line = self.lines.get(ip_before).copied().unwrap_or(0);
2785            let op = &ops[self.ip];
2786            self.ip += 1;
2787
2788            // Debugger hook: check if we should stop at this line
2789            if let Some(ref mut dbg) = self.interp.debugger {
2790                if dbg.should_stop(line) {
2791                    let call_stack = self.interp.debug_call_stack.clone();
2792                    match dbg.prompt(line, &self.interp.scope, &call_stack) {
2793                        crate::debugger::DebugAction::Quit => {
2794                            return Err(StrykeError::runtime("debugger: quit", line));
2795                        }
2796                        crate::debugger::DebugAction::Continue => {}
2797                        crate::debugger::DebugAction::Prompt => {}
2798                    }
2799                }
2800            }
2801
2802            let op_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2803            // Closure: `?` / `return Err` inside `match op` must not return from
2804            // `run_main_dispatch_loop` — they must become `__op_res` so `try_recover_from_exception`
2805            // can run before propagating.
2806            let __op_res: StrykeResult<()> = (|| -> StrykeResult<()> {
2807                match op {
2808                    Op::Nop => Ok(()),
2809                    // ── Constants ──
2810                    Op::LoadInt(n) => {
2811                        self.push(StrykeValue::integer(*n));
2812                        Ok(())
2813                    }
2814                    Op::LoadFloat(f) => {
2815                        self.push(StrykeValue::float(*f));
2816                        Ok(())
2817                    }
2818                    Op::LoadConst(idx) => {
2819                        self.push(self.constant(*idx).clone());
2820                        Ok(())
2821                    }
2822                    Op::LoadUndef => {
2823                        self.push(StrykeValue::UNDEF);
2824                        Ok(())
2825                    }
2826                    Op::RuntimeErrorConst(idx) => {
2827                        let msg = self.constant(*idx).to_string();
2828                        let line = self.line();
2829                        Err(crate::error::StrykeError::runtime(msg, line))
2830                    }
2831                    Op::BarewordRvalue(name_idx) => {
2832                        let name = names[*name_idx as usize].clone();
2833                        let line = self.line();
2834                        let out = vm_interp_result(
2835                            self.interp.resolve_bareword_rvalue(
2836                                &name,
2837                                crate::vm_helper::WantarrayCtx::Scalar,
2838                                line,
2839                            ),
2840                            line,
2841                        )?;
2842                        self.push(out);
2843                        Ok(())
2844                    }
2845
2846                    // ── Stack ──
2847                    Op::Pop => {
2848                        let v = self.pop();
2849                        // Drain iterators used as void statements so side effects fire.
2850                        if v.is_iterator() {
2851                            let iter = v.into_iterator();
2852                            while iter.next_item().is_some() {}
2853                        }
2854                        Ok(())
2855                    }
2856                    Op::Dup => {
2857                        let v = self.peek().dup_stack();
2858                        self.push(v);
2859                        Ok(())
2860                    }
2861                    Op::Dup2 => {
2862                        let b = self.pop();
2863                        let a = self.pop();
2864                        self.push(a.dup_stack());
2865                        self.push(b.dup_stack());
2866                        self.push(a);
2867                        self.push(b);
2868                        Ok(())
2869                    }
2870                    Op::Swap => {
2871                        let top = self.pop();
2872                        let below = self.pop();
2873                        self.push(top);
2874                        self.push(below);
2875                        Ok(())
2876                    }
2877                    Op::Rot => {
2878                        let c = self.pop();
2879                        let b = self.pop();
2880                        let a = self.pop();
2881                        self.push(b);
2882                        self.push(c);
2883                        self.push(a);
2884                        Ok(())
2885                    }
2886                    Op::ValueScalarContext => {
2887                        let v = self.pop();
2888                        self.push(v.scalar_context());
2889                        Ok(())
2890                    }
2891                    Op::ListFirst => {
2892                        let v = self.pop();
2893                        let first = if let Some(arr) = v.as_array_vec() {
2894                            arr.first().cloned().unwrap_or(StrykeValue::UNDEF)
2895                        } else {
2896                            v
2897                        };
2898                        self.push(first);
2899                        Ok(())
2900                    }
2901
2902                    // ── Scalars ──
2903                    Op::GetScalar(idx) => {
2904                        let n = names[*idx as usize].as_str();
2905                        let val = self.interp.get_special_var(n);
2906                        self.push(val);
2907                        Ok(())
2908                    }
2909                    Op::GetScalarPlain(idx) => {
2910                        let n = names[*idx as usize].as_str();
2911                        let val = self.interp.scope.get_scalar(n);
2912                        self.push(val);
2913                        Ok(())
2914                    }
2915                    Op::SetScalar(idx) => {
2916                        let val = self.pop();
2917                        let n = names[*idx as usize].as_str();
2918                        self.require_scalar_mutable(n)?;
2919                        self.interp.maybe_invalidate_regex_capture_memo(n);
2920                        self.interp
2921                            .set_special_var(n, &val)
2922                            .map_err(|e| e.at_line(self.line()))?;
2923                        Ok(())
2924                    }
2925                    Op::SetScalarPlain(idx) => {
2926                        let val = self.pop();
2927                        let n = names[*idx as usize].as_str();
2928                        self.require_scalar_mutable(n)?;
2929                        self.interp.maybe_invalidate_regex_capture_memo(n);
2930                        self.interp
2931                            .scope
2932                            .set_scalar(n, val)
2933                            .map_err(|e| e.at_line(self.line()))?;
2934                        Ok(())
2935                    }
2936                    Op::SetScalarKeep(idx) => {
2937                        let val = self.peek().dup_stack();
2938                        let n = names[*idx as usize].as_str();
2939                        self.require_scalar_mutable(n)?;
2940                        self.interp.maybe_invalidate_regex_capture_memo(n);
2941                        self.interp
2942                            .set_special_var(n, &val)
2943                            .map_err(|e| e.at_line(self.line()))?;
2944                        Ok(())
2945                    }
2946                    Op::SetScalarKeepPlain(idx) => {
2947                        let val = self.peek().dup_stack();
2948                        let n = names[*idx as usize].as_str();
2949                        self.require_scalar_mutable(n)?;
2950                        self.interp.maybe_invalidate_regex_capture_memo(n);
2951                        self.interp
2952                            .scope
2953                            .set_scalar(n, val)
2954                            .map_err(|e| e.at_line(self.line()))?;
2955                        Ok(())
2956                    }
2957                    Op::DeclareScalar(idx) => {
2958                        let val = self.pop();
2959                        let n = names[*idx as usize].as_str();
2960                        self.interp
2961                            .scope
2962                            .declare_scalar_frozen(n, val, false, None)
2963                            .map_err(|e| e.at_line(self.line()))?;
2964                        Ok(())
2965                    }
2966                    Op::DeclareScalarFrozen(idx) => {
2967                        let val = self.pop();
2968                        let n = names[*idx as usize].as_str();
2969                        self.interp
2970                            .scope
2971                            .declare_scalar_frozen(n, val, true, None)
2972                            .map_err(|e| e.at_line(self.line()))?;
2973                        Ok(())
2974                    }
2975                    Op::DeclareScalarTyped(idx, tyb) => {
2976                        let val = self.pop();
2977                        let n = names[*idx as usize].as_str();
2978                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
2979                            StrykeError::runtime(
2980                                format!("invalid typed scalar type byte {}", tyb),
2981                                self.line(),
2982                            )
2983                        })?;
2984                        self.interp
2985                            .scope
2986                            .declare_scalar_frozen(n, val, false, Some(ty))
2987                            .map_err(|e| e.at_line(self.line()))?;
2988                        Ok(())
2989                    }
2990                    Op::DeclareScalarTypedFrozen(idx, tyb) => {
2991                        let val = self.pop();
2992                        let n = names[*idx as usize].as_str();
2993                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
2994                            StrykeError::runtime(
2995                                format!("invalid typed scalar type byte {}", tyb),
2996                                self.line(),
2997                            )
2998                        })?;
2999                        self.interp
3000                            .scope
3001                            .declare_scalar_frozen(n, val, true, Some(ty))
3002                            .map_err(|e| e.at_line(self.line()))?;
3003                        Ok(())
3004                    }
3005                    Op::DeclareScalarTypedUser(name_idx, type_idx, flag) => {
3006                        let val = self.pop();
3007                        let n = names[*name_idx as usize].as_str();
3008                        let type_name = names[*type_idx as usize].clone();
3009                        let is_enum = (flag & 0b01) != 0;
3010                        let is_frozen = (flag & 0b10) != 0;
3011                        let ty = if is_enum {
3012                            PerlTypeName::Enum(type_name)
3013                        } else {
3014                            // Struct variant covers struct, class, and any
3015                            // user-defined nominal type — `check_value` for
3016                            // `Struct(name)` already accepts class instances
3017                            // via `c.isa(name)`.
3018                            PerlTypeName::Struct(type_name)
3019                        };
3020                        self.interp
3021                            .scope
3022                            .declare_scalar_frozen(n, val, is_frozen, Some(ty))
3023                            .map_err(|e| e.at_line(self.line()))?;
3024                        Ok(())
3025                    }
3026
3027                    // ── State variables (persist across calls) ──
3028                    Op::DeclareStateScalar(idx) => {
3029                        let init_val = self.pop();
3030                        let n = names[*idx as usize].as_str();
3031                        // Key by source line + name (matches interpreter's state_key format)
3032                        let state_key = format!("{}:{}", self.line(), n);
3033                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3034                            prev.clone()
3035                        } else {
3036                            self.interp
3037                                .state_vars
3038                                .insert(state_key.clone(), init_val.clone());
3039                            init_val
3040                        };
3041                        self.interp
3042                            .scope
3043                            .declare_scalar_frozen(n, val, false, None)
3044                            .map_err(|e| e.at_line(self.line()))?;
3045                        // Register for save-back when scope pops
3046                        if let Some(frame) = self.interp.state_bindings_stack.last_mut() {
3047                            frame.push((n.to_string(), state_key));
3048                        }
3049                        Ok(())
3050                    }
3051                    Op::DeclareStateArray(idx) => {
3052                        let init_val = self.pop();
3053                        let n = names[*idx as usize].as_str();
3054                        let state_key = format!("{}:{}", self.line(), n);
3055                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3056                            prev.clone()
3057                        } else {
3058                            self.interp
3059                                .state_vars
3060                                .insert(state_key.clone(), init_val.clone());
3061                            init_val
3062                        };
3063                        self.interp.scope.declare_array(n, val.to_list());
3064                        Ok(())
3065                    }
3066                    Op::DeclareStateHash(idx) => {
3067                        let init_val = self.pop();
3068                        let n = names[*idx as usize].as_str();
3069                        let state_key = format!("{}:{}", self.line(), n);
3070                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
3071                            prev.clone()
3072                        } else {
3073                            self.interp
3074                                .state_vars
3075                                .insert(state_key.clone(), init_val.clone());
3076                            init_val
3077                        };
3078                        let items = val.to_list();
3079                        let mut map = IndexMap::new();
3080                        let mut i = 0;
3081                        while i + 1 < items.len() {
3082                            map.insert(items[i].to_string(), items[i + 1].clone());
3083                            i += 2;
3084                        }
3085                        self.interp.scope.declare_hash(n, map);
3086                        Ok(())
3087                    }
3088
3089                    // ── Arrays ──
3090                    Op::GetArray(idx) => {
3091                        let n = names[*idx as usize].as_str();
3092                        let arr = self.interp.scope.get_array(n);
3093                        self.push(StrykeValue::array(arr));
3094                        Ok(())
3095                    }
3096                    Op::SetArray(idx) => {
3097                        let val = self.pop();
3098                        let n = names[*idx as usize].as_str();
3099                        self.require_array_mutable(n)?;
3100                        self.interp
3101                            .scope
3102                            .set_array(n, val.to_list())
3103                            .map_err(|e| e.at_line(self.line()))?;
3104                        Ok(())
3105                    }
3106                    Op::DeclareArray(idx) => {
3107                        let val = self.pop();
3108                        let n = names[*idx as usize].as_str();
3109                        self.interp.scope.declare_array(n, val.to_list());
3110                        Ok(())
3111                    }
3112                    Op::DeclareArrayFrozen(idx) => {
3113                        let val = self.pop();
3114                        let n = names[*idx as usize].as_str();
3115                        self.interp
3116                            .scope
3117                            .declare_array_frozen(n, val.to_list(), true);
3118                        Ok(())
3119                    }
3120                    Op::GetArrayElem(idx) => {
3121                        let index = self.pop().to_int();
3122                        let n = names[*idx as usize].as_str();
3123                        // Stryke string-index sugar: bareword `_[N]` parses
3124                        // to a `__topicstr__N` synthetic name. Index the
3125                        // scalar (`$_` / `$_N`) by char.
3126                        if let Some(real) = n.strip_prefix("__topicstr__") {
3127                            let s = self.interp.scope.get_scalar(real).to_string();
3128                            let cnt = s.chars().count() as i64;
3129                            let i = if index < 0 { index + cnt } else { index };
3130                            let v = if i >= 0 && i < cnt {
3131                                s.chars()
3132                                    .nth(i as usize)
3133                                    .map(|c| StrykeValue::string(c.to_string()))
3134                                    .unwrap_or(StrykeValue::UNDEF)
3135                            } else {
3136                                StrykeValue::UNDEF
3137                            };
3138                            self.push(v);
3139                            return Ok(());
3140                        }
3141                        // Stryke (non-compat) sugar: `$s[i]` indexes by
3142                        // Unicode char when `@s` is missing or empty but
3143                        // `$s` is a non-empty string. NB: `$_[0]` keeps
3144                        // Perl's `@_`-access semantics because `@_` is
3145                        // populated inside any sub; the bareword `_[0]`
3146                        // parses to the same AST so it behaves identically.
3147                        // Use `substr(_, 0, 1)` for char-of-topic inside
3148                        // a sub. Compat mode = Perl semantics.
3149                        if !crate::compat_mode() && self.interp.scope.scalar_binding_exists(n) {
3150                            let prefer_scalar = self.interp.scope.get_array(n).is_empty();
3151                            if prefer_scalar {
3152                                let s = self.interp.scope.get_scalar(n).to_string();
3153                                if !s.is_empty() {
3154                                    let cnt = s.chars().count() as i64;
3155                                    let i = if index < 0 { index + cnt } else { index };
3156                                    let v = if i >= 0 && i < cnt {
3157                                        s.chars()
3158                                            .nth(i as usize)
3159                                            .map(|c| StrykeValue::string(c.to_string()))
3160                                            .unwrap_or(StrykeValue::UNDEF)
3161                                    } else {
3162                                        StrykeValue::UNDEF
3163                                    };
3164                                    self.push(v);
3165                                    return Ok(());
3166                                }
3167                            }
3168                        }
3169                        let val = self.interp.scope.get_array_element(n, index);
3170                        self.push(val);
3171                        Ok(())
3172                    }
3173                    Op::ExistsArrayElem(idx) => {
3174                        let index = self.pop().to_int();
3175                        let n = names[*idx as usize].as_str();
3176                        let yes = self.interp.scope.exists_array_element(n, index);
3177                        self.push(StrykeValue::integer(if yes { 1 } else { 0 }));
3178                        Ok(())
3179                    }
3180                    Op::DeleteArrayElem(idx) => {
3181                        let index = self.pop().to_int();
3182                        let n = names[*idx as usize].as_str();
3183                        self.require_array_mutable(n)?;
3184                        let v = self
3185                            .interp
3186                            .scope
3187                            .delete_array_element(n, index)
3188                            .map_err(|e| e.at_line(self.line()))?;
3189                        self.push(v);
3190                        Ok(())
3191                    }
3192                    Op::SetArrayElem(idx) => {
3193                        let index = self.pop().to_int();
3194                        let val = self.pop();
3195                        let n = names[*idx as usize].as_str();
3196                        self.require_array_mutable(n)?;
3197                        self.interp
3198                            .scope
3199                            .set_array_element(n, index, val)
3200                            .map_err(|e| e.at_line(self.line()))?;
3201                        Ok(())
3202                    }
3203                    Op::SetArrayElemKeep(idx) => {
3204                        let index = self.pop().to_int();
3205                        let val = self.pop();
3206                        let val_keep = val.clone();
3207                        let n = names[*idx as usize].as_str();
3208                        self.require_array_mutable(n)?;
3209                        let line = self.line();
3210                        self.interp
3211                            .scope
3212                            .set_array_element(n, index, val)
3213                            .map_err(|e| e.at_line(line))?;
3214                        self.push(val_keep);
3215                        Ok(())
3216                    }
3217                    Op::PushArray(idx) => {
3218                        let val = self.pop();
3219                        let n = names[*idx as usize].as_str();
3220                        self.require_array_mutable(n)?;
3221                        let line = self.line();
3222                        if let Some(items) = val.as_array_vec() {
3223                            for item in items {
3224                                self.interp
3225                                    .scope
3226                                    .push_to_array(n, item)
3227                                    .map_err(|e| e.at_line(line))?;
3228                            }
3229                        } else {
3230                            self.interp
3231                                .scope
3232                                .push_to_array(n, val)
3233                                .map_err(|e| e.at_line(line))?;
3234                        }
3235                        Ok(())
3236                    }
3237                    Op::PopArray(idx) => {
3238                        let n = names[*idx as usize].as_str();
3239                        self.require_array_mutable(n)?;
3240                        let line = self.line();
3241                        let val = self
3242                            .interp
3243                            .scope
3244                            .pop_from_array(n)
3245                            .map_err(|e| e.at_line(line))?;
3246                        self.push(val);
3247                        Ok(())
3248                    }
3249                    Op::ShiftArray(idx) => {
3250                        let n = names[*idx as usize].as_str();
3251                        self.require_array_mutable(n)?;
3252                        let line = self.line();
3253                        let val = self
3254                            .interp
3255                            .scope
3256                            .shift_from_array(n)
3257                            .map_err(|e| e.at_line(line))?;
3258                        self.push(val);
3259                        Ok(())
3260                    }
3261                    Op::PushArrayDeref => {
3262                        let val = self.pop();
3263                        let r = self.pop();
3264                        let line = self.line();
3265                        vm_interp_result(
3266                            self.interp
3267                                .push_array_deref_value(r.clone(), val, line)
3268                                .map(|_| StrykeValue::UNDEF),
3269                            line,
3270                        )?;
3271                        self.push(r);
3272                        Ok(())
3273                    }
3274                    Op::ArrayDerefLen => {
3275                        let r = self.pop();
3276                        let line = self.line();
3277                        let n = match self.interp.array_deref_len(r, line) {
3278                            Ok(n) => n,
3279                            Err(FlowOrError::Error(e)) => return Err(e),
3280                            Err(FlowOrError::Flow(_)) => {
3281                                return Err(StrykeError::runtime(
3282                                    "unexpected flow in tree-assisted opcode",
3283                                    line,
3284                                ));
3285                            }
3286                        };
3287                        self.push(StrykeValue::integer(n));
3288                        Ok(())
3289                    }
3290                    Op::PopArrayDeref => {
3291                        let r = self.pop();
3292                        let line = self.line();
3293                        let v = vm_interp_result(self.interp.pop_array_deref(r, line), line)?;
3294                        self.push(v);
3295                        Ok(())
3296                    }
3297                    Op::ShiftArrayDeref => {
3298                        let r = self.pop();
3299                        let line = self.line();
3300                        let v = vm_interp_result(self.interp.shift_array_deref(r, line), line)?;
3301                        self.push(v);
3302                        Ok(())
3303                    }
3304                    Op::UnshiftArrayDeref(n_extra) => {
3305                        let n = *n_extra as usize;
3306                        let mut vals: Vec<StrykeValue> = Vec::with_capacity(n);
3307                        for _ in 0..n {
3308                            vals.push(self.pop());
3309                        }
3310                        vals.reverse();
3311                        let r = self.pop();
3312                        let line = self.line();
3313                        let len = match self.interp.unshift_array_deref_multi(r, vals, line) {
3314                            Ok(n) => n,
3315                            Err(FlowOrError::Error(e)) => return Err(e),
3316                            Err(FlowOrError::Flow(_)) => {
3317                                return Err(StrykeError::runtime(
3318                                    "unexpected flow in tree-assisted opcode",
3319                                    line,
3320                                ));
3321                            }
3322                        };
3323                        self.push(StrykeValue::integer(len));
3324                        Ok(())
3325                    }
3326                    Op::SpliceArrayDeref(n_rep) => {
3327                        let n = *n_rep as usize;
3328                        let mut rep_vals: Vec<StrykeValue> = Vec::with_capacity(n);
3329                        for _ in 0..n {
3330                            rep_vals.push(self.pop());
3331                        }
3332                        rep_vals.reverse();
3333                        let length_val = self.pop();
3334                        let offset_val = self.pop();
3335                        let aref = self.pop();
3336                        let line = self.line();
3337                        let v = vm_interp_result(
3338                            self.interp
3339                                .splice_array_deref(aref, offset_val, length_val, rep_vals, line),
3340                            line,
3341                        )?;
3342                        self.push(v);
3343                        Ok(())
3344                    }
3345                    Op::ArrayLen(idx) => {
3346                        let len = self.interp.scope.array_len(&self.names[*idx as usize]);
3347                        self.push(StrykeValue::integer(len as i64));
3348                        Ok(())
3349                    }
3350                    Op::ArraySlicePart(idx) => {
3351                        let spec = self.pop();
3352                        let n = names[*idx as usize].as_str();
3353                        let mut out = Vec::new();
3354                        if let Some(indices) = spec.as_array_vec() {
3355                            for pv in indices {
3356                                out.push(self.interp.scope.get_array_element(n, pv.to_int()));
3357                            }
3358                        } else {
3359                            out.push(self.interp.scope.get_array_element(n, spec.to_int()));
3360                        }
3361                        self.push(StrykeValue::array(out));
3362                        Ok(())
3363                    }
3364                    Op::GetArrayFromIndex(idx, start) => {
3365                        let n = names[*idx as usize].as_str();
3366                        let arr = self.interp.scope.get_array(n);
3367                        let start = *start as usize;
3368                        let out: Vec<StrykeValue> = if start >= arr.len() {
3369                            Vec::new()
3370                        } else {
3371                            arr[start..].to_vec()
3372                        };
3373                        self.push(StrykeValue::array(out));
3374                        Ok(())
3375                    }
3376                    Op::ArrayConcatTwo => {
3377                        let b = self.pop();
3378                        let a = self.pop();
3379                        let mut av = a.as_array_vec().unwrap_or_else(|| vec![a]);
3380                        let bv = b.as_array_vec().unwrap_or_else(|| vec![b]);
3381                        av.extend(bv);
3382                        self.push(StrykeValue::array(av));
3383                        Ok(())
3384                    }
3385
3386                    // ── Hashes ──
3387                    Op::GetHash(idx) => {
3388                        let n = names[*idx as usize].as_str();
3389                        self.interp.touch_env_hash(n);
3390                        let h = self.interp.scope.get_hash(n);
3391                        self.push(StrykeValue::hash(h));
3392                        Ok(())
3393                    }
3394                    Op::SetHash(idx) => {
3395                        let val = self.pop();
3396                        let items = val.to_list();
3397                        let mut map = IndexMap::new();
3398                        let mut i = 0;
3399                        while i + 1 < items.len() {
3400                            map.insert(items[i].to_string(), items[i + 1].clone());
3401                            i += 2;
3402                        }
3403                        let n = names[*idx as usize].as_str();
3404                        self.require_hash_mutable(n)?;
3405                        self.interp
3406                            .scope
3407                            .set_hash(n, map)
3408                            .map_err(|e| e.at_line(self.line()))?;
3409                        Ok(())
3410                    }
3411                    Op::DeclareHash(idx) => {
3412                        let val = self.pop();
3413                        let items = val.to_list();
3414                        let mut map = IndexMap::new();
3415                        let mut i = 0;
3416                        while i + 1 < items.len() {
3417                            map.insert(items[i].to_string(), items[i + 1].clone());
3418                            i += 2;
3419                        }
3420                        let n = names[*idx as usize].as_str();
3421                        self.interp.scope.declare_hash(n, map);
3422                        Ok(())
3423                    }
3424                    Op::DeclareHashFrozen(idx) => {
3425                        let val = self.pop();
3426                        let items = val.to_list();
3427                        let mut map = IndexMap::new();
3428                        let mut i = 0;
3429                        while i + 1 < items.len() {
3430                            map.insert(items[i].to_string(), items[i + 1].clone());
3431                            i += 2;
3432                        }
3433                        let n = names[*idx as usize].as_str();
3434                        self.interp.scope.declare_hash_frozen(n, map, true);
3435                        Ok(())
3436                    }
3437                    Op::LocalDeclareScalar(idx) => {
3438                        let val = self.pop();
3439                        let n = names[*idx as usize].as_str();
3440                        // `local $X` on a special var (`$/`, `$\`, `$,`, `$"`, …) — see
3441                        // Perl's `local` handler. Save prior value to
3442                        // the interpreter's `special_var_restore_frames` so `scope_pop_hook`
3443                        // restores the backing field on block exit.
3444                        if VMHelper::is_special_scalar_name_for_set(n) {
3445                            let old = self.interp.get_special_var(n);
3446                            if let Some(frame) = self.interp.special_var_restore_frames.last_mut() {
3447                                frame.push((n.to_string(), old));
3448                            }
3449                            let line = self.line();
3450                            self.interp
3451                                .set_special_var(n, &val)
3452                                .map_err(|e| e.at_line(line))?;
3453                        }
3454                        self.interp
3455                            .scope
3456                            .local_set_scalar(n, val.clone())
3457                            .map_err(|e| e.at_line(self.line()))?;
3458                        self.push(val);
3459                        Ok(())
3460                    }
3461                    Op::LocalDeclareArray(idx) => {
3462                        let val = self.pop();
3463                        let n = names[*idx as usize].as_str();
3464                        self.interp
3465                            .scope
3466                            .local_set_array(n, val.to_list())
3467                            .map_err(|e| e.at_line(self.line()))?;
3468                        self.push(val);
3469                        Ok(())
3470                    }
3471                    Op::LocalDeclareHash(idx) => {
3472                        let val = self.pop();
3473                        let items = val.to_list();
3474                        let mut map = IndexMap::new();
3475                        let mut i = 0;
3476                        while i + 1 < items.len() {
3477                            map.insert(items[i].to_string(), items[i + 1].clone());
3478                            i += 2;
3479                        }
3480                        let n = names[*idx as usize].as_str();
3481                        self.interp.touch_env_hash(n);
3482                        self.interp
3483                            .scope
3484                            .local_set_hash(n, map)
3485                            .map_err(|e| e.at_line(self.line()))?;
3486                        self.push(val);
3487                        Ok(())
3488                    }
3489                    Op::LocalDeclareHashElement(idx) => {
3490                        let key = self.pop().to_string();
3491                        let val = self.pop();
3492                        let n = names[*idx as usize].as_str();
3493                        self.interp.touch_env_hash(n);
3494                        self.interp
3495                            .scope
3496                            .local_set_hash_element(n, key.as_str(), val.clone())
3497                            .map_err(|e| e.at_line(self.line()))?;
3498                        self.push(val);
3499                        Ok(())
3500                    }
3501                    Op::LocalDeclareArrayElement(idx) => {
3502                        let index = self.pop().to_int();
3503                        let val = self.pop();
3504                        let n = names[*idx as usize].as_str();
3505                        self.require_array_mutable(n)?;
3506                        self.interp
3507                            .scope
3508                            .local_set_array_element(n, index, val.clone())
3509                            .map_err(|e| e.at_line(self.line()))?;
3510                        self.push(val);
3511                        Ok(())
3512                    }
3513                    Op::LocalDeclareTypeglob(lhs_i, rhs_opt) => {
3514                        let lhs = names[*lhs_i as usize].as_str();
3515                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
3516                        let line = self.line();
3517                        self.interp
3518                            .local_declare_typeglob(lhs, rhs, line)
3519                            .map_err(|e| e.at_line(line))?;
3520                        Ok(())
3521                    }
3522                    Op::LocalDeclareTypeglobDynamic(rhs_opt) => {
3523                        let lhs = self.pop().to_string();
3524                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
3525                        let line = self.line();
3526                        self.interp
3527                            .local_declare_typeglob(lhs.as_str(), rhs, line)
3528                            .map_err(|e| e.at_line(line))?;
3529                        Ok(())
3530                    }
3531                    Op::GetHashElem(idx) => {
3532                        let key = self.pop().to_string();
3533                        let n = names[*idx as usize].as_str();
3534                        self.interp.touch_env_hash(n);
3535                        let val = self.interp.scope.get_hash_element(n, &key);
3536                        self.push(val);
3537                        Ok(())
3538                    }
3539                    Op::SetHashElem(idx) => {
3540                        let key = self.pop().to_string();
3541                        let val = self.pop();
3542                        let n = names[*idx as usize].as_str();
3543                        self.require_hash_mutable(n)?;
3544                        self.interp.touch_env_hash(n);
3545                        self.interp
3546                            .scope
3547                            .set_hash_element(n, &key, val)
3548                            .map_err(|e| e.at_line(self.line()))?;
3549                        Ok(())
3550                    }
3551                    Op::SetHashElemKeep(idx) => {
3552                        let key = self.pop().to_string();
3553                        let val = self.pop();
3554                        let val_keep = val.clone();
3555                        let n = names[*idx as usize].as_str();
3556                        self.require_hash_mutable(n)?;
3557                        self.interp.touch_env_hash(n);
3558                        let line = self.line();
3559                        self.interp
3560                            .scope
3561                            .set_hash_element(n, &key, val)
3562                            .map_err(|e| e.at_line(line))?;
3563                        self.push(val_keep);
3564                        Ok(())
3565                    }
3566                    Op::DeleteHashElem(idx) => {
3567                        let key = self.pop().to_string();
3568                        let n = names[*idx as usize].as_str();
3569                        self.require_hash_mutable(n)?;
3570                        self.interp.touch_env_hash(n);
3571                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
3572                            let class = obj
3573                                .as_blessed_ref()
3574                                .map(|b| b.class.clone())
3575                                .unwrap_or_default();
3576                            let full = format!("{}::DELETE", class);
3577                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
3578                                let line = self.line();
3579                                let v = vm_interp_result(
3580                                    self.interp.call_sub(
3581                                        &sub,
3582                                        vec![obj, StrykeValue::string(key)],
3583                                        WantarrayCtx::Scalar,
3584                                        line,
3585                                    ),
3586                                    line,
3587                                )?;
3588                                self.push(v);
3589                                return Ok(());
3590                            }
3591                        }
3592                        let val = self
3593                            .interp
3594                            .scope
3595                            .delete_hash_element(n, &key)
3596                            .map_err(|e| e.at_line(self.line()))?;
3597                        self.push(val);
3598                        Ok(())
3599                    }
3600                    Op::ExistsHashElem(idx) => {
3601                        let key = self.pop().to_string();
3602                        let n = names[*idx as usize].as_str();
3603                        self.interp.touch_env_hash(n);
3604                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
3605                            let class = obj
3606                                .as_blessed_ref()
3607                                .map(|b| b.class.clone())
3608                                .unwrap_or_default();
3609                            let full = format!("{}::EXISTS", class);
3610                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
3611                                let line = self.line();
3612                                let v = vm_interp_result(
3613                                    self.interp.call_sub(
3614                                        &sub,
3615                                        vec![obj, StrykeValue::string(key)],
3616                                        WantarrayCtx::Scalar,
3617                                        line,
3618                                    ),
3619                                    line,
3620                                )?;
3621                                self.push(v);
3622                                return Ok(());
3623                            }
3624                        }
3625                        let exists = self.interp.scope.exists_hash_element(n, &key);
3626                        self.push(StrykeValue::integer(if exists { 1 } else { 0 }));
3627                        Ok(())
3628                    }
3629                    Op::ExistsArrowHashElem => {
3630                        let key = self.pop().to_string();
3631                        let container = self.pop();
3632                        let line = self.line();
3633                        let yes = vm_interp_result(
3634                            self.interp
3635                                .exists_arrow_hash_element(container, &key, line)
3636                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
3637                                .map_err(FlowOrError::Error),
3638                            line,
3639                        )?;
3640                        self.push(yes);
3641                        Ok(())
3642                    }
3643                    Op::DeleteArrowHashElem => {
3644                        let key = self.pop().to_string();
3645                        let container = self.pop();
3646                        let line = self.line();
3647                        let v = vm_interp_result(
3648                            self.interp
3649                                .delete_arrow_hash_element(container, &key, line)
3650                                .map_err(FlowOrError::Error),
3651                            line,
3652                        )?;
3653                        self.push(v);
3654                        Ok(())
3655                    }
3656                    Op::ExistsArrowArrayElem => {
3657                        let idx = self.pop().to_int();
3658                        let container = self.pop();
3659                        let line = self.line();
3660                        let yes = vm_interp_result(
3661                            self.interp
3662                                .exists_arrow_array_element(container, idx, line)
3663                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
3664                                .map_err(FlowOrError::Error),
3665                            line,
3666                        )?;
3667                        self.push(yes);
3668                        Ok(())
3669                    }
3670                    Op::DeleteArrowArrayElem => {
3671                        let idx = self.pop().to_int();
3672                        let container = self.pop();
3673                        let line = self.line();
3674                        let v = vm_interp_result(
3675                            self.interp
3676                                .delete_arrow_array_element(container, idx, line)
3677                                .map_err(FlowOrError::Error),
3678                            line,
3679                        )?;
3680                        self.push(v);
3681                        Ok(())
3682                    }
3683                    Op::HashKeys(idx) => {
3684                        let n = names[*idx as usize].as_str();
3685                        self.interp.touch_env_hash(n);
3686                        let h = self.interp.scope.get_hash(n);
3687                        let keys: Vec<StrykeValue> =
3688                            h.keys().map(|k| StrykeValue::string(k.clone())).collect();
3689                        self.push(StrykeValue::array(keys));
3690                        Ok(())
3691                    }
3692                    Op::HashKeysScalar(idx) => {
3693                        let n = names[*idx as usize].as_str();
3694                        self.interp.touch_env_hash(n);
3695                        let h = self.interp.scope.get_hash(n);
3696                        self.push(StrykeValue::integer(h.len() as i64));
3697                        Ok(())
3698                    }
3699                    Op::HashValues(idx) => {
3700                        let n = names[*idx as usize].as_str();
3701                        self.interp.touch_env_hash(n);
3702                        let h = self.interp.scope.get_hash(n);
3703                        let vals: Vec<StrykeValue> = h.values().cloned().collect();
3704                        self.push(StrykeValue::array(vals));
3705                        Ok(())
3706                    }
3707                    Op::HashValuesScalar(idx) => {
3708                        let n = names[*idx as usize].as_str();
3709                        self.interp.touch_env_hash(n);
3710                        let h = self.interp.scope.get_hash(n);
3711                        self.push(StrykeValue::integer(h.len() as i64));
3712                        Ok(())
3713                    }
3714                    Op::KeysFromValue => {
3715                        let val = self.pop();
3716                        let line = self.line();
3717                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
3718                        self.push(v);
3719                        Ok(())
3720                    }
3721                    Op::KeysFromValueScalar => {
3722                        let val = self.pop();
3723                        let line = self.line();
3724                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
3725                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
3726                        self.push(StrykeValue::integer(n));
3727                        Ok(())
3728                    }
3729                    Op::ValuesFromValue => {
3730                        let val = self.pop();
3731                        let line = self.line();
3732                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
3733                        self.push(v);
3734                        Ok(())
3735                    }
3736                    Op::ValuesFromValueScalar => {
3737                        let val = self.pop();
3738                        let line = self.line();
3739                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
3740                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
3741                        self.push(StrykeValue::integer(n));
3742                        Ok(())
3743                    }
3744
3745                    // ── Arithmetic (integer fast paths) ──
3746                    Op::Add => {
3747                        let b = self.pop();
3748                        let a = self.pop();
3749                        self.push_binop_with_overload(BinOp::Add, a, b, |a, b| {
3750                            if let Some(s) = crate::sketches::try_sketch_binop(
3751                                crate::sketches::SketchOp::Add,
3752                                a,
3753                                b,
3754                            ) {
3755                                return Ok(s);
3756                            }
3757                            Ok(crate::value::compat_add(a, b))
3758                        })
3759                    }
3760                    Op::Sub => {
3761                        let b = self.pop();
3762                        let a = self.pop();
3763                        self.push_binop_with_overload(BinOp::Sub, a, b, |a, b| {
3764                            if let Some(s) = crate::sketches::try_sketch_binop(
3765                                crate::sketches::SketchOp::Sub,
3766                                a,
3767                                b,
3768                            ) {
3769                                return Ok(s);
3770                            }
3771                            Ok(crate::value::compat_sub(a, b))
3772                        })
3773                    }
3774                    Op::Mul => {
3775                        let b = self.pop();
3776                        let a = self.pop();
3777                        self.push_binop_with_overload(BinOp::Mul, a, b, |a, b| {
3778                            Ok(crate::value::compat_mul(a, b))
3779                        })
3780                    }
3781                    Op::Div => {
3782                        let b = self.pop();
3783                        let a = self.pop();
3784                        let line = self.line();
3785                        self.push_binop_with_overload(BinOp::Div, a, b, |a, b| {
3786                            if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
3787                                if y == 0 {
3788                                    return Err(StrykeError::division_by_zero(
3789                                        "Illegal division by zero",
3790                                        line,
3791                                    ));
3792                                }
3793                                Ok(if x % y == 0 {
3794                                    StrykeValue::integer(x / y)
3795                                } else {
3796                                    StrykeValue::float(x as f64 / y as f64)
3797                                })
3798                            } else {
3799                                let d = b.to_number();
3800                                if d == 0.0 {
3801                                    return Err(StrykeError::division_by_zero(
3802                                        "Illegal division by zero",
3803                                        line,
3804                                    ));
3805                                }
3806                                Ok(StrykeValue::float(a.to_number() / d))
3807                            }
3808                        })
3809                    }
3810                    Op::Mod => {
3811                        let b = self.pop();
3812                        let a = self.pop();
3813                        let line = self.line();
3814                        self.push_binop_with_overload(BinOp::Mod, a, b, |a, b| {
3815                            let b = b.to_int();
3816                            let a = a.to_int();
3817                            if b == 0 {
3818                                return Err(StrykeError::division_by_zero(
3819                                    "Illegal modulus zero",
3820                                    line,
3821                                ));
3822                            }
3823                            Ok(StrykeValue::integer(crate::value::perl_mod_i64(a, b)))
3824                        })
3825                    }
3826                    Op::Pow => {
3827                        let b = self.pop();
3828                        let a = self.pop();
3829                        self.push_binop_with_overload(BinOp::Pow, a, b, |a, b| {
3830                            Ok(crate::value::compat_pow(a, b))
3831                        })
3832                    }
3833                    Op::Negate => {
3834                        let a = self.pop();
3835                        let line = self.line();
3836                        if let Some(exec_res) =
3837                            self.interp.try_overload_unary_dispatch("neg", &a, line)
3838                        {
3839                            self.push(vm_interp_result(exec_res, line)?);
3840                        } else {
3841                            self.push(if let Some(n) = a.as_integer() {
3842                                StrykeValue::integer(-n)
3843                            } else {
3844                                StrykeValue::float(-a.to_number())
3845                            });
3846                        }
3847                        Ok(())
3848                    }
3849                    Op::Inc => {
3850                        let a = self.pop();
3851                        self.push(if let Some(n) = a.as_integer() {
3852                            StrykeValue::integer(n.wrapping_add(1))
3853                        } else {
3854                            StrykeValue::float(a.to_number() + 1.0)
3855                        });
3856                        Ok(())
3857                    }
3858                    Op::Dec => {
3859                        let a = self.pop();
3860                        self.push(if let Some(n) = a.as_integer() {
3861                            StrykeValue::integer(n.wrapping_sub(1))
3862                        } else {
3863                            StrykeValue::float(a.to_number() - 1.0)
3864                        });
3865                        Ok(())
3866                    }
3867
3868                    // ── String ──
3869                    Op::Concat => {
3870                        let b = self.pop();
3871                        let a = self.pop();
3872                        let out = self.concat_stack_values(a, b)?;
3873                        self.push(out);
3874                        Ok(())
3875                    }
3876                    Op::ArrayStringifyListSep => {
3877                        let raw = self.pop();
3878                        let v = self.interp.peel_array_ref_for_list_join(raw);
3879                        let sep = self.interp.list_separator.clone();
3880                        let list = v.to_list();
3881                        let joined = list
3882                            .iter()
3883                            .map(|x| x.to_string())
3884                            .collect::<Vec<_>>()
3885                            .join(&sep);
3886                        self.push(StrykeValue::string(joined));
3887                        Ok(())
3888                    }
3889                    Op::StringRepeat => {
3890                        let n = self.pop().to_int().max(0) as usize;
3891                        let val = self.pop();
3892                        self.push(StrykeValue::string(val.to_string().repeat(n)));
3893                        Ok(())
3894                    }
3895                    Op::ListRepeat => {
3896                        let n = self.pop().to_int().max(0) as usize;
3897                        let val = self.pop();
3898                        // Flatten to a Vec<StrykeValue>: an array value gives its
3899                        // items; a scalar (e.g. `(0) x 5` after the LHS evaluates
3900                        // through scalar-collapse paths) wraps as a 1-elt list.
3901                        let items: Vec<StrykeValue> =
3902                            val.as_array_vec().unwrap_or_else(|| vec![val]);
3903                        let mut out = Vec::with_capacity(items.len().saturating_mul(n));
3904                        for _ in 0..n {
3905                            out.extend(items.iter().cloned());
3906                        }
3907                        self.push(StrykeValue::array(out));
3908                        Ok(())
3909                    }
3910                    Op::ProcessCaseEscapes => {
3911                        let val = self.pop();
3912                        let s = val.to_string();
3913                        let processed = VMHelper::process_case_escapes(&s);
3914                        self.push(StrykeValue::string(processed));
3915                        Ok(())
3916                    }
3917
3918                    // ── Numeric comparison ──
3919                    Op::NumEq => {
3920                        let b = self.pop();
3921                        let a = self.pop();
3922                        self.push_binop_with_overload(BinOp::NumEq, a.clone(), b.clone(), |a, b| {
3923                            // Struct equality: compare all fields
3924                            if let (Some(sa), Some(sb)) = (a.as_struct_inst(), b.as_struct_inst()) {
3925                                if sa.def.name != sb.def.name {
3926                                    return Ok(StrykeValue::integer(0));
3927                                }
3928                                let av = sa.get_values();
3929                                let bv = sb.get_values();
3930                                let eq = av.len() == bv.len()
3931                                    && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
3932                                Ok(StrykeValue::integer(if eq { 1 } else { 0 }))
3933                            } else {
3934                                if !crate::compat_mode() && both_non_numeric_strings(a, b) {
3935                                    let sa = a.to_string();
3936                                    let sb = b.to_string();
3937                                    return Ok(StrykeValue::integer(if sa == sb { 1 } else { 0 }));
3938                                }
3939                                Ok(int_cmp(a, b, |x, y| x == y, |x, y| x == y))
3940                            }
3941                        })
3942                    }
3943                    Op::NumNe => {
3944                        let b = self.pop();
3945                        let a = self.pop();
3946                        self.push_binop_with_overload(BinOp::NumNe, a, b, |a, b| {
3947                            // Stryke (non-compat) sugar: when both operands are
3948                            // non-numeric strings, fall back to `ne`. In Perl,
3949                            // `"G" != "T"` is `0 != 0` = false; in stryke we
3950                            // want char/string compare. Compat mode keeps
3951                            // Perl semantics.
3952                            if !crate::compat_mode() && both_non_numeric_strings(a, b) {
3953                                let sa = a.to_string();
3954                                let sb = b.to_string();
3955                                return Ok(StrykeValue::integer(if sa != sb { 1 } else { 0 }));
3956                            }
3957                            Ok(int_cmp(a, b, |x, y| x != y, |x, y| x != y))
3958                        })
3959                    }
3960                    Op::NumLt => {
3961                        let b = self.pop();
3962                        let a = self.pop();
3963                        self.push_binop_with_overload(BinOp::NumLt, a, b, |a, b| {
3964                            Ok(int_cmp(a, b, |x, y| x < y, |x, y| x < y))
3965                        })
3966                    }
3967                    Op::NumGt => {
3968                        let b = self.pop();
3969                        let a = self.pop();
3970                        self.push_binop_with_overload(BinOp::NumGt, a, b, |a, b| {
3971                            Ok(int_cmp(a, b, |x, y| x > y, |x, y| x > y))
3972                        })
3973                    }
3974                    Op::NumLe => {
3975                        let b = self.pop();
3976                        let a = self.pop();
3977                        self.push_binop_with_overload(BinOp::NumLe, a, b, |a, b| {
3978                            Ok(int_cmp(a, b, |x, y| x <= y, |x, y| x <= y))
3979                        })
3980                    }
3981                    Op::NumGe => {
3982                        let b = self.pop();
3983                        let a = self.pop();
3984                        self.push_binop_with_overload(BinOp::NumGe, a, b, |a, b| {
3985                            Ok(int_cmp(a, b, |x, y| x >= y, |x, y| x >= y))
3986                        })
3987                    }
3988                    Op::Spaceship => {
3989                        let b = self.pop();
3990                        let a = self.pop();
3991                        self.push_binop_with_overload(BinOp::Spaceship, a, b, |a, b| {
3992                            Ok(
3993                                if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
3994                                    StrykeValue::integer(if x < y {
3995                                        -1
3996                                    } else if x > y {
3997                                        1
3998                                    } else {
3999                                        0
4000                                    })
4001                                } else {
4002                                    let x = a.to_number();
4003                                    let y = b.to_number();
4004                                    StrykeValue::integer(if x < y {
4005                                        -1
4006                                    } else if x > y {
4007                                        1
4008                                    } else {
4009                                        0
4010                                    })
4011                                },
4012                            )
4013                        })
4014                    }
4015
4016                    // ── String comparison ──
4017                    Op::StrEq => {
4018                        let b = self.pop();
4019                        let a = self.pop();
4020                        self.push_binop_with_overload(BinOp::StrEq, a, b, |a, b| {
4021                            Ok(StrykeValue::integer(if a.str_eq(b) { 1 } else { 0 }))
4022                        })
4023                    }
4024                    Op::StrNe => {
4025                        let b = self.pop();
4026                        let a = self.pop();
4027                        self.push_binop_with_overload(BinOp::StrNe, a, b, |a, b| {
4028                            Ok(StrykeValue::integer(if !a.str_eq(b) { 1 } else { 0 }))
4029                        })
4030                    }
4031                    Op::StrLt => {
4032                        let b = self.pop();
4033                        let a = self.pop();
4034                        self.push_binop_with_overload(BinOp::StrLt, a, b, |a, b| {
4035                            Ok(StrykeValue::integer(
4036                                if a.str_cmp(b) == std::cmp::Ordering::Less {
4037                                    1
4038                                } else {
4039                                    0
4040                                },
4041                            ))
4042                        })
4043                    }
4044                    Op::StrGt => {
4045                        let b = self.pop();
4046                        let a = self.pop();
4047                        self.push_binop_with_overload(BinOp::StrGt, a, b, |a, b| {
4048                            Ok(StrykeValue::integer(
4049                                if a.str_cmp(b) == std::cmp::Ordering::Greater {
4050                                    1
4051                                } else {
4052                                    0
4053                                },
4054                            ))
4055                        })
4056                    }
4057                    Op::StrLe => {
4058                        let b = self.pop();
4059                        let a = self.pop();
4060                        self.push_binop_with_overload(BinOp::StrLe, a, b, |a, b| {
4061                            let o = a.str_cmp(b);
4062                            Ok(StrykeValue::integer(
4063                                if matches!(o, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
4064                                {
4065                                    1
4066                                } else {
4067                                    0
4068                                },
4069                            ))
4070                        })
4071                    }
4072                    Op::StrGe => {
4073                        let b = self.pop();
4074                        let a = self.pop();
4075                        self.push_binop_with_overload(BinOp::StrGe, a, b, |a, b| {
4076                            let o = a.str_cmp(b);
4077                            Ok(StrykeValue::integer(
4078                                if matches!(
4079                                    o,
4080                                    std::cmp::Ordering::Greater | std::cmp::Ordering::Equal
4081                                ) {
4082                                    1
4083                                } else {
4084                                    0
4085                                },
4086                            ))
4087                        })
4088                    }
4089                    Op::StrCmp => {
4090                        let b = self.pop();
4091                        let a = self.pop();
4092                        self.push_binop_with_overload(BinOp::StrCmp, a, b, |a, b| {
4093                            let cmp = a.str_cmp(b);
4094                            Ok(StrykeValue::integer(match cmp {
4095                                std::cmp::Ordering::Less => -1,
4096                                std::cmp::Ordering::Greater => 1,
4097                                std::cmp::Ordering::Equal => 0,
4098                            }))
4099                        })
4100                    }
4101
4102                    // ── Logical / Bitwise ──
4103                    Op::LogNot => {
4104                        let a = self.pop();
4105                        let line = self.line();
4106                        if let Some(exec_res) =
4107                            self.interp.try_overload_unary_dispatch("bool", &a, line)
4108                        {
4109                            let pv = vm_interp_result(exec_res, line)?;
4110                            self.push(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
4111                        } else {
4112                            self.push(StrykeValue::integer(if a.is_true() { 0 } else { 1 }));
4113                        }
4114                        Ok(())
4115                    }
4116                    Op::BitAnd => {
4117                        let rv = self.pop();
4118                        let lv = self.pop();
4119                        if let Some(s) = crate::value::set_intersection(&lv, &rv) {
4120                            self.push(s);
4121                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4122                            crate::sketches::SketchOp::And,
4123                            &lv,
4124                            &rv,
4125                        ) {
4126                            self.push(s);
4127                        } else {
4128                            self.push(StrykeValue::integer(lv.to_int() & rv.to_int()));
4129                        }
4130                        Ok(())
4131                    }
4132                    Op::BitOr => {
4133                        let rv = self.pop();
4134                        let lv = self.pop();
4135                        if let Some(s) = crate::value::set_union(&lv, &rv) {
4136                            self.push(s);
4137                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4138                            crate::sketches::SketchOp::Or,
4139                            &lv,
4140                            &rv,
4141                        ) {
4142                            self.push(s);
4143                        } else {
4144                            self.push(StrykeValue::integer(lv.to_int() | rv.to_int()));
4145                        }
4146                        Ok(())
4147                    }
4148                    Op::BitXor => {
4149                        let rv = self.pop();
4150                        let lv = self.pop();
4151                        if let Some(s) = crate::sketches::try_sketch_binop(
4152                            crate::sketches::SketchOp::Xor,
4153                            &lv,
4154                            &rv,
4155                        ) {
4156                            self.push(s);
4157                        } else {
4158                            self.push(StrykeValue::integer(lv.to_int() ^ rv.to_int()));
4159                        }
4160                        Ok(())
4161                    }
4162                    Op::BitNot => {
4163                        let a = self.pop().to_int();
4164                        self.push(StrykeValue::integer(!a));
4165                        Ok(())
4166                    }
4167                    Op::Shl => {
4168                        let b = self.pop().to_int();
4169                        let a = self.pop().to_int();
4170                        self.push(StrykeValue::integer(perl_shl_i64(a, b)));
4171                        Ok(())
4172                    }
4173                    Op::Shr => {
4174                        let b = self.pop().to_int();
4175                        let a = self.pop().to_int();
4176                        self.push(StrykeValue::integer(perl_shr_i64(a, b)));
4177                        Ok(())
4178                    }
4179
4180                    // ── Control flow ──
4181                    Op::Jump(target) => {
4182                        self.ip = *target;
4183                        Ok(())
4184                    }
4185                    Op::JumpIfTrue(target) => {
4186                        let val = self.pop();
4187                        if val.is_true() {
4188                            self.ip = *target;
4189                        }
4190                        Ok(())
4191                    }
4192                    Op::JumpIfFalse(target) => {
4193                        let val = self.pop();
4194                        if !val.is_true() {
4195                            self.ip = *target;
4196                        }
4197                        Ok(())
4198                    }
4199                    Op::JumpIfFalseKeep(target) => {
4200                        if !self.peek().is_true() {
4201                            self.ip = *target;
4202                        } else {
4203                            self.pop();
4204                        }
4205                        Ok(())
4206                    }
4207                    Op::JumpIfTrueKeep(target) => {
4208                        if self.peek().is_true() {
4209                            self.ip = *target;
4210                        } else {
4211                            self.pop();
4212                        }
4213                        Ok(())
4214                    }
4215                    Op::JumpIfDefinedKeep(target) => {
4216                        if !self.peek().is_undef() {
4217                            self.ip = *target;
4218                        } else {
4219                            self.pop();
4220                        }
4221                        Ok(())
4222                    }
4223
4224                    // ── Increment / Decrement ──
4225                    Op::PreInc(idx) => {
4226                        let n = names[*idx as usize].as_str();
4227                        self.require_scalar_mutable(n)?;
4228                        let en = self.interp.english_scalar_name(n);
4229                        let new_val = self
4230                            .interp
4231                            .scope
4232                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() + 1))
4233                            .map_err(|e| e.at_line(self.line()))?;
4234                        self.push(new_val);
4235                        Ok(())
4236                    }
4237                    Op::PreDec(idx) => {
4238                        let n = names[*idx as usize].as_str();
4239                        self.require_scalar_mutable(n)?;
4240                        let en = self.interp.english_scalar_name(n);
4241                        let new_val = self
4242                            .interp
4243                            .scope
4244                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() - 1))
4245                            .map_err(|e| e.at_line(self.line()))?;
4246                        self.push(new_val);
4247                        Ok(())
4248                    }
4249                    Op::PostInc(idx) => {
4250                        let n = names[*idx as usize].as_str();
4251                        self.require_scalar_mutable(n)?;
4252                        let en = self.interp.english_scalar_name(n);
4253                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4254                            self.interp
4255                                .scope
4256                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
4257                                .map_err(|e| e.at_line(self.line()))?;
4258                            self.ip += 1;
4259                        } else {
4260                            let old = self
4261                                .interp
4262                                .scope
4263                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
4264                                .map_err(|e| e.at_line(self.line()))?;
4265                            self.push(old);
4266                        }
4267                        Ok(())
4268                    }
4269                    Op::PostDec(idx) => {
4270                        let n = names[*idx as usize].as_str();
4271                        self.require_scalar_mutable(n)?;
4272                        let en = self.interp.english_scalar_name(n);
4273                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4274                            self.interp
4275                                .scope
4276                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
4277                                .map_err(|e| e.at_line(self.line()))?;
4278                            self.ip += 1;
4279                        } else {
4280                            let old = self
4281                                .interp
4282                                .scope
4283                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
4284                                .map_err(|e| e.at_line(self.line()))?;
4285                            self.push(old);
4286                        }
4287                        Ok(())
4288                    }
4289                    Op::PreIncSlot(slot) => {
4290                        let cur = self.interp.scope.get_scalar_slot(*slot);
4291                        let new_val = crate::vm_helper::perl_inc(&cur);
4292                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
4293                        self.push(new_val);
4294                        Ok(())
4295                    }
4296                    Op::PreIncSlotVoid(slot) => {
4297                        let cur = self.interp.scope.get_scalar_slot(*slot);
4298                        let new_val = crate::vm_helper::perl_inc(&cur);
4299                        self.interp.scope.set_scalar_slot(*slot, new_val);
4300                        Ok(())
4301                    }
4302                    Op::PreDecSlot(slot) => {
4303                        let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
4304                        let new_val = StrykeValue::integer(val);
4305                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
4306                        self.push(new_val);
4307                        Ok(())
4308                    }
4309                    Op::PostIncSlot(slot) => {
4310                        // Fuse PostIncSlot+Pop: if next op discards the old value, skip stack work.
4311                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4312                            let cur = self.interp.scope.get_scalar_slot(*slot);
4313                            let new_val = crate::vm_helper::perl_inc(&cur);
4314                            self.interp.scope.set_scalar_slot(*slot, new_val);
4315                            self.ip += 1; // skip Pop
4316                        } else {
4317                            let old = self.interp.scope.get_scalar_slot(*slot);
4318                            let new_val = crate::vm_helper::perl_inc(&old);
4319                            self.interp.scope.set_scalar_slot(*slot, new_val);
4320                            self.push(old);
4321                        }
4322                        Ok(())
4323                    }
4324                    Op::PostDecSlot(slot) => {
4325                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4326                            let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
4327                            self.interp
4328                                .scope
4329                                .set_scalar_slot(*slot, StrykeValue::integer(val));
4330                            self.ip += 1;
4331                        } else {
4332                            let old = self.interp.scope.get_scalar_slot(*slot);
4333                            let new_val = StrykeValue::integer(old.to_int() - 1);
4334                            self.interp.scope.set_scalar_slot(*slot, new_val);
4335                            self.push(old);
4336                        }
4337                        Ok(())
4338                    }
4339
4340                    // ── Functions ──
4341                    Op::Call(name_idx, argc, wa) => {
4342                        // A bare callable spelling (`sum`, `set`, `count`, …) routes
4343                        // to the global builtin even when a same-named user sub is
4344                        // registered. There is no shadowing of stryke builtins in
4345                        // default mode: user code can declare `fn sum {}` inside a
4346                        // non-main package, but the only way to reach that user sub
4347                        // is the fully-qualified `Pkg::sum(...)` spelling.
4348                        // `--compat` (full Perl 5 mode) restores classic UDF-wins
4349                        // semantics so unmodified Perl 5 modules keep working.
4350                        let name = &self.names[*name_idx as usize];
4351                        let entry_opt = if !crate::compat_mode()
4352                            && !name.contains("::")
4353                            && crate::builtins::is_callable_spelling(name)
4354                        {
4355                            None
4356                        } else {
4357                            self.find_sub_entry(*name_idx)
4358                        };
4359                        self.vm_dispatch_user_call(*name_idx, entry_opt, *argc, *wa, None)?;
4360                        Ok(())
4361                    }
4362                    Op::CallStaticSubId(sid, name_idx, argc, wa) => {
4363                        let t = self.static_sub_calls.get(*sid as usize).ok_or_else(|| {
4364                            StrykeError::runtime("VM: invalid CallStaticSubId", self.line())
4365                        })?;
4366                        debug_assert_eq!(t.2, *name_idx);
4367                        let closure_sub = self
4368                            .static_sub_closure_subs
4369                            .get(*sid as usize)
4370                            .and_then(|x| x.clone());
4371                        self.vm_dispatch_user_call(
4372                            *name_idx,
4373                            Some((t.0, t.1)),
4374                            *argc,
4375                            *wa,
4376                            closure_sub,
4377                        )?;
4378                        Ok(())
4379                    }
4380                    Op::Return => {
4381                        if let Some(frame) = self.call_stack.pop() {
4382                            if frame.block_region {
4383                                return Err(StrykeError::runtime(
4384                                    "Return in map/grep/sort block bytecode",
4385                                    self.line(),
4386                                ));
4387                            }
4388                            if let Some(t0) = frame.sub_profiler_start {
4389                                if let Some(p) = &mut self.interp.profiler {
4390                                    p.exit_sub(t0.elapsed());
4391                                }
4392                            }
4393                            self.interp.debugger_leave_sub();
4394                            self.interp.wantarray_kind = frame.saved_wantarray;
4395                            self.stack.truncate(frame.stack_base);
4396                            self.interp.pop_scope_to_depth(frame.scope_depth);
4397                            self.interp.current_sub_stack.pop();
4398                            if frame.jit_trampoline_return {
4399                                self.jit_trampoline_out = Some(StrykeValue::UNDEF);
4400                            } else {
4401                                self.push(StrykeValue::UNDEF);
4402                                self.ip = frame.return_ip;
4403                            }
4404                        } else {
4405                            self.exit_main_dispatch = true;
4406                        }
4407                        Ok(())
4408                    }
4409                    Op::ReturnValue => {
4410                        let val = self.pop();
4411                        // Resolve binding refs to real refs before scope cleanup.
4412                        // `\@array` creates a name-based ArrayBindingRef that looks
4413                        // up by name at dereference time.  If the array is a `my`
4414                        // variable, its frame will be destroyed below — so we must
4415                        // snapshot the data into an Arc-based ref now.
4416                        let val = self.resolve_binding_ref(val);
4417                        // Caller-context coercion: `return LIST` from a sub called
4418                        // in scalar context yields the **last** element of the
4419                        // list (Perl wantarray semantics). Without this, the
4420                        // whole list propagates and a `my $x = sub_returning_list()`
4421                        // sees the array stringified rather than its last element.
4422                        // (BUG-010 / BUG-011)
4423                        let val = if matches!(self.interp.wantarray_kind, WantarrayCtx::Scalar) {
4424                            if let Some(items) = val.as_array_vec() {
4425                                items.last().cloned().unwrap_or(StrykeValue::UNDEF)
4426                            } else {
4427                                val
4428                            }
4429                        } else {
4430                            val
4431                        };
4432                        if let Some(frame) = self.call_stack.pop() {
4433                            if frame.block_region {
4434                                return Err(StrykeError::runtime(
4435                                    "Return in map/grep/sort block bytecode",
4436                                    self.line(),
4437                                ));
4438                            }
4439                            if let Some(t0) = frame.sub_profiler_start {
4440                                if let Some(p) = &mut self.interp.profiler {
4441                                    p.exit_sub(t0.elapsed());
4442                                }
4443                            }
4444                            self.interp.debugger_leave_sub();
4445                            self.interp.wantarray_kind = frame.saved_wantarray;
4446                            self.stack.truncate(frame.stack_base);
4447                            self.interp.pop_scope_to_depth(frame.scope_depth);
4448                            self.interp.current_sub_stack.pop();
4449                            if frame.jit_trampoline_return {
4450                                self.jit_trampoline_out = Some(val);
4451                            } else {
4452                                self.push(val);
4453                                self.ip = frame.return_ip;
4454                            }
4455                        } else {
4456                            self.exit_main_dispatch_value = Some(val);
4457                            self.exit_main_dispatch = true;
4458                        }
4459                        Ok(())
4460                    }
4461                    Op::BlockReturnValue => {
4462                        let val = self.pop();
4463                        let val = self.resolve_binding_ref(val);
4464                        if let Some(frame) = self.call_stack.pop() {
4465                            if !frame.block_region {
4466                                return Err(StrykeError::runtime(
4467                                    "BlockReturnValue without map/grep/sort block frame",
4468                                    self.line(),
4469                                ));
4470                            }
4471                            self.interp.wantarray_kind = frame.saved_wantarray;
4472                            self.stack.truncate(frame.stack_base);
4473                            self.interp.pop_scope_to_depth(frame.scope_depth);
4474                            self.block_region_return = Some(val);
4475                            Ok(())
4476                        } else {
4477                            Err(StrykeError::runtime(
4478                                "BlockReturnValue with empty call stack",
4479                                self.line(),
4480                            ))
4481                        }
4482                    }
4483                    Op::BindSubClosure(name_idx) => {
4484                        let n = names[*name_idx as usize].as_str();
4485                        self.interp.rebind_sub_closure(n);
4486                        Ok(())
4487                    }
4488
4489                    // ── Scope ──
4490                    Op::PushFrame => {
4491                        self.interp.scope_push_hook();
4492                        Ok(())
4493                    }
4494                    Op::PopFrame => {
4495                        self.interp.scope_pop_hook();
4496                        Ok(())
4497                    }
4498                    // ── I/O ──
4499                    Op::Print(handle_idx, argc) => {
4500                        let argc = *argc as usize;
4501                        let mut args = Vec::with_capacity(argc);
4502                        for _ in 0..argc {
4503                            args.push(self.pop());
4504                        }
4505                        args.reverse();
4506                        let mut output = String::new();
4507                        if args.is_empty() {
4508                            let topic = self.interp.scope.get_scalar("_").clone();
4509                            let s = match self.interp.stringify_value(topic, self.line()) {
4510                                Ok(s) => s,
4511                                Err(FlowOrError::Error(e)) => return Err(e),
4512                                Err(FlowOrError::Flow(_)) => {
4513                                    return Err(StrykeError::runtime(
4514                                        "print: unexpected control flow",
4515                                        self.line(),
4516                                    ));
4517                                }
4518                            };
4519                            output.push_str(&s);
4520                        } else {
4521                            for (i, arg) in args.iter().enumerate() {
4522                                if i > 0 && !self.interp.ofs.is_empty() {
4523                                    output.push_str(&self.interp.ofs);
4524                                }
4525                                for item in arg.to_list() {
4526                                    let s = match self.interp.stringify_value(item, self.line()) {
4527                                        Ok(s) => s,
4528                                        Err(FlowOrError::Error(e)) => return Err(e),
4529                                        Err(FlowOrError::Flow(_)) => {
4530                                            return Err(StrykeError::runtime(
4531                                                "print: unexpected control flow",
4532                                                self.line(),
4533                                            ));
4534                                        }
4535                                    };
4536                                    output.push_str(&s);
4537                                }
4538                            }
4539                        }
4540                        output.push_str(&self.interp.ors);
4541                        let handle_name = match handle_idx {
4542                            Some(idx) => self.interp.resolve_io_handle_name(
4543                                self.names
4544                                    .get(*idx as usize)
4545                                    .map_or("STDOUT", |s| s.as_str()),
4546                            ),
4547                            None => self
4548                                .interp
4549                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
4550                        };
4551                        self.interp.write_formatted_print(
4552                            handle_name.as_str(),
4553                            &output,
4554                            self.line(),
4555                        )?;
4556                        self.push(StrykeValue::integer(1));
4557                        Ok(())
4558                    }
4559                    Op::Printf(handle_idx, argc) => {
4560                        let argc = *argc as usize;
4561                        let mut args = Vec::with_capacity(argc);
4562                        for _ in 0..argc {
4563                            args.push(self.pop());
4564                        }
4565                        args.reverse();
4566                        let (fmt, rest) = match args.split_first() {
4567                            Some((f, r)) => (f.to_string(), r),
4568                            None => {
4569                                return Err(StrykeError::runtime(
4570                                    "printf requires a format string",
4571                                    self.line(),
4572                                ));
4573                            }
4574                        };
4575                        // sprintf the args, then route through the handle the
4576                        // same way Print does — fixes printf's silent
4577                        // misdirection to STDOUT.
4578                        let mut flat = Vec::new();
4579                        for a in rest {
4580                            if let Some(items) = a.as_array_vec() {
4581                                flat.extend(items);
4582                            } else {
4583                                flat.push(a.clone());
4584                            }
4585                        }
4586                        let s = match self.interp.perl_sprintf_stringify(&fmt, &flat, self.line()) {
4587                            Ok(s) => s,
4588                            Err(FlowOrError::Error(e)) => return Err(e),
4589                            Err(FlowOrError::Flow(_)) => {
4590                                return Err(StrykeError::runtime(
4591                                    "printf: unexpected control flow",
4592                                    self.line(),
4593                                ));
4594                            }
4595                        };
4596                        let handle_name = match handle_idx {
4597                            Some(idx) => self.interp.resolve_io_handle_name(
4598                                self.names
4599                                    .get(*idx as usize)
4600                                    .map_or("STDOUT", |s| s.as_str()),
4601                            ),
4602                            None => self
4603                                .interp
4604                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
4605                        };
4606                        self.interp
4607                            .write_formatted_print(handle_name.as_str(), &s, self.line())?;
4608                        self.push(StrykeValue::integer(1));
4609                        Ok(())
4610                    }
4611                    Op::Say(handle_idx, argc) => {
4612                        if (self.interp.feature_bits & crate::vm_helper::FEAT_SAY) == 0 {
4613                            return Err(StrykeError::runtime(
4614                            "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
4615                            self.line(),
4616                        ));
4617                        }
4618                        let argc = *argc as usize;
4619                        let mut args = Vec::with_capacity(argc);
4620                        for _ in 0..argc {
4621                            args.push(self.pop());
4622                        }
4623                        args.reverse();
4624                        let mut output = String::new();
4625                        if args.is_empty() {
4626                            let topic = self.interp.scope.get_scalar("_").clone();
4627                            let s = match self.interp.stringify_value(topic, self.line()) {
4628                                Ok(s) => s,
4629                                Err(FlowOrError::Error(e)) => return Err(e),
4630                                Err(FlowOrError::Flow(_)) => {
4631                                    return Err(StrykeError::runtime(
4632                                        "say: unexpected control flow",
4633                                        self.line(),
4634                                    ));
4635                                }
4636                            };
4637                            output.push_str(&s);
4638                        } else {
4639                            for (i, arg) in args.iter().enumerate() {
4640                                if i > 0 && !self.interp.ofs.is_empty() {
4641                                    output.push_str(&self.interp.ofs);
4642                                }
4643                                for item in arg.to_list() {
4644                                    let s = match self.interp.stringify_value(item, self.line()) {
4645                                        Ok(s) => s,
4646                                        Err(FlowOrError::Error(e)) => return Err(e),
4647                                        Err(FlowOrError::Flow(_)) => {
4648                                            return Err(StrykeError::runtime(
4649                                                "say: unexpected control flow",
4650                                                self.line(),
4651                                            ));
4652                                        }
4653                                    };
4654                                    output.push_str(&s);
4655                                }
4656                            }
4657                        }
4658                        output.push('\n');
4659                        output.push_str(&self.interp.ors);
4660                        let handle_name = match handle_idx {
4661                            Some(idx) => self.interp.resolve_io_handle_name(
4662                                self.names
4663                                    .get(*idx as usize)
4664                                    .map_or("STDOUT", |s| s.as_str()),
4665                            ),
4666                            None => self
4667                                .interp
4668                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
4669                        };
4670                        self.interp.write_formatted_print(
4671                            handle_name.as_str(),
4672                            &output,
4673                            self.line(),
4674                        )?;
4675                        self.push(StrykeValue::integer(1));
4676                        Ok(())
4677                    }
4678
4679                    // ── Built-in dispatch ──
4680                    Op::CallBuiltin(id, argc) => {
4681                        let argc = *argc as usize;
4682                        let mut args = Vec::with_capacity(argc);
4683                        for _ in 0..argc {
4684                            args.push(self.pop());
4685                        }
4686                        args.reverse();
4687                        let result = self.exec_builtin(*id, args)?;
4688                        self.push(result);
4689                        Ok(())
4690                    }
4691                    Op::WantarrayPush(wa) => {
4692                        self.wantarray_stack.push(self.interp.wantarray_kind);
4693                        self.interp.wantarray_kind = WantarrayCtx::from_byte(*wa);
4694                        Ok(())
4695                    }
4696                    Op::WantarrayPop => {
4697                        self.interp.wantarray_kind =
4698                            self.wantarray_stack.pop().unwrap_or(WantarrayCtx::Scalar);
4699                        Ok(())
4700                    }
4701
4702                    // ── List / Range ──
4703                    Op::MakeArray(n) => {
4704                        let n = *n as usize;
4705                        // Pops are last-to-first on the stack; reverse to source (left-to-right) order,
4706                        // then flatten nested arrays in place (Perl list literal semantics).
4707                        // Hashes flatten to alternating key/value entries — Perl's
4708                        // `(%a, %b)` splat-merge idiom relies on this; without it
4709                        // each hash collapses to its scalar bucket-fill string.
4710                        let mut stack_vals = Vec::with_capacity(n);
4711                        for _ in 0..n {
4712                            stack_vals.push(self.pop());
4713                        }
4714                        stack_vals.reverse();
4715                        let mut arr = Vec::new();
4716                        for v in stack_vals {
4717                            if let Some(items) = v.as_array_vec() {
4718                                arr.extend(items);
4719                            } else if let Some(map) = v.as_hash_map() {
4720                                for (k, vv) in map {
4721                                    arr.push(StrykeValue::string(k));
4722                                    arr.push(vv);
4723                                }
4724                            } else {
4725                                arr.push(v);
4726                            }
4727                        }
4728                        self.push(StrykeValue::array(arr));
4729                        Ok(())
4730                    }
4731                    Op::HashSliceDeref(n) => {
4732                        let n = *n as usize;
4733                        let mut key_vals = Vec::with_capacity(n);
4734                        for _ in 0..n {
4735                            key_vals.push(self.pop());
4736                        }
4737                        key_vals.reverse();
4738                        let container = self.pop();
4739                        let line = self.line();
4740                        let out = vm_interp_result(
4741                            self.interp
4742                                .hash_slice_deref_values(&container, &key_vals, line),
4743                            line,
4744                        )?;
4745                        self.push(out);
4746                        Ok(())
4747                    }
4748                    Op::ArrowArraySlice(n) => {
4749                        let n = *n as usize;
4750                        let idxs = self.pop_flattened_array_slice_specs(n);
4751                        let r = self.pop();
4752                        let line = self.line();
4753                        let out = vm_interp_result(
4754                            self.interp.arrow_array_slice_values(r, &idxs, line),
4755                            line,
4756                        )?;
4757                        self.push(out);
4758                        Ok(())
4759                    }
4760                    Op::SetHashSliceDeref(n) => {
4761                        let n = *n as usize;
4762                        let mut key_vals = Vec::with_capacity(n);
4763                        for _ in 0..n {
4764                            key_vals.push(self.pop());
4765                        }
4766                        key_vals.reverse();
4767                        let container = self.pop();
4768                        let val = self.pop();
4769                        let line = self.line();
4770                        vm_interp_result(
4771                            self.interp
4772                                .assign_hash_slice_deref(container, key_vals, val, line),
4773                            line,
4774                        )?;
4775                        Ok(())
4776                    }
4777                    Op::SetHashSlice(hash_idx, n) => {
4778                        let n = *n as usize;
4779                        let mut key_vals = Vec::with_capacity(n);
4780                        for _ in 0..n {
4781                            key_vals.push(self.pop());
4782                        }
4783                        key_vals.reverse();
4784                        let name = names[*hash_idx as usize].as_str();
4785                        self.require_hash_mutable(name)?;
4786                        let val = self.pop();
4787                        let line = self.line();
4788                        vm_interp_result(
4789                            self.interp
4790                                .assign_named_hash_slice(name, key_vals, val, line),
4791                            line,
4792                        )?;
4793                        Ok(())
4794                    }
4795                    Op::GetHashSlice(hash_idx, n) => {
4796                        let n = *n as usize;
4797                        let mut key_vals = Vec::with_capacity(n);
4798                        for _ in 0..n {
4799                            key_vals.push(self.pop());
4800                        }
4801                        key_vals.reverse();
4802                        let name = names[*hash_idx as usize].as_str();
4803                        let h = self.interp.scope.get_hash(name);
4804                        let mut result = Vec::new();
4805                        for kv in &key_vals {
4806                            // Flatten arrays AND arrayrefs (e.g. `@h{@$kref}`)
4807                            // — both shapes can carry the keys list.
4808                            if let Some(vv) = kv.as_array_vec() {
4809                                for v in vv {
4810                                    let k = v.to_string();
4811                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
4812                                }
4813                            } else if let Some(r) = kv.as_array_ref() {
4814                                for v in r.read().iter() {
4815                                    let k = v.to_string();
4816                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
4817                                }
4818                            } else {
4819                                let k = kv.to_string();
4820                                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
4821                            }
4822                        }
4823                        self.push(StrykeValue::array(result));
4824                        Ok(())
4825                    }
4826                    Op::HashSliceDerefCompound(op_byte, n) => {
4827                        let n = *n as usize;
4828                        let mut key_vals = Vec::with_capacity(n);
4829                        for _ in 0..n {
4830                            key_vals.push(self.pop());
4831                        }
4832                        key_vals.reverse();
4833                        let container = self.pop();
4834                        let rhs = self.pop();
4835                        let line = self.line();
4836                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
4837                            .ok_or_else(|| {
4838                                crate::error::StrykeError::runtime(
4839                                    "VM: HashSliceDerefCompound: bad op byte",
4840                                    line,
4841                                )
4842                            })?;
4843                        let new_val = vm_interp_result(
4844                            self.interp.compound_assign_hash_slice_deref(
4845                                container, key_vals, op, rhs, line,
4846                            ),
4847                            line,
4848                        )?;
4849                        self.push(new_val);
4850                        Ok(())
4851                    }
4852                    Op::HashSliceDerefIncDec(kind, n) => {
4853                        let n = *n as usize;
4854                        let mut key_vals = Vec::with_capacity(n);
4855                        for _ in 0..n {
4856                            key_vals.push(self.pop());
4857                        }
4858                        key_vals.reverse();
4859                        let container = self.pop();
4860                        let line = self.line();
4861                        let out = vm_interp_result(
4862                            self.interp
4863                                .hash_slice_deref_inc_dec(container, key_vals, *kind, line),
4864                            line,
4865                        )?;
4866                        self.push(out);
4867                        Ok(())
4868                    }
4869                    Op::NamedHashSliceCompound(op_byte, hash_idx, n) => {
4870                        let n = *n as usize;
4871                        let mut key_vals = Vec::with_capacity(n);
4872                        for _ in 0..n {
4873                            key_vals.push(self.pop());
4874                        }
4875                        key_vals.reverse();
4876                        let name = names[*hash_idx as usize].as_str();
4877                        self.require_hash_mutable(name)?;
4878                        let rhs = self.pop();
4879                        let line = self.line();
4880                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
4881                            .ok_or_else(|| {
4882                                crate::error::StrykeError::runtime(
4883                                    "VM: NamedHashSliceCompound: bad op byte",
4884                                    line,
4885                                )
4886                            })?;
4887                        let new_val = vm_interp_result(
4888                            self.interp
4889                                .compound_assign_named_hash_slice(name, key_vals, op, rhs, line),
4890                            line,
4891                        )?;
4892                        self.push(new_val);
4893                        Ok(())
4894                    }
4895                    Op::NamedHashSliceIncDec(kind, hash_idx, n) => {
4896                        let n = *n as usize;
4897                        let mut key_vals = Vec::with_capacity(n);
4898                        for _ in 0..n {
4899                            key_vals.push(self.pop());
4900                        }
4901                        key_vals.reverse();
4902                        let name = names[*hash_idx as usize].as_str();
4903                        self.require_hash_mutable(name)?;
4904                        let line = self.line();
4905                        let out = vm_interp_result(
4906                            self.interp
4907                                .named_hash_slice_inc_dec(name, key_vals, *kind, line),
4908                            line,
4909                        )?;
4910                        self.push(out);
4911                        Ok(())
4912                    }
4913                    Op::NamedHashSlicePeekLast(hash_idx, n) => {
4914                        let n = *n as usize;
4915                        let line = self.line();
4916                        let name = names[*hash_idx as usize].as_str();
4917                        self.require_hash_mutable(name)?;
4918                        let len = self.stack.len();
4919                        if len < n {
4920                            return Err(StrykeError::runtime(
4921                                "VM: NamedHashSlicePeekLast: stack underflow",
4922                                line,
4923                            ));
4924                        }
4925                        let base = len - n;
4926                        let key_vals: Vec<StrykeValue> = self.stack[base..base + n].to_vec();
4927                        let ks = Self::flatten_hash_slice_key_slots(&key_vals);
4928                        let last_k = ks.last().ok_or_else(|| {
4929                            StrykeError::runtime("VM: NamedHashSlicePeekLast: empty key list", line)
4930                        })?;
4931                        self.interp.touch_env_hash(name);
4932                        let cur = self.interp.scope.get_hash_element(name, last_k.as_str());
4933                        self.push(cur);
4934                        Ok(())
4935                    }
4936                    Op::NamedHashSliceDropKeysKeepCur(n) => {
4937                        let n = *n as usize;
4938                        let cur = self.pop();
4939                        for _ in 0..n {
4940                            self.pop();
4941                        }
4942                        self.push(cur);
4943                        Ok(())
4944                    }
4945                    Op::SetNamedHashSliceLastKeep(hash_idx, n) => {
4946                        let n = *n as usize;
4947                        let line = self.line();
4948                        let name = names[*hash_idx as usize].as_str();
4949                        self.require_hash_mutable(name)?;
4950                        let mut key_vals_rev = Vec::with_capacity(n);
4951                        for _ in 0..n {
4952                            key_vals_rev.push(self.pop());
4953                        }
4954                        key_vals_rev.reverse();
4955                        let mut val = self.pop();
4956                        if let Some(av) = val.as_array_vec() {
4957                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
4958                        }
4959                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
4960                        let last_k = ks.last().ok_or_else(|| {
4961                            StrykeError::runtime(
4962                                "VM: SetNamedHashSliceLastKeep: empty key list",
4963                                line,
4964                            )
4965                        })?;
4966                        let val_keep = val.clone();
4967                        self.interp.touch_env_hash(name);
4968                        vm_interp_result(
4969                            self.interp
4970                                .scope
4971                                .set_hash_element(name, last_k.as_str(), val)
4972                                .map(|()| StrykeValue::UNDEF)
4973                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
4974                            line,
4975                        )?;
4976                        self.push(val_keep);
4977                        Ok(())
4978                    }
4979                    Op::HashSliceDerefPeekLast(n) => {
4980                        let n = *n as usize;
4981                        let line = self.line();
4982                        let len = self.stack.len();
4983                        if len < n + 1 {
4984                            return Err(StrykeError::runtime(
4985                                "VM: HashSliceDerefPeekLast: stack underflow",
4986                                line,
4987                            ));
4988                        }
4989                        let base = len - n - 1;
4990                        let container = self.stack[base].clone();
4991                        let key_vals: Vec<StrykeValue> =
4992                            self.stack[base + 1..base + 1 + n].to_vec();
4993                        let list = vm_interp_result(
4994                            self.interp
4995                                .hash_slice_deref_values(&container, &key_vals, line),
4996                            line,
4997                        )?;
4998                        let cur = list.to_list().last().cloned().unwrap_or(StrykeValue::UNDEF);
4999                        self.push(cur);
5000                        Ok(())
5001                    }
5002                    Op::HashSliceDerefRollValUnderKeys(n) => {
5003                        let n = *n as usize;
5004                        let val = self.pop();
5005                        let mut keys_rev = Vec::with_capacity(n);
5006                        for _ in 0..n {
5007                            keys_rev.push(self.pop());
5008                        }
5009                        let container = self.pop();
5010                        keys_rev.reverse();
5011                        self.push(val);
5012                        self.push(container);
5013                        for k in keys_rev {
5014                            self.push(k);
5015                        }
5016                        Ok(())
5017                    }
5018                    Op::HashSliceDerefSetLastKeep(n) => {
5019                        let n = *n as usize;
5020                        let line = self.line();
5021                        let mut key_vals_rev = Vec::with_capacity(n);
5022                        for _ in 0..n {
5023                            key_vals_rev.push(self.pop());
5024                        }
5025                        key_vals_rev.reverse();
5026                        let container = self.pop();
5027                        let mut val = self.pop();
5028                        if let Some(av) = val.as_array_vec() {
5029                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5030                        }
5031                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
5032                        let last_k = ks.last().ok_or_else(|| {
5033                            StrykeError::runtime(
5034                                "VM: HashSliceDerefSetLastKeep: empty key list",
5035                                line,
5036                            )
5037                        })?;
5038                        let val_keep = val.clone();
5039                        vm_interp_result(
5040                            self.interp.assign_hash_slice_one_key(
5041                                container,
5042                                last_k.as_str(),
5043                                val,
5044                                line,
5045                            ),
5046                            line,
5047                        )?;
5048                        self.push(val_keep);
5049                        Ok(())
5050                    }
5051                    Op::HashSliceDerefDropKeysKeepCur(n) => {
5052                        let n = *n as usize;
5053                        let cur = self.pop();
5054                        for _ in 0..n {
5055                            self.pop();
5056                        }
5057                        let _container = self.pop();
5058                        self.push(cur);
5059                        Ok(())
5060                    }
5061                    Op::SetArrowArraySlice(n) => {
5062                        let n = *n as usize;
5063                        let idxs = self.pop_flattened_array_slice_specs(n);
5064                        let aref = self.pop();
5065                        let val = self.pop();
5066                        let line = self.line();
5067                        vm_interp_result(
5068                            self.interp.assign_arrow_array_slice(aref, idxs, val, line),
5069                            line,
5070                        )?;
5071                        Ok(())
5072                    }
5073                    Op::ArrowArraySliceCompound(op_byte, n) => {
5074                        let n = *n as usize;
5075                        let idxs = self.pop_flattened_array_slice_specs(n);
5076                        let aref = self.pop();
5077                        let rhs = self.pop();
5078                        let line = self.line();
5079                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5080                            .ok_or_else(|| {
5081                                crate::error::StrykeError::runtime(
5082                                    "VM: ArrowArraySliceCompound: bad op byte",
5083                                    line,
5084                                )
5085                            })?;
5086                        let new_val = vm_interp_result(
5087                            self.interp
5088                                .compound_assign_arrow_array_slice(aref, idxs, op, rhs, line),
5089                            line,
5090                        )?;
5091                        self.push(new_val);
5092                        Ok(())
5093                    }
5094                    Op::ArrowArraySliceIncDec(kind, n) => {
5095                        let n = *n as usize;
5096                        let idxs = self.pop_flattened_array_slice_specs(n);
5097                        let aref = self.pop();
5098                        let line = self.line();
5099                        let out = vm_interp_result(
5100                            self.interp
5101                                .arrow_array_slice_inc_dec(aref, idxs, *kind, line),
5102                            line,
5103                        )?;
5104                        self.push(out);
5105                        Ok(())
5106                    }
5107                    Op::ArrowArraySlicePeekLast(n) => {
5108                        let n = *n as usize;
5109                        let line = self.line();
5110                        let len = self.stack.len();
5111                        if len < n + 1 {
5112                            return Err(StrykeError::runtime(
5113                                "VM: ArrowArraySlicePeekLast: stack underflow",
5114                                line,
5115                            ));
5116                        }
5117                        let base = len - n - 1;
5118                        let aref = self.stack[base].clone();
5119                        let idxs =
5120                            self.flatten_array_slice_specs_ordered_values(&self.stack[base + 1..])?;
5121                        let last = *idxs.last().ok_or_else(|| {
5122                            StrykeError::runtime(
5123                                "VM: ArrowArraySlicePeekLast: empty index list",
5124                                line,
5125                            )
5126                        })?;
5127                        let cur = vm_interp_result(
5128                            self.interp.read_arrow_array_element(aref, last, line),
5129                            line,
5130                        )?;
5131                        self.push(cur);
5132                        Ok(())
5133                    }
5134                    Op::ArrowArraySliceDropKeysKeepCur(n) => {
5135                        let n = *n as usize;
5136                        let cur = self.pop();
5137                        let _idxs = self.pop_flattened_array_slice_specs(n);
5138                        let _aref = self.pop();
5139                        self.push(cur);
5140                        Ok(())
5141                    }
5142                    Op::ArrowArraySliceRollValUnderSpecs(n) => {
5143                        let n = *n as usize;
5144                        let val = self.pop();
5145                        let mut specs_rev = Vec::with_capacity(n);
5146                        for _ in 0..n {
5147                            specs_rev.push(self.pop());
5148                        }
5149                        let aref = self.pop();
5150                        self.push(val);
5151                        self.push(aref);
5152                        for s in specs_rev.into_iter().rev() {
5153                            self.push(s);
5154                        }
5155                        Ok(())
5156                    }
5157                    Op::SetArrowArraySliceLastKeep(n) => {
5158                        let n = *n as usize;
5159                        let line = self.line();
5160                        let idxs = self.pop_flattened_array_slice_specs(n);
5161                        let aref = self.pop();
5162                        let mut val = self.pop();
5163                        // RHS is compiled in list context (`(3,4)` → one array value); Perl assigns
5164                        // only the **last** list element to the last slice index (`||=` / `&&=` / `//=`).
5165                        if let Some(av) = val.as_array_vec() {
5166                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5167                        }
5168                        let last = *idxs.last().ok_or_else(|| {
5169                            StrykeError::runtime(
5170                                "VM: SetArrowArraySliceLastKeep: empty index list",
5171                                line,
5172                            )
5173                        })?;
5174                        let val_keep = val.clone();
5175                        vm_interp_result(
5176                            self.interp.assign_arrow_array_deref(aref, last, val, line),
5177                            line,
5178                        )?;
5179                        self.push(val_keep);
5180                        Ok(())
5181                    }
5182                    Op::NamedArraySliceIncDec(kind, arr_idx, n) => {
5183                        let n = *n as usize;
5184                        let idxs = self.pop_flattened_array_slice_specs(n);
5185                        let name = names[*arr_idx as usize].as_str();
5186                        self.require_array_mutable(name)?;
5187                        let line = self.line();
5188                        let out = vm_interp_result(
5189                            self.interp
5190                                .named_array_slice_inc_dec(name, idxs, *kind, line),
5191                            line,
5192                        )?;
5193                        self.push(out);
5194                        Ok(())
5195                    }
5196                    Op::NamedArraySliceCompound(op_byte, arr_idx, n) => {
5197                        let n = *n as usize;
5198                        let idxs = self.pop_flattened_array_slice_specs(n);
5199                        let name = names[*arr_idx as usize].as_str();
5200                        self.require_array_mutable(name)?;
5201                        let rhs = self.pop();
5202                        let line = self.line();
5203                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5204                            .ok_or_else(|| {
5205                                crate::error::StrykeError::runtime(
5206                                    "VM: NamedArraySliceCompound: bad op byte",
5207                                    line,
5208                                )
5209                            })?;
5210                        let new_val = vm_interp_result(
5211                            self.interp
5212                                .compound_assign_named_array_slice(name, idxs, op, rhs, line),
5213                            line,
5214                        )?;
5215                        self.push(new_val);
5216                        Ok(())
5217                    }
5218                    Op::NamedArraySlicePeekLast(arr_idx, n) => {
5219                        let n = *n as usize;
5220                        let line = self.line();
5221                        let name = names[*arr_idx as usize].as_str();
5222                        self.require_array_mutable(name)?;
5223                        let len = self.stack.len();
5224                        if len < n {
5225                            return Err(StrykeError::runtime(
5226                                "VM: NamedArraySlicePeekLast: stack underflow",
5227                                line,
5228                            ));
5229                        }
5230                        let base = len - n;
5231                        let idxs =
5232                            self.flatten_array_slice_specs_ordered_values(&self.stack[base..])?;
5233                        let last = *idxs.last().ok_or_else(|| {
5234                            StrykeError::runtime(
5235                                "VM: NamedArraySlicePeekLast: empty index list",
5236                                line,
5237                            )
5238                        })?;
5239                        let cur = self.interp.scope.get_array_element(name, last);
5240                        self.push(cur);
5241                        Ok(())
5242                    }
5243                    Op::NamedArraySliceDropKeysKeepCur(n) => {
5244                        let n = *n as usize;
5245                        let cur = self.pop();
5246                        let _idxs = self.pop_flattened_array_slice_specs(n);
5247                        self.push(cur);
5248                        Ok(())
5249                    }
5250                    Op::NamedArraySliceRollValUnderSpecs(n) => {
5251                        let n = *n as usize;
5252                        let val = self.pop();
5253                        let mut specs_rev = Vec::with_capacity(n);
5254                        for _ in 0..n {
5255                            specs_rev.push(self.pop());
5256                        }
5257                        self.push(val);
5258                        for s in specs_rev.into_iter().rev() {
5259                            self.push(s);
5260                        }
5261                        Ok(())
5262                    }
5263                    Op::SetNamedArraySliceLastKeep(arr_idx, n) => {
5264                        let n = *n as usize;
5265                        let line = self.line();
5266                        let idxs = self.pop_flattened_array_slice_specs(n);
5267                        let name = names[*arr_idx as usize].as_str();
5268                        self.require_array_mutable(name)?;
5269                        let mut val = self.pop();
5270                        if let Some(av) = val.as_array_vec() {
5271                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5272                        }
5273                        let last = *idxs.last().ok_or_else(|| {
5274                            StrykeError::runtime(
5275                                "VM: SetNamedArraySliceLastKeep: empty index list",
5276                                line,
5277                            )
5278                        })?;
5279                        let val_keep = val.clone();
5280                        vm_interp_result(
5281                            self.interp
5282                                .scope
5283                                .set_array_element(name, last, val)
5284                                .map(|()| StrykeValue::UNDEF)
5285                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
5286                            line,
5287                        )?;
5288                        self.push(val_keep);
5289                        Ok(())
5290                    }
5291                    Op::SetNamedArraySlice(arr_idx, n) => {
5292                        let n = *n as usize;
5293                        let idxs = self.pop_flattened_array_slice_specs(n);
5294                        let name = names[*arr_idx as usize].as_str();
5295                        self.require_array_mutable(name)?;
5296                        let val = self.pop();
5297                        let line = self.line();
5298                        vm_interp_result(
5299                            self.interp.assign_named_array_slice(name, idxs, val, line),
5300                            line,
5301                        )?;
5302                        Ok(())
5303                    }
5304                    Op::MakeHash(n) => {
5305                        let n = *n as usize;
5306                        let mut items = Vec::with_capacity(n);
5307                        for _ in 0..n {
5308                            items.push(self.pop());
5309                        }
5310                        items.reverse();
5311                        let mut map = IndexMap::new();
5312                        let mut i = 0;
5313                        while i + 1 < items.len() {
5314                            map.insert(items[i].to_string(), items[i + 1].clone());
5315                            i += 2;
5316                        }
5317                        self.push(StrykeValue::hash(map));
5318                        Ok(())
5319                    }
5320                    Op::Range => {
5321                        let to = self.pop();
5322                        let from = self.pop();
5323                        let arr = perl_list_range_expand(from, to);
5324                        self.push(StrykeValue::array(arr));
5325                        Ok(())
5326                    }
5327                    Op::RangeStep => {
5328                        let step = self.pop();
5329                        let to = self.pop();
5330                        let from = self.pop();
5331                        let arr = crate::value::perl_list_range_expand_stepped(from, to, step);
5332                        self.push(StrykeValue::array(arr));
5333                        Ok(())
5334                    }
5335                    Op::ArraySliceRange(arr_idx) => {
5336                        let step = self.pop();
5337                        let to = self.pop();
5338                        let from = self.pop();
5339                        let line = self.line();
5340                        let name = names[*arr_idx as usize].as_str();
5341                        // Stryke topic-string slice: `_[from:to:step]` parses
5342                        // to ArraySliceRange on a `__topicstr__N` name.
5343                        if let Some(real) = name.strip_prefix("__topicstr__") {
5344                            let s = self.interp.scope.get_scalar(real).to_string();
5345                            let chars: Vec<char> = s.chars().collect();
5346                            let n = chars.len() as i64;
5347                            let step_i = if step.is_undef() { 1 } else { step.to_int() };
5348                            // Open slices: defaults depend on step direction
5349                            // step > 0: from=0, to=n-1 (forward)
5350                            // step < 0: from=n-1, to=0 (backward)
5351                            let mut from_i = if from.is_undef() {
5352                                if step_i >= 0 {
5353                                    0
5354                                } else {
5355                                    n - 1
5356                                }
5357                            } else {
5358                                from.to_int()
5359                            };
5360                            let mut to_i = if to.is_undef() {
5361                                if step_i >= 0 {
5362                                    n - 1
5363                                } else {
5364                                    0
5365                                }
5366                            } else {
5367                                to.to_int()
5368                            };
5369                            if from_i < 0 {
5370                                from_i += n
5371                            }
5372                            if to_i < 0 {
5373                                to_i += n
5374                            }
5375                            let mut out = String::new();
5376                            if step_i > 0 {
5377                                let mut i = from_i;
5378                                while i <= to_i && i < n {
5379                                    if i >= 0 {
5380                                        out.push(chars[i as usize]);
5381                                    }
5382                                    i += step_i;
5383                                }
5384                            } else if step_i < 0 {
5385                                let mut i = from_i;
5386                                while i >= to_i && i >= 0 {
5387                                    if i < n {
5388                                        out.push(chars[i as usize]);
5389                                    }
5390                                    i += step_i;
5391                                }
5392                            }
5393                            self.push(StrykeValue::string(out));
5394                            return Ok(());
5395                        }
5396                        let arr_len = self.interp.scope.array_len(name) as i64;
5397                        // Stryke string-slice sugar: when `@name` is empty
5398                        // (or doesn't exist) but `$name` is a non-empty
5399                        // string, treat `$name[from:to:step]` as Python-style
5400                        // substring slice. Returns a *string*, not an array.
5401                        if !crate::compat_mode()
5402                            && arr_len == 0
5403                            && self.interp.scope.scalar_binding_exists(name)
5404                        {
5405                            let s = self.interp.scope.get_scalar(name).to_string();
5406                            if !s.is_empty() {
5407                                let chars: Vec<char> = s.chars().collect();
5408                                let n = chars.len() as i64;
5409                                let step_i = if step.is_undef() { 1 } else { step.to_int() };
5410                                // Open slices: defaults depend on step direction
5411                                // step > 0: from=0, to=n-1 (forward)
5412                                // step < 0: from=n-1, to=0 (backward)
5413                                let mut from_i = if from.is_undef() {
5414                                    if step_i >= 0 {
5415                                        0
5416                                    } else {
5417                                        n - 1
5418                                    }
5419                                } else {
5420                                    from.to_int()
5421                                };
5422                                let mut to_i = if to.is_undef() {
5423                                    if step_i >= 0 {
5424                                        n - 1
5425                                    } else {
5426                                        0
5427                                    }
5428                                } else {
5429                                    to.to_int()
5430                                };
5431                                if from_i < 0 {
5432                                    from_i += n
5433                                }
5434                                if to_i < 0 {
5435                                    to_i += n
5436                                }
5437                                let mut out = String::new();
5438                                if step_i > 0 {
5439                                    let mut i = from_i;
5440                                    while i <= to_i && i < n {
5441                                        if i >= 0 {
5442                                            out.push(chars[i as usize]);
5443                                        }
5444                                        i += step_i;
5445                                    }
5446                                } else if step_i < 0 {
5447                                    let mut i = from_i;
5448                                    while i >= to_i && i >= 0 {
5449                                        if i < n {
5450                                            out.push(chars[i as usize]);
5451                                        }
5452                                        i += step_i;
5453                                    }
5454                                }
5455                                self.push(StrykeValue::string(out));
5456                                return Ok(());
5457                            }
5458                        }
5459                        let indices = match crate::value::compute_array_slice_indices(
5460                            arr_len, &from, &to, &step,
5461                        ) {
5462                            Ok(v) => v,
5463                            Err(msg) => {
5464                                return Err(StrykeError::runtime(msg, line));
5465                            }
5466                        };
5467                        let mut out = Vec::with_capacity(indices.len());
5468                        for i in indices {
5469                            out.push(self.interp.scope.get_array_element(name, i));
5470                        }
5471                        self.push(StrykeValue::array(out));
5472                        Ok(())
5473                    }
5474                    Op::HashSliceRange(hash_idx) => {
5475                        let step = self.pop();
5476                        let to = self.pop();
5477                        let from = self.pop();
5478                        let line = self.line();
5479                        let name = names[*hash_idx as usize].as_str();
5480                        let keys = match crate::value::compute_hash_slice_keys(&from, &to, &step) {
5481                            Ok(v) => v,
5482                            Err(msg) => {
5483                                return Err(StrykeError::runtime(msg, line));
5484                            }
5485                        };
5486                        let h = self.interp.scope.get_hash(name);
5487                        let mut out = Vec::with_capacity(keys.len());
5488                        for k in &keys {
5489                            out.push(h.get(k).cloned().unwrap_or(StrykeValue::UNDEF));
5490                        }
5491                        self.push(StrykeValue::array(out));
5492                        Ok(())
5493                    }
5494                    Op::ScalarFlipFlop(slot, exclusive) => {
5495                        let to = self.pop().to_int();
5496                        let from = self.pop().to_int();
5497                        let line = self.line();
5498                        let v = vm_interp_result(
5499                            self.interp
5500                                .scalar_flip_flop_eval(from, to, *slot as usize, *exclusive != 0)
5501                                .map_err(Into::into),
5502                            line,
5503                        )?;
5504                        self.push(v);
5505                        Ok(())
5506                    }
5507                    Op::RegexFlipFlop(slot, exclusive, lp, lf, rp, rf) => {
5508                        let line = self.line();
5509                        let left_pat = constants[*lp as usize].as_str_or_empty();
5510                        let left_flags = constants[*lf as usize].as_str_or_empty();
5511                        let right_pat = constants[*rp as usize].as_str_or_empty();
5512                        let right_flags = constants[*rf as usize].as_str_or_empty();
5513                        let v = vm_interp_result(
5514                            self.interp
5515                                .regex_flip_flop_eval(
5516                                    left_pat.as_str(),
5517                                    left_flags.as_str(),
5518                                    right_pat.as_str(),
5519                                    right_flags.as_str(),
5520                                    *slot as usize,
5521                                    *exclusive != 0,
5522                                    line,
5523                                )
5524                                .map_err(Into::into),
5525                            line,
5526                        )?;
5527                        self.push(v);
5528                        Ok(())
5529                    }
5530                    Op::RegexEofFlipFlop(slot, exclusive, lp, lf) => {
5531                        let line = self.line();
5532                        let left_pat = constants[*lp as usize].as_str_or_empty();
5533                        let left_flags = constants[*lf as usize].as_str_or_empty();
5534                        let v = vm_interp_result(
5535                            self.interp
5536                                .regex_eof_flip_flop_eval(
5537                                    left_pat.as_str(),
5538                                    left_flags.as_str(),
5539                                    *slot as usize,
5540                                    *exclusive != 0,
5541                                    line,
5542                                )
5543                                .map_err(Into::into),
5544                            line,
5545                        )?;
5546                        self.push(v);
5547                        Ok(())
5548                    }
5549                    Op::RegexFlipFlopExprRhs(slot, exclusive, lp, lf, rhs_idx) => {
5550                        let idx = *rhs_idx as usize;
5551                        let line = self.line();
5552                        let right_m = if let Some(&(start, end)) = self
5553                            .regex_flip_flop_rhs_expr_bytecode_ranges
5554                            .get(idx)
5555                            .and_then(|r| r.as_ref())
5556                        {
5557                            let val = self.run_block_region(start, end, op_count)?;
5558                            val.is_true()
5559                        } else {
5560                            let e = &self.regex_flip_flop_rhs_expr_entries[idx];
5561                            match self.interp.eval_boolean_rvalue_condition(e) {
5562                                Ok(b) => b,
5563                                Err(FlowOrError::Error(err)) => return Err(err),
5564                                Err(FlowOrError::Flow(_)) => {
5565                                    return Err(StrykeError::runtime(
5566                                        "unexpected flow in regex flip-flop RHS",
5567                                        line,
5568                                    ))
5569                                }
5570                            }
5571                        };
5572                        let left_pat = constants[*lp as usize].as_str_or_empty();
5573                        let left_flags = constants[*lf as usize].as_str_or_empty();
5574                        let v = vm_interp_result(
5575                            self.interp
5576                                .regex_flip_flop_eval_dynamic_right(
5577                                    left_pat.as_str(),
5578                                    left_flags.as_str(),
5579                                    *slot as usize,
5580                                    *exclusive != 0,
5581                                    line,
5582                                    right_m,
5583                                )
5584                                .map_err(Into::into),
5585                            line,
5586                        )?;
5587                        self.push(v);
5588                        Ok(())
5589                    }
5590                    Op::RegexFlipFlopDotLineRhs(slot, exclusive, lp, lf, line_cidx) => {
5591                        let line = self.line();
5592                        let rhs_line = constants[*line_cidx as usize].to_int();
5593                        let left_pat = constants[*lp as usize].as_str_or_empty();
5594                        let left_flags = constants[*lf as usize].as_str_or_empty();
5595                        let v = vm_interp_result(
5596                            self.interp
5597                                .regex_flip_flop_eval_dot_line_rhs(
5598                                    left_pat.as_str(),
5599                                    left_flags.as_str(),
5600                                    *slot as usize,
5601                                    *exclusive != 0,
5602                                    line,
5603                                    rhs_line,
5604                                )
5605                                .map_err(Into::into),
5606                            line,
5607                        )?;
5608                        self.push(v);
5609                        Ok(())
5610                    }
5611
5612                    // ── Regex ──
5613                    Op::RegexMatch(pat_idx, flags_idx, scalar_g, pos_key_idx) => {
5614                        let val = self.pop();
5615                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
5616                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5617                        let line = self.line();
5618                        if val.is_iterator() {
5619                            let source = crate::map_stream::into_pull_iter(val);
5620                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
5621                                Ok(r) => r,
5622                                Err(FlowOrError::Error(e)) => return Err(e),
5623                                Err(FlowOrError::Flow(_)) => {
5624                                    return Err(StrykeError::runtime(
5625                                        "unexpected flow in regex compile",
5626                                        line,
5627                                    ));
5628                                }
5629                            };
5630                            let global = flags.contains('g');
5631                            if global {
5632                                self.push(StrykeValue::iterator(std::sync::Arc::new(
5633                                    crate::map_stream::MatchGlobalStreamIterator::new(source, re),
5634                                )));
5635                            } else {
5636                                self.push(StrykeValue::iterator(std::sync::Arc::new(
5637                                    crate::map_stream::MatchStreamIterator::new(source, re),
5638                                )));
5639                            }
5640                            return Ok(());
5641                        }
5642                        let string = val.into_string();
5643                        let pos_key_owned = if *pos_key_idx == u16::MAX {
5644                            None
5645                        } else {
5646                            Some(constants[*pos_key_idx as usize].as_str_or_empty())
5647                        };
5648                        let pos_key: &str = pos_key_owned.as_deref().unwrap_or("_");
5649                        match self
5650                            .interp
5651                            .regex_match_execute(string, &pattern, &flags, *scalar_g, pos_key, line)
5652                        {
5653                            Ok(v) => {
5654                                self.push(v);
5655                                Ok(())
5656                            }
5657                            Err(FlowOrError::Error(e)) => Err(e),
5658                            Err(FlowOrError::Flow(_)) => {
5659                                Err(StrykeError::runtime("unexpected flow in regex match", line))
5660                            }
5661                        }
5662                    }
5663                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lvalue_idx) => {
5664                        let val = self.pop();
5665                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
5666                        let replacement = constants[*repl_idx as usize].as_str_or_empty();
5667                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5668                        let line = self.line();
5669                        if val.is_iterator() {
5670                            let source = crate::map_stream::into_pull_iter(val);
5671                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
5672                                Ok(r) => r,
5673                                Err(FlowOrError::Error(e)) => return Err(e),
5674                                Err(FlowOrError::Flow(_)) => {
5675                                    return Err(StrykeError::runtime(
5676                                        "unexpected flow in regex compile",
5677                                        line,
5678                                    ));
5679                                }
5680                            };
5681                            let global = flags.contains('g');
5682                            self.push(StrykeValue::iterator(std::sync::Arc::new(
5683                                crate::map_stream::SubstStreamIterator::new(
5684                                    source,
5685                                    re,
5686                                    crate::vm_helper::normalize_replacement_backrefs(&replacement),
5687                                    global,
5688                                ),
5689                            )));
5690                            return Ok(());
5691                        }
5692                        let string = val.into_string();
5693                        let target = &self.lvalues[*lvalue_idx as usize];
5694                        match self.interp.regex_subst_execute(
5695                            string,
5696                            &pattern,
5697                            &replacement,
5698                            &flags,
5699                            target,
5700                            line,
5701                        ) {
5702                            Ok(v) => {
5703                                self.push(v);
5704                                Ok(())
5705                            }
5706                            Err(FlowOrError::Error(e)) => Err(e),
5707                            Err(FlowOrError::Flow(_)) => {
5708                                Err(StrykeError::runtime("unexpected flow in s///", line))
5709                            }
5710                        }
5711                    }
5712                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lvalue_idx) => {
5713                        let val = self.pop();
5714                        let from = constants[*from_idx as usize].as_str_or_empty();
5715                        let to = constants[*to_idx as usize].as_str_or_empty();
5716                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5717                        let line = self.line();
5718                        if val.is_iterator() {
5719                            let source = crate::map_stream::into_pull_iter(val);
5720                            self.push(StrykeValue::iterator(std::sync::Arc::new(
5721                                crate::map_stream::TransliterateStreamIterator::new(
5722                                    source, &from, &to, &flags,
5723                                ),
5724                            )));
5725                            return Ok(());
5726                        }
5727                        let string = val.into_string();
5728                        let target = &self.lvalues[*lvalue_idx as usize];
5729                        match self
5730                            .interp
5731                            .regex_transliterate_execute(string, &from, &to, &flags, target, line)
5732                        {
5733                            Ok(v) => {
5734                                self.push(v);
5735                                Ok(())
5736                            }
5737                            Err(FlowOrError::Error(e)) => Err(e),
5738                            Err(FlowOrError::Flow(_)) => {
5739                                Err(StrykeError::runtime("unexpected flow in tr///", line))
5740                            }
5741                        }
5742                    }
5743                    Op::RegexMatchDyn(negate) => {
5744                        let rhs = self.pop();
5745                        let s = self.pop().into_string();
5746                        let line = self.line();
5747                        let exec = if let Some((pat, fl)) = rhs.regex_src_and_flags() {
5748                            self.interp
5749                                .regex_match_execute(s, &pat, &fl, false, "_", line)
5750                        } else {
5751                            let pattern = rhs.into_string();
5752                            self.interp
5753                                .regex_match_execute(s, &pattern, "", false, "_", line)
5754                        };
5755                        match exec {
5756                            Ok(v) => {
5757                                let matched = v.is_true();
5758                                let out = if *negate { !matched } else { matched };
5759                                self.push(StrykeValue::integer(if out { 1 } else { 0 }));
5760                            }
5761                            Err(FlowOrError::Error(e)) => return Err(e),
5762                            Err(FlowOrError::Flow(_)) => {
5763                                return Err(StrykeError::runtime("unexpected flow in =~", line));
5764                            }
5765                        }
5766                        Ok(())
5767                    }
5768                    Op::RegexBoolToScalar => {
5769                        let v = self.pop();
5770                        self.push(if v.is_true() {
5771                            StrykeValue::integer(1)
5772                        } else {
5773                            StrykeValue::string(String::new())
5774                        });
5775                        Ok(())
5776                    }
5777                    Op::SetRegexPos => {
5778                        let key = self.pop().to_string();
5779                        let val = self.pop();
5780                        if val.is_undef() {
5781                            self.interp.regex_pos.insert(key, None);
5782                        } else {
5783                            let u = val.to_int().max(0) as usize;
5784                            self.interp.regex_pos.insert(key, Some(u));
5785                        }
5786                        Ok(())
5787                    }
5788                    Op::LoadRegex(pat_idx, flags_idx) => {
5789                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
5790                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5791                        let line = self.line();
5792                        let pattern_owned = pattern.clone();
5793                        let re = match self.interp.compile_regex(&pattern, &flags, line) {
5794                            Ok(r) => r,
5795                            Err(FlowOrError::Error(e)) => return Err(e),
5796                            Err(FlowOrError::Flow(_)) => {
5797                                return Err(StrykeError::runtime(
5798                                    "unexpected flow in qr// compile",
5799                                    line,
5800                                ));
5801                            }
5802                        };
5803                        self.push(StrykeValue::regex(re, pattern_owned, flags.to_string()));
5804                        Ok(())
5805                    }
5806                    Op::ConcatAppend(idx) => {
5807                        let rhs = self.pop();
5808                        let n = names[*idx as usize].as_str();
5809                        let line = self.line();
5810                        let result = self
5811                            .interp
5812                            .scope
5813                            .scalar_concat_inplace(n, &rhs)
5814                            .map_err(|e| e.at_line(line))?;
5815                        self.push(result);
5816                        Ok(())
5817                    }
5818                    Op::ConcatAppendSlot(slot) => {
5819                        let rhs = self.pop();
5820                        let result = self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
5821                        self.push(result);
5822                        Ok(())
5823                    }
5824                    Op::ConcatAppendSlotVoid(slot) => {
5825                        let rhs = self.pop();
5826                        self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
5827                        Ok(())
5828                    }
5829                    Op::SlotLtIntJumpIfFalse(slot, limit, target) => {
5830                        let val = self.interp.scope.get_scalar_slot(*slot);
5831                        let lt = if let Some(i) = val.as_integer() {
5832                            i < *limit as i64
5833                        } else {
5834                            val.to_number() < *limit as f64
5835                        };
5836                        if !lt {
5837                            self.ip = *target;
5838                        }
5839                        Ok(())
5840                    }
5841                    Op::SlotIncLtIntJumpBack(slot, limit, body_target) => {
5842                        // Fused trailing `++$slot; goto top_test` for the bench_loop shape:
5843                        // matches `PreIncSlotVoid` + `Jump` + top `SlotLtIntJumpIfFalse` exactly so
5844                        // coercion, wrap-around, and integer-only write semantics line up byte-for-byte
5845                        // with the un-fused form. Every iteration past the first skips the top check
5846                        // and the unconditional jump entirely.
5847                        let next_i = self
5848                            .interp
5849                            .scope
5850                            .get_scalar_slot(*slot)
5851                            .to_int()
5852                            .wrapping_add(1);
5853                        self.interp
5854                            .scope
5855                            .set_scalar_slot(*slot, StrykeValue::integer(next_i));
5856                        if next_i < *limit as i64 {
5857                            self.ip = *body_target;
5858                        }
5859                        Ok(())
5860                    }
5861                    Op::AccumSumLoop(sum_slot, i_slot, limit) => {
5862                        // Runs the entire counted `while $i < limit { $sum += $i; $i += 1 }` loop in
5863                        // native Rust. The peephole only fires when the body is exactly this one
5864                        // accumulate statement, so every side effect is captured by the final
5865                        // `$sum` and `$i` writes; there is nothing else to do per iteration.
5866                        let mut sum = self.interp.scope.get_scalar_slot(*sum_slot).to_int();
5867                        let mut i = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5868                        let limit = *limit as i64;
5869                        while i < limit {
5870                            sum = sum.wrapping_add(i);
5871                            i = i.wrapping_add(1);
5872                        }
5873                        self.interp
5874                            .scope
5875                            .set_scalar_slot(*sum_slot, StrykeValue::integer(sum));
5876                        self.interp
5877                            .scope
5878                            .set_scalar_slot(*i_slot, StrykeValue::integer(i));
5879                        Ok(())
5880                    }
5881                    Op::AddHashElemPlainKeyToSlot(sum_slot, k_name_idx, h_name_idx) => {
5882                        // `$sum += $h{$k}` — single-dispatch slot += hash[name-scalar] with no
5883                        // VM stack traffic. The key scalar is read via plain (name-based) access
5884                        // because the compiler's `for my $k (keys %h)` lowering currently backs
5885                        // `$k` with a frame scalar, not a slot.
5886                        let k_name = names[*k_name_idx as usize].as_str();
5887                        let h_name = names[*h_name_idx as usize].as_str();
5888                        self.interp.touch_env_hash(h_name);
5889                        let key = self.interp.scope.get_scalar(k_name).to_string();
5890                        let elem = self.interp.scope.get_hash_element(h_name, &key);
5891                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
5892                        let new_v =
5893                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
5894                                StrykeValue::integer(a.wrapping_add(b))
5895                            } else {
5896                                StrykeValue::float(cur.to_number() + elem.to_number())
5897                            };
5898                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
5899                        Ok(())
5900                    }
5901                    Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_name_idx) => {
5902                        // `$sum += $h{$k}` — slot counter, slot key, slot sum. Zero name lookups
5903                        // for `$sum` and `$k`; one frame-walk for `%h` (same as the non-slot form).
5904                        let h_name = names[*h_name_idx as usize].as_str();
5905                        self.interp.touch_env_hash(h_name);
5906                        let key_val = self.interp.scope.get_scalar_slot(*k_slot);
5907                        let key = key_val.to_string();
5908                        let elem = self.interp.scope.get_hash_element(h_name, &key);
5909                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
5910                        let new_v =
5911                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
5912                                StrykeValue::integer(a.wrapping_add(b))
5913                            } else {
5914                                StrykeValue::float(cur.to_number() + elem.to_number())
5915                            };
5916                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
5917                        Ok(())
5918                    }
5919                    Op::SumHashValuesToSlot(sum_slot, h_name_idx) => {
5920                        // `for my $k (keys %h) { $sum += $h{$k} }` fused to a single op that walks
5921                        // `hash.values()` in a tight native loop. No key stringification, no stack
5922                        // traffic, no per-iter dispatch. The foreach body reduced to
5923                        // `AddHashElemSlotKeyToSlot`, so this fusion is correct regardless of `$k`
5924                        // slot assignment — we never read `$k`.
5925                        let h_name = names[*h_name_idx as usize].as_str();
5926                        self.interp.touch_env_hash(h_name);
5927                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
5928                        let mut int_acc: i64 = cur.as_integer().unwrap_or(0);
5929                        let mut float_acc: f64 = 0.0;
5930                        let mut is_int = cur.as_integer().is_some();
5931                        if !is_int {
5932                            float_acc = cur.to_number();
5933                        }
5934                        // Walk the hash via the scope's borrow path without cloning the whole
5935                        // IndexMap. `for_each_hash_value` takes a visitor so the lock (if any) is
5936                        // held once rather than per-element.
5937                        self.interp.scope.for_each_hash_value(h_name, |v| {
5938                            if is_int {
5939                                if let Some(x) = v.as_integer() {
5940                                    int_acc = int_acc.wrapping_add(x);
5941                                    return;
5942                                }
5943                                float_acc = int_acc as f64;
5944                                is_int = false;
5945                            }
5946                            float_acc += v.to_number();
5947                        });
5948                        let new_v = if is_int {
5949                            StrykeValue::integer(int_acc)
5950                        } else {
5951                            StrykeValue::float(float_acc)
5952                        };
5953                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
5954                        Ok(())
5955                    }
5956                    Op::SetHashIntTimesLoop(h_name_idx, i_slot, k, limit) => {
5957                        // Runs the counted `while $i < limit { $h{$i} = $i * k; $i += 1 }` loop
5958                        // natively: the hash is `reserve()`d once, keys are stringified via
5959                        // `itoa` (no `format!` allocation), and values are inserted in a tight
5960                        // Rust loop. `$i` is left at `limit` on exit, matching the un-fused shape.
5961                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5962                        let lim = *limit as i64;
5963                        if i_cur < lim {
5964                            let n = names[*h_name_idx as usize].as_str();
5965                            self.require_hash_mutable(n)?;
5966                            self.interp.touch_env_hash(n);
5967                            let line = self.line();
5968                            self.interp
5969                                .scope
5970                                .set_hash_int_times_range(n, i_cur, lim, *k as i64)
5971                                .map_err(|e| e.at_line(line))?;
5972                        }
5973                        self.interp
5974                            .scope
5975                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
5976                        Ok(())
5977                    }
5978                    Op::PushIntRangeToArrayLoop(arr_name_idx, i_slot, limit) => {
5979                        // Runs the entire counted `while $i < limit { push @arr, $i; $i += 1 }`
5980                        // loop in native Rust. The array's `Vec<StrykeValue>` is reserved once and
5981                        // `push(StrykeValue::integer(i))` runs in a tight Rust loop — no per-iter
5982                        // op dispatch, no `require_array_mutable` check per iter.
5983                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5984                        let lim = *limit as i64;
5985                        if i_cur < lim {
5986                            let n = names[*arr_name_idx as usize].as_str();
5987                            self.require_array_mutable(n)?;
5988                            let line = self.line();
5989                            self.interp
5990                                .scope
5991                                .push_int_range_to_array(n, i_cur, lim)
5992                                .map_err(|e| e.at_line(line))?;
5993                        }
5994                        self.interp
5995                            .scope
5996                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
5997                        Ok(())
5998                    }
5999                    Op::ConcatConstSlotLoop(const_idx, s_slot, i_slot, limit) => {
6000                        // Runs the entire counted `while $i < limit { $s .= CONST; $i += 1 }` loop
6001                        // in native Rust. We stringify the constant once, reserve `(limit-i_cur) *
6002                        // const.len()` up front so the owning `String` reallocs at most twice, then
6003                        // `push_str` in a tight loop (see `try_concat_repeat_inplace`). Falls back
6004                        // to the per-iteration slow path when the slot is not the sole owner of a
6005                        // heap `String` — `.=` semantics match the un-fused shape byte-for-byte.
6006                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
6007                        let lim = *limit as i64;
6008                        if i_cur < lim {
6009                            let n_iters = (lim - i_cur) as usize;
6010                            let rhs = constants[*const_idx as usize].as_str_or_empty();
6011                            if !self
6012                                .interp
6013                                .scope
6014                                .scalar_slot_concat_repeat_inplace(*s_slot, &rhs, n_iters)
6015                            {
6016                                self.interp
6017                                    .scope
6018                                    .scalar_slot_concat_repeat_slow(*s_slot, &rhs, n_iters);
6019                            }
6020                        }
6021                        self.interp
6022                            .scope
6023                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
6024                        Ok(())
6025                    }
6026                    Op::AddAssignSlotSlot(dst, src) => {
6027                        let a = self.interp.scope.get_scalar_slot(*dst);
6028                        let b = self.interp.scope.get_scalar_slot(*src);
6029                        let result = crate::value::compat_add(&a, &b);
6030                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6031                        self.push(result);
6032                        Ok(())
6033                    }
6034                    Op::AddAssignSlotSlotVoid(dst, src) => {
6035                        let a = self.interp.scope.get_scalar_slot(*dst);
6036                        let b = self.interp.scope.get_scalar_slot(*src);
6037                        let result = crate::value::compat_add(&a, &b);
6038                        self.interp.scope.set_scalar_slot(*dst, result);
6039                        Ok(())
6040                    }
6041                    Op::SubAssignSlotSlot(dst, src) => {
6042                        let a = self.interp.scope.get_scalar_slot(*dst);
6043                        let b = self.interp.scope.get_scalar_slot(*src);
6044                        let result = crate::value::compat_sub(&a, &b);
6045                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6046                        self.push(result);
6047                        Ok(())
6048                    }
6049                    Op::MulAssignSlotSlot(dst, src) => {
6050                        let a = self.interp.scope.get_scalar_slot(*dst);
6051                        let b = self.interp.scope.get_scalar_slot(*src);
6052                        let result = crate::value::compat_mul(&a, &b);
6053                        self.interp.scope.set_scalar_slot(*dst, result.clone());
6054                        self.push(result);
6055                        Ok(())
6056                    }
6057
6058                    // ── Frame-local scalar slots (O(1), no string lookup) ──
6059                    Op::GetScalarSlot(slot) => {
6060                        let val = self.interp.scope.get_scalar_slot(*slot);
6061                        self.push(val);
6062                        Ok(())
6063                    }
6064                    Op::SetScalarSlot(slot) => {
6065                        let val = self.pop();
6066                        self.interp
6067                            .scope
6068                            .set_scalar_slot_checked(*slot, val, None)
6069                            .map_err(|e| e.at_line(self.line()))?;
6070                        Ok(())
6071                    }
6072                    Op::SetScalarSlotKeep(slot) => {
6073                        let val = self.peek().dup_stack();
6074                        self.interp
6075                            .scope
6076                            .set_scalar_slot_checked(*slot, val, None)
6077                            .map_err(|e| e.at_line(self.line()))?;
6078                        Ok(())
6079                    }
6080                    Op::DeclareScalarSlot(slot, name_idx) => {
6081                        let val = self.pop();
6082                        let name_opt = if *name_idx == u16::MAX {
6083                            None
6084                        } else {
6085                            Some(names[*name_idx as usize].as_str())
6086                        };
6087                        self.interp.scope.declare_scalar_slot(*slot, val, name_opt);
6088                        Ok(())
6089                    }
6090                    Op::GetArg(idx) => {
6091                        // Read argument from caller's stack region without @_ allocation.
6092                        let val = if let Some(frame) = self.call_stack.last() {
6093                            let arg_pos = frame.stack_base + *idx as usize;
6094                            self.stack
6095                                .get(arg_pos)
6096                                .cloned()
6097                                .unwrap_or(StrykeValue::UNDEF)
6098                        } else {
6099                            StrykeValue::UNDEF
6100                        };
6101                        self.push(val);
6102                        Ok(())
6103                    }
6104
6105                    Op::ReadIntoVar(name_idx) => {
6106                        let length = self.pop().to_int() as usize;
6107                        let fh_val = self.pop();
6108                        let name = &names[*name_idx as usize];
6109                        let line = self.line();
6110                        let result = vm_interp_result(
6111                            self.interp.builtin_read_into(fh_val, name, length, line),
6112                            line,
6113                        )?;
6114                        self.push(result);
6115                        Ok(())
6116                    }
6117                    Op::ChompInPlace(lvalue_idx) => {
6118                        let val = self.pop();
6119                        let target = &self.lvalues[*lvalue_idx as usize];
6120                        let line = self.line();
6121                        match self.interp.chomp_inplace_execute(val, target) {
6122                            Ok(v) => self.push(v),
6123                            Err(FlowOrError::Error(e)) => return Err(e),
6124                            Err(FlowOrError::Flow(_)) => {
6125                                return Err(StrykeError::runtime("unexpected flow in chomp", line));
6126                            }
6127                        }
6128                        Ok(())
6129                    }
6130                    Op::ChopInPlace(lvalue_idx) => {
6131                        let val = self.pop();
6132                        let target = &self.lvalues[*lvalue_idx as usize];
6133                        let line = self.line();
6134                        match self.interp.chop_inplace_execute(val, target) {
6135                            Ok(v) => self.push(v),
6136                            Err(FlowOrError::Error(e)) => return Err(e),
6137                            Err(FlowOrError::Flow(_)) => {
6138                                return Err(StrykeError::runtime("unexpected flow in chop", line));
6139                            }
6140                        }
6141                        Ok(())
6142                    }
6143                    Op::SubstrFourArg(idx) => {
6144                        let (string_e, offset_e, length_e, rep_e) =
6145                            &self.substr_four_arg_entries[*idx as usize];
6146                        let v = vm_interp_result(
6147                            self.interp.eval_substr_expr(
6148                                string_e,
6149                                offset_e,
6150                                length_e.as_ref(),
6151                                Some(rep_e),
6152                                self.line(),
6153                            ),
6154                            self.line(),
6155                        )?;
6156                        self.push(v);
6157                        Ok(())
6158                    }
6159                    Op::KeysExpr(idx) => {
6160                        let i = *idx as usize;
6161                        let line = self.line();
6162                        let v = if let Some(&(start, end)) = self
6163                            .keys_expr_bytecode_ranges
6164                            .get(i)
6165                            .and_then(|r| r.as_ref())
6166                        {
6167                            let val = self.run_block_region(start, end, op_count)?;
6168                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
6169                        } else {
6170                            let e = &self.keys_expr_entries[i];
6171                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
6172                        };
6173                        self.push(v);
6174                        Ok(())
6175                    }
6176                    Op::KeysExprScalar(idx) => {
6177                        let i = *idx as usize;
6178                        let line = self.line();
6179                        let v = if let Some(&(start, end)) = self
6180                            .keys_expr_bytecode_ranges
6181                            .get(i)
6182                            .and_then(|r| r.as_ref())
6183                        {
6184                            let val = self.run_block_region(start, end, op_count)?;
6185                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
6186                        } else {
6187                            let e = &self.keys_expr_entries[i];
6188                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
6189                        };
6190                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
6191                        self.push(StrykeValue::integer(n));
6192                        Ok(())
6193                    }
6194                    Op::ValuesExpr(idx) => {
6195                        let i = *idx as usize;
6196                        let line = self.line();
6197                        let v = if let Some(&(start, end)) = self
6198                            .values_expr_bytecode_ranges
6199                            .get(i)
6200                            .and_then(|r| r.as_ref())
6201                        {
6202                            let val = self.run_block_region(start, end, op_count)?;
6203                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
6204                        } else {
6205                            let e = &self.values_expr_entries[i];
6206                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
6207                        };
6208                        self.push(v);
6209                        Ok(())
6210                    }
6211                    Op::ValuesExprScalar(idx) => {
6212                        let i = *idx as usize;
6213                        let line = self.line();
6214                        let v = if let Some(&(start, end)) = self
6215                            .values_expr_bytecode_ranges
6216                            .get(i)
6217                            .and_then(|r| r.as_ref())
6218                        {
6219                            let val = self.run_block_region(start, end, op_count)?;
6220                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
6221                        } else {
6222                            let e = &self.values_expr_entries[i];
6223                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
6224                        };
6225                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
6226                        self.push(StrykeValue::integer(n));
6227                        Ok(())
6228                    }
6229                    Op::DeleteExpr(idx) => {
6230                        let e = &self.delete_expr_entries[*idx as usize];
6231                        let v = vm_interp_result(
6232                            self.interp.eval_delete_operand(e, self.line()),
6233                            self.line(),
6234                        )?;
6235                        self.push(v);
6236                        Ok(())
6237                    }
6238                    Op::ExistsExpr(idx) => {
6239                        let e = &self.exists_expr_entries[*idx as usize];
6240                        let v = vm_interp_result(
6241                            self.interp.eval_exists_operand(e, self.line()),
6242                            self.line(),
6243                        )?;
6244                        self.push(v);
6245                        Ok(())
6246                    }
6247                    Op::PushExpr(idx) => {
6248                        let (array, values) = &self.push_expr_entries[*idx as usize];
6249                        let v = vm_interp_result(
6250                            self.interp
6251                                .eval_push_expr(array, values.as_slice(), self.line()),
6252                            self.line(),
6253                        )?;
6254                        self.push(v);
6255                        Ok(())
6256                    }
6257                    Op::PopExpr(idx) => {
6258                        let e = &self.pop_expr_entries[*idx as usize];
6259                        let v = vm_interp_result(
6260                            self.interp.eval_pop_expr(e, self.line()),
6261                            self.line(),
6262                        )?;
6263                        self.push(v);
6264                        Ok(())
6265                    }
6266                    Op::ShiftExpr(idx) => {
6267                        let e = &self.shift_expr_entries[*idx as usize];
6268                        let v = vm_interp_result(
6269                            self.interp.eval_shift_expr(e, self.line()),
6270                            self.line(),
6271                        )?;
6272                        self.push(v);
6273                        Ok(())
6274                    }
6275                    Op::UnshiftExpr(idx) => {
6276                        let (array, values) = &self.unshift_expr_entries[*idx as usize];
6277                        let v = vm_interp_result(
6278                            self.interp
6279                                .eval_unshift_expr(array, values.as_slice(), self.line()),
6280                            self.line(),
6281                        )?;
6282                        self.push(v);
6283                        Ok(())
6284                    }
6285                    Op::SpliceExpr(idx) => {
6286                        let (array, offset, length, replacement) =
6287                            &self.splice_expr_entries[*idx as usize];
6288                        let v = vm_interp_result(
6289                            self.interp.eval_splice_expr(
6290                                array,
6291                                offset.as_ref(),
6292                                length.as_ref(),
6293                                replacement.as_slice(),
6294                                self.interp.wantarray_kind,
6295                                self.line(),
6296                            ),
6297                            self.line(),
6298                        )?;
6299                        self.push(v);
6300                        Ok(())
6301                    }
6302
6303                    // ── References ──
6304                    Op::MakeScalarRef => {
6305                        let val = self.pop();
6306                        self.push(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))));
6307                        Ok(())
6308                    }
6309                    Op::MakeScalarBindingRef(name_idx) => {
6310                        let name = names[*name_idx as usize].clone();
6311                        self.push(StrykeValue::scalar_binding_ref(name));
6312                        Ok(())
6313                    }
6314                    Op::MakeArrayBindingRef(name_idx) => {
6315                        let name = &names[*name_idx as usize];
6316                        // Promote the scope's array to shared Arc-backed storage.
6317                        // Both the scope and the returned ref share the same Arc,
6318                        // so mutations through either path are visible.
6319                        let arc = self.interp.scope.promote_array_to_shared(name);
6320                        self.push(StrykeValue::array_ref(arc));
6321                        Ok(())
6322                    }
6323                    Op::MakeHashBindingRef(name_idx) => {
6324                        let name = &names[*name_idx as usize];
6325                        // Lazy-init hook: `\%all` / `\%parameters` / `\%main::`
6326                        // bypass `Op::GetHash`, so without this call the
6327                        // reference is taken before the hash is populated and
6328                        // the user gets an empty hashref.
6329                        self.interp.touch_env_hash(name);
6330                        let arc = self.interp.scope.promote_hash_to_shared(name);
6331                        self.push(StrykeValue::hash_ref(arc));
6332                        Ok(())
6333                    }
6334                    Op::MakeArrayRefAlias => {
6335                        let v = self.pop();
6336                        let line = self.line();
6337                        let out =
6338                            vm_interp_result(self.interp.make_array_ref_alias(v, line), line)?;
6339                        self.push(out);
6340                        Ok(())
6341                    }
6342                    Op::MakeHashRefAlias => {
6343                        let v = self.pop();
6344                        let line = self.line();
6345                        let out = vm_interp_result(self.interp.make_hash_ref_alias(v, line), line)?;
6346                        self.push(out);
6347                        Ok(())
6348                    }
6349                    Op::MakeArrayRef => {
6350                        let val = self.pop();
6351                        let val = self.interp.scope.resolve_container_binding_ref(val);
6352                        let arr = if let Some(a) = val.as_array_vec() {
6353                            a
6354                        } else {
6355                            vec![val]
6356                        };
6357                        self.push(StrykeValue::array_ref(Arc::new(RwLock::new(arr))));
6358                        Ok(())
6359                    }
6360                    Op::MakeHashRef => {
6361                        let val = self.pop();
6362                        let map = if let Some(h) = val.as_hash_map() {
6363                            h
6364                        } else {
6365                            let items = val.to_list();
6366                            let mut m = IndexMap::new();
6367                            let mut i = 0;
6368                            while i + 1 < items.len() {
6369                                m.insert(items[i].to_string(), items[i + 1].clone());
6370                                i += 2;
6371                            }
6372                            m
6373                        };
6374                        self.push(StrykeValue::hash_ref(Arc::new(RwLock::new(map))));
6375                        Ok(())
6376                    }
6377                    Op::MakeCodeRef(block_idx, sig_idx) => {
6378                        let block = self.blocks[*block_idx as usize].clone();
6379                        let params = self.code_ref_sigs[*sig_idx as usize].clone();
6380                        let captured = self.interp.scope.capture();
6381                        self.push(StrykeValue::code_ref(Arc::new(crate::value::StrykeSub {
6382                            name: "__ANON__".to_string(),
6383                            params,
6384                            body: block,
6385                            closure_env: Some(captured),
6386                            prototype: None,
6387                            fib_like: None,
6388                        })));
6389                        Ok(())
6390                    }
6391                    Op::LoadNamedSubRef(name_idx) => {
6392                        let name = names[*name_idx as usize].as_str();
6393                        let line = self.line();
6394                        let sub = self.interp.resolve_sub_by_name(name).ok_or_else(|| {
6395                            StrykeError::runtime(
6396                                self.interp.undefined_subroutine_resolve_message(name),
6397                                line,
6398                            )
6399                        })?;
6400                        self.push(StrykeValue::code_ref(sub));
6401                        Ok(())
6402                    }
6403                    Op::LoadDynamicSubRef => {
6404                        let name = self.pop().to_string();
6405                        let line = self.line();
6406                        let sub = self.interp.resolve_sub_by_name(&name).ok_or_else(|| {
6407                            StrykeError::runtime(
6408                                self.interp.undefined_subroutine_resolve_message(&name),
6409                                line,
6410                            )
6411                        })?;
6412                        self.push(StrykeValue::code_ref(sub));
6413                        Ok(())
6414                    }
6415                    Op::LoadDynamicTypeglob => {
6416                        let name = self.pop().to_string();
6417                        let n = self.interp.resolve_io_handle_name(&name);
6418                        self.push(StrykeValue::string(n));
6419                        Ok(())
6420                    }
6421                    Op::CopyTypeglobSlots(lhs_i, rhs_i) => {
6422                        let lhs = self.names[*lhs_i as usize].as_str();
6423                        let rhs = self.names[*rhs_i as usize].as_str();
6424                        let line = self.line();
6425                        self.interp
6426                            .copy_typeglob_slots(lhs, rhs, line)
6427                            .map_err(|e| e.at_line(line))?;
6428                        Ok(())
6429                    }
6430                    Op::TypeglobAssignFromValue(name_idx) => {
6431                        let val = self.pop();
6432                        let name = self.names[*name_idx as usize].as_str();
6433                        let line = self.line();
6434                        vm_interp_result(
6435                            self.interp.assign_typeglob_value(name, val.clone(), line),
6436                            line,
6437                        )?;
6438                        self.push(val);
6439                        Ok(())
6440                    }
6441                    Op::TypeglobAssignFromValueDynamic => {
6442                        let val = self.pop();
6443                        let name = self.pop().to_string();
6444                        let line = self.line();
6445                        vm_interp_result(
6446                            self.interp.assign_typeglob_value(&name, val.clone(), line),
6447                            line,
6448                        )?;
6449                        self.push(val);
6450                        Ok(())
6451                    }
6452                    Op::CopyTypeglobSlotsDynamicLhs(rhs_i) => {
6453                        let lhs = self.pop().to_string();
6454                        let rhs = self.names[*rhs_i as usize].as_str();
6455                        let line = self.line();
6456                        self.interp
6457                            .copy_typeglob_slots(&lhs, rhs, line)
6458                            .map_err(|e| e.at_line(line))?;
6459                        Ok(())
6460                    }
6461                    Op::SymbolicDeref(kind_byte) => {
6462                        let v = self.pop();
6463                        let kind = match *kind_byte {
6464                            0 => Sigil::Scalar,
6465                            1 => Sigil::Array,
6466                            2 => Sigil::Hash,
6467                            3 => Sigil::Typeglob,
6468                            _ => {
6469                                return Err(StrykeError::runtime(
6470                                    "VM: bad SymbolicDeref kind byte",
6471                                    self.line(),
6472                                ));
6473                            }
6474                        };
6475                        let line = self.line();
6476                        let out =
6477                            vm_interp_result(self.interp.symbolic_deref(v, kind, line), line)?;
6478                        self.push(out);
6479                        Ok(())
6480                    }
6481
6482                    // ── Arrow dereference ──
6483                    Op::ArrowArray => {
6484                        let idx = self.pop().to_int();
6485                        let r = self.pop();
6486                        let line = self.line();
6487                        let v = vm_interp_result(
6488                            self.interp.read_arrow_array_element(r, idx, line),
6489                            line,
6490                        )?;
6491                        self.push(v);
6492                        Ok(())
6493                    }
6494                    Op::ArrowHash => {
6495                        let key = self.pop().to_string();
6496                        let r = self.pop();
6497                        let line = self.line();
6498                        let v = vm_interp_result(
6499                            self.interp.read_arrow_hash_element(r, key.as_str(), line),
6500                            line,
6501                        )?;
6502                        self.push(v);
6503                        Ok(())
6504                    }
6505                    Op::SetArrowHash => {
6506                        let key = self.pop().to_string();
6507                        let r = self.pop();
6508                        let val = self.pop();
6509                        let line = self.line();
6510                        vm_interp_result(
6511                            self.interp.assign_arrow_hash_deref(r, key, val, line),
6512                            line,
6513                        )?;
6514                        Ok(())
6515                    }
6516                    Op::SetArrowArray => {
6517                        let idx = self.pop().to_int();
6518                        let r = self.pop();
6519                        let val = self.pop();
6520                        let line = self.line();
6521                        vm_interp_result(
6522                            self.interp.assign_arrow_array_deref(r, idx, val, line),
6523                            line,
6524                        )?;
6525                        Ok(())
6526                    }
6527                    Op::SetArrowArrayKeep => {
6528                        let idx = self.pop().to_int();
6529                        let r = self.pop();
6530                        let val = self.pop();
6531                        let val_keep = val.clone();
6532                        let line = self.line();
6533                        vm_interp_result(
6534                            self.interp.assign_arrow_array_deref(r, idx, val, line),
6535                            line,
6536                        )?;
6537                        self.push(val_keep);
6538                        Ok(())
6539                    }
6540                    Op::SetArrowHashKeep => {
6541                        let key = self.pop().to_string();
6542                        let r = self.pop();
6543                        let val = self.pop();
6544                        let val_keep = val.clone();
6545                        let line = self.line();
6546                        vm_interp_result(
6547                            self.interp.assign_arrow_hash_deref(r, key, val, line),
6548                            line,
6549                        )?;
6550                        self.push(val_keep);
6551                        Ok(())
6552                    }
6553                    Op::ArrowArrayPostfix(b) => {
6554                        let idx = self.pop().to_int();
6555                        let r = self.pop();
6556                        let line = self.line();
6557                        let old = vm_interp_result(
6558                            self.interp.arrow_array_postfix(r, idx, *b == 1, line),
6559                            line,
6560                        )?;
6561                        self.push(old);
6562                        Ok(())
6563                    }
6564                    Op::ArrowHashPostfix(b) => {
6565                        let key = self.pop().to_string();
6566                        let r = self.pop();
6567                        let line = self.line();
6568                        let old = vm_interp_result(
6569                            self.interp.arrow_hash_postfix(r, key, *b == 1, line),
6570                            line,
6571                        )?;
6572                        self.push(old);
6573                        Ok(())
6574                    }
6575                    Op::SetSymbolicScalarRef => {
6576                        let r = self.pop();
6577                        let val = self.pop();
6578                        let line = self.line();
6579                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
6580                        Ok(())
6581                    }
6582                    Op::SetSymbolicScalarRefKeep => {
6583                        let r = self.pop();
6584                        let val = self.pop();
6585                        let val_keep = val.clone();
6586                        let line = self.line();
6587                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
6588                        self.push(val_keep);
6589                        Ok(())
6590                    }
6591                    Op::SetSymbolicArrayRef => {
6592                        let r = self.pop();
6593                        let val = self.pop();
6594                        let line = self.line();
6595                        vm_interp_result(
6596                            self.interp.assign_symbolic_array_ref_deref(r, val, line),
6597                            line,
6598                        )?;
6599                        Ok(())
6600                    }
6601                    Op::SetSymbolicHashRef => {
6602                        let r = self.pop();
6603                        let val = self.pop();
6604                        let line = self.line();
6605                        vm_interp_result(
6606                            self.interp.assign_symbolic_hash_ref_deref(r, val, line),
6607                            line,
6608                        )?;
6609                        Ok(())
6610                    }
6611                    Op::SetSymbolicTypeglobRef => {
6612                        let r = self.pop();
6613                        let val = self.pop();
6614                        let line = self.line();
6615                        vm_interp_result(
6616                            self.interp.assign_symbolic_typeglob_ref_deref(r, val, line),
6617                            line,
6618                        )?;
6619                        Ok(())
6620                    }
6621                    Op::SymbolicScalarRefPostfix(b) => {
6622                        let r = self.pop();
6623                        let line = self.line();
6624                        let old = vm_interp_result(
6625                            self.interp.symbolic_scalar_ref_postfix(r, *b == 1, line),
6626                            line,
6627                        )?;
6628                        self.push(old);
6629                        Ok(())
6630                    }
6631                    Op::ArrowCall(wa) => {
6632                        let want = WantarrayCtx::from_byte(*wa);
6633                        let args_val = self.pop();
6634                        let r = self.pop();
6635                        // Auto-deref ScalarRef so closures that captured $f can call $f->()
6636                        let r = if let Some(inner) = r.as_scalar_ref() {
6637                            inner.read().clone()
6638                        } else {
6639                            r
6640                        };
6641                        let args = args_val.to_list();
6642                        if let Some(sub) = r.as_code_ref() {
6643                            // Higher-order function wrappers (comp, partial, memoize, etc.)
6644                            // have empty bodies + magic closure_env keys. Dispatch them via
6645                            // the interpreter's try_hof_dispatch before falling through to
6646                            // the normal body execution path.
6647                            if let Some(hof_result) =
6648                                self.interp.try_hof_dispatch(&sub, &args, want, self.line())
6649                            {
6650                                let v = vm_interp_result(hof_result, self.line())?;
6651                                self.push(v);
6652                                return Ok(());
6653                            }
6654                            self.interp.current_sub_stack.push(sub.clone());
6655                            let saved_wa = self.interp.wantarray_kind;
6656                            self.interp.wantarray_kind = want;
6657                            self.interp.scope_push_hook();
6658                            self.interp.scope.declare_array("_", args.clone());
6659                            if let Some(ref env) = sub.closure_env {
6660                                self.interp.scope.restore_capture(env);
6661                            }
6662                            let line = self.line();
6663                            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
6664                            self.interp
6665                                .apply_sub_signature(sub.as_ref(), &argv, line)
6666                                .map_err(|e| e.at_line(line))?;
6667                            self.interp.scope.declare_array("_", argv.clone());
6668                            // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
6669                            self.interp.scope.set_closure_args(&argv);
6670                            let result = self.interp.exec_block_no_scope(&sub.body);
6671                            self.interp.wantarray_kind = saved_wa;
6672                            self.interp.scope_pop_hook();
6673                            self.interp.current_sub_stack.pop();
6674                            match result {
6675                                Ok(v) => self.push(v),
6676                                Err(crate::vm_helper::FlowOrError::Flow(
6677                                    crate::vm_helper::Flow::Return(v),
6678                                )) => self.push(v),
6679                                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
6680                                Err(_) => self.push(StrykeValue::UNDEF),
6681                            }
6682                        } else {
6683                            return Err(StrykeError::runtime("Not a code reference", self.line()));
6684                        }
6685                        Ok(())
6686                    }
6687                    Op::IndirectCall(argc, wa, pass_flag) => {
6688                        let want = WantarrayCtx::from_byte(*wa);
6689                        let line = self.line();
6690                        let arg_vals = if *pass_flag != 0 {
6691                            self.interp.scope.get_array("_")
6692                        } else {
6693                            let n = *argc as usize;
6694                            let mut args = Vec::with_capacity(n);
6695                            for _ in 0..n {
6696                                args.push(self.pop());
6697                            }
6698                            args.reverse();
6699                            args
6700                        };
6701                        let target = self.pop();
6702                        // HOF wrapper fast path (comp, partial, memoize, etc.)
6703                        if let Some(sub) = target.as_code_ref() {
6704                            if let Some(hof_result) =
6705                                self.interp.try_hof_dispatch(&sub, &arg_vals, want, line)
6706                            {
6707                                let v = vm_interp_result(hof_result, line)?;
6708                                self.push(v);
6709                                return Ok(());
6710                            }
6711                        }
6712                        let r = self
6713                            .interp
6714                            .dispatch_indirect_call(target, arg_vals, want, line);
6715                        let v = vm_interp_result(r, line)?;
6716                        self.push(v);
6717                        Ok(())
6718                    }
6719
6720                    // ── Method call ──
6721                    Op::MethodCall(name_idx, argc, wa) => {
6722                        self.run_method_op(*name_idx, *argc, *wa, false)?;
6723                        Ok(())
6724                    }
6725                    Op::MethodCallSuper(name_idx, argc, wa) => {
6726                        self.run_method_op(*name_idx, *argc, *wa, true)?;
6727                        Ok(())
6728                    }
6729
6730                    // ── File test ──
6731                    Op::FileTestOp(test) => {
6732                        let path = self.pop().to_string();
6733                        let op = *test as char;
6734                        // -M, -A, -C return fractional days (float)
6735                        if matches!(op, 'M' | 'A' | 'C') {
6736                            #[cfg(unix)]
6737                            {
6738                                let v = match crate::perl_fs::filetest_age_days(&path, op) {
6739                                    Some(days) => StrykeValue::float(days),
6740                                    None => StrykeValue::UNDEF,
6741                                };
6742                                self.push(v);
6743                                return Ok(());
6744                            }
6745                            #[cfg(not(unix))]
6746                            {
6747                                self.push(StrykeValue::UNDEF);
6748                                return Ok(());
6749                            }
6750                        }
6751                        // -s returns file size (integer)
6752                        if op == 's' {
6753                            let v = match std::fs::metadata(&path) {
6754                                Ok(m) => StrykeValue::integer(m.len() as i64),
6755                                Err(_) => StrykeValue::UNDEF,
6756                            };
6757                            self.push(v);
6758                            return Ok(());
6759                        }
6760                        let result = match op {
6761                            'e' => std::path::Path::new(&path).exists(),
6762                            'f' => std::path::Path::new(&path).is_file(),
6763                            'd' => std::path::Path::new(&path).is_dir(),
6764                            'l' => std::path::Path::new(&path).is_symlink(),
6765                            #[cfg(unix)]
6766                            'r' => crate::perl_fs::filetest_effective_access(&path, 4),
6767                            #[cfg(not(unix))]
6768                            'r' => std::fs::metadata(&path).is_ok(),
6769                            #[cfg(unix)]
6770                            'w' => crate::perl_fs::filetest_effective_access(&path, 2),
6771                            #[cfg(not(unix))]
6772                            'w' => std::fs::metadata(&path).is_ok(),
6773                            #[cfg(unix)]
6774                            'x' => crate::perl_fs::filetest_effective_access(&path, 1),
6775                            #[cfg(not(unix))]
6776                            'x' => false,
6777                            #[cfg(unix)]
6778                            'o' => crate::perl_fs::filetest_owned_effective(&path),
6779                            #[cfg(not(unix))]
6780                            'o' => false,
6781                            #[cfg(unix)]
6782                            'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
6783                            #[cfg(not(unix))]
6784                            'R' => false,
6785                            #[cfg(unix)]
6786                            'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
6787                            #[cfg(not(unix))]
6788                            'W' => false,
6789                            #[cfg(unix)]
6790                            'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
6791                            #[cfg(not(unix))]
6792                            'X' => false,
6793                            #[cfg(unix)]
6794                            'O' => crate::perl_fs::filetest_owned_real(&path),
6795                            #[cfg(not(unix))]
6796                            'O' => false,
6797                            'z' => std::fs::metadata(&path)
6798                                .map(|m| m.len() == 0)
6799                                .unwrap_or(true),
6800                            't' => crate::perl_fs::filetest_is_tty(&path),
6801                            #[cfg(unix)]
6802                            'p' => crate::perl_fs::filetest_is_pipe(&path),
6803                            #[cfg(not(unix))]
6804                            'p' => false,
6805                            #[cfg(unix)]
6806                            'S' => crate::perl_fs::filetest_is_socket(&path),
6807                            #[cfg(not(unix))]
6808                            'S' => false,
6809                            #[cfg(unix)]
6810                            'b' => crate::perl_fs::filetest_is_block_device(&path),
6811                            #[cfg(not(unix))]
6812                            'b' => false,
6813                            #[cfg(unix)]
6814                            'c' => crate::perl_fs::filetest_is_char_device(&path),
6815                            #[cfg(not(unix))]
6816                            'c' => false,
6817                            #[cfg(unix)]
6818                            'u' => crate::perl_fs::filetest_is_setuid(&path),
6819                            #[cfg(not(unix))]
6820                            'u' => false,
6821                            #[cfg(unix)]
6822                            'g' => crate::perl_fs::filetest_is_setgid(&path),
6823                            #[cfg(not(unix))]
6824                            'g' => false,
6825                            #[cfg(unix)]
6826                            'k' => crate::perl_fs::filetest_is_sticky(&path),
6827                            #[cfg(not(unix))]
6828                            'k' => false,
6829                            'T' => crate::perl_fs::filetest_is_text(&path),
6830                            'B' => crate::perl_fs::filetest_is_binary(&path),
6831                            _ => false,
6832                        };
6833                        self.push(StrykeValue::integer(if result { 1 } else { 0 }));
6834                        Ok(())
6835                    }
6836
6837                    // ── Map/Grep/Sort with blocks (opcodes when lowered; else AST block fallback) ──
6838                    Op::MapIntMul(k) => {
6839                        let list = self.pop().to_list();
6840                        if list.len() == 1 {
6841                            if let Some(p) = list[0].as_pipeline() {
6842                                let line = self.line();
6843                                let sub = VMHelper::pipeline_int_mul_sub(*k);
6844                                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
6845                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
6846                                return Ok(());
6847                            }
6848                        }
6849                        let mut result = Vec::with_capacity(list.len());
6850                        for item in list {
6851                            let n = item.to_int();
6852                            result.push(StrykeValue::integer(n.wrapping_mul(*k)));
6853                        }
6854                        self.push(StrykeValue::array(result));
6855                        Ok(())
6856                    }
6857                    Op::GrepIntModEq(m, r) => {
6858                        let list = self.pop().to_list();
6859                        let mut result = Vec::new();
6860                        for item in list {
6861                            let n = item.to_int();
6862                            if n % m == *r {
6863                                result.push(item);
6864                            }
6865                        }
6866                        self.push(StrykeValue::array(result));
6867                        Ok(())
6868                    }
6869                    Op::MapWithBlock(block_idx) => {
6870                        let list = self.pop().to_list();
6871                        self.map_with_block_common(list, *block_idx, false, op_count)
6872                    }
6873                    Op::FlatMapWithBlock(block_idx) => {
6874                        let list = self.pop().to_list();
6875                        self.map_with_block_common(list, *block_idx, true, op_count)
6876                    }
6877                    Op::MapWithExpr(expr_idx) => {
6878                        let list = self.pop().to_list();
6879                        self.map_with_expr_common(list, *expr_idx, false, op_count)
6880                    }
6881                    Op::FlatMapWithExpr(expr_idx) => {
6882                        let list = self.pop().to_list();
6883                        self.map_with_expr_common(list, *expr_idx, true, op_count)
6884                    }
6885                    Op::MapsWithBlock(block_idx) => {
6886                        let val = self.pop();
6887                        let block = self.blocks[*block_idx as usize].clone();
6888                        let out =
6889                            self.interp
6890                                .map_stream_block_output(val, &block, false, self.line())?;
6891                        self.push(out);
6892                        Ok(())
6893                    }
6894                    Op::MapsFlatMapWithBlock(block_idx) => {
6895                        let val = self.pop();
6896                        let block = self.blocks[*block_idx as usize].clone();
6897                        let out =
6898                            self.interp
6899                                .map_stream_block_output(val, &block, true, self.line())?;
6900                        self.push(out);
6901                        Ok(())
6902                    }
6903                    Op::MapsWithExpr(expr_idx) => {
6904                        let val = self.pop();
6905                        let idx = *expr_idx as usize;
6906                        let expr = self.map_expr_entries[idx].clone();
6907                        let out =
6908                            self.interp
6909                                .map_stream_expr_output(val, &expr, false, self.line())?;
6910                        self.push(out);
6911                        Ok(())
6912                    }
6913                    Op::MapsFlatMapWithExpr(expr_idx) => {
6914                        let val = self.pop();
6915                        let idx = *expr_idx as usize;
6916                        let expr = self.map_expr_entries[idx].clone();
6917                        let out =
6918                            self.interp
6919                                .map_stream_expr_output(val, &expr, true, self.line())?;
6920                        self.push(out);
6921                        Ok(())
6922                    }
6923                    Op::FilterWithBlock(block_idx) => {
6924                        let val = self.pop();
6925                        let block = self.blocks[*block_idx as usize].clone();
6926                        let out =
6927                            self.interp
6928                                .filter_stream_block_output(val, &block, self.line())?;
6929                        self.push(out);
6930                        Ok(())
6931                    }
6932                    Op::FilterWithExpr(expr_idx) => {
6933                        let val = self.pop();
6934                        let idx = *expr_idx as usize;
6935                        let expr = self.grep_expr_entries[idx].clone();
6936                        let out = self
6937                            .interp
6938                            .filter_stream_expr_output(val, &expr, self.line())?;
6939                        self.push(out);
6940                        Ok(())
6941                    }
6942                    Op::ChunkByWithBlock(block_idx) => {
6943                        let list = self.pop().to_list();
6944                        self.chunk_by_with_block_common(list, *block_idx, op_count)
6945                    }
6946                    Op::ChunkByWithExpr(expr_idx) => {
6947                        let list = self.pop().to_list();
6948                        self.chunk_by_with_expr_common(list, *expr_idx, op_count)
6949                    }
6950                    Op::GrepWithBlock(block_idx) => {
6951                        let list = self.pop().to_list();
6952                        if list.len() == 1 {
6953                            if let Some(p) = list[0].as_pipeline() {
6954                                let idx = *block_idx as usize;
6955                                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
6956                                let line = self.line();
6957                                self.interp
6958                                    .pipeline_push(&p, PipelineOp::Filter(sub), line)?;
6959                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
6960                                return Ok(());
6961                            }
6962                        }
6963                        let idx = *block_idx as usize;
6964                        if let Some(&(start, end)) =
6965                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
6966                        {
6967                            let mut result = Vec::new();
6968                            for item in list {
6969                                self.interp.scope.set_topic(item.clone());
6970                                let val = self.run_block_region(start, end, op_count)?;
6971                                // Bare regex → match against $_ (Perl: /pat/ in grep is $_ =~ /pat/)
6972                                let keep = if let Some(re) = val.as_regex() {
6973                                    re.is_match(&item.to_string())
6974                                } else {
6975                                    val.is_true()
6976                                };
6977                                if keep {
6978                                    result.push(item);
6979                                }
6980                            }
6981                            self.push(StrykeValue::array(result));
6982                            Ok(())
6983                        } else {
6984                            let block = self.blocks[idx].clone();
6985                            let mut result = Vec::new();
6986                            for item in list {
6987                                self.interp.scope.set_topic(item.clone());
6988                                match self.interp.exec_block(&block) {
6989                                    Ok(val) => {
6990                                        let keep = if let Some(re) = val.as_regex() {
6991                                            re.is_match(&item.to_string())
6992                                        } else {
6993                                            val.is_true()
6994                                        };
6995                                        if keep {
6996                                            result.push(item);
6997                                        }
6998                                    }
6999                                    Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
7000                                    Err(_) => {}
7001                                }
7002                            }
7003                            self.push(StrykeValue::array(result));
7004                            Ok(())
7005                        }
7006                    }
7007                    Op::ForEachWithBlock(block_idx) => {
7008                        let val = self.pop();
7009                        let idx = *block_idx as usize;
7010                        // Lazy iterator: consume one-at-a-time without materializing.
7011                        if val.is_iterator() {
7012                            let iter = val.into_iterator();
7013                            let mut count = 0i64;
7014                            if let Some(&(start, end)) =
7015                                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7016                            {
7017                                while let Some(item) = iter.next_item() {
7018                                    count += 1;
7019                                    self.interp.scope.set_topic(item);
7020                                    self.run_block_region(start, end, op_count)?;
7021                                }
7022                            } else {
7023                                let block = self.blocks[idx].clone();
7024                                while let Some(item) = iter.next_item() {
7025                                    count += 1;
7026                                    self.interp.scope.set_topic(item);
7027                                    match self.interp.exec_block(&block) {
7028                                        Ok(_) => {}
7029                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
7030                                            return Err(e)
7031                                        }
7032                                        Err(_) => {}
7033                                    }
7034                                }
7035                            }
7036                            self.push(StrykeValue::integer(count));
7037                            return Ok(());
7038                        }
7039                        let list = val.to_list();
7040                        let count = list.len() as i64;
7041                        if let Some(&(start, end)) =
7042                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7043                        {
7044                            for item in list {
7045                                self.interp.scope.set_topic(item);
7046                                self.run_block_region(start, end, op_count)?;
7047                            }
7048                        } else {
7049                            let block = self.blocks[idx].clone();
7050                            for item in list {
7051                                self.interp.scope.set_topic(item);
7052                                match self.interp.exec_block(&block) {
7053                                    Ok(_) => {}
7054                                    Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
7055                                    Err(_) => {}
7056                                }
7057                            }
7058                        }
7059                        self.push(StrykeValue::integer(count));
7060                        Ok(())
7061                    }
7062                    Op::GrepWithExpr(expr_idx) => {
7063                        let list = self.pop().to_list();
7064                        let idx = *expr_idx as usize;
7065                        let dispatch_coderef = !crate::compat_mode();
7066                        // EXPR-form: see `map_with_expr_common` — no `{}` block
7067                        // boundary, so use `set_topic_local` (no chain shift,
7068                        // no slot 1+ zero).
7069                        if let Some(&(start, end)) = self
7070                            .grep_expr_bytecode_ranges
7071                            .get(idx)
7072                            .and_then(|r| r.as_ref())
7073                        {
7074                            let mut result = Vec::new();
7075                            for item in list {
7076                                self.interp.scope.set_topic_local(item.clone());
7077                                let val = self.run_block_region(start, end, op_count)?;
7078                                let val = self.maybe_call_coderef_with_item(
7079                                    val,
7080                                    &item,
7081                                    dispatch_coderef,
7082                                )?;
7083                                let keep = if let Some(re) = val.as_regex() {
7084                                    re.is_match(&item.to_string())
7085                                } else {
7086                                    val.is_true()
7087                                };
7088                                if keep {
7089                                    result.push(item);
7090                                }
7091                            }
7092                            self.push(StrykeValue::array(result));
7093                            Ok(())
7094                        } else {
7095                            let e = self.grep_expr_entries[idx].clone();
7096                            let mut result = Vec::new();
7097                            for item in list {
7098                                self.interp.scope.set_topic_local(item.clone());
7099                                let val = vm_interp_result(self.interp.eval_expr(&e), self.line())?;
7100                                let val = self.maybe_call_coderef_with_item(
7101                                    val,
7102                                    &item,
7103                                    dispatch_coderef,
7104                                )?;
7105                                let keep = if let Some(re) = val.as_regex() {
7106                                    re.is_match(&item.to_string())
7107                                } else {
7108                                    val.is_true()
7109                                };
7110                                if keep {
7111                                    result.push(item);
7112                                }
7113                            }
7114                            self.push(StrykeValue::array(result));
7115                            Ok(())
7116                        }
7117                    }
7118                    Op::SortWithBlock(block_idx) => {
7119                        let mut items = self.pop().to_list();
7120                        let idx = *block_idx as usize;
7121                        // Save the topic chain before sort — set_sort_pair writes to $_
7122                        // which would corrupt _< for subsequent pipeline stages (grep, map).
7123                        let saved_topic = self.interp.scope.save_topic_chain();
7124                        if let Some(&(start, end)) =
7125                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7126                        {
7127                            let mut sort_err: Option<StrykeError> = None;
7128                            items.sort_by(|a, b| {
7129                                if sort_err.is_some() {
7130                                    return std::cmp::Ordering::Equal;
7131                                }
7132                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
7133                                match self.run_block_region(start, end, op_count) {
7134                                    Ok(v) => {
7135                                        let n = v.to_int();
7136                                        if n < 0 {
7137                                            std::cmp::Ordering::Less
7138                                        } else if n > 0 {
7139                                            std::cmp::Ordering::Greater
7140                                        } else {
7141                                            std::cmp::Ordering::Equal
7142                                        }
7143                                    }
7144                                    Err(e) => {
7145                                        sort_err = Some(e);
7146                                        std::cmp::Ordering::Equal
7147                                    }
7148                                }
7149                            });
7150                            self.interp.scope.restore_topic_chain(saved_topic);
7151                            if let Some(e) = sort_err {
7152                                return Err(e);
7153                            }
7154                            self.push(StrykeValue::array(items));
7155                            Ok(())
7156                        } else {
7157                            let block = self.blocks[idx].clone();
7158                            items.sort_by(|a, b| {
7159                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
7160                                match self.interp.exec_block(&block) {
7161                                    Ok(v) => {
7162                                        let n = v.to_int();
7163                                        if n < 0 {
7164                                            std::cmp::Ordering::Less
7165                                        } else if n > 0 {
7166                                            std::cmp::Ordering::Greater
7167                                        } else {
7168                                            std::cmp::Ordering::Equal
7169                                        }
7170                                    }
7171                                    Err(_) => std::cmp::Ordering::Equal,
7172                                }
7173                            });
7174                            self.interp.scope.restore_topic_chain(saved_topic);
7175                            self.push(StrykeValue::array(items));
7176                            Ok(())
7177                        }
7178                    }
7179                    Op::SortWithBlockFast(tag) => {
7180                        let mut items = self.pop().to_list();
7181                        let mode = match *tag {
7182                            0 => SortBlockFast::Numeric,
7183                            1 => SortBlockFast::String,
7184                            2 => SortBlockFast::NumericRev,
7185                            3 => SortBlockFast::StringRev,
7186                            _ => SortBlockFast::Numeric,
7187                        };
7188                        items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
7189                        self.push(StrykeValue::array(items));
7190                        Ok(())
7191                    }
7192                    Op::SortNoBlock => {
7193                        let mut items = self.pop().to_list();
7194                        items.sort_by_key(|a| a.to_string());
7195                        self.push(StrykeValue::array(items));
7196                        Ok(())
7197                    }
7198                    Op::SortWithCodeComparator(wa) => {
7199                        let want = WantarrayCtx::from_byte(*wa);
7200                        let cmp_val = self.pop();
7201                        let mut items = self.pop().to_list();
7202                        let line = self.line();
7203                        let Some(sub) = cmp_val.as_code_ref() else {
7204                            return Err(StrykeError::runtime(
7205                                "sort: comparator must be a code reference",
7206                                line,
7207                            ));
7208                        };
7209                        let interp = &mut self.interp;
7210                        items.sort_by(|a, b| {
7211                            // `set_sort_pair` keeps Perl-style `$a`/`$b` access;
7212                            // positional args let stryke lambdas read via @_.
7213                            interp.scope.set_sort_pair(a.clone(), b.clone());
7214                            match interp.call_sub(
7215                                sub.as_ref(),
7216                                vec![a.clone(), b.clone()],
7217                                want,
7218                                line,
7219                            ) {
7220                                Ok(v) => {
7221                                    let n = v.to_int();
7222                                    if n < 0 {
7223                                        std::cmp::Ordering::Less
7224                                    } else if n > 0 {
7225                                        std::cmp::Ordering::Greater
7226                                    } else {
7227                                        std::cmp::Ordering::Equal
7228                                    }
7229                                }
7230                                Err(_) => std::cmp::Ordering::Equal,
7231                            }
7232                        });
7233                        self.push(StrykeValue::array(items));
7234                        Ok(())
7235                    }
7236                    Op::ReverseListOp => {
7237                        let val = self.pop();
7238                        if val.is_iterator() {
7239                            self.push(StrykeValue::iterator(std::sync::Arc::new(
7240                                crate::value::RevIterator::new(val.into_iterator()),
7241                            )));
7242                        } else {
7243                            let mut items = val.to_list();
7244                            items.reverse();
7245                            self.push(StrykeValue::array(items));
7246                        }
7247                        Ok(())
7248                    }
7249                    Op::ReverseScalarOp => {
7250                        let val = self.pop();
7251                        let items = val.to_list();
7252                        let s: String = items.iter().map(|v| v.to_string()).collect();
7253                        self.push(StrykeValue::string(s.chars().rev().collect()));
7254                        Ok(())
7255                    }
7256                    Op::RevListOp => {
7257                        let val = self.pop();
7258                        if val.is_iterator() {
7259                            // Collect the iterator fully and reverse the list order.
7260                            // RevIterator does per-element char reversal, not list reversal.
7261                            let mut items = val.to_list();
7262                            items.reverse();
7263                            self.push(StrykeValue::array(items));
7264                        } else if let Some(s) = crate::value::set_payload(&val) {
7265                            let mut out = crate::value::PerlSet::new();
7266                            for (k, v) in s.iter().rev() {
7267                                out.insert(k.clone(), v.clone());
7268                            }
7269                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
7270                        } else if let Some(ar) = val.as_array_ref() {
7271                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
7272                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
7273                                parking_lot::RwLock::new(items),
7274                            )));
7275                        } else if let Some(hr) = val.as_hash_ref() {
7276                            let mut out: indexmap::IndexMap<String, StrykeValue> =
7277                                indexmap::IndexMap::new();
7278                            for (k, v) in hr.read().iter() {
7279                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
7280                            }
7281                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
7282                                parking_lot::RwLock::new(out),
7283                            )));
7284                        } else if let Some(hm) = val.as_hash_map() {
7285                            let mut out: indexmap::IndexMap<String, StrykeValue> =
7286                                indexmap::IndexMap::new();
7287                            for (k, v) in hm.iter() {
7288                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
7289                            }
7290                            self.push(StrykeValue::hash(out));
7291                        } else if val.as_array_vec().is_some() {
7292                            let mut items = val.to_list();
7293                            items.reverse();
7294                            self.push(StrykeValue::array(items));
7295                        } else {
7296                            let s = val.to_string();
7297                            self.push(StrykeValue::string(s.chars().rev().collect()));
7298                        }
7299                        Ok(())
7300                    }
7301                    Op::RevScalarOp => {
7302                        let val = self.pop();
7303                        if let Some(s) = crate::value::set_payload(&val) {
7304                            let mut out = crate::value::PerlSet::new();
7305                            for (k, v) in s.iter().rev() {
7306                                out.insert(k.clone(), v.clone());
7307                            }
7308                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
7309                        } else if let Some(ar) = val.as_array_ref() {
7310                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
7311                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
7312                                parking_lot::RwLock::new(items),
7313                            )));
7314                        } else if let Some(hr) = val.as_hash_ref() {
7315                            let mut out: indexmap::IndexMap<String, StrykeValue> =
7316                                indexmap::IndexMap::new();
7317                            for (k, v) in hr.read().iter() {
7318                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
7319                            }
7320                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
7321                                parking_lot::RwLock::new(out),
7322                            )));
7323                        } else {
7324                            let items = val.to_list();
7325                            let s: String = items.iter().map(|v| v.to_string()).collect();
7326                            self.push(StrykeValue::string(s.chars().rev().collect()));
7327                        }
7328                        Ok(())
7329                    }
7330                    Op::StackArrayLen => {
7331                        let v = self.pop();
7332                        self.push(StrykeValue::integer(v.to_list().len() as i64));
7333                        Ok(())
7334                    }
7335                    Op::ListSliceToScalar => {
7336                        let v = self.pop();
7337                        let items = v.to_list();
7338                        self.push(items.last().cloned().unwrap_or(StrykeValue::UNDEF));
7339                        Ok(())
7340                    }
7341
7342                    // ── Eval block ──
7343                    Op::EvalBlock(block_idx, want) => {
7344                        let block = self.blocks[*block_idx as usize].clone();
7345                        let tail = crate::vm_helper::WantarrayCtx::from_byte(*want);
7346                        self.interp.eval_nesting += 1;
7347                        // Use exec_block (with scope frame) so local/my declarations
7348                        // inside the block are properly scoped.
7349                        match self.interp.exec_block_with_tail(&block, tail) {
7350                            Ok(v) => {
7351                                self.interp.clear_eval_error();
7352                                self.push(v);
7353                            }
7354                            Err(crate::vm_helper::FlowOrError::Error(e)) => {
7355                                self.interp.set_eval_error_from_perl_error(&e);
7356                                self.push(StrykeValue::UNDEF);
7357                            }
7358                            Err(_) => self.push(StrykeValue::UNDEF),
7359                        }
7360                        self.interp.eval_nesting -= 1;
7361                        Ok(())
7362                    }
7363                    Op::TraceBlock(block_idx) => {
7364                        let block = self.blocks[*block_idx as usize].clone();
7365                        crate::parallel_trace::trace_enter();
7366                        self.interp.eval_nesting += 1;
7367                        match self.interp.exec_block(&block) {
7368                            Ok(v) => {
7369                                self.interp.clear_eval_error();
7370                                self.push(v);
7371                            }
7372                            Err(FlowOrError::Error(e)) => {
7373                                self.interp.set_eval_error_from_perl_error(&e);
7374                                self.push(StrykeValue::UNDEF);
7375                            }
7376                            Err(_) => self.push(StrykeValue::UNDEF),
7377                        }
7378                        self.interp.eval_nesting -= 1;
7379                        crate::parallel_trace::trace_leave();
7380                        Ok(())
7381                    }
7382                    Op::TimerBlock(block_idx) => {
7383                        let block = self.blocks[*block_idx as usize].clone();
7384                        let start = std::time::Instant::now();
7385                        self.interp.eval_nesting += 1;
7386                        let _ = match self.interp.exec_block(&block) {
7387                            Ok(v) => {
7388                                self.interp.clear_eval_error();
7389                                v
7390                            }
7391                            Err(FlowOrError::Error(e)) => {
7392                                self.interp.set_eval_error_from_perl_error(&e);
7393                                StrykeValue::UNDEF
7394                            }
7395                            Err(_) => StrykeValue::UNDEF,
7396                        };
7397                        self.interp.eval_nesting -= 1;
7398                        let ms = start.elapsed().as_secs_f64() * 1000.0;
7399                        self.push(StrykeValue::float(ms));
7400                        Ok(())
7401                    }
7402                    Op::BenchBlock(block_idx) => {
7403                        let n_i = self.pop().to_int();
7404                        if n_i < 0 {
7405                            return Err(StrykeError::runtime(
7406                                "bench: iteration count must be non-negative",
7407                                self.line(),
7408                            ));
7409                        }
7410                        let n = n_i as usize;
7411                        let block = self.blocks[*block_idx as usize].clone();
7412                        let v = vm_interp_result(
7413                            self.interp.run_bench_block(&block, n, self.line()),
7414                            self.line(),
7415                        )?;
7416                        self.push(v);
7417                        Ok(())
7418                    }
7419                    Op::Given(idx) => {
7420                        let i = *idx as usize;
7421                        let line = self.line();
7422                        let v = if let Some(&(start, end)) = self
7423                            .given_topic_bytecode_ranges
7424                            .get(i)
7425                            .and_then(|r| r.as_ref())
7426                        {
7427                            let topic_val = self.run_block_region(start, end, op_count)?;
7428                            let body = &self.given_entries[i].1;
7429                            vm_interp_result(
7430                                self.interp.exec_given_with_topic_value(topic_val, body),
7431                                line,
7432                            )?
7433                        } else {
7434                            let (topic, body) = &self.given_entries[i];
7435                            vm_interp_result(self.interp.exec_given(topic, body), line)?
7436                        };
7437                        self.push(v);
7438                        Ok(())
7439                    }
7440                    Op::EvalTimeout(idx) => {
7441                        let i = *idx as usize;
7442                        let body = self.eval_timeout_entries[i].1.clone();
7443                        let secs = if let Some(&(start, end)) = self
7444                            .eval_timeout_expr_bytecode_ranges
7445                            .get(i)
7446                            .and_then(|r| r.as_ref())
7447                        {
7448                            self.run_block_region(start, end, op_count)?.to_number()
7449                        } else {
7450                            let timeout_expr = &self.eval_timeout_entries[i].0;
7451                            vm_interp_result(self.interp.eval_expr(timeout_expr), self.line())?
7452                                .to_number()
7453                        };
7454                        let v = vm_interp_result(
7455                            self.interp.eval_timeout_block(&body, secs, self.line()),
7456                            self.line(),
7457                        )?;
7458                        self.push(v);
7459                        Ok(())
7460                    }
7461                    Op::AlgebraicMatch(idx) => {
7462                        let i = *idx as usize;
7463                        let line = self.line();
7464                        let v = if let Some(&(start, end)) = self
7465                            .algebraic_match_subject_bytecode_ranges
7466                            .get(i)
7467                            .and_then(|r| r.as_ref())
7468                        {
7469                            let subject_val = self.run_block_region(start, end, op_count)?;
7470                            let arms = &self.algebraic_match_entries[i].1;
7471                            vm_interp_result(
7472                                self.interp.eval_algebraic_match_with_subject_value(
7473                                    subject_val,
7474                                    arms,
7475                                    line,
7476                                ),
7477                                self.line(),
7478                            )?
7479                        } else {
7480                            let (subject, arms) = &self.algebraic_match_entries[i];
7481                            vm_interp_result(
7482                                self.interp.eval_algebraic_match(subject, arms, line),
7483                                self.line(),
7484                            )?
7485                        };
7486                        self.push(v);
7487                        Ok(())
7488                    }
7489                    Op::ParLines(idx) => {
7490                        let (path, callback, progress) = &self.par_lines_entries[*idx as usize];
7491                        let v = vm_interp_result(
7492                            self.interp.eval_par_lines_expr(
7493                                path,
7494                                callback,
7495                                progress.as_ref(),
7496                                self.line(),
7497                            ),
7498                            self.line(),
7499                        )?;
7500                        self.push(v);
7501                        Ok(())
7502                    }
7503                    Op::ParWalk(idx) => {
7504                        let (path, callback, progress) = &self.par_walk_entries[*idx as usize];
7505                        let v = vm_interp_result(
7506                            self.interp.eval_par_walk_expr(
7507                                path,
7508                                callback,
7509                                progress.as_ref(),
7510                                self.line(),
7511                            ),
7512                            self.line(),
7513                        )?;
7514                        self.push(v);
7515                        Ok(())
7516                    }
7517                    Op::Pwatch(idx) => {
7518                        let (path, callback) = &self.pwatch_entries[*idx as usize];
7519                        let v = vm_interp_result(
7520                            self.interp.eval_pwatch_expr(path, callback, self.line()),
7521                            self.line(),
7522                        )?;
7523                        self.push(v);
7524                        Ok(())
7525                    }
7526
7527                    // ── Parallel operations (rayon) ──
7528                    Op::PMapWithBlock(block_idx) => {
7529                        let list = self.pop().to_list();
7530                        let progress_flag = self.pop().is_true();
7531                        let idx = *block_idx as usize;
7532                        let subs = self.interp.subs.clone();
7533                        let (scope_capture, atomic_arrays, atomic_hashes) =
7534                            self.interp.scope.capture_with_atomics();
7535                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7536                        let n_workers = rayon::current_num_threads();
7537                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
7538                            .map(|_| {
7539                                let mut interp = VMHelper::new();
7540                                interp.subs = subs.clone();
7541                                interp.scope.restore_capture(&scope_capture);
7542                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
7543                                interp.enable_parallel_guard();
7544                                Mutex::new(interp)
7545                            })
7546                            .collect();
7547                        if let Some(&(start, end)) =
7548                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7549                        {
7550                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7551                            let results: Vec<StrykeValue> = list
7552                                .into_par_iter()
7553                                .map(|item| {
7554                                    let tid =
7555                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7556                                    let mut local_interp = pool[tid].lock();
7557                                    local_interp.scope.set_topic(item);
7558                                    let mut vm = shared.worker_vm(&mut local_interp);
7559                                    let mut op_count = 0u64;
7560                                    let val = match vm.run_block_region(start, end, &mut op_count) {
7561                                        Ok(v) => v,
7562                                        Err(_) => StrykeValue::UNDEF,
7563                                    };
7564                                    pmap_progress.tick();
7565                                    val
7566                                })
7567                                .collect();
7568                            pmap_progress.finish();
7569                            self.push(StrykeValue::array(results));
7570                            Ok(())
7571                        } else {
7572                            let block = self.blocks[idx].clone();
7573                            let results: Vec<StrykeValue> = list
7574                                .into_par_iter()
7575                                .map(|item| {
7576                                    let tid =
7577                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7578                                    let mut local_interp = pool[tid].lock();
7579                                    local_interp.scope.set_topic(item);
7580                                    local_interp.scope_push_hook();
7581                                    let val = match local_interp.exec_block_no_scope(&block) {
7582                                        Ok(val) => val,
7583                                        Err(_) => StrykeValue::UNDEF,
7584                                    };
7585                                    local_interp.scope_pop_hook();
7586                                    pmap_progress.tick();
7587                                    val
7588                                })
7589                                .collect();
7590                            pmap_progress.finish();
7591                            self.push(StrykeValue::array(results));
7592                            Ok(())
7593                        }
7594                    }
7595                    Op::PFlatMapWithBlock(block_idx) => {
7596                        let list = self.pop().to_list();
7597                        let progress_flag = self.pop().is_true();
7598                        let idx = *block_idx as usize;
7599                        let subs = self.interp.subs.clone();
7600                        let (scope_capture, atomic_arrays, atomic_hashes) =
7601                            self.interp.scope.capture_with_atomics();
7602                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7603                        let n_workers = rayon::current_num_threads();
7604                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
7605                            .map(|_| {
7606                                let mut interp = VMHelper::new();
7607                                interp.subs = subs.clone();
7608                                interp.scope.restore_capture(&scope_capture);
7609                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
7610                                interp.enable_parallel_guard();
7611                                Mutex::new(interp)
7612                            })
7613                            .collect();
7614                        if let Some(&(start, end)) =
7615                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7616                        {
7617                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7618                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
7619                                .into_par_iter()
7620                                .enumerate()
7621                                .map(|(i, item)| {
7622                                    let tid =
7623                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7624                                    let mut local_interp = pool[tid].lock();
7625                                    local_interp.scope.set_topic(item);
7626                                    let mut vm = shared.worker_vm(&mut local_interp);
7627                                    let mut op_count = 0u64;
7628                                    let val = match vm.run_block_region(start, end, &mut op_count) {
7629                                        Ok(v) => v,
7630                                        Err(_) => StrykeValue::UNDEF,
7631                                    };
7632                                    let out = val.map_flatten_outputs(true);
7633                                    pmap_progress.tick();
7634                                    (i, out)
7635                                })
7636                                .collect();
7637                            pmap_progress.finish();
7638                            indexed.sort_by_key(|(i, _)| *i);
7639                            let results: Vec<StrykeValue> =
7640                                indexed.into_iter().flat_map(|(_, v)| v).collect();
7641                            self.push(StrykeValue::array(results));
7642                            Ok(())
7643                        } else {
7644                            let block = self.blocks[idx].clone();
7645                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
7646                                .into_par_iter()
7647                                .enumerate()
7648                                .map(|(i, item)| {
7649                                    let tid =
7650                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7651                                    let mut local_interp = pool[tid].lock();
7652                                    local_interp.scope.set_topic(item);
7653                                    local_interp.scope_push_hook();
7654                                    let val = match local_interp.exec_block_no_scope(&block) {
7655                                        Ok(val) => val,
7656                                        Err(_) => StrykeValue::UNDEF,
7657                                    };
7658                                    local_interp.scope_pop_hook();
7659                                    let out = val.map_flatten_outputs(true);
7660                                    pmap_progress.tick();
7661                                    (i, out)
7662                                })
7663                                .collect();
7664                            pmap_progress.finish();
7665                            indexed.sort_by_key(|(i, _)| *i);
7666                            let results: Vec<StrykeValue> =
7667                                indexed.into_iter().flat_map(|(_, v)| v).collect();
7668                            self.push(StrykeValue::array(results));
7669                            Ok(())
7670                        }
7671                    }
7672                    Op::PMapRemote { block_idx, flat } => {
7673                        let cluster = self.pop();
7674                        let list_pv = self.pop();
7675                        let progress_flag = self.pop().is_true();
7676                        let idx = *block_idx as usize;
7677                        let block = self.blocks[idx].clone();
7678                        let flat_outputs = *flat != 0;
7679                        let v = vm_interp_result(
7680                            self.interp.eval_pmap_remote(
7681                                cluster,
7682                                list_pv,
7683                                progress_flag,
7684                                &block,
7685                                flat_outputs,
7686                                self.line(),
7687                            ),
7688                            self.line(),
7689                        )?;
7690                        self.push(v);
7691                        Ok(())
7692                    }
7693                    Op::Puniq => {
7694                        let list = self.pop().to_list();
7695                        let progress_flag = self.pop().is_true();
7696                        let n_threads = self.interp.parallel_thread_count();
7697                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7698                        let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
7699                        pmap_progress.finish();
7700                        self.push(StrykeValue::array(out));
7701                        Ok(())
7702                    }
7703                    Op::PFirstWithBlock(block_idx) => {
7704                        let list = self.pop().to_list();
7705                        let progress_flag = self.pop().is_true();
7706                        let idx = *block_idx as usize;
7707                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7708                        let subs = self.interp.subs.clone();
7709                        let (scope_capture, atomic_arrays, atomic_hashes) =
7710                            self.interp.scope.capture_with_atomics();
7711                        let out = if let Some(&(start, end)) =
7712                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7713                        {
7714                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7715                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
7716                                let mut local_interp = VMHelper::new();
7717                                local_interp.subs = subs.clone();
7718                                local_interp.scope.restore_capture(&scope_capture);
7719                                local_interp
7720                                    .scope
7721                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7722                                local_interp.enable_parallel_guard();
7723                                local_interp.scope.set_topic(item);
7724                                let mut vm = shared.worker_vm(&mut local_interp);
7725                                let mut op_count = 0u64;
7726                                match vm.run_block_region(start, end, &mut op_count) {
7727                                    Ok(v) => v.is_true(),
7728                                    Err(_) => false,
7729                                }
7730                            })
7731                        } else {
7732                            let block = self.blocks[idx].clone();
7733                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
7734                                let mut local_interp = VMHelper::new();
7735                                local_interp.subs = subs.clone();
7736                                local_interp.scope.restore_capture(&scope_capture);
7737                                local_interp
7738                                    .scope
7739                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7740                                local_interp.enable_parallel_guard();
7741                                local_interp.scope.set_topic(item);
7742                                local_interp.scope_push_hook();
7743                                let ok = match local_interp.exec_block_no_scope(&block) {
7744                                    Ok(v) => v.is_true(),
7745                                    Err(_) => false,
7746                                };
7747                                local_interp.scope_pop_hook();
7748                                ok
7749                            })
7750                        };
7751                        pmap_progress.finish();
7752                        self.push(out.unwrap_or(StrykeValue::UNDEF));
7753                        Ok(())
7754                    }
7755                    Op::PAnyWithBlock(block_idx) => {
7756                        let list = self.pop().to_list();
7757                        let progress_flag = self.pop().is_true();
7758                        let idx = *block_idx as usize;
7759                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7760                        let subs = self.interp.subs.clone();
7761                        let (scope_capture, atomic_arrays, atomic_hashes) =
7762                            self.interp.scope.capture_with_atomics();
7763                        let b = if let Some(&(start, end)) =
7764                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7765                        {
7766                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7767                            crate::par_list::pany_run(list, &pmap_progress, |item| {
7768                                let mut local_interp = VMHelper::new();
7769                                local_interp.subs = subs.clone();
7770                                local_interp.scope.restore_capture(&scope_capture);
7771                                local_interp
7772                                    .scope
7773                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7774                                local_interp.enable_parallel_guard();
7775                                local_interp.scope.set_topic(item);
7776                                let mut vm = shared.worker_vm(&mut local_interp);
7777                                let mut op_count = 0u64;
7778                                match vm.run_block_region(start, end, &mut op_count) {
7779                                    Ok(v) => v.is_true(),
7780                                    Err(_) => false,
7781                                }
7782                            })
7783                        } else {
7784                            let block = self.blocks[idx].clone();
7785                            crate::par_list::pany_run(list, &pmap_progress, |item| {
7786                                let mut local_interp = VMHelper::new();
7787                                local_interp.subs = subs.clone();
7788                                local_interp.scope.restore_capture(&scope_capture);
7789                                local_interp
7790                                    .scope
7791                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7792                                local_interp.enable_parallel_guard();
7793                                local_interp.scope.set_topic(item);
7794                                local_interp.scope_push_hook();
7795                                let ok = match local_interp.exec_block_no_scope(&block) {
7796                                    Ok(v) => v.is_true(),
7797                                    Err(_) => false,
7798                                };
7799                                local_interp.scope_pop_hook();
7800                                ok
7801                            })
7802                        };
7803                        pmap_progress.finish();
7804                        self.push(StrykeValue::integer(if b { 1 } else { 0 }));
7805                        Ok(())
7806                    }
7807                    Op::PMapChunkedWithBlock(block_idx) => {
7808                        let list = self.pop().to_list();
7809                        let chunk_n = self.pop().to_int().max(1) as usize;
7810                        let progress_flag = self.pop().is_true();
7811                        let idx = *block_idx as usize;
7812                        let subs = self.interp.subs.clone();
7813                        let (scope_capture, atomic_arrays, atomic_hashes) =
7814                            self.interp.scope.capture_with_atomics();
7815                        let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = list
7816                            .chunks(chunk_n)
7817                            .enumerate()
7818                            .map(|(i, c)| (i, c.to_vec()))
7819                            .collect();
7820                        let n_chunks = indexed_chunks.len();
7821                        let pmap_progress = PmapProgress::new(progress_flag, n_chunks);
7822                        if let Some(&(start, end)) =
7823                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7824                        {
7825                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7826                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
7827                                .into_par_iter()
7828                                .map(|(chunk_idx, chunk)| {
7829                                    let mut local_interp = VMHelper::new();
7830                                    local_interp.subs = subs.clone();
7831                                    local_interp.scope.restore_capture(&scope_capture);
7832                                    local_interp
7833                                        .scope
7834                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
7835                                    local_interp.enable_parallel_guard();
7836                                    let mut out = Vec::with_capacity(chunk.len());
7837                                    for item in chunk {
7838                                        local_interp.scope.set_topic(item);
7839                                        let mut vm = shared.worker_vm(&mut local_interp);
7840                                        let mut op_count = 0u64;
7841                                        let val =
7842                                            match vm.run_block_region(start, end, &mut op_count) {
7843                                                Ok(v) => v,
7844                                                Err(_) => StrykeValue::UNDEF,
7845                                            };
7846                                        out.push(val);
7847                                    }
7848                                    pmap_progress.tick();
7849                                    (chunk_idx, out)
7850                                })
7851                                .collect();
7852                            pmap_progress.finish();
7853                            chunk_results.sort_by_key(|(i, _)| *i);
7854                            let results: Vec<StrykeValue> =
7855                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
7856                            self.push(StrykeValue::array(results));
7857                            Ok(())
7858                        } else {
7859                            let block = self.blocks[idx].clone();
7860                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
7861                                .into_par_iter()
7862                                .map(|(chunk_idx, chunk)| {
7863                                    let mut local_interp = VMHelper::new();
7864                                    local_interp.subs = subs.clone();
7865                                    local_interp.scope.restore_capture(&scope_capture);
7866                                    local_interp
7867                                        .scope
7868                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
7869                                    local_interp.enable_parallel_guard();
7870                                    let mut out = Vec::with_capacity(chunk.len());
7871                                    for item in chunk {
7872                                        local_interp.scope.set_topic(item);
7873                                        local_interp.scope_push_hook();
7874                                        let val = match local_interp.exec_block_no_scope(&block) {
7875                                            Ok(val) => val,
7876                                            Err(_) => StrykeValue::UNDEF,
7877                                        };
7878                                        local_interp.scope_pop_hook();
7879                                        out.push(val);
7880                                    }
7881                                    pmap_progress.tick();
7882                                    (chunk_idx, out)
7883                                })
7884                                .collect();
7885                            pmap_progress.finish();
7886                            chunk_results.sort_by_key(|(i, _)| *i);
7887                            let results: Vec<StrykeValue> =
7888                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
7889                            self.push(StrykeValue::array(results));
7890                            Ok(())
7891                        }
7892                    }
7893                    Op::ReduceWithBlock(block_idx) => {
7894                        let list = self.pop().to_list();
7895                        let idx = *block_idx as usize;
7896                        let subs = self.interp.subs.clone();
7897                        let scope_capture = self.interp.scope.capture();
7898                        if list.is_empty() {
7899                            self.push(StrykeValue::UNDEF);
7900                            return Ok(());
7901                        }
7902                        if list.len() == 1 {
7903                            self.push(list.into_iter().next().unwrap());
7904                            return Ok(());
7905                        }
7906                        let mut items = list;
7907                        let mut acc = items.remove(0);
7908                        let rest = items;
7909                        if let Some(&(start, end)) =
7910                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7911                        {
7912                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7913                            for b in rest {
7914                                let mut local_interp = VMHelper::new();
7915                                local_interp.subs = subs.clone();
7916                                local_interp.scope.restore_capture(&scope_capture);
7917                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
7918                                let mut vm = shared.worker_vm(&mut local_interp);
7919                                let mut op_count = 0u64;
7920                                acc = match vm.run_block_region(start, end, &mut op_count) {
7921                                    Ok(v) => v,
7922                                    Err(_) => StrykeValue::UNDEF,
7923                                };
7924                            }
7925                        } else {
7926                            let block = self.blocks[idx].clone();
7927                            for b in rest {
7928                                let mut local_interp = VMHelper::new();
7929                                local_interp.subs = subs.clone();
7930                                local_interp.scope.restore_capture(&scope_capture);
7931                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
7932                                acc = match local_interp.exec_block(&block) {
7933                                    Ok(val) => val,
7934                                    Err(_) => StrykeValue::UNDEF,
7935                                };
7936                            }
7937                        }
7938                        self.push(acc);
7939                        Ok(())
7940                    }
7941                    Op::PReduceWithBlock(block_idx) => {
7942                        let list = self.pop().to_list();
7943                        let progress_flag = self.pop().is_true();
7944                        let idx = *block_idx as usize;
7945                        let subs = self.interp.subs.clone();
7946                        let scope_capture = self.interp.scope.capture();
7947                        if list.is_empty() {
7948                            self.push(StrykeValue::UNDEF);
7949                            return Ok(());
7950                        }
7951                        if list.len() == 1 {
7952                            self.push(list.into_iter().next().unwrap());
7953                            return Ok(());
7954                        }
7955                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7956                        if let Some(&(start, end)) =
7957                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7958                        {
7959                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7960                            let result = list
7961                                .into_par_iter()
7962                                .map(|x| {
7963                                    pmap_progress.tick();
7964                                    x
7965                                })
7966                                .reduce_with(|a, b| {
7967                                    let mut local_interp = VMHelper::new();
7968                                    local_interp.subs = subs.clone();
7969                                    local_interp.scope.restore_capture(&scope_capture);
7970                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
7971                                    let mut vm = shared.worker_vm(&mut local_interp);
7972                                    let mut op_count = 0u64;
7973                                    match vm.run_block_region(start, end, &mut op_count) {
7974                                        Ok(val) => val,
7975                                        Err(_) => StrykeValue::UNDEF,
7976                                    }
7977                                });
7978                            pmap_progress.finish();
7979                            self.push(result.unwrap_or(StrykeValue::UNDEF));
7980                            Ok(())
7981                        } else {
7982                            let block = self.blocks[idx].clone();
7983                            let result = list
7984                                .into_par_iter()
7985                                .map(|x| {
7986                                    pmap_progress.tick();
7987                                    x
7988                                })
7989                                .reduce_with(|a, b| {
7990                                    let mut local_interp = VMHelper::new();
7991                                    local_interp.subs = subs.clone();
7992                                    local_interp.scope.restore_capture(&scope_capture);
7993                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
7994                                    match local_interp.exec_block(&block) {
7995                                        Ok(val) => val,
7996                                        Err(_) => StrykeValue::UNDEF,
7997                                    }
7998                                });
7999                            pmap_progress.finish();
8000                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8001                            Ok(())
8002                        }
8003                    }
8004                    Op::PReduceInitWithBlock(block_idx) => {
8005                        let init_val = self.pop();
8006                        let list = self.pop().to_list();
8007                        let progress_flag = self.pop().is_true();
8008                        let idx = *block_idx as usize;
8009                        let subs = self.interp.subs.clone();
8010                        let scope_capture = self.interp.scope.capture();
8011                        let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
8012                        let block = self.blocks[idx].clone();
8013                        if list.is_empty() {
8014                            self.push(init_val);
8015                            return Ok(());
8016                        }
8017                        if list.len() == 1 {
8018                            let v = fold_preduce_init_step(
8019                                &subs,
8020                                cap,
8021                                &block,
8022                                preduce_init_fold_identity(&init_val),
8023                                list.into_iter().next().unwrap(),
8024                            );
8025                            self.push(v);
8026                            return Ok(());
8027                        }
8028                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8029                        let result = list
8030                            .into_par_iter()
8031                            .fold(
8032                                || preduce_init_fold_identity(&init_val),
8033                                |acc, item| {
8034                                    pmap_progress.tick();
8035                                    fold_preduce_init_step(&subs, cap, &block, acc, item)
8036                                },
8037                            )
8038                            .reduce(
8039                                || preduce_init_fold_identity(&init_val),
8040                                |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
8041                            );
8042                        pmap_progress.finish();
8043                        self.push(result);
8044                        Ok(())
8045                    }
8046                    Op::PMapReduceWithBlocks(map_idx, reduce_idx) => {
8047                        let list = self.pop().to_list();
8048                        let progress_flag = self.pop().is_true();
8049                        let map_i = *map_idx as usize;
8050                        let reduce_i = *reduce_idx as usize;
8051                        let subs = self.interp.subs.clone();
8052                        let scope_capture = self.interp.scope.capture();
8053                        if list.is_empty() {
8054                            self.push(StrykeValue::UNDEF);
8055                            return Ok(());
8056                        }
8057                        if list.len() == 1 {
8058                            let mut local_interp = VMHelper::new();
8059                            local_interp.subs = subs.clone();
8060                            local_interp.scope.restore_capture(&scope_capture);
8061                            local_interp
8062                                .scope
8063                                .set_topic(list.into_iter().next().unwrap());
8064                            let map_block = self.blocks[map_i].clone();
8065                            let v = match local_interp.exec_block_no_scope(&map_block) {
8066                                Ok(v) => v,
8067                                Err(_) => StrykeValue::UNDEF,
8068                            };
8069                            self.push(v);
8070                            return Ok(());
8071                        }
8072                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8073                        let map_range = self
8074                            .block_bytecode_ranges
8075                            .get(map_i)
8076                            .and_then(|r| r.as_ref())
8077                            .copied();
8078                        let reduce_range = self
8079                            .block_bytecode_ranges
8080                            .get(reduce_i)
8081                            .and_then(|r| r.as_ref())
8082                            .copied();
8083                        if let (Some((map_start, map_end)), Some((reduce_start, reduce_end))) =
8084                            (map_range, reduce_range)
8085                        {
8086                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8087                            let result = list
8088                                .into_par_iter()
8089                                .map(|item| {
8090                                    let mut local_interp = VMHelper::new();
8091                                    local_interp.subs = subs.clone();
8092                                    local_interp.scope.restore_capture(&scope_capture);
8093                                    local_interp.scope.set_topic(item);
8094                                    let mut vm = shared.worker_vm(&mut local_interp);
8095                                    let mut op_count = 0u64;
8096                                    let val = match vm.run_block_region(
8097                                        map_start,
8098                                        map_end,
8099                                        &mut op_count,
8100                                    ) {
8101                                        Ok(val) => val,
8102                                        Err(_) => StrykeValue::UNDEF,
8103                                    };
8104                                    pmap_progress.tick();
8105                                    val
8106                                })
8107                                .reduce_with(|a, b| {
8108                                    let mut local_interp = VMHelper::new();
8109                                    local_interp.subs = subs.clone();
8110                                    local_interp.scope.restore_capture(&scope_capture);
8111                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8112                                    let mut vm = shared.worker_vm(&mut local_interp);
8113                                    let mut op_count = 0u64;
8114                                    match vm.run_block_region(
8115                                        reduce_start,
8116                                        reduce_end,
8117                                        &mut op_count,
8118                                    ) {
8119                                        Ok(val) => val,
8120                                        Err(_) => StrykeValue::UNDEF,
8121                                    }
8122                                });
8123                            pmap_progress.finish();
8124                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8125                            Ok(())
8126                        } else {
8127                            let map_block = self.blocks[map_i].clone();
8128                            let reduce_block = self.blocks[reduce_i].clone();
8129                            let result = list
8130                                .into_par_iter()
8131                                .map(|item| {
8132                                    let mut local_interp = VMHelper::new();
8133                                    local_interp.subs = subs.clone();
8134                                    local_interp.scope.restore_capture(&scope_capture);
8135                                    local_interp.scope.set_topic(item);
8136                                    let val = match local_interp.exec_block_no_scope(&map_block) {
8137                                        Ok(val) => val,
8138                                        Err(_) => StrykeValue::UNDEF,
8139                                    };
8140                                    pmap_progress.tick();
8141                                    val
8142                                })
8143                                .reduce_with(|a, b| {
8144                                    let mut local_interp = VMHelper::new();
8145                                    local_interp.subs = subs.clone();
8146                                    local_interp.scope.restore_capture(&scope_capture);
8147                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
8148                                    match local_interp.exec_block_no_scope(&reduce_block) {
8149                                        Ok(val) => val,
8150                                        Err(_) => StrykeValue::UNDEF,
8151                                    }
8152                                });
8153                            pmap_progress.finish();
8154                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8155                            Ok(())
8156                        }
8157                    }
8158                    Op::PcacheWithBlock(block_idx) => {
8159                        let list = self.pop().to_list();
8160                        let progress_flag = self.pop().is_true();
8161                        let idx = *block_idx as usize;
8162                        let subs = self.interp.subs.clone();
8163                        let scope_capture = self.interp.scope.capture();
8164                        let block = self.blocks[idx].clone();
8165                        let cache = &*crate::pcache::GLOBAL_PCACHE;
8166                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8167                        if let Some(&(start, end)) =
8168                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8169                        {
8170                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8171                            let results: Vec<StrykeValue> = list
8172                                .into_par_iter()
8173                                .map(|item| {
8174                                    let k = crate::pcache::cache_key(&item);
8175                                    if let Some(v) = cache.get(&k) {
8176                                        pmap_progress.tick();
8177                                        return v.clone();
8178                                    }
8179                                    let mut local_interp = VMHelper::new();
8180                                    local_interp.subs = subs.clone();
8181                                    local_interp.scope.restore_capture(&scope_capture);
8182                                    local_interp.scope.set_topic(item.clone());
8183                                    let mut vm = shared.worker_vm(&mut local_interp);
8184                                    let mut op_count = 0u64;
8185                                    let val = match vm.run_block_region(start, end, &mut op_count) {
8186                                        Ok(v) => v,
8187                                        Err(_) => StrykeValue::UNDEF,
8188                                    };
8189                                    cache.insert(k, val.clone());
8190                                    pmap_progress.tick();
8191                                    val
8192                                })
8193                                .collect();
8194                            pmap_progress.finish();
8195                            self.push(StrykeValue::array(results));
8196                            Ok(())
8197                        } else {
8198                            let results: Vec<StrykeValue> = list
8199                                .into_par_iter()
8200                                .map(|item| {
8201                                    let k = crate::pcache::cache_key(&item);
8202                                    if let Some(v) = cache.get(&k) {
8203                                        pmap_progress.tick();
8204                                        return v.clone();
8205                                    }
8206                                    let mut local_interp = VMHelper::new();
8207                                    local_interp.subs = subs.clone();
8208                                    local_interp.scope.restore_capture(&scope_capture);
8209                                    local_interp.scope.set_topic(item.clone());
8210                                    let val = match local_interp.exec_block_no_scope(&block) {
8211                                        Ok(v) => v,
8212                                        Err(_) => StrykeValue::UNDEF,
8213                                    };
8214                                    cache.insert(k, val.clone());
8215                                    pmap_progress.tick();
8216                                    val
8217                                })
8218                                .collect();
8219                            pmap_progress.finish();
8220                            self.push(StrykeValue::array(results));
8221                            Ok(())
8222                        }
8223                    }
8224                    Op::Pselect { n_rx, has_timeout } => {
8225                        let timeout = if *has_timeout {
8226                            let t = self.pop().to_number();
8227                            Some(std::time::Duration::from_secs_f64(t.max(0.0)))
8228                        } else {
8229                            None
8230                        };
8231                        let mut rx_vals = Vec::with_capacity(*n_rx as usize);
8232                        for _ in 0..*n_rx {
8233                            rx_vals.push(self.pop());
8234                        }
8235                        rx_vals.reverse();
8236                        let line = self.line();
8237                        let v = crate::pchannel::pselect_recv_with_optional_timeout(
8238                            &rx_vals, timeout, line,
8239                        )?;
8240                        self.push(v);
8241                        Ok(())
8242                    }
8243                    Op::PGrepWithBlock(block_idx) => {
8244                        let list = self.pop().to_list();
8245                        let progress_flag = self.pop().is_true();
8246                        let idx = *block_idx as usize;
8247                        let subs = self.interp.subs.clone();
8248                        let (scope_capture, atomic_arrays, atomic_hashes) =
8249                            self.interp.scope.capture_with_atomics();
8250                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8251                        let n_workers = rayon::current_num_threads();
8252                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8253                            .map(|_| {
8254                                let mut interp = VMHelper::new();
8255                                interp.subs = subs.clone();
8256                                interp.scope.restore_capture(&scope_capture);
8257                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8258                                interp.enable_parallel_guard();
8259                                Mutex::new(interp)
8260                            })
8261                            .collect();
8262                        if let Some(&(start, end)) =
8263                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8264                        {
8265                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8266                            let results: Vec<StrykeValue> = list
8267                                .into_par_iter()
8268                                .filter_map(|item| {
8269                                    let tid =
8270                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8271                                    let mut local_interp = pool[tid].lock();
8272                                    local_interp.scope.set_topic(item.clone());
8273                                    let mut vm = shared.worker_vm(&mut local_interp);
8274                                    let mut op_count = 0u64;
8275                                    let keep = match vm.run_block_region(start, end, &mut op_count)
8276                                    {
8277                                        Ok(val) => val.is_true(),
8278                                        Err(_) => false,
8279                                    };
8280                                    pmap_progress.tick();
8281                                    if keep {
8282                                        Some(item)
8283                                    } else {
8284                                        None
8285                                    }
8286                                })
8287                                .collect();
8288                            pmap_progress.finish();
8289                            self.push(StrykeValue::array(results));
8290                            Ok(())
8291                        } else {
8292                            let block = self.blocks[idx].clone();
8293                            let results: Vec<StrykeValue> = list
8294                                .into_par_iter()
8295                                .filter_map(|item| {
8296                                    let tid =
8297                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8298                                    let mut local_interp = pool[tid].lock();
8299                                    local_interp.scope.set_topic(item.clone());
8300                                    local_interp.scope_push_hook();
8301                                    let keep = match local_interp.exec_block_no_scope(&block) {
8302                                        Ok(val) => val.is_true(),
8303                                        Err(_) => false,
8304                                    };
8305                                    local_interp.scope_pop_hook();
8306                                    pmap_progress.tick();
8307                                    if keep {
8308                                        Some(item)
8309                                    } else {
8310                                        None
8311                                    }
8312                                })
8313                                .collect();
8314                            pmap_progress.finish();
8315                            self.push(StrykeValue::array(results));
8316                            Ok(())
8317                        }
8318                    }
8319                    Op::PMapsWithBlock(block_idx) => {
8320                        let val = self.pop();
8321                        let block = self.blocks[*block_idx as usize].clone();
8322                        let source = crate::map_stream::into_pull_iter(val);
8323                        let sub = self.interp.anon_coderef_from_block(&block);
8324                        let (capture, atomic_arrays, atomic_hashes) =
8325                            self.interp.scope.capture_with_atomics();
8326                        let out = StrykeValue::iterator(Arc::new(
8327                            crate::map_stream::PMapStreamIterator::new(
8328                                source,
8329                                sub,
8330                                self.interp.subs.clone(),
8331                                capture,
8332                                atomic_arrays,
8333                                atomic_hashes,
8334                                false,
8335                            ),
8336                        ));
8337                        self.push(out);
8338                        Ok(())
8339                    }
8340                    Op::PFlatMapsWithBlock(block_idx) => {
8341                        let val = self.pop();
8342                        let block = self.blocks[*block_idx as usize].clone();
8343                        let source = crate::map_stream::into_pull_iter(val);
8344                        let sub = self.interp.anon_coderef_from_block(&block);
8345                        let (capture, atomic_arrays, atomic_hashes) =
8346                            self.interp.scope.capture_with_atomics();
8347                        let out = StrykeValue::iterator(Arc::new(
8348                            crate::map_stream::PMapStreamIterator::new(
8349                                source,
8350                                sub,
8351                                self.interp.subs.clone(),
8352                                capture,
8353                                atomic_arrays,
8354                                atomic_hashes,
8355                                true,
8356                            ),
8357                        ));
8358                        self.push(out);
8359                        Ok(())
8360                    }
8361                    Op::PGrepsWithBlock(block_idx) => {
8362                        let val = self.pop();
8363                        let block = self.blocks[*block_idx as usize].clone();
8364                        let source = crate::map_stream::into_pull_iter(val);
8365                        let sub = self.interp.anon_coderef_from_block(&block);
8366                        let (capture, atomic_arrays, atomic_hashes) =
8367                            self.interp.scope.capture_with_atomics();
8368                        let out = StrykeValue::iterator(Arc::new(
8369                            crate::map_stream::PGrepStreamIterator::new(
8370                                source,
8371                                sub,
8372                                self.interp.subs.clone(),
8373                                capture,
8374                                atomic_arrays,
8375                                atomic_hashes,
8376                            ),
8377                        ));
8378                        self.push(out);
8379                        Ok(())
8380                    }
8381                    Op::PForWithBlock(block_idx) => {
8382                        let line = self.line();
8383                        let list = self.pop().to_list();
8384                        let progress_flag = self.pop().is_true();
8385                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8386                        let idx = *block_idx as usize;
8387                        let subs = self.interp.subs.clone();
8388                        let (scope_capture, atomic_arrays, atomic_hashes) =
8389                            self.interp.scope.capture_with_atomics();
8390                        let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
8391                        let n_workers = rayon::current_num_threads();
8392                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8393                            .map(|_| {
8394                                let mut interp = VMHelper::new();
8395                                interp.subs = subs.clone();
8396                                interp.scope.restore_capture(&scope_capture);
8397                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8398                                interp.enable_parallel_guard();
8399                                Mutex::new(interp)
8400                            })
8401                            .collect();
8402                        if let Some(&(start, end)) =
8403                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8404                        {
8405                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8406                            list.into_par_iter().for_each(|item| {
8407                                if first_err.lock().is_some() {
8408                                    return;
8409                                }
8410                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
8411                                let mut local_interp = pool[tid].lock();
8412                                local_interp.scope.set_topic(item);
8413                                let mut vm = shared.worker_vm(&mut local_interp);
8414                                let mut op_count = 0u64;
8415                                match vm.run_block_region(start, end, &mut op_count) {
8416                                    Ok(_) => {}
8417                                    Err(e) => {
8418                                        let mut g = first_err.lock();
8419                                        if g.is_none() {
8420                                            *g = Some(e);
8421                                        }
8422                                    }
8423                                }
8424                                pmap_progress.tick();
8425                            });
8426                        } else {
8427                            let block = self.blocks[idx].clone();
8428                            list.into_par_iter().for_each(|item| {
8429                                if first_err.lock().is_some() {
8430                                    return;
8431                                }
8432                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
8433                                let mut local_interp = pool[tid].lock();
8434                                local_interp.scope.set_topic(item);
8435                                local_interp.scope_push_hook();
8436                                match local_interp.exec_block_no_scope(&block) {
8437                                    Ok(_) => {}
8438                                    Err(e) => {
8439                                        let stryke = match e {
8440                                            FlowOrError::Error(stryke) => stryke,
8441                                            FlowOrError::Flow(_) => StrykeError::runtime(
8442                                                "return/last/next/redo not supported inside pfor block",
8443                                                line,
8444                                            ),
8445                                        };
8446                                        let mut g = first_err.lock();
8447                                        if g.is_none() {
8448                                            *g = Some(stryke);
8449                                        }
8450                                    }
8451                                }
8452                                local_interp.scope_pop_hook();
8453                                pmap_progress.tick();
8454                            });
8455                        }
8456                        pmap_progress.finish();
8457                        if let Some(e) = first_err.lock().take() {
8458                            return Err(e);
8459                        }
8460                        self.push(StrykeValue::UNDEF);
8461                        Ok(())
8462                    }
8463                    Op::PSortWithBlock(block_idx) => {
8464                        let mut items = self.pop().to_list();
8465                        let progress_flag = self.pop().is_true();
8466                        let pmap_progress = PmapProgress::new(progress_flag, 2);
8467                        pmap_progress.tick();
8468                        let idx = *block_idx as usize;
8469                        let subs = self.interp.subs.clone();
8470                        let (scope_capture, atomic_arrays, atomic_hashes) =
8471                            self.interp.scope.capture_with_atomics();
8472                        if let Some(&(start, end)) =
8473                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8474                        {
8475                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8476                            items.par_sort_by(|a, b| {
8477                                let mut local_interp = VMHelper::new();
8478                                local_interp.subs = subs.clone();
8479                                local_interp.scope.restore_capture(&scope_capture);
8480                                local_interp
8481                                    .scope
8482                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8483                                local_interp.enable_parallel_guard();
8484                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
8485                                // Populate slot-based positional args so the
8486                                // bytecode block can read `$_0`/`$_1` (and the
8487                                // bareword `_0`/`_1`) through the slot fast
8488                                // path. `set_sort_pair` only sets the named
8489                                // scalars; without slots, an `$_0` reference
8490                                // resolves to undef in worker bytecode.
8491                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
8492                                let mut vm = shared.worker_vm(&mut local_interp);
8493                                let mut op_count = 0u64;
8494                                match vm.run_block_region(start, end, &mut op_count) {
8495                                    Ok(v) => {
8496                                        let n = v.to_int();
8497                                        if n < 0 {
8498                                            std::cmp::Ordering::Less
8499                                        } else if n > 0 {
8500                                            std::cmp::Ordering::Greater
8501                                        } else {
8502                                            std::cmp::Ordering::Equal
8503                                        }
8504                                    }
8505                                    Err(_) => std::cmp::Ordering::Equal,
8506                                }
8507                            });
8508                        } else {
8509                            let block = self.blocks[idx].clone();
8510                            items.par_sort_by(|a, b| {
8511                                let mut local_interp = VMHelper::new();
8512                                local_interp.subs = subs.clone();
8513                                local_interp.scope.restore_capture(&scope_capture);
8514                                local_interp
8515                                    .scope
8516                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8517                                local_interp.enable_parallel_guard();
8518                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
8519                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
8520                                local_interp.scope_push_hook();
8521                                let ord = match local_interp.exec_block_no_scope(&block) {
8522                                    Ok(v) => {
8523                                        let n = v.to_int();
8524                                        if n < 0 {
8525                                            std::cmp::Ordering::Less
8526                                        } else if n > 0 {
8527                                            std::cmp::Ordering::Greater
8528                                        } else {
8529                                            std::cmp::Ordering::Equal
8530                                        }
8531                                    }
8532                                    Err(_) => std::cmp::Ordering::Equal,
8533                                };
8534                                local_interp.scope_pop_hook();
8535                                ord
8536                            });
8537                        }
8538                        pmap_progress.tick();
8539                        pmap_progress.finish();
8540                        self.push(StrykeValue::array(items));
8541                        Ok(())
8542                    }
8543                    Op::PSortWithBlockFast(tag) => {
8544                        let mut items = self.pop().to_list();
8545                        let progress_flag = self.pop().is_true();
8546                        let pmap_progress = PmapProgress::new(progress_flag, 2);
8547                        pmap_progress.tick();
8548                        let mode = match *tag {
8549                            0 => SortBlockFast::Numeric,
8550                            1 => SortBlockFast::String,
8551                            2 => SortBlockFast::NumericRev,
8552                            3 => SortBlockFast::StringRev,
8553                            _ => SortBlockFast::Numeric,
8554                        };
8555                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
8556                        pmap_progress.tick();
8557                        pmap_progress.finish();
8558                        self.push(StrykeValue::array(items));
8559                        Ok(())
8560                    }
8561                    Op::PSortNoBlockParallel => {
8562                        let mut items = self.pop().to_list();
8563                        let progress_flag = self.pop().is_true();
8564                        let pmap_progress = PmapProgress::new(progress_flag, 2);
8565                        pmap_progress.tick();
8566                        items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
8567                        pmap_progress.tick();
8568                        pmap_progress.finish();
8569                        self.push(StrykeValue::array(items));
8570                        Ok(())
8571                    }
8572                    Op::FanWithBlock(block_idx) => {
8573                        let line = self.line();
8574                        let n = self.pop().to_int().max(0) as usize;
8575                        let progress_flag = self.pop().is_true();
8576                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
8577                        Ok(())
8578                    }
8579                    Op::FanWithBlockAuto(block_idx) => {
8580                        let line = self.line();
8581                        let n = self.interp.parallel_thread_count();
8582                        let progress_flag = self.pop().is_true();
8583                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
8584                        Ok(())
8585                    }
8586                    Op::FanCapWithBlock(block_idx) => {
8587                        let line = self.line();
8588                        let n = self.pop().to_int().max(0) as usize;
8589                        let progress_flag = self.pop().is_true();
8590                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
8591                        Ok(())
8592                    }
8593                    Op::FanCapWithBlockAuto(block_idx) => {
8594                        let line = self.line();
8595                        let n = self.interp.parallel_thread_count();
8596                        let progress_flag = self.pop().is_true();
8597                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
8598                        Ok(())
8599                    }
8600
8601                    Op::AsyncBlock(block_idx) => {
8602                        let block = self.blocks[*block_idx as usize].clone();
8603                        let subs = self.interp.subs.clone();
8604                        let (scope_capture, atomic_arrays, atomic_hashes) =
8605                            self.interp.scope.capture_with_atomics();
8606                        let result_slot: Arc<Mutex<Option<StrykeResult<StrykeValue>>>> =
8607                            Arc::new(Mutex::new(None));
8608                        let join_slot: Arc<Mutex<Option<std::thread::JoinHandle<()>>>> =
8609                            Arc::new(Mutex::new(None));
8610                        let rs = Arc::clone(&result_slot);
8611                        let h = std::thread::spawn(move || {
8612                            let mut local_interp = VMHelper::new();
8613                            local_interp.subs = subs;
8614                            local_interp.scope.restore_capture(&scope_capture);
8615                            local_interp
8616                                .scope
8617                                .restore_atomics(&atomic_arrays, &atomic_hashes);
8618                            local_interp.enable_parallel_guard();
8619                            local_interp.scope_push_hook();
8620                            let out = match local_interp.exec_block_no_scope(&block) {
8621                                Ok(v) => Ok(v),
8622                                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
8623                                Err(FlowOrError::Error(e)) => Err(e),
8624                                Err(_) => Ok(StrykeValue::UNDEF),
8625                            };
8626                            local_interp.scope_pop_hook();
8627                            *rs.lock() = Some(out);
8628                        });
8629                        *join_slot.lock() = Some(h);
8630                        self.push(StrykeValue::async_task(Arc::new(StrykeAsyncTask {
8631                            result: result_slot,
8632                            join: join_slot,
8633                        })));
8634                        Ok(())
8635                    }
8636                    Op::Await => {
8637                        let v = self.pop();
8638                        if let Some(t) = v.as_async_task() {
8639                            let r = t.await_result();
8640                            self.push(r?);
8641                        } else {
8642                            self.push(v);
8643                        }
8644                        Ok(())
8645                    }
8646
8647                    Op::LoadCurrentSub => {
8648                        if let Some(sub) = self.interp.current_sub_stack.last().cloned() {
8649                            self.push(StrykeValue::code_ref(sub));
8650                        } else {
8651                            self.push(StrykeValue::UNDEF);
8652                        }
8653                        Ok(())
8654                    }
8655
8656                    Op::DeferBlock => {
8657                        let coderef = self.pop();
8658                        self.interp.scope.push_defer(coderef);
8659                        Ok(())
8660                    }
8661
8662                    // ── try / catch / finally ──
8663                    Op::TryPush { .. } => {
8664                        self.try_stack.push(TryFrame {
8665                            try_push_op_idx: self.ip - 1,
8666                            state: TryState::Trying,
8667                            deferred_error: None,
8668                        });
8669                        Ok(())
8670                    }
8671                    Op::TryContinueNormal => {
8672                        let frame = self.try_stack.last().ok_or_else(|| {
8673                            StrykeError::runtime(
8674                                "TryContinueNormal without active try",
8675                                self.line(),
8676                            )
8677                        })?;
8678                        let Op::TryPush {
8679                            finally_ip,
8680                            after_ip,
8681                            ..
8682                        } = &self.ops[frame.try_push_op_idx]
8683                        else {
8684                            return Err(StrykeError::runtime(
8685                                "TryContinueNormal: corrupt try frame",
8686                                self.line(),
8687                            ));
8688                        };
8689                        if let Some(fin_ip) = *finally_ip {
8690                            self.ip = fin_ip;
8691                            Ok(())
8692                        } else {
8693                            self.try_stack.pop();
8694                            self.ip = *after_ip;
8695                            Ok(())
8696                        }
8697                    }
8698                    Op::TryFinallyEnd => {
8699                        let frame = self.try_stack.pop().ok_or_else(|| {
8700                            StrykeError::runtime("TryFinallyEnd without active try", self.line())
8701                        })?;
8702                        // If `catch` threw and we ran `finally` to clean up, re-raise the
8703                        // deferred error now that finally has completed.
8704                        if let Some(deferred) = frame.deferred_error {
8705                            return Err(deferred);
8706                        }
8707                        let Op::TryPush { after_ip, .. } = &self.ops[frame.try_push_op_idx] else {
8708                            return Err(StrykeError::runtime(
8709                                "TryFinallyEnd: corrupt try frame",
8710                                self.line(),
8711                            ));
8712                        };
8713                        self.ip = *after_ip;
8714                        Ok(())
8715                    }
8716                    Op::CatchReceive(idx) => {
8717                        let val = self.pending_catch_error.take().ok_or_else(|| {
8718                            StrykeError::runtime(
8719                                "CatchReceive without pending exception",
8720                                self.line(),
8721                            )
8722                        })?;
8723                        let n = names[*idx as usize].as_str();
8724                        self.interp.scope_pop_hook();
8725                        self.interp.scope_push_hook();
8726                        self.interp.scope.declare_scalar(n, val);
8727                        self.interp.english_note_lexical_scalar(n);
8728                        Ok(())
8729                    }
8730
8731                    Op::DeclareMySyncScalar(name_idx) => {
8732                        let val = self.pop();
8733                        let n = names[*name_idx as usize].as_str();
8734                        let stored = if val.is_mysync_deque_or_heap() {
8735                            val
8736                        } else {
8737                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
8738                        };
8739                        self.interp.scope.declare_scalar(n, stored);
8740                        Ok(())
8741                    }
8742                    Op::DeclareMySyncArray(name_idx) => {
8743                        let val = self.pop();
8744                        let n = names[*name_idx as usize].as_str();
8745                        self.interp.scope.declare_atomic_array(n, val.to_list());
8746                        Ok(())
8747                    }
8748                    Op::DeclareMySyncHash(name_idx) => {
8749                        let val = self.pop();
8750                        let n = names[*name_idx as usize].as_str();
8751                        let items = val.to_list();
8752                        let mut map = IndexMap::new();
8753                        let mut i = 0usize;
8754                        while i + 1 < items.len() {
8755                            map.insert(items[i].to_string(), items[i + 1].clone());
8756                            i += 2;
8757                        }
8758                        self.interp.scope.declare_atomic_hash(n, map);
8759                        Ok(())
8760                    }
8761                    Op::DeclareOurSyncScalar(name_idx) => {
8762                        let val = self.pop();
8763                        let n = names[*name_idx as usize].as_str();
8764                        let stored = if val.is_mysync_deque_or_heap() {
8765                            val
8766                        } else {
8767                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
8768                        };
8769                        self.interp.scope.declare_scalar(n, stored);
8770                        // Register the bare name (everything after `Pkg::`) in the
8771                        // tree-walker tracking sets so worker `$x` reads inside fan/pmap
8772                        // bodies (which run via `exec_block_no_scope`, not bytecode)
8773                        // rewrite to `Pkg::x` and find the shared cell.
8774                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
8775                        self.interp.english_note_lexical_scalar_pub(&bare);
8776                        self.interp.note_our_scalar_pub(&bare);
8777                        Ok(())
8778                    }
8779                    Op::DeclareOurSyncArray(name_idx) => {
8780                        let val = self.pop();
8781                        let n = names[*name_idx as usize].as_str();
8782                        self.interp.scope.declare_atomic_array(n, val.to_list());
8783                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
8784                        self.interp.english_note_lexical_scalar_pub(&bare);
8785                        self.interp.note_our_scalar_pub(&bare);
8786                        Ok(())
8787                    }
8788                    Op::DeclareOurSyncHash(name_idx) => {
8789                        let val = self.pop();
8790                        let n = names[*name_idx as usize].as_str();
8791                        let items = val.to_list();
8792                        let mut map = IndexMap::new();
8793                        let mut i = 0usize;
8794                        while i + 1 < items.len() {
8795                            map.insert(items[i].to_string(), items[i + 1].clone());
8796                            i += 2;
8797                        }
8798                        self.interp.scope.declare_atomic_hash(n, map);
8799                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
8800                        self.interp.english_note_lexical_scalar_pub(&bare);
8801                        self.interp.note_our_scalar_pub(&bare);
8802                        Ok(())
8803                    }
8804                    Op::RuntimeSubDecl(idx) => {
8805                        let rs = &self.runtime_sub_decls[*idx as usize];
8806                        let key = self.interp.qualify_sub_key(&rs.name);
8807                        let captured = self.interp.scope.capture();
8808                        let closure_env = if captured.is_empty() {
8809                            None
8810                        } else {
8811                            Some(captured)
8812                        };
8813                        let mut sub = StrykeSub {
8814                            name: rs.name.clone(),
8815                            params: rs.params.clone(),
8816                            body: rs.body.clone(),
8817                            closure_env,
8818                            prototype: rs.prototype.clone(),
8819                            fib_like: None,
8820                        };
8821                        sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
8822                        self.interp.subs.insert(key, Arc::new(sub));
8823                        Ok(())
8824                    }
8825                    Op::RegisterAdvice(idx) => {
8826                        let rd = &self.runtime_advice_decls[*idx as usize];
8827                        let id = self.interp.next_intercept_id;
8828                        self.interp.next_intercept_id = id.saturating_add(1);
8829                        self.interp.intercepts.push(crate::aop::Intercept {
8830                            id,
8831                            kind: rd.kind,
8832                            pattern: rd.pattern.clone(),
8833                            body: rd.body.clone(),
8834                            body_block_idx: rd.body_block_idx,
8835                        });
8836                        Ok(())
8837                    }
8838                    Op::Tie {
8839                        target_kind,
8840                        name_idx,
8841                        argc,
8842                    } => {
8843                        let argc = *argc as usize;
8844                        let mut stack_vals = Vec::with_capacity(argc);
8845                        for _ in 0..argc {
8846                            stack_vals.push(self.pop());
8847                        }
8848                        stack_vals.reverse();
8849                        let name = names[*name_idx as usize].as_str();
8850                        let line = self.line();
8851                        self.interp
8852                            .tie_execute(*target_kind, name, stack_vals, line)
8853                            .map_err(|e| e.at_line(line))?;
8854                        Ok(())
8855                    }
8856                    Op::FormatDecl(idx) => {
8857                        let (basename, lines) = &self.format_decls[*idx as usize];
8858                        let line = self.line();
8859                        self.interp
8860                            .install_format_decl(basename.as_str(), lines, line)
8861                            .map_err(|e| e.at_line(line))?;
8862                        Ok(())
8863                    }
8864                    Op::UseOverload(idx) => {
8865                        let pairs = &self.use_overload_entries[*idx as usize];
8866                        self.interp.install_use_overload_pairs(pairs);
8867                        Ok(())
8868                    }
8869                    Op::ScalarCompoundAssign { name_idx, op: op_b } => {
8870                        let rhs = self.pop();
8871                        let n = names[*name_idx as usize].as_str();
8872                        let op = scalar_compound_op_from_byte(*op_b).ok_or_else(|| {
8873                            StrykeError::runtime(
8874                                "ScalarCompoundAssign: invalid op byte",
8875                                self.line(),
8876                            )
8877                        })?;
8878                        let en = self.interp.english_scalar_name(n);
8879                        let val = self
8880                            .interp
8881                            .scalar_compound_assign_scalar_target(en, op, rhs)
8882                            .map_err(|e| e.at_line(self.line()))?;
8883                        self.push(val);
8884                        Ok(())
8885                    }
8886
8887                    Op::SetGlobalPhase(phase) => {
8888                        let s = match *phase {
8889                            crate::bytecode::GP_START => "START",
8890                            crate::bytecode::GP_UNITCHECK => "UNITCHECK",
8891                            crate::bytecode::GP_CHECK => "CHECK",
8892                            crate::bytecode::GP_INIT => "INIT",
8893                            crate::bytecode::GP_RUN => "RUN",
8894                            crate::bytecode::GP_END => "END",
8895                            _ => {
8896                                return Err(StrykeError::runtime(
8897                                    format!("SetGlobalPhase: invalid phase byte {}", phase),
8898                                    self.line(),
8899                                ));
8900                            }
8901                        };
8902                        self.interp.global_phase = s.to_string();
8903                        Ok(())
8904                    }
8905
8906                    // ── Halt ──
8907                    Op::Halt => {
8908                        self.halt = true;
8909                        Ok(())
8910                    }
8911                    Op::EvalAstExpr(idx) => {
8912                        let expr = &self.ast_eval_exprs[*idx as usize];
8913                        let val = match self.interp.eval_expr_ctx(expr, self.interp.wantarray_kind)
8914                        {
8915                            Ok(v) => v,
8916                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
8917                            Err(crate::vm_helper::FlowOrError::Flow(f)) => {
8918                                return Err(StrykeError::runtime(
8919                                    format!("unexpected flow control in EvalAstExpr: {:?}", f),
8920                                    self.line(),
8921                                ));
8922                            }
8923                        };
8924                        self.push(val);
8925                        Ok(())
8926                    }
8927                }
8928            })();
8929            if let (Some(prof), Some(t0)) = (&mut self.interp.profiler, op_prof_t0) {
8930                prof.on_line(&self.interp.file, line, t0.elapsed());
8931            }
8932            if let Err(e) = __op_res {
8933                if self.try_recover_from_exception(&e)? {
8934                    continue;
8935                }
8936                return Err(e);
8937            }
8938            // Blessed refcount drops enqueue from `StrykeValue::drop`; drain before the next opcode
8939            // so `$x = undef; f()` runs `DESTROY` before `f` (Perl semantics).
8940            if crate::pending_destroy::pending_destroy_vm_sync_needed() {
8941                self.interp.drain_pending_destroys(line)?;
8942            }
8943            if self.exit_main_dispatch {
8944                if let Some(v) = self.exit_main_dispatch_value.take() {
8945                    last = v;
8946                }
8947                break;
8948            }
8949            if self.halt {
8950                break;
8951            }
8952        }
8953
8954        if !self.stack.is_empty() {
8955            last = self.stack.last().cloned().unwrap_or(StrykeValue::UNDEF);
8956            // Drain iterators left on the stack so side effects fire
8957            // (e.g. `pmaps { system(...) } @list` with no consumer).
8958            if last.is_iterator() {
8959                let iter = last.clone().into_iterator();
8960                while iter.next_item().is_some() {}
8961                last = StrykeValue::UNDEF;
8962            }
8963        }
8964
8965        Ok(last)
8966    }
8967
8968    /// Called from Cranelift (`stryke_jit_call_sub`) to run a compiled sub by bytecode IP with `i64` args.
8969    pub(crate) fn jit_trampoline_run_sub(
8970        &mut self,
8971        entry_ip: usize,
8972        want: WantarrayCtx,
8973        args: &[i64],
8974    ) -> StrykeResult<StrykeValue> {
8975        let saved_wa = self.interp.wantarray_kind;
8976        for a in args {
8977            self.push(StrykeValue::integer(*a));
8978        }
8979        let stack_base = self.stack.len() - args.len();
8980        let mut sub_prof_t0 = None;
8981        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
8982            sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
8983            let nm_owned = self.names[nidx as usize].to_string();
8984            if let Some(p) = &mut self.interp.profiler {
8985                p.enter_sub(nm_owned.as_str());
8986            }
8987            self.interp.debugger_enter_sub(nm_owned.as_str());
8988        }
8989        self.call_stack.push(CallFrame {
8990            return_ip: 0,
8991            stack_base,
8992            scope_depth: self.interp.scope.depth(),
8993            saved_wantarray: saved_wa,
8994            jit_trampoline_return: true,
8995            block_region: false,
8996            sub_profiler_start: sub_prof_t0,
8997        });
8998        self.interp.wantarray_kind = want;
8999        self.interp.scope_push_hook();
9000        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
9001            let nm = self.names[nidx as usize].as_str();
9002            if let Some(sub) = self.interp.subs.get(nm).cloned() {
9003                if let Some(ref env) = sub.closure_env {
9004                    self.interp.scope.restore_capture(env);
9005                }
9006            }
9007        }
9008        self.ip = entry_ip;
9009        self.jit_trampoline_out = None;
9010        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_add(1);
9011        let mut op_count = 0u64;
9012        let last = StrykeValue::UNDEF;
9013        let r = self.run_main_dispatch_loop(last, &mut op_count, true);
9014        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_sub(1);
9015        r?;
9016        self.jit_trampoline_out.take().ok_or_else(|| {
9017            StrykeError::runtime("JIT trampoline: subroutine did not return", self.line())
9018        })
9019    }
9020
9021    #[inline]
9022    fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
9023        self.sub_entry_by_name.get(&name_idx).copied()
9024    }
9025
9026    /// Name pool index for a compiled sub entry IP (for closure env + JIT trampoline).
9027    fn sub_entry_name_idx(&self, entry_ip: usize) -> Option<u16> {
9028        for &(n, ip, _) in &self.sub_entries {
9029            if ip == entry_ip {
9030                return Some(n);
9031            }
9032        }
9033        None
9034    }
9035
9036    fn exec_builtin(&mut self, id: u16, args: Vec<StrykeValue>) -> StrykeResult<StrykeValue> {
9037        let line = self.line();
9038        let bid = BuiltinId::from_u16(id);
9039        match bid {
9040            Some(BuiltinId::Length) => {
9041                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9042                Ok(if let Some(a) = val.as_array_vec() {
9043                    StrykeValue::integer(a.len() as i64)
9044                } else if let Some(h) = val.as_hash_map() {
9045                    StrykeValue::integer(h.len() as i64)
9046                } else if let Some(b) = val.as_bytes_arc() {
9047                    // Raw byte buffer: always byte count, regardless of utf8 pragma.
9048                    StrykeValue::integer(b.len() as i64)
9049                } else {
9050                    let s = val.to_string();
9051                    let n = if self.interp.utf8_pragma {
9052                        s.chars().count()
9053                    } else {
9054                        s.len()
9055                    };
9056                    StrykeValue::integer(n as i64)
9057                })
9058            }
9059            Some(BuiltinId::Defined) => {
9060                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9061                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
9062            }
9063            Some(BuiltinId::Abs) => {
9064                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9065                Ok(StrykeValue::float(val.to_number().abs()))
9066            }
9067            Some(BuiltinId::Int) => {
9068                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9069                Ok(StrykeValue::integer(val.to_number() as i64))
9070            }
9071            Some(BuiltinId::Sqrt) => {
9072                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9073                Ok(StrykeValue::float(val.to_number().sqrt()))
9074            }
9075            Some(BuiltinId::Sin) => {
9076                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9077                Ok(StrykeValue::float(val.to_number().sin()))
9078            }
9079            Some(BuiltinId::Cos) => {
9080                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9081                Ok(StrykeValue::float(val.to_number().cos()))
9082            }
9083            Some(BuiltinId::Atan2) => {
9084                let mut it = args.into_iter();
9085                let y = it.next().unwrap_or(StrykeValue::UNDEF);
9086                let x = it.next().unwrap_or(StrykeValue::UNDEF);
9087                Ok(StrykeValue::float(y.to_number().atan2(x.to_number())))
9088            }
9089            Some(BuiltinId::Exp) => {
9090                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9091                Ok(StrykeValue::float(val.to_number().exp()))
9092            }
9093            Some(BuiltinId::Log) => {
9094                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9095                Ok(StrykeValue::float(val.to_number().ln()))
9096            }
9097            Some(BuiltinId::Rand) => {
9098                let upper = match args.len() {
9099                    0 => 1.0,
9100                    _ => args[0].to_number(),
9101                };
9102                Ok(StrykeValue::float(self.interp.perl_rand(upper)))
9103            }
9104            Some(BuiltinId::Srand) => {
9105                let seed = match args.len() {
9106                    0 => None,
9107                    _ => Some(args[0].to_number()),
9108                };
9109                Ok(StrykeValue::integer(self.interp.perl_srand(seed)))
9110            }
9111            Some(BuiltinId::Crypt) => {
9112                let mut it = args.into_iter();
9113                let p = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
9114                let salt = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
9115                Ok(StrykeValue::string(crate::crypt_util::perl_crypt(
9116                    &p, &salt,
9117                )))
9118            }
9119            Some(BuiltinId::Fc) => {
9120                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9121                Ok(StrykeValue::string(default_case_fold_str(&s.to_string())))
9122            }
9123            Some(BuiltinId::Pos) => {
9124                let key = if args.is_empty() {
9125                    "_".to_string()
9126                } else {
9127                    args[0].to_string()
9128                };
9129                Ok(self
9130                    .interp
9131                    .regex_pos
9132                    .get(&key)
9133                    .copied()
9134                    .flatten()
9135                    .map(|n| StrykeValue::integer(n as i64))
9136                    .unwrap_or(StrykeValue::UNDEF))
9137            }
9138            Some(BuiltinId::Study) => {
9139                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9140                Ok(VMHelper::study_return_value(&s.to_string()))
9141            }
9142            Some(BuiltinId::Chr) => {
9143                let n = args
9144                    .into_iter()
9145                    .next()
9146                    .unwrap_or(StrykeValue::UNDEF)
9147                    .to_int() as u32;
9148                Ok(StrykeValue::string(
9149                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
9150                ))
9151            }
9152            Some(BuiltinId::Ord) => {
9153                let s = args
9154                    .into_iter()
9155                    .next()
9156                    .unwrap_or(StrykeValue::UNDEF)
9157                    .to_string();
9158                Ok(StrykeValue::integer(
9159                    s.chars().next().map(|c| c as i64).unwrap_or(0),
9160                ))
9161            }
9162            Some(BuiltinId::Hex) => {
9163                let s = args
9164                    .into_iter()
9165                    .next()
9166                    .unwrap_or(StrykeValue::UNDEF)
9167                    .to_string();
9168                let clean = s.trim().trim_start_matches("0x").trim_start_matches("0X");
9169                Ok(StrykeValue::integer(
9170                    i64::from_str_radix(clean, 16).unwrap_or(0),
9171                ))
9172            }
9173            Some(BuiltinId::Oct) => {
9174                let s = args
9175                    .into_iter()
9176                    .next()
9177                    .unwrap_or(StrykeValue::UNDEF)
9178                    .to_string();
9179                let s = s.trim();
9180                let n = if s.starts_with("0x") || s.starts_with("0X") {
9181                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
9182                } else if s.starts_with("0b") || s.starts_with("0B") {
9183                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
9184                } else if s.starts_with("0o") || s.starts_with("0O") {
9185                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
9186                } else {
9187                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
9188                };
9189                Ok(StrykeValue::integer(n))
9190            }
9191            Some(BuiltinId::Uc) => {
9192                let s = args
9193                    .into_iter()
9194                    .next()
9195                    .unwrap_or(StrykeValue::UNDEF)
9196                    .to_string();
9197                Ok(StrykeValue::string(s.to_uppercase()))
9198            }
9199            Some(BuiltinId::Lc) => {
9200                let s = args
9201                    .into_iter()
9202                    .next()
9203                    .unwrap_or(StrykeValue::UNDEF)
9204                    .to_string();
9205                Ok(StrykeValue::string(s.to_lowercase()))
9206            }
9207            Some(BuiltinId::Ref) => {
9208                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9209                Ok(val.ref_type())
9210            }
9211            Some(BuiltinId::Scalar) => {
9212                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9213                Ok(val.scalar_context())
9214            }
9215            Some(BuiltinId::Join) => {
9216                let mut iter = args.into_iter();
9217                let sep = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
9218                let list = iter.next().unwrap_or(StrykeValue::UNDEF).to_list();
9219                let mut strs = Vec::with_capacity(list.len());
9220                for v in list {
9221                    let s = match self.interp.stringify_value(v, line) {
9222                        Ok(s) => s,
9223                        Err(FlowOrError::Error(e)) => return Err(e),
9224                        Err(FlowOrError::Flow(_)) => {
9225                            return Err(StrykeError::runtime(
9226                                "join: unexpected control flow",
9227                                line,
9228                            ));
9229                        }
9230                    };
9231                    strs.push(s);
9232                }
9233                Ok(StrykeValue::string(strs.join(&sep)))
9234            }
9235            Some(BuiltinId::Split) => {
9236                let mut iter = args.into_iter();
9237                let pat_val = iter.next().unwrap_or(StrykeValue::string(" ".into()));
9238                // Prefer the regex source over the Display form: `qr//`'s Display is
9239                // `(?:)` (matches everywhere), which is NOT the same as Perl's empty-
9240                // pattern semantics ("split between every character"). Pulling the
9241                // source out via `regex_src_and_flags` lets us treat `//` as truly
9242                // empty so the char-split branch fires.
9243                let pat = pat_val
9244                    .regex_src_and_flags()
9245                    .map(|(s, _)| s)
9246                    .unwrap_or_else(|| pat_val.to_string());
9247                let s = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
9248                // Perl 5: splitting the empty string yields the empty list for any
9249                // pattern / limit (regex `split` on `""` would otherwise leave one field).
9250                if s.is_empty() {
9251                    return Ok(StrykeValue::array(vec![]));
9252                }
9253                // Perl LIMIT semantics:
9254                //   omitted / 0  → no truncation, strip trailing empties.
9255                //   > 0          → at most LIMIT fields, keep empties up to limit.
9256                //   < 0          → no truncation, keep all empties.
9257                let lim_signed: Option<i64> = iter.next().map(|v| v.to_int());
9258
9259                let mut parts: Vec<String> = if pat.is_empty() {
9260                    // Empty pattern → "split between every character" (Perl). The
9261                    // regex engine would also match at the boundaries, producing
9262                    // spurious empties; `s.chars()` is the right primitive.
9263                    let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
9264                    match lim_signed {
9265                        // LIMIT > 0 (Perl):
9266                        //   n < |chars|        → first n-1 chars then the tail in one field
9267                        //                        (`split //, "abcde", 3` → ("a","b","cde")).
9268                        //   n == |chars|       → chars exactly, no trailing empty.
9269                        //   n > |chars|        → chars + "" (Perl emits the end-of-string
9270                        //                        match as a final empty when LIMIT permits).
9271                        Some(l) if l > 0 => {
9272                            let n = l as usize;
9273                            if n < chars.len() {
9274                                let mut head: Vec<String> =
9275                                    chars.iter().take(n.saturating_sub(1)).cloned().collect();
9276                                let tail: String = s.chars().skip(n.saturating_sub(1)).collect();
9277                                head.push(tail);
9278                                head
9279                            } else if n == chars.len() {
9280                                chars
9281                            } else {
9282                                let mut v = chars;
9283                                v.push(String::new());
9284                                v
9285                            }
9286                        }
9287                        // LIMIT < 0 → chars + trailing empty.
9288                        Some(l) if l < 0 => {
9289                            let mut v = chars;
9290                            v.push(String::new());
9291                            v
9292                        }
9293                        // No limit / 0 → just the chars; the trailing-empty strip
9294                        // below is a no-op (`chars()` never emits one).
9295                        _ => chars,
9296                    }
9297                } else {
9298                    let re =
9299                        regex::Regex::new(&pat).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
9300                    match lim_signed {
9301                        Some(l) if l > 0 => {
9302                            re.splitn(&s, l as usize).map(|p| p.to_string()).collect()
9303                        }
9304                        _ => re.split(&s).map(|p| p.to_string()).collect(),
9305                    }
9306                };
9307
9308                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted or
9309                // zero. Positive LIMIT keeps trailing empties (capped at LIMIT).
9310                // Negative LIMIT also keeps them.
9311                let strip_trailing = matches!(lim_signed, None | Some(0));
9312                if strip_trailing {
9313                    while parts.last().is_some_and(|p| p.is_empty()) {
9314                        parts.pop();
9315                    }
9316                }
9317
9318                Ok(StrykeValue::array(
9319                    parts.into_iter().map(StrykeValue::string).collect(),
9320                ))
9321            }
9322            Some(BuiltinId::Sprintf) => {
9323                // sprintf arg list is Perl list context; flatten ranges / arrays / reverse
9324                // output into individual format arguments (same splatting as printf).
9325                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
9326                for a in args.into_iter() {
9327                    if let Some(items) = a.as_array_vec() {
9328                        flat.extend(items);
9329                    } else {
9330                        flat.push(a);
9331                    }
9332                }
9333                let args = flat;
9334                if args.is_empty() {
9335                    return Ok(StrykeValue::string(String::new()));
9336                }
9337                let fmt = args[0].to_string();
9338                let rest = &args[1..];
9339                match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
9340                    Ok(s) => Ok(StrykeValue::string(s)),
9341                    Err(FlowOrError::Error(e)) => Err(e),
9342                    Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
9343                        "sprintf: unexpected control flow",
9344                        line,
9345                    )),
9346                }
9347            }
9348            Some(BuiltinId::Reverse) => {
9349                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9350                Ok(if let Some(mut a) = val.as_array_vec() {
9351                    a.reverse();
9352                    StrykeValue::array(a)
9353                } else if let Some(s) = val.as_str() {
9354                    StrykeValue::string(s.chars().rev().collect())
9355                } else {
9356                    StrykeValue::string(val.to_string().chars().rev().collect())
9357                })
9358            }
9359            Some(BuiltinId::Die) => {
9360                // Single-ref arg: preserve the original value (hash/array/code/blessed ref)
9361                // so `$@` and `try/catch` see the ref, not a stringification.
9362                if args.len() == 1 {
9363                    let v = &args[0];
9364                    if v.as_hash_ref().is_some()
9365                        || v.as_blessed_ref().is_some()
9366                        || v.as_array_ref().is_some()
9367                        || v.as_code_ref().is_some()
9368                    {
9369                        let msg = v.to_string();
9370                        self.interp.fire_pseudosig_die(&msg, line)?;
9371                        return Err(StrykeError::die_with_value(v.clone(), msg, line));
9372                    }
9373                }
9374                let mut msg = String::new();
9375                for a in &args {
9376                    msg.push_str(&a.to_string());
9377                }
9378                if msg.is_empty() {
9379                    msg = "Died".to_string();
9380                }
9381                if !msg.ends_with('\n') {
9382                    msg.push_str(&self.interp.die_warn_at_suffix(line));
9383                    msg.push('\n');
9384                }
9385                self.interp.fire_pseudosig_die(&msg, line)?;
9386                Err(StrykeError::die(msg, line))
9387            }
9388            Some(BuiltinId::Warn) => {
9389                let mut msg = String::new();
9390                for a in &args {
9391                    msg.push_str(&a.to_string());
9392                }
9393                if msg.is_empty() {
9394                    msg = "Warning: something's wrong".to_string();
9395                }
9396                if !msg.ends_with('\n') {
9397                    msg.push_str(&self.interp.die_warn_at_suffix(line));
9398                    msg.push('\n');
9399                }
9400                self.interp.fire_pseudosig_warn(&msg, line)?;
9401                Ok(StrykeValue::integer(1))
9402            }
9403            Some(BuiltinId::Exit) => {
9404                let code = args
9405                    .into_iter()
9406                    .next()
9407                    .map(|v| v.to_int() as i32)
9408                    .unwrap_or(0);
9409                Err(StrykeError::new(
9410                    ErrorKind::Exit(code),
9411                    "",
9412                    line,
9413                    &self.interp.file,
9414                ))
9415            }
9416            Some(BuiltinId::System) => {
9417                // Perl's `system`:
9418                //   - `system "cmd args"` (single string)  → `sh -c "cmd args"`
9419                //   - `system "cmd", "arg1", "arg2", ...`  → exec the program
9420                //     directly with the trailing args as argv (no shell).
9421                // Return value is the encoded `$?` status word (exit_code << 8
9422                // on a clean exit; raw signal number for signals), not the bare
9423                // exit code, so `$rc == 0` <=> clean success and bit-twiddles
9424                // like `($? >> 8)` work on the return value too.
9425                let strs: Vec<String> = args.iter().map(|a| a.to_string()).collect();
9426                if strs.is_empty() {
9427                    self.interp.child_exit_status = -1;
9428                    return Ok(StrykeValue::integer(-1));
9429                }
9430                let status = if strs.len() == 1 {
9431                    std::process::Command::new("sh")
9432                        .arg("-c")
9433                        .arg(&strs[0])
9434                        .status()
9435                } else {
9436                    std::process::Command::new(&strs[0])
9437                        .args(&strs[1..])
9438                        .status()
9439                };
9440                match status {
9441                    Ok(s) => {
9442                        self.interp.record_child_exit_status(s);
9443                        Ok(StrykeValue::integer(self.interp.child_exit_status))
9444                    }
9445                    Err(e) => {
9446                        self.interp.errno = e.to_string();
9447                        self.interp.child_exit_status = -1;
9448                        Ok(StrykeValue::integer(-1))
9449                    }
9450                }
9451            }
9452            Some(BuiltinId::Ssh) => self.interp.ssh_builtin_execute(&args),
9453            Some(BuiltinId::Chomp) => {
9454                // Chomp modifies the variable in-place — but in CallBuiltin we get the value, not a reference.
9455                // Return the number of chars removed (like Perl).
9456                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9457                let s = val.to_string();
9458                Ok(StrykeValue::integer(if s.ends_with('\n') { 1 } else { 0 }))
9459            }
9460            Some(BuiltinId::Chop) => {
9461                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9462                let s = val.to_string();
9463                Ok(s.chars()
9464                    .last()
9465                    .map(|c| StrykeValue::string(c.to_string()))
9466                    .unwrap_or(StrykeValue::UNDEF))
9467            }
9468            Some(BuiltinId::Substr) => {
9469                let s = args.first().map(|v| v.to_string()).unwrap_or_default();
9470                let slen = s.len() as i64;
9471                let off = args.get(1).map(|v| v.to_int()).unwrap_or(0);
9472                let start = if off < 0 { (slen + off).max(0) } else { off }.min(slen) as usize;
9473
9474                let end = if let Some(l_val) = args.get(2) {
9475                    let l = l_val.to_int();
9476                    if l < 0 {
9477                        (slen + l).max(start as i64)
9478                    } else {
9479                        (start as i64 + l).min(slen)
9480                    }
9481                } else {
9482                    slen
9483                } as usize;
9484
9485                Ok(StrykeValue::string(
9486                    s.get(start..end).unwrap_or("").to_string(),
9487                ))
9488            }
9489            Some(BuiltinId::Index) => {
9490                let s = args.first().map(|v| v.to_string()).unwrap_or_default();
9491                let sub = args.get(1).map(|v| v.to_string()).unwrap_or_default();
9492                // Perl: negative POS clamps to 0; POS past end returns -1
9493                // (or, for empty needle, returns POS clamped to len).
9494                let pos_raw = args.get(2).map(|v| v.to_int()).unwrap_or(0);
9495                let pos = if pos_raw < 0 {
9496                    0usize
9497                } else {
9498                    (pos_raw as usize).min(s.len())
9499                };
9500                Ok(StrykeValue::integer(
9501                    s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1),
9502                ))
9503            }
9504            Some(BuiltinId::Rindex) => {
9505                let s = args.first().map(|v| v.to_string()).unwrap_or_default();
9506                let sub = args.get(1).map(|v| v.to_string()).unwrap_or_default();
9507                // Perl: negative POS means "search must end at or before POS";
9508                // any negative value past -1 implies no possible match.
9509                let result = match args.get(2) {
9510                    Some(v) => {
9511                        let p = v.to_int();
9512                        if p < 0 {
9513                            -1
9514                        } else {
9515                            let end = (p as usize).saturating_add(sub.len()).min(s.len());
9516                            s[..end].rfind(&sub).map(|i| i as i64).unwrap_or(-1)
9517                        }
9518                    }
9519                    None => s.rfind(&sub).map(|i| i as i64).unwrap_or(-1),
9520                };
9521                Ok(StrykeValue::integer(result))
9522            }
9523            Some(BuiltinId::Ucfirst) => {
9524                let s = args
9525                    .into_iter()
9526                    .next()
9527                    .unwrap_or(StrykeValue::UNDEF)
9528                    .to_string();
9529                let mut chars = s.chars();
9530                let result = match chars.next() {
9531                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
9532                    None => String::new(),
9533                };
9534                Ok(StrykeValue::string(result))
9535            }
9536            Some(BuiltinId::Lcfirst) => {
9537                let s = args
9538                    .into_iter()
9539                    .next()
9540                    .unwrap_or(StrykeValue::UNDEF)
9541                    .to_string();
9542                let mut chars = s.chars();
9543                let result = match chars.next() {
9544                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
9545                    None => String::new(),
9546                };
9547                Ok(StrykeValue::string(result))
9548            }
9549            Some(BuiltinId::Splice) => self.interp.splice_builtin_execute(&args, line),
9550            Some(BuiltinId::Unshift) => self.interp.unshift_builtin_execute(&args, line),
9551            Some(BuiltinId::Printf) => {
9552                // Flatten list-context operands (ranges, arrays, `reverse`, …) so format
9553                // placeholders line up with individual values instead of an array reference.
9554                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
9555                for a in args.into_iter() {
9556                    if let Some(items) = a.as_array_vec() {
9557                        flat.extend(items);
9558                    } else {
9559                        flat.push(a);
9560                    }
9561                }
9562                let args = flat;
9563                let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
9564                    let s = match self
9565                        .interp
9566                        .stringify_value(self.interp.scope.get_scalar("_").clone(), line)
9567                    {
9568                        Ok(s) => s,
9569                        Err(FlowOrError::Error(e)) => return Err(e),
9570                        Err(FlowOrError::Flow(_)) => {
9571                            return Err(StrykeError::runtime(
9572                                "printf: unexpected control flow",
9573                                line,
9574                            ));
9575                        }
9576                    };
9577                    (s, &[])
9578                } else {
9579                    (args[0].to_string(), &args[1..])
9580                };
9581                let out = match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
9582                    Ok(s) => s,
9583                    Err(FlowOrError::Error(e)) => return Err(e),
9584                    Err(FlowOrError::Flow(_)) => {
9585                        return Err(StrykeError::runtime(
9586                            "printf: unexpected control flow",
9587                            line,
9588                        ));
9589                    }
9590                };
9591                print!("{}", out);
9592                if self.interp.output_autoflush {
9593                    let _ = io::stdout().flush();
9594                }
9595                Ok(StrykeValue::integer(1))
9596            }
9597            Some(BuiltinId::Open) => {
9598                if args.len() < 2 {
9599                    return Err(StrykeError::runtime(
9600                        "open requires at least 2 arguments",
9601                        line,
9602                    ));
9603                }
9604                let handle_name = args[0].to_string();
9605                let mode_s = args[1].to_string();
9606                let file_opt = args.get(2).map(|v| v.to_string());
9607                self.interp
9608                    .open_builtin_execute(handle_name, mode_s, file_opt, line)
9609            }
9610            Some(BuiltinId::Close) => {
9611                let name = args
9612                    .into_iter()
9613                    .next()
9614                    .unwrap_or(StrykeValue::UNDEF)
9615                    .to_string();
9616                self.interp.close_builtin_execute(name)
9617            }
9618            Some(BuiltinId::Eof) => self.interp.eof_builtin_execute(&args, line),
9619            Some(BuiltinId::ReadLine) => {
9620                let h = if args.is_empty() {
9621                    None
9622                } else {
9623                    Some(args[0].to_string())
9624                };
9625                self.interp.readline_builtin_execute(h.as_deref())
9626            }
9627            Some(BuiltinId::ReadLineList) => {
9628                let h = if args.is_empty() {
9629                    None
9630                } else {
9631                    Some(args[0].to_string())
9632                };
9633                self.interp.readline_builtin_execute_list(h.as_deref())
9634            }
9635            Some(BuiltinId::Exec) => {
9636                let cmd = args
9637                    .iter()
9638                    .map(|a| a.to_string())
9639                    .collect::<Vec<_>>()
9640                    .join(" ");
9641                let status = std::process::Command::new("sh")
9642                    .arg("-c")
9643                    .arg(&cmd)
9644                    .status();
9645                std::process::exit(status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1));
9646            }
9647            Some(BuiltinId::Chdir) => {
9648                let path = args
9649                    .into_iter()
9650                    .next()
9651                    .unwrap_or(StrykeValue::UNDEF)
9652                    .to_string();
9653                if std::env::set_current_dir(&path).is_ok() {
9654                    if let Ok(c) = std::env::current_dir() {
9655                        self.interp.stryke_pwd = std::fs::canonicalize(&c).unwrap_or(c);
9656                    }
9657                    Ok(StrykeValue::integer(1))
9658                } else {
9659                    Ok(StrykeValue::integer(0))
9660                }
9661            }
9662            Some(BuiltinId::Mkdir) => {
9663                let path = args.first().map(|v| v.to_string()).unwrap_or_default();
9664                let path = self.interp.resolve_stryke_path_string(&path);
9665                Ok(StrykeValue::integer(
9666                    if std::fs::create_dir(&path).is_ok() {
9667                        1
9668                    } else {
9669                        0
9670                    },
9671                ))
9672            }
9673            Some(BuiltinId::Unlink) => {
9674                let mut count = 0i64;
9675                for a in &args {
9676                    let p = self.interp.resolve_stryke_path_string(&a.to_string());
9677                    if std::fs::remove_file(&p).is_ok() {
9678                        count += 1;
9679                    }
9680                }
9681                Ok(StrykeValue::integer(count))
9682            }
9683            Some(BuiltinId::Rmdir) => self.interp.builtin_rmdir_execute(&args, line),
9684            Some(BuiltinId::Utime) => self.interp.builtin_utime_execute(&args, line),
9685            Some(BuiltinId::Umask) => self.interp.builtin_umask_execute(&args, line),
9686            Some(BuiltinId::Getcwd) => self.interp.builtin_getcwd_execute(&args, line),
9687            Some(BuiltinId::Pipe) => self.interp.builtin_pipe_execute(&args, line),
9688            Some(BuiltinId::Rename) => {
9689                let old = self.interp.resolve_stryke_path_string(
9690                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
9691                );
9692                let new = self.interp.resolve_stryke_path_string(
9693                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
9694                );
9695                Ok(crate::perl_fs::rename_paths(&old, &new))
9696            }
9697            Some(BuiltinId::Chmod) => {
9698                if args.is_empty() {
9699                    return Ok(StrykeValue::integer(0));
9700                }
9701                let mode = args[0].to_int();
9702                let paths: Vec<String> = args
9703                    .iter()
9704                    .skip(1)
9705                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9706                    .collect();
9707                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
9708                    &paths, mode,
9709                )))
9710            }
9711            Some(BuiltinId::Chown) => {
9712                if args.len() < 3 {
9713                    return Ok(StrykeValue::integer(0));
9714                }
9715                let uid = args[0].to_int();
9716                let gid = args[1].to_int();
9717                let paths: Vec<String> = args
9718                    .iter()
9719                    .skip(2)
9720                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9721                    .collect();
9722                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
9723                    &paths, uid, gid,
9724                )))
9725            }
9726            Some(BuiltinId::Stat) => {
9727                let path = self.interp.resolve_stryke_path_string(
9728                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
9729                );
9730                Ok(crate::perl_fs::stat_path(&path, false))
9731            }
9732            Some(BuiltinId::Lstat) => {
9733                let path = self.interp.resolve_stryke_path_string(
9734                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
9735                );
9736                Ok(crate::perl_fs::stat_path(&path, true))
9737            }
9738            Some(BuiltinId::Link) => {
9739                let old = self.interp.resolve_stryke_path_string(
9740                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
9741                );
9742                let new = self.interp.resolve_stryke_path_string(
9743                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
9744                );
9745                Ok(crate::perl_fs::link_hard(&old, &new))
9746            }
9747            Some(BuiltinId::Symlink) => {
9748                let old = args.first().map(|v| v.to_string()).unwrap_or_default();
9749                let new = self.interp.resolve_stryke_path_string(
9750                    &args.get(1).map(|v| v.to_string()).unwrap_or_default(),
9751                );
9752                Ok(crate::perl_fs::link_sym(&old, &new))
9753            }
9754            Some(BuiltinId::Readlink) => {
9755                let path = self.interp.resolve_stryke_path_string(
9756                    &args.first().map(|v| v.to_string()).unwrap_or_default(),
9757                );
9758                Ok(crate::perl_fs::read_link(&path))
9759            }
9760            Some(BuiltinId::Glob) => {
9761                // Pass user patterns through verbatim: zsh::glob runs from OS cwd,
9762                // which `chdir` keeps in sync with `stryke_pwd`. Absolutising the
9763                // pattern up front would turn relative-pattern results into
9764                // absolute paths (breaking `glob("**(/)")` → "sub" contract,
9765                // pinned in tests/suite/glob_zsh_qualifiers.rs).
9766                let pats: Vec<String> = args.iter().map(|v| v.to_string()).collect();
9767                Ok(crate::perl_fs::glob_patterns(&pats))
9768            }
9769            Some(BuiltinId::Files) => {
9770                let dir = if args.is_empty() {
9771                    self.interp.resolve_stryke_path_string(".")
9772                } else {
9773                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9774                };
9775                Ok(crate::perl_fs::list_files(&dir))
9776            }
9777            Some(BuiltinId::Filesf) => {
9778                let dir = if args.is_empty() {
9779                    self.interp.resolve_stryke_path_string(".")
9780                } else {
9781                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9782                };
9783                Ok(crate::perl_fs::list_filesf(&dir))
9784            }
9785            Some(BuiltinId::FilesfRecursive) => {
9786                let dir = if args.is_empty() {
9787                    self.interp.resolve_stryke_path_string(".")
9788                } else {
9789                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9790                };
9791                Ok(StrykeValue::iterator(std::sync::Arc::new(
9792                    crate::value::FsWalkIterator::new(&dir, true),
9793                )))
9794            }
9795            Some(BuiltinId::Dirs) => {
9796                let dir = if args.is_empty() {
9797                    self.interp.resolve_stryke_path_string(".")
9798                } else {
9799                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9800                };
9801                Ok(crate::perl_fs::list_dirs(&dir))
9802            }
9803            Some(BuiltinId::DirsRecursive) => {
9804                let dir = if args.is_empty() {
9805                    self.interp.resolve_stryke_path_string(".")
9806                } else {
9807                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9808                };
9809                Ok(StrykeValue::iterator(std::sync::Arc::new(
9810                    crate::value::FsWalkIterator::new(&dir, false),
9811                )))
9812            }
9813            Some(BuiltinId::SymLinks) => {
9814                let dir = if args.is_empty() {
9815                    self.interp.resolve_stryke_path_string(".")
9816                } else {
9817                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9818                };
9819                Ok(crate::perl_fs::list_sym_links(&dir))
9820            }
9821            Some(BuiltinId::Sockets) => {
9822                let dir = if args.is_empty() {
9823                    self.interp.resolve_stryke_path_string(".")
9824                } else {
9825                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9826                };
9827                Ok(crate::perl_fs::list_sockets(&dir))
9828            }
9829            Some(BuiltinId::Pipes) => {
9830                let dir = if args.is_empty() {
9831                    self.interp.resolve_stryke_path_string(".")
9832                } else {
9833                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9834                };
9835                Ok(crate::perl_fs::list_pipes(&dir))
9836            }
9837            Some(BuiltinId::BlockDevices) => {
9838                let dir = if args.is_empty() {
9839                    self.interp.resolve_stryke_path_string(".")
9840                } else {
9841                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9842                };
9843                Ok(crate::perl_fs::list_block_devices(&dir))
9844            }
9845            Some(BuiltinId::CharDevices) => {
9846                let dir = if args.is_empty() {
9847                    self.interp.resolve_stryke_path_string(".")
9848                } else {
9849                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9850                };
9851                Ok(crate::perl_fs::list_char_devices(&dir))
9852            }
9853            Some(BuiltinId::Executables) => {
9854                let dir = if args.is_empty() {
9855                    self.interp.resolve_stryke_path_string(".")
9856                } else {
9857                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9858                };
9859                Ok(crate::perl_fs::list_executables(&dir))
9860            }
9861            Some(BuiltinId::GlobPar) => {
9862                let pats: Vec<String> = args
9863                    .iter()
9864                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9865                    .collect();
9866                Ok(crate::perl_fs::glob_par_patterns(&pats))
9867            }
9868            Some(BuiltinId::GlobParProgress) => {
9869                let progress = args.last().map(|v| v.is_true()).unwrap_or(false);
9870                let pats: Vec<String> = args[..args.len().saturating_sub(1)]
9871                    .iter()
9872                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9873                    .collect();
9874                Ok(crate::perl_fs::glob_par_patterns_with_progress(
9875                    &pats, progress,
9876                ))
9877            }
9878            Some(BuiltinId::ParSed) => self.interp.builtin_par_sed(&args, line, false),
9879            Some(BuiltinId::ParSedProgress) => self.interp.builtin_par_sed(&args, line, true),
9880            Some(BuiltinId::Opendir) => {
9881                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9882                let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
9883                Ok(self.interp.opendir_handle(&handle, &path))
9884            }
9885            Some(BuiltinId::Readdir) => {
9886                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9887                Ok(self.interp.readdir_handle(&handle))
9888            }
9889            Some(BuiltinId::ReaddirList) => {
9890                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9891                Ok(self.interp.readdir_handle_list(&handle))
9892            }
9893            Some(BuiltinId::Closedir) => {
9894                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9895                Ok(self.interp.closedir_handle(&handle))
9896            }
9897            Some(BuiltinId::Rewinddir) => {
9898                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9899                Ok(self.interp.rewinddir_handle(&handle))
9900            }
9901            Some(BuiltinId::Telldir) => {
9902                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9903                Ok(self.interp.telldir_handle(&handle))
9904            }
9905            Some(BuiltinId::Seekdir) => {
9906                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9907                let pos = args.get(1).map(|v| v.to_int().max(0) as usize).unwrap_or(0);
9908                Ok(self.interp.seekdir_handle(&handle, pos))
9909            }
9910            Some(BuiltinId::Slurp) => {
9911                let path = args
9912                    .into_iter()
9913                    .next()
9914                    .unwrap_or(StrykeValue::UNDEF)
9915                    .to_string();
9916                let path = self.interp.resolve_stryke_path_string(&path);
9917                crate::perl_fs::read_file_text_or_glob(&path)
9918                    .map(StrykeValue::string)
9919                    .map_err(|e| StrykeError::runtime(format!("slurp: {}", e), line))
9920            }
9921            Some(BuiltinId::Capture) => {
9922                let cmd = args
9923                    .into_iter()
9924                    .next()
9925                    .unwrap_or(StrykeValue::UNDEF)
9926                    .to_string();
9927                crate::capture::run_capture(self.interp, &cmd, line)
9928            }
9929            Some(BuiltinId::Ppool) => {
9930                let n = args
9931                    .first()
9932                    .map(|v| v.to_int().max(0) as usize)
9933                    .unwrap_or(1);
9934                crate::ppool::create_pool(n)
9935            }
9936            Some(BuiltinId::Wantarray) => Ok(match self.interp.wantarray_kind {
9937                crate::vm_helper::WantarrayCtx::Void => StrykeValue::UNDEF,
9938                crate::vm_helper::WantarrayCtx::Scalar => StrykeValue::integer(0),
9939                crate::vm_helper::WantarrayCtx::List => StrykeValue::integer(1),
9940            }),
9941            Some(BuiltinId::FetchUrl) => {
9942                let url = args
9943                    .into_iter()
9944                    .next()
9945                    .unwrap_or(StrykeValue::UNDEF)
9946                    .to_string();
9947                ureq::get(&url)
9948                    .call()
9949                    .map_err(|e| StrykeError::runtime(format!("fetch_url: {}", e), line))
9950                    .and_then(|r| {
9951                        r.into_string()
9952                            .map(StrykeValue::string)
9953                            .map_err(|e| StrykeError::runtime(format!("fetch_url: {}", e), line))
9954                    })
9955            }
9956            Some(BuiltinId::Pchannel) => {
9957                if args.is_empty() {
9958                    Ok(crate::pchannel::create_pair())
9959                } else if args.len() == 1 {
9960                    let n = args[0].to_int().max(1) as usize;
9961                    Ok(crate::pchannel::create_bounded_pair(n))
9962                } else {
9963                    Err(StrykeError::runtime(
9964                        "pchannel() takes 0 or 1 arguments (capacity)",
9965                        line,
9966                    ))
9967                }
9968            }
9969            Some(BuiltinId::Pselect) => crate::pchannel::pselect_recv(&args, line),
9970            Some(BuiltinId::DequeNew) => {
9971                if !args.is_empty() {
9972                    return Err(StrykeError::runtime("deque() takes no arguments", line));
9973                }
9974                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
9975            }
9976            Some(BuiltinId::HeapNew) => {
9977                if args.len() != 1 {
9978                    return Err(StrykeError::runtime(
9979                        "heap() expects one comparator sub",
9980                        line,
9981                    ));
9982                }
9983                let a0 = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9984                if let Some(sub) = a0.as_code_ref() {
9985                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
9986                        items: Vec::new(),
9987                        cmp: Arc::clone(&sub),
9988                    }))))
9989                } else {
9990                    Err(StrykeError::runtime(
9991                        "heap() requires a code reference",
9992                        line,
9993                    ))
9994                }
9995            }
9996            Some(BuiltinId::BarrierNew) => {
9997                let n = args
9998                    .first()
9999                    .map(|v| v.to_int().max(1) as usize)
10000                    .unwrap_or(1);
10001                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
10002            }
10003            Some(BuiltinId::ClusterNew) => {
10004                // `cluster(HOST...)` — accepts one operand (flattened) or
10005                // multiple (each is a slot spec). Same surface as the
10006                // tree-walker arm in `vm_helper.rs` `call_named_sub`'s
10007                // "cluster" case so `pmap_on` / `~d>` see identical
10008                // `RemoteCluster` values from either dispatch path.
10009                let items = if args.len() == 1 {
10010                    args[0].to_list()
10011                } else {
10012                    args.clone()
10013                };
10014                let c = crate::value::RemoteCluster::from_list_args(&items)
10015                    .map_err(|msg| StrykeError::runtime(msg, line))?;
10016                Ok(StrykeValue::remote_cluster(std::sync::Arc::new(c)))
10017            }
10018            Some(BuiltinId::Pipeline) => {
10019                let mut items = Vec::new();
10020                for v in args {
10021                    if let Some(a) = v.as_array_vec() {
10022                        items.extend(a);
10023                    } else {
10024                        items.push(v);
10025                    }
10026                }
10027                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
10028                    source: items,
10029                    ops: Vec::new(),
10030                    has_scalar_terminal: false,
10031                    par_stream: false,
10032                    streaming: false,
10033                    streaming_workers: 0,
10034                    streaming_buffer: 256,
10035                }))))
10036            }
10037            Some(BuiltinId::ParPipeline) => {
10038                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
10039                    return crate::par_pipeline::run_par_pipeline(self.interp, &args, line);
10040                }
10041                let mut items = Vec::new();
10042                for v in args {
10043                    if let Some(a) = v.as_array_vec() {
10044                        items.extend(a);
10045                    } else {
10046                        items.push(v);
10047                    }
10048                }
10049                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
10050                    source: items,
10051                    ops: Vec::new(),
10052                    has_scalar_terminal: false,
10053                    par_stream: true,
10054                    streaming: false,
10055                    streaming_workers: 0,
10056                    streaming_buffer: 256,
10057                }))))
10058            }
10059            Some(BuiltinId::ParPipelineStream) => {
10060                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
10061                    return crate::par_pipeline::run_par_pipeline_streaming(
10062                        self.interp,
10063                        &args,
10064                        line,
10065                    );
10066                }
10067                self.interp.builtin_par_pipeline_stream_new(&args, line)
10068            }
10069            Some(BuiltinId::Each) => {
10070                let _arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10071                Ok(StrykeValue::array(vec![]))
10072            }
10073            Some(BuiltinId::Readpipe) => {
10074                let cmd = args
10075                    .into_iter()
10076                    .next()
10077                    .unwrap_or(StrykeValue::UNDEF)
10078                    .to_string();
10079                crate::capture::run_readpipe(self.interp, &cmd, line)
10080            }
10081            Some(BuiltinId::ReadpipeList) => {
10082                let cmd = args
10083                    .into_iter()
10084                    .next()
10085                    .unwrap_or(StrykeValue::UNDEF)
10086                    .to_string();
10087                let v = crate::capture::run_readpipe(self.interp, &cmd, line)?;
10088                let s = v.to_string();
10089                if s.is_empty() {
10090                    return Ok(StrykeValue::array(Vec::new()));
10091                }
10092                let mut lines = Vec::new();
10093                let mut buf = String::new();
10094                for c in s.chars() {
10095                    buf.push(c);
10096                    if c == '\n' {
10097                        lines.push(StrykeValue::string(std::mem::take(&mut buf)));
10098                    }
10099                }
10100                if !buf.is_empty() {
10101                    lines.push(StrykeValue::string(buf));
10102                }
10103                Ok(StrykeValue::array(lines))
10104            }
10105            Some(BuiltinId::Eval) => {
10106                let arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
10107                self.interp.eval_nesting += 1;
10108                let out = if let Some(sub) = arg.as_code_ref() {
10109                    match self.interp.exec_block(&sub.body) {
10110                        Ok(v) => {
10111                            self.interp.clear_eval_error();
10112                            Ok(v)
10113                        }
10114                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
10115                            self.interp.set_eval_error_from_perl_error(&e);
10116                            Ok(StrykeValue::UNDEF)
10117                        }
10118                        Err(crate::vm_helper::FlowOrError::Flow(_)) => {
10119                            self.interp.clear_eval_error();
10120                            Ok(StrykeValue::UNDEF)
10121                        }
10122                    }
10123                } else {
10124                    let code = arg.to_string();
10125                    match crate::parse_and_run_string(&code, self.interp) {
10126                        Ok(v) => {
10127                            self.interp.clear_eval_error();
10128                            Ok(v)
10129                        }
10130                        Err(e) => {
10131                            self.interp.set_eval_error_from_perl_error(&e);
10132                            Ok(StrykeValue::UNDEF)
10133                        }
10134                    }
10135                };
10136                self.interp.eval_nesting -= 1;
10137                out
10138            }
10139            Some(BuiltinId::Do) => {
10140                let filename = args
10141                    .into_iter()
10142                    .next()
10143                    .unwrap_or(StrykeValue::UNDEF)
10144                    .to_string();
10145                match read_file_text_perl_compat(&filename) {
10146                    Ok(code) => {
10147                        let code = crate::data_section::strip_perl_end_marker(&code);
10148                        crate::parse_and_run_string_in_file(code, self.interp, &filename)
10149                            .or(Ok(StrykeValue::UNDEF))
10150                    }
10151                    Err(_) => Ok(StrykeValue::UNDEF),
10152                }
10153            }
10154            Some(BuiltinId::Require) => {
10155                let name = args
10156                    .into_iter()
10157                    .next()
10158                    .unwrap_or(StrykeValue::UNDEF)
10159                    .to_string();
10160                self.interp.require_execute(&name, line)
10161            }
10162            Some(BuiltinId::Bless) => {
10163                let ref_val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
10164                let class = args
10165                    .get(1)
10166                    .map(|v| v.to_string())
10167                    .unwrap_or_else(|| self.interp.scope.get_scalar("__PACKAGE__").to_string());
10168                Ok(StrykeValue::blessed(Arc::new(
10169                    crate::value::BlessedRef::new_blessed(class, ref_val),
10170                )))
10171            }
10172            Some(BuiltinId::Caller) => {
10173                // Simplified caller frame: (package, file, line, subname).
10174                // The sub name is the fully-qualified name of the currently
10175                // executing sub so logger / decorator patterns work.
10176                let sub_name = self
10177                    .interp
10178                    .current_sub_stack
10179                    .last()
10180                    .map(|s| StrykeValue::string(s.name.clone()))
10181                    .unwrap_or(StrykeValue::UNDEF);
10182                let pkg = self.interp.current_package();
10183                Ok(StrykeValue::array(vec![
10184                    StrykeValue::string(pkg),
10185                    StrykeValue::string(self.interp.file.clone()),
10186                    StrykeValue::integer(line as i64),
10187                    sub_name,
10188                ]))
10189            }
10190            // Parallel ops (shouldn't reach here — handled by block ops)
10191            Some(BuiltinId::PMap)
10192            | Some(BuiltinId::PGrep)
10193            | Some(BuiltinId::PFor)
10194            | Some(BuiltinId::PSort)
10195            | Some(BuiltinId::Fan)
10196            | Some(BuiltinId::MapBlock)
10197            | Some(BuiltinId::GrepBlock)
10198            | Some(BuiltinId::SortBlock)
10199            | Some(BuiltinId::Sort) => Ok(StrykeValue::UNDEF),
10200            _ => Err(StrykeError::runtime(
10201                format!("Unimplemented builtin {:?}", bid),
10202                line,
10203            )),
10204        }
10205    }
10206}
10207
10208/// Integer fast-path comparison helper.
10209#[inline]
10210/// True when both values are non-numeric strings — used by `==` / `!=` in
10211/// stryke non-compat mode to decide whether to fall back to string compare.
10212/// "Numeric string" matches `looks_like_number` semantics (digits, optional
10213/// sign, optional decimal/exponent). Non-string values (refs, undef) are
10214/// excluded so `==` on objects keeps its overload-driven behavior.
10215fn both_non_numeric_strings(a: &StrykeValue, b: &StrykeValue) -> bool {
10216    if !a.is_string_like() || !b.is_string_like() {
10217        return false;
10218    }
10219    let sa = a.to_string();
10220    let sb = b.to_string();
10221    !looks_numeric(&sa) && !looks_numeric(&sb)
10222}
10223
10224#[inline]
10225fn looks_numeric(s: &str) -> bool {
10226    let t = s.trim();
10227    if t.is_empty() {
10228        return false;
10229    }
10230    t.parse::<f64>().is_ok()
10231}
10232
10233fn int_cmp(
10234    a: &StrykeValue,
10235    b: &StrykeValue,
10236    int_op: fn(&i64, &i64) -> bool,
10237    float_op: fn(f64, f64) -> bool,
10238) -> StrykeValue {
10239    if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
10240        StrykeValue::integer(if int_op(&x, &y) { 1 } else { 0 })
10241    } else {
10242        StrykeValue::integer(if float_op(a.to_number(), b.to_number()) {
10243            1
10244        } else {
10245            0
10246        })
10247    }
10248}
10249
10250/// Block JIT hook: string concat with `use overload` / `""` stringify (matches [`Op::Concat`]).
10251///
10252/// # Safety
10253///
10254/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call.
10255#[no_mangle]
10256pub unsafe extern "C" fn stryke_jit_concat_vm(vm: *mut std::ffi::c_void, a: i64, b: i64) -> i64 {
10257    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
10258    let pa = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(a));
10259    let pb = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(b));
10260    match vm.concat_stack_values(pa, pb) {
10261        Ok(pv) => pv.raw_bits() as i64,
10262        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
10263    }
10264}
10265
10266/// Cranelift host hook: re-enter the VM for [`Op::Call`] to a compiled sub (stack-args, scalar `i64` args).
10267/// `sub_ip`, `argc`, `wa` are passed as `i64` for a uniform Cranelift signature.
10268///
10269/// # Safety
10270///
10271/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call (JIT only
10272/// invokes this while the VM is executing).
10273#[no_mangle]
10274pub unsafe extern "C" fn stryke_jit_call_sub(
10275    vm: *mut std::ffi::c_void,
10276    sub_ip: i64,
10277    argc: i64,
10278    wa: i64,
10279    a0: i64,
10280    a1: i64,
10281    a2: i64,
10282    a3: i64,
10283    a4: i64,
10284    a5: i64,
10285    a6: i64,
10286    a7: i64,
10287) -> i64 {
10288    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
10289    let want = WantarrayCtx::from_byte(wa as u8);
10290    if want != WantarrayCtx::Scalar {
10291        return StrykeValue::UNDEF.raw_bits() as i64;
10292    }
10293    let argc = argc.clamp(0, 8) as usize;
10294    let args = [a0, a1, a2, a3, a4, a5, a6, a7];
10295    let args = &args[..argc];
10296    match vm.jit_trampoline_run_sub(sub_ip as usize, want, args) {
10297        Ok(pv) => {
10298            if let Some(n) = pv.as_integer() {
10299                n
10300            } else {
10301                pv.raw_bits() as i64
10302            }
10303        }
10304        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
10305    }
10306}
10307
10308#[cfg(test)]
10309mod tests {
10310    use super::*;
10311    use crate::bytecode::{Chunk, Op};
10312    use crate::value::StrykeValue;
10313
10314    fn run_chunk(chunk: &Chunk) -> StrykeResult<StrykeValue> {
10315        let mut interp = VMHelper::new();
10316        let mut vm = VM::new(chunk, &mut interp);
10317        vm.execute()
10318    }
10319
10320    /// Block-JIT-eligible loop: `for ($i=0; $i<limit; $i++) { $sum += $i }` — sum 0..limit-1.
10321    fn block_jit_sum_chunk(limit: i64) -> Chunk {
10322        let mut c = Chunk::new();
10323        let ni = c.intern_name("i");
10324        let ns = c.intern_name("sum");
10325        c.emit(Op::LoadInt(0), 1);
10326        c.emit(Op::DeclareScalarSlot(0, ni), 1);
10327        c.emit(Op::LoadInt(0), 1);
10328        c.emit(Op::DeclareScalarSlot(1, ns), 1);
10329        c.emit(Op::GetScalarSlot(0), 1);
10330        c.emit(Op::LoadInt(limit), 1);
10331        c.emit(Op::NumLt, 1);
10332        c.emit(Op::JumpIfFalse(15), 1);
10333        c.emit(Op::GetScalarSlot(1), 1);
10334        c.emit(Op::GetScalarSlot(0), 1);
10335        c.emit(Op::Add, 1);
10336        c.emit(Op::SetScalarSlot(1), 1);
10337        c.emit(Op::PostIncSlot(0), 1);
10338        c.emit(Op::Pop, 1);
10339        c.emit(Op::Jump(4), 1);
10340        c.emit(Op::GetScalarSlot(1), 1);
10341        c.emit(Op::Halt, 1);
10342        c
10343    }
10344
10345    #[test]
10346    fn jit_disabled_same_result_as_jit_block_loop() {
10347        let limit = 500i64;
10348        let chunk = block_jit_sum_chunk(limit);
10349        let expect = limit * (limit - 1) / 2;
10350
10351        let mut interp_on = VMHelper::new();
10352        let mut vm_on = VM::new(&chunk, &mut interp_on);
10353        assert_eq!(vm_on.execute().expect("vm").to_int(), expect);
10354
10355        let mut interp_off = VMHelper::new();
10356        let mut vm_off = VM::new(&chunk, &mut interp_off);
10357        vm_off.set_jit_enabled(false);
10358        assert_eq!(vm_off.execute().expect("vm").to_int(), expect);
10359    }
10360
10361    #[test]
10362    fn vm_add_two_integers() {
10363        let mut c = Chunk::new();
10364        c.emit(Op::LoadInt(2), 1);
10365        c.emit(Op::LoadInt(3), 1);
10366        c.emit(Op::Add, 1);
10367        c.emit(Op::Halt, 1);
10368        let v = run_chunk(&c).expect("vm");
10369        assert_eq!(v.to_int(), 5);
10370    }
10371
10372    #[test]
10373    fn vm_sub_mul_div() {
10374        let mut c = Chunk::new();
10375        c.emit(Op::LoadInt(10), 1);
10376        c.emit(Op::LoadInt(3), 1);
10377        c.emit(Op::Sub, 1);
10378        c.emit(Op::Halt, 1);
10379        assert_eq!(run_chunk(&c).expect("vm").to_int(), 7);
10380
10381        let mut c = Chunk::new();
10382        c.emit(Op::LoadInt(6), 1);
10383        c.emit(Op::LoadInt(7), 1);
10384        c.emit(Op::Mul, 1);
10385        c.emit(Op::Halt, 1);
10386        assert_eq!(run_chunk(&c).expect("vm").to_int(), 42);
10387
10388        let mut c = Chunk::new();
10389        c.emit(Op::LoadInt(20), 1);
10390        c.emit(Op::LoadInt(4), 1);
10391        c.emit(Op::Div, 1);
10392        c.emit(Op::Halt, 1);
10393        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
10394    }
10395
10396    #[test]
10397    fn vm_mod_and_pow() {
10398        let mut c = Chunk::new();
10399        c.emit(Op::LoadInt(17), 1);
10400        c.emit(Op::LoadInt(5), 1);
10401        c.emit(Op::Mod, 1);
10402        c.emit(Op::Halt, 1);
10403        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10404
10405        let mut c = Chunk::new();
10406        c.emit(Op::LoadInt(2), 1);
10407        c.emit(Op::LoadInt(3), 1);
10408        c.emit(Op::Pow, 1);
10409        c.emit(Op::Halt, 1);
10410        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
10411    }
10412
10413    #[test]
10414    fn vm_negate() {
10415        let mut c = Chunk::new();
10416        c.emit(Op::LoadInt(7), 1);
10417        c.emit(Op::Negate, 1);
10418        c.emit(Op::Halt, 1);
10419        assert_eq!(run_chunk(&c).expect("vm").to_int(), -7);
10420    }
10421
10422    #[test]
10423    fn vm_dup_and_pop() {
10424        let mut c = Chunk::new();
10425        c.emit(Op::LoadInt(1), 1);
10426        c.emit(Op::Dup, 1);
10427        c.emit(Op::Add, 1);
10428        c.emit(Op::Halt, 1);
10429        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10430
10431        let mut c = Chunk::new();
10432        c.emit(Op::LoadInt(1), 1);
10433        c.emit(Op::LoadInt(2), 1);
10434        c.emit(Op::Pop, 1);
10435        c.emit(Op::Halt, 1);
10436        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10437    }
10438
10439    #[test]
10440    fn vm_set_get_scalar() {
10441        let mut c = Chunk::new();
10442        let i = c.intern_name("v");
10443        c.emit(Op::LoadInt(99), 1);
10444        c.emit(Op::SetScalar(i), 1);
10445        c.emit(Op::GetScalar(i), 1);
10446        c.emit(Op::Halt, 1);
10447        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
10448    }
10449
10450    #[test]
10451    fn vm_scalar_plain_roundtrip_and_keep() {
10452        let mut c = Chunk::new();
10453        let i = c.intern_name("plainvar");
10454        c.emit(Op::LoadInt(99), 1);
10455        c.emit(Op::SetScalarPlain(i), 1);
10456        c.emit(Op::GetScalarPlain(i), 1);
10457        c.emit(Op::Halt, 1);
10458        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
10459
10460        let mut c = Chunk::new();
10461        let k = c.intern_name("keepme");
10462        c.emit(Op::LoadInt(5), 1);
10463        c.emit(Op::SetScalarKeepPlain(k), 1);
10464        c.emit(Op::Halt, 1);
10465        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
10466    }
10467
10468    #[test]
10469    fn vm_get_scalar_plain_skips_special_global_zero() {
10470        let mut c = Chunk::new();
10471        let idx = c.intern_name("0");
10472        c.emit(Op::GetScalar(idx), 1);
10473        c.emit(Op::Halt, 1);
10474        assert_eq!(run_chunk(&c).expect("vm").to_string(), "stryke");
10475
10476        let mut c = Chunk::new();
10477        let idx = c.intern_name("0");
10478        c.emit(Op::GetScalarPlain(idx), 1);
10479        c.emit(Op::Halt, 1);
10480        assert!(run_chunk(&c).expect("vm").is_undef());
10481    }
10482
10483    #[test]
10484    fn vm_slot_pre_post_inc_dec() {
10485        let mut c = Chunk::new();
10486        c.emit(Op::LoadInt(10), 1);
10487        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10488        c.emit(Op::PostIncSlot(0), 1);
10489        c.emit(Op::Pop, 1);
10490        c.emit(Op::GetScalarSlot(0), 1);
10491        c.emit(Op::Halt, 1);
10492        assert_eq!(run_chunk(&c).expect("vm").to_int(), 11);
10493
10494        let mut c = Chunk::new();
10495        c.emit(Op::LoadInt(0), 1);
10496        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10497        c.emit(Op::PreIncSlot(0), 1);
10498        c.emit(Op::Halt, 1);
10499        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10500
10501        let mut c = Chunk::new();
10502        c.emit(Op::LoadInt(5), 1);
10503        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10504        c.emit(Op::PreDecSlot(0), 1);
10505        c.emit(Op::Halt, 1);
10506        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
10507
10508        let mut c = Chunk::new();
10509        c.emit(Op::LoadInt(3), 1);
10510        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10511        c.emit(Op::PostDecSlot(0), 1);
10512        c.emit(Op::Pop, 1);
10513        c.emit(Op::GetScalarSlot(0), 1);
10514        c.emit(Op::Halt, 1);
10515        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10516    }
10517
10518    #[test]
10519    fn vm_str_eq_ne_heap_strings() {
10520        let mut c = Chunk::new();
10521        let a = c.add_constant(StrykeValue::string("same".into()));
10522        let b = c.add_constant(StrykeValue::string("same".into()));
10523        c.emit(Op::LoadConst(a), 1);
10524        c.emit(Op::LoadConst(b), 1);
10525        c.emit(Op::StrEq, 1);
10526        c.emit(Op::Halt, 1);
10527        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10528
10529        let mut c = Chunk::new();
10530        let a = c.add_constant(StrykeValue::string("a".into()));
10531        let b = c.add_constant(StrykeValue::string("b".into()));
10532        c.emit(Op::LoadConst(a), 1);
10533        c.emit(Op::LoadConst(b), 1);
10534        c.emit(Op::StrNe, 1);
10535        c.emit(Op::Halt, 1);
10536        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10537    }
10538
10539    #[test]
10540    fn vm_num_eq_ine() {
10541        let mut c = Chunk::new();
10542        c.emit(Op::LoadInt(1), 1);
10543        c.emit(Op::LoadInt(1), 1);
10544        c.emit(Op::NumEq, 1);
10545        c.emit(Op::Halt, 1);
10546        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10547
10548        let mut c = Chunk::new();
10549        c.emit(Op::LoadInt(1), 1);
10550        c.emit(Op::LoadInt(2), 1);
10551        c.emit(Op::NumNe, 1);
10552        c.emit(Op::Halt, 1);
10553        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10554    }
10555
10556    #[test]
10557    fn vm_num_ordering() {
10558        for (a, b, op, want) in [
10559            (1i64, 2i64, Op::NumLt, 1),
10560            (3i64, 2i64, Op::NumGt, 1),
10561            (2i64, 2i64, Op::NumLe, 1),
10562            (2i64, 2i64, Op::NumGe, 1),
10563        ] {
10564            let mut c = Chunk::new();
10565            c.emit(Op::LoadInt(a), 1);
10566            c.emit(Op::LoadInt(b), 1);
10567            c.emit(op, 1);
10568            c.emit(Op::Halt, 1);
10569            assert_eq!(run_chunk(&c).expect("vm").to_int(), want);
10570        }
10571    }
10572
10573    #[test]
10574    fn vm_concat_and_str_cmp() {
10575        let mut c = Chunk::new();
10576        let i1 = c.add_constant(StrykeValue::string("a".into()));
10577        let i2 = c.add_constant(StrykeValue::string("b".into()));
10578        c.emit(Op::LoadConst(i1), 1);
10579        c.emit(Op::LoadConst(i2), 1);
10580        c.emit(Op::Concat, 1);
10581        c.emit(Op::Halt, 1);
10582        assert_eq!(run_chunk(&c).expect("vm").to_string(), "ab");
10583
10584        let mut c = Chunk::new();
10585        let i1 = c.add_constant(StrykeValue::string("a".into()));
10586        let i2 = c.add_constant(StrykeValue::string("b".into()));
10587        c.emit(Op::LoadConst(i1), 1);
10588        c.emit(Op::LoadConst(i2), 1);
10589        c.emit(Op::StrCmp, 1);
10590        c.emit(Op::Halt, 1);
10591        let v = run_chunk(&c).expect("vm");
10592        assert!(v.to_int() < 0);
10593    }
10594
10595    #[test]
10596    fn vm_log_not() {
10597        let mut c = Chunk::new();
10598        c.emit(Op::LoadInt(0), 1);
10599        c.emit(Op::LogNot, 1);
10600        c.emit(Op::Halt, 1);
10601        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10602    }
10603
10604    #[test]
10605    fn vm_bit_and_or_xor_not() {
10606        let mut c = Chunk::new();
10607        c.emit(Op::LoadInt(0b1100), 1);
10608        c.emit(Op::LoadInt(0b1010), 1);
10609        c.emit(Op::BitAnd, 1);
10610        c.emit(Op::Halt, 1);
10611        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1000);
10612
10613        let mut c = Chunk::new();
10614        c.emit(Op::LoadInt(0b1100), 1);
10615        c.emit(Op::LoadInt(0b1010), 1);
10616        c.emit(Op::BitOr, 1);
10617        c.emit(Op::Halt, 1);
10618        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1110);
10619
10620        let mut c = Chunk::new();
10621        c.emit(Op::LoadInt(0b1100), 1);
10622        c.emit(Op::LoadInt(0b1010), 1);
10623        c.emit(Op::BitXor, 1);
10624        c.emit(Op::Halt, 1);
10625        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b0110);
10626
10627        let mut c = Chunk::new();
10628        c.emit(Op::LoadInt(0), 1);
10629        c.emit(Op::BitNot, 1);
10630        c.emit(Op::Halt, 1);
10631        assert!((run_chunk(&c).expect("vm").to_int() & 0xFF) != 0);
10632    }
10633
10634    #[test]
10635    fn vm_shl_shr() {
10636        let mut c = Chunk::new();
10637        c.emit(Op::LoadInt(1), 1);
10638        c.emit(Op::LoadInt(3), 1);
10639        c.emit(Op::Shl, 1);
10640        c.emit(Op::Halt, 1);
10641        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
10642
10643        let mut c = Chunk::new();
10644        c.emit(Op::LoadInt(16), 1);
10645        c.emit(Op::LoadInt(2), 1);
10646        c.emit(Op::Shr, 1);
10647        c.emit(Op::Halt, 1);
10648        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
10649    }
10650
10651    #[test]
10652    fn vm_load_undef_float_constant() {
10653        let mut c = Chunk::new();
10654        c.emit(Op::LoadUndef, 1);
10655        c.emit(Op::Halt, 1);
10656        assert!(run_chunk(&c).expect("vm").is_undef());
10657
10658        let mut c = Chunk::new();
10659        c.emit(Op::LoadFloat(2.5), 1);
10660        c.emit(Op::Halt, 1);
10661        assert!((run_chunk(&c).expect("vm").to_number() - 2.5).abs() < 1e-9);
10662    }
10663
10664    #[test]
10665    fn vm_jump_skips_ops() {
10666        let mut c = Chunk::new();
10667        let j = c.emit(Op::Jump(0), 1);
10668        c.emit(Op::LoadInt(1), 1);
10669        c.emit(Op::LoadInt(2), 1);
10670        c.emit(Op::Add, 1);
10671        c.patch_jump_here(j);
10672        c.emit(Op::LoadInt(40), 1);
10673        c.emit(Op::Halt, 1);
10674        assert_eq!(run_chunk(&c).expect("vm").to_int(), 40);
10675    }
10676
10677    #[test]
10678    fn vm_jump_if_false() {
10679        let mut c = Chunk::new();
10680        c.emit(Op::LoadInt(0), 1);
10681        let j = c.emit(Op::JumpIfFalse(0), 1);
10682        c.emit(Op::LoadInt(1), 1);
10683        c.emit(Op::Halt, 1);
10684        c.patch_jump_here(j);
10685        c.emit(Op::LoadInt(2), 1);
10686        c.emit(Op::Halt, 1);
10687        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10688    }
10689
10690    #[test]
10691    fn vm_call_builtin_defined() {
10692        let mut c = Chunk::new();
10693        c.emit(Op::LoadUndef, 1);
10694        c.emit(Op::CallBuiltin(BuiltinId::Defined as u16, 1), 1);
10695        c.emit(Op::Halt, 1);
10696        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0);
10697    }
10698
10699    #[test]
10700    fn vm_call_builtin_length_string() {
10701        let mut c = Chunk::new();
10702        let idx = c.add_constant(StrykeValue::string("abc".into()));
10703        c.emit(Op::LoadConst(idx), 1);
10704        c.emit(Op::CallBuiltin(BuiltinId::Length as u16, 1), 1);
10705        c.emit(Op::Halt, 1);
10706        assert_eq!(run_chunk(&c).expect("vm").to_int(), 3);
10707    }
10708
10709    #[test]
10710    fn vm_make_array_two() {
10711        let mut c = Chunk::new();
10712        c.emit(Op::LoadInt(1), 1);
10713        c.emit(Op::LoadInt(2), 1);
10714        c.emit(Op::MakeArray(2), 1);
10715        c.emit(Op::Halt, 1);
10716        let v = run_chunk(&c).expect("vm");
10717        let a = v.as_array_vec().expect("array");
10718        assert_eq!(a.len(), 2);
10719        assert_eq!(a[0].to_int(), 1);
10720        assert_eq!(a[1].to_int(), 2);
10721    }
10722
10723    #[test]
10724    fn vm_spaceship() {
10725        let mut c = Chunk::new();
10726        c.emit(Op::LoadInt(1), 1);
10727        c.emit(Op::LoadInt(2), 1);
10728        c.emit(Op::Spaceship, 1);
10729        c.emit(Op::Halt, 1);
10730        assert_eq!(run_chunk(&c).expect("vm").to_int(), -1);
10731    }
10732
10733    #[test]
10734    fn compiled_try_catch_catches_die_via_vm() {
10735        let program = crate::parse(
10736            r#"
10737        try {
10738            die "boom";
10739        } catch ($err) {
10740            42;
10741        }
10742    "#,
10743        )
10744        .expect("parse");
10745        let chunk = crate::compiler::Compiler::new()
10746            .compile_program(&program)
10747            .expect("compile");
10748        let tp = chunk
10749            .ops
10750            .iter()
10751            .position(|o| matches!(o, Op::TryPush { .. }))
10752            .expect("TryPush op");
10753        match &chunk.ops[tp] {
10754            Op::TryPush {
10755                catch_ip, after_ip, ..
10756            } => {
10757                assert_ne!(*catch_ip, 0, "catch_ip must be patched");
10758                assert_ne!(*after_ip, 0, "after_ip must be patched");
10759            }
10760            _ => unreachable!(),
10761        }
10762        let mut interp = VMHelper::new();
10763        let mut vm = VM::new(&chunk, &mut interp);
10764        vm.set_jit_enabled(false);
10765        let v = vm.execute().expect("vm should catch die");
10766        assert_eq!(v.to_int(), 42);
10767    }
10768}