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, PerlError, PerlResult};
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, PerlAsyncTask, PerlBarrier, PerlHeap, PerlSub, PipelineInner,
20    PipelineOp, 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<PerlSub>>>,
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) -> PerlResult<StrykeValue> {
228    match r {
229        Ok(v) => Ok(v),
230        Err(FlowOrError::Error(e)) => Err(e),
231        Err(FlowOrError::Flow(_)) => Err(PerlError::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)]
241pub(crate) struct TryFrame {
242    pub(crate) try_push_op_idx: usize,
243}
244
245/// Saved state when entering a function call.
246#[derive(Debug)]
247struct CallFrame {
248    return_ip: usize,
249    stack_base: usize,
250    scope_depth: usize,
251    saved_wantarray: WantarrayCtx,
252    /// [`stryke_jit_call_sub`] — no bytecode resume; result stored in [`VM::jit_trampoline_out`].
253    jit_trampoline_return: bool,
254    /// Synthetic frame for [`Op::BlockReturnValue`] (`map`/`grep`/`sort` block bytecode), paired with
255    /// `scope_push_hook` at [`VM::run_block_region`] entry (not a sub call; no closure capture).
256    block_region: bool,
257    /// Wall-clock start for [`crate::profiler::Profiler::exit_sub`] (paired with `enter_sub` on `Call`).
258    sub_profiler_start: Option<std::time::Instant>,
259}
260
261/// Stack-based bytecode virtual machine.
262pub struct VM<'a> {
263    /// Shared with parallel workers via [`Self::new_parallel_worker`] (cheap `Arc` clones).
264    names: Arc<Vec<String>>,
265    constants: Arc<Vec<StrykeValue>>,
266    ops: Arc<Vec<Op>>,
267    lines: Arc<Vec<usize>>,
268    sub_entries: Vec<(u16, usize, bool)>,
269    /// See [`Chunk::static_sub_calls`] (`Op::CallStaticSubId`).
270    static_sub_calls: Vec<(usize, bool, u16)>,
271    blocks: Vec<Block>,
272    code_ref_sigs: Vec<Vec<SubSigParam>>,
273    /// Optional `ops[start..end]` lowering for [`Self::blocks`] (see [`Chunk::block_bytecode_ranges`]).
274    block_bytecode_ranges: Vec<Option<(usize, usize)>>,
275    /// Optional lowering for [`Chunk::map_expr_entries`] (see [`Chunk::map_expr_bytecode_ranges`]).
276    map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
277    /// Optional lowering for [`Chunk::grep_expr_entries`] (see [`Chunk::grep_expr_bytecode_ranges`]).
278    grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
279    given_entries: Vec<(Expr, Block)>,
280    given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
281    eval_timeout_entries: Vec<(Expr, Block)>,
282    eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
283    algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
284    algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
285    par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
286    par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
287    pwatch_entries: Vec<(Expr, Expr)>,
288    substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
289    keys_expr_entries: Vec<Expr>,
290    keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
291    map_expr_entries: Vec<Expr>,
292    grep_expr_entries: Vec<Expr>,
293    regex_flip_flop_rhs_expr_entries: Vec<Expr>,
294    regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
295    values_expr_entries: Vec<Expr>,
296    values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
297    delete_expr_entries: Vec<Expr>,
298    exists_expr_entries: Vec<Expr>,
299    push_expr_entries: Vec<(Expr, Vec<Expr>)>,
300    pop_expr_entries: Vec<Expr>,
301    shift_expr_entries: Vec<Expr>,
302    unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
303    splice_expr_entries: Vec<SpliceExprEntry>,
304    lvalues: Vec<Expr>,
305    ast_eval_exprs: Vec<Expr>,
306    format_decls: Vec<(String, Vec<String>)>,
307    use_overload_entries: Vec<Vec<(String, String)>>,
308    runtime_sub_decls: Arc<Vec<RuntimeSubDecl>>,
309    runtime_advice_decls: Arc<Vec<crate::bytecode::RuntimeAdviceDecl>>,
310    pub(crate) ip: usize,
311    stack: Vec<StrykeValue>,
312    call_stack: Vec<CallFrame>,
313    /// Paired with [`Op::WantarrayPush`] / [`Op::WantarrayPop`] (e.g. `splice` list vs scalar return).
314    wantarray_stack: Vec<WantarrayCtx>,
315    interp: &'a mut VMHelper,
316    /// When `false`, [`VM::execute`] skips Cranelift JIT (linear, block, and subroutine linear) and
317    /// uses only the opcode interpreter. Default `true`.
318    jit_enabled: bool,
319    /// `sub_jit_skip_linear[ip]` — true when linear sub-JIT cannot apply (control flow / calls).
320    /// Indexed by IP for O(1) lookup instead of hashing (recursive subs like fib hit this millions of times).
321    sub_jit_skip_linear: Vec<bool>,
322    /// `sub_jit_skip_block[ip]` — true when block sub-JIT cannot apply.
323    sub_jit_skip_block: Vec<bool>,
324    /// `sub_entry_at_ip[ip]` — faster than hashing on every opcode (recursive subs dispatch millions of ops).
325    sub_entry_at_ip: Vec<bool>,
326    /// Invocations per sub-entry IP (tiered JIT: interpreter until count exceeds threshold).
327    sub_entry_invoke_count: Vec<u32>,
328    /// Minimum invocations before attempting subroutine JIT. Override with `STRYKE_JIT_SUB_INVOKES` (default 50).
329    jit_sub_invoke_threshold: u32,
330    /// Reused `i64` tables for sub-JIT / top-level JIT attempts (avoids `vec![0; n]` on every try).
331    jit_buf_slot: Vec<i64>,
332    jit_buf_plain: Vec<i64>,
333    jit_buf_arg: Vec<i64>,
334    /// Set when running [`VM::jit_trampoline_run_sub`]; [`Op::ReturnValue`] stores here and exits dispatch.
335    jit_trampoline_out: Option<StrykeValue>,
336    /// Nesting depth for [`Self::jit_trampoline_run_sub`]; dispatch breaks on [`Self::jit_trampoline_out`] only when `> 0`.
337    jit_trampoline_depth: u32,
338    /// Set by [`Op::Halt`]; outer loop exits after handling [`Self::try_recover_from_exception`].
339    halt: bool,
340    /// Stack of active `try` regions (LIFO).
341    try_stack: Vec<TryFrame>,
342    /// Error message for the next [`Op::CatchReceive`] (set before jumping to `catch_ip`).
343    pub(crate) pending_catch_error: Option<String>,
344    /// [`Op::Return`] / [`Op::ReturnValue`] with no caller frame: exit the main dispatch loop (was `break`).
345    exit_main_dispatch: bool,
346    /// Top-level [`Op::ReturnValue`] with no frame: value for implicit return (was `last = val; break`).
347    exit_main_dispatch_value: Option<StrykeValue>,
348    /// [`Chunk::static_sub_calls`] index → pre-resolved [`PerlSub`] for closure restore (stash key lookup once at VM build).
349    static_sub_closure_subs: Vec<Option<Arc<PerlSub>>>,
350    /// O(1) [`Chunk::sub_entries`] lookup (same first-wins semantics as the old linear scan).
351    sub_entry_by_name: HashMap<u16, (usize, bool)>,
352    /// When executing [`Chunk::block_bytecode_ranges`] via [`Self::run_block_region`].
353    block_region_mode: bool,
354    block_region_end: usize,
355    block_region_return: Option<StrykeValue>,
356}
357
358impl<'a> VM<'a> {
359    pub fn new(chunk: &Chunk, interp: &'a mut VMHelper) -> Self {
360        let static_sub_closure_subs: Vec<Option<Arc<PerlSub>>> = chunk
361            .static_sub_calls
362            .iter()
363            .map(|(_, _, name_idx)| {
364                let nm = chunk.names[*name_idx as usize].as_str();
365                interp.subs.get(nm).cloned()
366            })
367            .collect();
368        let mut sub_entry_by_name = HashMap::with_capacity(chunk.sub_entries.len());
369        for &(n, ip, sa) in &chunk.sub_entries {
370            sub_entry_by_name.entry(n).or_insert((ip, sa));
371        }
372        Self {
373            names: Arc::new(chunk.names.clone()),
374            constants: Arc::new(chunk.constants.clone()),
375            ops: Arc::new(chunk.ops.clone()),
376            lines: Arc::new(chunk.lines.clone()),
377            sub_entries: chunk.sub_entries.clone(),
378            static_sub_calls: chunk.static_sub_calls.clone(),
379            blocks: chunk.blocks.clone(),
380            code_ref_sigs: chunk.code_ref_sigs.clone(),
381            block_bytecode_ranges: chunk.block_bytecode_ranges.clone(),
382            map_expr_bytecode_ranges: chunk.map_expr_bytecode_ranges.clone(),
383            grep_expr_bytecode_ranges: chunk.grep_expr_bytecode_ranges.clone(),
384            regex_flip_flop_rhs_expr_bytecode_ranges: chunk
385                .regex_flip_flop_rhs_expr_bytecode_ranges
386                .clone(),
387            given_entries: chunk.given_entries.clone(),
388            given_topic_bytecode_ranges: chunk.given_topic_bytecode_ranges.clone(),
389            eval_timeout_entries: chunk.eval_timeout_entries.clone(),
390            eval_timeout_expr_bytecode_ranges: chunk.eval_timeout_expr_bytecode_ranges.clone(),
391            algebraic_match_entries: chunk.algebraic_match_entries.clone(),
392            algebraic_match_subject_bytecode_ranges: chunk
393                .algebraic_match_subject_bytecode_ranges
394                .clone(),
395            par_lines_entries: chunk.par_lines_entries.clone(),
396            par_walk_entries: chunk.par_walk_entries.clone(),
397            pwatch_entries: chunk.pwatch_entries.clone(),
398            substr_four_arg_entries: chunk.substr_four_arg_entries.clone(),
399            keys_expr_entries: chunk.keys_expr_entries.clone(),
400            keys_expr_bytecode_ranges: chunk.keys_expr_bytecode_ranges.clone(),
401            map_expr_entries: chunk.map_expr_entries.clone(),
402            grep_expr_entries: chunk.grep_expr_entries.clone(),
403            regex_flip_flop_rhs_expr_entries: chunk.regex_flip_flop_rhs_expr_entries.clone(),
404            values_expr_entries: chunk.values_expr_entries.clone(),
405            values_expr_bytecode_ranges: chunk.values_expr_bytecode_ranges.clone(),
406            delete_expr_entries: chunk.delete_expr_entries.clone(),
407            exists_expr_entries: chunk.exists_expr_entries.clone(),
408            push_expr_entries: chunk.push_expr_entries.clone(),
409            pop_expr_entries: chunk.pop_expr_entries.clone(),
410            shift_expr_entries: chunk.shift_expr_entries.clone(),
411            unshift_expr_entries: chunk.unshift_expr_entries.clone(),
412            splice_expr_entries: chunk.splice_expr_entries.clone(),
413            lvalues: chunk.lvalues.clone(),
414            ast_eval_exprs: chunk.ast_eval_exprs.clone(),
415            format_decls: chunk.format_decls.clone(),
416            use_overload_entries: chunk.use_overload_entries.clone(),
417            runtime_sub_decls: Arc::new(chunk.runtime_sub_decls.clone()),
418            runtime_advice_decls: Arc::new(chunk.runtime_advice_decls.clone()),
419            ip: 0,
420            stack: Vec::with_capacity(256),
421            call_stack: Vec::with_capacity(32),
422            wantarray_stack: Vec::with_capacity(8),
423            interp,
424            jit_enabled: true,
425            sub_jit_skip_linear: vec![false; chunk.ops.len().saturating_add(1)],
426            sub_jit_skip_block: vec![false; chunk.ops.len().saturating_add(1)],
427            sub_entry_at_ip: {
428                let mut v = vec![false; chunk.ops.len().saturating_add(1)];
429                for (_, e, _) in &chunk.sub_entries {
430                    if *e < v.len() {
431                        v[*e] = true;
432                    }
433                }
434                v
435            },
436            sub_entry_invoke_count: vec![0; chunk.ops.len().saturating_add(1)],
437            jit_sub_invoke_threshold: std::env::var("STRYKE_JIT_SUB_INVOKES")
438                .ok()
439                .and_then(|s| s.parse().ok())
440                .unwrap_or(50),
441            jit_buf_slot: Vec::new(),
442            jit_buf_plain: Vec::new(),
443            jit_buf_arg: Vec::new(),
444            jit_trampoline_out: None,
445            jit_trampoline_depth: 0,
446            halt: false,
447            try_stack: Vec::new(),
448            pending_catch_error: None,
449            exit_main_dispatch: false,
450            exit_main_dispatch_value: None,
451            static_sub_closure_subs,
452            sub_entry_by_name,
453            block_region_mode: false,
454            block_region_end: 0,
455            block_region_return: None,
456        }
457    }
458
459    /// Pop a synthetic [`CallFrame::block_region`] frame if dispatch exited before
460    /// [`Op::BlockReturnValue`] (error or fallthrough), restoring stack and scope.
461    fn unwind_stale_block_region_frame(&mut self) {
462        if let Some(frame) = self.call_stack.pop() {
463            if frame.block_region {
464                self.interp.wantarray_kind = frame.saved_wantarray;
465                self.stack.truncate(frame.stack_base);
466                self.interp.pop_scope_to_depth(frame.scope_depth);
467            } else {
468                self.call_stack.push(frame);
469            }
470        }
471    }
472
473    /// Run `ops[start..end]` (exclusive) for a compiled `map`/`grep`/`sort` block body.
474    ///
475    /// Matches [`VMHelper::exec_block`]: `$_` / `$a` / `$b` are set in the caller before each
476    /// iteration; then one block-local scope frame is pushed (no closure capture) and the body runs
477    /// inline. [`Op::BlockReturnValue`] unwinds that frame via [`Self::unwind_stale_block_region_frame`]
478    /// on error paths here.
479    fn run_block_region(
480        &mut self,
481        start: usize,
482        end: usize,
483        op_count: &mut u64,
484    ) -> PerlResult<StrykeValue> {
485        let resume_ip = self.ip;
486        let saved_mode = self.block_region_mode;
487        let saved_end = self.block_region_end;
488        let saved_ret = self.block_region_return.take();
489
490        let scope_depth_before = self.interp.scope.depth();
491        let saved_wa = self.interp.wantarray_kind;
492
493        self.call_stack.push(CallFrame {
494            return_ip: 0,
495            stack_base: self.stack.len(),
496            scope_depth: scope_depth_before,
497            saved_wantarray: saved_wa,
498            jit_trampoline_return: false,
499            block_region: true,
500            sub_profiler_start: None,
501        });
502        self.interp.scope_push_hook();
503        self.interp.wantarray_kind = WantarrayCtx::Scalar;
504        self.ip = start;
505        self.block_region_mode = true;
506        self.block_region_end = end;
507        self.block_region_return = None;
508
509        let r = self.run_main_dispatch_loop(StrykeValue::UNDEF, op_count, false);
510        let out = self.block_region_return.take();
511
512        self.block_region_return = saved_ret;
513        self.block_region_mode = saved_mode;
514        self.block_region_end = saved_end;
515        self.ip = resume_ip;
516
517        match r {
518            Ok(_) => {
519                if let Some(val) = out {
520                    Ok(val)
521                } else {
522                    self.unwind_stale_block_region_frame();
523                    Err(PerlError::runtime(
524                        "block bytecode region did not finish with BlockReturnValue",
525                        self.line(),
526                    ))
527                }
528            }
529            Err(e) => {
530                self.unwind_stale_block_region_frame();
531                Err(e)
532            }
533        }
534    }
535
536    #[inline]
537    fn extend_map_outputs(dst: &mut Vec<StrykeValue>, val: StrykeValue, peel_array_ref: bool) {
538        dst.extend(val.map_flatten_outputs(peel_array_ref));
539    }
540
541    fn map_with_block_common(
542        &mut self,
543        list: Vec<StrykeValue>,
544        block_idx: u16,
545        peel_array_ref: bool,
546        op_count: &mut u64,
547    ) -> PerlResult<()> {
548        if list.len() == 1 {
549            if let Some(p) = list[0].as_pipeline() {
550                if peel_array_ref {
551                    return Err(PerlError::runtime(
552                        "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
553                        self.line(),
554                    ));
555                }
556                let idx = block_idx as usize;
557                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
558                let line = self.line();
559                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
560                self.push(StrykeValue::pipeline(Arc::clone(&p)));
561                return Ok(());
562            }
563        }
564        let idx = block_idx as usize;
565        // map's BLOCK is list context. The shared block bytecode region is compiled with a
566        // scalar-context tail (grep/sort consumers need that), so when the block's tail is
567        // list-sensitive (`($_, $_*10)`, `1..$_`, `reverse …`, an array variable, …) fall
568        // back to the interpreter's list-tail [`Interpreter::exec_block_with_tail`]. For
569        // plain scalar tails (`$_ * 2`, `f($_)`, string ops) the bytecode region produces
570        // the same value in either context, so keep using it for speed.
571        let block_tail_is_list_sensitive = self
572            .blocks
573            .get(idx)
574            .and_then(|b| b.last())
575            .map(|stmt| match &stmt.kind {
576                crate::ast::StmtKind::Expression(expr) => {
577                    crate::compiler::expr_tail_is_list_sensitive(expr)
578                }
579                _ => true,
580            })
581            .unwrap_or(true);
582        if !block_tail_is_list_sensitive {
583            if let Some(&(start, end)) =
584                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
585            {
586                let mut result = Vec::new();
587                for item in list {
588                    self.interp.scope.set_topic(item);
589                    let val = self.run_block_region(start, end, op_count)?;
590                    Self::extend_map_outputs(&mut result, val, peel_array_ref);
591                }
592                self.push(StrykeValue::array(result));
593                return Ok(());
594            }
595        }
596        let block = self.blocks[idx].clone();
597        let mut result = Vec::new();
598        for item in list {
599            self.interp.scope.set_topic(item);
600            match self.interp.exec_block_with_tail(&block, WantarrayCtx::List) {
601                Ok(val) => Self::extend_map_outputs(&mut result, val, peel_array_ref),
602                Err(FlowOrError::Error(e)) => return Err(e),
603                Err(_) => {}
604            }
605        }
606        self.push(StrykeValue::array(result));
607        Ok(())
608    }
609
610    fn map_with_expr_common(
611        &mut self,
612        list: Vec<StrykeValue>,
613        expr_idx: u16,
614        peel_array_ref: bool,
615        op_count: &mut u64,
616    ) -> PerlResult<()> {
617        let idx = expr_idx as usize;
618        let dispatch_coderef = !crate::compat_mode();
619        // EXPR-form `map EXPR, LIST`: no block boundary, so use
620        // `set_topic_local` (rebinds `_`/`_0` only, no chain shift, no
621        // slot 1+ zero). Block-form `map { ... }` goes through a
622        // separate dispatch path that uses full `set_topic`.
623        if let Some(&(start, end)) = self
624            .map_expr_bytecode_ranges
625            .get(idx)
626            .and_then(|r| r.as_ref())
627        {
628            let mut result = Vec::new();
629            for item in list {
630                self.interp.scope.set_topic_local(item.clone());
631                let val = self.run_block_region(start, end, op_count)?;
632                let val = self.maybe_call_coderef_with_item(val, &item, dispatch_coderef)?;
633                Self::extend_map_outputs(&mut result, val, peel_array_ref);
634            }
635            self.push(StrykeValue::array(result));
636        } else {
637            let e = self.map_expr_entries[idx].clone();
638            let mut result = Vec::new();
639            for item in list {
640                self.interp.scope.set_topic_local(item.clone());
641                let val = vm_interp_result(
642                    self.interp.eval_expr_ctx(&e, WantarrayCtx::List),
643                    self.line(),
644                )?;
645                let val = self.maybe_call_coderef_with_item(val, &item, dispatch_coderef)?;
646                Self::extend_map_outputs(&mut result, val, peel_array_ref);
647            }
648            self.push(StrykeValue::array(result));
649        }
650        Ok(())
651    }
652
653    /// If `val` is a code reference and `dispatch` is true (i.e. not in
654    /// `--compat` mode), call it with `item` as the sole argument and
655    /// return the call result. Otherwise return `val` unchanged. Powers
656    /// the "coderef-in-expr-position" feature for `grep $f, @l`,
657    /// `map $f, @l`, and pipe-forward `|> grep $f`.
658    fn maybe_call_coderef_with_item(
659        &mut self,
660        val: StrykeValue,
661        item: &StrykeValue,
662        dispatch: bool,
663    ) -> PerlResult<StrykeValue> {
664        if !dispatch {
665            return Ok(val);
666        }
667        if let Some(sub) = val.as_code_ref() {
668            let sub = sub.clone();
669            let line = self.line();
670            return vm_interp_result(
671                self.interp
672                    .call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line),
673                line,
674            );
675        }
676        Ok(val)
677    }
678
679    /// Consecutive groups: key from block with `$_`; keys compared with [`StrykeValue::str_eq`].
680    fn chunk_by_with_block_common(
681        &mut self,
682        list: Vec<StrykeValue>,
683        block_idx: u16,
684        op_count: &mut u64,
685    ) -> PerlResult<()> {
686        if list.is_empty() {
687            self.push(StrykeValue::array(vec![]));
688            return Ok(());
689        }
690        let idx = block_idx as usize;
691        let mut chunks: Vec<StrykeValue> = Vec::new();
692        let mut run: Vec<StrykeValue> = Vec::new();
693        let mut prev_key: Option<StrykeValue> = None;
694
695        let eval_key =
696            |vm: &mut VM, item: StrykeValue, op_count: &mut u64| -> PerlResult<StrykeValue> {
697                vm.interp.scope.set_topic(item);
698                if let Some(&(start, end)) =
699                    vm.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
700                {
701                    vm.run_block_region(start, end, op_count)
702                } else {
703                    let block = vm.blocks[idx].clone();
704                    match vm.interp.exec_block(&block) {
705                        Ok(val) => Ok(val),
706                        Err(FlowOrError::Error(e)) => Err(e),
707                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
708                        Err(_) => Ok(StrykeValue::UNDEF),
709                    }
710                }
711            };
712
713        for item in list {
714            let key = eval_key(self, item.clone(), op_count)?;
715            match &prev_key {
716                None => {
717                    run.push(item);
718                    prev_key = Some(key);
719                }
720                Some(pk) => {
721                    if key.str_eq(pk) {
722                        run.push(item);
723                    } else {
724                        chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
725                            std::mem::take(&mut run),
726                        ))));
727                        run.push(item);
728                        prev_key = Some(key);
729                    }
730                }
731            }
732        }
733        if !run.is_empty() {
734            chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
735        }
736        self.push(StrykeValue::array(chunks));
737        Ok(())
738    }
739
740    fn chunk_by_with_expr_common(
741        &mut self,
742        list: Vec<StrykeValue>,
743        expr_idx: u16,
744        op_count: &mut u64,
745    ) -> PerlResult<()> {
746        if list.is_empty() {
747            self.push(StrykeValue::array(vec![]));
748            return Ok(());
749        }
750        let idx = expr_idx as usize;
751        let mut chunks: Vec<StrykeValue> = Vec::new();
752        let mut run: Vec<StrykeValue> = Vec::new();
753        let mut prev_key: Option<StrykeValue> = None;
754        for item in list {
755            self.interp.scope.set_topic(item.clone());
756            let key = if let Some(&(start, end)) = self
757                .map_expr_bytecode_ranges
758                .get(idx)
759                .and_then(|r| r.as_ref())
760            {
761                self.run_block_region(start, end, op_count)?
762            } else {
763                let e = &self.map_expr_entries[idx];
764                vm_interp_result(
765                    self.interp.eval_expr_ctx(e, WantarrayCtx::Scalar),
766                    self.line(),
767                )?
768            };
769            match &prev_key {
770                None => {
771                    run.push(item);
772                    prev_key = Some(key);
773                }
774                Some(pk) => {
775                    if key.str_eq(pk) {
776                        run.push(item);
777                    } else {
778                        chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
779                            std::mem::take(&mut run),
780                        ))));
781                        run.push(item);
782                        prev_key = Some(key);
783                    }
784                }
785            }
786        }
787        if !run.is_empty() {
788            chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
789        }
790        self.push(StrykeValue::array(chunks));
791        Ok(())
792    }
793
794    #[inline]
795    fn sub_jit_skip_linear_test(&self, ip: usize) -> bool {
796        self.sub_jit_skip_linear.get(ip).copied().unwrap_or(false)
797    }
798
799    #[inline]
800    fn sub_jit_skip_linear_mark(&mut self, ip: usize) {
801        if ip >= self.sub_jit_skip_linear.len() {
802            self.sub_jit_skip_linear.resize(ip + 1, false);
803        }
804        self.sub_jit_skip_linear[ip] = true;
805    }
806
807    #[inline]
808    fn sub_jit_skip_block_test(&self, ip: usize) -> bool {
809        self.sub_jit_skip_block.get(ip).copied().unwrap_or(false)
810    }
811
812    #[inline]
813    fn sub_jit_skip_block_mark(&mut self, ip: usize) {
814        if ip >= self.sub_jit_skip_block.len() {
815            self.sub_jit_skip_block.resize(ip + 1, false);
816        }
817        self.sub_jit_skip_block[ip] = true;
818    }
819
820    /// Enable or disable Cranelift JIT for this execution. Disabling skips compilation and buffer
821    /// prefetch for JIT paths (pure interpreter).
822    pub fn set_jit_enabled(&mut self, enabled: bool) {
823        self.jit_enabled = enabled;
824    }
825
826    #[inline]
827    fn push(&mut self, val: StrykeValue) {
828        self.stack.push(val);
829    }
830
831    #[inline]
832    fn pop(&mut self) -> StrykeValue {
833        self.stack.pop().unwrap_or(StrykeValue::UNDEF)
834    }
835
836    /// Convert a name-based binding ref (`\@array`, `\%hash`, `\$scalar`) into a
837    /// real `Arc`-based ref by snapshotting the current scope data.  This must be
838    /// called before the declaring scope is destroyed (e.g. on function return)
839    /// so the ref survives scope exit — matching Perl 5's refcount semantics.
840    fn resolve_binding_ref(&self, val: StrykeValue) -> StrykeValue {
841        if let Some(name) = val.as_array_binding_name() {
842            let data = self.interp.scope.get_array(&name);
843            return StrykeValue::array_ref(Arc::new(RwLock::new(data)));
844        }
845        if let Some(name) = val.as_hash_binding_name() {
846            let data = self.interp.scope.get_hash(&name);
847            return StrykeValue::hash_ref(Arc::new(RwLock::new(data)));
848        }
849        if let Some(name) = val.as_scalar_binding_name() {
850            let data = self.interp.scope.get_scalar(&name);
851            return StrykeValue::scalar_ref(Arc::new(RwLock::new(data)));
852        }
853        val
854    }
855
856    /// Pop `n` array-slice index specs (TOS = last spec). Each spec is a scalar index or an array
857    /// of indices (list-context `..`, `qw/.../`, parenthesized list), matching
858    /// [`crate::compiler::Compiler::compile_array_slice_index_expr`]. Returns flattened indices in
859    /// source order (first spec’s indices first).
860    fn pop_flattened_array_slice_specs(&mut self, n: usize) -> Vec<i64> {
861        let mut chunks: Vec<Vec<i64>> = Vec::with_capacity(n);
862        for _ in 0..n {
863            let spec = self.pop();
864            let mut flat = Vec::new();
865            if let Some(av) = spec.as_array_vec() {
866                for pv in av.iter() {
867                    flat.push(pv.to_int());
868                }
869            } else {
870                flat.push(spec.to_int());
871            }
872            chunks.push(flat);
873        }
874        chunks.reverse();
875        chunks.into_iter().flatten().collect()
876    }
877
878    /// Call operands are pushed so the rightmost syntactic argument is on top. Restore
879    /// left-to-right order, then flatten list-valued operands (`qw/.../`, list literals, hashes)
880    /// into successive scalars — matching Perl's argument list for simple calls. Reversing after
881    /// flattening would incorrectly reverse elements inside expanded lists.
882    fn pop_call_operands_flattened(&mut self, argc: usize) -> Vec<StrykeValue> {
883        let mut slots = Vec::with_capacity(argc);
884        for _ in 0..argc {
885            slots.push(self.pop());
886        }
887        slots.reverse();
888        let mut out = Vec::new();
889        for v in slots {
890            if let Some(items) = v.as_array_vec() {
891                out.extend(items);
892            } else if let Some(h) = v.as_hash_map() {
893                for (k, val) in h {
894                    out.push(StrykeValue::string(k));
895                    out.push(val);
896                }
897            } else {
898                out.push(v);
899            }
900        }
901        out
902    }
903
904    /// Like [`Self::pop_call_operands_flattened`], but each syntactic argument stays one
905    /// [`StrykeValue`] (`zip` / `mesh` need full lists per operand, not Perl's flattened `@_`).
906    fn pop_call_operands_preserved(&mut self, argc: usize) -> Vec<StrykeValue> {
907        let mut slots = Vec::with_capacity(argc);
908        for _ in 0..argc {
909            slots.push(self.pop());
910        }
911        slots.reverse();
912        slots
913    }
914
915    #[inline]
916    fn call_preserve_operand_arrays(name: &str) -> bool {
917        // Stryke builtins are unprefixed; `CORE::` callers route to bare names.
918        let name = name.strip_prefix("CORE::").unwrap_or(name);
919        matches!(
920            name,
921            "zip"
922                | "zip_longest"
923                | "zip_shortest"
924                | "mesh"
925                | "mesh_longest"
926                | "mesh_shortest"
927                | "take"
928                | "head"
929                | "tail"
930                | "drop"
931                // `len` / `count` / … must receive list-valued operands as **one** value.
932                // Otherwise `len stat $path` flattens `@_`: empty stat → 0 args → `$_` fallback
933                // (wrong), success → 13 args → list_count semantics (wrong for `len`).
934                | "len"
935                | "cnt"
936                | "count"
937                | "list_count"
938                | "list_size"
939        )
940    }
941
942    fn flatten_array_slice_specs_ordered_values(
943        &self,
944        specs: &[StrykeValue],
945    ) -> Result<Vec<i64>, PerlError> {
946        let mut out = Vec::new();
947        for spec in specs {
948            if let Some(av) = spec.as_array_vec() {
949                for pv in av.iter() {
950                    out.push(pv.to_int());
951                }
952            } else {
953                out.push(spec.to_int());
954            }
955        }
956        Ok(out)
957    }
958
959    /// Hash `{…}` slice key slots in source order (each slot may expand to many string keys).
960    fn flatten_hash_slice_key_slots(key_vals: &[StrykeValue]) -> Vec<String> {
961        let mut ks = Vec::new();
962        for kv in key_vals {
963            if let Some(vv) = kv.as_array_vec() {
964                ks.extend(vv.iter().map(|x| x.to_string()));
965            } else {
966                ks.push(kv.to_string());
967            }
968        }
969        ks
970    }
971
972    #[inline]
973    fn peek(&self) -> &StrykeValue {
974        self.stack.last().unwrap_or(&PEEK_UNDEF)
975    }
976
977    #[inline]
978    fn constant(&self, idx: u16) -> &StrykeValue {
979        &self.constants[idx as usize]
980    }
981
982    fn line(&self) -> usize {
983        self.lines
984            .get(self.ip.saturating_sub(1))
985            .copied()
986            .unwrap_or(0)
987    }
988
989    /// Cranelift linear JIT for a subroutine body when `ip` is a compiled sub entry (see `Chunk::sub_entries`).
990    /// Returns `Ok(true)` when the sub was executed natively and the VM should continue at `return_ip`.
991    fn try_jit_subroutine_linear(&mut self) -> Result<bool, PerlError> {
992        let ip = self.ip;
993        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
994        if self.sub_jit_skip_linear_test(ip) {
995            return Ok(false);
996        }
997        let ops: &Vec<Op> = &self.ops;
998        let ops = ops as *const Vec<Op>;
999        let ops = unsafe { &*ops };
1000        let constants: &Vec<StrykeValue> = &self.constants;
1001        let constants = constants as *const Vec<StrykeValue>;
1002        let constants = unsafe { &*constants };
1003        let names: &Vec<String> = &self.names;
1004        let names = names as *const Vec<String>;
1005        let names = unsafe { &*names };
1006        let Some((seg, _)) = crate::jit::sub_entry_segment(ops, ip) else {
1007            return Ok(false);
1008        };
1009        // `try_run_linear_sub` rejects these segments without compiling — skip expensive work before
1010        // resize/fill of reusable scratch buffers (`jit_buf_*`).
1011        if crate::jit::segment_blocks_subroutine_linear_jit(seg, &self.sub_entries) {
1012            self.sub_jit_skip_linear_mark(ip);
1013            return Ok(false);
1014        }
1015        let mut slot_len: Option<usize> = None;
1016        if let Some(max) = crate::jit::linear_slot_ops_max_index_seq(seg) {
1017            let n = max as usize + 1;
1018            self.jit_buf_slot.resize(n, 0);
1019            let mut ok = true;
1020            for i in 0..=max {
1021                let pv = self.interp.scope.get_scalar_slot(i);
1022                self.jit_buf_slot[i as usize] = match pv.as_integer() {
1023                    Some(v) => v,
1024                    None if pv.is_undef() && crate::jit::slot_undef_prefill_ok_seq(seg, i) => 0,
1025                    None => {
1026                        ok = false;
1027                        break;
1028                    }
1029                };
1030            }
1031            if ok {
1032                slot_len = Some(n);
1033            }
1034        }
1035        let mut plain_len: Option<usize> = None;
1036        if let Some(max) = crate::jit::linear_plain_ops_max_index_seq(seg) {
1037            if (max as usize) < names.len() {
1038                let n = max as usize + 1;
1039                self.jit_buf_plain.resize(n, 0);
1040                let mut ok = true;
1041                for i in 0..=max {
1042                    let nm = names[i as usize].as_str();
1043                    match self.interp.scope.get_scalar(nm).as_integer() {
1044                        Some(v) => self.jit_buf_plain[i as usize] = v,
1045                        None => {
1046                            ok = false;
1047                            break;
1048                        }
1049                    }
1050                }
1051                if ok {
1052                    plain_len = Some(n);
1053                }
1054            }
1055        }
1056        let mut arg_len: Option<usize> = None;
1057        if let Some(max) = crate::jit::linear_arg_ops_max_index_seq(seg) {
1058            if let Some(frame) = self.call_stack.last() {
1059                let base = frame.stack_base;
1060                let n = max as usize + 1;
1061                self.jit_buf_arg.resize(n, 0);
1062                let mut ok = true;
1063                for i in 0..=max {
1064                    let pos = base + i as usize;
1065                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1066                    match pv.as_integer() {
1067                        Some(v) => self.jit_buf_arg[i as usize] = v,
1068                        None => {
1069                            ok = false;
1070                            break;
1071                        }
1072                    }
1073                }
1074                if ok {
1075                    arg_len = Some(n);
1076                }
1077            }
1078        }
1079        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1080        let slot_buf = slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1081        let plain_buf = plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1082        let arg_buf = arg_len.map(|n| &self.jit_buf_arg[..n]);
1083        let Some(v) = crate::jit::try_run_linear_sub(
1084            ops,
1085            ip,
1086            slot_buf,
1087            plain_buf,
1088            arg_buf,
1089            constants,
1090            &self.sub_entries,
1091            vm_ptr,
1092        ) else {
1093            return Ok(false);
1094        };
1095        if let Some(n) = slot_len {
1096            let buf = &self.jit_buf_slot[..n];
1097            for idx in crate::jit::linear_slot_ops_written_indices_seq(seg) {
1098                self.interp
1099                    .scope
1100                    .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
1101            }
1102        }
1103        if let Some(n) = plain_len {
1104            let buf = &self.jit_buf_plain[..n];
1105            for idx in crate::jit::linear_plain_ops_written_indices_seq(seg) {
1106                let name = names[idx as usize].as_str();
1107                self.interp
1108                    .scope
1109                    .set_scalar(name, StrykeValue::integer(buf[idx as usize]))
1110                    .map_err(|e| e.at_line(self.line()))?;
1111            }
1112        }
1113        if let Some(frame) = self.call_stack.pop() {
1114            self.interp.wantarray_kind = frame.saved_wantarray;
1115            self.stack.truncate(frame.stack_base);
1116            self.interp.pop_scope_to_depth(frame.scope_depth);
1117            if frame.jit_trampoline_return {
1118                self.jit_trampoline_out = Some(v);
1119            } else {
1120                self.push(v);
1121                self.ip = frame.return_ip;
1122            }
1123        }
1124        Ok(true)
1125    }
1126
1127    /// Cranelift block JIT for a subroutine with control flow (see [`crate::jit::block_jit_validate_sub`]).
1128    fn try_jit_subroutine_block(&mut self) -> Result<bool, PerlError> {
1129        let ip = self.ip;
1130        debug_assert!(self.sub_entry_at_ip.get(ip).copied().unwrap_or(false));
1131        if self.sub_jit_skip_block_test(ip) {
1132            return Ok(false);
1133        }
1134        let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1135        let ops: &Vec<Op> = &self.ops;
1136        let constants: &Vec<StrykeValue> = &self.constants;
1137        let names: &Vec<String> = &self.names;
1138        let Some((full_body, term)) = crate::jit::sub_full_body(ops, ip) else {
1139            return Ok(false);
1140        };
1141        if crate::jit::sub_body_blocks_subroutine_block_jit(full_body) {
1142            self.sub_jit_skip_block_mark(ip);
1143            return Ok(false);
1144        }
1145        let Some(validated) =
1146            crate::jit::block_jit_validate_sub(full_body, constants, term, &self.sub_entries)
1147        else {
1148            self.sub_jit_skip_block_mark(ip);
1149            return Ok(false);
1150        };
1151        let block_buf_mode = validated.buffer_mode();
1152
1153        let mut b_slot_len: Option<usize> = None;
1154        if let Some(max) = crate::jit::block_slot_ops_max_index(full_body) {
1155            let n = max as usize + 1;
1156            self.jit_buf_slot.resize(n, 0);
1157            let mut ok = true;
1158            for i in 0..=max {
1159                let pv = self.interp.scope.get_scalar_slot(i);
1160                self.jit_buf_slot[i as usize] = match block_buf_mode {
1161                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => pv.raw_bits() as i64,
1162                    crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1163                        Some(v) => v,
1164                        None if pv.is_undef()
1165                            && crate::jit::block_slot_undef_prefill_ok(full_body, i) =>
1166                        {
1167                            0
1168                        }
1169                        None => {
1170                            ok = false;
1171                            break;
1172                        }
1173                    },
1174                };
1175            }
1176            if ok {
1177                b_slot_len = Some(n);
1178            }
1179        }
1180
1181        let mut b_plain_len: Option<usize> = None;
1182        if let Some(max) = crate::jit::block_plain_ops_max_index(full_body) {
1183            if (max as usize) < names.len() {
1184                let n = max as usize + 1;
1185                self.jit_buf_plain.resize(n, 0);
1186                let mut ok = true;
1187                for i in 0..=max {
1188                    let nm = names[i as usize].as_str();
1189                    let pv = self.interp.scope.get_scalar(nm);
1190                    self.jit_buf_plain[i as usize] = match block_buf_mode {
1191                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1192                            pv.raw_bits() as i64
1193                        }
1194                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1195                            Some(v) => v,
1196                            None => {
1197                                ok = false;
1198                                break;
1199                            }
1200                        },
1201                    };
1202                }
1203                if ok {
1204                    b_plain_len = Some(n);
1205                }
1206            }
1207        }
1208
1209        let mut b_arg_len: Option<usize> = None;
1210        if let Some(max) = crate::jit::block_arg_ops_max_index(full_body) {
1211            if let Some(frame) = self.call_stack.last() {
1212                let base = frame.stack_base;
1213                let n = max as usize + 1;
1214                self.jit_buf_arg.resize(n, 0);
1215                let mut ok = true;
1216                for i in 0..=max {
1217                    let pos = base + i as usize;
1218                    let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1219                    self.jit_buf_arg[i as usize] = match block_buf_mode {
1220                        crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1221                            pv.raw_bits() as i64
1222                        }
1223                        crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1224                            Some(v) => v,
1225                            None => {
1226                                ok = false;
1227                                break;
1228                            }
1229                        },
1230                    };
1231                }
1232                if ok {
1233                    b_arg_len = Some(n);
1234                }
1235            }
1236        }
1237
1238        let block_slot_buf = b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1239        let block_plain_buf = b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1240        let block_arg_buf = b_arg_len.map(|n| &self.jit_buf_arg[..n]);
1241
1242        let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
1243            full_body,
1244            block_slot_buf,
1245            block_plain_buf,
1246            block_arg_buf,
1247            constants,
1248            Some(validated),
1249            vm_ptr,
1250            &self.sub_entries,
1251        ) else {
1252            self.sub_jit_skip_block_mark(ip);
1253            return Ok(false);
1254        };
1255
1256        if let Some(n) = b_slot_len {
1257            let buf = &self.jit_buf_slot[..n];
1258            for idx in crate::jit::block_slot_ops_written_indices(full_body) {
1259                let bits = buf[idx as usize] as u64;
1260                let pv = match buf_mode {
1261                    crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1262                        StrykeValue::from_raw_bits(bits)
1263                    }
1264                    crate::jit::BlockJitBufferMode::I64AsInteger => {
1265                        StrykeValue::integer(buf[idx as usize])
1266                    }
1267                };
1268                self.interp.scope.set_scalar_slot(idx, pv);
1269            }
1270        }
1271        if let Some(n) = b_plain_len {
1272            let buf = &self.jit_buf_plain[..n];
1273            for idx in crate::jit::block_plain_ops_written_indices(full_body) {
1274                let name = names[idx as usize].as_str();
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
1285                    .scope
1286                    .set_scalar(name, pv)
1287                    .map_err(|e| e.at_line(self.line()))?;
1288            }
1289        }
1290        if let Some(frame) = self.call_stack.pop() {
1291            self.interp.wantarray_kind = frame.saved_wantarray;
1292            self.stack.truncate(frame.stack_base);
1293            self.interp.pop_scope_to_depth(frame.scope_depth);
1294            if frame.jit_trampoline_return {
1295                self.jit_trampoline_out = Some(v);
1296            } else {
1297                self.push(v);
1298                self.ip = frame.return_ip;
1299            }
1300        }
1301        Ok(true)
1302    }
1303
1304    fn run_method_op(
1305        &mut self,
1306        name_idx: u16,
1307        argc: u8,
1308        wa: u8,
1309        super_call: bool,
1310    ) -> PerlResult<()> {
1311        let method_owned = self.names[name_idx as usize].clone();
1312        let argc = argc as usize;
1313        let want = WantarrayCtx::from_byte(wa);
1314        let mut args = Vec::with_capacity(argc);
1315        for _ in 0..argc {
1316            args.push(self.pop());
1317        }
1318        args.reverse();
1319        let obj = self.pop();
1320        let method = method_owned.as_str();
1321        if let Some(r) = crate::pchannel::dispatch_method(&obj, method, &args, self.line()) {
1322            self.push(r?);
1323            return Ok(());
1324        }
1325        if let Some(r) = self
1326            .interp
1327            .try_native_method(&obj, method, &args, self.line())
1328        {
1329            self.push(r?);
1330            return Ok(());
1331        }
1332        let class = if let Some(b) = obj.as_blessed_ref() {
1333            b.class.clone()
1334        } else if let Some(s) = obj.as_str() {
1335            s
1336        } else {
1337            return Err(PerlError::runtime(
1338                "Can't call method on non-object",
1339                self.line(),
1340            ));
1341        };
1342        if method == "VERSION" && !super_call {
1343            if let Some(ver) = self.interp.package_version_scalar(class.as_str())? {
1344                self.push(ver);
1345                return Ok(());
1346            }
1347        }
1348        // UNIVERSAL methods: isa, can, DOES
1349        if !super_call {
1350            match method {
1351                "isa" => {
1352                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
1353                    let mro = self.interp.mro_linearize(&class);
1354                    let result = mro.iter().any(|c| c == &target);
1355                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
1356                    return Ok(());
1357                }
1358                "can" => {
1359                    let target_method = args.first().map(|v| v.to_string()).unwrap_or_default();
1360                    let found = self
1361                        .interp
1362                        .resolve_method_full_name(&class, &target_method, false)
1363                        .and_then(|fq| self.interp.subs.get(&fq))
1364                        .is_some();
1365                    if found {
1366                        self.push(StrykeValue::code_ref(std::sync::Arc::new(
1367                            crate::value::PerlSub {
1368                                name: target_method,
1369                                params: vec![],
1370                                body: vec![],
1371                                closure_env: None,
1372                                prototype: None,
1373                                fib_like: None,
1374                            },
1375                        )));
1376                    } else {
1377                        self.push(StrykeValue::UNDEF);
1378                    }
1379                    return Ok(());
1380                }
1381                "DOES" => {
1382                    let target = args.first().map(|v| v.to_string()).unwrap_or_default();
1383                    let mro = self.interp.mro_linearize(&class);
1384                    let result = mro.iter().any(|c| c == &target);
1385                    self.push(StrykeValue::integer(if result { 1 } else { 0 }));
1386                    return Ok(());
1387                }
1388                _ => {}
1389            }
1390        }
1391        let mut all_args = vec![obj];
1392        all_args.extend(args);
1393        let full_name = match self
1394            .interp
1395            .resolve_method_full_name(&class, method, super_call)
1396        {
1397            Some(f) => f,
1398            None => {
1399                return Err(PerlError::runtime(
1400                    format!(
1401                        "Can't locate method \"{}\" via inheritance (invocant \"{}\")",
1402                        method, class
1403                    ),
1404                    self.line(),
1405                ));
1406            }
1407        };
1408        if let Some(sub) = self.interp.subs.get(&full_name).cloned() {
1409            let saved_wa = self.interp.wantarray_kind;
1410            self.interp.wantarray_kind = want;
1411            self.interp.scope_push_hook();
1412            self.interp.scope.declare_array("_", all_args);
1413            if let Some(ref env) = sub.closure_env {
1414                self.interp.scope.restore_capture(env);
1415            }
1416            let line = self.line();
1417            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
1418            self.interp
1419                .apply_sub_signature(sub.as_ref(), &argv, line)
1420                .map_err(|e| e.at_line(line))?;
1421            self.interp.scope.declare_array("_", argv);
1422            let result = self.interp.exec_block_no_scope(&sub.body);
1423            self.interp.wantarray_kind = saved_wa;
1424            self.interp.scope_pop_hook();
1425            match result {
1426                Ok(v) => self.push(v),
1427                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
1428                    self.push(v)
1429                }
1430                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
1431                Err(_) => self.push(StrykeValue::UNDEF),
1432            }
1433        } else if method == "new" && !super_call {
1434            if class == "Set" {
1435                self.push(crate::value::set_from_elements(
1436                    all_args.into_iter().skip(1),
1437                ));
1438            } else if let Some(def) = self.interp.struct_defs.get(&class).cloned() {
1439                let line = self.line();
1440                let mut provided = Vec::new();
1441                let mut i = 1;
1442                while i + 1 < all_args.len() {
1443                    let k = all_args[i].to_string();
1444                    let v = all_args[i + 1].clone();
1445                    provided.push((k, v));
1446                    i += 2;
1447                }
1448                let mut defaults = Vec::with_capacity(def.fields.len());
1449                for field in &def.fields {
1450                    if let Some(ref expr) = field.default {
1451                        let val = self.interp.eval_expr(expr).map_err(|e| match e {
1452                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
1453                            _ => PerlError::runtime("default evaluation flow", line),
1454                        })?;
1455                        defaults.push(Some(val));
1456                    } else {
1457                        defaults.push(None);
1458                    }
1459                }
1460                let v =
1461                    crate::native_data::struct_new_with_defaults(&def, &provided, &defaults, line)?;
1462                self.push(v);
1463            } else if let Some(def) = self.interp.class_defs.get(&class).cloned() {
1464                // Stryke `class` declarations route through `class_construct`
1465                // so the result is a real `ClassInstance` (typed-my checks,
1466                // isa walk, BUILD hooks). Without this the bytecode path
1467                // fell through to the default Perl-style blessed-hashref
1468                // below, breaking method dispatch for `$self` binding.
1469                // Mirrors the tree-walker fix in `vm_helper::builtin_new`.
1470                // Skip `all_args[0]` (the class-name receiver) since
1471                // `class_construct` expects user args only.
1472                let line = self.line();
1473                let user_args: Vec<StrykeValue> = all_args.into_iter().skip(1).collect();
1474                let v =
1475                    self.interp
1476                        .class_construct(&def, user_args, line)
1477                        .map_err(|e| match e {
1478                            crate::vm_helper::FlowOrError::Error(stryke) => stryke,
1479                            _ => PerlError::runtime("class_construct flow", line),
1480                        })?;
1481                self.push(v);
1482            } else {
1483                let mut map = IndexMap::new();
1484                let mut i = 1;
1485                while i + 1 < all_args.len() {
1486                    map.insert(all_args[i].to_string(), all_args[i + 1].clone());
1487                    i += 2;
1488                }
1489                self.push(StrykeValue::blessed(Arc::new(
1490                    crate::value::BlessedRef::new_blessed(class, StrykeValue::hash(map)),
1491                )));
1492            }
1493        } else if let Some(result) =
1494            self.interp
1495                .try_autoload_call(&full_name, all_args, self.line(), want, Some(&class))
1496        {
1497            match result {
1498                Ok(v) => self.push(v),
1499                Err(crate::vm_helper::FlowOrError::Flow(crate::vm_helper::Flow::Return(v))) => {
1500                    self.push(v)
1501                }
1502                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
1503                Err(_) => self.push(StrykeValue::UNDEF),
1504            }
1505        } else {
1506            return Err(PerlError::runtime(
1507                format!(
1508                    "Can't locate method \"{}\" in package \"{}\"",
1509                    method, class
1510                ),
1511                self.line(),
1512            ));
1513        }
1514        Ok(())
1515    }
1516
1517    fn run_fan_block(
1518        &mut self,
1519        block_idx: u16,
1520        n: usize,
1521        line: usize,
1522        progress: bool,
1523    ) -> PerlResult<()> {
1524        let block = self.blocks[block_idx as usize].clone();
1525        let subs = self.interp.subs.clone();
1526        let (scope_capture, atomic_arrays, atomic_hashes) =
1527            self.interp.scope.capture_with_atomics();
1528        // Worker bodies execute via the tree walker (`exec_block_no_scope`) which uses
1529        // `tree_scalar_storage_name` to rewrite `$x` → `Pkg::x`. That helper consults
1530        // `english_lexical_scalars` + `our_lexical_scalars` — empty in a fresh worker —
1531        // so without copying the parent's sets, `our` / `oursync` reads see UNDEF.
1532        let lex_scalars = self.interp.english_lexical_scalars_clone();
1533        let our_scalars = self.interp.our_lexical_scalars_clone();
1534        let fan_progress = FanProgress::new(progress, n);
1535        let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
1536        (0..n).into_par_iter().for_each(|i| {
1537            if first_err.lock().is_some() {
1538                return;
1539            }
1540            fan_progress.start_worker(i);
1541            let mut local_interp = VMHelper::new();
1542            local_interp.subs = subs.clone();
1543            local_interp.suppress_stdout = progress;
1544            local_interp.scope.restore_capture(&scope_capture);
1545            local_interp
1546                .scope
1547                .restore_atomics(&atomic_arrays, &atomic_hashes);
1548            local_interp.set_english_lexical_scalars(lex_scalars.clone());
1549            local_interp.set_our_lexical_scalars(our_scalars.clone());
1550            local_interp.enable_parallel_guard();
1551            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
1552            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
1553            local_interp.scope_push_hook();
1554            match local_interp.exec_block_no_scope(&block) {
1555                Ok(_) => {}
1556                Err(e) => {
1557                    let stryke = match e {
1558                        FlowOrError::Error(stryke) => stryke,
1559                        FlowOrError::Flow(_) => PerlError::runtime(
1560                            "return/last/next/redo not supported inside fan block",
1561                            line,
1562                        ),
1563                    };
1564                    let mut g = first_err.lock();
1565                    if g.is_none() {
1566                        *g = Some(stryke);
1567                    }
1568                }
1569            }
1570            local_interp.scope_pop_hook();
1571            crate::parallel_trace::fan_worker_set_index(None);
1572            fan_progress.finish_worker(i);
1573        });
1574        fan_progress.finish();
1575        if let Some(e) = first_err.lock().take() {
1576            return Err(e);
1577        }
1578        self.push(StrykeValue::UNDEF);
1579        Ok(())
1580    }
1581
1582    fn run_fan_cap_block(
1583        &mut self,
1584        block_idx: u16,
1585        n: usize,
1586        line: usize,
1587        progress: bool,
1588    ) -> PerlResult<()> {
1589        let block = self.blocks[block_idx as usize].clone();
1590        let subs = self.interp.subs.clone();
1591        let (scope_capture, atomic_arrays, atomic_hashes) =
1592            self.interp.scope.capture_with_atomics();
1593        // See run_fan_block for why we copy lexical-scalar tracking sets.
1594        let lex_scalars = self.interp.english_lexical_scalars_clone();
1595        let our_scalars = self.interp.our_lexical_scalars_clone();
1596        let fan_progress = FanProgress::new(progress, n);
1597        let pairs: Vec<(usize, Result<StrykeValue, FlowOrError>)> = (0..n)
1598            .into_par_iter()
1599            .map(|i| {
1600                fan_progress.start_worker(i);
1601                let mut local_interp = VMHelper::new();
1602                local_interp.subs = subs.clone();
1603                local_interp.suppress_stdout = progress;
1604                local_interp.scope.restore_capture(&scope_capture);
1605                local_interp
1606                    .scope
1607                    .restore_atomics(&atomic_arrays, &atomic_hashes);
1608                local_interp.set_english_lexical_scalars(lex_scalars.clone());
1609                local_interp.set_our_lexical_scalars(our_scalars.clone());
1610                local_interp.enable_parallel_guard();
1611                local_interp.scope.set_topic(StrykeValue::integer(i as i64));
1612                crate::parallel_trace::fan_worker_set_index(Some(i as i64));
1613                local_interp.scope_push_hook();
1614                let res = local_interp.exec_block_no_scope(&block);
1615                local_interp.scope_pop_hook();
1616                crate::parallel_trace::fan_worker_set_index(None);
1617                fan_progress.finish_worker(i);
1618                (i, res)
1619            })
1620            .collect();
1621        fan_progress.finish();
1622        let mut pairs = pairs;
1623        pairs.sort_by_key(|(i, _)| *i);
1624        let mut out = Vec::with_capacity(n);
1625        for (_, r) in pairs {
1626            match r {
1627                Ok(v) => out.push(v),
1628                Err(e) => {
1629                    let stryke = match e {
1630                        FlowOrError::Error(stryke) => stryke,
1631                        FlowOrError::Flow(_) => PerlError::runtime(
1632                            "return/last/next/redo not supported inside fan_cap block",
1633                            line,
1634                        ),
1635                    };
1636                    return Err(stryke);
1637                }
1638            }
1639        }
1640        self.push(StrykeValue::array(out));
1641        Ok(())
1642    }
1643
1644    fn require_scalar_mutable(&self, name: &str) -> PerlResult<()> {
1645        if self.interp.scope.is_scalar_frozen(name) {
1646            return Err(PerlError::syntax(
1647                format!("cannot assign to frozen variable `${}`", name),
1648                self.line(),
1649            ));
1650        }
1651        Ok(())
1652    }
1653
1654    fn require_array_mutable(&self, name: &str) -> PerlResult<()> {
1655        if self.interp.scope.is_array_frozen(name) {
1656            return Err(PerlError::syntax(
1657                format!("cannot modify frozen array `@{}`", name),
1658                self.line(),
1659            ));
1660        }
1661        Ok(())
1662    }
1663
1664    fn require_hash_mutable(&self, name: &str) -> PerlResult<()> {
1665        if self.interp.scope.is_hash_frozen(name) || Self::is_reflection_hash(name) {
1666            return Err(PerlError::syntax(
1667                format!("cannot modify frozen hash `%{}`", name),
1668                self.line(),
1669            ));
1670        }
1671        Ok(())
1672    }
1673
1674    /// Reflection hashes are frozen builtins even before lazy init.
1675    fn is_reflection_hash(name: &str) -> bool {
1676        matches!(name, "b" | "pc" | "e" | "a" | "d" | "c" | "p" | "all")
1677            || name.starts_with("stryke::")
1678    }
1679
1680    /// Run bytecode: first attempts Cranelift method JIT for eligible numeric fragments (unless
1681    /// [`VM::set_jit_enabled`] disabled it). For block JIT, `block_jit_validate` runs once per attempt;
1682    /// buffers may use `StrykeValue::raw_bits` for `defined`-style control flow. Then the main opcode
1683    /// interpreter loop.
1684    pub fn execute(&mut self) -> PerlResult<StrykeValue> {
1685        let ops_ref: &Vec<Op> = &self.ops;
1686        let ops = ops_ref as *const Vec<Op>;
1687        // SAFETY: ops doesn't change during execution; pointer avoids borrow on self
1688        let ops = unsafe { &*ops };
1689        let names_ref: &Vec<String> = &self.names;
1690        let names = names_ref as *const Vec<String>;
1691        // SAFETY: names doesn't change during execution; pointer avoids borrow on self
1692        let names = unsafe { &*names };
1693        let constants_ref: &Vec<StrykeValue> = &self.constants;
1694        let constants = constants_ref as *const Vec<StrykeValue>;
1695        // SAFETY: constants doesn't change during execution; pointer avoids borrow on self
1696        let constants = unsafe { &*constants };
1697        let mut last = StrykeValue::UNDEF;
1698        // Safety limit: [`run_main_dispatch_loop`] counts ops (1B cap).
1699        let mut op_count: u64 = 0;
1700
1701        // Match Perl signal delivery: deliver `%SIG` and set `$^C` latch (Unix).
1702        crate::perl_signal::poll(self.interp)?;
1703        if self.jit_enabled {
1704            let mut top_slot_len: Option<usize> = None;
1705            if let Some(max) = crate::jit::linear_slot_ops_max_index(ops) {
1706                let n = max as usize + 1;
1707                self.jit_buf_slot.resize(n, 0);
1708                let mut ok = true;
1709                for i in 0..=max {
1710                    let pv = self.interp.scope.get_scalar_slot(i);
1711                    self.jit_buf_slot[i as usize] = match pv.as_integer() {
1712                        Some(v) => v,
1713                        None if pv.is_undef() && crate::jit::slot_undef_prefill_ok(ops, i) => 0,
1714                        None => {
1715                            ok = false;
1716                            break;
1717                        }
1718                    };
1719                }
1720                if ok {
1721                    top_slot_len = Some(n);
1722                }
1723            }
1724
1725            let mut top_plain_len: Option<usize> = None;
1726            if let Some(max) = crate::jit::linear_plain_ops_max_index(ops) {
1727                if (max as usize) < names.len() {
1728                    let n = max as usize + 1;
1729                    self.jit_buf_plain.resize(n, 0);
1730                    let mut ok = true;
1731                    for i in 0..=max {
1732                        let nm = names[i as usize].as_str();
1733                        match self.interp.scope.get_scalar(nm).as_integer() {
1734                            Some(v) => self.jit_buf_plain[i as usize] = v,
1735                            None => {
1736                                ok = false;
1737                                break;
1738                            }
1739                        }
1740                    }
1741                    if ok {
1742                        top_plain_len = Some(n);
1743                    }
1744                }
1745            }
1746
1747            let mut top_arg_len: Option<usize> = None;
1748            if let Some(max) = crate::jit::linear_arg_ops_max_index(ops) {
1749                if let Some(frame) = self.call_stack.last() {
1750                    let base = frame.stack_base;
1751                    let n = max as usize + 1;
1752                    self.jit_buf_arg.resize(n, 0);
1753                    let mut ok = true;
1754                    for i in 0..=max {
1755                        let pos = base + i as usize;
1756                        let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1757                        match pv.as_integer() {
1758                            Some(v) => self.jit_buf_arg[i as usize] = v,
1759                            None => {
1760                                ok = false;
1761                                break;
1762                            }
1763                        }
1764                    }
1765                    if ok {
1766                        top_arg_len = Some(n);
1767                    }
1768                }
1769            }
1770
1771            let slot_buf = top_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1772            let plain_buf = top_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1773            let arg_buf = top_arg_len.map(|n| &self.jit_buf_arg[..n]);
1774
1775            if let Some(v) =
1776                crate::jit::try_run_linear_ops(ops, slot_buf, plain_buf, arg_buf, constants)
1777            {
1778                if let Some(n) = top_slot_len {
1779                    let buf = &self.jit_buf_slot[..n];
1780                    for idx in crate::jit::linear_slot_ops_written_indices(ops) {
1781                        self.interp
1782                            .scope
1783                            .set_scalar_slot(idx, StrykeValue::integer(buf[idx as usize]));
1784                    }
1785                }
1786                if let Some(n) = top_plain_len {
1787                    let buf = &self.jit_buf_plain[..n];
1788                    for idx in crate::jit::linear_plain_ops_written_indices(ops) {
1789                        let name = names[idx as usize].as_str();
1790                        self.interp
1791                            .scope
1792                            .set_scalar(name, StrykeValue::integer(buf[idx as usize]))?;
1793                    }
1794                }
1795                return Ok(v);
1796            }
1797
1798            // ── Block JIT: try to compile sequences with control flow (loops, conditionals). ──
1799            if let Some(validated) =
1800                crate::jit::block_jit_validate(ops, constants, &self.sub_entries)
1801            {
1802                let block_buf_mode = validated.buffer_mode();
1803
1804                let mut top_b_slot_len: Option<usize> = None;
1805                if let Some(max) = crate::jit::block_slot_ops_max_index(ops) {
1806                    let n = max as usize + 1;
1807                    self.jit_buf_slot.resize(n, 0);
1808                    let mut ok = true;
1809                    for i in 0..=max {
1810                        let pv = self.interp.scope.get_scalar_slot(i);
1811                        self.jit_buf_slot[i as usize] = match block_buf_mode {
1812                            crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1813                                pv.raw_bits() as i64
1814                            }
1815                            crate::jit::BlockJitBufferMode::I64AsInteger => match pv.as_integer() {
1816                                Some(v) => v,
1817                                None if pv.is_undef()
1818                                    && crate::jit::block_slot_undef_prefill_ok(ops, i) =>
1819                                {
1820                                    0
1821                                }
1822                                None => {
1823                                    ok = false;
1824                                    break;
1825                                }
1826                            },
1827                        };
1828                    }
1829                    if ok {
1830                        top_b_slot_len = Some(n);
1831                    }
1832                }
1833
1834                let mut top_b_plain_len: Option<usize> = None;
1835                if let Some(max) = crate::jit::block_plain_ops_max_index(ops) {
1836                    if (max as usize) < names.len() {
1837                        let n = max as usize + 1;
1838                        self.jit_buf_plain.resize(n, 0);
1839                        let mut ok = true;
1840                        for i in 0..=max {
1841                            let nm = names[i as usize].as_str();
1842                            let pv = self.interp.scope.get_scalar(nm);
1843                            self.jit_buf_plain[i as usize] = match block_buf_mode {
1844                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1845                                    pv.raw_bits() as i64
1846                                }
1847                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1848                                    match pv.as_integer() {
1849                                        Some(v) => v,
1850                                        None => {
1851                                            ok = false;
1852                                            break;
1853                                        }
1854                                    }
1855                                }
1856                            };
1857                        }
1858                        if ok {
1859                            top_b_plain_len = Some(n);
1860                        }
1861                    }
1862                }
1863
1864                let mut top_b_arg_len: Option<usize> = None;
1865                if let Some(max) = crate::jit::block_arg_ops_max_index(ops) {
1866                    if let Some(frame) = self.call_stack.last() {
1867                        let base = frame.stack_base;
1868                        let n = max as usize + 1;
1869                        self.jit_buf_arg.resize(n, 0);
1870                        let mut ok = true;
1871                        for i in 0..=max {
1872                            let pos = base + i as usize;
1873                            let pv = self.stack.get(pos).cloned().unwrap_or(StrykeValue::UNDEF);
1874                            self.jit_buf_arg[i as usize] = match block_buf_mode {
1875                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1876                                    pv.raw_bits() as i64
1877                                }
1878                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1879                                    match pv.as_integer() {
1880                                        Some(v) => v,
1881                                        None => {
1882                                            ok = false;
1883                                            break;
1884                                        }
1885                                    }
1886                                }
1887                            };
1888                        }
1889                        if ok {
1890                            top_b_arg_len = Some(n);
1891                        }
1892                    }
1893                }
1894
1895                let vm_ptr = self as *mut VM<'_> as *mut std::ffi::c_void;
1896                let block_slot_buf = top_b_slot_len.map(|n| &mut self.jit_buf_slot[..n]);
1897                let block_plain_buf = top_b_plain_len.map(|n| &mut self.jit_buf_plain[..n]);
1898                let block_arg_buf = top_b_arg_len.map(|n| &self.jit_buf_arg[..n]);
1899
1900                if let Some((v, buf_mode)) = crate::jit::try_run_block_ops(
1901                    ops,
1902                    block_slot_buf,
1903                    block_plain_buf,
1904                    block_arg_buf,
1905                    constants,
1906                    Some(validated),
1907                    vm_ptr,
1908                    &self.sub_entries,
1909                ) {
1910                    if let Some(n) = top_b_slot_len {
1911                        let buf = &self.jit_buf_slot[..n];
1912                        for idx in crate::jit::block_slot_ops_written_indices(ops) {
1913                            let bits = buf[idx as usize] as u64;
1914                            let pv = match buf_mode {
1915                                crate::jit::BlockJitBufferMode::I64AsStrykeValueBits => {
1916                                    StrykeValue::from_raw_bits(bits)
1917                                }
1918                                crate::jit::BlockJitBufferMode::I64AsInteger => {
1919                                    StrykeValue::integer(buf[idx as usize])
1920                                }
1921                            };
1922                            self.interp.scope.set_scalar_slot(idx, pv);
1923                        }
1924                    }
1925                    if let Some(n) = top_b_plain_len {
1926                        let buf = &self.jit_buf_plain[..n];
1927                        for idx in crate::jit::block_plain_ops_written_indices(ops) {
1928                            let name = names[idx as usize].as_str();
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(name, pv)?;
1939                        }
1940                    }
1941                    return Ok(v);
1942                }
1943            }
1944        }
1945
1946        last = self.run_main_dispatch_loop(last, &mut op_count, true)?;
1947
1948        Ok(last)
1949    }
1950
1951    /// `die` / runtime errors inside `try` jump to `catch_ip` unless the error is [`ErrorKind::Exit`].
1952    fn try_recover_from_exception(&mut self, e: &PerlError) -> PerlResult<bool> {
1953        if matches!(e.kind, ErrorKind::Exit(_)) {
1954            return Ok(false);
1955        }
1956        let Some(frame) = self.try_stack.last() else {
1957            return Ok(false);
1958        };
1959        let Op::TryPush { catch_ip, .. } = &self.ops[frame.try_push_op_idx] else {
1960            return Ok(false);
1961        };
1962        self.pending_catch_error = Some(e.to_string());
1963        self.ip = *catch_ip;
1964        Ok(true)
1965    }
1966
1967    /// Stash lookup only (qualified key from compiler); avoids `resolve_sub_by_name`'s package fallback on hot calls.
1968    #[inline]
1969    fn sub_for_closure_restore(&self, name: &str) -> Option<Arc<PerlSub>> {
1970        self.interp.subs.get(name).cloned()
1971    }
1972
1973    /// AOP: run before-advice → original (or around) → after-advice for `name`.
1974    /// Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). Args are popped synchronously
1975    /// off the stack; the original is invoked via `Interpreter::call_sub` so the retval is
1976    /// available to after-advice and to the around block (via `proceed`).
1977    #[cold]
1978    fn dispatch_with_advice(
1979        &mut self,
1980        name: &str,
1981        closure_sub_hint: Option<Arc<PerlSub>>,
1982        argc: usize,
1983        want: WantarrayCtx,
1984        preserve_arrays: bool,
1985    ) -> PerlResult<()> {
1986        use crate::ast::AdviceKind;
1987
1988        let line = self.line();
1989
1990        let args = if preserve_arrays {
1991            self.pop_call_operands_preserved(argc)
1992        } else {
1993            self.pop_call_operands_flattened(argc)
1994        };
1995
1996        let sub_opt = closure_sub_hint.or_else(|| self.interp.resolve_sub_by_name(name));
1997
1998        let matching: Vec<crate::aop::Intercept> = self
1999            .interp
2000            .intercepts
2001            .iter()
2002            .filter(|i| crate::aop::glob_match(&i.pattern, name))
2003            .cloned()
2004            .collect();
2005
2006        // Context vars visible to advice bodies (mirrors zshrs INTERCEPT_NAME / INTERCEPT_ARGS).
2007        self.interp
2008            .scope
2009            .declare_scalar("INTERCEPT_NAME", StrykeValue::string(name.to_string()));
2010        self.interp
2011            .scope
2012            .declare_array("INTERCEPT_ARGS", args.clone());
2013
2014        self.interp.intercept_active_names.push(name.to_string());
2015
2016        // Run all matching `before` advices via the bytecode VM (`run_block_region`).
2017        // We never fall back to `interp.exec_block` here — the advice body must use the
2018        // same name resolution as the surrounding bytecode (see the source-level test
2019        // in `tests/tree_walker_absent_aop.rs`).
2020        for adv in matching
2021            .iter()
2022            .filter(|i| matches!(i.kind, AdviceKind::Before))
2023        {
2024            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2025                self.interp.intercept_active_names.pop();
2026                return Err(e);
2027            }
2028        }
2029
2030        let around = matching
2031            .iter()
2032            .find(|i| matches!(i.kind, AdviceKind::Around));
2033
2034        let t0 = std::time::Instant::now();
2035        let retval = if let Some(around) = around {
2036            self.interp
2037                .intercept_ctx_stack
2038                .push(crate::aop::InterceptCtx {
2039                    name: name.to_string(),
2040                    args: args.clone(),
2041                    proceeded: false,
2042                    retval: StrykeValue::UNDEF,
2043                });
2044            let exec_res = self.run_advice_body_bytecode(around, line);
2045            let _ctx = self.interp.intercept_ctx_stack.pop();
2046            // AspectJ-style: the around block's evaluated value is the call's return.
2047            // If the user wants to forward the original's value, they say `proceed()`
2048            // as the last expression; if they want to transform, `proceed() + 1`; if
2049            // they want to replace, just emit a value without calling proceed.
2050            match exec_res {
2051                Ok(v) => v,
2052                Err(e) => {
2053                    self.interp.intercept_active_names.pop();
2054                    return Err(e);
2055                }
2056            }
2057        } else if let Some(sub) = sub_opt {
2058            match self.interp.call_sub(&sub, args.clone(), want, line) {
2059                Ok(v) => v,
2060                Err(FlowOrError::Flow(Flow::Return(v))) => v,
2061                Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2062                Err(FlowOrError::Error(e)) => {
2063                    self.interp.intercept_active_names.pop();
2064                    return Err(e.at_line(line));
2065                }
2066            }
2067        } else {
2068            // Sub not resolvable — fall back to builtins (matches the non-advice fallback).
2069            let saved_wa_call = self.interp.wantarray_kind;
2070            self.interp.wantarray_kind = want;
2071            let r = crate::builtins::try_builtin(self.interp, name, &args, line);
2072            self.interp.wantarray_kind = saved_wa_call;
2073            match r {
2074                Some(Ok(v)) => v,
2075                Some(Err(e)) => {
2076                    self.interp.intercept_active_names.pop();
2077                    return Err(e.at_line(line));
2078                }
2079                None => {
2080                    self.interp.intercept_active_names.pop();
2081                    return Err(PerlError::runtime(
2082                        format!("undefined sub `{}` (advice fallback)", name),
2083                        line,
2084                    ));
2085                }
2086            }
2087        };
2088        let elapsed = t0.elapsed();
2089
2090        // Timing context vars for after-advice (matches zshrs INTERCEPT_MS / INTERCEPT_US).
2091        self.interp.scope.declare_scalar(
2092            "INTERCEPT_MS",
2093            StrykeValue::float(elapsed.as_secs_f64() * 1000.0),
2094        );
2095        self.interp.scope.declare_scalar(
2096            "INTERCEPT_US",
2097            StrykeValue::integer(elapsed.as_micros() as i64),
2098        );
2099        self.interp
2100            .scope
2101            .declare_scalar("INTERCEPT_RESULT", retval.clone());
2102
2103        for adv in matching
2104            .iter()
2105            .filter(|i| matches!(i.kind, AdviceKind::After))
2106        {
2107            if let Err(e) = self.run_advice_body_bytecode(adv, line) {
2108                self.interp.intercept_active_names.pop();
2109                return Err(e);
2110            }
2111        }
2112
2113        self.interp.intercept_active_names.pop();
2114        self.push(retval);
2115        Ok(())
2116    }
2117
2118    /// Dispatch one advice body through the VM bytecode helper (`run_block_region`),
2119    /// the same path used by `map { }` / `grep { }` blocks. Always returns the body's
2120    /// final value on success. The body is required to have a lowered bytecode region
2121    /// (`Chunk::block_bytecode_ranges[idx]`) — the compiler's fourth pass populates
2122    /// this for every chunk block, so the only reason it would be missing is if the
2123    /// body contains a construct the lowering rejects (e.g. a literal `return`); in
2124    /// that case we error out loudly rather than silently fall back to the
2125    /// tree-walker. See `tests/tree_walker_absent_aop.rs`.
2126    #[inline]
2127    fn run_advice_body_bytecode(
2128        &mut self,
2129        adv: &crate::aop::Intercept,
2130        line: usize,
2131    ) -> PerlResult<StrykeValue> {
2132        let idx = adv.body_block_idx as usize;
2133        let range = self
2134            .block_bytecode_ranges
2135            .get(idx)
2136            .copied()
2137            .flatten()
2138            .ok_or_else(|| {
2139                PerlError::runtime(
2140                    format!(
2141                        "AOP {} advice body for `{}` could not be lowered to bytecode \
2142                         (likely contains a construct unsupported by block lowering, \
2143                         e.g. a literal `return`); rewrite the body without it",
2144                        match adv.kind {
2145                            crate::ast::AdviceKind::Before => "before",
2146                            crate::ast::AdviceKind::After => "after",
2147                            crate::ast::AdviceKind::Around => "around",
2148                        },
2149                        adv.pattern,
2150                    ),
2151                    line,
2152                )
2153            })?;
2154        let mut op_count: u64 = 0;
2155        self.run_block_region(range.0, range.1, &mut op_count)
2156    }
2157
2158    fn vm_dispatch_user_call(
2159        &mut self,
2160        name_idx: u16,
2161        entry_opt: Option<(usize, bool)>,
2162        argc_u8: u8,
2163        wa_byte: u8,
2164        // Pre-resolved sub for `Op::CallStaticSubId` (stash lookup once in `VM::new`).
2165        closure_sub_hint: Option<Arc<PerlSub>>,
2166    ) -> PerlResult<()> {
2167        let name_owned = self.names[name_idx as usize].clone();
2168        let name = name_owned.as_str();
2169        let argc = argc_u8 as usize;
2170        let want = WantarrayCtx::from_byte(wa_byte);
2171
2172        // AOP advice path: at least one matching intercept and no re-entrancy guard for `name`.
2173        // Mirrors zshrs `run_intercepts` (exec.rs:14656-14759). The fast-path skip below is the
2174        // common case (no intercepts registered); when the registry is non-empty we still bail
2175        // out cheaply unless a glob actually matches.
2176        if !self.interp.intercepts.is_empty()
2177            && !self.interp.intercept_active_names.iter().any(|n| n == name)
2178            && self
2179                .interp
2180                .intercepts
2181                .iter()
2182                .any(|i| crate::aop::glob_match(&i.pattern, name))
2183        {
2184            let preserve = Self::call_preserve_operand_arrays(name);
2185            return self.dispatch_with_advice(&name_owned, closure_sub_hint, argc, want, preserve);
2186        }
2187
2188        if let Some((entry_ip, stack_args)) = entry_opt {
2189            let saved_wa = self.interp.wantarray_kind;
2190            let sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2191            if let Some(p) = &mut self.interp.profiler {
2192                p.enter_sub(name);
2193            }
2194            self.interp.debugger_enter_sub(name);
2195
2196            // Fib-shaped recursive-add fast path: if the target sub is tagged with a
2197            // `fib_like` pattern (detected at sub-registration time in the compiler and
2198            // cached in `static_sub_closure_subs`), skip frame setup entirely and
2199            // evaluate the closed-form-ish iterative version. `bench_fib` collapses from
2200            // ~2.7M recursive VM calls to a single `while` loop.
2201            let fib_sub: Option<Arc<PerlSub>> = closure_sub_hint
2202                .clone()
2203                .or_else(|| self.sub_for_closure_restore(name));
2204            if let Some(ref sub_arc) = fib_sub {
2205                if let Some(pat) = sub_arc.fib_like.as_ref() {
2206                    // stack_args path pushes exactly `argc` ints; non-stack_args pops them
2207                    // off the stack into @_. Only the argc==1 / integer case qualifies.
2208                    if argc == 1 {
2209                        let top_idx = self.stack.len().saturating_sub(1);
2210                        if let Some(n0) = self.stack.get(top_idx).and_then(|v| v.as_integer()) {
2211                            let result = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
2212                            // Drop the arg, push the result, keep wantarray as the caller had it.
2213                            self.stack.truncate(top_idx);
2214                            self.push(StrykeValue::integer(result));
2215                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, sub_prof_t0) {
2216                                p.exit_sub(t0.elapsed());
2217                            }
2218                            self.interp.debugger_leave_sub();
2219                            self.interp.wantarray_kind = saved_wa;
2220                            return Ok(());
2221                        }
2222                    }
2223                }
2224            }
2225
2226            if stack_args {
2227                let eff_argc = if argc == 0 {
2228                    self.push(self.interp.scope.get_scalar("_").clone());
2229                    1
2230                } else {
2231                    argc
2232                };
2233                let stack_base = self.stack.len() - eff_argc;
2234                self.call_stack.push(CallFrame {
2235                    return_ip: self.ip,
2236                    stack_base,
2237                    scope_depth: self.interp.scope.depth(),
2238                    saved_wantarray: saved_wa,
2239                    jit_trampoline_return: false,
2240                    block_region: false,
2241                    sub_profiler_start: sub_prof_t0,
2242                });
2243                self.interp.wantarray_kind = want;
2244                self.interp.scope_push_hook();
2245                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
2246                if let Some(ref sub) = closure_sub {
2247                    if let Some(ref env) = sub.closure_env {
2248                        self.interp.scope.restore_capture(env);
2249                    }
2250                    self.interp.current_sub_stack.push(sub.clone());
2251                }
2252                self.ip = entry_ip;
2253            } else {
2254                let args = if Self::call_preserve_operand_arrays(name) {
2255                    self.pop_call_operands_preserved(argc)
2256                } else {
2257                    self.pop_call_operands_flattened(argc)
2258                };
2259                // Only substitute $_ when the call site has no syntactic arguments (argc == 0).
2260                // When argc > 0 but args is empty (e.g., passing an empty array), keep args empty.
2261                let args = if argc == 0 {
2262                    self.interp.with_topic_default_args(args)
2263                } else {
2264                    args
2265                };
2266                self.call_stack.push(CallFrame {
2267                    return_ip: self.ip,
2268                    stack_base: self.stack.len(),
2269                    scope_depth: self.interp.scope.depth(),
2270                    saved_wantarray: saved_wa,
2271                    jit_trampoline_return: false,
2272                    block_region: false,
2273                    sub_profiler_start: sub_prof_t0,
2274                });
2275                self.interp.wantarray_kind = want;
2276                self.interp.scope_push_hook();
2277                self.interp.scope.declare_array("_", args);
2278                let closure_sub = closure_sub_hint.or_else(|| self.sub_for_closure_restore(name));
2279                if let Some(ref sub) = closure_sub {
2280                    if let Some(ref env) = sub.closure_env {
2281                        self.interp.scope.restore_capture(env);
2282                    }
2283                    let line = self.line();
2284                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
2285                    self.interp
2286                        .apply_sub_signature(sub.as_ref(), &argv, line)
2287                        .map_err(|e| e.at_line(line))?;
2288                    self.interp.scope.declare_array("_", argv.clone());
2289                    self.interp.scope.set_closure_args(&argv);
2290                    self.interp.current_sub_stack.push(sub.clone());
2291                }
2292                self.ip = entry_ip;
2293            }
2294        } else {
2295            let args = if Self::call_preserve_operand_arrays(name) {
2296                self.pop_call_operands_preserved(argc)
2297            } else {
2298                self.pop_call_operands_flattened(argc)
2299            };
2300
2301            let saved_wa_call = self.interp.wantarray_kind;
2302            self.interp.wantarray_kind = want;
2303            if let Some(r) = crate::builtins::try_builtin(self.interp, name, &args, self.line()) {
2304                self.interp.wantarray_kind = saved_wa_call;
2305                self.push(r?);
2306            } else {
2307                self.interp.wantarray_kind = saved_wa_call;
2308                if let Some(sub) = self.interp.resolve_sub_by_name(name) {
2309                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2310                    if let Some(p) = &mut self.interp.profiler {
2311                        p.enter_sub(name);
2312                    }
2313                    self.interp.debugger_enter_sub(name);
2314                    // Only substitute $_ when argc == 0; passing an empty array keeps args empty.
2315                    let args = if argc == 0 {
2316                        self.interp.with_topic_default_args(args)
2317                    } else {
2318                        args
2319                    };
2320                    let saved_wa = self.interp.wantarray_kind;
2321                    self.interp.wantarray_kind = want;
2322                    self.interp.scope_push_hook();
2323                    self.interp.scope.declare_array("_", args);
2324                    if let Some(ref env) = sub.closure_env {
2325                        self.interp.scope.restore_capture(env);
2326                    }
2327                    let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
2328                    let line = self.line();
2329                    self.interp
2330                        .apply_sub_signature(&sub, &argv, line)
2331                        .map_err(|e| e.at_line(line))?;
2332                    let result = {
2333                        self.interp.scope.declare_array("_", argv.clone());
2334                        self.interp.scope.set_closure_args(&argv);
2335                        self.interp
2336                            .exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List)
2337                    };
2338                    self.interp.wantarray_kind = saved_wa;
2339                    self.interp.scope_pop_hook();
2340                    match result {
2341                        Ok(v) => self.push(v),
2342                        Err(crate::vm_helper::FlowOrError::Flow(
2343                            crate::vm_helper::Flow::Return(v),
2344                        )) => self.push(v),
2345                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2346                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2347                                p.exit_sub(t0.elapsed());
2348                            }
2349                            self.interp.debugger_leave_sub();
2350                            return Err(e);
2351                        }
2352                        Err(_) => self.push(StrykeValue::UNDEF),
2353                    }
2354                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2355                        p.exit_sub(t0.elapsed());
2356                    }
2357                    self.interp.debugger_leave_sub();
2358                } else if !name.contains("::")
2359                    && matches!(
2360                        name,
2361                        "uniq"
2362                            | "distinct"
2363                            | "uniqstr"
2364                            | "uniqint"
2365                            | "uniqnum"
2366                            | "shuffle"
2367                            | "sample"
2368                            | "chunked"
2369                            | "windowed"
2370                            | "zip"
2371                            | "zip_shortest"
2372                            | "zip_longest"
2373                            | "mesh"
2374                            | "mesh_shortest"
2375                            | "mesh_longest"
2376                            | "any"
2377                            | "all"
2378                            | "none"
2379                            | "notall"
2380                            | "first"
2381                            | "reduce"
2382                            | "reductions"
2383                            | "sum"
2384                            | "sum0"
2385                            | "product"
2386                            | "min"
2387                            | "max"
2388                            | "minstr"
2389                            | "maxstr"
2390                            | "mean"
2391                            | "median"
2392                            | "mode"
2393                            | "stddev"
2394                            | "variance"
2395                            | "pairs"
2396                            | "unpairs"
2397                            | "pairkeys"
2398                            | "pairvalues"
2399                            | "pairgrep"
2400                            | "pairmap"
2401                            | "pairfirst"
2402                            // Scalar/Sub/utf8-utility bare builtins (no module — direct names)
2403                            | "blessed"
2404                            | "refaddr"
2405                            | "reftype"
2406                            | "weaken"
2407                            | "unweaken"
2408                            | "isweak"
2409                            | "set_subname"
2410                            | "subname"
2411                            | "unicode_to_native"
2412                    )
2413                {
2414                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2415                    if let Some(p) = &mut self.interp.profiler {
2416                        p.enter_sub(name);
2417                    }
2418                    self.interp.debugger_enter_sub(name);
2419                    let saved_wa = self.interp.wantarray_kind;
2420                    self.interp.wantarray_kind = want;
2421                    let out = self
2422                        .interp
2423                        .call_bare_list_builtin(name, args, self.line(), want);
2424                    self.interp.wantarray_kind = saved_wa;
2425                    match out {
2426                        Ok(v) => self.push(v),
2427                        Err(crate::vm_helper::FlowOrError::Flow(
2428                            crate::vm_helper::Flow::Return(v),
2429                        )) => self.push(v),
2430                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2431                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2432                                p.exit_sub(t0.elapsed());
2433                            }
2434                            self.interp.debugger_leave_sub();
2435                            return Err(e);
2436                        }
2437                        Err(_) => self.push(StrykeValue::UNDEF),
2438                    }
2439                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2440                        p.exit_sub(t0.elapsed());
2441                    }
2442                    self.interp.debugger_leave_sub();
2443                } else if let Some(result) = self.interp.try_autoload_call(
2444                    name,
2445                    if argc == 0 {
2446                        self.interp.with_topic_default_args(args.clone())
2447                    } else {
2448                        args.clone()
2449                    },
2450                    self.line(),
2451                    want,
2452                    None,
2453                ) {
2454                    let t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2455                    if let Some(p) = &mut self.interp.profiler {
2456                        p.enter_sub(name);
2457                    }
2458                    self.interp.debugger_enter_sub(name);
2459                    match result {
2460                        Ok(v) => self.push(v),
2461                        Err(crate::vm_helper::FlowOrError::Flow(
2462                            crate::vm_helper::Flow::Return(v),
2463                        )) => self.push(v),
2464                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2465                            if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2466                                p.exit_sub(t0.elapsed());
2467                            }
2468                            self.interp.debugger_leave_sub();
2469                            return Err(e);
2470                        }
2471                        Err(_) => self.push(StrykeValue::UNDEF),
2472                    }
2473                    if let (Some(p), Some(t0)) = (&mut self.interp.profiler, t0) {
2474                        p.exit_sub(t0.elapsed());
2475                    }
2476                    self.interp.debugger_leave_sub();
2477                } else if let Some(def) = self.interp.struct_defs.get(name).cloned() {
2478                    // Struct constructor: Point(x => 1, y => 2) or Point(1, 2)
2479                    let result = self.interp.struct_construct(&def, args, self.line());
2480                    match result {
2481                        Ok(v) => self.push(v),
2482                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2483                        _ => self.push(StrykeValue::UNDEF),
2484                    }
2485                } else if let Some(def) = self.interp.class_defs.get(name).cloned() {
2486                    // Class constructor: Dog(name => "Rex") or Dog("Rex", 5)
2487                    let result = self.interp.class_construct(&def, args, self.line());
2488                    match result {
2489                        Ok(v) => self.push(v),
2490                        Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2491                        _ => self.push(StrykeValue::UNDEF),
2492                    }
2493                } else if let Some((prefix, suffix)) = name.rsplit_once("::") {
2494                    // Enum variant constructor: Color::Red or Maybe::Some(value)
2495                    if let Some(def) = self.interp.enum_defs.get(prefix).cloned() {
2496                        let result = self.interp.enum_construct(&def, suffix, args, self.line());
2497                        match result {
2498                            Ok(v) => self.push(v),
2499                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
2500                            _ => self.push(StrykeValue::UNDEF),
2501                        }
2502                    // Static class method: Math::add(...)
2503                    } else if let Some(def) = self.interp.class_defs.get(prefix).cloned() {
2504                        if let Some(m) = def.method(suffix) {
2505                            if m.is_static {
2506                                if let Some(ref body) = m.body {
2507                                    let params = m.params.clone();
2508                                    match self.interp.call_static_class_method(
2509                                        body,
2510                                        &params,
2511                                        args.clone(),
2512                                        self.line(),
2513                                    ) {
2514                                        Ok(v) => self.push(v),
2515                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
2516                                            return Err(e)
2517                                        }
2518                                        Err(crate::vm_helper::FlowOrError::Flow(
2519                                            crate::vm_helper::Flow::Return(v),
2520                                        )) => self.push(v),
2521                                        _ => self.push(StrykeValue::UNDEF),
2522                                    }
2523                                } else {
2524                                    self.push(StrykeValue::UNDEF);
2525                                }
2526                            } else {
2527                                return Err(PerlError::runtime(
2528                                    format!("method `{}` is not static", suffix),
2529                                    self.line(),
2530                                ));
2531                            }
2532                        } else if def.static_fields.iter().any(|sf| sf.name == suffix) {
2533                            // Static field access: getter (0 args) or setter (1 arg)
2534                            let key = format!("{}::{}", prefix, suffix);
2535                            match args.len() {
2536                                0 => {
2537                                    let val = self.interp.scope.get_scalar(&key);
2538                                    self.push(val);
2539                                }
2540                                1 => {
2541                                    let _ = self.interp.scope.set_scalar(&key, args[0].clone());
2542                                    self.push(args[0].clone());
2543                                }
2544                                _ => {
2545                                    return Err(PerlError::runtime(
2546                                        format!(
2547                                            "static field `{}::{}` takes 0 or 1 arguments",
2548                                            prefix, suffix
2549                                        ),
2550                                        self.line(),
2551                                    ));
2552                                }
2553                            }
2554                        } else {
2555                            return Err(PerlError::runtime(
2556                                self.interp.undefined_subroutine_call_message(name),
2557                                self.line(),
2558                            ));
2559                        }
2560                    } else {
2561                        return Err(PerlError::runtime(
2562                            self.interp.undefined_subroutine_call_message(name),
2563                            self.line(),
2564                        ));
2565                    }
2566                } else {
2567                    return Err(PerlError::runtime(
2568                        self.interp.undefined_subroutine_call_message(name),
2569                        self.line(),
2570                    ));
2571                }
2572            }
2573        }
2574        Ok(())
2575    }
2576
2577    #[inline]
2578    fn push_binop_with_overload<F>(
2579        &mut self,
2580        op: BinOp,
2581        a: StrykeValue,
2582        b: StrykeValue,
2583        default: F,
2584    ) -> PerlResult<()>
2585    where
2586        F: FnOnce(&StrykeValue, &StrykeValue) -> PerlResult<StrykeValue>,
2587    {
2588        let line = self.line();
2589        if let Some(exec_res) = self.interp.try_overload_binop(op, &a, &b, line) {
2590            self.push(vm_interp_result(exec_res, line)?);
2591        } else {
2592            self.push(default(&a, &b)?);
2593        }
2594        Ok(())
2595    }
2596
2597    pub(crate) fn concat_stack_values(
2598        &mut self,
2599        a: StrykeValue,
2600        b: StrykeValue,
2601    ) -> PerlResult<StrykeValue> {
2602        let line = self.line();
2603        if let Some(exec_res) = self.interp.try_overload_binop(BinOp::Concat, &a, &b, line) {
2604            vm_interp_result(exec_res, line)
2605        } else {
2606            let sa = match self.interp.stringify_value(a, line) {
2607                Ok(s) => s,
2608                Err(FlowOrError::Error(e)) => return Err(e),
2609                Err(FlowOrError::Flow(_)) => {
2610                    return Err(PerlError::runtime("concat: unexpected control flow", line));
2611                }
2612            };
2613            let sb = match self.interp.stringify_value(b, line) {
2614                Ok(s) => s,
2615                Err(FlowOrError::Error(e)) => return Err(e),
2616                Err(FlowOrError::Flow(_)) => {
2617                    return Err(PerlError::runtime("concat: unexpected control flow", line));
2618                }
2619            };
2620            let mut s = sa;
2621            s.push_str(&sb);
2622            Ok(StrykeValue::string(s))
2623        }
2624    }
2625
2626    fn run_main_dispatch_loop(
2627        &mut self,
2628        mut last: StrykeValue,
2629        op_count: &mut u64,
2630        init_dispatch: bool,
2631    ) -> PerlResult<StrykeValue> {
2632        if init_dispatch {
2633            self.halt = false;
2634            self.exit_main_dispatch = false;
2635            self.exit_main_dispatch_value = None;
2636        }
2637        let ops_ref: &Vec<Op> = &self.ops;
2638        let ops = ops_ref as *const Vec<Op>;
2639        let ops = unsafe { &*ops };
2640        let names_ref: &Vec<String> = &self.names;
2641        let names = names_ref as *const Vec<String>;
2642        let names = unsafe { &*names };
2643        let constants_ref: &Vec<StrykeValue> = &self.constants;
2644        let constants = constants_ref as *const Vec<StrykeValue>;
2645        let constants = unsafe { &*constants };
2646        let len = ops.len();
2647        const MAX_OPS: u64 = 1_000_000_000;
2648        loop {
2649            if self.jit_trampoline_depth > 0 && self.jit_trampoline_out.is_some() {
2650                break;
2651            }
2652            if self.block_region_return.is_some() {
2653                break;
2654            }
2655            if self.block_region_mode && self.ip >= self.block_region_end {
2656                return Err(PerlError::runtime(
2657                    "block bytecode region fell through without BlockReturnValue",
2658                    self.line(),
2659                ));
2660            }
2661            if self.ip >= len {
2662                break;
2663            }
2664
2665            if !self.block_region_mode
2666                && self.jit_enabled
2667                && self.sub_entry_at_ip.get(self.ip).copied().unwrap_or(false)
2668            {
2669                let sub_ip = self.ip;
2670                if sub_ip >= self.sub_entry_invoke_count.len() {
2671                    self.sub_entry_invoke_count.resize(sub_ip + 1, 0);
2672                }
2673                let c = &mut self.sub_entry_invoke_count[sub_ip];
2674                if *c <= self.jit_sub_invoke_threshold {
2675                    *c = c.saturating_add(1);
2676                }
2677                let should_try_jit = *c > self.jit_sub_invoke_threshold
2678                    && (!self.sub_jit_skip_linear_test(sub_ip)
2679                        || !self.sub_jit_skip_block_test(sub_ip));
2680                if should_try_jit {
2681                    if !self.sub_jit_skip_linear_test(sub_ip) && self.try_jit_subroutine_linear()? {
2682                        continue;
2683                    }
2684                    if !self.sub_jit_skip_block_test(sub_ip) && self.try_jit_subroutine_block()? {
2685                        continue;
2686                    }
2687                }
2688            }
2689
2690            *op_count += 1;
2691            // `%SIG` delivery and the execution cap: same cadence as the old per-op poll (signals
2692            // remain responsive; hot loops avoid a syscall/atomic path every opcode).
2693            if (*op_count & 0x3FF) == 0 {
2694                crate::perl_signal::poll(self.interp)?;
2695                if *op_count > MAX_OPS {
2696                    return Err(PerlError::runtime(
2697                        "VM execution limit exceeded (possible infinite loop)",
2698                        self.line(),
2699                    ));
2700                }
2701            }
2702
2703            let ip_before = self.ip;
2704            let line = self.lines.get(ip_before).copied().unwrap_or(0);
2705            let op = &ops[self.ip];
2706            self.ip += 1;
2707
2708            // Debugger hook: check if we should stop at this line
2709            if let Some(ref mut dbg) = self.interp.debugger {
2710                if dbg.should_stop(line) {
2711                    let call_stack = self.interp.debug_call_stack.clone();
2712                    match dbg.prompt(line, &self.interp.scope, &call_stack) {
2713                        crate::debugger::DebugAction::Quit => {
2714                            return Err(PerlError::runtime("debugger: quit", line));
2715                        }
2716                        crate::debugger::DebugAction::Continue => {}
2717                        crate::debugger::DebugAction::Prompt => {}
2718                    }
2719                }
2720            }
2721
2722            let op_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
2723            // Closure: `?` / `return Err` inside `match op` must not return from
2724            // `run_main_dispatch_loop` — they must become `__op_res` so `try_recover_from_exception`
2725            // can run before propagating.
2726            let __op_res: PerlResult<()> = (|| -> PerlResult<()> {
2727                match op {
2728                    Op::Nop => Ok(()),
2729                    // ── Constants ──
2730                    Op::LoadInt(n) => {
2731                        self.push(StrykeValue::integer(*n));
2732                        Ok(())
2733                    }
2734                    Op::LoadFloat(f) => {
2735                        self.push(StrykeValue::float(*f));
2736                        Ok(())
2737                    }
2738                    Op::LoadConst(idx) => {
2739                        self.push(self.constant(*idx).clone());
2740                        Ok(())
2741                    }
2742                    Op::LoadUndef => {
2743                        self.push(StrykeValue::UNDEF);
2744                        Ok(())
2745                    }
2746                    Op::RuntimeErrorConst(idx) => {
2747                        let msg = self.constant(*idx).to_string();
2748                        let line = self.line();
2749                        Err(crate::error::PerlError::runtime(msg, line))
2750                    }
2751                    Op::BarewordRvalue(name_idx) => {
2752                        let name = names[*name_idx as usize].clone();
2753                        let line = self.line();
2754                        let out = vm_interp_result(
2755                            self.interp.resolve_bareword_rvalue(
2756                                &name,
2757                                crate::vm_helper::WantarrayCtx::Scalar,
2758                                line,
2759                            ),
2760                            line,
2761                        )?;
2762                        self.push(out);
2763                        Ok(())
2764                    }
2765
2766                    // ── Stack ──
2767                    Op::Pop => {
2768                        let v = self.pop();
2769                        // Drain iterators used as void statements so side effects fire.
2770                        if v.is_iterator() {
2771                            let iter = v.into_iterator();
2772                            while iter.next_item().is_some() {}
2773                        }
2774                        Ok(())
2775                    }
2776                    Op::Dup => {
2777                        let v = self.peek().dup_stack();
2778                        self.push(v);
2779                        Ok(())
2780                    }
2781                    Op::Dup2 => {
2782                        let b = self.pop();
2783                        let a = self.pop();
2784                        self.push(a.dup_stack());
2785                        self.push(b.dup_stack());
2786                        self.push(a);
2787                        self.push(b);
2788                        Ok(())
2789                    }
2790                    Op::Swap => {
2791                        let top = self.pop();
2792                        let below = self.pop();
2793                        self.push(top);
2794                        self.push(below);
2795                        Ok(())
2796                    }
2797                    Op::Rot => {
2798                        let c = self.pop();
2799                        let b = self.pop();
2800                        let a = self.pop();
2801                        self.push(b);
2802                        self.push(c);
2803                        self.push(a);
2804                        Ok(())
2805                    }
2806                    Op::ValueScalarContext => {
2807                        let v = self.pop();
2808                        self.push(v.scalar_context());
2809                        Ok(())
2810                    }
2811                    Op::ListFirst => {
2812                        let v = self.pop();
2813                        let first = if let Some(arr) = v.as_array_vec() {
2814                            arr.first().cloned().unwrap_or(StrykeValue::UNDEF)
2815                        } else {
2816                            v
2817                        };
2818                        self.push(first);
2819                        Ok(())
2820                    }
2821
2822                    // ── Scalars ──
2823                    Op::GetScalar(idx) => {
2824                        let n = names[*idx as usize].as_str();
2825                        let val = self.interp.get_special_var(n);
2826                        self.push(val);
2827                        Ok(())
2828                    }
2829                    Op::GetScalarPlain(idx) => {
2830                        let n = names[*idx as usize].as_str();
2831                        let val = self.interp.scope.get_scalar(n);
2832                        self.push(val);
2833                        Ok(())
2834                    }
2835                    Op::SetScalar(idx) => {
2836                        let val = self.pop();
2837                        let n = names[*idx as usize].as_str();
2838                        self.require_scalar_mutable(n)?;
2839                        self.interp.maybe_invalidate_regex_capture_memo(n);
2840                        self.interp
2841                            .set_special_var(n, &val)
2842                            .map_err(|e| e.at_line(self.line()))?;
2843                        Ok(())
2844                    }
2845                    Op::SetScalarPlain(idx) => {
2846                        let val = self.pop();
2847                        let n = names[*idx as usize].as_str();
2848                        self.require_scalar_mutable(n)?;
2849                        self.interp.maybe_invalidate_regex_capture_memo(n);
2850                        self.interp
2851                            .scope
2852                            .set_scalar(n, val)
2853                            .map_err(|e| e.at_line(self.line()))?;
2854                        Ok(())
2855                    }
2856                    Op::SetScalarKeep(idx) => {
2857                        let val = self.peek().dup_stack();
2858                        let n = names[*idx as usize].as_str();
2859                        self.require_scalar_mutable(n)?;
2860                        self.interp.maybe_invalidate_regex_capture_memo(n);
2861                        self.interp
2862                            .set_special_var(n, &val)
2863                            .map_err(|e| e.at_line(self.line()))?;
2864                        Ok(())
2865                    }
2866                    Op::SetScalarKeepPlain(idx) => {
2867                        let val = self.peek().dup_stack();
2868                        let n = names[*idx as usize].as_str();
2869                        self.require_scalar_mutable(n)?;
2870                        self.interp.maybe_invalidate_regex_capture_memo(n);
2871                        self.interp
2872                            .scope
2873                            .set_scalar(n, val)
2874                            .map_err(|e| e.at_line(self.line()))?;
2875                        Ok(())
2876                    }
2877                    Op::DeclareScalar(idx) => {
2878                        let val = self.pop();
2879                        let n = names[*idx as usize].as_str();
2880                        self.interp
2881                            .scope
2882                            .declare_scalar_frozen(n, val, false, None)
2883                            .map_err(|e| e.at_line(self.line()))?;
2884                        Ok(())
2885                    }
2886                    Op::DeclareScalarFrozen(idx) => {
2887                        let val = self.pop();
2888                        let n = names[*idx as usize].as_str();
2889                        self.interp
2890                            .scope
2891                            .declare_scalar_frozen(n, val, true, None)
2892                            .map_err(|e| e.at_line(self.line()))?;
2893                        Ok(())
2894                    }
2895                    Op::DeclareScalarTyped(idx, tyb) => {
2896                        let val = self.pop();
2897                        let n = names[*idx as usize].as_str();
2898                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
2899                            PerlError::runtime(
2900                                format!("invalid typed scalar type byte {}", tyb),
2901                                self.line(),
2902                            )
2903                        })?;
2904                        self.interp
2905                            .scope
2906                            .declare_scalar_frozen(n, val, false, Some(ty))
2907                            .map_err(|e| e.at_line(self.line()))?;
2908                        Ok(())
2909                    }
2910                    Op::DeclareScalarTypedFrozen(idx, tyb) => {
2911                        let val = self.pop();
2912                        let n = names[*idx as usize].as_str();
2913                        let ty = PerlTypeName::from_byte(*tyb).ok_or_else(|| {
2914                            PerlError::runtime(
2915                                format!("invalid typed scalar type byte {}", tyb),
2916                                self.line(),
2917                            )
2918                        })?;
2919                        self.interp
2920                            .scope
2921                            .declare_scalar_frozen(n, val, true, Some(ty))
2922                            .map_err(|e| e.at_line(self.line()))?;
2923                        Ok(())
2924                    }
2925                    Op::DeclareScalarTypedUser(name_idx, type_idx, flag) => {
2926                        let val = self.pop();
2927                        let n = names[*name_idx as usize].as_str();
2928                        let type_name = names[*type_idx as usize].clone();
2929                        let is_enum = (flag & 0b01) != 0;
2930                        let is_frozen = (flag & 0b10) != 0;
2931                        let ty = if is_enum {
2932                            PerlTypeName::Enum(type_name)
2933                        } else {
2934                            // Struct variant covers struct, class, and any
2935                            // user-defined nominal type — `check_value` for
2936                            // `Struct(name)` already accepts class instances
2937                            // via `c.isa(name)`.
2938                            PerlTypeName::Struct(type_name)
2939                        };
2940                        self.interp
2941                            .scope
2942                            .declare_scalar_frozen(n, val, is_frozen, Some(ty))
2943                            .map_err(|e| e.at_line(self.line()))?;
2944                        Ok(())
2945                    }
2946
2947                    // ── State variables (persist across calls) ──
2948                    Op::DeclareStateScalar(idx) => {
2949                        let init_val = self.pop();
2950                        let n = names[*idx as usize].as_str();
2951                        // Key by source line + name (matches interpreter's state_key format)
2952                        let state_key = format!("{}:{}", self.line(), n);
2953                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
2954                            prev.clone()
2955                        } else {
2956                            self.interp
2957                                .state_vars
2958                                .insert(state_key.clone(), init_val.clone());
2959                            init_val
2960                        };
2961                        self.interp
2962                            .scope
2963                            .declare_scalar_frozen(n, val, false, None)
2964                            .map_err(|e| e.at_line(self.line()))?;
2965                        // Register for save-back when scope pops
2966                        if let Some(frame) = self.interp.state_bindings_stack.last_mut() {
2967                            frame.push((n.to_string(), state_key));
2968                        }
2969                        Ok(())
2970                    }
2971                    Op::DeclareStateArray(idx) => {
2972                        let init_val = self.pop();
2973                        let n = names[*idx as usize].as_str();
2974                        let state_key = format!("{}:{}", self.line(), n);
2975                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
2976                            prev.clone()
2977                        } else {
2978                            self.interp
2979                                .state_vars
2980                                .insert(state_key.clone(), init_val.clone());
2981                            init_val
2982                        };
2983                        self.interp.scope.declare_array(n, val.to_list());
2984                        Ok(())
2985                    }
2986                    Op::DeclareStateHash(idx) => {
2987                        let init_val = self.pop();
2988                        let n = names[*idx as usize].as_str();
2989                        let state_key = format!("{}:{}", self.line(), n);
2990                        let val = if let Some(prev) = self.interp.state_vars.get(&state_key) {
2991                            prev.clone()
2992                        } else {
2993                            self.interp
2994                                .state_vars
2995                                .insert(state_key.clone(), init_val.clone());
2996                            init_val
2997                        };
2998                        let items = val.to_list();
2999                        let mut map = IndexMap::new();
3000                        let mut i = 0;
3001                        while i + 1 < items.len() {
3002                            map.insert(items[i].to_string(), items[i + 1].clone());
3003                            i += 2;
3004                        }
3005                        self.interp.scope.declare_hash(n, map);
3006                        Ok(())
3007                    }
3008
3009                    // ── Arrays ──
3010                    Op::GetArray(idx) => {
3011                        let n = names[*idx as usize].as_str();
3012                        let arr = self.interp.scope.get_array(n);
3013                        self.push(StrykeValue::array(arr));
3014                        Ok(())
3015                    }
3016                    Op::SetArray(idx) => {
3017                        let val = self.pop();
3018                        let n = names[*idx as usize].as_str();
3019                        self.require_array_mutable(n)?;
3020                        self.interp
3021                            .scope
3022                            .set_array(n, val.to_list())
3023                            .map_err(|e| e.at_line(self.line()))?;
3024                        Ok(())
3025                    }
3026                    Op::DeclareArray(idx) => {
3027                        let val = self.pop();
3028                        let n = names[*idx as usize].as_str();
3029                        self.interp.scope.declare_array(n, val.to_list());
3030                        Ok(())
3031                    }
3032                    Op::DeclareArrayFrozen(idx) => {
3033                        let val = self.pop();
3034                        let n = names[*idx as usize].as_str();
3035                        self.interp
3036                            .scope
3037                            .declare_array_frozen(n, val.to_list(), true);
3038                        Ok(())
3039                    }
3040                    Op::GetArrayElem(idx) => {
3041                        let index = self.pop().to_int();
3042                        let n = names[*idx as usize].as_str();
3043                        // Stryke string-index sugar: bareword `_[N]` parses
3044                        // to a `__topicstr__N` synthetic name. Index the
3045                        // scalar (`$_` / `$_N`) by char.
3046                        if let Some(real) = n.strip_prefix("__topicstr__") {
3047                            let s = self.interp.scope.get_scalar(real).to_string();
3048                            let cnt = s.chars().count() as i64;
3049                            let i = if index < 0 { index + cnt } else { index };
3050                            let v = if i >= 0 && i < cnt {
3051                                s.chars()
3052                                    .nth(i as usize)
3053                                    .map(|c| StrykeValue::string(c.to_string()))
3054                                    .unwrap_or(StrykeValue::UNDEF)
3055                            } else {
3056                                StrykeValue::UNDEF
3057                            };
3058                            self.push(v);
3059                            return Ok(());
3060                        }
3061                        // Stryke (non-compat) sugar: `$s[i]` indexes by
3062                        // Unicode char when `@s` is missing or empty but
3063                        // `$s` is a non-empty string. NB: `$_[0]` keeps
3064                        // Perl's `@_`-access semantics because `@_` is
3065                        // populated inside any sub; the bareword `_[0]`
3066                        // parses to the same AST so it behaves identically.
3067                        // Use `substr(_, 0, 1)` for char-of-topic inside
3068                        // a sub. Compat mode = Perl semantics.
3069                        if !crate::compat_mode() && self.interp.scope.scalar_binding_exists(n) {
3070                            let prefer_scalar = self.interp.scope.get_array(n).is_empty();
3071                            if prefer_scalar {
3072                                let s = self.interp.scope.get_scalar(n).to_string();
3073                                if !s.is_empty() {
3074                                    let cnt = s.chars().count() as i64;
3075                                    let i = if index < 0 { index + cnt } else { index };
3076                                    let v = if i >= 0 && i < cnt {
3077                                        s.chars()
3078                                            .nth(i as usize)
3079                                            .map(|c| StrykeValue::string(c.to_string()))
3080                                            .unwrap_or(StrykeValue::UNDEF)
3081                                    } else {
3082                                        StrykeValue::UNDEF
3083                                    };
3084                                    self.push(v);
3085                                    return Ok(());
3086                                }
3087                            }
3088                        }
3089                        let val = self.interp.scope.get_array_element(n, index);
3090                        self.push(val);
3091                        Ok(())
3092                    }
3093                    Op::ExistsArrayElem(idx) => {
3094                        let index = self.pop().to_int();
3095                        let n = names[*idx as usize].as_str();
3096                        let yes = self.interp.scope.exists_array_element(n, index);
3097                        self.push(StrykeValue::integer(if yes { 1 } else { 0 }));
3098                        Ok(())
3099                    }
3100                    Op::DeleteArrayElem(idx) => {
3101                        let index = self.pop().to_int();
3102                        let n = names[*idx as usize].as_str();
3103                        self.require_array_mutable(n)?;
3104                        let v = self
3105                            .interp
3106                            .scope
3107                            .delete_array_element(n, index)
3108                            .map_err(|e| e.at_line(self.line()))?;
3109                        self.push(v);
3110                        Ok(())
3111                    }
3112                    Op::SetArrayElem(idx) => {
3113                        let index = self.pop().to_int();
3114                        let val = self.pop();
3115                        let n = names[*idx as usize].as_str();
3116                        self.require_array_mutable(n)?;
3117                        self.interp
3118                            .scope
3119                            .set_array_element(n, index, val)
3120                            .map_err(|e| e.at_line(self.line()))?;
3121                        Ok(())
3122                    }
3123                    Op::SetArrayElemKeep(idx) => {
3124                        let index = self.pop().to_int();
3125                        let val = self.pop();
3126                        let val_keep = val.clone();
3127                        let n = names[*idx as usize].as_str();
3128                        self.require_array_mutable(n)?;
3129                        let line = self.line();
3130                        self.interp
3131                            .scope
3132                            .set_array_element(n, index, val)
3133                            .map_err(|e| e.at_line(line))?;
3134                        self.push(val_keep);
3135                        Ok(())
3136                    }
3137                    Op::PushArray(idx) => {
3138                        let val = self.pop();
3139                        let n = names[*idx as usize].as_str();
3140                        self.require_array_mutable(n)?;
3141                        let line = self.line();
3142                        if let Some(items) = val.as_array_vec() {
3143                            for item in items {
3144                                self.interp
3145                                    .scope
3146                                    .push_to_array(n, item)
3147                                    .map_err(|e| e.at_line(line))?;
3148                            }
3149                        } else {
3150                            self.interp
3151                                .scope
3152                                .push_to_array(n, val)
3153                                .map_err(|e| e.at_line(line))?;
3154                        }
3155                        Ok(())
3156                    }
3157                    Op::PopArray(idx) => {
3158                        let n = names[*idx as usize].as_str();
3159                        self.require_array_mutable(n)?;
3160                        let line = self.line();
3161                        let val = self
3162                            .interp
3163                            .scope
3164                            .pop_from_array(n)
3165                            .map_err(|e| e.at_line(line))?;
3166                        self.push(val);
3167                        Ok(())
3168                    }
3169                    Op::ShiftArray(idx) => {
3170                        let n = names[*idx as usize].as_str();
3171                        self.require_array_mutable(n)?;
3172                        let line = self.line();
3173                        let val = self
3174                            .interp
3175                            .scope
3176                            .shift_from_array(n)
3177                            .map_err(|e| e.at_line(line))?;
3178                        self.push(val);
3179                        Ok(())
3180                    }
3181                    Op::PushArrayDeref => {
3182                        let val = self.pop();
3183                        let r = self.pop();
3184                        let line = self.line();
3185                        vm_interp_result(
3186                            self.interp
3187                                .push_array_deref_value(r.clone(), val, line)
3188                                .map(|_| StrykeValue::UNDEF),
3189                            line,
3190                        )?;
3191                        self.push(r);
3192                        Ok(())
3193                    }
3194                    Op::ArrayDerefLen => {
3195                        let r = self.pop();
3196                        let line = self.line();
3197                        let n = match self.interp.array_deref_len(r, line) {
3198                            Ok(n) => n,
3199                            Err(FlowOrError::Error(e)) => return Err(e),
3200                            Err(FlowOrError::Flow(_)) => {
3201                                return Err(PerlError::runtime(
3202                                    "unexpected flow in tree-assisted opcode",
3203                                    line,
3204                                ));
3205                            }
3206                        };
3207                        self.push(StrykeValue::integer(n));
3208                        Ok(())
3209                    }
3210                    Op::PopArrayDeref => {
3211                        let r = self.pop();
3212                        let line = self.line();
3213                        let v = vm_interp_result(self.interp.pop_array_deref(r, line), line)?;
3214                        self.push(v);
3215                        Ok(())
3216                    }
3217                    Op::ShiftArrayDeref => {
3218                        let r = self.pop();
3219                        let line = self.line();
3220                        let v = vm_interp_result(self.interp.shift_array_deref(r, line), line)?;
3221                        self.push(v);
3222                        Ok(())
3223                    }
3224                    Op::UnshiftArrayDeref(n_extra) => {
3225                        let n = *n_extra as usize;
3226                        let mut vals: Vec<StrykeValue> = Vec::with_capacity(n);
3227                        for _ in 0..n {
3228                            vals.push(self.pop());
3229                        }
3230                        vals.reverse();
3231                        let r = self.pop();
3232                        let line = self.line();
3233                        let len = match self.interp.unshift_array_deref_multi(r, vals, line) {
3234                            Ok(n) => n,
3235                            Err(FlowOrError::Error(e)) => return Err(e),
3236                            Err(FlowOrError::Flow(_)) => {
3237                                return Err(PerlError::runtime(
3238                                    "unexpected flow in tree-assisted opcode",
3239                                    line,
3240                                ));
3241                            }
3242                        };
3243                        self.push(StrykeValue::integer(len));
3244                        Ok(())
3245                    }
3246                    Op::SpliceArrayDeref(n_rep) => {
3247                        let n = *n_rep as usize;
3248                        let mut rep_vals: Vec<StrykeValue> = Vec::with_capacity(n);
3249                        for _ in 0..n {
3250                            rep_vals.push(self.pop());
3251                        }
3252                        rep_vals.reverse();
3253                        let length_val = self.pop();
3254                        let offset_val = self.pop();
3255                        let aref = self.pop();
3256                        let line = self.line();
3257                        let v = vm_interp_result(
3258                            self.interp
3259                                .splice_array_deref(aref, offset_val, length_val, rep_vals, line),
3260                            line,
3261                        )?;
3262                        self.push(v);
3263                        Ok(())
3264                    }
3265                    Op::ArrayLen(idx) => {
3266                        let len = self.interp.scope.array_len(&self.names[*idx as usize]);
3267                        self.push(StrykeValue::integer(len as i64));
3268                        Ok(())
3269                    }
3270                    Op::ArraySlicePart(idx) => {
3271                        let spec = self.pop();
3272                        let n = names[*idx as usize].as_str();
3273                        let mut out = Vec::new();
3274                        if let Some(indices) = spec.as_array_vec() {
3275                            for pv in indices {
3276                                out.push(self.interp.scope.get_array_element(n, pv.to_int()));
3277                            }
3278                        } else {
3279                            out.push(self.interp.scope.get_array_element(n, spec.to_int()));
3280                        }
3281                        self.push(StrykeValue::array(out));
3282                        Ok(())
3283                    }
3284                    Op::GetArrayFromIndex(idx, start) => {
3285                        let n = names[*idx as usize].as_str();
3286                        let arr = self.interp.scope.get_array(n);
3287                        let start = *start as usize;
3288                        let out: Vec<StrykeValue> = if start >= arr.len() {
3289                            Vec::new()
3290                        } else {
3291                            arr[start..].to_vec()
3292                        };
3293                        self.push(StrykeValue::array(out));
3294                        Ok(())
3295                    }
3296                    Op::ArrayConcatTwo => {
3297                        let b = self.pop();
3298                        let a = self.pop();
3299                        let mut av = a.as_array_vec().unwrap_or_else(|| vec![a]);
3300                        let bv = b.as_array_vec().unwrap_or_else(|| vec![b]);
3301                        av.extend(bv);
3302                        self.push(StrykeValue::array(av));
3303                        Ok(())
3304                    }
3305
3306                    // ── Hashes ──
3307                    Op::GetHash(idx) => {
3308                        let n = names[*idx as usize].as_str();
3309                        self.interp.touch_env_hash(n);
3310                        let h = self.interp.scope.get_hash(n);
3311                        self.push(StrykeValue::hash(h));
3312                        Ok(())
3313                    }
3314                    Op::SetHash(idx) => {
3315                        let val = self.pop();
3316                        let items = val.to_list();
3317                        let mut map = IndexMap::new();
3318                        let mut i = 0;
3319                        while i + 1 < items.len() {
3320                            map.insert(items[i].to_string(), items[i + 1].clone());
3321                            i += 2;
3322                        }
3323                        let n = names[*idx as usize].as_str();
3324                        self.require_hash_mutable(n)?;
3325                        self.interp
3326                            .scope
3327                            .set_hash(n, map)
3328                            .map_err(|e| e.at_line(self.line()))?;
3329                        Ok(())
3330                    }
3331                    Op::DeclareHash(idx) => {
3332                        let val = self.pop();
3333                        let items = val.to_list();
3334                        let mut map = IndexMap::new();
3335                        let mut i = 0;
3336                        while i + 1 < items.len() {
3337                            map.insert(items[i].to_string(), items[i + 1].clone());
3338                            i += 2;
3339                        }
3340                        let n = names[*idx as usize].as_str();
3341                        self.interp.scope.declare_hash(n, map);
3342                        Ok(())
3343                    }
3344                    Op::DeclareHashFrozen(idx) => {
3345                        let val = self.pop();
3346                        let items = val.to_list();
3347                        let mut map = IndexMap::new();
3348                        let mut i = 0;
3349                        while i + 1 < items.len() {
3350                            map.insert(items[i].to_string(), items[i + 1].clone());
3351                            i += 2;
3352                        }
3353                        let n = names[*idx as usize].as_str();
3354                        self.interp.scope.declare_hash_frozen(n, map, true);
3355                        Ok(())
3356                    }
3357                    Op::LocalDeclareScalar(idx) => {
3358                        let val = self.pop();
3359                        let n = names[*idx as usize].as_str();
3360                        // `local $X` on a special var (`$/`, `$\`, `$,`, `$"`, …) — see
3361                        // Perl's `local` handler. Save prior value to
3362                        // the interpreter's `special_var_restore_frames` so `scope_pop_hook`
3363                        // restores the backing field on block exit.
3364                        if VMHelper::is_special_scalar_name_for_set(n) {
3365                            let old = self.interp.get_special_var(n);
3366                            if let Some(frame) = self.interp.special_var_restore_frames.last_mut() {
3367                                frame.push((n.to_string(), old));
3368                            }
3369                            let line = self.line();
3370                            self.interp
3371                                .set_special_var(n, &val)
3372                                .map_err(|e| e.at_line(line))?;
3373                        }
3374                        self.interp
3375                            .scope
3376                            .local_set_scalar(n, val.clone())
3377                            .map_err(|e| e.at_line(self.line()))?;
3378                        self.push(val);
3379                        Ok(())
3380                    }
3381                    Op::LocalDeclareArray(idx) => {
3382                        let val = self.pop();
3383                        let n = names[*idx as usize].as_str();
3384                        self.interp
3385                            .scope
3386                            .local_set_array(n, val.to_list())
3387                            .map_err(|e| e.at_line(self.line()))?;
3388                        self.push(val);
3389                        Ok(())
3390                    }
3391                    Op::LocalDeclareHash(idx) => {
3392                        let val = self.pop();
3393                        let items = val.to_list();
3394                        let mut map = IndexMap::new();
3395                        let mut i = 0;
3396                        while i + 1 < items.len() {
3397                            map.insert(items[i].to_string(), items[i + 1].clone());
3398                            i += 2;
3399                        }
3400                        let n = names[*idx as usize].as_str();
3401                        self.interp.touch_env_hash(n);
3402                        self.interp
3403                            .scope
3404                            .local_set_hash(n, map)
3405                            .map_err(|e| e.at_line(self.line()))?;
3406                        self.push(val);
3407                        Ok(())
3408                    }
3409                    Op::LocalDeclareHashElement(idx) => {
3410                        let key = self.pop().to_string();
3411                        let val = self.pop();
3412                        let n = names[*idx as usize].as_str();
3413                        self.interp.touch_env_hash(n);
3414                        self.interp
3415                            .scope
3416                            .local_set_hash_element(n, key.as_str(), val.clone())
3417                            .map_err(|e| e.at_line(self.line()))?;
3418                        self.push(val);
3419                        Ok(())
3420                    }
3421                    Op::LocalDeclareArrayElement(idx) => {
3422                        let index = self.pop().to_int();
3423                        let val = self.pop();
3424                        let n = names[*idx as usize].as_str();
3425                        self.require_array_mutable(n)?;
3426                        self.interp
3427                            .scope
3428                            .local_set_array_element(n, index, val.clone())
3429                            .map_err(|e| e.at_line(self.line()))?;
3430                        self.push(val);
3431                        Ok(())
3432                    }
3433                    Op::LocalDeclareTypeglob(lhs_i, rhs_opt) => {
3434                        let lhs = names[*lhs_i as usize].as_str();
3435                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
3436                        let line = self.line();
3437                        self.interp
3438                            .local_declare_typeglob(lhs, rhs, line)
3439                            .map_err(|e| e.at_line(line))?;
3440                        Ok(())
3441                    }
3442                    Op::LocalDeclareTypeglobDynamic(rhs_opt) => {
3443                        let lhs = self.pop().to_string();
3444                        let rhs = rhs_opt.map(|i| names[i as usize].as_str());
3445                        let line = self.line();
3446                        self.interp
3447                            .local_declare_typeglob(lhs.as_str(), rhs, line)
3448                            .map_err(|e| e.at_line(line))?;
3449                        Ok(())
3450                    }
3451                    Op::GetHashElem(idx) => {
3452                        let key = self.pop().to_string();
3453                        let n = names[*idx as usize].as_str();
3454                        self.interp.touch_env_hash(n);
3455                        let val = self.interp.scope.get_hash_element(n, &key);
3456                        self.push(val);
3457                        Ok(())
3458                    }
3459                    Op::SetHashElem(idx) => {
3460                        let key = self.pop().to_string();
3461                        let val = self.pop();
3462                        let n = names[*idx as usize].as_str();
3463                        self.require_hash_mutable(n)?;
3464                        self.interp.touch_env_hash(n);
3465                        self.interp
3466                            .scope
3467                            .set_hash_element(n, &key, val)
3468                            .map_err(|e| e.at_line(self.line()))?;
3469                        Ok(())
3470                    }
3471                    Op::SetHashElemKeep(idx) => {
3472                        let key = self.pop().to_string();
3473                        let val = self.pop();
3474                        let val_keep = val.clone();
3475                        let n = names[*idx as usize].as_str();
3476                        self.require_hash_mutable(n)?;
3477                        self.interp.touch_env_hash(n);
3478                        let line = self.line();
3479                        self.interp
3480                            .scope
3481                            .set_hash_element(n, &key, val)
3482                            .map_err(|e| e.at_line(line))?;
3483                        self.push(val_keep);
3484                        Ok(())
3485                    }
3486                    Op::DeleteHashElem(idx) => {
3487                        let key = self.pop().to_string();
3488                        let n = names[*idx as usize].as_str();
3489                        self.require_hash_mutable(n)?;
3490                        self.interp.touch_env_hash(n);
3491                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
3492                            let class = obj
3493                                .as_blessed_ref()
3494                                .map(|b| b.class.clone())
3495                                .unwrap_or_default();
3496                            let full = format!("{}::DELETE", class);
3497                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
3498                                let line = self.line();
3499                                let v = vm_interp_result(
3500                                    self.interp.call_sub(
3501                                        &sub,
3502                                        vec![obj, StrykeValue::string(key)],
3503                                        WantarrayCtx::Scalar,
3504                                        line,
3505                                    ),
3506                                    line,
3507                                )?;
3508                                self.push(v);
3509                                return Ok(());
3510                            }
3511                        }
3512                        let val = self
3513                            .interp
3514                            .scope
3515                            .delete_hash_element(n, &key)
3516                            .map_err(|e| e.at_line(self.line()))?;
3517                        self.push(val);
3518                        Ok(())
3519                    }
3520                    Op::ExistsHashElem(idx) => {
3521                        let key = self.pop().to_string();
3522                        let n = names[*idx as usize].as_str();
3523                        self.interp.touch_env_hash(n);
3524                        if let Some(obj) = self.interp.tied_hashes.get(n).cloned() {
3525                            let class = obj
3526                                .as_blessed_ref()
3527                                .map(|b| b.class.clone())
3528                                .unwrap_or_default();
3529                            let full = format!("{}::EXISTS", class);
3530                            if let Some(sub) = self.interp.subs.get(&full).cloned() {
3531                                let line = self.line();
3532                                let v = vm_interp_result(
3533                                    self.interp.call_sub(
3534                                        &sub,
3535                                        vec![obj, StrykeValue::string(key)],
3536                                        WantarrayCtx::Scalar,
3537                                        line,
3538                                    ),
3539                                    line,
3540                                )?;
3541                                self.push(v);
3542                                return Ok(());
3543                            }
3544                        }
3545                        let exists = self.interp.scope.exists_hash_element(n, &key);
3546                        self.push(StrykeValue::integer(if exists { 1 } else { 0 }));
3547                        Ok(())
3548                    }
3549                    Op::ExistsArrowHashElem => {
3550                        let key = self.pop().to_string();
3551                        let container = self.pop();
3552                        let line = self.line();
3553                        let yes = vm_interp_result(
3554                            self.interp
3555                                .exists_arrow_hash_element(container, &key, line)
3556                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
3557                                .map_err(FlowOrError::Error),
3558                            line,
3559                        )?;
3560                        self.push(yes);
3561                        Ok(())
3562                    }
3563                    Op::DeleteArrowHashElem => {
3564                        let key = self.pop().to_string();
3565                        let container = self.pop();
3566                        let line = self.line();
3567                        let v = vm_interp_result(
3568                            self.interp
3569                                .delete_arrow_hash_element(container, &key, line)
3570                                .map_err(FlowOrError::Error),
3571                            line,
3572                        )?;
3573                        self.push(v);
3574                        Ok(())
3575                    }
3576                    Op::ExistsArrowArrayElem => {
3577                        let idx = self.pop().to_int();
3578                        let container = self.pop();
3579                        let line = self.line();
3580                        let yes = vm_interp_result(
3581                            self.interp
3582                                .exists_arrow_array_element(container, idx, line)
3583                                .map(|b| StrykeValue::integer(if b { 1 } else { 0 }))
3584                                .map_err(FlowOrError::Error),
3585                            line,
3586                        )?;
3587                        self.push(yes);
3588                        Ok(())
3589                    }
3590                    Op::DeleteArrowArrayElem => {
3591                        let idx = self.pop().to_int();
3592                        let container = self.pop();
3593                        let line = self.line();
3594                        let v = vm_interp_result(
3595                            self.interp
3596                                .delete_arrow_array_element(container, idx, line)
3597                                .map_err(FlowOrError::Error),
3598                            line,
3599                        )?;
3600                        self.push(v);
3601                        Ok(())
3602                    }
3603                    Op::HashKeys(idx) => {
3604                        let n = names[*idx as usize].as_str();
3605                        self.interp.touch_env_hash(n);
3606                        let h = self.interp.scope.get_hash(n);
3607                        let keys: Vec<StrykeValue> =
3608                            h.keys().map(|k| StrykeValue::string(k.clone())).collect();
3609                        self.push(StrykeValue::array(keys));
3610                        Ok(())
3611                    }
3612                    Op::HashKeysScalar(idx) => {
3613                        let n = names[*idx as usize].as_str();
3614                        self.interp.touch_env_hash(n);
3615                        let h = self.interp.scope.get_hash(n);
3616                        self.push(StrykeValue::integer(h.len() as i64));
3617                        Ok(())
3618                    }
3619                    Op::HashValues(idx) => {
3620                        let n = names[*idx as usize].as_str();
3621                        self.interp.touch_env_hash(n);
3622                        let h = self.interp.scope.get_hash(n);
3623                        let vals: Vec<StrykeValue> = h.values().cloned().collect();
3624                        self.push(StrykeValue::array(vals));
3625                        Ok(())
3626                    }
3627                    Op::HashValuesScalar(idx) => {
3628                        let n = names[*idx as usize].as_str();
3629                        self.interp.touch_env_hash(n);
3630                        let h = self.interp.scope.get_hash(n);
3631                        self.push(StrykeValue::integer(h.len() as i64));
3632                        Ok(())
3633                    }
3634                    Op::KeysFromValue => {
3635                        let val = self.pop();
3636                        let line = self.line();
3637                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
3638                        self.push(v);
3639                        Ok(())
3640                    }
3641                    Op::KeysFromValueScalar => {
3642                        let val = self.pop();
3643                        let line = self.line();
3644                        let v = vm_interp_result(VMHelper::keys_from_value(val, line), line)?;
3645                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
3646                        self.push(StrykeValue::integer(n));
3647                        Ok(())
3648                    }
3649                    Op::ValuesFromValue => {
3650                        let val = self.pop();
3651                        let line = self.line();
3652                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
3653                        self.push(v);
3654                        Ok(())
3655                    }
3656                    Op::ValuesFromValueScalar => {
3657                        let val = self.pop();
3658                        let line = self.line();
3659                        let v = vm_interp_result(VMHelper::values_from_value(val, line), line)?;
3660                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
3661                        self.push(StrykeValue::integer(n));
3662                        Ok(())
3663                    }
3664
3665                    // ── Arithmetic (integer fast paths) ──
3666                    Op::Add => {
3667                        let b = self.pop();
3668                        let a = self.pop();
3669                        self.push_binop_with_overload(BinOp::Add, a, b, |a, b| {
3670                            if let Some(s) = crate::sketches::try_sketch_binop(
3671                                crate::sketches::SketchOp::Add,
3672                                a,
3673                                b,
3674                            ) {
3675                                return Ok(s);
3676                            }
3677                            Ok(crate::value::compat_add(a, b))
3678                        })
3679                    }
3680                    Op::Sub => {
3681                        let b = self.pop();
3682                        let a = self.pop();
3683                        self.push_binop_with_overload(BinOp::Sub, a, b, |a, b| {
3684                            if let Some(s) = crate::sketches::try_sketch_binop(
3685                                crate::sketches::SketchOp::Sub,
3686                                a,
3687                                b,
3688                            ) {
3689                                return Ok(s);
3690                            }
3691                            Ok(crate::value::compat_sub(a, b))
3692                        })
3693                    }
3694                    Op::Mul => {
3695                        let b = self.pop();
3696                        let a = self.pop();
3697                        self.push_binop_with_overload(BinOp::Mul, a, b, |a, b| {
3698                            Ok(crate::value::compat_mul(a, b))
3699                        })
3700                    }
3701                    Op::Div => {
3702                        let b = self.pop();
3703                        let a = self.pop();
3704                        let line = self.line();
3705                        self.push_binop_with_overload(BinOp::Div, a, b, |a, b| {
3706                            if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
3707                                if y == 0 {
3708                                    return Err(PerlError::division_by_zero(
3709                                        "Illegal division by zero",
3710                                        line,
3711                                    ));
3712                                }
3713                                Ok(if x % y == 0 {
3714                                    StrykeValue::integer(x / y)
3715                                } else {
3716                                    StrykeValue::float(x as f64 / y as f64)
3717                                })
3718                            } else {
3719                                let d = b.to_number();
3720                                if d == 0.0 {
3721                                    return Err(PerlError::division_by_zero(
3722                                        "Illegal division by zero",
3723                                        line,
3724                                    ));
3725                                }
3726                                Ok(StrykeValue::float(a.to_number() / d))
3727                            }
3728                        })
3729                    }
3730                    Op::Mod => {
3731                        let b = self.pop();
3732                        let a = self.pop();
3733                        let line = self.line();
3734                        self.push_binop_with_overload(BinOp::Mod, a, b, |a, b| {
3735                            let b = b.to_int();
3736                            let a = a.to_int();
3737                            if b == 0 {
3738                                return Err(PerlError::division_by_zero(
3739                                    "Illegal modulus zero",
3740                                    line,
3741                                ));
3742                            }
3743                            Ok(StrykeValue::integer(crate::value::perl_mod_i64(a, b)))
3744                        })
3745                    }
3746                    Op::Pow => {
3747                        let b = self.pop();
3748                        let a = self.pop();
3749                        self.push_binop_with_overload(BinOp::Pow, a, b, |a, b| {
3750                            Ok(crate::value::compat_pow(a, b))
3751                        })
3752                    }
3753                    Op::Negate => {
3754                        let a = self.pop();
3755                        let line = self.line();
3756                        if let Some(exec_res) =
3757                            self.interp.try_overload_unary_dispatch("neg", &a, line)
3758                        {
3759                            self.push(vm_interp_result(exec_res, line)?);
3760                        } else {
3761                            self.push(if let Some(n) = a.as_integer() {
3762                                StrykeValue::integer(-n)
3763                            } else {
3764                                StrykeValue::float(-a.to_number())
3765                            });
3766                        }
3767                        Ok(())
3768                    }
3769                    Op::Inc => {
3770                        let a = self.pop();
3771                        self.push(if let Some(n) = a.as_integer() {
3772                            StrykeValue::integer(n.wrapping_add(1))
3773                        } else {
3774                            StrykeValue::float(a.to_number() + 1.0)
3775                        });
3776                        Ok(())
3777                    }
3778                    Op::Dec => {
3779                        let a = self.pop();
3780                        self.push(if let Some(n) = a.as_integer() {
3781                            StrykeValue::integer(n.wrapping_sub(1))
3782                        } else {
3783                            StrykeValue::float(a.to_number() - 1.0)
3784                        });
3785                        Ok(())
3786                    }
3787
3788                    // ── String ──
3789                    Op::Concat => {
3790                        let b = self.pop();
3791                        let a = self.pop();
3792                        let out = self.concat_stack_values(a, b)?;
3793                        self.push(out);
3794                        Ok(())
3795                    }
3796                    Op::ArrayStringifyListSep => {
3797                        let raw = self.pop();
3798                        let v = self.interp.peel_array_ref_for_list_join(raw);
3799                        let sep = self.interp.list_separator.clone();
3800                        let list = v.to_list();
3801                        let joined = list
3802                            .iter()
3803                            .map(|x| x.to_string())
3804                            .collect::<Vec<_>>()
3805                            .join(&sep);
3806                        self.push(StrykeValue::string(joined));
3807                        Ok(())
3808                    }
3809                    Op::StringRepeat => {
3810                        let n = self.pop().to_int().max(0) as usize;
3811                        let val = self.pop();
3812                        self.push(StrykeValue::string(val.to_string().repeat(n)));
3813                        Ok(())
3814                    }
3815                    Op::ListRepeat => {
3816                        let n = self.pop().to_int().max(0) as usize;
3817                        let val = self.pop();
3818                        // Flatten to a Vec<StrykeValue>: an array value gives its
3819                        // items; a scalar (e.g. `(0) x 5` after the LHS evaluates
3820                        // through scalar-collapse paths) wraps as a 1-elt list.
3821                        let items: Vec<StrykeValue> =
3822                            val.as_array_vec().unwrap_or_else(|| vec![val]);
3823                        let mut out = Vec::with_capacity(items.len().saturating_mul(n));
3824                        for _ in 0..n {
3825                            out.extend(items.iter().cloned());
3826                        }
3827                        self.push(StrykeValue::array(out));
3828                        Ok(())
3829                    }
3830                    Op::ProcessCaseEscapes => {
3831                        let val = self.pop();
3832                        let s = val.to_string();
3833                        let processed = VMHelper::process_case_escapes(&s);
3834                        self.push(StrykeValue::string(processed));
3835                        Ok(())
3836                    }
3837
3838                    // ── Numeric comparison ──
3839                    Op::NumEq => {
3840                        let b = self.pop();
3841                        let a = self.pop();
3842                        self.push_binop_with_overload(BinOp::NumEq, a.clone(), b.clone(), |a, b| {
3843                            // Struct equality: compare all fields
3844                            if let (Some(sa), Some(sb)) = (a.as_struct_inst(), b.as_struct_inst()) {
3845                                if sa.def.name != sb.def.name {
3846                                    return Ok(StrykeValue::integer(0));
3847                                }
3848                                let av = sa.get_values();
3849                                let bv = sb.get_values();
3850                                let eq = av.len() == bv.len()
3851                                    && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
3852                                Ok(StrykeValue::integer(if eq { 1 } else { 0 }))
3853                            } else {
3854                                if !crate::compat_mode() && both_non_numeric_strings(a, b) {
3855                                    let sa = a.to_string();
3856                                    let sb = b.to_string();
3857                                    return Ok(StrykeValue::integer(if sa == sb { 1 } else { 0 }));
3858                                }
3859                                Ok(int_cmp(a, b, |x, y| x == y, |x, y| x == y))
3860                            }
3861                        })
3862                    }
3863                    Op::NumNe => {
3864                        let b = self.pop();
3865                        let a = self.pop();
3866                        self.push_binop_with_overload(BinOp::NumNe, a, b, |a, b| {
3867                            // Stryke (non-compat) sugar: when both operands are
3868                            // non-numeric strings, fall back to `ne`. In Perl,
3869                            // `"G" != "T"` is `0 != 0` = false; in stryke we
3870                            // want char/string compare. Compat mode keeps
3871                            // Perl semantics.
3872                            if !crate::compat_mode() && both_non_numeric_strings(a, b) {
3873                                let sa = a.to_string();
3874                                let sb = b.to_string();
3875                                return Ok(StrykeValue::integer(if sa != sb { 1 } else { 0 }));
3876                            }
3877                            Ok(int_cmp(a, b, |x, y| x != y, |x, y| x != y))
3878                        })
3879                    }
3880                    Op::NumLt => {
3881                        let b = self.pop();
3882                        let a = self.pop();
3883                        self.push_binop_with_overload(BinOp::NumLt, a, b, |a, b| {
3884                            Ok(int_cmp(a, b, |x, y| x < y, |x, y| x < y))
3885                        })
3886                    }
3887                    Op::NumGt => {
3888                        let b = self.pop();
3889                        let a = self.pop();
3890                        self.push_binop_with_overload(BinOp::NumGt, a, b, |a, b| {
3891                            Ok(int_cmp(a, b, |x, y| x > y, |x, y| x > y))
3892                        })
3893                    }
3894                    Op::NumLe => {
3895                        let b = self.pop();
3896                        let a = self.pop();
3897                        self.push_binop_with_overload(BinOp::NumLe, a, b, |a, b| {
3898                            Ok(int_cmp(a, b, |x, y| x <= y, |x, y| x <= y))
3899                        })
3900                    }
3901                    Op::NumGe => {
3902                        let b = self.pop();
3903                        let a = self.pop();
3904                        self.push_binop_with_overload(BinOp::NumGe, a, b, |a, b| {
3905                            Ok(int_cmp(a, b, |x, y| x >= y, |x, y| x >= y))
3906                        })
3907                    }
3908                    Op::Spaceship => {
3909                        let b = self.pop();
3910                        let a = self.pop();
3911                        self.push_binop_with_overload(BinOp::Spaceship, a, b, |a, b| {
3912                            Ok(
3913                                if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
3914                                    StrykeValue::integer(if x < y {
3915                                        -1
3916                                    } else if x > y {
3917                                        1
3918                                    } else {
3919                                        0
3920                                    })
3921                                } else {
3922                                    let x = a.to_number();
3923                                    let y = b.to_number();
3924                                    StrykeValue::integer(if x < y {
3925                                        -1
3926                                    } else if x > y {
3927                                        1
3928                                    } else {
3929                                        0
3930                                    })
3931                                },
3932                            )
3933                        })
3934                    }
3935
3936                    // ── String comparison ──
3937                    Op::StrEq => {
3938                        let b = self.pop();
3939                        let a = self.pop();
3940                        self.push_binop_with_overload(BinOp::StrEq, a, b, |a, b| {
3941                            Ok(StrykeValue::integer(if a.str_eq(b) { 1 } else { 0 }))
3942                        })
3943                    }
3944                    Op::StrNe => {
3945                        let b = self.pop();
3946                        let a = self.pop();
3947                        self.push_binop_with_overload(BinOp::StrNe, a, b, |a, b| {
3948                            Ok(StrykeValue::integer(if !a.str_eq(b) { 1 } else { 0 }))
3949                        })
3950                    }
3951                    Op::StrLt => {
3952                        let b = self.pop();
3953                        let a = self.pop();
3954                        self.push_binop_with_overload(BinOp::StrLt, a, b, |a, b| {
3955                            Ok(StrykeValue::integer(
3956                                if a.str_cmp(b) == std::cmp::Ordering::Less {
3957                                    1
3958                                } else {
3959                                    0
3960                                },
3961                            ))
3962                        })
3963                    }
3964                    Op::StrGt => {
3965                        let b = self.pop();
3966                        let a = self.pop();
3967                        self.push_binop_with_overload(BinOp::StrGt, a, b, |a, b| {
3968                            Ok(StrykeValue::integer(
3969                                if a.str_cmp(b) == std::cmp::Ordering::Greater {
3970                                    1
3971                                } else {
3972                                    0
3973                                },
3974                            ))
3975                        })
3976                    }
3977                    Op::StrLe => {
3978                        let b = self.pop();
3979                        let a = self.pop();
3980                        self.push_binop_with_overload(BinOp::StrLe, a, b, |a, b| {
3981                            let o = a.str_cmp(b);
3982                            Ok(StrykeValue::integer(
3983                                if matches!(o, std::cmp::Ordering::Less | std::cmp::Ordering::Equal)
3984                                {
3985                                    1
3986                                } else {
3987                                    0
3988                                },
3989                            ))
3990                        })
3991                    }
3992                    Op::StrGe => {
3993                        let b = self.pop();
3994                        let a = self.pop();
3995                        self.push_binop_with_overload(BinOp::StrGe, a, b, |a, b| {
3996                            let o = a.str_cmp(b);
3997                            Ok(StrykeValue::integer(
3998                                if matches!(
3999                                    o,
4000                                    std::cmp::Ordering::Greater | std::cmp::Ordering::Equal
4001                                ) {
4002                                    1
4003                                } else {
4004                                    0
4005                                },
4006                            ))
4007                        })
4008                    }
4009                    Op::StrCmp => {
4010                        let b = self.pop();
4011                        let a = self.pop();
4012                        self.push_binop_with_overload(BinOp::StrCmp, a, b, |a, b| {
4013                            let cmp = a.str_cmp(b);
4014                            Ok(StrykeValue::integer(match cmp {
4015                                std::cmp::Ordering::Less => -1,
4016                                std::cmp::Ordering::Greater => 1,
4017                                std::cmp::Ordering::Equal => 0,
4018                            }))
4019                        })
4020                    }
4021
4022                    // ── Logical / Bitwise ──
4023                    Op::LogNot => {
4024                        let a = self.pop();
4025                        let line = self.line();
4026                        if let Some(exec_res) =
4027                            self.interp.try_overload_unary_dispatch("bool", &a, line)
4028                        {
4029                            let pv = vm_interp_result(exec_res, line)?;
4030                            self.push(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
4031                        } else {
4032                            self.push(StrykeValue::integer(if a.is_true() { 0 } else { 1 }));
4033                        }
4034                        Ok(())
4035                    }
4036                    Op::BitAnd => {
4037                        let rv = self.pop();
4038                        let lv = self.pop();
4039                        if let Some(s) = crate::value::set_intersection(&lv, &rv) {
4040                            self.push(s);
4041                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4042                            crate::sketches::SketchOp::And,
4043                            &lv,
4044                            &rv,
4045                        ) {
4046                            self.push(s);
4047                        } else {
4048                            self.push(StrykeValue::integer(lv.to_int() & rv.to_int()));
4049                        }
4050                        Ok(())
4051                    }
4052                    Op::BitOr => {
4053                        let rv = self.pop();
4054                        let lv = self.pop();
4055                        if let Some(s) = crate::value::set_union(&lv, &rv) {
4056                            self.push(s);
4057                        } else if let Some(s) = crate::sketches::try_sketch_binop(
4058                            crate::sketches::SketchOp::Or,
4059                            &lv,
4060                            &rv,
4061                        ) {
4062                            self.push(s);
4063                        } else {
4064                            self.push(StrykeValue::integer(lv.to_int() | rv.to_int()));
4065                        }
4066                        Ok(())
4067                    }
4068                    Op::BitXor => {
4069                        let rv = self.pop();
4070                        let lv = self.pop();
4071                        if let Some(s) = crate::sketches::try_sketch_binop(
4072                            crate::sketches::SketchOp::Xor,
4073                            &lv,
4074                            &rv,
4075                        ) {
4076                            self.push(s);
4077                        } else {
4078                            self.push(StrykeValue::integer(lv.to_int() ^ rv.to_int()));
4079                        }
4080                        Ok(())
4081                    }
4082                    Op::BitNot => {
4083                        let a = self.pop().to_int();
4084                        self.push(StrykeValue::integer(!a));
4085                        Ok(())
4086                    }
4087                    Op::Shl => {
4088                        let b = self.pop().to_int();
4089                        let a = self.pop().to_int();
4090                        self.push(StrykeValue::integer(a << b));
4091                        Ok(())
4092                    }
4093                    Op::Shr => {
4094                        let b = self.pop().to_int();
4095                        let a = self.pop().to_int();
4096                        self.push(StrykeValue::integer(a >> b));
4097                        Ok(())
4098                    }
4099
4100                    // ── Control flow ──
4101                    Op::Jump(target) => {
4102                        self.ip = *target;
4103                        Ok(())
4104                    }
4105                    Op::JumpIfTrue(target) => {
4106                        let val = self.pop();
4107                        if val.is_true() {
4108                            self.ip = *target;
4109                        }
4110                        Ok(())
4111                    }
4112                    Op::JumpIfFalse(target) => {
4113                        let val = self.pop();
4114                        if !val.is_true() {
4115                            self.ip = *target;
4116                        }
4117                        Ok(())
4118                    }
4119                    Op::JumpIfFalseKeep(target) => {
4120                        if !self.peek().is_true() {
4121                            self.ip = *target;
4122                        } else {
4123                            self.pop();
4124                        }
4125                        Ok(())
4126                    }
4127                    Op::JumpIfTrueKeep(target) => {
4128                        if self.peek().is_true() {
4129                            self.ip = *target;
4130                        } else {
4131                            self.pop();
4132                        }
4133                        Ok(())
4134                    }
4135                    Op::JumpIfDefinedKeep(target) => {
4136                        if !self.peek().is_undef() {
4137                            self.ip = *target;
4138                        } else {
4139                            self.pop();
4140                        }
4141                        Ok(())
4142                    }
4143
4144                    // ── Increment / Decrement ──
4145                    Op::PreInc(idx) => {
4146                        let n = names[*idx as usize].as_str();
4147                        self.require_scalar_mutable(n)?;
4148                        let en = self.interp.english_scalar_name(n);
4149                        let new_val = self
4150                            .interp
4151                            .scope
4152                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() + 1))
4153                            .map_err(|e| e.at_line(self.line()))?;
4154                        self.push(new_val);
4155                        Ok(())
4156                    }
4157                    Op::PreDec(idx) => {
4158                        let n = names[*idx as usize].as_str();
4159                        self.require_scalar_mutable(n)?;
4160                        let en = self.interp.english_scalar_name(n);
4161                        let new_val = self
4162                            .interp
4163                            .scope
4164                            .atomic_mutate(en, |v| StrykeValue::integer(v.to_int() - 1))
4165                            .map_err(|e| e.at_line(self.line()))?;
4166                        self.push(new_val);
4167                        Ok(())
4168                    }
4169                    Op::PostInc(idx) => {
4170                        let n = names[*idx as usize].as_str();
4171                        self.require_scalar_mutable(n)?;
4172                        let en = self.interp.english_scalar_name(n);
4173                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4174                            self.interp
4175                                .scope
4176                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
4177                                .map_err(|e| e.at_line(self.line()))?;
4178                            self.ip += 1;
4179                        } else {
4180                            let old = self
4181                                .interp
4182                                .scope
4183                                .atomic_mutate_post(en, crate::vm_helper::perl_inc)
4184                                .map_err(|e| e.at_line(self.line()))?;
4185                            self.push(old);
4186                        }
4187                        Ok(())
4188                    }
4189                    Op::PostDec(idx) => {
4190                        let n = names[*idx as usize].as_str();
4191                        self.require_scalar_mutable(n)?;
4192                        let en = self.interp.english_scalar_name(n);
4193                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4194                            self.interp
4195                                .scope
4196                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
4197                                .map_err(|e| e.at_line(self.line()))?;
4198                            self.ip += 1;
4199                        } else {
4200                            let old = self
4201                                .interp
4202                                .scope
4203                                .atomic_mutate_post(en, |v| StrykeValue::integer(v.to_int() - 1))
4204                                .map_err(|e| e.at_line(self.line()))?;
4205                            self.push(old);
4206                        }
4207                        Ok(())
4208                    }
4209                    Op::PreIncSlot(slot) => {
4210                        let cur = self.interp.scope.get_scalar_slot(*slot);
4211                        let new_val = crate::vm_helper::perl_inc(&cur);
4212                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
4213                        self.push(new_val);
4214                        Ok(())
4215                    }
4216                    Op::PreIncSlotVoid(slot) => {
4217                        let cur = self.interp.scope.get_scalar_slot(*slot);
4218                        let new_val = crate::vm_helper::perl_inc(&cur);
4219                        self.interp.scope.set_scalar_slot(*slot, new_val);
4220                        Ok(())
4221                    }
4222                    Op::PreDecSlot(slot) => {
4223                        let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
4224                        let new_val = StrykeValue::integer(val);
4225                        self.interp.scope.set_scalar_slot(*slot, new_val.clone());
4226                        self.push(new_val);
4227                        Ok(())
4228                    }
4229                    Op::PostIncSlot(slot) => {
4230                        // Fuse PostIncSlot+Pop: if next op discards the old value, skip stack work.
4231                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4232                            let cur = self.interp.scope.get_scalar_slot(*slot);
4233                            let new_val = crate::vm_helper::perl_inc(&cur);
4234                            self.interp.scope.set_scalar_slot(*slot, new_val);
4235                            self.ip += 1; // skip Pop
4236                        } else {
4237                            let old = self.interp.scope.get_scalar_slot(*slot);
4238                            let new_val = crate::vm_helper::perl_inc(&old);
4239                            self.interp.scope.set_scalar_slot(*slot, new_val);
4240                            self.push(old);
4241                        }
4242                        Ok(())
4243                    }
4244                    Op::PostDecSlot(slot) => {
4245                        if self.ip < len && matches!(ops[self.ip], Op::Pop) {
4246                            let val = self.interp.scope.get_scalar_slot(*slot).to_int() - 1;
4247                            self.interp
4248                                .scope
4249                                .set_scalar_slot(*slot, StrykeValue::integer(val));
4250                            self.ip += 1;
4251                        } else {
4252                            let old = self.interp.scope.get_scalar_slot(*slot);
4253                            let new_val = StrykeValue::integer(old.to_int() - 1);
4254                            self.interp.scope.set_scalar_slot(*slot, new_val);
4255                            self.push(old);
4256                        }
4257                        Ok(())
4258                    }
4259
4260                    // ── Functions ──
4261                    Op::Call(name_idx, argc, wa) => {
4262                        let entry_opt = self.find_sub_entry(*name_idx);
4263                        self.vm_dispatch_user_call(*name_idx, entry_opt, *argc, *wa, None)?;
4264                        Ok(())
4265                    }
4266                    Op::CallStaticSubId(sid, name_idx, argc, wa) => {
4267                        let t = self.static_sub_calls.get(*sid as usize).ok_or_else(|| {
4268                            PerlError::runtime("VM: invalid CallStaticSubId", self.line())
4269                        })?;
4270                        debug_assert_eq!(t.2, *name_idx);
4271                        let closure_sub = self
4272                            .static_sub_closure_subs
4273                            .get(*sid as usize)
4274                            .and_then(|x| x.clone());
4275                        self.vm_dispatch_user_call(
4276                            *name_idx,
4277                            Some((t.0, t.1)),
4278                            *argc,
4279                            *wa,
4280                            closure_sub,
4281                        )?;
4282                        Ok(())
4283                    }
4284                    Op::Return => {
4285                        if let Some(frame) = self.call_stack.pop() {
4286                            if frame.block_region {
4287                                return Err(PerlError::runtime(
4288                                    "Return in map/grep/sort block bytecode",
4289                                    self.line(),
4290                                ));
4291                            }
4292                            if let Some(t0) = frame.sub_profiler_start {
4293                                if let Some(p) = &mut self.interp.profiler {
4294                                    p.exit_sub(t0.elapsed());
4295                                }
4296                            }
4297                            self.interp.debugger_leave_sub();
4298                            self.interp.wantarray_kind = frame.saved_wantarray;
4299                            self.stack.truncate(frame.stack_base);
4300                            self.interp.pop_scope_to_depth(frame.scope_depth);
4301                            self.interp.current_sub_stack.pop();
4302                            if frame.jit_trampoline_return {
4303                                self.jit_trampoline_out = Some(StrykeValue::UNDEF);
4304                            } else {
4305                                self.push(StrykeValue::UNDEF);
4306                                self.ip = frame.return_ip;
4307                            }
4308                        } else {
4309                            self.exit_main_dispatch = true;
4310                        }
4311                        Ok(())
4312                    }
4313                    Op::ReturnValue => {
4314                        let val = self.pop();
4315                        // Resolve binding refs to real refs before scope cleanup.
4316                        // `\@array` creates a name-based ArrayBindingRef that looks
4317                        // up by name at dereference time.  If the array is a `my`
4318                        // variable, its frame will be destroyed below — so we must
4319                        // snapshot the data into an Arc-based ref now.
4320                        let val = self.resolve_binding_ref(val);
4321                        // Caller-context coercion: `return LIST` from a sub called
4322                        // in scalar context yields the **last** element of the
4323                        // list (Perl wantarray semantics). Without this, the
4324                        // whole list propagates and a `my $x = sub_returning_list()`
4325                        // sees the array stringified rather than its last element.
4326                        // (BUG-010 / BUG-011)
4327                        let val = if matches!(self.interp.wantarray_kind, WantarrayCtx::Scalar) {
4328                            if let Some(items) = val.as_array_vec() {
4329                                items.last().cloned().unwrap_or(StrykeValue::UNDEF)
4330                            } else {
4331                                val
4332                            }
4333                        } else {
4334                            val
4335                        };
4336                        if let Some(frame) = self.call_stack.pop() {
4337                            if frame.block_region {
4338                                return Err(PerlError::runtime(
4339                                    "Return in map/grep/sort block bytecode",
4340                                    self.line(),
4341                                ));
4342                            }
4343                            if let Some(t0) = frame.sub_profiler_start {
4344                                if let Some(p) = &mut self.interp.profiler {
4345                                    p.exit_sub(t0.elapsed());
4346                                }
4347                            }
4348                            self.interp.debugger_leave_sub();
4349                            self.interp.wantarray_kind = frame.saved_wantarray;
4350                            self.stack.truncate(frame.stack_base);
4351                            self.interp.pop_scope_to_depth(frame.scope_depth);
4352                            self.interp.current_sub_stack.pop();
4353                            if frame.jit_trampoline_return {
4354                                self.jit_trampoline_out = Some(val);
4355                            } else {
4356                                self.push(val);
4357                                self.ip = frame.return_ip;
4358                            }
4359                        } else {
4360                            self.exit_main_dispatch_value = Some(val);
4361                            self.exit_main_dispatch = true;
4362                        }
4363                        Ok(())
4364                    }
4365                    Op::BlockReturnValue => {
4366                        let val = self.pop();
4367                        let val = self.resolve_binding_ref(val);
4368                        if let Some(frame) = self.call_stack.pop() {
4369                            if !frame.block_region {
4370                                return Err(PerlError::runtime(
4371                                    "BlockReturnValue without map/grep/sort block frame",
4372                                    self.line(),
4373                                ));
4374                            }
4375                            self.interp.wantarray_kind = frame.saved_wantarray;
4376                            self.stack.truncate(frame.stack_base);
4377                            self.interp.pop_scope_to_depth(frame.scope_depth);
4378                            self.block_region_return = Some(val);
4379                            Ok(())
4380                        } else {
4381                            Err(PerlError::runtime(
4382                                "BlockReturnValue with empty call stack",
4383                                self.line(),
4384                            ))
4385                        }
4386                    }
4387                    Op::BindSubClosure(name_idx) => {
4388                        let n = names[*name_idx as usize].as_str();
4389                        self.interp.rebind_sub_closure(n);
4390                        Ok(())
4391                    }
4392
4393                    // ── Scope ──
4394                    Op::PushFrame => {
4395                        self.interp.scope_push_hook();
4396                        Ok(())
4397                    }
4398                    Op::PopFrame => {
4399                        self.interp.scope_pop_hook();
4400                        Ok(())
4401                    }
4402                    // ── I/O ──
4403                    Op::Print(handle_idx, argc) => {
4404                        let argc = *argc as usize;
4405                        let mut args = Vec::with_capacity(argc);
4406                        for _ in 0..argc {
4407                            args.push(self.pop());
4408                        }
4409                        args.reverse();
4410                        let mut output = String::new();
4411                        if args.is_empty() {
4412                            let topic = self.interp.scope.get_scalar("_").clone();
4413                            let s = match self.interp.stringify_value(topic, self.line()) {
4414                                Ok(s) => s,
4415                                Err(FlowOrError::Error(e)) => return Err(e),
4416                                Err(FlowOrError::Flow(_)) => {
4417                                    return Err(PerlError::runtime(
4418                                        "print: unexpected control flow",
4419                                        self.line(),
4420                                    ));
4421                                }
4422                            };
4423                            output.push_str(&s);
4424                        } else {
4425                            for (i, arg) in args.iter().enumerate() {
4426                                if i > 0 && !self.interp.ofs.is_empty() {
4427                                    output.push_str(&self.interp.ofs);
4428                                }
4429                                for item in arg.to_list() {
4430                                    let s = match self.interp.stringify_value(item, self.line()) {
4431                                        Ok(s) => s,
4432                                        Err(FlowOrError::Error(e)) => return Err(e),
4433                                        Err(FlowOrError::Flow(_)) => {
4434                                            return Err(PerlError::runtime(
4435                                                "print: unexpected control flow",
4436                                                self.line(),
4437                                            ));
4438                                        }
4439                                    };
4440                                    output.push_str(&s);
4441                                }
4442                            }
4443                        }
4444                        output.push_str(&self.interp.ors);
4445                        let handle_name = match handle_idx {
4446                            Some(idx) => self.interp.resolve_io_handle_name(
4447                                self.names
4448                                    .get(*idx as usize)
4449                                    .map_or("STDOUT", |s| s.as_str()),
4450                            ),
4451                            None => self
4452                                .interp
4453                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
4454                        };
4455                        self.interp.write_formatted_print(
4456                            handle_name.as_str(),
4457                            &output,
4458                            self.line(),
4459                        )?;
4460                        self.push(StrykeValue::integer(1));
4461                        Ok(())
4462                    }
4463                    Op::Say(handle_idx, argc) => {
4464                        if (self.interp.feature_bits & crate::vm_helper::FEAT_SAY) == 0 {
4465                            return Err(PerlError::runtime(
4466                            "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
4467                            self.line(),
4468                        ));
4469                        }
4470                        let argc = *argc as usize;
4471                        let mut args = Vec::with_capacity(argc);
4472                        for _ in 0..argc {
4473                            args.push(self.pop());
4474                        }
4475                        args.reverse();
4476                        let mut output = String::new();
4477                        if args.is_empty() {
4478                            let topic = self.interp.scope.get_scalar("_").clone();
4479                            let s = match self.interp.stringify_value(topic, self.line()) {
4480                                Ok(s) => s,
4481                                Err(FlowOrError::Error(e)) => return Err(e),
4482                                Err(FlowOrError::Flow(_)) => {
4483                                    return Err(PerlError::runtime(
4484                                        "say: unexpected control flow",
4485                                        self.line(),
4486                                    ));
4487                                }
4488                            };
4489                            output.push_str(&s);
4490                        } else {
4491                            for (i, arg) in args.iter().enumerate() {
4492                                if i > 0 && !self.interp.ofs.is_empty() {
4493                                    output.push_str(&self.interp.ofs);
4494                                }
4495                                for item in arg.to_list() {
4496                                    let s = match self.interp.stringify_value(item, self.line()) {
4497                                        Ok(s) => s,
4498                                        Err(FlowOrError::Error(e)) => return Err(e),
4499                                        Err(FlowOrError::Flow(_)) => {
4500                                            return Err(PerlError::runtime(
4501                                                "say: unexpected control flow",
4502                                                self.line(),
4503                                            ));
4504                                        }
4505                                    };
4506                                    output.push_str(&s);
4507                                }
4508                            }
4509                        }
4510                        output.push('\n');
4511                        output.push_str(&self.interp.ors);
4512                        let handle_name = match handle_idx {
4513                            Some(idx) => self.interp.resolve_io_handle_name(
4514                                self.names
4515                                    .get(*idx as usize)
4516                                    .map_or("STDOUT", |s| s.as_str()),
4517                            ),
4518                            None => self
4519                                .interp
4520                                .resolve_io_handle_name(self.interp.default_print_handle.as_str()),
4521                        };
4522                        self.interp.write_formatted_print(
4523                            handle_name.as_str(),
4524                            &output,
4525                            self.line(),
4526                        )?;
4527                        self.push(StrykeValue::integer(1));
4528                        Ok(())
4529                    }
4530
4531                    // ── Built-in dispatch ──
4532                    Op::CallBuiltin(id, argc) => {
4533                        let argc = *argc as usize;
4534                        let mut args = Vec::with_capacity(argc);
4535                        for _ in 0..argc {
4536                            args.push(self.pop());
4537                        }
4538                        args.reverse();
4539                        let result = self.exec_builtin(*id, args)?;
4540                        self.push(result);
4541                        Ok(())
4542                    }
4543                    Op::WantarrayPush(wa) => {
4544                        self.wantarray_stack.push(self.interp.wantarray_kind);
4545                        self.interp.wantarray_kind = WantarrayCtx::from_byte(*wa);
4546                        Ok(())
4547                    }
4548                    Op::WantarrayPop => {
4549                        self.interp.wantarray_kind =
4550                            self.wantarray_stack.pop().unwrap_or(WantarrayCtx::Scalar);
4551                        Ok(())
4552                    }
4553
4554                    // ── List / Range ──
4555                    Op::MakeArray(n) => {
4556                        let n = *n as usize;
4557                        // Pops are last-to-first on the stack; reverse to source (left-to-right) order,
4558                        // then flatten nested arrays in place (Perl list literal semantics).
4559                        // Hashes flatten to alternating key/value entries — Perl's
4560                        // `(%a, %b)` splat-merge idiom relies on this; without it
4561                        // each hash collapses to its scalar bucket-fill string.
4562                        let mut stack_vals = Vec::with_capacity(n);
4563                        for _ in 0..n {
4564                            stack_vals.push(self.pop());
4565                        }
4566                        stack_vals.reverse();
4567                        let mut arr = Vec::new();
4568                        for v in stack_vals {
4569                            if let Some(items) = v.as_array_vec() {
4570                                arr.extend(items);
4571                            } else if let Some(map) = v.as_hash_map() {
4572                                for (k, vv) in map {
4573                                    arr.push(StrykeValue::string(k));
4574                                    arr.push(vv);
4575                                }
4576                            } else {
4577                                arr.push(v);
4578                            }
4579                        }
4580                        self.push(StrykeValue::array(arr));
4581                        Ok(())
4582                    }
4583                    Op::HashSliceDeref(n) => {
4584                        let n = *n as usize;
4585                        let mut key_vals = Vec::with_capacity(n);
4586                        for _ in 0..n {
4587                            key_vals.push(self.pop());
4588                        }
4589                        key_vals.reverse();
4590                        let container = self.pop();
4591                        let line = self.line();
4592                        let out = vm_interp_result(
4593                            self.interp
4594                                .hash_slice_deref_values(&container, &key_vals, line),
4595                            line,
4596                        )?;
4597                        self.push(out);
4598                        Ok(())
4599                    }
4600                    Op::ArrowArraySlice(n) => {
4601                        let n = *n as usize;
4602                        let idxs = self.pop_flattened_array_slice_specs(n);
4603                        let r = self.pop();
4604                        let line = self.line();
4605                        let out = vm_interp_result(
4606                            self.interp.arrow_array_slice_values(r, &idxs, line),
4607                            line,
4608                        )?;
4609                        self.push(out);
4610                        Ok(())
4611                    }
4612                    Op::SetHashSliceDeref(n) => {
4613                        let n = *n as usize;
4614                        let mut key_vals = Vec::with_capacity(n);
4615                        for _ in 0..n {
4616                            key_vals.push(self.pop());
4617                        }
4618                        key_vals.reverse();
4619                        let container = self.pop();
4620                        let val = self.pop();
4621                        let line = self.line();
4622                        vm_interp_result(
4623                            self.interp
4624                                .assign_hash_slice_deref(container, key_vals, val, line),
4625                            line,
4626                        )?;
4627                        Ok(())
4628                    }
4629                    Op::SetHashSlice(hash_idx, n) => {
4630                        let n = *n as usize;
4631                        let mut key_vals = Vec::with_capacity(n);
4632                        for _ in 0..n {
4633                            key_vals.push(self.pop());
4634                        }
4635                        key_vals.reverse();
4636                        let name = names[*hash_idx as usize].as_str();
4637                        self.require_hash_mutable(name)?;
4638                        let val = self.pop();
4639                        let line = self.line();
4640                        vm_interp_result(
4641                            self.interp
4642                                .assign_named_hash_slice(name, key_vals, val, line),
4643                            line,
4644                        )?;
4645                        Ok(())
4646                    }
4647                    Op::GetHashSlice(hash_idx, n) => {
4648                        let n = *n as usize;
4649                        let mut key_vals = Vec::with_capacity(n);
4650                        for _ in 0..n {
4651                            key_vals.push(self.pop());
4652                        }
4653                        key_vals.reverse();
4654                        let name = names[*hash_idx as usize].as_str();
4655                        let h = self.interp.scope.get_hash(name);
4656                        let mut result = Vec::new();
4657                        for kv in &key_vals {
4658                            if let Some(vv) = kv.as_array_vec() {
4659                                for v in vv {
4660                                    let k = v.to_string();
4661                                    result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
4662                                }
4663                            } else {
4664                                let k = kv.to_string();
4665                                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
4666                            }
4667                        }
4668                        self.push(StrykeValue::array(result));
4669                        Ok(())
4670                    }
4671                    Op::HashSliceDerefCompound(op_byte, n) => {
4672                        let n = *n as usize;
4673                        let mut key_vals = Vec::with_capacity(n);
4674                        for _ in 0..n {
4675                            key_vals.push(self.pop());
4676                        }
4677                        key_vals.reverse();
4678                        let container = self.pop();
4679                        let rhs = self.pop();
4680                        let line = self.line();
4681                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
4682                            .ok_or_else(|| {
4683                                crate::error::PerlError::runtime(
4684                                    "VM: HashSliceDerefCompound: bad op byte",
4685                                    line,
4686                                )
4687                            })?;
4688                        let new_val = vm_interp_result(
4689                            self.interp.compound_assign_hash_slice_deref(
4690                                container, key_vals, op, rhs, line,
4691                            ),
4692                            line,
4693                        )?;
4694                        self.push(new_val);
4695                        Ok(())
4696                    }
4697                    Op::HashSliceDerefIncDec(kind, n) => {
4698                        let n = *n as usize;
4699                        let mut key_vals = Vec::with_capacity(n);
4700                        for _ in 0..n {
4701                            key_vals.push(self.pop());
4702                        }
4703                        key_vals.reverse();
4704                        let container = self.pop();
4705                        let line = self.line();
4706                        let out = vm_interp_result(
4707                            self.interp
4708                                .hash_slice_deref_inc_dec(container, key_vals, *kind, line),
4709                            line,
4710                        )?;
4711                        self.push(out);
4712                        Ok(())
4713                    }
4714                    Op::NamedHashSliceCompound(op_byte, hash_idx, n) => {
4715                        let n = *n as usize;
4716                        let mut key_vals = Vec::with_capacity(n);
4717                        for _ in 0..n {
4718                            key_vals.push(self.pop());
4719                        }
4720                        key_vals.reverse();
4721                        let name = names[*hash_idx as usize].as_str();
4722                        self.require_hash_mutable(name)?;
4723                        let rhs = self.pop();
4724                        let line = self.line();
4725                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
4726                            .ok_or_else(|| {
4727                                crate::error::PerlError::runtime(
4728                                    "VM: NamedHashSliceCompound: bad op byte",
4729                                    line,
4730                                )
4731                            })?;
4732                        let new_val = vm_interp_result(
4733                            self.interp
4734                                .compound_assign_named_hash_slice(name, key_vals, op, rhs, line),
4735                            line,
4736                        )?;
4737                        self.push(new_val);
4738                        Ok(())
4739                    }
4740                    Op::NamedHashSliceIncDec(kind, hash_idx, n) => {
4741                        let n = *n as usize;
4742                        let mut key_vals = Vec::with_capacity(n);
4743                        for _ in 0..n {
4744                            key_vals.push(self.pop());
4745                        }
4746                        key_vals.reverse();
4747                        let name = names[*hash_idx as usize].as_str();
4748                        self.require_hash_mutable(name)?;
4749                        let line = self.line();
4750                        let out = vm_interp_result(
4751                            self.interp
4752                                .named_hash_slice_inc_dec(name, key_vals, *kind, line),
4753                            line,
4754                        )?;
4755                        self.push(out);
4756                        Ok(())
4757                    }
4758                    Op::NamedHashSlicePeekLast(hash_idx, n) => {
4759                        let n = *n as usize;
4760                        let line = self.line();
4761                        let name = names[*hash_idx as usize].as_str();
4762                        self.require_hash_mutable(name)?;
4763                        let len = self.stack.len();
4764                        if len < n {
4765                            return Err(PerlError::runtime(
4766                                "VM: NamedHashSlicePeekLast: stack underflow",
4767                                line,
4768                            ));
4769                        }
4770                        let base = len - n;
4771                        let key_vals: Vec<StrykeValue> = self.stack[base..base + n].to_vec();
4772                        let ks = Self::flatten_hash_slice_key_slots(&key_vals);
4773                        let last_k = ks.last().ok_or_else(|| {
4774                            PerlError::runtime("VM: NamedHashSlicePeekLast: empty key list", line)
4775                        })?;
4776                        self.interp.touch_env_hash(name);
4777                        let cur = self.interp.scope.get_hash_element(name, last_k.as_str());
4778                        self.push(cur);
4779                        Ok(())
4780                    }
4781                    Op::NamedHashSliceDropKeysKeepCur(n) => {
4782                        let n = *n as usize;
4783                        let cur = self.pop();
4784                        for _ in 0..n {
4785                            self.pop();
4786                        }
4787                        self.push(cur);
4788                        Ok(())
4789                    }
4790                    Op::SetNamedHashSliceLastKeep(hash_idx, n) => {
4791                        let n = *n as usize;
4792                        let line = self.line();
4793                        let name = names[*hash_idx as usize].as_str();
4794                        self.require_hash_mutable(name)?;
4795                        let mut key_vals_rev = Vec::with_capacity(n);
4796                        for _ in 0..n {
4797                            key_vals_rev.push(self.pop());
4798                        }
4799                        key_vals_rev.reverse();
4800                        let mut val = self.pop();
4801                        if let Some(av) = val.as_array_vec() {
4802                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
4803                        }
4804                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
4805                        let last_k = ks.last().ok_or_else(|| {
4806                            PerlError::runtime(
4807                                "VM: SetNamedHashSliceLastKeep: empty key list",
4808                                line,
4809                            )
4810                        })?;
4811                        let val_keep = val.clone();
4812                        self.interp.touch_env_hash(name);
4813                        vm_interp_result(
4814                            self.interp
4815                                .scope
4816                                .set_hash_element(name, last_k.as_str(), val)
4817                                .map(|()| StrykeValue::UNDEF)
4818                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
4819                            line,
4820                        )?;
4821                        self.push(val_keep);
4822                        Ok(())
4823                    }
4824                    Op::HashSliceDerefPeekLast(n) => {
4825                        let n = *n as usize;
4826                        let line = self.line();
4827                        let len = self.stack.len();
4828                        if len < n + 1 {
4829                            return Err(PerlError::runtime(
4830                                "VM: HashSliceDerefPeekLast: stack underflow",
4831                                line,
4832                            ));
4833                        }
4834                        let base = len - n - 1;
4835                        let container = self.stack[base].clone();
4836                        let key_vals: Vec<StrykeValue> =
4837                            self.stack[base + 1..base + 1 + n].to_vec();
4838                        let list = vm_interp_result(
4839                            self.interp
4840                                .hash_slice_deref_values(&container, &key_vals, line),
4841                            line,
4842                        )?;
4843                        let cur = list.to_list().last().cloned().unwrap_or(StrykeValue::UNDEF);
4844                        self.push(cur);
4845                        Ok(())
4846                    }
4847                    Op::HashSliceDerefRollValUnderKeys(n) => {
4848                        let n = *n as usize;
4849                        let val = self.pop();
4850                        let mut keys_rev = Vec::with_capacity(n);
4851                        for _ in 0..n {
4852                            keys_rev.push(self.pop());
4853                        }
4854                        let container = self.pop();
4855                        keys_rev.reverse();
4856                        self.push(val);
4857                        self.push(container);
4858                        for k in keys_rev {
4859                            self.push(k);
4860                        }
4861                        Ok(())
4862                    }
4863                    Op::HashSliceDerefSetLastKeep(n) => {
4864                        let n = *n as usize;
4865                        let line = self.line();
4866                        let mut key_vals_rev = Vec::with_capacity(n);
4867                        for _ in 0..n {
4868                            key_vals_rev.push(self.pop());
4869                        }
4870                        key_vals_rev.reverse();
4871                        let container = self.pop();
4872                        let mut val = self.pop();
4873                        if let Some(av) = val.as_array_vec() {
4874                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
4875                        }
4876                        let ks = Self::flatten_hash_slice_key_slots(&key_vals_rev);
4877                        let last_k = ks.last().ok_or_else(|| {
4878                            PerlError::runtime(
4879                                "VM: HashSliceDerefSetLastKeep: empty key list",
4880                                line,
4881                            )
4882                        })?;
4883                        let val_keep = val.clone();
4884                        vm_interp_result(
4885                            self.interp.assign_hash_slice_one_key(
4886                                container,
4887                                last_k.as_str(),
4888                                val,
4889                                line,
4890                            ),
4891                            line,
4892                        )?;
4893                        self.push(val_keep);
4894                        Ok(())
4895                    }
4896                    Op::HashSliceDerefDropKeysKeepCur(n) => {
4897                        let n = *n as usize;
4898                        let cur = self.pop();
4899                        for _ in 0..n {
4900                            self.pop();
4901                        }
4902                        let _container = self.pop();
4903                        self.push(cur);
4904                        Ok(())
4905                    }
4906                    Op::SetArrowArraySlice(n) => {
4907                        let n = *n as usize;
4908                        let idxs = self.pop_flattened_array_slice_specs(n);
4909                        let aref = self.pop();
4910                        let val = self.pop();
4911                        let line = self.line();
4912                        vm_interp_result(
4913                            self.interp.assign_arrow_array_slice(aref, idxs, val, line),
4914                            line,
4915                        )?;
4916                        Ok(())
4917                    }
4918                    Op::ArrowArraySliceCompound(op_byte, n) => {
4919                        let n = *n as usize;
4920                        let idxs = self.pop_flattened_array_slice_specs(n);
4921                        let aref = self.pop();
4922                        let rhs = self.pop();
4923                        let line = self.line();
4924                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
4925                            .ok_or_else(|| {
4926                                crate::error::PerlError::runtime(
4927                                    "VM: ArrowArraySliceCompound: bad op byte",
4928                                    line,
4929                                )
4930                            })?;
4931                        let new_val = vm_interp_result(
4932                            self.interp
4933                                .compound_assign_arrow_array_slice(aref, idxs, op, rhs, line),
4934                            line,
4935                        )?;
4936                        self.push(new_val);
4937                        Ok(())
4938                    }
4939                    Op::ArrowArraySliceIncDec(kind, n) => {
4940                        let n = *n as usize;
4941                        let idxs = self.pop_flattened_array_slice_specs(n);
4942                        let aref = self.pop();
4943                        let line = self.line();
4944                        let out = vm_interp_result(
4945                            self.interp
4946                                .arrow_array_slice_inc_dec(aref, idxs, *kind, line),
4947                            line,
4948                        )?;
4949                        self.push(out);
4950                        Ok(())
4951                    }
4952                    Op::ArrowArraySlicePeekLast(n) => {
4953                        let n = *n as usize;
4954                        let line = self.line();
4955                        let len = self.stack.len();
4956                        if len < n + 1 {
4957                            return Err(PerlError::runtime(
4958                                "VM: ArrowArraySlicePeekLast: stack underflow",
4959                                line,
4960                            ));
4961                        }
4962                        let base = len - n - 1;
4963                        let aref = self.stack[base].clone();
4964                        let idxs =
4965                            self.flatten_array_slice_specs_ordered_values(&self.stack[base + 1..])?;
4966                        let last = *idxs.last().ok_or_else(|| {
4967                            PerlError::runtime(
4968                                "VM: ArrowArraySlicePeekLast: empty index list",
4969                                line,
4970                            )
4971                        })?;
4972                        let cur = vm_interp_result(
4973                            self.interp.read_arrow_array_element(aref, last, line),
4974                            line,
4975                        )?;
4976                        self.push(cur);
4977                        Ok(())
4978                    }
4979                    Op::ArrowArraySliceDropKeysKeepCur(n) => {
4980                        let n = *n as usize;
4981                        let cur = self.pop();
4982                        let _idxs = self.pop_flattened_array_slice_specs(n);
4983                        let _aref = self.pop();
4984                        self.push(cur);
4985                        Ok(())
4986                    }
4987                    Op::ArrowArraySliceRollValUnderSpecs(n) => {
4988                        let n = *n as usize;
4989                        let val = self.pop();
4990                        let mut specs_rev = Vec::with_capacity(n);
4991                        for _ in 0..n {
4992                            specs_rev.push(self.pop());
4993                        }
4994                        let aref = self.pop();
4995                        self.push(val);
4996                        self.push(aref);
4997                        for s in specs_rev.into_iter().rev() {
4998                            self.push(s);
4999                        }
5000                        Ok(())
5001                    }
5002                    Op::SetArrowArraySliceLastKeep(n) => {
5003                        let n = *n as usize;
5004                        let line = self.line();
5005                        let idxs = self.pop_flattened_array_slice_specs(n);
5006                        let aref = self.pop();
5007                        let mut val = self.pop();
5008                        // RHS is compiled in list context (`(3,4)` → one array value); Perl assigns
5009                        // only the **last** list element to the last slice index (`||=` / `&&=` / `//=`).
5010                        if let Some(av) = val.as_array_vec() {
5011                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5012                        }
5013                        let last = *idxs.last().ok_or_else(|| {
5014                            PerlError::runtime(
5015                                "VM: SetArrowArraySliceLastKeep: empty index list",
5016                                line,
5017                            )
5018                        })?;
5019                        let val_keep = val.clone();
5020                        vm_interp_result(
5021                            self.interp.assign_arrow_array_deref(aref, last, val, line),
5022                            line,
5023                        )?;
5024                        self.push(val_keep);
5025                        Ok(())
5026                    }
5027                    Op::NamedArraySliceIncDec(kind, arr_idx, n) => {
5028                        let n = *n as usize;
5029                        let idxs = self.pop_flattened_array_slice_specs(n);
5030                        let name = names[*arr_idx as usize].as_str();
5031                        self.require_array_mutable(name)?;
5032                        let line = self.line();
5033                        let out = vm_interp_result(
5034                            self.interp
5035                                .named_array_slice_inc_dec(name, idxs, *kind, line),
5036                            line,
5037                        )?;
5038                        self.push(out);
5039                        Ok(())
5040                    }
5041                    Op::NamedArraySliceCompound(op_byte, arr_idx, n) => {
5042                        let n = *n as usize;
5043                        let idxs = self.pop_flattened_array_slice_specs(n);
5044                        let name = names[*arr_idx as usize].as_str();
5045                        self.require_array_mutable(name)?;
5046                        let rhs = self.pop();
5047                        let line = self.line();
5048                        let op = crate::compiler::scalar_compound_op_from_byte(*op_byte)
5049                            .ok_or_else(|| {
5050                                crate::error::PerlError::runtime(
5051                                    "VM: NamedArraySliceCompound: bad op byte",
5052                                    line,
5053                                )
5054                            })?;
5055                        let new_val = vm_interp_result(
5056                            self.interp
5057                                .compound_assign_named_array_slice(name, idxs, op, rhs, line),
5058                            line,
5059                        )?;
5060                        self.push(new_val);
5061                        Ok(())
5062                    }
5063                    Op::NamedArraySlicePeekLast(arr_idx, n) => {
5064                        let n = *n as usize;
5065                        let line = self.line();
5066                        let name = names[*arr_idx as usize].as_str();
5067                        self.require_array_mutable(name)?;
5068                        let len = self.stack.len();
5069                        if len < n {
5070                            return Err(PerlError::runtime(
5071                                "VM: NamedArraySlicePeekLast: stack underflow",
5072                                line,
5073                            ));
5074                        }
5075                        let base = len - n;
5076                        let idxs =
5077                            self.flatten_array_slice_specs_ordered_values(&self.stack[base..])?;
5078                        let last = *idxs.last().ok_or_else(|| {
5079                            PerlError::runtime(
5080                                "VM: NamedArraySlicePeekLast: empty index list",
5081                                line,
5082                            )
5083                        })?;
5084                        let cur = self.interp.scope.get_array_element(name, last);
5085                        self.push(cur);
5086                        Ok(())
5087                    }
5088                    Op::NamedArraySliceDropKeysKeepCur(n) => {
5089                        let n = *n as usize;
5090                        let cur = self.pop();
5091                        let _idxs = self.pop_flattened_array_slice_specs(n);
5092                        self.push(cur);
5093                        Ok(())
5094                    }
5095                    Op::NamedArraySliceRollValUnderSpecs(n) => {
5096                        let n = *n as usize;
5097                        let val = self.pop();
5098                        let mut specs_rev = Vec::with_capacity(n);
5099                        for _ in 0..n {
5100                            specs_rev.push(self.pop());
5101                        }
5102                        self.push(val);
5103                        for s in specs_rev.into_iter().rev() {
5104                            self.push(s);
5105                        }
5106                        Ok(())
5107                    }
5108                    Op::SetNamedArraySliceLastKeep(arr_idx, n) => {
5109                        let n = *n as usize;
5110                        let line = self.line();
5111                        let idxs = self.pop_flattened_array_slice_specs(n);
5112                        let name = names[*arr_idx as usize].as_str();
5113                        self.require_array_mutable(name)?;
5114                        let mut val = self.pop();
5115                        if let Some(av) = val.as_array_vec() {
5116                            val = av.last().cloned().unwrap_or(StrykeValue::UNDEF);
5117                        }
5118                        let last = *idxs.last().ok_or_else(|| {
5119                            PerlError::runtime(
5120                                "VM: SetNamedArraySliceLastKeep: empty index list",
5121                                line,
5122                            )
5123                        })?;
5124                        let val_keep = val.clone();
5125                        vm_interp_result(
5126                            self.interp
5127                                .scope
5128                                .set_array_element(name, last, val)
5129                                .map(|()| StrykeValue::UNDEF)
5130                                .map_err(|e| FlowOrError::Error(e.at_line(line))),
5131                            line,
5132                        )?;
5133                        self.push(val_keep);
5134                        Ok(())
5135                    }
5136                    Op::SetNamedArraySlice(arr_idx, n) => {
5137                        let n = *n as usize;
5138                        let idxs = self.pop_flattened_array_slice_specs(n);
5139                        let name = names[*arr_idx as usize].as_str();
5140                        self.require_array_mutable(name)?;
5141                        let val = self.pop();
5142                        let line = self.line();
5143                        vm_interp_result(
5144                            self.interp.assign_named_array_slice(name, idxs, val, line),
5145                            line,
5146                        )?;
5147                        Ok(())
5148                    }
5149                    Op::MakeHash(n) => {
5150                        let n = *n as usize;
5151                        let mut items = Vec::with_capacity(n);
5152                        for _ in 0..n {
5153                            items.push(self.pop());
5154                        }
5155                        items.reverse();
5156                        let mut map = IndexMap::new();
5157                        let mut i = 0;
5158                        while i + 1 < items.len() {
5159                            map.insert(items[i].to_string(), items[i + 1].clone());
5160                            i += 2;
5161                        }
5162                        self.push(StrykeValue::hash(map));
5163                        Ok(())
5164                    }
5165                    Op::Range => {
5166                        let to = self.pop();
5167                        let from = self.pop();
5168                        let arr = perl_list_range_expand(from, to);
5169                        self.push(StrykeValue::array(arr));
5170                        Ok(())
5171                    }
5172                    Op::RangeStep => {
5173                        let step = self.pop();
5174                        let to = self.pop();
5175                        let from = self.pop();
5176                        let arr = crate::value::perl_list_range_expand_stepped(from, to, step);
5177                        self.push(StrykeValue::array(arr));
5178                        Ok(())
5179                    }
5180                    Op::ArraySliceRange(arr_idx) => {
5181                        let step = self.pop();
5182                        let to = self.pop();
5183                        let from = self.pop();
5184                        let line = self.line();
5185                        let name = names[*arr_idx as usize].as_str();
5186                        // Stryke topic-string slice: `_[from:to:step]` parses
5187                        // to ArraySliceRange on a `__topicstr__N` name.
5188                        if let Some(real) = name.strip_prefix("__topicstr__") {
5189                            let s = self.interp.scope.get_scalar(real).to_string();
5190                            let chars: Vec<char> = s.chars().collect();
5191                            let n = chars.len() as i64;
5192                            let step_i = if step.is_undef() { 1 } else { step.to_int() };
5193                            // Open slices: defaults depend on step direction
5194                            // step > 0: from=0, to=n-1 (forward)
5195                            // step < 0: from=n-1, to=0 (backward)
5196                            let mut from_i = if from.is_undef() {
5197                                if step_i >= 0 {
5198                                    0
5199                                } else {
5200                                    n - 1
5201                                }
5202                            } else {
5203                                from.to_int()
5204                            };
5205                            let mut to_i = if to.is_undef() {
5206                                if step_i >= 0 {
5207                                    n - 1
5208                                } else {
5209                                    0
5210                                }
5211                            } else {
5212                                to.to_int()
5213                            };
5214                            if from_i < 0 {
5215                                from_i += n
5216                            }
5217                            if to_i < 0 {
5218                                to_i += n
5219                            }
5220                            let mut out = String::new();
5221                            if step_i > 0 {
5222                                let mut i = from_i;
5223                                while i <= to_i && i < n {
5224                                    if i >= 0 {
5225                                        out.push(chars[i as usize]);
5226                                    }
5227                                    i += step_i;
5228                                }
5229                            } else if step_i < 0 {
5230                                let mut i = from_i;
5231                                while i >= to_i && i >= 0 {
5232                                    if i < n {
5233                                        out.push(chars[i as usize]);
5234                                    }
5235                                    i += step_i;
5236                                }
5237                            }
5238                            self.push(StrykeValue::string(out));
5239                            return Ok(());
5240                        }
5241                        let arr_len = self.interp.scope.array_len(name) as i64;
5242                        // Stryke string-slice sugar: when `@name` is empty
5243                        // (or doesn't exist) but `$name` is a non-empty
5244                        // string, treat `$name[from:to:step]` as Python-style
5245                        // substring slice. Returns a *string*, not an array.
5246                        if !crate::compat_mode()
5247                            && arr_len == 0
5248                            && self.interp.scope.scalar_binding_exists(name)
5249                        {
5250                            let s = self.interp.scope.get_scalar(name).to_string();
5251                            if !s.is_empty() {
5252                                let chars: Vec<char> = s.chars().collect();
5253                                let n = chars.len() as i64;
5254                                let step_i = if step.is_undef() { 1 } else { step.to_int() };
5255                                // Open slices: defaults depend on step direction
5256                                // step > 0: from=0, to=n-1 (forward)
5257                                // step < 0: from=n-1, to=0 (backward)
5258                                let mut from_i = if from.is_undef() {
5259                                    if step_i >= 0 {
5260                                        0
5261                                    } else {
5262                                        n - 1
5263                                    }
5264                                } else {
5265                                    from.to_int()
5266                                };
5267                                let mut to_i = if to.is_undef() {
5268                                    if step_i >= 0 {
5269                                        n - 1
5270                                    } else {
5271                                        0
5272                                    }
5273                                } else {
5274                                    to.to_int()
5275                                };
5276                                if from_i < 0 {
5277                                    from_i += n
5278                                }
5279                                if to_i < 0 {
5280                                    to_i += n
5281                                }
5282                                let mut out = String::new();
5283                                if step_i > 0 {
5284                                    let mut i = from_i;
5285                                    while i <= to_i && i < n {
5286                                        if i >= 0 {
5287                                            out.push(chars[i as usize]);
5288                                        }
5289                                        i += step_i;
5290                                    }
5291                                } else if step_i < 0 {
5292                                    let mut i = from_i;
5293                                    while i >= to_i && i >= 0 {
5294                                        if i < n {
5295                                            out.push(chars[i as usize]);
5296                                        }
5297                                        i += step_i;
5298                                    }
5299                                }
5300                                self.push(StrykeValue::string(out));
5301                                return Ok(());
5302                            }
5303                        }
5304                        let indices = match crate::value::compute_array_slice_indices(
5305                            arr_len, &from, &to, &step,
5306                        ) {
5307                            Ok(v) => v,
5308                            Err(msg) => {
5309                                return Err(PerlError::runtime(msg, line));
5310                            }
5311                        };
5312                        let mut out = Vec::with_capacity(indices.len());
5313                        for i in indices {
5314                            out.push(self.interp.scope.get_array_element(name, i));
5315                        }
5316                        self.push(StrykeValue::array(out));
5317                        Ok(())
5318                    }
5319                    Op::HashSliceRange(hash_idx) => {
5320                        let step = self.pop();
5321                        let to = self.pop();
5322                        let from = self.pop();
5323                        let line = self.line();
5324                        let name = names[*hash_idx as usize].as_str();
5325                        let keys = match crate::value::compute_hash_slice_keys(&from, &to, &step) {
5326                            Ok(v) => v,
5327                            Err(msg) => {
5328                                return Err(PerlError::runtime(msg, line));
5329                            }
5330                        };
5331                        let h = self.interp.scope.get_hash(name);
5332                        let mut out = Vec::with_capacity(keys.len());
5333                        for k in &keys {
5334                            out.push(h.get(k).cloned().unwrap_or(StrykeValue::UNDEF));
5335                        }
5336                        self.push(StrykeValue::array(out));
5337                        Ok(())
5338                    }
5339                    Op::ScalarFlipFlop(slot, exclusive) => {
5340                        let to = self.pop().to_int();
5341                        let from = self.pop().to_int();
5342                        let line = self.line();
5343                        let v = vm_interp_result(
5344                            self.interp
5345                                .scalar_flip_flop_eval(from, to, *slot as usize, *exclusive != 0)
5346                                .map_err(Into::into),
5347                            line,
5348                        )?;
5349                        self.push(v);
5350                        Ok(())
5351                    }
5352                    Op::RegexFlipFlop(slot, exclusive, lp, lf, rp, rf) => {
5353                        let line = self.line();
5354                        let left_pat = constants[*lp as usize].as_str_or_empty();
5355                        let left_flags = constants[*lf as usize].as_str_or_empty();
5356                        let right_pat = constants[*rp as usize].as_str_or_empty();
5357                        let right_flags = constants[*rf as usize].as_str_or_empty();
5358                        let v = vm_interp_result(
5359                            self.interp
5360                                .regex_flip_flop_eval(
5361                                    left_pat.as_str(),
5362                                    left_flags.as_str(),
5363                                    right_pat.as_str(),
5364                                    right_flags.as_str(),
5365                                    *slot as usize,
5366                                    *exclusive != 0,
5367                                    line,
5368                                )
5369                                .map_err(Into::into),
5370                            line,
5371                        )?;
5372                        self.push(v);
5373                        Ok(())
5374                    }
5375                    Op::RegexEofFlipFlop(slot, exclusive, lp, lf) => {
5376                        let line = self.line();
5377                        let left_pat = constants[*lp as usize].as_str_or_empty();
5378                        let left_flags = constants[*lf as usize].as_str_or_empty();
5379                        let v = vm_interp_result(
5380                            self.interp
5381                                .regex_eof_flip_flop_eval(
5382                                    left_pat.as_str(),
5383                                    left_flags.as_str(),
5384                                    *slot as usize,
5385                                    *exclusive != 0,
5386                                    line,
5387                                )
5388                                .map_err(Into::into),
5389                            line,
5390                        )?;
5391                        self.push(v);
5392                        Ok(())
5393                    }
5394                    Op::RegexFlipFlopExprRhs(slot, exclusive, lp, lf, rhs_idx) => {
5395                        let idx = *rhs_idx as usize;
5396                        let line = self.line();
5397                        let right_m = if let Some(&(start, end)) = self
5398                            .regex_flip_flop_rhs_expr_bytecode_ranges
5399                            .get(idx)
5400                            .and_then(|r| r.as_ref())
5401                        {
5402                            let val = self.run_block_region(start, end, op_count)?;
5403                            val.is_true()
5404                        } else {
5405                            let e = &self.regex_flip_flop_rhs_expr_entries[idx];
5406                            match self.interp.eval_boolean_rvalue_condition(e) {
5407                                Ok(b) => b,
5408                                Err(FlowOrError::Error(err)) => return Err(err),
5409                                Err(FlowOrError::Flow(_)) => {
5410                                    return Err(PerlError::runtime(
5411                                        "unexpected flow in regex flip-flop RHS",
5412                                        line,
5413                                    ))
5414                                }
5415                            }
5416                        };
5417                        let left_pat = constants[*lp as usize].as_str_or_empty();
5418                        let left_flags = constants[*lf as usize].as_str_or_empty();
5419                        let v = vm_interp_result(
5420                            self.interp
5421                                .regex_flip_flop_eval_dynamic_right(
5422                                    left_pat.as_str(),
5423                                    left_flags.as_str(),
5424                                    *slot as usize,
5425                                    *exclusive != 0,
5426                                    line,
5427                                    right_m,
5428                                )
5429                                .map_err(Into::into),
5430                            line,
5431                        )?;
5432                        self.push(v);
5433                        Ok(())
5434                    }
5435                    Op::RegexFlipFlopDotLineRhs(slot, exclusive, lp, lf, line_cidx) => {
5436                        let line = self.line();
5437                        let rhs_line = constants[*line_cidx as usize].to_int();
5438                        let left_pat = constants[*lp as usize].as_str_or_empty();
5439                        let left_flags = constants[*lf as usize].as_str_or_empty();
5440                        let v = vm_interp_result(
5441                            self.interp
5442                                .regex_flip_flop_eval_dot_line_rhs(
5443                                    left_pat.as_str(),
5444                                    left_flags.as_str(),
5445                                    *slot as usize,
5446                                    *exclusive != 0,
5447                                    line,
5448                                    rhs_line,
5449                                )
5450                                .map_err(Into::into),
5451                            line,
5452                        )?;
5453                        self.push(v);
5454                        Ok(())
5455                    }
5456
5457                    // ── Regex ──
5458                    Op::RegexMatch(pat_idx, flags_idx, scalar_g, pos_key_idx) => {
5459                        let val = self.pop();
5460                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
5461                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5462                        let line = self.line();
5463                        if val.is_iterator() {
5464                            let source = crate::map_stream::into_pull_iter(val);
5465                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
5466                                Ok(r) => r,
5467                                Err(FlowOrError::Error(e)) => return Err(e),
5468                                Err(FlowOrError::Flow(_)) => {
5469                                    return Err(PerlError::runtime(
5470                                        "unexpected flow in regex compile",
5471                                        line,
5472                                    ));
5473                                }
5474                            };
5475                            let global = flags.contains('g');
5476                            if global {
5477                                self.push(StrykeValue::iterator(std::sync::Arc::new(
5478                                    crate::map_stream::MatchGlobalStreamIterator::new(source, re),
5479                                )));
5480                            } else {
5481                                self.push(StrykeValue::iterator(std::sync::Arc::new(
5482                                    crate::map_stream::MatchStreamIterator::new(source, re),
5483                                )));
5484                            }
5485                            return Ok(());
5486                        }
5487                        let string = val.into_string();
5488                        let pos_key_owned = if *pos_key_idx == u16::MAX {
5489                            None
5490                        } else {
5491                            Some(constants[*pos_key_idx as usize].as_str_or_empty())
5492                        };
5493                        let pos_key: &str = pos_key_owned.as_deref().unwrap_or("_");
5494                        match self
5495                            .interp
5496                            .regex_match_execute(string, &pattern, &flags, *scalar_g, pos_key, line)
5497                        {
5498                            Ok(v) => {
5499                                self.push(v);
5500                                Ok(())
5501                            }
5502                            Err(FlowOrError::Error(e)) => Err(e),
5503                            Err(FlowOrError::Flow(_)) => {
5504                                Err(PerlError::runtime("unexpected flow in regex match", line))
5505                            }
5506                        }
5507                    }
5508                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lvalue_idx) => {
5509                        let val = self.pop();
5510                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
5511                        let replacement = constants[*repl_idx as usize].as_str_or_empty();
5512                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5513                        let line = self.line();
5514                        if val.is_iterator() {
5515                            let source = crate::map_stream::into_pull_iter(val);
5516                            let re = match self.interp.compile_regex(&pattern, &flags, line) {
5517                                Ok(r) => r,
5518                                Err(FlowOrError::Error(e)) => return Err(e),
5519                                Err(FlowOrError::Flow(_)) => {
5520                                    return Err(PerlError::runtime(
5521                                        "unexpected flow in regex compile",
5522                                        line,
5523                                    ));
5524                                }
5525                            };
5526                            let global = flags.contains('g');
5527                            self.push(StrykeValue::iterator(std::sync::Arc::new(
5528                                crate::map_stream::SubstStreamIterator::new(
5529                                    source,
5530                                    re,
5531                                    crate::vm_helper::normalize_replacement_backrefs(&replacement),
5532                                    global,
5533                                ),
5534                            )));
5535                            return Ok(());
5536                        }
5537                        let string = val.into_string();
5538                        let target = &self.lvalues[*lvalue_idx as usize];
5539                        match self.interp.regex_subst_execute(
5540                            string,
5541                            &pattern,
5542                            &replacement,
5543                            &flags,
5544                            target,
5545                            line,
5546                        ) {
5547                            Ok(v) => {
5548                                self.push(v);
5549                                Ok(())
5550                            }
5551                            Err(FlowOrError::Error(e)) => Err(e),
5552                            Err(FlowOrError::Flow(_)) => {
5553                                Err(PerlError::runtime("unexpected flow in s///", line))
5554                            }
5555                        }
5556                    }
5557                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lvalue_idx) => {
5558                        let val = self.pop();
5559                        let from = constants[*from_idx as usize].as_str_or_empty();
5560                        let to = constants[*to_idx as usize].as_str_or_empty();
5561                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5562                        let line = self.line();
5563                        if val.is_iterator() {
5564                            let source = crate::map_stream::into_pull_iter(val);
5565                            self.push(StrykeValue::iterator(std::sync::Arc::new(
5566                                crate::map_stream::TransliterateStreamIterator::new(
5567                                    source, &from, &to, &flags,
5568                                ),
5569                            )));
5570                            return Ok(());
5571                        }
5572                        let string = val.into_string();
5573                        let target = &self.lvalues[*lvalue_idx as usize];
5574                        match self
5575                            .interp
5576                            .regex_transliterate_execute(string, &from, &to, &flags, target, line)
5577                        {
5578                            Ok(v) => {
5579                                self.push(v);
5580                                Ok(())
5581                            }
5582                            Err(FlowOrError::Error(e)) => Err(e),
5583                            Err(FlowOrError::Flow(_)) => {
5584                                Err(PerlError::runtime("unexpected flow in tr///", line))
5585                            }
5586                        }
5587                    }
5588                    Op::RegexMatchDyn(negate) => {
5589                        let rhs = self.pop();
5590                        let s = self.pop().into_string();
5591                        let line = self.line();
5592                        let exec = if let Some((pat, fl)) = rhs.regex_src_and_flags() {
5593                            self.interp
5594                                .regex_match_execute(s, &pat, &fl, false, "_", line)
5595                        } else {
5596                            let pattern = rhs.into_string();
5597                            self.interp
5598                                .regex_match_execute(s, &pattern, "", false, "_", line)
5599                        };
5600                        match exec {
5601                            Ok(v) => {
5602                                let matched = v.is_true();
5603                                let out = if *negate { !matched } else { matched };
5604                                self.push(StrykeValue::integer(if out { 1 } else { 0 }));
5605                            }
5606                            Err(FlowOrError::Error(e)) => return Err(e),
5607                            Err(FlowOrError::Flow(_)) => {
5608                                return Err(PerlError::runtime("unexpected flow in =~", line));
5609                            }
5610                        }
5611                        Ok(())
5612                    }
5613                    Op::RegexBoolToScalar => {
5614                        let v = self.pop();
5615                        self.push(if v.is_true() {
5616                            StrykeValue::integer(1)
5617                        } else {
5618                            StrykeValue::string(String::new())
5619                        });
5620                        Ok(())
5621                    }
5622                    Op::SetRegexPos => {
5623                        let key = self.pop().to_string();
5624                        let val = self.pop();
5625                        if val.is_undef() {
5626                            self.interp.regex_pos.insert(key, None);
5627                        } else {
5628                            let u = val.to_int().max(0) as usize;
5629                            self.interp.regex_pos.insert(key, Some(u));
5630                        }
5631                        Ok(())
5632                    }
5633                    Op::LoadRegex(pat_idx, flags_idx) => {
5634                        let pattern = constants[*pat_idx as usize].as_str_or_empty();
5635                        let flags = constants[*flags_idx as usize].as_str_or_empty();
5636                        let line = self.line();
5637                        let pattern_owned = pattern.clone();
5638                        let re = match self.interp.compile_regex(&pattern, &flags, line) {
5639                            Ok(r) => r,
5640                            Err(FlowOrError::Error(e)) => return Err(e),
5641                            Err(FlowOrError::Flow(_)) => {
5642                                return Err(PerlError::runtime(
5643                                    "unexpected flow in qr// compile",
5644                                    line,
5645                                ));
5646                            }
5647                        };
5648                        self.push(StrykeValue::regex(re, pattern_owned, flags.to_string()));
5649                        Ok(())
5650                    }
5651                    Op::ConcatAppend(idx) => {
5652                        let rhs = self.pop();
5653                        let n = names[*idx as usize].as_str();
5654                        let line = self.line();
5655                        let result = self
5656                            .interp
5657                            .scope
5658                            .scalar_concat_inplace(n, &rhs)
5659                            .map_err(|e| e.at_line(line))?;
5660                        self.push(result);
5661                        Ok(())
5662                    }
5663                    Op::ConcatAppendSlot(slot) => {
5664                        let rhs = self.pop();
5665                        let result = self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
5666                        self.push(result);
5667                        Ok(())
5668                    }
5669                    Op::ConcatAppendSlotVoid(slot) => {
5670                        let rhs = self.pop();
5671                        self.interp.scope.scalar_slot_concat_inplace(*slot, &rhs);
5672                        Ok(())
5673                    }
5674                    Op::SlotLtIntJumpIfFalse(slot, limit, target) => {
5675                        let val = self.interp.scope.get_scalar_slot(*slot);
5676                        let lt = if let Some(i) = val.as_integer() {
5677                            i < *limit as i64
5678                        } else {
5679                            val.to_number() < *limit as f64
5680                        };
5681                        if !lt {
5682                            self.ip = *target;
5683                        }
5684                        Ok(())
5685                    }
5686                    Op::SlotIncLtIntJumpBack(slot, limit, body_target) => {
5687                        // Fused trailing `++$slot; goto top_test` for the bench_loop shape:
5688                        // matches `PreIncSlotVoid` + `Jump` + top `SlotLtIntJumpIfFalse` exactly so
5689                        // coercion, wrap-around, and integer-only write semantics line up byte-for-byte
5690                        // with the un-fused form. Every iteration past the first skips the top check
5691                        // and the unconditional jump entirely.
5692                        let next_i = self
5693                            .interp
5694                            .scope
5695                            .get_scalar_slot(*slot)
5696                            .to_int()
5697                            .wrapping_add(1);
5698                        self.interp
5699                            .scope
5700                            .set_scalar_slot(*slot, StrykeValue::integer(next_i));
5701                        if next_i < *limit as i64 {
5702                            self.ip = *body_target;
5703                        }
5704                        Ok(())
5705                    }
5706                    Op::AccumSumLoop(sum_slot, i_slot, limit) => {
5707                        // Runs the entire counted `while $i < limit { $sum += $i; $i += 1 }` loop in
5708                        // native Rust. The peephole only fires when the body is exactly this one
5709                        // accumulate statement, so every side effect is captured by the final
5710                        // `$sum` and `$i` writes; there is nothing else to do per iteration.
5711                        let mut sum = self.interp.scope.get_scalar_slot(*sum_slot).to_int();
5712                        let mut i = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5713                        let limit = *limit as i64;
5714                        while i < limit {
5715                            sum = sum.wrapping_add(i);
5716                            i = i.wrapping_add(1);
5717                        }
5718                        self.interp
5719                            .scope
5720                            .set_scalar_slot(*sum_slot, StrykeValue::integer(sum));
5721                        self.interp
5722                            .scope
5723                            .set_scalar_slot(*i_slot, StrykeValue::integer(i));
5724                        Ok(())
5725                    }
5726                    Op::AddHashElemPlainKeyToSlot(sum_slot, k_name_idx, h_name_idx) => {
5727                        // `$sum += $h{$k}` — single-dispatch slot += hash[name-scalar] with no
5728                        // VM stack traffic. The key scalar is read via plain (name-based) access
5729                        // because the compiler's `for my $k (keys %h)` lowering currently backs
5730                        // `$k` with a frame scalar, not a slot.
5731                        let k_name = names[*k_name_idx as usize].as_str();
5732                        let h_name = names[*h_name_idx as usize].as_str();
5733                        self.interp.touch_env_hash(h_name);
5734                        let key = self.interp.scope.get_scalar(k_name).to_string();
5735                        let elem = self.interp.scope.get_hash_element(h_name, &key);
5736                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
5737                        let new_v =
5738                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
5739                                StrykeValue::integer(a.wrapping_add(b))
5740                            } else {
5741                                StrykeValue::float(cur.to_number() + elem.to_number())
5742                            };
5743                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
5744                        Ok(())
5745                    }
5746                    Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_name_idx) => {
5747                        // `$sum += $h{$k}` — slot counter, slot key, slot sum. Zero name lookups
5748                        // for `$sum` and `$k`; one frame-walk for `%h` (same as the non-slot form).
5749                        let h_name = names[*h_name_idx as usize].as_str();
5750                        self.interp.touch_env_hash(h_name);
5751                        let key_val = self.interp.scope.get_scalar_slot(*k_slot);
5752                        let key = key_val.to_string();
5753                        let elem = self.interp.scope.get_hash_element(h_name, &key);
5754                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
5755                        let new_v =
5756                            if let (Some(a), Some(b)) = (cur.as_integer(), elem.as_integer()) {
5757                                StrykeValue::integer(a.wrapping_add(b))
5758                            } else {
5759                                StrykeValue::float(cur.to_number() + elem.to_number())
5760                            };
5761                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
5762                        Ok(())
5763                    }
5764                    Op::SumHashValuesToSlot(sum_slot, h_name_idx) => {
5765                        // `for my $k (keys %h) { $sum += $h{$k} }` fused to a single op that walks
5766                        // `hash.values()` in a tight native loop. No key stringification, no stack
5767                        // traffic, no per-iter dispatch. The foreach body reduced to
5768                        // `AddHashElemSlotKeyToSlot`, so this fusion is correct regardless of `$k`
5769                        // slot assignment — we never read `$k`.
5770                        let h_name = names[*h_name_idx as usize].as_str();
5771                        self.interp.touch_env_hash(h_name);
5772                        let cur = self.interp.scope.get_scalar_slot(*sum_slot);
5773                        let mut int_acc: i64 = cur.as_integer().unwrap_or(0);
5774                        let mut float_acc: f64 = 0.0;
5775                        let mut is_int = cur.as_integer().is_some();
5776                        if !is_int {
5777                            float_acc = cur.to_number();
5778                        }
5779                        // Walk the hash via the scope's borrow path without cloning the whole
5780                        // IndexMap. `for_each_hash_value` takes a visitor so the lock (if any) is
5781                        // held once rather than per-element.
5782                        self.interp.scope.for_each_hash_value(h_name, |v| {
5783                            if is_int {
5784                                if let Some(x) = v.as_integer() {
5785                                    int_acc = int_acc.wrapping_add(x);
5786                                    return;
5787                                }
5788                                float_acc = int_acc as f64;
5789                                is_int = false;
5790                            }
5791                            float_acc += v.to_number();
5792                        });
5793                        let new_v = if is_int {
5794                            StrykeValue::integer(int_acc)
5795                        } else {
5796                            StrykeValue::float(float_acc)
5797                        };
5798                        self.interp.scope.set_scalar_slot(*sum_slot, new_v);
5799                        Ok(())
5800                    }
5801                    Op::SetHashIntTimesLoop(h_name_idx, i_slot, k, limit) => {
5802                        // Runs the counted `while $i < limit { $h{$i} = $i * k; $i += 1 }` loop
5803                        // natively: the hash is `reserve()`d once, keys are stringified via
5804                        // `itoa` (no `format!` allocation), and values are inserted in a tight
5805                        // Rust loop. `$i` is left at `limit` on exit, matching the un-fused shape.
5806                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5807                        let lim = *limit as i64;
5808                        if i_cur < lim {
5809                            let n = names[*h_name_idx as usize].as_str();
5810                            self.require_hash_mutable(n)?;
5811                            self.interp.touch_env_hash(n);
5812                            let line = self.line();
5813                            self.interp
5814                                .scope
5815                                .set_hash_int_times_range(n, i_cur, lim, *k as i64)
5816                                .map_err(|e| e.at_line(line))?;
5817                        }
5818                        self.interp
5819                            .scope
5820                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
5821                        Ok(())
5822                    }
5823                    Op::PushIntRangeToArrayLoop(arr_name_idx, i_slot, limit) => {
5824                        // Runs the entire counted `while $i < limit { push @arr, $i; $i += 1 }`
5825                        // loop in native Rust. The array's `Vec<StrykeValue>` is reserved once and
5826                        // `push(StrykeValue::integer(i))` runs in a tight Rust loop — no per-iter
5827                        // op dispatch, no `require_array_mutable` check per iter.
5828                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5829                        let lim = *limit as i64;
5830                        if i_cur < lim {
5831                            let n = names[*arr_name_idx as usize].as_str();
5832                            self.require_array_mutable(n)?;
5833                            let line = self.line();
5834                            self.interp
5835                                .scope
5836                                .push_int_range_to_array(n, i_cur, lim)
5837                                .map_err(|e| e.at_line(line))?;
5838                        }
5839                        self.interp
5840                            .scope
5841                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
5842                        Ok(())
5843                    }
5844                    Op::ConcatConstSlotLoop(const_idx, s_slot, i_slot, limit) => {
5845                        // Runs the entire counted `while $i < limit { $s .= CONST; $i += 1 }` loop
5846                        // in native Rust. We stringify the constant once, reserve `(limit-i_cur) *
5847                        // const.len()` up front so the owning `String` reallocs at most twice, then
5848                        // `push_str` in a tight loop (see `try_concat_repeat_inplace`). Falls back
5849                        // to the per-iteration slow path when the slot is not the sole owner of a
5850                        // heap `String` — `.=` semantics match the un-fused shape byte-for-byte.
5851                        let i_cur = self.interp.scope.get_scalar_slot(*i_slot).to_int();
5852                        let lim = *limit as i64;
5853                        if i_cur < lim {
5854                            let n_iters = (lim - i_cur) as usize;
5855                            let rhs = constants[*const_idx as usize].as_str_or_empty();
5856                            if !self
5857                                .interp
5858                                .scope
5859                                .scalar_slot_concat_repeat_inplace(*s_slot, &rhs, n_iters)
5860                            {
5861                                self.interp
5862                                    .scope
5863                                    .scalar_slot_concat_repeat_slow(*s_slot, &rhs, n_iters);
5864                            }
5865                        }
5866                        self.interp
5867                            .scope
5868                            .set_scalar_slot(*i_slot, StrykeValue::integer(lim));
5869                        Ok(())
5870                    }
5871                    Op::AddAssignSlotSlot(dst, src) => {
5872                        let a = self.interp.scope.get_scalar_slot(*dst);
5873                        let b = self.interp.scope.get_scalar_slot(*src);
5874                        let result = crate::value::compat_add(&a, &b);
5875                        self.interp.scope.set_scalar_slot(*dst, result.clone());
5876                        self.push(result);
5877                        Ok(())
5878                    }
5879                    Op::AddAssignSlotSlotVoid(dst, src) => {
5880                        let a = self.interp.scope.get_scalar_slot(*dst);
5881                        let b = self.interp.scope.get_scalar_slot(*src);
5882                        let result = crate::value::compat_add(&a, &b);
5883                        self.interp.scope.set_scalar_slot(*dst, result);
5884                        Ok(())
5885                    }
5886                    Op::SubAssignSlotSlot(dst, src) => {
5887                        let a = self.interp.scope.get_scalar_slot(*dst);
5888                        let b = self.interp.scope.get_scalar_slot(*src);
5889                        let result = crate::value::compat_sub(&a, &b);
5890                        self.interp.scope.set_scalar_slot(*dst, result.clone());
5891                        self.push(result);
5892                        Ok(())
5893                    }
5894                    Op::MulAssignSlotSlot(dst, src) => {
5895                        let a = self.interp.scope.get_scalar_slot(*dst);
5896                        let b = self.interp.scope.get_scalar_slot(*src);
5897                        let result = crate::value::compat_mul(&a, &b);
5898                        self.interp.scope.set_scalar_slot(*dst, result.clone());
5899                        self.push(result);
5900                        Ok(())
5901                    }
5902
5903                    // ── Frame-local scalar slots (O(1), no string lookup) ──
5904                    Op::GetScalarSlot(slot) => {
5905                        let val = self.interp.scope.get_scalar_slot(*slot);
5906                        self.push(val);
5907                        Ok(())
5908                    }
5909                    Op::SetScalarSlot(slot) => {
5910                        let val = self.pop();
5911                        self.interp
5912                            .scope
5913                            .set_scalar_slot_checked(*slot, val, None)
5914                            .map_err(|e| e.at_line(self.line()))?;
5915                        Ok(())
5916                    }
5917                    Op::SetScalarSlotKeep(slot) => {
5918                        let val = self.peek().dup_stack();
5919                        self.interp
5920                            .scope
5921                            .set_scalar_slot_checked(*slot, val, None)
5922                            .map_err(|e| e.at_line(self.line()))?;
5923                        Ok(())
5924                    }
5925                    Op::DeclareScalarSlot(slot, name_idx) => {
5926                        let val = self.pop();
5927                        let name_opt = if *name_idx == u16::MAX {
5928                            None
5929                        } else {
5930                            Some(names[*name_idx as usize].as_str())
5931                        };
5932                        self.interp.scope.declare_scalar_slot(*slot, val, name_opt);
5933                        Ok(())
5934                    }
5935                    Op::GetArg(idx) => {
5936                        // Read argument from caller's stack region without @_ allocation.
5937                        let val = if let Some(frame) = self.call_stack.last() {
5938                            let arg_pos = frame.stack_base + *idx as usize;
5939                            self.stack
5940                                .get(arg_pos)
5941                                .cloned()
5942                                .unwrap_or(StrykeValue::UNDEF)
5943                        } else {
5944                            StrykeValue::UNDEF
5945                        };
5946                        self.push(val);
5947                        Ok(())
5948                    }
5949
5950                    Op::ReadIntoVar(name_idx) => {
5951                        let length = self.pop().to_int() as usize;
5952                        let fh_val = self.pop();
5953                        let name = &names[*name_idx as usize];
5954                        let line = self.line();
5955                        let result = vm_interp_result(
5956                            self.interp.builtin_read_into(fh_val, name, length, line),
5957                            line,
5958                        )?;
5959                        self.push(result);
5960                        Ok(())
5961                    }
5962                    Op::ChompInPlace(lvalue_idx) => {
5963                        let val = self.pop();
5964                        let target = &self.lvalues[*lvalue_idx as usize];
5965                        let line = self.line();
5966                        match self.interp.chomp_inplace_execute(val, target) {
5967                            Ok(v) => self.push(v),
5968                            Err(FlowOrError::Error(e)) => return Err(e),
5969                            Err(FlowOrError::Flow(_)) => {
5970                                return Err(PerlError::runtime("unexpected flow in chomp", line));
5971                            }
5972                        }
5973                        Ok(())
5974                    }
5975                    Op::ChopInPlace(lvalue_idx) => {
5976                        let val = self.pop();
5977                        let target = &self.lvalues[*lvalue_idx as usize];
5978                        let line = self.line();
5979                        match self.interp.chop_inplace_execute(val, target) {
5980                            Ok(v) => self.push(v),
5981                            Err(FlowOrError::Error(e)) => return Err(e),
5982                            Err(FlowOrError::Flow(_)) => {
5983                                return Err(PerlError::runtime("unexpected flow in chop", line));
5984                            }
5985                        }
5986                        Ok(())
5987                    }
5988                    Op::SubstrFourArg(idx) => {
5989                        let (string_e, offset_e, length_e, rep_e) =
5990                            &self.substr_four_arg_entries[*idx as usize];
5991                        let v = vm_interp_result(
5992                            self.interp.eval_substr_expr(
5993                                string_e,
5994                                offset_e,
5995                                length_e.as_ref(),
5996                                Some(rep_e),
5997                                self.line(),
5998                            ),
5999                            self.line(),
6000                        )?;
6001                        self.push(v);
6002                        Ok(())
6003                    }
6004                    Op::KeysExpr(idx) => {
6005                        let i = *idx as usize;
6006                        let line = self.line();
6007                        let v = if let Some(&(start, end)) = self
6008                            .keys_expr_bytecode_ranges
6009                            .get(i)
6010                            .and_then(|r| r.as_ref())
6011                        {
6012                            let val = self.run_block_region(start, end, op_count)?;
6013                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
6014                        } else {
6015                            let e = &self.keys_expr_entries[i];
6016                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
6017                        };
6018                        self.push(v);
6019                        Ok(())
6020                    }
6021                    Op::KeysExprScalar(idx) => {
6022                        let i = *idx as usize;
6023                        let line = self.line();
6024                        let v = if let Some(&(start, end)) = self
6025                            .keys_expr_bytecode_ranges
6026                            .get(i)
6027                            .and_then(|r| r.as_ref())
6028                        {
6029                            let val = self.run_block_region(start, end, op_count)?;
6030                            vm_interp_result(VMHelper::keys_from_value(val, line), line)?
6031                        } else {
6032                            let e = &self.keys_expr_entries[i];
6033                            vm_interp_result(self.interp.eval_keys_expr(e, line), line)?
6034                        };
6035                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
6036                        self.push(StrykeValue::integer(n));
6037                        Ok(())
6038                    }
6039                    Op::ValuesExpr(idx) => {
6040                        let i = *idx as usize;
6041                        let line = self.line();
6042                        let v = if let Some(&(start, end)) = self
6043                            .values_expr_bytecode_ranges
6044                            .get(i)
6045                            .and_then(|r| r.as_ref())
6046                        {
6047                            let val = self.run_block_region(start, end, op_count)?;
6048                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
6049                        } else {
6050                            let e = &self.values_expr_entries[i];
6051                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
6052                        };
6053                        self.push(v);
6054                        Ok(())
6055                    }
6056                    Op::ValuesExprScalar(idx) => {
6057                        let i = *idx as usize;
6058                        let line = self.line();
6059                        let v = if let Some(&(start, end)) = self
6060                            .values_expr_bytecode_ranges
6061                            .get(i)
6062                            .and_then(|r| r.as_ref())
6063                        {
6064                            let val = self.run_block_region(start, end, op_count)?;
6065                            vm_interp_result(VMHelper::values_from_value(val, line), line)?
6066                        } else {
6067                            let e = &self.values_expr_entries[i];
6068                            vm_interp_result(self.interp.eval_values_expr(e, line), line)?
6069                        };
6070                        let n = v.as_array_vec().map(|a| a.len()).unwrap_or(0) as i64;
6071                        self.push(StrykeValue::integer(n));
6072                        Ok(())
6073                    }
6074                    Op::DeleteExpr(idx) => {
6075                        let e = &self.delete_expr_entries[*idx as usize];
6076                        let v = vm_interp_result(
6077                            self.interp.eval_delete_operand(e, self.line()),
6078                            self.line(),
6079                        )?;
6080                        self.push(v);
6081                        Ok(())
6082                    }
6083                    Op::ExistsExpr(idx) => {
6084                        let e = &self.exists_expr_entries[*idx as usize];
6085                        let v = vm_interp_result(
6086                            self.interp.eval_exists_operand(e, self.line()),
6087                            self.line(),
6088                        )?;
6089                        self.push(v);
6090                        Ok(())
6091                    }
6092                    Op::PushExpr(idx) => {
6093                        let (array, values) = &self.push_expr_entries[*idx as usize];
6094                        let v = vm_interp_result(
6095                            self.interp
6096                                .eval_push_expr(array, values.as_slice(), self.line()),
6097                            self.line(),
6098                        )?;
6099                        self.push(v);
6100                        Ok(())
6101                    }
6102                    Op::PopExpr(idx) => {
6103                        let e = &self.pop_expr_entries[*idx as usize];
6104                        let v = vm_interp_result(
6105                            self.interp.eval_pop_expr(e, self.line()),
6106                            self.line(),
6107                        )?;
6108                        self.push(v);
6109                        Ok(())
6110                    }
6111                    Op::ShiftExpr(idx) => {
6112                        let e = &self.shift_expr_entries[*idx as usize];
6113                        let v = vm_interp_result(
6114                            self.interp.eval_shift_expr(e, self.line()),
6115                            self.line(),
6116                        )?;
6117                        self.push(v);
6118                        Ok(())
6119                    }
6120                    Op::UnshiftExpr(idx) => {
6121                        let (array, values) = &self.unshift_expr_entries[*idx as usize];
6122                        let v = vm_interp_result(
6123                            self.interp
6124                                .eval_unshift_expr(array, values.as_slice(), self.line()),
6125                            self.line(),
6126                        )?;
6127                        self.push(v);
6128                        Ok(())
6129                    }
6130                    Op::SpliceExpr(idx) => {
6131                        let (array, offset, length, replacement) =
6132                            &self.splice_expr_entries[*idx as usize];
6133                        let v = vm_interp_result(
6134                            self.interp.eval_splice_expr(
6135                                array,
6136                                offset.as_ref(),
6137                                length.as_ref(),
6138                                replacement.as_slice(),
6139                                self.interp.wantarray_kind,
6140                                self.line(),
6141                            ),
6142                            self.line(),
6143                        )?;
6144                        self.push(v);
6145                        Ok(())
6146                    }
6147
6148                    // ── References ──
6149                    Op::MakeScalarRef => {
6150                        let val = self.pop();
6151                        self.push(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))));
6152                        Ok(())
6153                    }
6154                    Op::MakeScalarBindingRef(name_idx) => {
6155                        let name = names[*name_idx as usize].clone();
6156                        self.push(StrykeValue::scalar_binding_ref(name));
6157                        Ok(())
6158                    }
6159                    Op::MakeArrayBindingRef(name_idx) => {
6160                        let name = &names[*name_idx as usize];
6161                        // Promote the scope's array to shared Arc-backed storage.
6162                        // Both the scope and the returned ref share the same Arc,
6163                        // so mutations through either path are visible.
6164                        let arc = self.interp.scope.promote_array_to_shared(name);
6165                        self.push(StrykeValue::array_ref(arc));
6166                        Ok(())
6167                    }
6168                    Op::MakeHashBindingRef(name_idx) => {
6169                        let name = &names[*name_idx as usize];
6170                        // Lazy-init hook: `\%all` / `\%parameters` / `\%main::`
6171                        // bypass `Op::GetHash`, so without this call the
6172                        // reference is taken before the hash is populated and
6173                        // the user gets an empty hashref.
6174                        self.interp.touch_env_hash(name);
6175                        let arc = self.interp.scope.promote_hash_to_shared(name);
6176                        self.push(StrykeValue::hash_ref(arc));
6177                        Ok(())
6178                    }
6179                    Op::MakeArrayRefAlias => {
6180                        let v = self.pop();
6181                        let line = self.line();
6182                        let out =
6183                            vm_interp_result(self.interp.make_array_ref_alias(v, line), line)?;
6184                        self.push(out);
6185                        Ok(())
6186                    }
6187                    Op::MakeHashRefAlias => {
6188                        let v = self.pop();
6189                        let line = self.line();
6190                        let out = vm_interp_result(self.interp.make_hash_ref_alias(v, line), line)?;
6191                        self.push(out);
6192                        Ok(())
6193                    }
6194                    Op::MakeArrayRef => {
6195                        let val = self.pop();
6196                        let val = self.interp.scope.resolve_container_binding_ref(val);
6197                        let arr = if let Some(a) = val.as_array_vec() {
6198                            a
6199                        } else {
6200                            vec![val]
6201                        };
6202                        self.push(StrykeValue::array_ref(Arc::new(RwLock::new(arr))));
6203                        Ok(())
6204                    }
6205                    Op::MakeHashRef => {
6206                        let val = self.pop();
6207                        let map = if let Some(h) = val.as_hash_map() {
6208                            h
6209                        } else {
6210                            let items = val.to_list();
6211                            let mut m = IndexMap::new();
6212                            let mut i = 0;
6213                            while i + 1 < items.len() {
6214                                m.insert(items[i].to_string(), items[i + 1].clone());
6215                                i += 2;
6216                            }
6217                            m
6218                        };
6219                        self.push(StrykeValue::hash_ref(Arc::new(RwLock::new(map))));
6220                        Ok(())
6221                    }
6222                    Op::MakeCodeRef(block_idx, sig_idx) => {
6223                        let block = self.blocks[*block_idx as usize].clone();
6224                        let params = self.code_ref_sigs[*sig_idx as usize].clone();
6225                        let captured = self.interp.scope.capture();
6226                        self.push(StrykeValue::code_ref(Arc::new(crate::value::PerlSub {
6227                            name: "__ANON__".to_string(),
6228                            params,
6229                            body: block,
6230                            closure_env: Some(captured),
6231                            prototype: None,
6232                            fib_like: None,
6233                        })));
6234                        Ok(())
6235                    }
6236                    Op::LoadNamedSubRef(name_idx) => {
6237                        let name = names[*name_idx as usize].as_str();
6238                        let line = self.line();
6239                        let sub = self.interp.resolve_sub_by_name(name).ok_or_else(|| {
6240                            PerlError::runtime(
6241                                self.interp.undefined_subroutine_resolve_message(name),
6242                                line,
6243                            )
6244                        })?;
6245                        self.push(StrykeValue::code_ref(sub));
6246                        Ok(())
6247                    }
6248                    Op::LoadDynamicSubRef => {
6249                        let name = self.pop().to_string();
6250                        let line = self.line();
6251                        let sub = self.interp.resolve_sub_by_name(&name).ok_or_else(|| {
6252                            PerlError::runtime(
6253                                self.interp.undefined_subroutine_resolve_message(&name),
6254                                line,
6255                            )
6256                        })?;
6257                        self.push(StrykeValue::code_ref(sub));
6258                        Ok(())
6259                    }
6260                    Op::LoadDynamicTypeglob => {
6261                        let name = self.pop().to_string();
6262                        let n = self.interp.resolve_io_handle_name(&name);
6263                        self.push(StrykeValue::string(n));
6264                        Ok(())
6265                    }
6266                    Op::CopyTypeglobSlots(lhs_i, rhs_i) => {
6267                        let lhs = self.names[*lhs_i as usize].as_str();
6268                        let rhs = self.names[*rhs_i as usize].as_str();
6269                        let line = self.line();
6270                        self.interp
6271                            .copy_typeglob_slots(lhs, rhs, line)
6272                            .map_err(|e| e.at_line(line))?;
6273                        Ok(())
6274                    }
6275                    Op::TypeglobAssignFromValue(name_idx) => {
6276                        let val = self.pop();
6277                        let name = self.names[*name_idx as usize].as_str();
6278                        let line = self.line();
6279                        vm_interp_result(
6280                            self.interp.assign_typeglob_value(name, val.clone(), line),
6281                            line,
6282                        )?;
6283                        self.push(val);
6284                        Ok(())
6285                    }
6286                    Op::TypeglobAssignFromValueDynamic => {
6287                        let val = self.pop();
6288                        let name = self.pop().to_string();
6289                        let line = self.line();
6290                        vm_interp_result(
6291                            self.interp.assign_typeglob_value(&name, val.clone(), line),
6292                            line,
6293                        )?;
6294                        self.push(val);
6295                        Ok(())
6296                    }
6297                    Op::CopyTypeglobSlotsDynamicLhs(rhs_i) => {
6298                        let lhs = self.pop().to_string();
6299                        let rhs = self.names[*rhs_i as usize].as_str();
6300                        let line = self.line();
6301                        self.interp
6302                            .copy_typeglob_slots(&lhs, rhs, line)
6303                            .map_err(|e| e.at_line(line))?;
6304                        Ok(())
6305                    }
6306                    Op::SymbolicDeref(kind_byte) => {
6307                        let v = self.pop();
6308                        let kind = match *kind_byte {
6309                            0 => Sigil::Scalar,
6310                            1 => Sigil::Array,
6311                            2 => Sigil::Hash,
6312                            3 => Sigil::Typeglob,
6313                            _ => {
6314                                return Err(PerlError::runtime(
6315                                    "VM: bad SymbolicDeref kind byte",
6316                                    self.line(),
6317                                ));
6318                            }
6319                        };
6320                        let line = self.line();
6321                        let out =
6322                            vm_interp_result(self.interp.symbolic_deref(v, kind, line), line)?;
6323                        self.push(out);
6324                        Ok(())
6325                    }
6326
6327                    // ── Arrow dereference ──
6328                    Op::ArrowArray => {
6329                        let idx = self.pop().to_int();
6330                        let r = self.pop();
6331                        let line = self.line();
6332                        let v = vm_interp_result(
6333                            self.interp.read_arrow_array_element(r, idx, line),
6334                            line,
6335                        )?;
6336                        self.push(v);
6337                        Ok(())
6338                    }
6339                    Op::ArrowHash => {
6340                        let key = self.pop().to_string();
6341                        let r = self.pop();
6342                        let line = self.line();
6343                        let v = vm_interp_result(
6344                            self.interp.read_arrow_hash_element(r, key.as_str(), line),
6345                            line,
6346                        )?;
6347                        self.push(v);
6348                        Ok(())
6349                    }
6350                    Op::SetArrowHash => {
6351                        let key = self.pop().to_string();
6352                        let r = self.pop();
6353                        let val = self.pop();
6354                        let line = self.line();
6355                        vm_interp_result(
6356                            self.interp.assign_arrow_hash_deref(r, key, val, line),
6357                            line,
6358                        )?;
6359                        Ok(())
6360                    }
6361                    Op::SetArrowArray => {
6362                        let idx = self.pop().to_int();
6363                        let r = self.pop();
6364                        let val = self.pop();
6365                        let line = self.line();
6366                        vm_interp_result(
6367                            self.interp.assign_arrow_array_deref(r, idx, val, line),
6368                            line,
6369                        )?;
6370                        Ok(())
6371                    }
6372                    Op::SetArrowArrayKeep => {
6373                        let idx = self.pop().to_int();
6374                        let r = self.pop();
6375                        let val = self.pop();
6376                        let val_keep = val.clone();
6377                        let line = self.line();
6378                        vm_interp_result(
6379                            self.interp.assign_arrow_array_deref(r, idx, val, line),
6380                            line,
6381                        )?;
6382                        self.push(val_keep);
6383                        Ok(())
6384                    }
6385                    Op::SetArrowHashKeep => {
6386                        let key = self.pop().to_string();
6387                        let r = self.pop();
6388                        let val = self.pop();
6389                        let val_keep = val.clone();
6390                        let line = self.line();
6391                        vm_interp_result(
6392                            self.interp.assign_arrow_hash_deref(r, key, val, line),
6393                            line,
6394                        )?;
6395                        self.push(val_keep);
6396                        Ok(())
6397                    }
6398                    Op::ArrowArrayPostfix(b) => {
6399                        let idx = self.pop().to_int();
6400                        let r = self.pop();
6401                        let line = self.line();
6402                        let old = vm_interp_result(
6403                            self.interp.arrow_array_postfix(r, idx, *b == 1, line),
6404                            line,
6405                        )?;
6406                        self.push(old);
6407                        Ok(())
6408                    }
6409                    Op::ArrowHashPostfix(b) => {
6410                        let key = self.pop().to_string();
6411                        let r = self.pop();
6412                        let line = self.line();
6413                        let old = vm_interp_result(
6414                            self.interp.arrow_hash_postfix(r, key, *b == 1, line),
6415                            line,
6416                        )?;
6417                        self.push(old);
6418                        Ok(())
6419                    }
6420                    Op::SetSymbolicScalarRef => {
6421                        let r = self.pop();
6422                        let val = self.pop();
6423                        let line = self.line();
6424                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
6425                        Ok(())
6426                    }
6427                    Op::SetSymbolicScalarRefKeep => {
6428                        let r = self.pop();
6429                        let val = self.pop();
6430                        let val_keep = val.clone();
6431                        let line = self.line();
6432                        vm_interp_result(self.interp.assign_scalar_ref_deref(r, val, line), line)?;
6433                        self.push(val_keep);
6434                        Ok(())
6435                    }
6436                    Op::SetSymbolicArrayRef => {
6437                        let r = self.pop();
6438                        let val = self.pop();
6439                        let line = self.line();
6440                        vm_interp_result(
6441                            self.interp.assign_symbolic_array_ref_deref(r, val, line),
6442                            line,
6443                        )?;
6444                        Ok(())
6445                    }
6446                    Op::SetSymbolicHashRef => {
6447                        let r = self.pop();
6448                        let val = self.pop();
6449                        let line = self.line();
6450                        vm_interp_result(
6451                            self.interp.assign_symbolic_hash_ref_deref(r, val, line),
6452                            line,
6453                        )?;
6454                        Ok(())
6455                    }
6456                    Op::SetSymbolicTypeglobRef => {
6457                        let r = self.pop();
6458                        let val = self.pop();
6459                        let line = self.line();
6460                        vm_interp_result(
6461                            self.interp.assign_symbolic_typeglob_ref_deref(r, val, line),
6462                            line,
6463                        )?;
6464                        Ok(())
6465                    }
6466                    Op::SymbolicScalarRefPostfix(b) => {
6467                        let r = self.pop();
6468                        let line = self.line();
6469                        let old = vm_interp_result(
6470                            self.interp.symbolic_scalar_ref_postfix(r, *b == 1, line),
6471                            line,
6472                        )?;
6473                        self.push(old);
6474                        Ok(())
6475                    }
6476                    Op::ArrowCall(wa) => {
6477                        let want = WantarrayCtx::from_byte(*wa);
6478                        let args_val = self.pop();
6479                        let r = self.pop();
6480                        // Auto-deref ScalarRef so closures that captured $f can call $f->()
6481                        let r = if let Some(inner) = r.as_scalar_ref() {
6482                            inner.read().clone()
6483                        } else {
6484                            r
6485                        };
6486                        let args = args_val.to_list();
6487                        if let Some(sub) = r.as_code_ref() {
6488                            // Higher-order function wrappers (comp, partial, memoize, etc.)
6489                            // have empty bodies + magic closure_env keys. Dispatch them via
6490                            // the interpreter's try_hof_dispatch before falling through to
6491                            // the normal body execution path.
6492                            if let Some(hof_result) =
6493                                self.interp.try_hof_dispatch(&sub, &args, want, self.line())
6494                            {
6495                                let v = vm_interp_result(hof_result, self.line())?;
6496                                self.push(v);
6497                                return Ok(());
6498                            }
6499                            self.interp.current_sub_stack.push(sub.clone());
6500                            let saved_wa = self.interp.wantarray_kind;
6501                            self.interp.wantarray_kind = want;
6502                            self.interp.scope_push_hook();
6503                            self.interp.scope.declare_array("_", args.clone());
6504                            if let Some(ref env) = sub.closure_env {
6505                                self.interp.scope.restore_capture(env);
6506                            }
6507                            let line = self.line();
6508                            let argv = self.interp.scope.take_sub_underscore().unwrap_or_default();
6509                            self.interp
6510                                .apply_sub_signature(sub.as_ref(), &argv, line)
6511                                .map_err(|e| e.at_line(line))?;
6512                            self.interp.scope.declare_array("_", argv.clone());
6513                            // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
6514                            self.interp.scope.set_closure_args(&argv);
6515                            let result = self.interp.exec_block_no_scope(&sub.body);
6516                            self.interp.wantarray_kind = saved_wa;
6517                            self.interp.scope_pop_hook();
6518                            self.interp.current_sub_stack.pop();
6519                            match result {
6520                                Ok(v) => self.push(v),
6521                                Err(crate::vm_helper::FlowOrError::Flow(
6522                                    crate::vm_helper::Flow::Return(v),
6523                                )) => self.push(v),
6524                                Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
6525                                Err(_) => self.push(StrykeValue::UNDEF),
6526                            }
6527                        } else {
6528                            return Err(PerlError::runtime("Not a code reference", self.line()));
6529                        }
6530                        Ok(())
6531                    }
6532                    Op::IndirectCall(argc, wa, pass_flag) => {
6533                        let want = WantarrayCtx::from_byte(*wa);
6534                        let line = self.line();
6535                        let arg_vals = if *pass_flag != 0 {
6536                            self.interp.scope.get_array("_")
6537                        } else {
6538                            let n = *argc as usize;
6539                            let mut args = Vec::with_capacity(n);
6540                            for _ in 0..n {
6541                                args.push(self.pop());
6542                            }
6543                            args.reverse();
6544                            args
6545                        };
6546                        let target = self.pop();
6547                        // HOF wrapper fast path (comp, partial, memoize, etc.)
6548                        if let Some(sub) = target.as_code_ref() {
6549                            if let Some(hof_result) =
6550                                self.interp.try_hof_dispatch(&sub, &arg_vals, want, line)
6551                            {
6552                                let v = vm_interp_result(hof_result, line)?;
6553                                self.push(v);
6554                                return Ok(());
6555                            }
6556                        }
6557                        let r = self
6558                            .interp
6559                            .dispatch_indirect_call(target, arg_vals, want, line);
6560                        let v = vm_interp_result(r, line)?;
6561                        self.push(v);
6562                        Ok(())
6563                    }
6564
6565                    // ── Method call ──
6566                    Op::MethodCall(name_idx, argc, wa) => {
6567                        self.run_method_op(*name_idx, *argc, *wa, false)?;
6568                        Ok(())
6569                    }
6570                    Op::MethodCallSuper(name_idx, argc, wa) => {
6571                        self.run_method_op(*name_idx, *argc, *wa, true)?;
6572                        Ok(())
6573                    }
6574
6575                    // ── File test ──
6576                    Op::FileTestOp(test) => {
6577                        let path = self.pop().to_string();
6578                        let op = *test as char;
6579                        // -M, -A, -C return fractional days (float)
6580                        if matches!(op, 'M' | 'A' | 'C') {
6581                            #[cfg(unix)]
6582                            {
6583                                let v = match crate::perl_fs::filetest_age_days(&path, op) {
6584                                    Some(days) => StrykeValue::float(days),
6585                                    None => StrykeValue::UNDEF,
6586                                };
6587                                self.push(v);
6588                                return Ok(());
6589                            }
6590                            #[cfg(not(unix))]
6591                            {
6592                                self.push(StrykeValue::UNDEF);
6593                                return Ok(());
6594                            }
6595                        }
6596                        // -s returns file size (integer)
6597                        if op == 's' {
6598                            let v = match std::fs::metadata(&path) {
6599                                Ok(m) => StrykeValue::integer(m.len() as i64),
6600                                Err(_) => StrykeValue::UNDEF,
6601                            };
6602                            self.push(v);
6603                            return Ok(());
6604                        }
6605                        let result = match op {
6606                            'e' => std::path::Path::new(&path).exists(),
6607                            'f' => std::path::Path::new(&path).is_file(),
6608                            'd' => std::path::Path::new(&path).is_dir(),
6609                            'l' => std::path::Path::new(&path).is_symlink(),
6610                            #[cfg(unix)]
6611                            'r' => crate::perl_fs::filetest_effective_access(&path, 4),
6612                            #[cfg(not(unix))]
6613                            'r' => std::fs::metadata(&path).is_ok(),
6614                            #[cfg(unix)]
6615                            'w' => crate::perl_fs::filetest_effective_access(&path, 2),
6616                            #[cfg(not(unix))]
6617                            'w' => std::fs::metadata(&path).is_ok(),
6618                            #[cfg(unix)]
6619                            'x' => crate::perl_fs::filetest_effective_access(&path, 1),
6620                            #[cfg(not(unix))]
6621                            'x' => false,
6622                            #[cfg(unix)]
6623                            'o' => crate::perl_fs::filetest_owned_effective(&path),
6624                            #[cfg(not(unix))]
6625                            'o' => false,
6626                            #[cfg(unix)]
6627                            'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
6628                            #[cfg(not(unix))]
6629                            'R' => false,
6630                            #[cfg(unix)]
6631                            'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
6632                            #[cfg(not(unix))]
6633                            'W' => false,
6634                            #[cfg(unix)]
6635                            'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
6636                            #[cfg(not(unix))]
6637                            'X' => false,
6638                            #[cfg(unix)]
6639                            'O' => crate::perl_fs::filetest_owned_real(&path),
6640                            #[cfg(not(unix))]
6641                            'O' => false,
6642                            'z' => std::fs::metadata(&path)
6643                                .map(|m| m.len() == 0)
6644                                .unwrap_or(true),
6645                            't' => crate::perl_fs::filetest_is_tty(&path),
6646                            #[cfg(unix)]
6647                            'p' => crate::perl_fs::filetest_is_pipe(&path),
6648                            #[cfg(not(unix))]
6649                            'p' => false,
6650                            #[cfg(unix)]
6651                            'S' => crate::perl_fs::filetest_is_socket(&path),
6652                            #[cfg(not(unix))]
6653                            'S' => false,
6654                            #[cfg(unix)]
6655                            'b' => crate::perl_fs::filetest_is_block_device(&path),
6656                            #[cfg(not(unix))]
6657                            'b' => false,
6658                            #[cfg(unix)]
6659                            'c' => crate::perl_fs::filetest_is_char_device(&path),
6660                            #[cfg(not(unix))]
6661                            'c' => false,
6662                            #[cfg(unix)]
6663                            'u' => crate::perl_fs::filetest_is_setuid(&path),
6664                            #[cfg(not(unix))]
6665                            'u' => false,
6666                            #[cfg(unix)]
6667                            'g' => crate::perl_fs::filetest_is_setgid(&path),
6668                            #[cfg(not(unix))]
6669                            'g' => false,
6670                            #[cfg(unix)]
6671                            'k' => crate::perl_fs::filetest_is_sticky(&path),
6672                            #[cfg(not(unix))]
6673                            'k' => false,
6674                            'T' => crate::perl_fs::filetest_is_text(&path),
6675                            'B' => crate::perl_fs::filetest_is_binary(&path),
6676                            _ => false,
6677                        };
6678                        self.push(StrykeValue::integer(if result { 1 } else { 0 }));
6679                        Ok(())
6680                    }
6681
6682                    // ── Map/Grep/Sort with blocks (opcodes when lowered; else AST block fallback) ──
6683                    Op::MapIntMul(k) => {
6684                        let list = self.pop().to_list();
6685                        if list.len() == 1 {
6686                            if let Some(p) = list[0].as_pipeline() {
6687                                let line = self.line();
6688                                let sub = VMHelper::pipeline_int_mul_sub(*k);
6689                                self.interp.pipeline_push(&p, PipelineOp::Map(sub), line)?;
6690                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
6691                                return Ok(());
6692                            }
6693                        }
6694                        let mut result = Vec::with_capacity(list.len());
6695                        for item in list {
6696                            let n = item.to_int();
6697                            result.push(StrykeValue::integer(n.wrapping_mul(*k)));
6698                        }
6699                        self.push(StrykeValue::array(result));
6700                        Ok(())
6701                    }
6702                    Op::GrepIntModEq(m, r) => {
6703                        let list = self.pop().to_list();
6704                        let mut result = Vec::new();
6705                        for item in list {
6706                            let n = item.to_int();
6707                            if n % m == *r {
6708                                result.push(item);
6709                            }
6710                        }
6711                        self.push(StrykeValue::array(result));
6712                        Ok(())
6713                    }
6714                    Op::MapWithBlock(block_idx) => {
6715                        let list = self.pop().to_list();
6716                        self.map_with_block_common(list, *block_idx, false, op_count)
6717                    }
6718                    Op::FlatMapWithBlock(block_idx) => {
6719                        let list = self.pop().to_list();
6720                        self.map_with_block_common(list, *block_idx, true, op_count)
6721                    }
6722                    Op::MapWithExpr(expr_idx) => {
6723                        let list = self.pop().to_list();
6724                        self.map_with_expr_common(list, *expr_idx, false, op_count)
6725                    }
6726                    Op::FlatMapWithExpr(expr_idx) => {
6727                        let list = self.pop().to_list();
6728                        self.map_with_expr_common(list, *expr_idx, true, op_count)
6729                    }
6730                    Op::MapsWithBlock(block_idx) => {
6731                        let val = self.pop();
6732                        let block = self.blocks[*block_idx as usize].clone();
6733                        let out =
6734                            self.interp
6735                                .map_stream_block_output(val, &block, false, self.line())?;
6736                        self.push(out);
6737                        Ok(())
6738                    }
6739                    Op::MapsFlatMapWithBlock(block_idx) => {
6740                        let val = self.pop();
6741                        let block = self.blocks[*block_idx as usize].clone();
6742                        let out =
6743                            self.interp
6744                                .map_stream_block_output(val, &block, true, self.line())?;
6745                        self.push(out);
6746                        Ok(())
6747                    }
6748                    Op::MapsWithExpr(expr_idx) => {
6749                        let val = self.pop();
6750                        let idx = *expr_idx as usize;
6751                        let expr = self.map_expr_entries[idx].clone();
6752                        let out =
6753                            self.interp
6754                                .map_stream_expr_output(val, &expr, false, self.line())?;
6755                        self.push(out);
6756                        Ok(())
6757                    }
6758                    Op::MapsFlatMapWithExpr(expr_idx) => {
6759                        let val = self.pop();
6760                        let idx = *expr_idx as usize;
6761                        let expr = self.map_expr_entries[idx].clone();
6762                        let out =
6763                            self.interp
6764                                .map_stream_expr_output(val, &expr, true, self.line())?;
6765                        self.push(out);
6766                        Ok(())
6767                    }
6768                    Op::FilterWithBlock(block_idx) => {
6769                        let val = self.pop();
6770                        let block = self.blocks[*block_idx as usize].clone();
6771                        let out =
6772                            self.interp
6773                                .filter_stream_block_output(val, &block, self.line())?;
6774                        self.push(out);
6775                        Ok(())
6776                    }
6777                    Op::FilterWithExpr(expr_idx) => {
6778                        let val = self.pop();
6779                        let idx = *expr_idx as usize;
6780                        let expr = self.grep_expr_entries[idx].clone();
6781                        let out = self
6782                            .interp
6783                            .filter_stream_expr_output(val, &expr, self.line())?;
6784                        self.push(out);
6785                        Ok(())
6786                    }
6787                    Op::ChunkByWithBlock(block_idx) => {
6788                        let list = self.pop().to_list();
6789                        self.chunk_by_with_block_common(list, *block_idx, op_count)
6790                    }
6791                    Op::ChunkByWithExpr(expr_idx) => {
6792                        let list = self.pop().to_list();
6793                        self.chunk_by_with_expr_common(list, *expr_idx, op_count)
6794                    }
6795                    Op::GrepWithBlock(block_idx) => {
6796                        let list = self.pop().to_list();
6797                        if list.len() == 1 {
6798                            if let Some(p) = list[0].as_pipeline() {
6799                                let idx = *block_idx as usize;
6800                                let sub = self.interp.anon_coderef_from_block(&self.blocks[idx]);
6801                                let line = self.line();
6802                                self.interp
6803                                    .pipeline_push(&p, PipelineOp::Filter(sub), line)?;
6804                                self.push(StrykeValue::pipeline(Arc::clone(&p)));
6805                                return Ok(());
6806                            }
6807                        }
6808                        let idx = *block_idx as usize;
6809                        if let Some(&(start, end)) =
6810                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
6811                        {
6812                            let mut result = Vec::new();
6813                            for item in list {
6814                                self.interp.scope.set_topic(item.clone());
6815                                let val = self.run_block_region(start, end, op_count)?;
6816                                // Bare regex → match against $_ (Perl: /pat/ in grep is $_ =~ /pat/)
6817                                let keep = if let Some(re) = val.as_regex() {
6818                                    re.is_match(&item.to_string())
6819                                } else {
6820                                    val.is_true()
6821                                };
6822                                if keep {
6823                                    result.push(item);
6824                                }
6825                            }
6826                            self.push(StrykeValue::array(result));
6827                            Ok(())
6828                        } else {
6829                            let block = self.blocks[idx].clone();
6830                            let mut result = Vec::new();
6831                            for item in list {
6832                                self.interp.scope.set_topic(item.clone());
6833                                match self.interp.exec_block(&block) {
6834                                    Ok(val) => {
6835                                        let keep = if let Some(re) = val.as_regex() {
6836                                            re.is_match(&item.to_string())
6837                                        } else {
6838                                            val.is_true()
6839                                        };
6840                                        if keep {
6841                                            result.push(item);
6842                                        }
6843                                    }
6844                                    Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
6845                                    Err(_) => {}
6846                                }
6847                            }
6848                            self.push(StrykeValue::array(result));
6849                            Ok(())
6850                        }
6851                    }
6852                    Op::ForEachWithBlock(block_idx) => {
6853                        let val = self.pop();
6854                        let idx = *block_idx as usize;
6855                        // Lazy iterator: consume one-at-a-time without materializing.
6856                        if val.is_iterator() {
6857                            let iter = val.into_iterator();
6858                            let mut count = 0i64;
6859                            if let Some(&(start, end)) =
6860                                self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
6861                            {
6862                                while let Some(item) = iter.next_item() {
6863                                    count += 1;
6864                                    self.interp.scope.set_topic(item);
6865                                    self.run_block_region(start, end, op_count)?;
6866                                }
6867                            } else {
6868                                let block = self.blocks[idx].clone();
6869                                while let Some(item) = iter.next_item() {
6870                                    count += 1;
6871                                    self.interp.scope.set_topic(item);
6872                                    match self.interp.exec_block(&block) {
6873                                        Ok(_) => {}
6874                                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
6875                                            return Err(e)
6876                                        }
6877                                        Err(_) => {}
6878                                    }
6879                                }
6880                            }
6881                            self.push(StrykeValue::integer(count));
6882                            return Ok(());
6883                        }
6884                        let list = val.to_list();
6885                        let count = list.len() as i64;
6886                        if let Some(&(start, end)) =
6887                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
6888                        {
6889                            for item in list {
6890                                self.interp.scope.set_topic(item);
6891                                self.run_block_region(start, end, op_count)?;
6892                            }
6893                        } else {
6894                            let block = self.blocks[idx].clone();
6895                            for item in list {
6896                                self.interp.scope.set_topic(item);
6897                                match self.interp.exec_block(&block) {
6898                                    Ok(_) => {}
6899                                    Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
6900                                    Err(_) => {}
6901                                }
6902                            }
6903                        }
6904                        self.push(StrykeValue::integer(count));
6905                        Ok(())
6906                    }
6907                    Op::GrepWithExpr(expr_idx) => {
6908                        let list = self.pop().to_list();
6909                        let idx = *expr_idx as usize;
6910                        let dispatch_coderef = !crate::compat_mode();
6911                        // EXPR-form: see `map_with_expr_common` — no `{}` block
6912                        // boundary, so use `set_topic_local` (no chain shift,
6913                        // no slot 1+ zero).
6914                        if let Some(&(start, end)) = self
6915                            .grep_expr_bytecode_ranges
6916                            .get(idx)
6917                            .and_then(|r| r.as_ref())
6918                        {
6919                            let mut result = Vec::new();
6920                            for item in list {
6921                                self.interp.scope.set_topic_local(item.clone());
6922                                let val = self.run_block_region(start, end, op_count)?;
6923                                let val = self.maybe_call_coderef_with_item(
6924                                    val,
6925                                    &item,
6926                                    dispatch_coderef,
6927                                )?;
6928                                let keep = if let Some(re) = val.as_regex() {
6929                                    re.is_match(&item.to_string())
6930                                } else {
6931                                    val.is_true()
6932                                };
6933                                if keep {
6934                                    result.push(item);
6935                                }
6936                            }
6937                            self.push(StrykeValue::array(result));
6938                            Ok(())
6939                        } else {
6940                            let e = self.grep_expr_entries[idx].clone();
6941                            let mut result = Vec::new();
6942                            for item in list {
6943                                self.interp.scope.set_topic_local(item.clone());
6944                                let val = vm_interp_result(self.interp.eval_expr(&e), self.line())?;
6945                                let val = self.maybe_call_coderef_with_item(
6946                                    val,
6947                                    &item,
6948                                    dispatch_coderef,
6949                                )?;
6950                                let keep = if let Some(re) = val.as_regex() {
6951                                    re.is_match(&item.to_string())
6952                                } else {
6953                                    val.is_true()
6954                                };
6955                                if keep {
6956                                    result.push(item);
6957                                }
6958                            }
6959                            self.push(StrykeValue::array(result));
6960                            Ok(())
6961                        }
6962                    }
6963                    Op::SortWithBlock(block_idx) => {
6964                        let mut items = self.pop().to_list();
6965                        let idx = *block_idx as usize;
6966                        // Save the topic chain before sort — set_sort_pair writes to $_
6967                        // which would corrupt _< for subsequent pipeline stages (grep, map).
6968                        let saved_topic = self.interp.scope.save_topic_chain();
6969                        if let Some(&(start, end)) =
6970                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
6971                        {
6972                            let mut sort_err: Option<PerlError> = None;
6973                            items.sort_by(|a, b| {
6974                                if sort_err.is_some() {
6975                                    return std::cmp::Ordering::Equal;
6976                                }
6977                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
6978                                match self.run_block_region(start, end, op_count) {
6979                                    Ok(v) => {
6980                                        let n = v.to_int();
6981                                        if n < 0 {
6982                                            std::cmp::Ordering::Less
6983                                        } else if n > 0 {
6984                                            std::cmp::Ordering::Greater
6985                                        } else {
6986                                            std::cmp::Ordering::Equal
6987                                        }
6988                                    }
6989                                    Err(e) => {
6990                                        sort_err = Some(e);
6991                                        std::cmp::Ordering::Equal
6992                                    }
6993                                }
6994                            });
6995                            self.interp.scope.restore_topic_chain(saved_topic);
6996                            if let Some(e) = sort_err {
6997                                return Err(e);
6998                            }
6999                            self.push(StrykeValue::array(items));
7000                            Ok(())
7001                        } else {
7002                            let block = self.blocks[idx].clone();
7003                            items.sort_by(|a, b| {
7004                                self.interp.scope.set_sort_pair(a.clone(), b.clone());
7005                                match self.interp.exec_block(&block) {
7006                                    Ok(v) => {
7007                                        let n = v.to_int();
7008                                        if n < 0 {
7009                                            std::cmp::Ordering::Less
7010                                        } else if n > 0 {
7011                                            std::cmp::Ordering::Greater
7012                                        } else {
7013                                            std::cmp::Ordering::Equal
7014                                        }
7015                                    }
7016                                    Err(_) => std::cmp::Ordering::Equal,
7017                                }
7018                            });
7019                            self.interp.scope.restore_topic_chain(saved_topic);
7020                            self.push(StrykeValue::array(items));
7021                            Ok(())
7022                        }
7023                    }
7024                    Op::SortWithBlockFast(tag) => {
7025                        let mut items = self.pop().to_list();
7026                        let mode = match *tag {
7027                            0 => SortBlockFast::Numeric,
7028                            1 => SortBlockFast::String,
7029                            2 => SortBlockFast::NumericRev,
7030                            3 => SortBlockFast::StringRev,
7031                            _ => SortBlockFast::Numeric,
7032                        };
7033                        items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
7034                        self.push(StrykeValue::array(items));
7035                        Ok(())
7036                    }
7037                    Op::SortNoBlock => {
7038                        let mut items = self.pop().to_list();
7039                        items.sort_by_key(|a| a.to_string());
7040                        self.push(StrykeValue::array(items));
7041                        Ok(())
7042                    }
7043                    Op::SortWithCodeComparator(wa) => {
7044                        let want = WantarrayCtx::from_byte(*wa);
7045                        let cmp_val = self.pop();
7046                        let mut items = self.pop().to_list();
7047                        let line = self.line();
7048                        let Some(sub) = cmp_val.as_code_ref() else {
7049                            return Err(PerlError::runtime(
7050                                "sort: comparator must be a code reference",
7051                                line,
7052                            ));
7053                        };
7054                        let interp = &mut self.interp;
7055                        items.sort_by(|a, b| {
7056                            // `set_sort_pair` keeps Perl-style `$a`/`$b` access;
7057                            // positional args let stryke lambdas read via @_.
7058                            interp.scope.set_sort_pair(a.clone(), b.clone());
7059                            match interp.call_sub(
7060                                sub.as_ref(),
7061                                vec![a.clone(), b.clone()],
7062                                want,
7063                                line,
7064                            ) {
7065                                Ok(v) => {
7066                                    let n = v.to_int();
7067                                    if n < 0 {
7068                                        std::cmp::Ordering::Less
7069                                    } else if n > 0 {
7070                                        std::cmp::Ordering::Greater
7071                                    } else {
7072                                        std::cmp::Ordering::Equal
7073                                    }
7074                                }
7075                                Err(_) => std::cmp::Ordering::Equal,
7076                            }
7077                        });
7078                        self.push(StrykeValue::array(items));
7079                        Ok(())
7080                    }
7081                    Op::ReverseListOp => {
7082                        let val = self.pop();
7083                        if val.is_iterator() {
7084                            self.push(StrykeValue::iterator(std::sync::Arc::new(
7085                                crate::value::RevIterator::new(val.into_iterator()),
7086                            )));
7087                        } else {
7088                            let mut items = val.to_list();
7089                            items.reverse();
7090                            self.push(StrykeValue::array(items));
7091                        }
7092                        Ok(())
7093                    }
7094                    Op::ReverseScalarOp => {
7095                        let val = self.pop();
7096                        let items = val.to_list();
7097                        let s: String = items.iter().map(|v| v.to_string()).collect();
7098                        self.push(StrykeValue::string(s.chars().rev().collect()));
7099                        Ok(())
7100                    }
7101                    Op::RevListOp => {
7102                        let val = self.pop();
7103                        if val.is_iterator() {
7104                            // Collect the iterator fully and reverse the list order.
7105                            // RevIterator does per-element char reversal, not list reversal.
7106                            let mut items = val.to_list();
7107                            items.reverse();
7108                            self.push(StrykeValue::array(items));
7109                        } else if let Some(s) = crate::value::set_payload(&val) {
7110                            let mut out = crate::value::PerlSet::new();
7111                            for (k, v) in s.iter().rev() {
7112                                out.insert(k.clone(), v.clone());
7113                            }
7114                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
7115                        } else if let Some(ar) = val.as_array_ref() {
7116                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
7117                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
7118                                parking_lot::RwLock::new(items),
7119                            )));
7120                        } else if let Some(hr) = val.as_hash_ref() {
7121                            let mut out: indexmap::IndexMap<String, StrykeValue> =
7122                                indexmap::IndexMap::new();
7123                            for (k, v) in hr.read().iter() {
7124                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
7125                            }
7126                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
7127                                parking_lot::RwLock::new(out),
7128                            )));
7129                        } else if let Some(hm) = val.as_hash_map() {
7130                            let mut out: indexmap::IndexMap<String, StrykeValue> =
7131                                indexmap::IndexMap::new();
7132                            for (k, v) in hm.iter() {
7133                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
7134                            }
7135                            self.push(StrykeValue::hash(out));
7136                        } else if val.as_array_vec().is_some() {
7137                            let mut items = val.to_list();
7138                            items.reverse();
7139                            self.push(StrykeValue::array(items));
7140                        } else {
7141                            let s = val.to_string();
7142                            self.push(StrykeValue::string(s.chars().rev().collect()));
7143                        }
7144                        Ok(())
7145                    }
7146                    Op::RevScalarOp => {
7147                        let val = self.pop();
7148                        if let Some(s) = crate::value::set_payload(&val) {
7149                            let mut out = crate::value::PerlSet::new();
7150                            for (k, v) in s.iter().rev() {
7151                                out.insert(k.clone(), v.clone());
7152                            }
7153                            self.push(StrykeValue::set(std::sync::Arc::new(out)));
7154                        } else if let Some(ar) = val.as_array_ref() {
7155                            let items: Vec<_> = ar.read().iter().rev().cloned().collect();
7156                            self.push(StrykeValue::array_ref(std::sync::Arc::new(
7157                                parking_lot::RwLock::new(items),
7158                            )));
7159                        } else if let Some(hr) = val.as_hash_ref() {
7160                            let mut out: indexmap::IndexMap<String, StrykeValue> =
7161                                indexmap::IndexMap::new();
7162                            for (k, v) in hr.read().iter() {
7163                                out.insert(v.to_string(), StrykeValue::string(k.clone()));
7164                            }
7165                            self.push(StrykeValue::hash_ref(std::sync::Arc::new(
7166                                parking_lot::RwLock::new(out),
7167                            )));
7168                        } else {
7169                            let items = val.to_list();
7170                            let s: String = items.iter().map(|v| v.to_string()).collect();
7171                            self.push(StrykeValue::string(s.chars().rev().collect()));
7172                        }
7173                        Ok(())
7174                    }
7175                    Op::StackArrayLen => {
7176                        let v = self.pop();
7177                        self.push(StrykeValue::integer(v.to_list().len() as i64));
7178                        Ok(())
7179                    }
7180                    Op::ListSliceToScalar => {
7181                        let v = self.pop();
7182                        let items = v.to_list();
7183                        self.push(items.last().cloned().unwrap_or(StrykeValue::UNDEF));
7184                        Ok(())
7185                    }
7186
7187                    // ── Eval block ──
7188                    Op::EvalBlock(block_idx, want) => {
7189                        let block = self.blocks[*block_idx as usize].clone();
7190                        let tail = crate::vm_helper::WantarrayCtx::from_byte(*want);
7191                        self.interp.eval_nesting += 1;
7192                        // Use exec_block (with scope frame) so local/my declarations
7193                        // inside the block are properly scoped.
7194                        match self.interp.exec_block_with_tail(&block, tail) {
7195                            Ok(v) => {
7196                                self.interp.clear_eval_error();
7197                                self.push(v);
7198                            }
7199                            Err(crate::vm_helper::FlowOrError::Error(e)) => {
7200                                self.interp.set_eval_error_from_perl_error(&e);
7201                                self.push(StrykeValue::UNDEF);
7202                            }
7203                            Err(_) => self.push(StrykeValue::UNDEF),
7204                        }
7205                        self.interp.eval_nesting -= 1;
7206                        Ok(())
7207                    }
7208                    Op::TraceBlock(block_idx) => {
7209                        let block = self.blocks[*block_idx as usize].clone();
7210                        crate::parallel_trace::trace_enter();
7211                        self.interp.eval_nesting += 1;
7212                        match self.interp.exec_block(&block) {
7213                            Ok(v) => {
7214                                self.interp.clear_eval_error();
7215                                self.push(v);
7216                            }
7217                            Err(FlowOrError::Error(e)) => {
7218                                self.interp.set_eval_error_from_perl_error(&e);
7219                                self.push(StrykeValue::UNDEF);
7220                            }
7221                            Err(_) => self.push(StrykeValue::UNDEF),
7222                        }
7223                        self.interp.eval_nesting -= 1;
7224                        crate::parallel_trace::trace_leave();
7225                        Ok(())
7226                    }
7227                    Op::TimerBlock(block_idx) => {
7228                        let block = self.blocks[*block_idx as usize].clone();
7229                        let start = std::time::Instant::now();
7230                        self.interp.eval_nesting += 1;
7231                        let _ = match self.interp.exec_block(&block) {
7232                            Ok(v) => {
7233                                self.interp.clear_eval_error();
7234                                v
7235                            }
7236                            Err(FlowOrError::Error(e)) => {
7237                                self.interp.set_eval_error_from_perl_error(&e);
7238                                StrykeValue::UNDEF
7239                            }
7240                            Err(_) => StrykeValue::UNDEF,
7241                        };
7242                        self.interp.eval_nesting -= 1;
7243                        let ms = start.elapsed().as_secs_f64() * 1000.0;
7244                        self.push(StrykeValue::float(ms));
7245                        Ok(())
7246                    }
7247                    Op::BenchBlock(block_idx) => {
7248                        let n_i = self.pop().to_int();
7249                        if n_i < 0 {
7250                            return Err(PerlError::runtime(
7251                                "bench: iteration count must be non-negative",
7252                                self.line(),
7253                            ));
7254                        }
7255                        let n = n_i as usize;
7256                        let block = self.blocks[*block_idx as usize].clone();
7257                        let v = vm_interp_result(
7258                            self.interp.run_bench_block(&block, n, self.line()),
7259                            self.line(),
7260                        )?;
7261                        self.push(v);
7262                        Ok(())
7263                    }
7264                    Op::Given(idx) => {
7265                        let i = *idx as usize;
7266                        let line = self.line();
7267                        let v = if let Some(&(start, end)) = self
7268                            .given_topic_bytecode_ranges
7269                            .get(i)
7270                            .and_then(|r| r.as_ref())
7271                        {
7272                            let topic_val = self.run_block_region(start, end, op_count)?;
7273                            let body = &self.given_entries[i].1;
7274                            vm_interp_result(
7275                                self.interp.exec_given_with_topic_value(topic_val, body),
7276                                line,
7277                            )?
7278                        } else {
7279                            let (topic, body) = &self.given_entries[i];
7280                            vm_interp_result(self.interp.exec_given(topic, body), line)?
7281                        };
7282                        self.push(v);
7283                        Ok(())
7284                    }
7285                    Op::EvalTimeout(idx) => {
7286                        let i = *idx as usize;
7287                        let body = self.eval_timeout_entries[i].1.clone();
7288                        let secs = if let Some(&(start, end)) = self
7289                            .eval_timeout_expr_bytecode_ranges
7290                            .get(i)
7291                            .and_then(|r| r.as_ref())
7292                        {
7293                            self.run_block_region(start, end, op_count)?.to_number()
7294                        } else {
7295                            let timeout_expr = &self.eval_timeout_entries[i].0;
7296                            vm_interp_result(self.interp.eval_expr(timeout_expr), self.line())?
7297                                .to_number()
7298                        };
7299                        let v = vm_interp_result(
7300                            self.interp.eval_timeout_block(&body, secs, self.line()),
7301                            self.line(),
7302                        )?;
7303                        self.push(v);
7304                        Ok(())
7305                    }
7306                    Op::AlgebraicMatch(idx) => {
7307                        let i = *idx as usize;
7308                        let line = self.line();
7309                        let v = if let Some(&(start, end)) = self
7310                            .algebraic_match_subject_bytecode_ranges
7311                            .get(i)
7312                            .and_then(|r| r.as_ref())
7313                        {
7314                            let subject_val = self.run_block_region(start, end, op_count)?;
7315                            let arms = &self.algebraic_match_entries[i].1;
7316                            vm_interp_result(
7317                                self.interp.eval_algebraic_match_with_subject_value(
7318                                    subject_val,
7319                                    arms,
7320                                    line,
7321                                ),
7322                                self.line(),
7323                            )?
7324                        } else {
7325                            let (subject, arms) = &self.algebraic_match_entries[i];
7326                            vm_interp_result(
7327                                self.interp.eval_algebraic_match(subject, arms, line),
7328                                self.line(),
7329                            )?
7330                        };
7331                        self.push(v);
7332                        Ok(())
7333                    }
7334                    Op::ParLines(idx) => {
7335                        let (path, callback, progress) = &self.par_lines_entries[*idx as usize];
7336                        let v = vm_interp_result(
7337                            self.interp.eval_par_lines_expr(
7338                                path,
7339                                callback,
7340                                progress.as_ref(),
7341                                self.line(),
7342                            ),
7343                            self.line(),
7344                        )?;
7345                        self.push(v);
7346                        Ok(())
7347                    }
7348                    Op::ParWalk(idx) => {
7349                        let (path, callback, progress) = &self.par_walk_entries[*idx as usize];
7350                        let v = vm_interp_result(
7351                            self.interp.eval_par_walk_expr(
7352                                path,
7353                                callback,
7354                                progress.as_ref(),
7355                                self.line(),
7356                            ),
7357                            self.line(),
7358                        )?;
7359                        self.push(v);
7360                        Ok(())
7361                    }
7362                    Op::Pwatch(idx) => {
7363                        let (path, callback) = &self.pwatch_entries[*idx as usize];
7364                        let v = vm_interp_result(
7365                            self.interp.eval_pwatch_expr(path, callback, self.line()),
7366                            self.line(),
7367                        )?;
7368                        self.push(v);
7369                        Ok(())
7370                    }
7371
7372                    // ── Parallel operations (rayon) ──
7373                    Op::PMapWithBlock(block_idx) => {
7374                        let list = self.pop().to_list();
7375                        let progress_flag = self.pop().is_true();
7376                        let idx = *block_idx as usize;
7377                        let subs = self.interp.subs.clone();
7378                        let (scope_capture, atomic_arrays, atomic_hashes) =
7379                            self.interp.scope.capture_with_atomics();
7380                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7381                        let n_workers = rayon::current_num_threads();
7382                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
7383                            .map(|_| {
7384                                let mut interp = VMHelper::new();
7385                                interp.subs = subs.clone();
7386                                interp.scope.restore_capture(&scope_capture);
7387                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
7388                                interp.enable_parallel_guard();
7389                                Mutex::new(interp)
7390                            })
7391                            .collect();
7392                        if let Some(&(start, end)) =
7393                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7394                        {
7395                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7396                            let results: Vec<StrykeValue> = list
7397                                .into_par_iter()
7398                                .map(|item| {
7399                                    let tid =
7400                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7401                                    let mut local_interp = pool[tid].lock();
7402                                    local_interp.scope.set_topic(item);
7403                                    let mut vm = shared.worker_vm(&mut local_interp);
7404                                    let mut op_count = 0u64;
7405                                    let val = match vm.run_block_region(start, end, &mut op_count) {
7406                                        Ok(v) => v,
7407                                        Err(_) => StrykeValue::UNDEF,
7408                                    };
7409                                    pmap_progress.tick();
7410                                    val
7411                                })
7412                                .collect();
7413                            pmap_progress.finish();
7414                            self.push(StrykeValue::array(results));
7415                            Ok(())
7416                        } else {
7417                            let block = self.blocks[idx].clone();
7418                            let results: Vec<StrykeValue> = list
7419                                .into_par_iter()
7420                                .map(|item| {
7421                                    let tid =
7422                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7423                                    let mut local_interp = pool[tid].lock();
7424                                    local_interp.scope.set_topic(item);
7425                                    local_interp.scope_push_hook();
7426                                    let val = match local_interp.exec_block_no_scope(&block) {
7427                                        Ok(val) => val,
7428                                        Err(_) => StrykeValue::UNDEF,
7429                                    };
7430                                    local_interp.scope_pop_hook();
7431                                    pmap_progress.tick();
7432                                    val
7433                                })
7434                                .collect();
7435                            pmap_progress.finish();
7436                            self.push(StrykeValue::array(results));
7437                            Ok(())
7438                        }
7439                    }
7440                    Op::PFlatMapWithBlock(block_idx) => {
7441                        let list = self.pop().to_list();
7442                        let progress_flag = self.pop().is_true();
7443                        let idx = *block_idx as usize;
7444                        let subs = self.interp.subs.clone();
7445                        let (scope_capture, atomic_arrays, atomic_hashes) =
7446                            self.interp.scope.capture_with_atomics();
7447                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7448                        let n_workers = rayon::current_num_threads();
7449                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
7450                            .map(|_| {
7451                                let mut interp = VMHelper::new();
7452                                interp.subs = subs.clone();
7453                                interp.scope.restore_capture(&scope_capture);
7454                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
7455                                interp.enable_parallel_guard();
7456                                Mutex::new(interp)
7457                            })
7458                            .collect();
7459                        if let Some(&(start, end)) =
7460                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7461                        {
7462                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7463                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
7464                                .into_par_iter()
7465                                .enumerate()
7466                                .map(|(i, item)| {
7467                                    let tid =
7468                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7469                                    let mut local_interp = pool[tid].lock();
7470                                    local_interp.scope.set_topic(item);
7471                                    let mut vm = shared.worker_vm(&mut local_interp);
7472                                    let mut op_count = 0u64;
7473                                    let val = match vm.run_block_region(start, end, &mut op_count) {
7474                                        Ok(v) => v,
7475                                        Err(_) => StrykeValue::UNDEF,
7476                                    };
7477                                    let out = val.map_flatten_outputs(true);
7478                                    pmap_progress.tick();
7479                                    (i, out)
7480                                })
7481                                .collect();
7482                            pmap_progress.finish();
7483                            indexed.sort_by_key(|(i, _)| *i);
7484                            let results: Vec<StrykeValue> =
7485                                indexed.into_iter().flat_map(|(_, v)| v).collect();
7486                            self.push(StrykeValue::array(results));
7487                            Ok(())
7488                        } else {
7489                            let block = self.blocks[idx].clone();
7490                            let mut indexed: Vec<(usize, Vec<StrykeValue>)> = list
7491                                .into_par_iter()
7492                                .enumerate()
7493                                .map(|(i, item)| {
7494                                    let tid =
7495                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
7496                                    let mut local_interp = pool[tid].lock();
7497                                    local_interp.scope.set_topic(item);
7498                                    local_interp.scope_push_hook();
7499                                    let val = match local_interp.exec_block_no_scope(&block) {
7500                                        Ok(val) => val,
7501                                        Err(_) => StrykeValue::UNDEF,
7502                                    };
7503                                    local_interp.scope_pop_hook();
7504                                    let out = val.map_flatten_outputs(true);
7505                                    pmap_progress.tick();
7506                                    (i, out)
7507                                })
7508                                .collect();
7509                            pmap_progress.finish();
7510                            indexed.sort_by_key(|(i, _)| *i);
7511                            let results: Vec<StrykeValue> =
7512                                indexed.into_iter().flat_map(|(_, v)| v).collect();
7513                            self.push(StrykeValue::array(results));
7514                            Ok(())
7515                        }
7516                    }
7517                    Op::PMapRemote { block_idx, flat } => {
7518                        let cluster = self.pop();
7519                        let list_pv = self.pop();
7520                        let progress_flag = self.pop().is_true();
7521                        let idx = *block_idx as usize;
7522                        let block = self.blocks[idx].clone();
7523                        let flat_outputs = *flat != 0;
7524                        let v = vm_interp_result(
7525                            self.interp.eval_pmap_remote(
7526                                cluster,
7527                                list_pv,
7528                                progress_flag,
7529                                &block,
7530                                flat_outputs,
7531                                self.line(),
7532                            ),
7533                            self.line(),
7534                        )?;
7535                        self.push(v);
7536                        Ok(())
7537                    }
7538                    Op::Puniq => {
7539                        let list = self.pop().to_list();
7540                        let progress_flag = self.pop().is_true();
7541                        let n_threads = self.interp.parallel_thread_count();
7542                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7543                        let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
7544                        pmap_progress.finish();
7545                        self.push(StrykeValue::array(out));
7546                        Ok(())
7547                    }
7548                    Op::PFirstWithBlock(block_idx) => {
7549                        let list = self.pop().to_list();
7550                        let progress_flag = self.pop().is_true();
7551                        let idx = *block_idx as usize;
7552                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7553                        let subs = self.interp.subs.clone();
7554                        let (scope_capture, atomic_arrays, atomic_hashes) =
7555                            self.interp.scope.capture_with_atomics();
7556                        let out = if let Some(&(start, end)) =
7557                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7558                        {
7559                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7560                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
7561                                let mut local_interp = VMHelper::new();
7562                                local_interp.subs = subs.clone();
7563                                local_interp.scope.restore_capture(&scope_capture);
7564                                local_interp
7565                                    .scope
7566                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7567                                local_interp.enable_parallel_guard();
7568                                local_interp.scope.set_topic(item);
7569                                let mut vm = shared.worker_vm(&mut local_interp);
7570                                let mut op_count = 0u64;
7571                                match vm.run_block_region(start, end, &mut op_count) {
7572                                    Ok(v) => v.is_true(),
7573                                    Err(_) => false,
7574                                }
7575                            })
7576                        } else {
7577                            let block = self.blocks[idx].clone();
7578                            crate::par_list::pfirst_run(list, &pmap_progress, |item| {
7579                                let mut local_interp = VMHelper::new();
7580                                local_interp.subs = subs.clone();
7581                                local_interp.scope.restore_capture(&scope_capture);
7582                                local_interp
7583                                    .scope
7584                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7585                                local_interp.enable_parallel_guard();
7586                                local_interp.scope.set_topic(item);
7587                                local_interp.scope_push_hook();
7588                                let ok = match local_interp.exec_block_no_scope(&block) {
7589                                    Ok(v) => v.is_true(),
7590                                    Err(_) => false,
7591                                };
7592                                local_interp.scope_pop_hook();
7593                                ok
7594                            })
7595                        };
7596                        pmap_progress.finish();
7597                        self.push(out.unwrap_or(StrykeValue::UNDEF));
7598                        Ok(())
7599                    }
7600                    Op::PAnyWithBlock(block_idx) => {
7601                        let list = self.pop().to_list();
7602                        let progress_flag = self.pop().is_true();
7603                        let idx = *block_idx as usize;
7604                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7605                        let subs = self.interp.subs.clone();
7606                        let (scope_capture, atomic_arrays, atomic_hashes) =
7607                            self.interp.scope.capture_with_atomics();
7608                        let b = if let Some(&(start, end)) =
7609                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7610                        {
7611                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7612                            crate::par_list::pany_run(list, &pmap_progress, |item| {
7613                                let mut local_interp = VMHelper::new();
7614                                local_interp.subs = subs.clone();
7615                                local_interp.scope.restore_capture(&scope_capture);
7616                                local_interp
7617                                    .scope
7618                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7619                                local_interp.enable_parallel_guard();
7620                                local_interp.scope.set_topic(item);
7621                                let mut vm = shared.worker_vm(&mut local_interp);
7622                                let mut op_count = 0u64;
7623                                match vm.run_block_region(start, end, &mut op_count) {
7624                                    Ok(v) => v.is_true(),
7625                                    Err(_) => false,
7626                                }
7627                            })
7628                        } else {
7629                            let block = self.blocks[idx].clone();
7630                            crate::par_list::pany_run(list, &pmap_progress, |item| {
7631                                let mut local_interp = VMHelper::new();
7632                                local_interp.subs = subs.clone();
7633                                local_interp.scope.restore_capture(&scope_capture);
7634                                local_interp
7635                                    .scope
7636                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
7637                                local_interp.enable_parallel_guard();
7638                                local_interp.scope.set_topic(item);
7639                                local_interp.scope_push_hook();
7640                                let ok = match local_interp.exec_block_no_scope(&block) {
7641                                    Ok(v) => v.is_true(),
7642                                    Err(_) => false,
7643                                };
7644                                local_interp.scope_pop_hook();
7645                                ok
7646                            })
7647                        };
7648                        pmap_progress.finish();
7649                        self.push(StrykeValue::integer(if b { 1 } else { 0 }));
7650                        Ok(())
7651                    }
7652                    Op::PMapChunkedWithBlock(block_idx) => {
7653                        let list = self.pop().to_list();
7654                        let chunk_n = self.pop().to_int().max(1) as usize;
7655                        let progress_flag = self.pop().is_true();
7656                        let idx = *block_idx as usize;
7657                        let subs = self.interp.subs.clone();
7658                        let (scope_capture, atomic_arrays, atomic_hashes) =
7659                            self.interp.scope.capture_with_atomics();
7660                        let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = list
7661                            .chunks(chunk_n)
7662                            .enumerate()
7663                            .map(|(i, c)| (i, c.to_vec()))
7664                            .collect();
7665                        let n_chunks = indexed_chunks.len();
7666                        let pmap_progress = PmapProgress::new(progress_flag, n_chunks);
7667                        if let Some(&(start, end)) =
7668                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7669                        {
7670                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7671                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
7672                                .into_par_iter()
7673                                .map(|(chunk_idx, chunk)| {
7674                                    let mut local_interp = VMHelper::new();
7675                                    local_interp.subs = subs.clone();
7676                                    local_interp.scope.restore_capture(&scope_capture);
7677                                    local_interp
7678                                        .scope
7679                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
7680                                    local_interp.enable_parallel_guard();
7681                                    let mut out = Vec::with_capacity(chunk.len());
7682                                    for item in chunk {
7683                                        local_interp.scope.set_topic(item);
7684                                        let mut vm = shared.worker_vm(&mut local_interp);
7685                                        let mut op_count = 0u64;
7686                                        let val =
7687                                            match vm.run_block_region(start, end, &mut op_count) {
7688                                                Ok(v) => v,
7689                                                Err(_) => StrykeValue::UNDEF,
7690                                            };
7691                                        out.push(val);
7692                                    }
7693                                    pmap_progress.tick();
7694                                    (chunk_idx, out)
7695                                })
7696                                .collect();
7697                            pmap_progress.finish();
7698                            chunk_results.sort_by_key(|(i, _)| *i);
7699                            let results: Vec<StrykeValue> =
7700                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
7701                            self.push(StrykeValue::array(results));
7702                            Ok(())
7703                        } else {
7704                            let block = self.blocks[idx].clone();
7705                            let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
7706                                .into_par_iter()
7707                                .map(|(chunk_idx, chunk)| {
7708                                    let mut local_interp = VMHelper::new();
7709                                    local_interp.subs = subs.clone();
7710                                    local_interp.scope.restore_capture(&scope_capture);
7711                                    local_interp
7712                                        .scope
7713                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
7714                                    local_interp.enable_parallel_guard();
7715                                    let mut out = Vec::with_capacity(chunk.len());
7716                                    for item in chunk {
7717                                        local_interp.scope.set_topic(item);
7718                                        local_interp.scope_push_hook();
7719                                        let val = match local_interp.exec_block_no_scope(&block) {
7720                                            Ok(val) => val,
7721                                            Err(_) => StrykeValue::UNDEF,
7722                                        };
7723                                        local_interp.scope_pop_hook();
7724                                        out.push(val);
7725                                    }
7726                                    pmap_progress.tick();
7727                                    (chunk_idx, out)
7728                                })
7729                                .collect();
7730                            pmap_progress.finish();
7731                            chunk_results.sort_by_key(|(i, _)| *i);
7732                            let results: Vec<StrykeValue> =
7733                                chunk_results.into_iter().flat_map(|(_, v)| v).collect();
7734                            self.push(StrykeValue::array(results));
7735                            Ok(())
7736                        }
7737                    }
7738                    Op::ReduceWithBlock(block_idx) => {
7739                        let list = self.pop().to_list();
7740                        let idx = *block_idx as usize;
7741                        let subs = self.interp.subs.clone();
7742                        let scope_capture = self.interp.scope.capture();
7743                        if list.is_empty() {
7744                            self.push(StrykeValue::UNDEF);
7745                            return Ok(());
7746                        }
7747                        if list.len() == 1 {
7748                            self.push(list.into_iter().next().unwrap());
7749                            return Ok(());
7750                        }
7751                        let mut items = list;
7752                        let mut acc = items.remove(0);
7753                        let rest = items;
7754                        if let Some(&(start, end)) =
7755                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7756                        {
7757                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7758                            for b in rest {
7759                                let mut local_interp = VMHelper::new();
7760                                local_interp.subs = subs.clone();
7761                                local_interp.scope.restore_capture(&scope_capture);
7762                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
7763                                let mut vm = shared.worker_vm(&mut local_interp);
7764                                let mut op_count = 0u64;
7765                                acc = match vm.run_block_region(start, end, &mut op_count) {
7766                                    Ok(v) => v,
7767                                    Err(_) => StrykeValue::UNDEF,
7768                                };
7769                            }
7770                        } else {
7771                            let block = self.blocks[idx].clone();
7772                            for b in rest {
7773                                let mut local_interp = VMHelper::new();
7774                                local_interp.subs = subs.clone();
7775                                local_interp.scope.restore_capture(&scope_capture);
7776                                local_interp.scope.set_sort_pair(acc.clone(), b.clone());
7777                                acc = match local_interp.exec_block(&block) {
7778                                    Ok(val) => val,
7779                                    Err(_) => StrykeValue::UNDEF,
7780                                };
7781                            }
7782                        }
7783                        self.push(acc);
7784                        Ok(())
7785                    }
7786                    Op::PReduceWithBlock(block_idx) => {
7787                        let list = self.pop().to_list();
7788                        let progress_flag = self.pop().is_true();
7789                        let idx = *block_idx as usize;
7790                        let subs = self.interp.subs.clone();
7791                        let scope_capture = self.interp.scope.capture();
7792                        if list.is_empty() {
7793                            self.push(StrykeValue::UNDEF);
7794                            return Ok(());
7795                        }
7796                        if list.len() == 1 {
7797                            self.push(list.into_iter().next().unwrap());
7798                            return Ok(());
7799                        }
7800                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7801                        if let Some(&(start, end)) =
7802                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
7803                        {
7804                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7805                            let result = list
7806                                .into_par_iter()
7807                                .map(|x| {
7808                                    pmap_progress.tick();
7809                                    x
7810                                })
7811                                .reduce_with(|a, b| {
7812                                    let mut local_interp = VMHelper::new();
7813                                    local_interp.subs = subs.clone();
7814                                    local_interp.scope.restore_capture(&scope_capture);
7815                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
7816                                    let mut vm = shared.worker_vm(&mut local_interp);
7817                                    let mut op_count = 0u64;
7818                                    match vm.run_block_region(start, end, &mut op_count) {
7819                                        Ok(val) => val,
7820                                        Err(_) => StrykeValue::UNDEF,
7821                                    }
7822                                });
7823                            pmap_progress.finish();
7824                            self.push(result.unwrap_or(StrykeValue::UNDEF));
7825                            Ok(())
7826                        } else {
7827                            let block = self.blocks[idx].clone();
7828                            let result = list
7829                                .into_par_iter()
7830                                .map(|x| {
7831                                    pmap_progress.tick();
7832                                    x
7833                                })
7834                                .reduce_with(|a, b| {
7835                                    let mut local_interp = VMHelper::new();
7836                                    local_interp.subs = subs.clone();
7837                                    local_interp.scope.restore_capture(&scope_capture);
7838                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
7839                                    match local_interp.exec_block(&block) {
7840                                        Ok(val) => val,
7841                                        Err(_) => StrykeValue::UNDEF,
7842                                    }
7843                                });
7844                            pmap_progress.finish();
7845                            self.push(result.unwrap_or(StrykeValue::UNDEF));
7846                            Ok(())
7847                        }
7848                    }
7849                    Op::PReduceInitWithBlock(block_idx) => {
7850                        let init_val = self.pop();
7851                        let list = self.pop().to_list();
7852                        let progress_flag = self.pop().is_true();
7853                        let idx = *block_idx as usize;
7854                        let subs = self.interp.subs.clone();
7855                        let scope_capture = self.interp.scope.capture();
7856                        let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
7857                        let block = self.blocks[idx].clone();
7858                        if list.is_empty() {
7859                            self.push(init_val);
7860                            return Ok(());
7861                        }
7862                        if list.len() == 1 {
7863                            let v = fold_preduce_init_step(
7864                                &subs,
7865                                cap,
7866                                &block,
7867                                preduce_init_fold_identity(&init_val),
7868                                list.into_iter().next().unwrap(),
7869                            );
7870                            self.push(v);
7871                            return Ok(());
7872                        }
7873                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7874                        let result = list
7875                            .into_par_iter()
7876                            .fold(
7877                                || preduce_init_fold_identity(&init_val),
7878                                |acc, item| {
7879                                    pmap_progress.tick();
7880                                    fold_preduce_init_step(&subs, cap, &block, acc, item)
7881                                },
7882                            )
7883                            .reduce(
7884                                || preduce_init_fold_identity(&init_val),
7885                                |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
7886                            );
7887                        pmap_progress.finish();
7888                        self.push(result);
7889                        Ok(())
7890                    }
7891                    Op::PMapReduceWithBlocks(map_idx, reduce_idx) => {
7892                        let list = self.pop().to_list();
7893                        let progress_flag = self.pop().is_true();
7894                        let map_i = *map_idx as usize;
7895                        let reduce_i = *reduce_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                            let mut local_interp = VMHelper::new();
7904                            local_interp.subs = subs.clone();
7905                            local_interp.scope.restore_capture(&scope_capture);
7906                            local_interp
7907                                .scope
7908                                .set_topic(list.into_iter().next().unwrap());
7909                            let map_block = self.blocks[map_i].clone();
7910                            let v = match local_interp.exec_block_no_scope(&map_block) {
7911                                Ok(v) => v,
7912                                Err(_) => StrykeValue::UNDEF,
7913                            };
7914                            self.push(v);
7915                            return Ok(());
7916                        }
7917                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
7918                        let map_range = self
7919                            .block_bytecode_ranges
7920                            .get(map_i)
7921                            .and_then(|r| r.as_ref())
7922                            .copied();
7923                        let reduce_range = self
7924                            .block_bytecode_ranges
7925                            .get(reduce_i)
7926                            .and_then(|r| r.as_ref())
7927                            .copied();
7928                        if let (Some((map_start, map_end)), Some((reduce_start, reduce_end))) =
7929                            (map_range, reduce_range)
7930                        {
7931                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
7932                            let result = list
7933                                .into_par_iter()
7934                                .map(|item| {
7935                                    let mut local_interp = VMHelper::new();
7936                                    local_interp.subs = subs.clone();
7937                                    local_interp.scope.restore_capture(&scope_capture);
7938                                    local_interp.scope.set_topic(item);
7939                                    let mut vm = shared.worker_vm(&mut local_interp);
7940                                    let mut op_count = 0u64;
7941                                    let val = match vm.run_block_region(
7942                                        map_start,
7943                                        map_end,
7944                                        &mut op_count,
7945                                    ) {
7946                                        Ok(val) => val,
7947                                        Err(_) => StrykeValue::UNDEF,
7948                                    };
7949                                    pmap_progress.tick();
7950                                    val
7951                                })
7952                                .reduce_with(|a, b| {
7953                                    let mut local_interp = VMHelper::new();
7954                                    local_interp.subs = subs.clone();
7955                                    local_interp.scope.restore_capture(&scope_capture);
7956                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
7957                                    let mut vm = shared.worker_vm(&mut local_interp);
7958                                    let mut op_count = 0u64;
7959                                    match vm.run_block_region(
7960                                        reduce_start,
7961                                        reduce_end,
7962                                        &mut op_count,
7963                                    ) {
7964                                        Ok(val) => val,
7965                                        Err(_) => StrykeValue::UNDEF,
7966                                    }
7967                                });
7968                            pmap_progress.finish();
7969                            self.push(result.unwrap_or(StrykeValue::UNDEF));
7970                            Ok(())
7971                        } else {
7972                            let map_block = self.blocks[map_i].clone();
7973                            let reduce_block = self.blocks[reduce_i].clone();
7974                            let result = list
7975                                .into_par_iter()
7976                                .map(|item| {
7977                                    let mut local_interp = VMHelper::new();
7978                                    local_interp.subs = subs.clone();
7979                                    local_interp.scope.restore_capture(&scope_capture);
7980                                    local_interp.scope.set_topic(item);
7981                                    let val = match local_interp.exec_block_no_scope(&map_block) {
7982                                        Ok(val) => val,
7983                                        Err(_) => StrykeValue::UNDEF,
7984                                    };
7985                                    pmap_progress.tick();
7986                                    val
7987                                })
7988                                .reduce_with(|a, b| {
7989                                    let mut local_interp = VMHelper::new();
7990                                    local_interp.subs = subs.clone();
7991                                    local_interp.scope.restore_capture(&scope_capture);
7992                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
7993                                    match local_interp.exec_block_no_scope(&reduce_block) {
7994                                        Ok(val) => val,
7995                                        Err(_) => StrykeValue::UNDEF,
7996                                    }
7997                                });
7998                            pmap_progress.finish();
7999                            self.push(result.unwrap_or(StrykeValue::UNDEF));
8000                            Ok(())
8001                        }
8002                    }
8003                    Op::PcacheWithBlock(block_idx) => {
8004                        let list = self.pop().to_list();
8005                        let progress_flag = self.pop().is_true();
8006                        let idx = *block_idx as usize;
8007                        let subs = self.interp.subs.clone();
8008                        let scope_capture = self.interp.scope.capture();
8009                        let block = self.blocks[idx].clone();
8010                        let cache = &*crate::pcache::GLOBAL_PCACHE;
8011                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8012                        if let Some(&(start, end)) =
8013                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8014                        {
8015                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8016                            let results: Vec<StrykeValue> = list
8017                                .into_par_iter()
8018                                .map(|item| {
8019                                    let k = crate::pcache::cache_key(&item);
8020                                    if let Some(v) = cache.get(&k) {
8021                                        pmap_progress.tick();
8022                                        return v.clone();
8023                                    }
8024                                    let mut local_interp = VMHelper::new();
8025                                    local_interp.subs = subs.clone();
8026                                    local_interp.scope.restore_capture(&scope_capture);
8027                                    local_interp.scope.set_topic(item.clone());
8028                                    let mut vm = shared.worker_vm(&mut local_interp);
8029                                    let mut op_count = 0u64;
8030                                    let val = match vm.run_block_region(start, end, &mut op_count) {
8031                                        Ok(v) => v,
8032                                        Err(_) => StrykeValue::UNDEF,
8033                                    };
8034                                    cache.insert(k, val.clone());
8035                                    pmap_progress.tick();
8036                                    val
8037                                })
8038                                .collect();
8039                            pmap_progress.finish();
8040                            self.push(StrykeValue::array(results));
8041                            Ok(())
8042                        } else {
8043                            let results: Vec<StrykeValue> = list
8044                                .into_par_iter()
8045                                .map(|item| {
8046                                    let k = crate::pcache::cache_key(&item);
8047                                    if let Some(v) = cache.get(&k) {
8048                                        pmap_progress.tick();
8049                                        return v.clone();
8050                                    }
8051                                    let mut local_interp = VMHelper::new();
8052                                    local_interp.subs = subs.clone();
8053                                    local_interp.scope.restore_capture(&scope_capture);
8054                                    local_interp.scope.set_topic(item.clone());
8055                                    let val = match local_interp.exec_block_no_scope(&block) {
8056                                        Ok(v) => v,
8057                                        Err(_) => StrykeValue::UNDEF,
8058                                    };
8059                                    cache.insert(k, val.clone());
8060                                    pmap_progress.tick();
8061                                    val
8062                                })
8063                                .collect();
8064                            pmap_progress.finish();
8065                            self.push(StrykeValue::array(results));
8066                            Ok(())
8067                        }
8068                    }
8069                    Op::Pselect { n_rx, has_timeout } => {
8070                        let timeout = if *has_timeout {
8071                            let t = self.pop().to_number();
8072                            Some(std::time::Duration::from_secs_f64(t.max(0.0)))
8073                        } else {
8074                            None
8075                        };
8076                        let mut rx_vals = Vec::with_capacity(*n_rx as usize);
8077                        for _ in 0..*n_rx {
8078                            rx_vals.push(self.pop());
8079                        }
8080                        rx_vals.reverse();
8081                        let line = self.line();
8082                        let v = crate::pchannel::pselect_recv_with_optional_timeout(
8083                            &rx_vals, timeout, line,
8084                        )?;
8085                        self.push(v);
8086                        Ok(())
8087                    }
8088                    Op::PGrepWithBlock(block_idx) => {
8089                        let list = self.pop().to_list();
8090                        let progress_flag = self.pop().is_true();
8091                        let idx = *block_idx as usize;
8092                        let subs = self.interp.subs.clone();
8093                        let (scope_capture, atomic_arrays, atomic_hashes) =
8094                            self.interp.scope.capture_with_atomics();
8095                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8096                        let n_workers = rayon::current_num_threads();
8097                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8098                            .map(|_| {
8099                                let mut interp = VMHelper::new();
8100                                interp.subs = subs.clone();
8101                                interp.scope.restore_capture(&scope_capture);
8102                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8103                                interp.enable_parallel_guard();
8104                                Mutex::new(interp)
8105                            })
8106                            .collect();
8107                        if let Some(&(start, end)) =
8108                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8109                        {
8110                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8111                            let results: Vec<StrykeValue> = list
8112                                .into_par_iter()
8113                                .filter_map(|item| {
8114                                    let tid =
8115                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8116                                    let mut local_interp = pool[tid].lock();
8117                                    local_interp.scope.set_topic(item.clone());
8118                                    let mut vm = shared.worker_vm(&mut local_interp);
8119                                    let mut op_count = 0u64;
8120                                    let keep = match vm.run_block_region(start, end, &mut op_count)
8121                                    {
8122                                        Ok(val) => val.is_true(),
8123                                        Err(_) => false,
8124                                    };
8125                                    pmap_progress.tick();
8126                                    if keep {
8127                                        Some(item)
8128                                    } else {
8129                                        None
8130                                    }
8131                                })
8132                                .collect();
8133                            pmap_progress.finish();
8134                            self.push(StrykeValue::array(results));
8135                            Ok(())
8136                        } else {
8137                            let block = self.blocks[idx].clone();
8138                            let results: Vec<StrykeValue> = list
8139                                .into_par_iter()
8140                                .filter_map(|item| {
8141                                    let tid =
8142                                        rayon::current_thread_index().unwrap_or(0) % pool.len();
8143                                    let mut local_interp = pool[tid].lock();
8144                                    local_interp.scope.set_topic(item.clone());
8145                                    local_interp.scope_push_hook();
8146                                    let keep = match local_interp.exec_block_no_scope(&block) {
8147                                        Ok(val) => val.is_true(),
8148                                        Err(_) => false,
8149                                    };
8150                                    local_interp.scope_pop_hook();
8151                                    pmap_progress.tick();
8152                                    if keep {
8153                                        Some(item)
8154                                    } else {
8155                                        None
8156                                    }
8157                                })
8158                                .collect();
8159                            pmap_progress.finish();
8160                            self.push(StrykeValue::array(results));
8161                            Ok(())
8162                        }
8163                    }
8164                    Op::PMapsWithBlock(block_idx) => {
8165                        let val = self.pop();
8166                        let block = self.blocks[*block_idx as usize].clone();
8167                        let source = crate::map_stream::into_pull_iter(val);
8168                        let sub = self.interp.anon_coderef_from_block(&block);
8169                        let (capture, atomic_arrays, atomic_hashes) =
8170                            self.interp.scope.capture_with_atomics();
8171                        let out = StrykeValue::iterator(Arc::new(
8172                            crate::map_stream::PMapStreamIterator::new(
8173                                source,
8174                                sub,
8175                                self.interp.subs.clone(),
8176                                capture,
8177                                atomic_arrays,
8178                                atomic_hashes,
8179                                false,
8180                            ),
8181                        ));
8182                        self.push(out);
8183                        Ok(())
8184                    }
8185                    Op::PFlatMapsWithBlock(block_idx) => {
8186                        let val = self.pop();
8187                        let block = self.blocks[*block_idx as usize].clone();
8188                        let source = crate::map_stream::into_pull_iter(val);
8189                        let sub = self.interp.anon_coderef_from_block(&block);
8190                        let (capture, atomic_arrays, atomic_hashes) =
8191                            self.interp.scope.capture_with_atomics();
8192                        let out = StrykeValue::iterator(Arc::new(
8193                            crate::map_stream::PMapStreamIterator::new(
8194                                source,
8195                                sub,
8196                                self.interp.subs.clone(),
8197                                capture,
8198                                atomic_arrays,
8199                                atomic_hashes,
8200                                true,
8201                            ),
8202                        ));
8203                        self.push(out);
8204                        Ok(())
8205                    }
8206                    Op::PGrepsWithBlock(block_idx) => {
8207                        let val = self.pop();
8208                        let block = self.blocks[*block_idx as usize].clone();
8209                        let source = crate::map_stream::into_pull_iter(val);
8210                        let sub = self.interp.anon_coderef_from_block(&block);
8211                        let (capture, atomic_arrays, atomic_hashes) =
8212                            self.interp.scope.capture_with_atomics();
8213                        let out = StrykeValue::iterator(Arc::new(
8214                            crate::map_stream::PGrepStreamIterator::new(
8215                                source,
8216                                sub,
8217                                self.interp.subs.clone(),
8218                                capture,
8219                                atomic_arrays,
8220                                atomic_hashes,
8221                            ),
8222                        ));
8223                        self.push(out);
8224                        Ok(())
8225                    }
8226                    Op::PForWithBlock(block_idx) => {
8227                        let line = self.line();
8228                        let list = self.pop().to_list();
8229                        let progress_flag = self.pop().is_true();
8230                        let pmap_progress = PmapProgress::new(progress_flag, list.len());
8231                        let idx = *block_idx as usize;
8232                        let subs = self.interp.subs.clone();
8233                        let (scope_capture, atomic_arrays, atomic_hashes) =
8234                            self.interp.scope.capture_with_atomics();
8235                        let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
8236                        let n_workers = rayon::current_num_threads();
8237                        let pool: Vec<Mutex<VMHelper>> = (0..n_workers)
8238                            .map(|_| {
8239                                let mut interp = VMHelper::new();
8240                                interp.subs = subs.clone();
8241                                interp.scope.restore_capture(&scope_capture);
8242                                interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
8243                                interp.enable_parallel_guard();
8244                                Mutex::new(interp)
8245                            })
8246                            .collect();
8247                        if let Some(&(start, end)) =
8248                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8249                        {
8250                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8251                            list.into_par_iter().for_each(|item| {
8252                                if first_err.lock().is_some() {
8253                                    return;
8254                                }
8255                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
8256                                let mut local_interp = pool[tid].lock();
8257                                local_interp.scope.set_topic(item);
8258                                let mut vm = shared.worker_vm(&mut local_interp);
8259                                let mut op_count = 0u64;
8260                                match vm.run_block_region(start, end, &mut op_count) {
8261                                    Ok(_) => {}
8262                                    Err(e) => {
8263                                        let mut g = first_err.lock();
8264                                        if g.is_none() {
8265                                            *g = Some(e);
8266                                        }
8267                                    }
8268                                }
8269                                pmap_progress.tick();
8270                            });
8271                        } else {
8272                            let block = self.blocks[idx].clone();
8273                            list.into_par_iter().for_each(|item| {
8274                                if first_err.lock().is_some() {
8275                                    return;
8276                                }
8277                                let tid = rayon::current_thread_index().unwrap_or(0) % pool.len();
8278                                let mut local_interp = pool[tid].lock();
8279                                local_interp.scope.set_topic(item);
8280                                local_interp.scope_push_hook();
8281                                match local_interp.exec_block_no_scope(&block) {
8282                                    Ok(_) => {}
8283                                    Err(e) => {
8284                                        let stryke = match e {
8285                                            FlowOrError::Error(stryke) => stryke,
8286                                            FlowOrError::Flow(_) => PerlError::runtime(
8287                                                "return/last/next/redo not supported inside pfor block",
8288                                                line,
8289                                            ),
8290                                        };
8291                                        let mut g = first_err.lock();
8292                                        if g.is_none() {
8293                                            *g = Some(stryke);
8294                                        }
8295                                    }
8296                                }
8297                                local_interp.scope_pop_hook();
8298                                pmap_progress.tick();
8299                            });
8300                        }
8301                        pmap_progress.finish();
8302                        if let Some(e) = first_err.lock().take() {
8303                            return Err(e);
8304                        }
8305                        self.push(StrykeValue::UNDEF);
8306                        Ok(())
8307                    }
8308                    Op::PSortWithBlock(block_idx) => {
8309                        let mut items = self.pop().to_list();
8310                        let progress_flag = self.pop().is_true();
8311                        let pmap_progress = PmapProgress::new(progress_flag, 2);
8312                        pmap_progress.tick();
8313                        let idx = *block_idx as usize;
8314                        let subs = self.interp.subs.clone();
8315                        let (scope_capture, atomic_arrays, atomic_hashes) =
8316                            self.interp.scope.capture_with_atomics();
8317                        if let Some(&(start, end)) =
8318                            self.block_bytecode_ranges.get(idx).and_then(|r| r.as_ref())
8319                        {
8320                            let shared = Arc::new(ParallelBlockVmShared::from_vm(self));
8321                            items.par_sort_by(|a, b| {
8322                                let mut local_interp = VMHelper::new();
8323                                local_interp.subs = subs.clone();
8324                                local_interp.scope.restore_capture(&scope_capture);
8325                                local_interp
8326                                    .scope
8327                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8328                                local_interp.enable_parallel_guard();
8329                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
8330                                // Populate slot-based positional args so the
8331                                // bytecode block can read `$_0`/`$_1` (and the
8332                                // bareword `_0`/`_1`) through the slot fast
8333                                // path. `set_sort_pair` only sets the named
8334                                // scalars; without slots, an `$_0` reference
8335                                // resolves to undef in worker bytecode.
8336                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
8337                                let mut vm = shared.worker_vm(&mut local_interp);
8338                                let mut op_count = 0u64;
8339                                match vm.run_block_region(start, end, &mut op_count) {
8340                                    Ok(v) => {
8341                                        let n = v.to_int();
8342                                        if n < 0 {
8343                                            std::cmp::Ordering::Less
8344                                        } else if n > 0 {
8345                                            std::cmp::Ordering::Greater
8346                                        } else {
8347                                            std::cmp::Ordering::Equal
8348                                        }
8349                                    }
8350                                    Err(_) => std::cmp::Ordering::Equal,
8351                                }
8352                            });
8353                        } else {
8354                            let block = self.blocks[idx].clone();
8355                            items.par_sort_by(|a, b| {
8356                                let mut local_interp = VMHelper::new();
8357                                local_interp.subs = subs.clone();
8358                                local_interp.scope.restore_capture(&scope_capture);
8359                                local_interp
8360                                    .scope
8361                                    .restore_atomics(&atomic_arrays, &atomic_hashes);
8362                                local_interp.enable_parallel_guard();
8363                                local_interp.scope.set_sort_pair(a.clone(), b.clone());
8364                                local_interp.scope.set_closure_args(&[a.clone(), b.clone()]);
8365                                local_interp.scope_push_hook();
8366                                let ord = match local_interp.exec_block_no_scope(&block) {
8367                                    Ok(v) => {
8368                                        let n = v.to_int();
8369                                        if n < 0 {
8370                                            std::cmp::Ordering::Less
8371                                        } else if n > 0 {
8372                                            std::cmp::Ordering::Greater
8373                                        } else {
8374                                            std::cmp::Ordering::Equal
8375                                        }
8376                                    }
8377                                    Err(_) => std::cmp::Ordering::Equal,
8378                                };
8379                                local_interp.scope_pop_hook();
8380                                ord
8381                            });
8382                        }
8383                        pmap_progress.tick();
8384                        pmap_progress.finish();
8385                        self.push(StrykeValue::array(items));
8386                        Ok(())
8387                    }
8388                    Op::PSortWithBlockFast(tag) => {
8389                        let mut items = self.pop().to_list();
8390                        let progress_flag = self.pop().is_true();
8391                        let pmap_progress = PmapProgress::new(progress_flag, 2);
8392                        pmap_progress.tick();
8393                        let mode = match *tag {
8394                            0 => SortBlockFast::Numeric,
8395                            1 => SortBlockFast::String,
8396                            2 => SortBlockFast::NumericRev,
8397                            3 => SortBlockFast::StringRev,
8398                            _ => SortBlockFast::Numeric,
8399                        };
8400                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
8401                        pmap_progress.tick();
8402                        pmap_progress.finish();
8403                        self.push(StrykeValue::array(items));
8404                        Ok(())
8405                    }
8406                    Op::PSortNoBlockParallel => {
8407                        let mut items = self.pop().to_list();
8408                        let progress_flag = self.pop().is_true();
8409                        let pmap_progress = PmapProgress::new(progress_flag, 2);
8410                        pmap_progress.tick();
8411                        items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
8412                        pmap_progress.tick();
8413                        pmap_progress.finish();
8414                        self.push(StrykeValue::array(items));
8415                        Ok(())
8416                    }
8417                    Op::FanWithBlock(block_idx) => {
8418                        let line = self.line();
8419                        let n = self.pop().to_int().max(0) as usize;
8420                        let progress_flag = self.pop().is_true();
8421                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
8422                        Ok(())
8423                    }
8424                    Op::FanWithBlockAuto(block_idx) => {
8425                        let line = self.line();
8426                        let n = self.interp.parallel_thread_count();
8427                        let progress_flag = self.pop().is_true();
8428                        self.run_fan_block(*block_idx, n, line, progress_flag)?;
8429                        Ok(())
8430                    }
8431                    Op::FanCapWithBlock(block_idx) => {
8432                        let line = self.line();
8433                        let n = self.pop().to_int().max(0) as usize;
8434                        let progress_flag = self.pop().is_true();
8435                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
8436                        Ok(())
8437                    }
8438                    Op::FanCapWithBlockAuto(block_idx) => {
8439                        let line = self.line();
8440                        let n = self.interp.parallel_thread_count();
8441                        let progress_flag = self.pop().is_true();
8442                        self.run_fan_cap_block(*block_idx, n, line, progress_flag)?;
8443                        Ok(())
8444                    }
8445
8446                    Op::AsyncBlock(block_idx) => {
8447                        let block = self.blocks[*block_idx as usize].clone();
8448                        let subs = self.interp.subs.clone();
8449                        let (scope_capture, atomic_arrays, atomic_hashes) =
8450                            self.interp.scope.capture_with_atomics();
8451                        let result_slot: Arc<Mutex<Option<PerlResult<StrykeValue>>>> =
8452                            Arc::new(Mutex::new(None));
8453                        let join_slot: Arc<Mutex<Option<std::thread::JoinHandle<()>>>> =
8454                            Arc::new(Mutex::new(None));
8455                        let rs = Arc::clone(&result_slot);
8456                        let h = std::thread::spawn(move || {
8457                            let mut local_interp = VMHelper::new();
8458                            local_interp.subs = subs;
8459                            local_interp.scope.restore_capture(&scope_capture);
8460                            local_interp
8461                                .scope
8462                                .restore_atomics(&atomic_arrays, &atomic_hashes);
8463                            local_interp.enable_parallel_guard();
8464                            local_interp.scope_push_hook();
8465                            let out = match local_interp.exec_block_no_scope(&block) {
8466                                Ok(v) => Ok(v),
8467                                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
8468                                Err(FlowOrError::Error(e)) => Err(e),
8469                                Err(_) => Ok(StrykeValue::UNDEF),
8470                            };
8471                            local_interp.scope_pop_hook();
8472                            *rs.lock() = Some(out);
8473                        });
8474                        *join_slot.lock() = Some(h);
8475                        self.push(StrykeValue::async_task(Arc::new(PerlAsyncTask {
8476                            result: result_slot,
8477                            join: join_slot,
8478                        })));
8479                        Ok(())
8480                    }
8481                    Op::Await => {
8482                        let v = self.pop();
8483                        if let Some(t) = v.as_async_task() {
8484                            let r = t.await_result();
8485                            self.push(r?);
8486                        } else {
8487                            self.push(v);
8488                        }
8489                        Ok(())
8490                    }
8491
8492                    Op::LoadCurrentSub => {
8493                        if let Some(sub) = self.interp.current_sub_stack.last().cloned() {
8494                            self.push(StrykeValue::code_ref(sub));
8495                        } else {
8496                            self.push(StrykeValue::UNDEF);
8497                        }
8498                        Ok(())
8499                    }
8500
8501                    Op::DeferBlock => {
8502                        let coderef = self.pop();
8503                        self.interp.scope.push_defer(coderef);
8504                        Ok(())
8505                    }
8506
8507                    // ── try / catch / finally ──
8508                    Op::TryPush { .. } => {
8509                        self.try_stack.push(TryFrame {
8510                            try_push_op_idx: self.ip - 1,
8511                        });
8512                        Ok(())
8513                    }
8514                    Op::TryContinueNormal => {
8515                        let frame = self.try_stack.last().ok_or_else(|| {
8516                            PerlError::runtime("TryContinueNormal without active try", self.line())
8517                        })?;
8518                        let Op::TryPush {
8519                            finally_ip,
8520                            after_ip,
8521                            ..
8522                        } = &self.ops[frame.try_push_op_idx]
8523                        else {
8524                            return Err(PerlError::runtime(
8525                                "TryContinueNormal: corrupt try frame",
8526                                self.line(),
8527                            ));
8528                        };
8529                        if let Some(fin_ip) = *finally_ip {
8530                            self.ip = fin_ip;
8531                            Ok(())
8532                        } else {
8533                            self.try_stack.pop();
8534                            self.ip = *after_ip;
8535                            Ok(())
8536                        }
8537                    }
8538                    Op::TryFinallyEnd => {
8539                        let frame = self.try_stack.pop().ok_or_else(|| {
8540                            PerlError::runtime("TryFinallyEnd without active try", self.line())
8541                        })?;
8542                        let Op::TryPush { after_ip, .. } = &self.ops[frame.try_push_op_idx] else {
8543                            return Err(PerlError::runtime(
8544                                "TryFinallyEnd: corrupt try frame",
8545                                self.line(),
8546                            ));
8547                        };
8548                        self.ip = *after_ip;
8549                        Ok(())
8550                    }
8551                    Op::CatchReceive(idx) => {
8552                        let msg = self.pending_catch_error.take().ok_or_else(|| {
8553                            PerlError::runtime(
8554                                "CatchReceive without pending exception",
8555                                self.line(),
8556                            )
8557                        })?;
8558                        let n = names[*idx as usize].as_str();
8559                        self.interp.scope_pop_hook();
8560                        self.interp.scope_push_hook();
8561                        self.interp
8562                            .scope
8563                            .declare_scalar(n, StrykeValue::string(msg));
8564                        self.interp.english_note_lexical_scalar(n);
8565                        Ok(())
8566                    }
8567
8568                    Op::DeclareMySyncScalar(name_idx) => {
8569                        let val = self.pop();
8570                        let n = names[*name_idx as usize].as_str();
8571                        let stored = if val.is_mysync_deque_or_heap() {
8572                            val
8573                        } else {
8574                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
8575                        };
8576                        self.interp.scope.declare_scalar(n, stored);
8577                        Ok(())
8578                    }
8579                    Op::DeclareMySyncArray(name_idx) => {
8580                        let val = self.pop();
8581                        let n = names[*name_idx as usize].as_str();
8582                        self.interp.scope.declare_atomic_array(n, val.to_list());
8583                        Ok(())
8584                    }
8585                    Op::DeclareMySyncHash(name_idx) => {
8586                        let val = self.pop();
8587                        let n = names[*name_idx as usize].as_str();
8588                        let items = val.to_list();
8589                        let mut map = IndexMap::new();
8590                        let mut i = 0usize;
8591                        while i + 1 < items.len() {
8592                            map.insert(items[i].to_string(), items[i + 1].clone());
8593                            i += 2;
8594                        }
8595                        self.interp.scope.declare_atomic_hash(n, map);
8596                        Ok(())
8597                    }
8598                    Op::DeclareOurSyncScalar(name_idx) => {
8599                        let val = self.pop();
8600                        let n = names[*name_idx as usize].as_str();
8601                        let stored = if val.is_mysync_deque_or_heap() {
8602                            val
8603                        } else {
8604                            StrykeValue::atomic(Arc::new(Mutex::new(val)))
8605                        };
8606                        self.interp.scope.declare_scalar(n, stored);
8607                        // Register the bare name (everything after `Pkg::`) in the
8608                        // tree-walker tracking sets so worker `$x` reads inside fan/pmap
8609                        // bodies (which run via `exec_block_no_scope`, not bytecode)
8610                        // rewrite to `Pkg::x` and find the shared cell.
8611                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
8612                        self.interp.english_note_lexical_scalar_pub(&bare);
8613                        self.interp.note_our_scalar_pub(&bare);
8614                        Ok(())
8615                    }
8616                    Op::DeclareOurSyncArray(name_idx) => {
8617                        let val = self.pop();
8618                        let n = names[*name_idx as usize].as_str();
8619                        self.interp.scope.declare_atomic_array(n, val.to_list());
8620                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
8621                        self.interp.english_note_lexical_scalar_pub(&bare);
8622                        self.interp.note_our_scalar_pub(&bare);
8623                        Ok(())
8624                    }
8625                    Op::DeclareOurSyncHash(name_idx) => {
8626                        let val = self.pop();
8627                        let n = names[*name_idx as usize].as_str();
8628                        let items = val.to_list();
8629                        let mut map = IndexMap::new();
8630                        let mut i = 0usize;
8631                        while i + 1 < items.len() {
8632                            map.insert(items[i].to_string(), items[i + 1].clone());
8633                            i += 2;
8634                        }
8635                        self.interp.scope.declare_atomic_hash(n, map);
8636                        let bare = n.rsplit("::").next().unwrap_or(n).to_string();
8637                        self.interp.english_note_lexical_scalar_pub(&bare);
8638                        self.interp.note_our_scalar_pub(&bare);
8639                        Ok(())
8640                    }
8641                    Op::RuntimeSubDecl(idx) => {
8642                        let rs = &self.runtime_sub_decls[*idx as usize];
8643                        let key = self.interp.qualify_sub_key(&rs.name);
8644                        let captured = self.interp.scope.capture();
8645                        let closure_env = if captured.is_empty() {
8646                            None
8647                        } else {
8648                            Some(captured)
8649                        };
8650                        let mut sub = PerlSub {
8651                            name: rs.name.clone(),
8652                            params: rs.params.clone(),
8653                            body: rs.body.clone(),
8654                            closure_env,
8655                            prototype: rs.prototype.clone(),
8656                            fib_like: None,
8657                        };
8658                        sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
8659                        self.interp.subs.insert(key, Arc::new(sub));
8660                        Ok(())
8661                    }
8662                    Op::RegisterAdvice(idx) => {
8663                        let rd = &self.runtime_advice_decls[*idx as usize];
8664                        let id = self.interp.next_intercept_id;
8665                        self.interp.next_intercept_id = id.saturating_add(1);
8666                        self.interp.intercepts.push(crate::aop::Intercept {
8667                            id,
8668                            kind: rd.kind,
8669                            pattern: rd.pattern.clone(),
8670                            body: rd.body.clone(),
8671                            body_block_idx: rd.body_block_idx,
8672                        });
8673                        Ok(())
8674                    }
8675                    Op::Tie {
8676                        target_kind,
8677                        name_idx,
8678                        argc,
8679                    } => {
8680                        let argc = *argc as usize;
8681                        let mut stack_vals = Vec::with_capacity(argc);
8682                        for _ in 0..argc {
8683                            stack_vals.push(self.pop());
8684                        }
8685                        stack_vals.reverse();
8686                        let name = names[*name_idx as usize].as_str();
8687                        let line = self.line();
8688                        self.interp
8689                            .tie_execute(*target_kind, name, stack_vals, line)
8690                            .map_err(|e| e.at_line(line))?;
8691                        Ok(())
8692                    }
8693                    Op::FormatDecl(idx) => {
8694                        let (basename, lines) = &self.format_decls[*idx as usize];
8695                        let line = self.line();
8696                        self.interp
8697                            .install_format_decl(basename.as_str(), lines, line)
8698                            .map_err(|e| e.at_line(line))?;
8699                        Ok(())
8700                    }
8701                    Op::UseOverload(idx) => {
8702                        let pairs = &self.use_overload_entries[*idx as usize];
8703                        self.interp.install_use_overload_pairs(pairs);
8704                        Ok(())
8705                    }
8706                    Op::ScalarCompoundAssign { name_idx, op: op_b } => {
8707                        let rhs = self.pop();
8708                        let n = names[*name_idx as usize].as_str();
8709                        let op = scalar_compound_op_from_byte(*op_b).ok_or_else(|| {
8710                            PerlError::runtime("ScalarCompoundAssign: invalid op byte", self.line())
8711                        })?;
8712                        let en = self.interp.english_scalar_name(n);
8713                        let val = self
8714                            .interp
8715                            .scalar_compound_assign_scalar_target(en, op, rhs)
8716                            .map_err(|e| e.at_line(self.line()))?;
8717                        self.push(val);
8718                        Ok(())
8719                    }
8720
8721                    Op::SetGlobalPhase(phase) => {
8722                        let s = match *phase {
8723                            crate::bytecode::GP_START => "START",
8724                            crate::bytecode::GP_UNITCHECK => "UNITCHECK",
8725                            crate::bytecode::GP_CHECK => "CHECK",
8726                            crate::bytecode::GP_INIT => "INIT",
8727                            crate::bytecode::GP_RUN => "RUN",
8728                            crate::bytecode::GP_END => "END",
8729                            _ => {
8730                                return Err(PerlError::runtime(
8731                                    format!("SetGlobalPhase: invalid phase byte {}", phase),
8732                                    self.line(),
8733                                ));
8734                            }
8735                        };
8736                        self.interp.global_phase = s.to_string();
8737                        Ok(())
8738                    }
8739
8740                    // ── Halt ──
8741                    Op::Halt => {
8742                        self.halt = true;
8743                        Ok(())
8744                    }
8745                    Op::EvalAstExpr(idx) => {
8746                        let expr = &self.ast_eval_exprs[*idx as usize];
8747                        let val = match self.interp.eval_expr_ctx(expr, self.interp.wantarray_kind)
8748                        {
8749                            Ok(v) => v,
8750                            Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
8751                            Err(crate::vm_helper::FlowOrError::Flow(f)) => {
8752                                return Err(PerlError::runtime(
8753                                    format!("unexpected flow control in EvalAstExpr: {:?}", f),
8754                                    self.line(),
8755                                ));
8756                            }
8757                        };
8758                        self.push(val);
8759                        Ok(())
8760                    }
8761                }
8762            })();
8763            if let (Some(prof), Some(t0)) = (&mut self.interp.profiler, op_prof_t0) {
8764                prof.on_line(&self.interp.file, line, t0.elapsed());
8765            }
8766            if let Err(e) = __op_res {
8767                if self.try_recover_from_exception(&e)? {
8768                    continue;
8769                }
8770                return Err(e);
8771            }
8772            // Blessed refcount drops enqueue from `StrykeValue::drop`; drain before the next opcode
8773            // so `$x = undef; f()` runs `DESTROY` before `f` (Perl semantics).
8774            if crate::pending_destroy::pending_destroy_vm_sync_needed() {
8775                self.interp.drain_pending_destroys(line)?;
8776            }
8777            if self.exit_main_dispatch {
8778                if let Some(v) = self.exit_main_dispatch_value.take() {
8779                    last = v;
8780                }
8781                break;
8782            }
8783            if self.halt {
8784                break;
8785            }
8786        }
8787
8788        if !self.stack.is_empty() {
8789            last = self.stack.last().cloned().unwrap_or(StrykeValue::UNDEF);
8790            // Drain iterators left on the stack so side effects fire
8791            // (e.g. `pmaps { system(...) } @list` with no consumer).
8792            if last.is_iterator() {
8793                let iter = last.clone().into_iterator();
8794                while iter.next_item().is_some() {}
8795                last = StrykeValue::UNDEF;
8796            }
8797        }
8798
8799        Ok(last)
8800    }
8801
8802    /// Called from Cranelift (`stryke_jit_call_sub`) to run a compiled sub by bytecode IP with `i64` args.
8803    pub(crate) fn jit_trampoline_run_sub(
8804        &mut self,
8805        entry_ip: usize,
8806        want: WantarrayCtx,
8807        args: &[i64],
8808    ) -> PerlResult<StrykeValue> {
8809        let saved_wa = self.interp.wantarray_kind;
8810        for a in args {
8811            self.push(StrykeValue::integer(*a));
8812        }
8813        let stack_base = self.stack.len() - args.len();
8814        let mut sub_prof_t0 = None;
8815        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
8816            sub_prof_t0 = self.interp.profiler.is_some().then(std::time::Instant::now);
8817            let nm_owned = self.names[nidx as usize].to_string();
8818            if let Some(p) = &mut self.interp.profiler {
8819                p.enter_sub(nm_owned.as_str());
8820            }
8821            self.interp.debugger_enter_sub(nm_owned.as_str());
8822        }
8823        self.call_stack.push(CallFrame {
8824            return_ip: 0,
8825            stack_base,
8826            scope_depth: self.interp.scope.depth(),
8827            saved_wantarray: saved_wa,
8828            jit_trampoline_return: true,
8829            block_region: false,
8830            sub_profiler_start: sub_prof_t0,
8831        });
8832        self.interp.wantarray_kind = want;
8833        self.interp.scope_push_hook();
8834        if let Some(nidx) = self.sub_entry_name_idx(entry_ip) {
8835            let nm = self.names[nidx as usize].as_str();
8836            if let Some(sub) = self.interp.subs.get(nm).cloned() {
8837                if let Some(ref env) = sub.closure_env {
8838                    self.interp.scope.restore_capture(env);
8839                }
8840            }
8841        }
8842        self.ip = entry_ip;
8843        self.jit_trampoline_out = None;
8844        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_add(1);
8845        let mut op_count = 0u64;
8846        let last = StrykeValue::UNDEF;
8847        let r = self.run_main_dispatch_loop(last, &mut op_count, true);
8848        self.jit_trampoline_depth = self.jit_trampoline_depth.saturating_sub(1);
8849        r?;
8850        self.jit_trampoline_out.take().ok_or_else(|| {
8851            PerlError::runtime("JIT trampoline: subroutine did not return", self.line())
8852        })
8853    }
8854
8855    #[inline]
8856    fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
8857        self.sub_entry_by_name.get(&name_idx).copied()
8858    }
8859
8860    /// Name pool index for a compiled sub entry IP (for closure env + JIT trampoline).
8861    fn sub_entry_name_idx(&self, entry_ip: usize) -> Option<u16> {
8862        for &(n, ip, _) in &self.sub_entries {
8863            if ip == entry_ip {
8864                return Some(n);
8865            }
8866        }
8867        None
8868    }
8869
8870    fn exec_builtin(&mut self, id: u16, args: Vec<StrykeValue>) -> PerlResult<StrykeValue> {
8871        let line = self.line();
8872        let bid = BuiltinId::from_u16(id);
8873        match bid {
8874            Some(BuiltinId::Length) => {
8875                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8876                Ok(if let Some(a) = val.as_array_vec() {
8877                    StrykeValue::integer(a.len() as i64)
8878                } else if let Some(h) = val.as_hash_map() {
8879                    StrykeValue::integer(h.len() as i64)
8880                } else if let Some(b) = val.as_bytes_arc() {
8881                    // Raw byte buffer: always byte count, regardless of utf8 pragma.
8882                    StrykeValue::integer(b.len() as i64)
8883                } else {
8884                    let s = val.to_string();
8885                    let n = if self.interp.utf8_pragma {
8886                        s.chars().count()
8887                    } else {
8888                        s.len()
8889                    };
8890                    StrykeValue::integer(n as i64)
8891                })
8892            }
8893            Some(BuiltinId::Defined) => {
8894                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8895                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
8896            }
8897            Some(BuiltinId::Abs) => {
8898                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8899                Ok(StrykeValue::float(val.to_number().abs()))
8900            }
8901            Some(BuiltinId::Int) => {
8902                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8903                Ok(StrykeValue::integer(val.to_number() as i64))
8904            }
8905            Some(BuiltinId::Sqrt) => {
8906                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8907                Ok(StrykeValue::float(val.to_number().sqrt()))
8908            }
8909            Some(BuiltinId::Sin) => {
8910                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8911                Ok(StrykeValue::float(val.to_number().sin()))
8912            }
8913            Some(BuiltinId::Cos) => {
8914                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8915                Ok(StrykeValue::float(val.to_number().cos()))
8916            }
8917            Some(BuiltinId::Atan2) => {
8918                let mut it = args.into_iter();
8919                let y = it.next().unwrap_or(StrykeValue::UNDEF);
8920                let x = it.next().unwrap_or(StrykeValue::UNDEF);
8921                Ok(StrykeValue::float(y.to_number().atan2(x.to_number())))
8922            }
8923            Some(BuiltinId::Exp) => {
8924                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8925                Ok(StrykeValue::float(val.to_number().exp()))
8926            }
8927            Some(BuiltinId::Log) => {
8928                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8929                Ok(StrykeValue::float(val.to_number().ln()))
8930            }
8931            Some(BuiltinId::Rand) => {
8932                let upper = match args.len() {
8933                    0 => 1.0,
8934                    _ => args[0].to_number(),
8935                };
8936                Ok(StrykeValue::float(self.interp.perl_rand(upper)))
8937            }
8938            Some(BuiltinId::Srand) => {
8939                let seed = match args.len() {
8940                    0 => None,
8941                    _ => Some(args[0].to_number()),
8942                };
8943                Ok(StrykeValue::integer(self.interp.perl_srand(seed)))
8944            }
8945            Some(BuiltinId::Crypt) => {
8946                let mut it = args.into_iter();
8947                let p = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
8948                let salt = it.next().unwrap_or(StrykeValue::UNDEF).to_string();
8949                Ok(StrykeValue::string(crate::crypt_util::perl_crypt(
8950                    &p, &salt,
8951                )))
8952            }
8953            Some(BuiltinId::Fc) => {
8954                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8955                Ok(StrykeValue::string(default_case_fold_str(&s.to_string())))
8956            }
8957            Some(BuiltinId::Pos) => {
8958                let key = if args.is_empty() {
8959                    "_".to_string()
8960                } else {
8961                    args[0].to_string()
8962                };
8963                Ok(self
8964                    .interp
8965                    .regex_pos
8966                    .get(&key)
8967                    .copied()
8968                    .flatten()
8969                    .map(|n| StrykeValue::integer(n as i64))
8970                    .unwrap_or(StrykeValue::UNDEF))
8971            }
8972            Some(BuiltinId::Study) => {
8973                let s = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
8974                Ok(VMHelper::study_return_value(&s.to_string()))
8975            }
8976            Some(BuiltinId::Chr) => {
8977                let n = args
8978                    .into_iter()
8979                    .next()
8980                    .unwrap_or(StrykeValue::UNDEF)
8981                    .to_int() as u32;
8982                Ok(StrykeValue::string(
8983                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
8984                ))
8985            }
8986            Some(BuiltinId::Ord) => {
8987                let s = args
8988                    .into_iter()
8989                    .next()
8990                    .unwrap_or(StrykeValue::UNDEF)
8991                    .to_string();
8992                Ok(StrykeValue::integer(
8993                    s.chars().next().map(|c| c as i64).unwrap_or(0),
8994                ))
8995            }
8996            Some(BuiltinId::Hex) => {
8997                let s = args
8998                    .into_iter()
8999                    .next()
9000                    .unwrap_or(StrykeValue::UNDEF)
9001                    .to_string();
9002                let clean = s.trim().trim_start_matches("0x").trim_start_matches("0X");
9003                Ok(StrykeValue::integer(
9004                    i64::from_str_radix(clean, 16).unwrap_or(0),
9005                ))
9006            }
9007            Some(BuiltinId::Oct) => {
9008                let s = args
9009                    .into_iter()
9010                    .next()
9011                    .unwrap_or(StrykeValue::UNDEF)
9012                    .to_string();
9013                let s = s.trim();
9014                let n = if s.starts_with("0x") || s.starts_with("0X") {
9015                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
9016                } else if s.starts_with("0b") || s.starts_with("0B") {
9017                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
9018                } else if s.starts_with("0o") || s.starts_with("0O") {
9019                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
9020                } else {
9021                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
9022                };
9023                Ok(StrykeValue::integer(n))
9024            }
9025            Some(BuiltinId::Uc) => {
9026                let s = args
9027                    .into_iter()
9028                    .next()
9029                    .unwrap_or(StrykeValue::UNDEF)
9030                    .to_string();
9031                Ok(StrykeValue::string(s.to_uppercase()))
9032            }
9033            Some(BuiltinId::Lc) => {
9034                let s = args
9035                    .into_iter()
9036                    .next()
9037                    .unwrap_or(StrykeValue::UNDEF)
9038                    .to_string();
9039                Ok(StrykeValue::string(s.to_lowercase()))
9040            }
9041            Some(BuiltinId::Ref) => {
9042                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9043                Ok(val.ref_type())
9044            }
9045            Some(BuiltinId::Scalar) => {
9046                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9047                Ok(val.scalar_context())
9048            }
9049            Some(BuiltinId::Join) => {
9050                let mut iter = args.into_iter();
9051                let sep = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
9052                let list = iter.next().unwrap_or(StrykeValue::UNDEF).to_list();
9053                let mut strs = Vec::with_capacity(list.len());
9054                for v in list {
9055                    let s = match self.interp.stringify_value(v, line) {
9056                        Ok(s) => s,
9057                        Err(FlowOrError::Error(e)) => return Err(e),
9058                        Err(FlowOrError::Flow(_)) => {
9059                            return Err(PerlError::runtime("join: unexpected control flow", line));
9060                        }
9061                    };
9062                    strs.push(s);
9063                }
9064                Ok(StrykeValue::string(strs.join(&sep)))
9065            }
9066            Some(BuiltinId::Split) => {
9067                let mut iter = args.into_iter();
9068                let pat_val = iter.next().unwrap_or(StrykeValue::string(" ".into()));
9069                // Prefer the regex source over the Display form: `qr//`'s Display is
9070                // `(?:)` (matches everywhere), which is NOT the same as Perl's empty-
9071                // pattern semantics ("split between every character"). Pulling the
9072                // source out via `regex_src_and_flags` lets us treat `//` as truly
9073                // empty so the char-split branch fires.
9074                let pat = pat_val
9075                    .regex_src_and_flags()
9076                    .map(|(s, _)| s)
9077                    .unwrap_or_else(|| pat_val.to_string());
9078                let s = iter.next().unwrap_or(StrykeValue::UNDEF).to_string();
9079                // Perl 5: splitting the empty string yields the empty list for any
9080                // pattern / limit (regex `split` on `""` would otherwise leave one field).
9081                if s.is_empty() {
9082                    return Ok(StrykeValue::array(vec![]));
9083                }
9084                // Perl LIMIT semantics:
9085                //   omitted / 0  → no truncation, strip trailing empties.
9086                //   > 0          → at most LIMIT fields, keep empties up to limit.
9087                //   < 0          → no truncation, keep all empties.
9088                let lim_signed: Option<i64> = iter.next().map(|v| v.to_int());
9089
9090                let mut parts: Vec<String> = if pat.is_empty() {
9091                    // Empty pattern → "split between every character" (Perl). The
9092                    // regex engine would also match at the boundaries, producing
9093                    // spurious empties; `s.chars()` is the right primitive.
9094                    let chars: Vec<String> = s.chars().map(|c| c.to_string()).collect();
9095                    match lim_signed {
9096                        // LIMIT > 0 (Perl):
9097                        //   n < |chars|        → first n-1 chars then the tail in one field
9098                        //                        (`split //, "abcde", 3` → ("a","b","cde")).
9099                        //   n == |chars|       → chars exactly, no trailing empty.
9100                        //   n > |chars|        → chars + "" (Perl emits the end-of-string
9101                        //                        match as a final empty when LIMIT permits).
9102                        Some(l) if l > 0 => {
9103                            let n = l as usize;
9104                            if n < chars.len() {
9105                                let mut head: Vec<String> =
9106                                    chars.iter().take(n.saturating_sub(1)).cloned().collect();
9107                                let tail: String = s.chars().skip(n.saturating_sub(1)).collect();
9108                                head.push(tail);
9109                                head
9110                            } else if n == chars.len() {
9111                                chars
9112                            } else {
9113                                let mut v = chars;
9114                                v.push(String::new());
9115                                v
9116                            }
9117                        }
9118                        // LIMIT < 0 → chars + trailing empty.
9119                        Some(l) if l < 0 => {
9120                            let mut v = chars;
9121                            v.push(String::new());
9122                            v
9123                        }
9124                        // No limit / 0 → just the chars; the trailing-empty strip
9125                        // below is a no-op (`chars()` never emits one).
9126                        _ => chars,
9127                    }
9128                } else {
9129                    let re =
9130                        regex::Regex::new(&pat).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
9131                    match lim_signed {
9132                        Some(l) if l > 0 => {
9133                            re.splitn(&s, l as usize).map(|p| p.to_string()).collect()
9134                        }
9135                        _ => re.split(&s).map(|p| p.to_string()).collect(),
9136                    }
9137                };
9138
9139                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted or
9140                // zero. Positive LIMIT keeps trailing empties (capped at LIMIT).
9141                // Negative LIMIT also keeps them.
9142                let strip_trailing = matches!(lim_signed, None | Some(0));
9143                if strip_trailing {
9144                    while parts.last().is_some_and(|p| p.is_empty()) {
9145                        parts.pop();
9146                    }
9147                }
9148
9149                Ok(StrykeValue::array(
9150                    parts.into_iter().map(StrykeValue::string).collect(),
9151                ))
9152            }
9153            Some(BuiltinId::Sprintf) => {
9154                // sprintf arg list is Perl list context; flatten ranges / arrays / reverse
9155                // output into individual format arguments (same splatting as printf).
9156                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
9157                for a in args.into_iter() {
9158                    if let Some(items) = a.as_array_vec() {
9159                        flat.extend(items);
9160                    } else {
9161                        flat.push(a);
9162                    }
9163                }
9164                let args = flat;
9165                if args.is_empty() {
9166                    return Ok(StrykeValue::string(String::new()));
9167                }
9168                let fmt = args[0].to_string();
9169                let rest = &args[1..];
9170                match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
9171                    Ok(s) => Ok(StrykeValue::string(s)),
9172                    Err(FlowOrError::Error(e)) => Err(e),
9173                    Err(FlowOrError::Flow(_)) => {
9174                        Err(PerlError::runtime("sprintf: unexpected control flow", line))
9175                    }
9176                }
9177            }
9178            Some(BuiltinId::Reverse) => {
9179                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9180                Ok(if let Some(mut a) = val.as_array_vec() {
9181                    a.reverse();
9182                    StrykeValue::array(a)
9183                } else if let Some(s) = val.as_str() {
9184                    StrykeValue::string(s.chars().rev().collect())
9185                } else {
9186                    StrykeValue::string(val.to_string().chars().rev().collect())
9187                })
9188            }
9189            Some(BuiltinId::Die) => {
9190                let mut msg = String::new();
9191                for a in &args {
9192                    msg.push_str(&a.to_string());
9193                }
9194                if msg.is_empty() {
9195                    msg = "Died".to_string();
9196                }
9197                if !msg.ends_with('\n') {
9198                    msg.push_str(&self.interp.die_warn_at_suffix(line));
9199                    msg.push('\n');
9200                }
9201                self.interp.fire_pseudosig_die(&msg, line)?;
9202                Err(PerlError::die(msg, line))
9203            }
9204            Some(BuiltinId::Warn) => {
9205                let mut msg = String::new();
9206                for a in &args {
9207                    msg.push_str(&a.to_string());
9208                }
9209                if msg.is_empty() {
9210                    msg = "Warning: something's wrong".to_string();
9211                }
9212                if !msg.ends_with('\n') {
9213                    msg.push_str(&self.interp.die_warn_at_suffix(line));
9214                    msg.push('\n');
9215                }
9216                self.interp.fire_pseudosig_warn(&msg, line)?;
9217                Ok(StrykeValue::integer(1))
9218            }
9219            Some(BuiltinId::Exit) => {
9220                let code = args
9221                    .into_iter()
9222                    .next()
9223                    .map(|v| v.to_int() as i32)
9224                    .unwrap_or(0);
9225                Err(PerlError::new(
9226                    ErrorKind::Exit(code),
9227                    "",
9228                    line,
9229                    &self.interp.file,
9230                ))
9231            }
9232            Some(BuiltinId::System) => {
9233                let cmd = args
9234                    .iter()
9235                    .map(|a| a.to_string())
9236                    .collect::<Vec<_>>()
9237                    .join(" ");
9238                let status = std::process::Command::new("sh")
9239                    .arg("-c")
9240                    .arg(&cmd)
9241                    .status();
9242                match status {
9243                    Ok(s) => {
9244                        self.interp.record_child_exit_status(s);
9245                        Ok(StrykeValue::integer(s.code().unwrap_or(-1) as i64))
9246                    }
9247                    Err(e) => {
9248                        self.interp.errno = e.to_string();
9249                        self.interp.child_exit_status = -1;
9250                        Ok(StrykeValue::integer(-1))
9251                    }
9252                }
9253            }
9254            Some(BuiltinId::Ssh) => self.interp.ssh_builtin_execute(&args),
9255            Some(BuiltinId::Chomp) => {
9256                // Chomp modifies the variable in-place — but in CallBuiltin we get the value, not a reference.
9257                // Return the number of chars removed (like Perl).
9258                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9259                let s = val.to_string();
9260                Ok(StrykeValue::integer(if s.ends_with('\n') { 1 } else { 0 }))
9261            }
9262            Some(BuiltinId::Chop) => {
9263                let val = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9264                let s = val.to_string();
9265                Ok(s.chars()
9266                    .last()
9267                    .map(|c| StrykeValue::string(c.to_string()))
9268                    .unwrap_or(StrykeValue::UNDEF))
9269            }
9270            Some(BuiltinId::Substr) => {
9271                let s = args.first().map(|v| v.to_string()).unwrap_or_default();
9272                let slen = s.len() as i64;
9273                let off = args.get(1).map(|v| v.to_int()).unwrap_or(0);
9274                let start = if off < 0 { (slen + off).max(0) } else { off }.min(slen) as usize;
9275
9276                let end = if let Some(l_val) = args.get(2) {
9277                    let l = l_val.to_int();
9278                    if l < 0 {
9279                        (slen + l).max(start as i64)
9280                    } else {
9281                        (start as i64 + l).min(slen)
9282                    }
9283                } else {
9284                    slen
9285                } as usize;
9286
9287                Ok(StrykeValue::string(
9288                    s.get(start..end).unwrap_or("").to_string(),
9289                ))
9290            }
9291            Some(BuiltinId::Index) => {
9292                let s = args.first().map(|v| v.to_string()).unwrap_or_default();
9293                let sub = args.get(1).map(|v| v.to_string()).unwrap_or_default();
9294                let pos = args.get(2).map(|v| v.to_int() as usize).unwrap_or(0);
9295                Ok(StrykeValue::integer(
9296                    s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1),
9297                ))
9298            }
9299            Some(BuiltinId::Rindex) => {
9300                let s = args.first().map(|v| v.to_string()).unwrap_or_default();
9301                let sub = args.get(1).map(|v| v.to_string()).unwrap_or_default();
9302                let end = args
9303                    .get(2)
9304                    .map(|v| v.to_int() as usize + sub.len())
9305                    .unwrap_or(s.len());
9306                Ok(StrykeValue::integer(
9307                    s[..end.min(s.len())]
9308                        .rfind(&sub)
9309                        .map(|i| i as i64)
9310                        .unwrap_or(-1),
9311                ))
9312            }
9313            Some(BuiltinId::Ucfirst) => {
9314                let s = args
9315                    .into_iter()
9316                    .next()
9317                    .unwrap_or(StrykeValue::UNDEF)
9318                    .to_string();
9319                let mut chars = s.chars();
9320                let result = match chars.next() {
9321                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
9322                    None => String::new(),
9323                };
9324                Ok(StrykeValue::string(result))
9325            }
9326            Some(BuiltinId::Lcfirst) => {
9327                let s = args
9328                    .into_iter()
9329                    .next()
9330                    .unwrap_or(StrykeValue::UNDEF)
9331                    .to_string();
9332                let mut chars = s.chars();
9333                let result = match chars.next() {
9334                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
9335                    None => String::new(),
9336                };
9337                Ok(StrykeValue::string(result))
9338            }
9339            Some(BuiltinId::Splice) => self.interp.splice_builtin_execute(&args, line),
9340            Some(BuiltinId::Unshift) => self.interp.unshift_builtin_execute(&args, line),
9341            Some(BuiltinId::Printf) => {
9342                // Flatten list-context operands (ranges, arrays, `reverse`, …) so format
9343                // placeholders line up with individual values instead of an array reference.
9344                let mut flat: Vec<StrykeValue> = Vec::with_capacity(args.len());
9345                for a in args.into_iter() {
9346                    if let Some(items) = a.as_array_vec() {
9347                        flat.extend(items);
9348                    } else {
9349                        flat.push(a);
9350                    }
9351                }
9352                let args = flat;
9353                let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
9354                    let s = match self
9355                        .interp
9356                        .stringify_value(self.interp.scope.get_scalar("_").clone(), line)
9357                    {
9358                        Ok(s) => s,
9359                        Err(FlowOrError::Error(e)) => return Err(e),
9360                        Err(FlowOrError::Flow(_)) => {
9361                            return Err(PerlError::runtime(
9362                                "printf: unexpected control flow",
9363                                line,
9364                            ));
9365                        }
9366                    };
9367                    (s, &[])
9368                } else {
9369                    (args[0].to_string(), &args[1..])
9370                };
9371                let out = match self.interp.perl_sprintf_stringify(&fmt, rest, line) {
9372                    Ok(s) => s,
9373                    Err(FlowOrError::Error(e)) => return Err(e),
9374                    Err(FlowOrError::Flow(_)) => {
9375                        return Err(PerlError::runtime("printf: unexpected control flow", line));
9376                    }
9377                };
9378                print!("{}", out);
9379                if self.interp.output_autoflush {
9380                    let _ = io::stdout().flush();
9381                }
9382                Ok(StrykeValue::integer(1))
9383            }
9384            Some(BuiltinId::Open) => {
9385                if args.len() < 2 {
9386                    return Err(PerlError::runtime(
9387                        "open requires at least 2 arguments",
9388                        line,
9389                    ));
9390                }
9391                let handle_name = args[0].to_string();
9392                let mode_s = args[1].to_string();
9393                let file_opt = args.get(2).map(|v| v.to_string());
9394                self.interp
9395                    .open_builtin_execute(handle_name, mode_s, file_opt, line)
9396            }
9397            Some(BuiltinId::Close) => {
9398                let name = args
9399                    .into_iter()
9400                    .next()
9401                    .unwrap_or(StrykeValue::UNDEF)
9402                    .to_string();
9403                self.interp.close_builtin_execute(name)
9404            }
9405            Some(BuiltinId::Eof) => self.interp.eof_builtin_execute(&args, line),
9406            Some(BuiltinId::ReadLine) => {
9407                let h = if args.is_empty() {
9408                    None
9409                } else {
9410                    Some(args[0].to_string())
9411                };
9412                self.interp.readline_builtin_execute(h.as_deref())
9413            }
9414            Some(BuiltinId::ReadLineList) => {
9415                let h = if args.is_empty() {
9416                    None
9417                } else {
9418                    Some(args[0].to_string())
9419                };
9420                self.interp.readline_builtin_execute_list(h.as_deref())
9421            }
9422            Some(BuiltinId::Exec) => {
9423                let cmd = args
9424                    .iter()
9425                    .map(|a| a.to_string())
9426                    .collect::<Vec<_>>()
9427                    .join(" ");
9428                let status = std::process::Command::new("sh")
9429                    .arg("-c")
9430                    .arg(&cmd)
9431                    .status();
9432                std::process::exit(status.map(|s| s.code().unwrap_or(-1)).unwrap_or(-1));
9433            }
9434            Some(BuiltinId::Chdir) => {
9435                let path = args
9436                    .into_iter()
9437                    .next()
9438                    .unwrap_or(StrykeValue::UNDEF)
9439                    .to_string();
9440                if std::env::set_current_dir(&path).is_ok() {
9441                    if let Ok(c) = std::env::current_dir() {
9442                        self.interp.stryke_pwd =
9443                            std::fs::canonicalize(&c).unwrap_or(c);
9444                    }
9445                    Ok(StrykeValue::integer(1))
9446                } else {
9447                    Ok(StrykeValue::integer(0))
9448                }
9449            }
9450            Some(BuiltinId::Mkdir) => {
9451                let path = args.first().map(|v| v.to_string()).unwrap_or_default();
9452                let path = self.interp.resolve_stryke_path_string(&path);
9453                Ok(StrykeValue::integer(
9454                    if std::fs::create_dir(&path).is_ok() {
9455                        1
9456                    } else {
9457                        0
9458                    },
9459                ))
9460            }
9461            Some(BuiltinId::Unlink) => {
9462                let mut count = 0i64;
9463                for a in &args {
9464                    let p = self.interp.resolve_stryke_path_string(&a.to_string());
9465                    if std::fs::remove_file(&p).is_ok() {
9466                        count += 1;
9467                    }
9468                }
9469                Ok(StrykeValue::integer(count))
9470            }
9471            Some(BuiltinId::Rmdir) => self.interp.builtin_rmdir_execute(&args, line),
9472            Some(BuiltinId::Utime) => self.interp.builtin_utime_execute(&args, line),
9473            Some(BuiltinId::Umask) => self.interp.builtin_umask_execute(&args, line),
9474            Some(BuiltinId::Getcwd) => self.interp.builtin_getcwd_execute(&args, line),
9475            Some(BuiltinId::Pipe) => self.interp.builtin_pipe_execute(&args, line),
9476            Some(BuiltinId::Rename) => {
9477                let old = self
9478                    .interp
9479                    .resolve_stryke_path_string(&args.first().map(|v| v.to_string()).unwrap_or_default());
9480                let new = self
9481                    .interp
9482                    .resolve_stryke_path_string(&args.get(1).map(|v| v.to_string()).unwrap_or_default());
9483                Ok(crate::perl_fs::rename_paths(&old, &new))
9484            }
9485            Some(BuiltinId::Chmod) => {
9486                if args.is_empty() {
9487                    return Ok(StrykeValue::integer(0));
9488                }
9489                let mode = args[0].to_int();
9490                let paths: Vec<String> = args
9491                    .iter()
9492                    .skip(1)
9493                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9494                    .collect();
9495                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
9496                    &paths, mode,
9497                )))
9498            }
9499            Some(BuiltinId::Chown) => {
9500                if args.len() < 3 {
9501                    return Ok(StrykeValue::integer(0));
9502                }
9503                let uid = args[0].to_int();
9504                let gid = args[1].to_int();
9505                let paths: Vec<String> = args
9506                    .iter()
9507                    .skip(2)
9508                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9509                    .collect();
9510                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
9511                    &paths, uid, gid,
9512                )))
9513            }
9514            Some(BuiltinId::Stat) => {
9515                let path = self
9516                    .interp
9517                    .resolve_stryke_path_string(&args.first().map(|v| v.to_string()).unwrap_or_default());
9518                Ok(crate::perl_fs::stat_path(&path, false))
9519            }
9520            Some(BuiltinId::Lstat) => {
9521                let path = self
9522                    .interp
9523                    .resolve_stryke_path_string(&args.first().map(|v| v.to_string()).unwrap_or_default());
9524                Ok(crate::perl_fs::stat_path(&path, true))
9525            }
9526            Some(BuiltinId::Link) => {
9527                let old = self
9528                    .interp
9529                    .resolve_stryke_path_string(&args.first().map(|v| v.to_string()).unwrap_or_default());
9530                let new = self
9531                    .interp
9532                    .resolve_stryke_path_string(&args.get(1).map(|v| v.to_string()).unwrap_or_default());
9533                Ok(crate::perl_fs::link_hard(&old, &new))
9534            }
9535            Some(BuiltinId::Symlink) => {
9536                let old = args.first().map(|v| v.to_string()).unwrap_or_default();
9537                let new = self
9538                    .interp
9539                    .resolve_stryke_path_string(&args.get(1).map(|v| v.to_string()).unwrap_or_default());
9540                Ok(crate::perl_fs::link_sym(&old, &new))
9541            }
9542            Some(BuiltinId::Readlink) => {
9543                let path = self
9544                    .interp
9545                    .resolve_stryke_path_string(&args.first().map(|v| v.to_string()).unwrap_or_default());
9546                Ok(crate::perl_fs::read_link(&path))
9547            }
9548            Some(BuiltinId::Glob) => {
9549                // Pass user patterns through verbatim: zsh::glob runs from OS cwd,
9550                // which `chdir` keeps in sync with `stryke_pwd`. Absolutising the
9551                // pattern up front would turn relative-pattern results into
9552                // absolute paths (breaking `glob("**(/)")` → "sub" contract,
9553                // pinned in tests/suite/glob_zsh_qualifiers.rs).
9554                let pats: Vec<String> = args.iter().map(|v| v.to_string()).collect();
9555                Ok(crate::perl_fs::glob_patterns(&pats))
9556            }
9557            Some(BuiltinId::Files) => {
9558                let dir = if args.is_empty() {
9559                    self.interp.resolve_stryke_path_string(".")
9560                } else {
9561                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9562                };
9563                Ok(crate::perl_fs::list_files(&dir))
9564            }
9565            Some(BuiltinId::Filesf) => {
9566                let dir = if args.is_empty() {
9567                    self.interp.resolve_stryke_path_string(".")
9568                } else {
9569                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9570                };
9571                Ok(crate::perl_fs::list_filesf(&dir))
9572            }
9573            Some(BuiltinId::FilesfRecursive) => {
9574                let dir = if args.is_empty() {
9575                    self.interp.resolve_stryke_path_string(".")
9576                } else {
9577                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9578                };
9579                Ok(StrykeValue::iterator(std::sync::Arc::new(
9580                    crate::value::FsWalkIterator::new(&dir, true),
9581                )))
9582            }
9583            Some(BuiltinId::Dirs) => {
9584                let dir = if args.is_empty() {
9585                    self.interp.resolve_stryke_path_string(".")
9586                } else {
9587                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9588                };
9589                Ok(crate::perl_fs::list_dirs(&dir))
9590            }
9591            Some(BuiltinId::DirsRecursive) => {
9592                let dir = if args.is_empty() {
9593                    self.interp.resolve_stryke_path_string(".")
9594                } else {
9595                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9596                };
9597                Ok(StrykeValue::iterator(std::sync::Arc::new(
9598                    crate::value::FsWalkIterator::new(&dir, false),
9599                )))
9600            }
9601            Some(BuiltinId::SymLinks) => {
9602                let dir = if args.is_empty() {
9603                    self.interp.resolve_stryke_path_string(".")
9604                } else {
9605                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9606                };
9607                Ok(crate::perl_fs::list_sym_links(&dir))
9608            }
9609            Some(BuiltinId::Sockets) => {
9610                let dir = if args.is_empty() {
9611                    self.interp.resolve_stryke_path_string(".")
9612                } else {
9613                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9614                };
9615                Ok(crate::perl_fs::list_sockets(&dir))
9616            }
9617            Some(BuiltinId::Pipes) => {
9618                let dir = if args.is_empty() {
9619                    self.interp.resolve_stryke_path_string(".")
9620                } else {
9621                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9622                };
9623                Ok(crate::perl_fs::list_pipes(&dir))
9624            }
9625            Some(BuiltinId::BlockDevices) => {
9626                let dir = if args.is_empty() {
9627                    self.interp.resolve_stryke_path_string(".")
9628                } else {
9629                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9630                };
9631                Ok(crate::perl_fs::list_block_devices(&dir))
9632            }
9633            Some(BuiltinId::CharDevices) => {
9634                let dir = if args.is_empty() {
9635                    self.interp.resolve_stryke_path_string(".")
9636                } else {
9637                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9638                };
9639                Ok(crate::perl_fs::list_char_devices(&dir))
9640            }
9641            Some(BuiltinId::Executables) => {
9642                let dir = if args.is_empty() {
9643                    self.interp.resolve_stryke_path_string(".")
9644                } else {
9645                    self.interp.resolve_stryke_path_string(&args[0].to_string())
9646                };
9647                Ok(crate::perl_fs::list_executables(&dir))
9648            }
9649            Some(BuiltinId::GlobPar) => {
9650                let pats: Vec<String> = args
9651                    .iter()
9652                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9653                    .collect();
9654                Ok(crate::perl_fs::glob_par_patterns(&pats))
9655            }
9656            Some(BuiltinId::GlobParProgress) => {
9657                let progress = args.last().map(|v| v.is_true()).unwrap_or(false);
9658                let pats: Vec<String> = args[..args.len().saturating_sub(1)]
9659                    .iter()
9660                    .map(|v| self.interp.resolve_stryke_path_string(&v.to_string()))
9661                    .collect();
9662                Ok(crate::perl_fs::glob_par_patterns_with_progress(
9663                    &pats, progress,
9664                ))
9665            }
9666            Some(BuiltinId::ParSed) => self.interp.builtin_par_sed(&args, line, false),
9667            Some(BuiltinId::ParSedProgress) => self.interp.builtin_par_sed(&args, line, true),
9668            Some(BuiltinId::Opendir) => {
9669                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9670                let path = args.get(1).map(|v| v.to_string()).unwrap_or_default();
9671                Ok(self.interp.opendir_handle(&handle, &path))
9672            }
9673            Some(BuiltinId::Readdir) => {
9674                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9675                Ok(self.interp.readdir_handle(&handle))
9676            }
9677            Some(BuiltinId::ReaddirList) => {
9678                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9679                Ok(self.interp.readdir_handle_list(&handle))
9680            }
9681            Some(BuiltinId::Closedir) => {
9682                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9683                Ok(self.interp.closedir_handle(&handle))
9684            }
9685            Some(BuiltinId::Rewinddir) => {
9686                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9687                Ok(self.interp.rewinddir_handle(&handle))
9688            }
9689            Some(BuiltinId::Telldir) => {
9690                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9691                Ok(self.interp.telldir_handle(&handle))
9692            }
9693            Some(BuiltinId::Seekdir) => {
9694                let handle = args.first().map(|v| v.to_string()).unwrap_or_default();
9695                let pos = args.get(1).map(|v| v.to_int().max(0) as usize).unwrap_or(0);
9696                Ok(self.interp.seekdir_handle(&handle, pos))
9697            }
9698            Some(BuiltinId::Slurp) => {
9699                let path = args
9700                    .into_iter()
9701                    .next()
9702                    .unwrap_or(StrykeValue::UNDEF)
9703                    .to_string();
9704                let path = self.interp.resolve_stryke_path_string(&path);
9705                crate::perl_fs::read_file_text_or_glob(&path)
9706                    .map(StrykeValue::string)
9707                    .map_err(|e| PerlError::runtime(format!("slurp: {}", e), line))
9708            }
9709            Some(BuiltinId::Capture) => {
9710                let cmd = args
9711                    .into_iter()
9712                    .next()
9713                    .unwrap_or(StrykeValue::UNDEF)
9714                    .to_string();
9715                crate::capture::run_capture(self.interp, &cmd, line)
9716            }
9717            Some(BuiltinId::Ppool) => {
9718                let n = args
9719                    .first()
9720                    .map(|v| v.to_int().max(0) as usize)
9721                    .unwrap_or(1);
9722                crate::ppool::create_pool(n)
9723            }
9724            Some(BuiltinId::Wantarray) => Ok(match self.interp.wantarray_kind {
9725                crate::vm_helper::WantarrayCtx::Void => StrykeValue::UNDEF,
9726                crate::vm_helper::WantarrayCtx::Scalar => StrykeValue::integer(0),
9727                crate::vm_helper::WantarrayCtx::List => StrykeValue::integer(1),
9728            }),
9729            Some(BuiltinId::FetchUrl) => {
9730                let url = args
9731                    .into_iter()
9732                    .next()
9733                    .unwrap_or(StrykeValue::UNDEF)
9734                    .to_string();
9735                ureq::get(&url)
9736                    .call()
9737                    .map_err(|e| PerlError::runtime(format!("fetch_url: {}", e), line))
9738                    .and_then(|r| {
9739                        r.into_string()
9740                            .map(StrykeValue::string)
9741                            .map_err(|e| PerlError::runtime(format!("fetch_url: {}", e), line))
9742                    })
9743            }
9744            Some(BuiltinId::Pchannel) => {
9745                if args.is_empty() {
9746                    Ok(crate::pchannel::create_pair())
9747                } else if args.len() == 1 {
9748                    let n = args[0].to_int().max(1) as usize;
9749                    Ok(crate::pchannel::create_bounded_pair(n))
9750                } else {
9751                    Err(PerlError::runtime(
9752                        "pchannel() takes 0 or 1 arguments (capacity)",
9753                        line,
9754                    ))
9755                }
9756            }
9757            Some(BuiltinId::Pselect) => crate::pchannel::pselect_recv(&args, line),
9758            Some(BuiltinId::DequeNew) => {
9759                if !args.is_empty() {
9760                    return Err(PerlError::runtime("deque() takes no arguments", line));
9761                }
9762                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
9763            }
9764            Some(BuiltinId::HeapNew) => {
9765                if args.len() != 1 {
9766                    return Err(PerlError::runtime(
9767                        "heap() expects one comparator sub",
9768                        line,
9769                    ));
9770                }
9771                let a0 = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9772                if let Some(sub) = a0.as_code_ref() {
9773                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
9774                        items: Vec::new(),
9775                        cmp: Arc::clone(&sub),
9776                    }))))
9777                } else {
9778                    Err(PerlError::runtime("heap() requires a code reference", line))
9779                }
9780            }
9781            Some(BuiltinId::BarrierNew) => {
9782                let n = args
9783                    .first()
9784                    .map(|v| v.to_int().max(1) as usize)
9785                    .unwrap_or(1);
9786                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
9787            }
9788            Some(BuiltinId::ClusterNew) => {
9789                // `cluster(HOST...)` — accepts one operand (flattened) or
9790                // multiple (each is a slot spec). Same surface as the
9791                // tree-walker arm in `vm_helper.rs` `call_named_sub`'s
9792                // "cluster" case so `pmap_on` / `~d>` see identical
9793                // `RemoteCluster` values from either dispatch path.
9794                let items = if args.len() == 1 {
9795                    args[0].to_list()
9796                } else {
9797                    args.clone()
9798                };
9799                let c = crate::value::RemoteCluster::from_list_args(&items)
9800                    .map_err(|msg| PerlError::runtime(msg, line))?;
9801                Ok(StrykeValue::remote_cluster(std::sync::Arc::new(c)))
9802            }
9803            Some(BuiltinId::Pipeline) => {
9804                let mut items = Vec::new();
9805                for v in args {
9806                    if let Some(a) = v.as_array_vec() {
9807                        items.extend(a);
9808                    } else {
9809                        items.push(v);
9810                    }
9811                }
9812                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
9813                    source: items,
9814                    ops: Vec::new(),
9815                    has_scalar_terminal: false,
9816                    par_stream: false,
9817                    streaming: false,
9818                    streaming_workers: 0,
9819                    streaming_buffer: 256,
9820                }))))
9821            }
9822            Some(BuiltinId::ParPipeline) => {
9823                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
9824                    return crate::par_pipeline::run_par_pipeline(self.interp, &args, line);
9825                }
9826                let mut items = Vec::new();
9827                for v in args {
9828                    if let Some(a) = v.as_array_vec() {
9829                        items.extend(a);
9830                    } else {
9831                        items.push(v);
9832                    }
9833                }
9834                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
9835                    source: items,
9836                    ops: Vec::new(),
9837                    has_scalar_terminal: false,
9838                    par_stream: true,
9839                    streaming: false,
9840                    streaming_workers: 0,
9841                    streaming_buffer: 256,
9842                }))))
9843            }
9844            Some(BuiltinId::ParPipelineStream) => {
9845                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
9846                    return crate::par_pipeline::run_par_pipeline_streaming(
9847                        self.interp,
9848                        &args,
9849                        line,
9850                    );
9851                }
9852                self.interp.builtin_par_pipeline_stream_new(&args, line)
9853            }
9854            Some(BuiltinId::Each) => {
9855                let _arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9856                Ok(StrykeValue::array(vec![]))
9857            }
9858            Some(BuiltinId::Readpipe) => {
9859                let cmd = args
9860                    .into_iter()
9861                    .next()
9862                    .unwrap_or(StrykeValue::UNDEF)
9863                    .to_string();
9864                crate::capture::run_readpipe(self.interp, &cmd, line)
9865            }
9866            Some(BuiltinId::Eval) => {
9867                let arg = args.into_iter().next().unwrap_or(StrykeValue::UNDEF);
9868                self.interp.eval_nesting += 1;
9869                let out = if let Some(sub) = arg.as_code_ref() {
9870                    match self.interp.exec_block(&sub.body) {
9871                        Ok(v) => {
9872                            self.interp.clear_eval_error();
9873                            Ok(v)
9874                        }
9875                        Err(crate::vm_helper::FlowOrError::Error(e)) => {
9876                            self.interp.set_eval_error_from_perl_error(&e);
9877                            Ok(StrykeValue::UNDEF)
9878                        }
9879                        Err(crate::vm_helper::FlowOrError::Flow(_)) => {
9880                            self.interp.clear_eval_error();
9881                            Ok(StrykeValue::UNDEF)
9882                        }
9883                    }
9884                } else {
9885                    let code = arg.to_string();
9886                    match crate::parse_and_run_string(&code, self.interp) {
9887                        Ok(v) => {
9888                            self.interp.clear_eval_error();
9889                            Ok(v)
9890                        }
9891                        Err(e) => {
9892                            self.interp.set_eval_error_from_perl_error(&e);
9893                            Ok(StrykeValue::UNDEF)
9894                        }
9895                    }
9896                };
9897                self.interp.eval_nesting -= 1;
9898                out
9899            }
9900            Some(BuiltinId::Do) => {
9901                let filename = args
9902                    .into_iter()
9903                    .next()
9904                    .unwrap_or(StrykeValue::UNDEF)
9905                    .to_string();
9906                match read_file_text_perl_compat(&filename) {
9907                    Ok(code) => {
9908                        let code = crate::data_section::strip_perl_end_marker(&code);
9909                        crate::parse_and_run_string_in_file(code, self.interp, &filename)
9910                            .or(Ok(StrykeValue::UNDEF))
9911                    }
9912                    Err(_) => Ok(StrykeValue::UNDEF),
9913                }
9914            }
9915            Some(BuiltinId::Require) => {
9916                let name = args
9917                    .into_iter()
9918                    .next()
9919                    .unwrap_or(StrykeValue::UNDEF)
9920                    .to_string();
9921                self.interp.require_execute(&name, line)
9922            }
9923            Some(BuiltinId::Bless) => {
9924                let ref_val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
9925                let class = args
9926                    .get(1)
9927                    .map(|v| v.to_string())
9928                    .unwrap_or_else(|| self.interp.scope.get_scalar("__PACKAGE__").to_string());
9929                Ok(StrykeValue::blessed(Arc::new(
9930                    crate::value::BlessedRef::new_blessed(class, ref_val),
9931                )))
9932            }
9933            Some(BuiltinId::Caller) => Ok(StrykeValue::array(vec![
9934                StrykeValue::string("main".into()),
9935                StrykeValue::string(self.interp.file.clone()),
9936                StrykeValue::integer(line as i64),
9937            ])),
9938            // Parallel ops (shouldn't reach here — handled by block ops)
9939            Some(BuiltinId::PMap)
9940            | Some(BuiltinId::PGrep)
9941            | Some(BuiltinId::PFor)
9942            | Some(BuiltinId::PSort)
9943            | Some(BuiltinId::Fan)
9944            | Some(BuiltinId::MapBlock)
9945            | Some(BuiltinId::GrepBlock)
9946            | Some(BuiltinId::SortBlock)
9947            | Some(BuiltinId::Sort) => Ok(StrykeValue::UNDEF),
9948            _ => Err(PerlError::runtime(
9949                format!("Unimplemented builtin {:?}", bid),
9950                line,
9951            )),
9952        }
9953    }
9954}
9955
9956/// Integer fast-path comparison helper.
9957#[inline]
9958/// True when both values are non-numeric strings — used by `==` / `!=` in
9959/// stryke non-compat mode to decide whether to fall back to string compare.
9960/// "Numeric string" matches `looks_like_number` semantics (digits, optional
9961/// sign, optional decimal/exponent). Non-string values (refs, undef) are
9962/// excluded so `==` on objects keeps its overload-driven behavior.
9963fn both_non_numeric_strings(a: &StrykeValue, b: &StrykeValue) -> bool {
9964    if !a.is_string_like() || !b.is_string_like() {
9965        return false;
9966    }
9967    let sa = a.to_string();
9968    let sb = b.to_string();
9969    !looks_numeric(&sa) && !looks_numeric(&sb)
9970}
9971
9972#[inline]
9973fn looks_numeric(s: &str) -> bool {
9974    let t = s.trim();
9975    if t.is_empty() {
9976        return false;
9977    }
9978    t.parse::<f64>().is_ok()
9979}
9980
9981fn int_cmp(
9982    a: &StrykeValue,
9983    b: &StrykeValue,
9984    int_op: fn(&i64, &i64) -> bool,
9985    float_op: fn(f64, f64) -> bool,
9986) -> StrykeValue {
9987    if let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) {
9988        StrykeValue::integer(if int_op(&x, &y) { 1 } else { 0 })
9989    } else {
9990        StrykeValue::integer(if float_op(a.to_number(), b.to_number()) {
9991            1
9992        } else {
9993            0
9994        })
9995    }
9996}
9997
9998/// Block JIT hook: string concat with `use overload` / `""` stringify (matches [`Op::Concat`]).
9999///
10000/// # Safety
10001///
10002/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call.
10003#[no_mangle]
10004pub unsafe extern "C" fn stryke_jit_concat_vm(vm: *mut std::ffi::c_void, a: i64, b: i64) -> i64 {
10005    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
10006    let pa = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(a));
10007    let pb = StrykeValue::from_raw_bits(crate::jit::perl_value_bits_from_jit_string_operand(b));
10008    match vm.concat_stack_values(pa, pb) {
10009        Ok(pv) => pv.raw_bits() as i64,
10010        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
10011    }
10012}
10013
10014/// Cranelift host hook: re-enter the VM for [`Op::Call`] to a compiled sub (stack-args, scalar `i64` args).
10015/// `sub_ip`, `argc`, `wa` are passed as `i64` for a uniform Cranelift signature.
10016///
10017/// # Safety
10018///
10019/// `vm` must be a valid, non-null pointer to a live [`VM`] for the duration of this call (JIT only
10020/// invokes this while the VM is executing).
10021#[no_mangle]
10022pub unsafe extern "C" fn stryke_jit_call_sub(
10023    vm: *mut std::ffi::c_void,
10024    sub_ip: i64,
10025    argc: i64,
10026    wa: i64,
10027    a0: i64,
10028    a1: i64,
10029    a2: i64,
10030    a3: i64,
10031    a4: i64,
10032    a5: i64,
10033    a6: i64,
10034    a7: i64,
10035) -> i64 {
10036    let vm: &mut VM<'static> = unsafe { &mut *(vm as *mut VM<'static>) };
10037    let want = WantarrayCtx::from_byte(wa as u8);
10038    if want != WantarrayCtx::Scalar {
10039        return StrykeValue::UNDEF.raw_bits() as i64;
10040    }
10041    let argc = argc.clamp(0, 8) as usize;
10042    let args = [a0, a1, a2, a3, a4, a5, a6, a7];
10043    let args = &args[..argc];
10044    match vm.jit_trampoline_run_sub(sub_ip as usize, want, args) {
10045        Ok(pv) => {
10046            if let Some(n) = pv.as_integer() {
10047                n
10048            } else {
10049                pv.raw_bits() as i64
10050            }
10051        }
10052        Err(_) => StrykeValue::UNDEF.raw_bits() as i64,
10053    }
10054}
10055
10056#[cfg(test)]
10057mod tests {
10058    use super::*;
10059    use crate::bytecode::{Chunk, Op};
10060    use crate::value::StrykeValue;
10061
10062    fn run_chunk(chunk: &Chunk) -> PerlResult<StrykeValue> {
10063        let mut interp = VMHelper::new();
10064        let mut vm = VM::new(chunk, &mut interp);
10065        vm.execute()
10066    }
10067
10068    /// Block-JIT-eligible loop: `for ($i=0; $i<limit; $i++) { $sum += $i }` — sum 0..limit-1.
10069    fn block_jit_sum_chunk(limit: i64) -> Chunk {
10070        let mut c = Chunk::new();
10071        let ni = c.intern_name("i");
10072        let ns = c.intern_name("sum");
10073        c.emit(Op::LoadInt(0), 1);
10074        c.emit(Op::DeclareScalarSlot(0, ni), 1);
10075        c.emit(Op::LoadInt(0), 1);
10076        c.emit(Op::DeclareScalarSlot(1, ns), 1);
10077        c.emit(Op::GetScalarSlot(0), 1);
10078        c.emit(Op::LoadInt(limit), 1);
10079        c.emit(Op::NumLt, 1);
10080        c.emit(Op::JumpIfFalse(15), 1);
10081        c.emit(Op::GetScalarSlot(1), 1);
10082        c.emit(Op::GetScalarSlot(0), 1);
10083        c.emit(Op::Add, 1);
10084        c.emit(Op::SetScalarSlot(1), 1);
10085        c.emit(Op::PostIncSlot(0), 1);
10086        c.emit(Op::Pop, 1);
10087        c.emit(Op::Jump(4), 1);
10088        c.emit(Op::GetScalarSlot(1), 1);
10089        c.emit(Op::Halt, 1);
10090        c
10091    }
10092
10093    #[test]
10094    fn jit_disabled_same_result_as_jit_block_loop() {
10095        let limit = 500i64;
10096        let chunk = block_jit_sum_chunk(limit);
10097        let expect = limit * (limit - 1) / 2;
10098
10099        let mut interp_on = VMHelper::new();
10100        let mut vm_on = VM::new(&chunk, &mut interp_on);
10101        assert_eq!(vm_on.execute().expect("vm").to_int(), expect);
10102
10103        let mut interp_off = VMHelper::new();
10104        let mut vm_off = VM::new(&chunk, &mut interp_off);
10105        vm_off.set_jit_enabled(false);
10106        assert_eq!(vm_off.execute().expect("vm").to_int(), expect);
10107    }
10108
10109    #[test]
10110    fn vm_add_two_integers() {
10111        let mut c = Chunk::new();
10112        c.emit(Op::LoadInt(2), 1);
10113        c.emit(Op::LoadInt(3), 1);
10114        c.emit(Op::Add, 1);
10115        c.emit(Op::Halt, 1);
10116        let v = run_chunk(&c).expect("vm");
10117        assert_eq!(v.to_int(), 5);
10118    }
10119
10120    #[test]
10121    fn vm_sub_mul_div() {
10122        let mut c = Chunk::new();
10123        c.emit(Op::LoadInt(10), 1);
10124        c.emit(Op::LoadInt(3), 1);
10125        c.emit(Op::Sub, 1);
10126        c.emit(Op::Halt, 1);
10127        assert_eq!(run_chunk(&c).expect("vm").to_int(), 7);
10128
10129        let mut c = Chunk::new();
10130        c.emit(Op::LoadInt(6), 1);
10131        c.emit(Op::LoadInt(7), 1);
10132        c.emit(Op::Mul, 1);
10133        c.emit(Op::Halt, 1);
10134        assert_eq!(run_chunk(&c).expect("vm").to_int(), 42);
10135
10136        let mut c = Chunk::new();
10137        c.emit(Op::LoadInt(20), 1);
10138        c.emit(Op::LoadInt(4), 1);
10139        c.emit(Op::Div, 1);
10140        c.emit(Op::Halt, 1);
10141        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
10142    }
10143
10144    #[test]
10145    fn vm_mod_and_pow() {
10146        let mut c = Chunk::new();
10147        c.emit(Op::LoadInt(17), 1);
10148        c.emit(Op::LoadInt(5), 1);
10149        c.emit(Op::Mod, 1);
10150        c.emit(Op::Halt, 1);
10151        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10152
10153        let mut c = Chunk::new();
10154        c.emit(Op::LoadInt(2), 1);
10155        c.emit(Op::LoadInt(3), 1);
10156        c.emit(Op::Pow, 1);
10157        c.emit(Op::Halt, 1);
10158        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
10159    }
10160
10161    #[test]
10162    fn vm_negate() {
10163        let mut c = Chunk::new();
10164        c.emit(Op::LoadInt(7), 1);
10165        c.emit(Op::Negate, 1);
10166        c.emit(Op::Halt, 1);
10167        assert_eq!(run_chunk(&c).expect("vm").to_int(), -7);
10168    }
10169
10170    #[test]
10171    fn vm_dup_and_pop() {
10172        let mut c = Chunk::new();
10173        c.emit(Op::LoadInt(1), 1);
10174        c.emit(Op::Dup, 1);
10175        c.emit(Op::Add, 1);
10176        c.emit(Op::Halt, 1);
10177        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10178
10179        let mut c = Chunk::new();
10180        c.emit(Op::LoadInt(1), 1);
10181        c.emit(Op::LoadInt(2), 1);
10182        c.emit(Op::Pop, 1);
10183        c.emit(Op::Halt, 1);
10184        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10185    }
10186
10187    #[test]
10188    fn vm_set_get_scalar() {
10189        let mut c = Chunk::new();
10190        let i = c.intern_name("v");
10191        c.emit(Op::LoadInt(99), 1);
10192        c.emit(Op::SetScalar(i), 1);
10193        c.emit(Op::GetScalar(i), 1);
10194        c.emit(Op::Halt, 1);
10195        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
10196    }
10197
10198    #[test]
10199    fn vm_scalar_plain_roundtrip_and_keep() {
10200        let mut c = Chunk::new();
10201        let i = c.intern_name("plainvar");
10202        c.emit(Op::LoadInt(99), 1);
10203        c.emit(Op::SetScalarPlain(i), 1);
10204        c.emit(Op::GetScalarPlain(i), 1);
10205        c.emit(Op::Halt, 1);
10206        assert_eq!(run_chunk(&c).expect("vm").to_int(), 99);
10207
10208        let mut c = Chunk::new();
10209        let k = c.intern_name("keepme");
10210        c.emit(Op::LoadInt(5), 1);
10211        c.emit(Op::SetScalarKeepPlain(k), 1);
10212        c.emit(Op::Halt, 1);
10213        assert_eq!(run_chunk(&c).expect("vm").to_int(), 5);
10214    }
10215
10216    #[test]
10217    fn vm_get_scalar_plain_skips_special_global_zero() {
10218        let mut c = Chunk::new();
10219        let idx = c.intern_name("0");
10220        c.emit(Op::GetScalar(idx), 1);
10221        c.emit(Op::Halt, 1);
10222        assert_eq!(run_chunk(&c).expect("vm").to_string(), "stryke");
10223
10224        let mut c = Chunk::new();
10225        let idx = c.intern_name("0");
10226        c.emit(Op::GetScalarPlain(idx), 1);
10227        c.emit(Op::Halt, 1);
10228        assert!(run_chunk(&c).expect("vm").is_undef());
10229    }
10230
10231    #[test]
10232    fn vm_slot_pre_post_inc_dec() {
10233        let mut c = Chunk::new();
10234        c.emit(Op::LoadInt(10), 1);
10235        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10236        c.emit(Op::PostIncSlot(0), 1);
10237        c.emit(Op::Pop, 1);
10238        c.emit(Op::GetScalarSlot(0), 1);
10239        c.emit(Op::Halt, 1);
10240        assert_eq!(run_chunk(&c).expect("vm").to_int(), 11);
10241
10242        let mut c = Chunk::new();
10243        c.emit(Op::LoadInt(0), 1);
10244        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10245        c.emit(Op::PreIncSlot(0), 1);
10246        c.emit(Op::Halt, 1);
10247        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10248
10249        let mut c = Chunk::new();
10250        c.emit(Op::LoadInt(5), 1);
10251        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10252        c.emit(Op::PreDecSlot(0), 1);
10253        c.emit(Op::Halt, 1);
10254        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
10255
10256        let mut c = Chunk::new();
10257        c.emit(Op::LoadInt(3), 1);
10258        c.emit(Op::DeclareScalarSlot(0, u16::MAX), 1);
10259        c.emit(Op::PostDecSlot(0), 1);
10260        c.emit(Op::Pop, 1);
10261        c.emit(Op::GetScalarSlot(0), 1);
10262        c.emit(Op::Halt, 1);
10263        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10264    }
10265
10266    #[test]
10267    fn vm_str_eq_ne_heap_strings() {
10268        let mut c = Chunk::new();
10269        let a = c.add_constant(StrykeValue::string("same".into()));
10270        let b = c.add_constant(StrykeValue::string("same".into()));
10271        c.emit(Op::LoadConst(a), 1);
10272        c.emit(Op::LoadConst(b), 1);
10273        c.emit(Op::StrEq, 1);
10274        c.emit(Op::Halt, 1);
10275        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10276
10277        let mut c = Chunk::new();
10278        let a = c.add_constant(StrykeValue::string("a".into()));
10279        let b = c.add_constant(StrykeValue::string("b".into()));
10280        c.emit(Op::LoadConst(a), 1);
10281        c.emit(Op::LoadConst(b), 1);
10282        c.emit(Op::StrNe, 1);
10283        c.emit(Op::Halt, 1);
10284        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10285    }
10286
10287    #[test]
10288    fn vm_num_eq_ine() {
10289        let mut c = Chunk::new();
10290        c.emit(Op::LoadInt(1), 1);
10291        c.emit(Op::LoadInt(1), 1);
10292        c.emit(Op::NumEq, 1);
10293        c.emit(Op::Halt, 1);
10294        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10295
10296        let mut c = Chunk::new();
10297        c.emit(Op::LoadInt(1), 1);
10298        c.emit(Op::LoadInt(2), 1);
10299        c.emit(Op::NumNe, 1);
10300        c.emit(Op::Halt, 1);
10301        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10302    }
10303
10304    #[test]
10305    fn vm_num_ordering() {
10306        for (a, b, op, want) in [
10307            (1i64, 2i64, Op::NumLt, 1),
10308            (3i64, 2i64, Op::NumGt, 1),
10309            (2i64, 2i64, Op::NumLe, 1),
10310            (2i64, 2i64, Op::NumGe, 1),
10311        ] {
10312            let mut c = Chunk::new();
10313            c.emit(Op::LoadInt(a), 1);
10314            c.emit(Op::LoadInt(b), 1);
10315            c.emit(op, 1);
10316            c.emit(Op::Halt, 1);
10317            assert_eq!(run_chunk(&c).expect("vm").to_int(), want);
10318        }
10319    }
10320
10321    #[test]
10322    fn vm_concat_and_str_cmp() {
10323        let mut c = Chunk::new();
10324        let i1 = c.add_constant(StrykeValue::string("a".into()));
10325        let i2 = c.add_constant(StrykeValue::string("b".into()));
10326        c.emit(Op::LoadConst(i1), 1);
10327        c.emit(Op::LoadConst(i2), 1);
10328        c.emit(Op::Concat, 1);
10329        c.emit(Op::Halt, 1);
10330        assert_eq!(run_chunk(&c).expect("vm").to_string(), "ab");
10331
10332        let mut c = Chunk::new();
10333        let i1 = c.add_constant(StrykeValue::string("a".into()));
10334        let i2 = c.add_constant(StrykeValue::string("b".into()));
10335        c.emit(Op::LoadConst(i1), 1);
10336        c.emit(Op::LoadConst(i2), 1);
10337        c.emit(Op::StrCmp, 1);
10338        c.emit(Op::Halt, 1);
10339        let v = run_chunk(&c).expect("vm");
10340        assert!(v.to_int() < 0);
10341    }
10342
10343    #[test]
10344    fn vm_log_not() {
10345        let mut c = Chunk::new();
10346        c.emit(Op::LoadInt(0), 1);
10347        c.emit(Op::LogNot, 1);
10348        c.emit(Op::Halt, 1);
10349        assert_eq!(run_chunk(&c).expect("vm").to_int(), 1);
10350    }
10351
10352    #[test]
10353    fn vm_bit_and_or_xor_not() {
10354        let mut c = Chunk::new();
10355        c.emit(Op::LoadInt(0b1100), 1);
10356        c.emit(Op::LoadInt(0b1010), 1);
10357        c.emit(Op::BitAnd, 1);
10358        c.emit(Op::Halt, 1);
10359        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1000);
10360
10361        let mut c = Chunk::new();
10362        c.emit(Op::LoadInt(0b1100), 1);
10363        c.emit(Op::LoadInt(0b1010), 1);
10364        c.emit(Op::BitOr, 1);
10365        c.emit(Op::Halt, 1);
10366        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b1110);
10367
10368        let mut c = Chunk::new();
10369        c.emit(Op::LoadInt(0b1100), 1);
10370        c.emit(Op::LoadInt(0b1010), 1);
10371        c.emit(Op::BitXor, 1);
10372        c.emit(Op::Halt, 1);
10373        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0b0110);
10374
10375        let mut c = Chunk::new();
10376        c.emit(Op::LoadInt(0), 1);
10377        c.emit(Op::BitNot, 1);
10378        c.emit(Op::Halt, 1);
10379        assert!((run_chunk(&c).expect("vm").to_int() & 0xFF) != 0);
10380    }
10381
10382    #[test]
10383    fn vm_shl_shr() {
10384        let mut c = Chunk::new();
10385        c.emit(Op::LoadInt(1), 1);
10386        c.emit(Op::LoadInt(3), 1);
10387        c.emit(Op::Shl, 1);
10388        c.emit(Op::Halt, 1);
10389        assert_eq!(run_chunk(&c).expect("vm").to_int(), 8);
10390
10391        let mut c = Chunk::new();
10392        c.emit(Op::LoadInt(16), 1);
10393        c.emit(Op::LoadInt(2), 1);
10394        c.emit(Op::Shr, 1);
10395        c.emit(Op::Halt, 1);
10396        assert_eq!(run_chunk(&c).expect("vm").to_int(), 4);
10397    }
10398
10399    #[test]
10400    fn vm_load_undef_float_constant() {
10401        let mut c = Chunk::new();
10402        c.emit(Op::LoadUndef, 1);
10403        c.emit(Op::Halt, 1);
10404        assert!(run_chunk(&c).expect("vm").is_undef());
10405
10406        let mut c = Chunk::new();
10407        c.emit(Op::LoadFloat(2.5), 1);
10408        c.emit(Op::Halt, 1);
10409        assert!((run_chunk(&c).expect("vm").to_number() - 2.5).abs() < 1e-9);
10410    }
10411
10412    #[test]
10413    fn vm_jump_skips_ops() {
10414        let mut c = Chunk::new();
10415        let j = c.emit(Op::Jump(0), 1);
10416        c.emit(Op::LoadInt(1), 1);
10417        c.emit(Op::LoadInt(2), 1);
10418        c.emit(Op::Add, 1);
10419        c.patch_jump_here(j);
10420        c.emit(Op::LoadInt(40), 1);
10421        c.emit(Op::Halt, 1);
10422        assert_eq!(run_chunk(&c).expect("vm").to_int(), 40);
10423    }
10424
10425    #[test]
10426    fn vm_jump_if_false() {
10427        let mut c = Chunk::new();
10428        c.emit(Op::LoadInt(0), 1);
10429        let j = c.emit(Op::JumpIfFalse(0), 1);
10430        c.emit(Op::LoadInt(1), 1);
10431        c.emit(Op::Halt, 1);
10432        c.patch_jump_here(j);
10433        c.emit(Op::LoadInt(2), 1);
10434        c.emit(Op::Halt, 1);
10435        assert_eq!(run_chunk(&c).expect("vm").to_int(), 2);
10436    }
10437
10438    #[test]
10439    fn vm_call_builtin_defined() {
10440        let mut c = Chunk::new();
10441        c.emit(Op::LoadUndef, 1);
10442        c.emit(Op::CallBuiltin(BuiltinId::Defined as u16, 1), 1);
10443        c.emit(Op::Halt, 1);
10444        assert_eq!(run_chunk(&c).expect("vm").to_int(), 0);
10445    }
10446
10447    #[test]
10448    fn vm_call_builtin_length_string() {
10449        let mut c = Chunk::new();
10450        let idx = c.add_constant(StrykeValue::string("abc".into()));
10451        c.emit(Op::LoadConst(idx), 1);
10452        c.emit(Op::CallBuiltin(BuiltinId::Length as u16, 1), 1);
10453        c.emit(Op::Halt, 1);
10454        assert_eq!(run_chunk(&c).expect("vm").to_int(), 3);
10455    }
10456
10457    #[test]
10458    fn vm_make_array_two() {
10459        let mut c = Chunk::new();
10460        c.emit(Op::LoadInt(1), 1);
10461        c.emit(Op::LoadInt(2), 1);
10462        c.emit(Op::MakeArray(2), 1);
10463        c.emit(Op::Halt, 1);
10464        let v = run_chunk(&c).expect("vm");
10465        let a = v.as_array_vec().expect("array");
10466        assert_eq!(a.len(), 2);
10467        assert_eq!(a[0].to_int(), 1);
10468        assert_eq!(a[1].to_int(), 2);
10469    }
10470
10471    #[test]
10472    fn vm_spaceship() {
10473        let mut c = Chunk::new();
10474        c.emit(Op::LoadInt(1), 1);
10475        c.emit(Op::LoadInt(2), 1);
10476        c.emit(Op::Spaceship, 1);
10477        c.emit(Op::Halt, 1);
10478        assert_eq!(run_chunk(&c).expect("vm").to_int(), -1);
10479    }
10480
10481    #[test]
10482    fn compiled_try_catch_catches_die_via_vm() {
10483        let program = crate::parse(
10484            r#"
10485        try {
10486            die "boom";
10487        } catch ($err) {
10488            42;
10489        }
10490    "#,
10491        )
10492        .expect("parse");
10493        let chunk = crate::compiler::Compiler::new()
10494            .compile_program(&program)
10495            .expect("compile");
10496        let tp = chunk
10497            .ops
10498            .iter()
10499            .position(|o| matches!(o, Op::TryPush { .. }))
10500            .expect("TryPush op");
10501        match &chunk.ops[tp] {
10502            Op::TryPush {
10503                catch_ip, after_ip, ..
10504            } => {
10505                assert_ne!(*catch_ip, 0, "catch_ip must be patched");
10506                assert_ne!(*after_ip, 0, "after_ip must be patched");
10507            }
10508            _ => unreachable!(),
10509        }
10510        let mut interp = VMHelper::new();
10511        let mut vm = VM::new(&chunk, &mut interp);
10512        vm.set_jit_enabled(false);
10513        let v = vm.execute().expect("vm should catch die");
10514        assert_eq!(v.to_int(), 42);
10515    }
10516}