Skip to main content

stryke/
compiler.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::ast::*;
4use crate::bytecode::{
5    BuiltinId, Chunk, Op, RuntimeAdviceDecl, RuntimeSubDecl, GP_CHECK, GP_END, GP_INIT, GP_RUN,
6    GP_START,
7};
8use crate::sort_fast::detect_sort_block_fast;
9use crate::value::StrykeValue;
10use crate::vm_helper::{assign_rhs_wantarray, VMHelper, WantarrayCtx};
11
12/// True when EXPR as the *tail* of a `map { … }` block would produce a different value in
13/// list context than in scalar context — Range (flip-flop vs list), comma lists, `reverse` /
14/// `sort` / `map` / `grep` calls, array/hash variables and derefs, `@{...}`, etc. Shared
15/// block-bytecode regions compile the tail in scalar context for grep/sort, so map VM paths
16/// consult this predicate to decide whether to reuse the shared region or emit a
17/// list-tail [`Interpreter::exec_block_with_tail`](crate::vm_helper::VMHelper::exec_block_with_tail) call.
18pub fn expr_tail_is_list_sensitive(expr: &Expr) -> bool {
19    match &expr.kind {
20        // Range `..` in scalar context is flip-flop, in list context is expansion.
21        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => true,
22        // Multi-element list: `($a, $b)` returns 2 values in list, last in scalar.
23        ExprKind::List(items) => items.len() != 1 || expr_tail_is_list_sensitive(&items[0]),
24        ExprKind::QW(ws) => ws.len() != 1,
25        // Deref of array/hash: `@$ref` / `%$ref`.
26        ExprKind::Deref {
27            kind: Sigil::Array | Sigil::Hash,
28            ..
29        } => true,
30        // Slices always produce lists.
31        ExprKind::ArraySlice { .. }
32        | ExprKind::HashSlice { .. }
33        | ExprKind::HashSliceDeref { .. }
34        | ExprKind::AnonymousListSlice { .. } => true,
35        // Map/grep/sort blocks need list-context tail evaluation.
36        ExprKind::MapExpr { .. }
37        | ExprKind::MapExprComma { .. }
38        | ExprKind::GrepExpr { .. }
39        | ExprKind::SortExpr { .. } => true,
40        // FuncCalls that return lists in list context, scalars in scalar context.
41        ExprKind::FuncCall { name, .. } => matches!(
42            name.as_str(),
43            "wantarray"
44                | "reverse"
45                | "sort"
46                | "map"
47                | "grep"
48                | "keys"
49                | "values"
50                | "each"
51                | "split"
52                | "caller"
53                | "localtime"
54                | "gmtime"
55                | "stat"
56                | "lstat"
57        ),
58        ExprKind::Ternary {
59            then_expr,
60            else_expr,
61            ..
62        } => expr_tail_is_list_sensitive(then_expr) || expr_tail_is_list_sensitive(else_expr),
63        // ArrayVar, HashVar, slices — handled by VM's ReturnValue + List context
64        // compilation. Do NOT allow these to trigger a compilation error.
65        _ => false,
66    }
67}
68
69/// True when one `{…}` entry expands to multiple hash keys (`qw/a b/`, a list literal with 2+
70/// elems, or a list-context `..` range like `'a'..'c'`).
71pub(crate) fn hash_slice_key_expr_is_multi_key(k: &Expr) -> bool {
72    match &k.kind {
73        ExprKind::QW(ws) => ws.len() > 1,
74        ExprKind::List(el) => el.len() > 1,
75        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => true,
76        _ => false,
77    }
78}
79
80/// Use [`Op::HashSliceDeref`] / [`Op::HashSliceDerefCompound`] / [`Op::HashSliceDerefIncDec`], or
81/// [`Op::NamedHashSliceCompound`] / [`Op::NamedHashSliceIncDec`] for stash `@h{…}`, instead of arrow-hash single-slot ops.
82pub(crate) fn hash_slice_needs_slice_ops(keys: &[Expr]) -> bool {
83    keys.len() != 1 || keys.first().is_some_and(hash_slice_key_expr_is_multi_key)
84}
85
86/// `$r->[EXPR] //=` / `||=` / `&&=` — the bytecode fast path uses [`Op::ArrowArray`] (scalar index).
87/// Range / multi-word `qw`/list subscripts need different semantics; not yet lowered to bytecode.
88/// `$r->[IX]` reads/writes via [`Op::ArrowArray`] only when `IX` is a **plain scalar** subscript.
89/// `..` / `qw/.../` / `(a,b)` / nested lists always go through slice ops (flattened index specs).
90pub(crate) fn arrow_deref_arrow_subscript_is_plain_scalar_index(index: &Expr) -> bool {
91    match &index.kind {
92        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => false,
93        ExprKind::QW(_) => false,
94        ExprKind::List(el) => {
95            if el.len() == 1 {
96                arrow_deref_arrow_subscript_is_plain_scalar_index(&el[0])
97            } else {
98                false
99            }
100        }
101        _ => !hash_slice_key_expr_is_multi_key(index),
102    }
103}
104
105/// Compilation error — halts compilation with an error.
106#[derive(Debug)]
107pub enum CompileError {
108    /// `Unsupported` variant.
109    Unsupported(String),
110    /// Immutable binding reassignment (e.g. `frozen my $x` then `$x = 1`).
111    Frozen { line: usize, detail: String },
112}
113
114#[derive(Default, Clone)]
115struct ScopeLayer {
116    declared_scalars: HashSet<String>,
117    /// Bare names from `our $x` — rvalue/lvalue ops must use the package stash key (`main::x`).
118    declared_our_scalars: HashSet<String>,
119    declared_arrays: HashSet<String>,
120    declared_hashes: HashSet<String>,
121    /// Bare names from `our @x` — rvalue/lvalue ops must use the package stash key.
122    declared_our_arrays: HashSet<String>,
123    /// Bare names from `our %h` — rvalue/lvalue ops must use the package stash key.
124    declared_our_hashes: HashSet<String>,
125    frozen_scalars: HashSet<String>,
126    frozen_arrays: HashSet<String>,
127    frozen_hashes: HashSet<String>,
128    /// Slot-index mapping for `my` scalars in compiled subroutines.
129    /// When `use_slots` is true, `my $x` is assigned a u8 slot index
130    /// and the VM accesses it via `GetScalarSlot(idx)` — O(1).
131    scalar_slots: HashMap<String, u8>,
132    next_scalar_slot: u8,
133    /// True when compiling a subroutine body (enables slot assignment).
134    use_slots: bool,
135    /// `mysync @name` — element `++`/`--`/compound assign not yet lowered to bytecode (atomic RMW).
136    mysync_arrays: HashSet<String>,
137    /// `mysync %name` — same as [`Self::mysync_arrays`].
138    mysync_hashes: HashSet<String>,
139    /// `mysync $name` — opt-in shared closure capture (Arc-cell). Closures may
140    /// freely mutate these; default `my` scalars trigger a compile-time error
141    /// if a closure writes to them from inside a sub body. (DESIGN-001)
142    mysync_scalars: HashSet<String>,
143    /// True when this layer was entered for an anonymous-or-named sub body
144    /// (`sub { ... }` / `fn { ... }` / `sub foo { ... }`). Used by the
145    /// closure-write check to detect when a write crosses a sub-body
146    /// boundary (DESIGN-001). False for `map`/`grep`/`sort` block layers
147    /// (those are not closures in the value-snapshot sense).
148    is_sub_body: bool,
149}
150
151/// Loop context for resolving `last`/`next` jumps.
152///
153/// Pushed onto [`Compiler::loop_stack`] at every loop entry so `last`/`next` (including those
154/// nested inside `if`/`unless`/`{ }` blocks) can find the matching loop and patch their jumps.
155///
156/// `entry_frame_depth` is [`Compiler::frame_depth`] at loop entry — `last`/`next` from inside
157/// emits `(frame_depth - entry_frame_depth)` `Op::PopFrame` instructions before jumping so any
158/// `if`/block-pushed scope frames are torn down.
159///
160/// `entry_try_depth` mirrors `try { }` nesting; if a `last`/`next` would have to cross a try
161/// frame the compiler bails to `Unsupported` (try-frame unwind on flow control is not yet
162/// modeled in bytecode — the catch handler would still see the next exception).
163struct LoopCtx {
164    label: Option<String>,
165    entry_frame_depth: usize,
166    entry_try_depth: usize,
167    /// First bytecode IP of the loop **body** (after `while`/`until` condition, after `for` condition,
168    /// after `foreach` assigns `$var` from the list, or `do` body start) — target for `redo`.
169    body_start_ip: usize,
170    /// Positions of `last`/`next` jumps to patch after the loop body is fully compiled.
171    break_jumps: Vec<usize>,
172    /// `Op::Jump(0)` placeholders for `next` — patched to the loop increment / condition entry.
173    continue_jumps: Vec<usize>,
174}
175/// `Compiler` — see fields for layout.
176pub struct Compiler {
177    /// `chunk` field.
178    pub chunk: Chunk,
179    /// During compilation: stable [`Expr`] pointer → [`Chunk::ast_expr_pool`] index.
180    ast_expr_intern: HashMap<usize, u32>,
181    /// `begin_blocks` field.
182    pub begin_blocks: Vec<Block>,
183    /// `unit_check_blocks` field.
184    pub unit_check_blocks: Vec<Block>,
185    /// `check_blocks` field.
186    pub check_blocks: Vec<Block>,
187    /// `init_blocks` field.
188    pub init_blocks: Vec<Block>,
189    /// `end_blocks` field.
190    pub end_blocks: Vec<Block>,
191    /// Lexical `my` declarations per scope frame (mirrors `PushFrame` / sub bodies).
192    scope_stack: Vec<ScopeLayer>,
193    /// Current `package` for stash qualification (`@ISA`, `@EXPORT`, …), matching [`VMHelper::stash_array_name_for_package`].
194    current_package: String,
195    /// Set while compiling the main program body when the last statement must leave its value on the
196    /// stack (implicit return). Enables `try`/`catch` blocks to match `emit_block_value` semantics.
197    program_last_stmt_takes_value: bool,
198    /// Source path for `__FILE__` in bytecode (must match the interpreter's notion of current file when using the VM).
199    pub source_file: String,
200    /// Runtime activation depth — `Op::PushFrame` count minus `Op::PopFrame` count emitted so far.
201    /// Used by `last`/`next` to compute how many frames to pop before jumping.
202    frame_depth: usize,
203    /// `try { }` nesting depth — `last`/`next` cannot currently cross a try-frame in bytecode.
204    try_depth: usize,
205    /// Active loops, innermost at the back. `last`/`next` consult this stack.
206    loop_stack: Vec<LoopCtx>,
207    /// Per-function (top-level program or sub body) `goto LABEL` tracking. Top of the stack holds
208    /// the label→IP map and forward-goto patch list for the innermost enclosing label-scoped
209    /// region. `goto` is only resolved against the top frame (matches Perl's "goto must target a
210    /// label in the same lexical context" intuition).
211    goto_ctx_stack: Vec<GotoCtx>,
212    /// `use strict 'vars'` — reject access to undeclared globals at compile time (mirrors
213    /// `Interpreter::check_strict_*_var` runtime checks). Set via
214    /// [`Self::with_strict_vars`] before `compile_program` runs; stable throughout a single
215    /// compile because `use strict` is resolved in `prepare_program_top_level` before the VM
216    /// compile begins.
217    strict_vars: bool,
218    /// `-n`/`-p` line mode. When set, `END` blocks are compiled into a separate region placed
219    /// AFTER the main body's `Halt` (not before it), so per-line execution stops at the main
220    /// `Halt` and never runs `END`; the line driver runs the `END` region once after the loop
221    /// via [`crate::bytecode::Chunk::line_mode_end_ip`]. In normal mode `END` stays inline
222    /// before the single `Halt` so it runs once at program end.
223    line_mode: bool,
224    /// True while compiling a deferred sort/reduce block in the 4th pass. `$a` and `$b`
225    /// inside such blocks must use name-based access — `set_sort_pair` writes by name,
226    /// so a slot allocation from any outer `my $a`/`my $b` (which the deferred pass sees
227    /// because it runs after the whole program is compiled) would silently miss the
228    /// per-iteration value. Other variables continue to use slots so outer `my`
229    /// captures remain O(1).
230    force_name_for_sort_pair: bool,
231    /// Block indices registered via [`Self::register_sort_pair_block`] — sort/reduce
232    /// comparator bodies that bind `$a`/`$b` magic globals. The deferred 4th pass
233    /// turns on [`Self::force_name_for_sort_pair`] only for these blocks.
234    sort_pair_block_indices: std::collections::HashSet<u16>,
235    /// Block indices that came from an anonymous-or-named sub-body
236    /// (`sub { ... }` / `fn { ... }`). The 4th-pass compile pushes a
237    /// `is_sub_body: true` scope layer before compiling these so the
238    /// closure-write check (DESIGN-001) can detect outer-scope `my`
239    /// writes. Map/grep/sort blocks are NOT in this set.
240    sub_body_block_indices: std::collections::HashSet<u16>,
241    /// Per-block signature params (parallel to `chunk.blocks`) used by the
242    /// 4th-pass compile to register CodeRef params in the new sub-body
243    /// scope layer.
244    code_ref_block_params: Vec<Vec<crate::ast::SubSigParam>>,
245    /// Snapshot of `scope_stack` taken when each block was added. The 4th pass
246    /// (`compile_program`) defers map/grep/sort block compilation until after
247    /// every sub body has finished, by which point the parent scope_stack has
248    /// already been popped. Without these snapshots, references like the
249    /// fn-local `my $max` inside `map { … $max … }` would resolve against the
250    /// trailing top-level scope_stack and silently bind to a sibling top-level
251    /// `my $max` slot. The 4th pass swaps the matching snapshot in before
252    /// compiling each block. Index lines up with `chunk.blocks`; entries are
253    /// `None` for blocks added through code paths that don't go through the
254    /// compiler's [`Self::add_deferred_block`] wrapper.
255    block_scope_snapshots: Vec<Option<Vec<ScopeLayer>>>,
256}
257
258/// Label tracking for `goto LABEL` within a single label-scoped region (top-level main program
259/// or subroutine body). See [`Compiler::enter_goto_scope`] / [`Compiler::exit_goto_scope`].
260#[derive(Default)]
261struct GotoCtx {
262    /// `label_name → (bytecode IP of the labeled statement's first op, frame_depth at label)`
263    labels: HashMap<String, (usize, usize)>,
264    /// `(jump_op_ip, label_name, source_line, frame_depth_at_goto)` for forward `goto LABEL`.
265    pending: Vec<(usize, String, usize, usize)>,
266}
267
268impl Default for Compiler {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274impl Compiler {
275    /// Array/hash slice subscripts are list context: `@a[LIST]` flattens ranges, `reverse`,
276    /// `sort`, `grep`, `map`, and array variables the same way `@h{LIST}` does. Scalar
277    /// subscripts are unaffected because list context on a plain scalar is still the scalar.
278    fn compile_array_slice_index_expr(&mut self, index_expr: &Expr) -> Result<(), CompileError> {
279        self.compile_expr_ctx(index_expr, WantarrayCtx::List)
280    }
281
282    /// Hash-slice key component: anything that's syntactically a list-valued
283    /// operand (`'a'..'c'`, `@ks`, `@{$ref}`, `qw(a b)`, a list literal, an
284    /// array slice) is compiled in list context so the VM's hash-slice
285    /// helpers see all the elements instead of a scalarized count.
286    fn compile_hash_slice_key_expr(&mut self, key_expr: &Expr) -> Result<(), CompileError> {
287        if matches!(
288            &key_expr.kind,
289            ExprKind::Range { .. }
290                | ExprKind::SliceRange { .. }
291                | ExprKind::ArrayVar(_)
292                | ExprKind::Deref {
293                    kind: Sigil::Array,
294                    ..
295                }
296                | ExprKind::ArraySlice { .. }
297                | ExprKind::QW(_)
298                | ExprKind::List(_)
299        ) {
300            self.compile_expr_ctx(key_expr, WantarrayCtx::List)
301        } else {
302            self.compile_expr(key_expr)
303        }
304    }
305
306    /// Push the value of `expr` if present, or `Undef` (the omitted-endpoint sentinel
307    /// used by [`Op::ArraySliceRange`] / [`Op::HashSliceRange`]) if `None`.
308    fn compile_optional_or_undef(&mut self, expr: Option<&Expr>) -> Result<(), CompileError> {
309        match expr {
310            Some(e) => self.compile_expr(e),
311            None => {
312                self.emit_op(Op::LoadUndef, 0, None);
313                Ok(())
314            }
315        }
316    }
317    /// `new` — see implementation.
318    pub fn new() -> Self {
319        Self {
320            chunk: Chunk::new(),
321            ast_expr_intern: HashMap::new(),
322            begin_blocks: Vec::new(),
323            unit_check_blocks: Vec::new(),
324            check_blocks: Vec::new(),
325            init_blocks: Vec::new(),
326            end_blocks: Vec::new(),
327            // Main program `my $x` uses [`Op::GetScalarSlot`] / [`Op::SetScalarSlot`] like subs,
328            // so hot loops are not stuck on [`Op::GetScalarPlain`] (linear scan per access).
329            scope_stack: vec![ScopeLayer {
330                use_slots: true,
331                ..Default::default()
332            }],
333            current_package: String::new(),
334            program_last_stmt_takes_value: false,
335            source_file: String::new(),
336            frame_depth: 0,
337            try_depth: 0,
338            loop_stack: Vec::new(),
339            goto_ctx_stack: Vec::new(),
340            strict_vars: false,
341            line_mode: false,
342            force_name_for_sort_pair: false,
343            sort_pair_block_indices: std::collections::HashSet::new(),
344            sub_body_block_indices: std::collections::HashSet::new(),
345            code_ref_block_params: Vec::new(),
346            block_scope_snapshots: Vec::new(),
347        }
348    }
349
350    /// Add a deferred-compilation block (map/grep/sort/reduce/…) to the chunk
351    /// AND snapshot the current `scope_stack` so the 4th pass can compile the
352    /// block under the lexical scope it was originally defined in. Replaces
353    /// every former `self.add_deferred_block(…)` call site inside the compiler so
354    /// no block slips through unsnapshotted. See [`Self::block_scope_snapshots`].
355    fn add_deferred_block(&mut self, block: Block) -> u16 {
356        let idx = self.chunk.add_block(block);
357        let snap = self.scope_stack.clone();
358        while self.block_scope_snapshots.len() < idx as usize {
359            self.block_scope_snapshots.push(None);
360        }
361        self.block_scope_snapshots.push(Some(snap));
362        idx
363    }
364
365    /// Mark a block index as a sort/reduce comparator that binds `$a`/`$b`.
366    /// The deferred 4th pass enables [`Self::force_name_for_sort_pair`] before
367    /// compiling registered indices so `$a`/`$b` references resolve via name
368    /// instead of any outer `my` slot allocation.
369    fn register_sort_pair_block(&mut self, idx: u16) {
370        self.sort_pair_block_indices.insert(idx);
371    }
372
373    /// Set `use strict 'vars'` at compile time. When enabled, [`compile_expr`] rejects any read
374    /// or write of an undeclared global scalar / array / hash with `CompileError::Frozen` — the
375    /// same diagnostic emitted at runtime (`Global symbol "$name" requires
376    /// explicit package name`). `try_vm_execute` pulls the flag from `Interpreter::strict_vars`
377    /// before constructing the compiler, matching the timing of
378    /// `prepare_program_top_level` (which processes `use strict` before main body execution).
379    pub fn with_strict_vars(mut self, v: bool) -> Self {
380        self.strict_vars = v;
381        self
382    }
383
384    /// Compile for `-n`/`-p` line mode: `END` blocks go into a separate region after the main
385    /// `Halt` so the per-line loop never runs them; the driver runs that region once after the
386    /// loop. See [`Self::line_mode`].
387    pub fn with_line_mode(mut self, v: bool) -> Self {
388        self.line_mode = v;
389        self
390    }
391
392    /// Enter a `goto LABEL` scope (called when compiling the top-level main program or a sub
393    /// body). Labels defined inside can be targeted from any `goto` inside the same scope;
394    /// labels are *not* shared across nested functions.
395    fn enter_goto_scope(&mut self) {
396        self.goto_ctx_stack.push(GotoCtx::default());
397    }
398
399    /// Resolve all pending forward gotos and pop the scope. Returns `CompileError::Frozen` if a
400    /// `goto` targets a label that was never defined in this scope (same diagnostic as
401    /// runtime: `goto: unknown label NAME`). Returns `Unsupported` if a
402    /// `goto` crosses a frame boundary (e.g. from inside an `if` body out to an outer label) —
403    /// crossing frames would skip `PopFrame` ops and corrupt the scope stack. That case is
404    /// not yet lowered to bytecode.
405    fn exit_goto_scope(&mut self) -> Result<(), CompileError> {
406        let ctx = self
407            .goto_ctx_stack
408            .pop()
409            .expect("exit_goto_scope called without matching enter");
410        for (jump_ip, label, line, goto_frame_depth) in ctx.pending {
411            if let Some(&(target_ip, label_frame_depth)) = ctx.labels.get(&label) {
412                if label_frame_depth != goto_frame_depth {
413                    return Err(CompileError::Unsupported(format!(
414                        "goto LABEL crosses a scope frame (label `{}` at depth {} vs goto at depth {})",
415                        label, label_frame_depth, goto_frame_depth
416                    )));
417                }
418                self.chunk.patch_jump_to(jump_ip, target_ip);
419            } else {
420                return Err(CompileError::Frozen {
421                    line,
422                    detail: format!("goto: unknown label {}", label),
423                });
424            }
425        }
426        Ok(())
427    }
428
429    /// Record `label → current IP` if a goto-scope is active. Called before each labeled
430    /// statement is emitted; the label points to the first op of the statement.
431    fn record_stmt_label(&mut self, label: &str) {
432        if let Some(top) = self.goto_ctx_stack.last_mut() {
433            top.labels
434                .insert(label.to_string(), (self.chunk.len(), self.frame_depth));
435        }
436    }
437
438    /// If `target` is a compile-time-known label name (bareword or literal string), emit a
439    /// forward `Jump(0)` and record it for patching on goto-scope exit. Returns `true` if the
440    /// goto was handled (so the caller should not emit a fallback). Returns `false` if the target
441    /// is dynamic — the caller should bail to `CompileError::Unsupported` so the tree path can
442    /// still handle it in future.
443    fn try_emit_goto_label(&mut self, target: &Expr, line: usize) -> bool {
444        let name = match &target.kind {
445            ExprKind::Bareword(n) => n.clone(),
446            ExprKind::String(s) => s.clone(),
447            // Parser may produce a zero-arg FuncCall for a bare label name
448            ExprKind::FuncCall { name, args } if args.is_empty() => name.clone(),
449            _ => return false,
450        };
451        if self.goto_ctx_stack.is_empty() {
452            return false;
453        }
454        let jump_ip = self.chunk.emit(Op::Jump(0), line);
455        let frame_depth = self.frame_depth;
456        self.goto_ctx_stack
457            .last_mut()
458            .expect("goto scope must be active")
459            .pending
460            .push((jump_ip, name, line, frame_depth));
461        true
462    }
463
464    /// Emit `Op::PushFrame` and bump [`Self::frame_depth`].
465    fn emit_push_frame(&mut self, line: usize) {
466        self.chunk.emit(Op::PushFrame, line);
467        self.frame_depth += 1;
468    }
469
470    /// Emit `Op::PopFrame` and decrement [`Self::frame_depth`] (saturating).
471    fn emit_pop_frame(&mut self, line: usize) {
472        self.chunk.emit(Op::PopFrame, line);
473        self.frame_depth = self.frame_depth.saturating_sub(1);
474    }
475    /// `with_source_file` — see implementation.
476    pub fn with_source_file(mut self, path: String) -> Self {
477        self.source_file = path;
478        self
479    }
480
481    /// `@ISA` / `@EXPORT` / `@EXPORT_OK` outside `main` → `Pkg::NAME` (see interpreter stash rules).
482    fn qualify_stash_array_name(&self, name: &str) -> String {
483        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
484            let pkg = &self.current_package;
485            if !pkg.is_empty() && pkg != "main" {
486                return format!("{}::{}", pkg, name);
487            }
488        }
489        name.to_string()
490    }
491
492    /// Package stash key for `our @arr` — matches
493    /// [`VMHelper::stash_array_full_name_for_package`].
494    fn qualify_stash_array_name_full(&self, name: &str) -> String {
495        if name.contains("::") {
496            return name.to_string();
497        }
498        let pkg = &self.current_package;
499        if pkg.is_empty() || pkg == "main" {
500            format!("main::{}", name)
501        } else {
502            format!("{}::{}", pkg, name)
503        }
504    }
505
506    /// Package stash key for `our %h` — companion to
507    /// [`Self::qualify_stash_array_name_full`].
508    fn qualify_stash_hash_name_full(&self, name: &str) -> String {
509        if name.contains("::") {
510            return name.to_string();
511        }
512        let pkg = &self.current_package;
513        if pkg.is_empty() || pkg == "main" {
514            format!("main::{}", name)
515        } else {
516            format!("{}::{}", pkg, name)
517        }
518    }
519
520    /// Runtime name for `@arr` reads after `my`/`our` resolution. Mirrors
521    /// [`Self::scalar_storage_name_for_ops`]. When no lexical match is found
522    /// and the current package is non-main, fall back to the package stash
523    /// — `our` decls outside the lazily-compiled sub-body scope chain still
524    /// route to `Pkg::name`, matching `qualify_sub_key`'s policy.
525    fn array_storage_name_for_ops(&self, bare: &str) -> String {
526        if bare.contains("::") {
527            return bare.to_string();
528        }
529        for layer in self.scope_stack.iter().rev() {
530            if layer.declared_arrays.contains(bare) {
531                if layer.declared_our_arrays.contains(bare) {
532                    return self.qualify_stash_array_name_full(bare);
533                }
534                return bare.to_string();
535            }
536        }
537        // Unbound bare name in a non-main package: assume package stash.
538        if !self.current_package.is_empty() && self.current_package != "main" {
539            return self.qualify_stash_array_name_full(bare);
540        }
541        bare.to_string()
542    }
543
544    /// Runtime name for `%hash` reads after `my`/`our` resolution. Same
545    /// fallback policy as [`Self::array_storage_name_for_ops`].
546    fn hash_storage_name_for_ops(&self, bare: &str) -> String {
547        if bare.contains("::") {
548            return bare.to_string();
549        }
550        for layer in self.scope_stack.iter().rev() {
551            if layer.declared_hashes.contains(bare) {
552                if layer.declared_our_hashes.contains(bare) {
553                    return self.qualify_stash_hash_name_full(bare);
554                }
555                return bare.to_string();
556            }
557        }
558        if !self.current_package.is_empty() && self.current_package != "main" {
559            return self.qualify_stash_hash_name_full(bare);
560        }
561        bare.to_string()
562    }
563
564    /// Package stash key for `our $name` (matches [`VMHelper::stash_scalar_name_for_package`]).
565    fn qualify_stash_scalar_name(&self, name: &str) -> String {
566        if name.contains("::") {
567            return name.to_string();
568        }
569        let pkg = &self.current_package;
570        if pkg.is_empty() || pkg == "main" {
571            format!("main::{}", name)
572        } else {
573            format!("{}::{}", pkg, name)
574        }
575    }
576
577    /// Runtime name for `$x` in bytecode after `my`/`our` resolution (`our` → qualified stash).
578    fn scalar_storage_name_for_ops(&self, bare: &str) -> String {
579        if bare.contains("::") {
580            return bare.to_string();
581        }
582        for layer in self.scope_stack.iter().rev() {
583            if layer.declared_scalars.contains(bare) {
584                if layer.declared_our_scalars.contains(bare) {
585                    return self.qualify_stash_scalar_name(bare);
586                }
587                return bare.to_string();
588            }
589        }
590        bare.to_string()
591    }
592
593    #[inline]
594    fn intern_scalar_var_for_ops(&mut self, bare: &str) -> u16 {
595        let s = self.scalar_storage_name_for_ops(bare);
596        self.chunk.intern_name(&s)
597    }
598
599    /// For `local $x`, qualify to package stash since local only works on package variables.
600    /// Special vars (like `$/`, `$\`, `$,`, `$"`, or `^X` caret vars) are not qualified.
601    fn intern_scalar_for_local(&mut self, bare: &str) -> u16 {
602        if VMHelper::is_special_scalar_name_for_set(bare) || bare.starts_with('^') {
603            self.chunk.intern_name(bare)
604        } else {
605            let s = self.qualify_stash_scalar_name(bare);
606            self.chunk.intern_name(&s)
607        }
608    }
609
610    fn register_declare_our_scalar(&mut self, bare_name: &str) {
611        let layer = self.scope_stack.last_mut().expect("scope stack");
612        layer.declared_scalars.insert(bare_name.to_string());
613        layer.declared_our_scalars.insert(bare_name.to_string());
614    }
615
616    /// Per-phase-block accumulation of the `our` (package-global) declarations that appear
617    /// textually **before** each phase block, in source order (aligned with `begin_blocks`
618    /// etc.). Used to satisfy `use strict 'vars'` for phase blocks, which compile out of
619    /// source order (grouped by phase) and would otherwise not see an `our` declared earlier
620    /// in the main body. Perl: `our $x; BEGIN { $x }` is fine; `BEGIN { $x } our $x;` errors —
621    /// hence the textual-position accumulation rather than a whole-program union.
622    fn collect_phase_our_predecls(program: &Program) -> [Vec<Vec<(Sigil, String)>>; 4] {
623        let mut acc: Vec<(Sigil, String)> = Vec::new();
624        let mut begin = Vec::new();
625        let mut unitcheck = Vec::new();
626        let mut check = Vec::new();
627        let mut init = Vec::new();
628        for stmt in &program.statements {
629            match &stmt.kind {
630                StmtKind::Our(decls) | StmtKind::OurSync(decls) => {
631                    for d in decls {
632                        acc.push((d.sigil, d.name.clone()));
633                    }
634                }
635                StmtKind::Begin(_) => begin.push(acc.clone()),
636                StmtKind::UnitCheck(_) => unitcheck.push(acc.clone()),
637                StmtKind::Check(_) => check.push(acc.clone()),
638                StmtKind::Init(_) => init.push(acc.clone()),
639                _ => {}
640            }
641        }
642        [begin, unitcheck, check, init]
643    }
644
645    /// Register `our`-declared names (from [`Self::collect_phase_our_predecls`]) into the current
646    /// scope layer so a phase block compiled before its main-body declaration passes strict-vars.
647    fn register_our_predecls(&mut self, names: &[(Sigil, String)]) {
648        for (sigil, name) in names {
649            match sigil {
650                Sigil::Scalar => self.register_declare_our_scalar(name),
651                Sigil::Array => {
652                    if let Some(layer) = self.scope_stack.last_mut() {
653                        layer.declared_arrays.insert(name.clone());
654                        layer.declared_our_arrays.insert(name.clone());
655                    }
656                }
657                Sigil::Hash => {
658                    if let Some(layer) = self.scope_stack.last_mut() {
659                        layer.declared_hashes.insert(name.clone());
660                        layer.declared_our_hashes.insert(name.clone());
661                    }
662                }
663                Sigil::Typeglob => {}
664            }
665        }
666    }
667
668    /// `our $x` — package stash binding; no slot indices (bare `$x` maps to `main::x` / `Pkg::x`).
669    fn emit_declare_our_scalar(&mut self, bare_name: &str, line: usize, frozen: bool) {
670        let stash = self.qualify_stash_scalar_name(bare_name);
671        let stash_idx = self.chunk.intern_name(&stash);
672        self.register_declare_our_scalar(bare_name);
673        if frozen {
674            self.chunk.emit(Op::DeclareScalarFrozen(stash_idx), line);
675        } else {
676            self.chunk.emit(Op::DeclareScalar(stash_idx), line);
677        }
678    }
679
680    /// Call-site stash key for a subroutine name in the current package.
681    ///
682    /// Rule: a bare call inside `package Foo` qualifies to `Foo::name` so user
683    /// subs in the package are reachable by their short spelling — **unless**
684    /// `name` is one of the 11k+ global callable spellings (`is_callable_spelling`).
685    /// Builtins are always reachable bare in any package; a user sub in a
686    /// non-main package that shares a name with a builtin must be called by
687    /// its fully-qualified `Pkg::name` spelling. `--compat` (Perl 5 mode)
688    /// restores classic UDF-wins semantics so unmodified Perl 5 modules
689    /// keep working.
690    fn qualify_sub_key(&self, name: &str) -> String {
691        if name.contains("::") {
692            return name.to_string();
693        }
694        let pkg = &self.current_package;
695        if pkg.is_empty() || pkg == "main" {
696            return name.to_string();
697        }
698        if !crate::compat_mode() && crate::builtins::is_callable_spelling(name) {
699            return name.to_string();
700        }
701        format!("{}::{}", pkg, name)
702    }
703
704    /// Decl-site stash key — always qualifies to `Pkg::name` (or bare in
705    /// `main`). Mirrors [`Self::qualify_sub_decl_pass1`] so the storage
706    /// key used at pass-3 body compilation matches the entry recorded in
707    /// pass-1 sub registration even when the bare name collides with a
708    /// builtin spelling.
709    fn qualify_sub_decl_key(&self, name: &str) -> String {
710        if name.contains("::") {
711            return name.to_string();
712        }
713        let pkg = &self.current_package;
714        if pkg.is_empty() || pkg == "main" {
715            name.to_string()
716        } else {
717            format!("{}::{}", pkg, name)
718        }
719    }
720
721    /// First-pass sub registration: walk `package` statements like [`Self::compile_program`] does for
722    /// sub bodies so forward `sub` entries use the same stash key as runtime registration.
723    fn qualify_sub_decl_pass1(name: &str, pending_pkg: &str) -> String {
724        if name.contains("::") {
725            return name.to_string();
726        }
727        if pending_pkg.is_empty() || pending_pkg == "main" {
728            name.to_string()
729        } else {
730            format!("{}::{}", pending_pkg, name)
731        }
732    }
733
734    /// After all `sub` bodies are lowered, replace [`Op::Call`] with [`Op::CallStaticSubId`] when the
735    /// callee has a compiled entry (avoids linear `sub_entries` scan + extra stash work per call).
736    ///
737    /// Skip the peephole for bare callable spellings (`sum`, `set`, `count`, …) in default mode:
738    /// those calls must route to the global builtin at runtime even if a same-named user sub is
739    /// registered. `Op::CallStaticSubId` jumps straight into the resolved user-sub entry, which
740    /// would shadow the builtin and break the no-shadow invariant. `--compat` (Perl 5 mode)
741    /// restores UDF-wins semantics so this fast path applies to every named call.
742    fn patch_static_sub_calls(chunk: &mut Chunk) {
743        let allow_shadow_builtins = crate::compat_mode();
744        for i in 0..chunk.ops.len() {
745            if let Op::Call(name_idx, argc, wa) = chunk.ops[i] {
746                if !allow_shadow_builtins {
747                    let name = &chunk.names[name_idx as usize];
748                    if !name.contains("::") && crate::builtins::is_callable_spelling(name) {
749                        continue;
750                    }
751                }
752                if let Some((entry_ip, stack_args)) = chunk.find_sub_entry(name_idx) {
753                    if chunk.static_sub_calls.len() < u16::MAX as usize {
754                        let sid = chunk.static_sub_calls.len() as u16;
755                        chunk
756                            .static_sub_calls
757                            .push((entry_ip, stack_args, name_idx));
758                        chunk.ops[i] = Op::CallStaticSubId(sid, name_idx, argc, wa);
759                    }
760                }
761            }
762        }
763    }
764
765    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the stack must hold the **array reference**
766    /// (scalar), not `@{...}` / `@$r` expansion (which would push a cloned plain array).
767    fn compile_arrow_array_base_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
768        if let ExprKind::Deref {
769            expr: inner,
770            kind: Sigil::Array | Sigil::Scalar,
771        } = &expr.kind
772        {
773            self.compile_expr(inner)
774        } else {
775            self.compile_expr(expr)
776        }
777    }
778
779    /// For `$href->{k}` / `$$r{k}`: stack holds the hash **reference** scalar, not a copied `%` value.
780    fn compile_arrow_hash_base_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
781        if let ExprKind::Deref {
782            expr: inner,
783            kind: Sigil::Scalar,
784        } = &expr.kind
785        {
786            self.compile_expr(inner)
787        } else {
788            self.compile_expr(expr)
789        }
790    }
791
792    fn push_scope_layer(&mut self) {
793        self.scope_stack.push(ScopeLayer::default());
794    }
795
796    /// Push a scope layer with slot assignment enabled (for subroutine bodies).
797    fn push_scope_layer_with_slots(&mut self) {
798        self.scope_stack.push(ScopeLayer {
799            use_slots: true,
800            is_sub_body: true,
801            ..Default::default()
802        });
803    }
804
805    /// Register a signature parameter (`fn foo($x, @args, %opts) { ... }`) in
806    /// the current scope layer so the closure-write check (DESIGN-001)
807    /// recognises writes to the param as local — not outer-scope `my`.
808    fn register_sig_param(&mut self, p: &crate::ast::SubSigParam) {
809        use crate::ast::SubSigParam;
810        let layer = self.scope_stack.last_mut().expect("scope stack");
811        match p {
812            SubSigParam::Scalar(name, _, _) => {
813                layer.declared_scalars.insert(name.clone());
814            }
815            SubSigParam::Array(name, _) => {
816                layer.declared_arrays.insert(name.clone());
817            }
818            SubSigParam::Hash(name, _) => {
819                layer.declared_hashes.insert(name.clone());
820            }
821            SubSigParam::ArrayDestruct(elems) => {
822                for el in elems {
823                    match el {
824                        crate::ast::MatchArrayElem::CaptureScalar(n) => {
825                            layer.declared_scalars.insert(n.clone());
826                        }
827                        crate::ast::MatchArrayElem::RestBind(n) => {
828                            layer.declared_arrays.insert(n.clone());
829                        }
830                        _ => {}
831                    }
832                }
833            }
834            SubSigParam::HashDestruct(pairs) => {
835                for (_, var) in pairs {
836                    layer.declared_scalars.insert(var.clone());
837                }
838            }
839        }
840    }
841
842    fn pop_scope_layer(&mut self) {
843        if self.scope_stack.len() > 1 {
844            self.scope_stack.pop();
845        }
846    }
847
848    /// Look up a scalar's slot index in the current scope layer (if slots are enabled).
849    fn scalar_slot(&self, name: &str) -> Option<u8> {
850        // Deferred sort-block compilation: `$a`/`$b` must be name-based so the
851        // runtime `set_sort_pair` (which writes by name) and the comparator's
852        // reads agree. Slot allocation from any outer `my $a`/`my $b` is
853        // visible here because the 4th pass runs after the whole program is
854        // compiled. See [`Self::force_name_for_sort_pair`].
855        if self.force_name_for_sort_pair && (name == "a" || name == "b") {
856            return None;
857        }
858        if let Some(layer) = self.scope_stack.last() {
859            if layer.use_slots {
860                return layer.scalar_slots.get(name).copied();
861            }
862        }
863        None
864    }
865
866    /// Intern an [`Expr`] for [`Chunk::op_ast_expr`] (pointer-stable during compile).
867    fn intern_ast_expr(&mut self, expr: &Expr) -> u32 {
868        let p = expr as *const Expr as usize;
869        if let Some(&id) = self.ast_expr_intern.get(&p) {
870            return id;
871        }
872        let id = self.chunk.ast_expr_pool.len() as u32;
873        self.chunk.ast_expr_pool.push(expr.clone());
874        self.ast_expr_intern.insert(p, id);
875        id
876    }
877
878    /// Emit one opcode with optional link to the originating expression (expression compiler path).
879    #[inline]
880    fn emit_op(&mut self, op: Op, line: usize, ast: Option<&Expr>) -> usize {
881        let idx = ast.map(|e| self.intern_ast_expr(e));
882        self.chunk.emit_with_ast_idx(op, line, idx)
883    }
884
885    /// Emit GetScalar or GetScalarSlot depending on whether the variable has a slot.
886    fn emit_get_scalar(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
887        let name = &self.chunk.names[name_idx as usize];
888        if let Some(slot) = self.scalar_slot(name) {
889            self.emit_op(Op::GetScalarSlot(slot), line, ast);
890        } else if VMHelper::is_special_scalar_name_for_get(name) {
891            self.emit_op(Op::GetScalar(name_idx), line, ast);
892        } else {
893            self.emit_op(Op::GetScalarPlain(name_idx), line, ast);
894        }
895    }
896
897    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl), not “regex object is truthy”.
898    /// Emits `$_` + pattern and [`Op::RegexMatchDyn`] so match vars and truthy 0/1 match `=~`.
899    fn compile_boolean_rvalue_condition(&mut self, cond: &Expr) -> Result<(), CompileError> {
900        let line = cond.line;
901        if let ExprKind::Regex(pattern, flags) = &cond.kind {
902            let name_idx = self.chunk.intern_name("_");
903            self.emit_get_scalar(name_idx, line, Some(cond));
904            let pat_idx = self
905                .chunk
906                .add_constant(StrykeValue::string(pattern.clone()));
907            let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
908            self.emit_op(Op::LoadRegex(pat_idx, flags_idx), line, Some(cond));
909            self.emit_op(Op::RegexMatchDyn(false), line, Some(cond));
910            Ok(())
911        } else if matches!(&cond.kind, ExprKind::ReadLine(_)) {
912            // `while (<STDIN>)` — assign line to `$_` then test definedness (Perl).
913            self.compile_expr(cond)?;
914            let name_idx = self.chunk.intern_name("_");
915            self.emit_set_scalar_keep(name_idx, line, Some(cond));
916            self.emit_op(
917                Op::CallBuiltin(BuiltinId::Defined as u16, 1),
918                line,
919                Some(cond),
920            );
921            Ok(())
922        } else {
923            self.compile_expr(cond)
924        }
925    }
926
927    /// Emit SetScalar or SetScalarSlot depending on slot availability.
928    fn emit_set_scalar(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
929        let name = &self.chunk.names[name_idx as usize];
930        if let Some(slot) = self.scalar_slot(name) {
931            self.emit_op(Op::SetScalarSlot(slot), line, ast);
932        } else if VMHelper::is_special_scalar_name_for_set(name) {
933            self.emit_op(Op::SetScalar(name_idx), line, ast);
934        } else {
935            self.emit_op(Op::SetScalarPlain(name_idx), line, ast);
936        }
937    }
938
939    /// Emit SetScalarKeep or SetScalarSlotKeep depending on slot availability.
940    fn emit_set_scalar_keep(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
941        let name = &self.chunk.names[name_idx as usize];
942        if let Some(slot) = self.scalar_slot(name) {
943            self.emit_op(Op::SetScalarSlotKeep(slot), line, ast);
944        } else if VMHelper::is_special_scalar_name_for_set(name) {
945            self.emit_op(Op::SetScalarKeep(name_idx), line, ast);
946        } else {
947            self.emit_op(Op::SetScalarKeepPlain(name_idx), line, ast);
948        }
949    }
950
951    fn emit_pre_inc(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
952        let name = &self.chunk.names[name_idx as usize];
953        if let Some(slot) = self.scalar_slot(name) {
954            self.emit_op(Op::PreIncSlot(slot), line, ast);
955        } else {
956            self.emit_op(Op::PreInc(name_idx), line, ast);
957        }
958    }
959
960    fn emit_pre_dec(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
961        let name = &self.chunk.names[name_idx as usize];
962        if let Some(slot) = self.scalar_slot(name) {
963            self.emit_op(Op::PreDecSlot(slot), line, ast);
964        } else {
965            self.emit_op(Op::PreDec(name_idx), line, ast);
966        }
967    }
968
969    fn emit_post_inc(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
970        let name = &self.chunk.names[name_idx as usize];
971        if let Some(slot) = self.scalar_slot(name) {
972            self.emit_op(Op::PostIncSlot(slot), line, ast);
973        } else {
974            self.emit_op(Op::PostInc(name_idx), line, ast);
975        }
976    }
977
978    fn emit_post_dec(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
979        let name = &self.chunk.names[name_idx as usize];
980        if let Some(slot) = self.scalar_slot(name) {
981            self.emit_op(Op::PostDecSlot(slot), line, ast);
982        } else {
983            self.emit_op(Op::PostDec(name_idx), line, ast);
984        }
985    }
986
987    /// Assign a new slot index for a scalar in the current scope layer.
988    /// Returns the slot index if slots are enabled, None otherwise.
989    fn assign_scalar_slot(&mut self, name: &str) -> Option<u8> {
990        if let Some(layer) = self.scope_stack.last_mut() {
991            if layer.use_slots && layer.next_scalar_slot < 255 {
992                let slot = layer.next_scalar_slot;
993                layer.scalar_slots.insert(name.to_string(), slot);
994                layer.next_scalar_slot += 1;
995                return Some(slot);
996            }
997        }
998        None
999    }
1000
1001    fn register_declare(&mut self, sigil: Sigil, name: &str, frozen: bool) {
1002        let layer = self.scope_stack.last_mut().expect("scope stack");
1003        // Re-declaring the same name with a different mutability must REPLACE
1004        // the frozen flag, not OR it. Top-level `{...}` blocks that share a
1005        // slot-active layer with the file scope previously leaked block A's
1006        // `val $x` frozen flag into block B's `var $x` declaration because
1007        // the else branch was a no-op.
1008        match sigil {
1009            Sigil::Scalar => {
1010                layer.declared_scalars.insert(name.to_string());
1011                if frozen {
1012                    layer.frozen_scalars.insert(name.to_string());
1013                } else {
1014                    layer.frozen_scalars.remove(name);
1015                }
1016            }
1017            Sigil::Array => {
1018                layer.declared_arrays.insert(name.to_string());
1019                if frozen {
1020                    layer.frozen_arrays.insert(name.to_string());
1021                } else {
1022                    layer.frozen_arrays.remove(name);
1023                }
1024            }
1025            Sigil::Hash => {
1026                layer.declared_hashes.insert(name.to_string());
1027                if frozen {
1028                    layer.frozen_hashes.insert(name.to_string());
1029                } else {
1030                    layer.frozen_hashes.remove(name);
1031                }
1032            }
1033            Sigil::Typeglob => {
1034                layer.declared_scalars.insert(name.to_string());
1035            }
1036        }
1037    }
1038
1039    /// `use strict 'vars'` check for a scalar `$name`. Mirrors [`VMHelper::check_strict_scalar_var`]:
1040    /// ok if strict is off, the name contains `::` (package-qualified), the name is a Perl special
1041    /// scalar, or the name is declared via `my`/`our` in any enclosing compiler scope layer.
1042    /// Otherwise errors with the exact diagnostic message so the user sees a consistent
1043    /// error from the VM.
1044    fn check_strict_scalar_access(&self, name: &str, line: usize) -> Result<(), CompileError> {
1045        if !self.strict_vars
1046            || name.contains("::")
1047            || VMHelper::strict_scalar_exempt(name)
1048            || VMHelper::is_special_scalar_name_for_get(name)
1049            || self
1050                .scope_stack
1051                .iter()
1052                .any(|l| l.declared_scalars.contains(name))
1053        {
1054            return Ok(());
1055        }
1056        Err(CompileError::Frozen {
1057            line,
1058            detail: format!(
1059                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
1060                name, name
1061            ),
1062        })
1063    }
1064
1065    /// Array names that are always bound at runtime (Perl built-ins) and must not trigger a
1066    /// `use strict 'vars'` compile error even though they're never `my`-declared.
1067    /// Stryke topic-var arrays (`@_0`, `@_1`, …) are auto-bound inside any
1068    /// block that takes positional arguments — exempt them too so
1069    /// `$_1[0]` / `@_1[0]` / etc. compile under `use strict 'vars'`.
1070    fn strict_array_exempt(name: &str) -> bool {
1071        if name.starts_with("__topicstr__") {
1072            return true;
1073        }
1074        if matches!(
1075            name,
1076            "_" | "ARGV" | "INC" | "ENV" | "ISA" | "EXPORT" | "EXPORT_OK" | "EXPORT_FAIL"
1077        ) {
1078            return true;
1079        }
1080        name.starts_with('_') && name.len() > 1 && name[1..].chars().all(|c| c.is_ascii_digit())
1081    }
1082
1083    /// Hash names that are always bound at runtime. Also exempts stryke
1084    /// topic-var hashes (`%_0`, `%_1`, …) for symmetry with the scalar
1085    /// and array forms.
1086    fn strict_hash_exempt(name: &str) -> bool {
1087        if matches!(
1088            name,
1089            "ENV" | "INC" | "SIG" | "EXPORT_TAGS" | "ISA" | "OVERLOAD" | "+" | "-" | "!" | "^H"
1090        ) {
1091            return true;
1092        }
1093        name.starts_with('_') && name.len() > 1 && name[1..].chars().all(|c| c.is_ascii_digit())
1094    }
1095
1096    fn check_strict_array_access(&self, name: &str, line: usize) -> Result<(), CompileError> {
1097        if !self.strict_vars
1098            || name.contains("::")
1099            || Self::strict_array_exempt(name)
1100            || self
1101                .scope_stack
1102                .iter()
1103                .any(|l| l.declared_arrays.contains(name))
1104        {
1105            return Ok(());
1106        }
1107        Err(CompileError::Frozen {
1108            line,
1109            detail: format!(
1110                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
1111                name, name
1112            ),
1113        })
1114    }
1115
1116    fn check_strict_hash_access(&self, name: &str, line: usize) -> Result<(), CompileError> {
1117        if !self.strict_vars
1118            || name.contains("::")
1119            || Self::strict_hash_exempt(name)
1120            || self
1121                .scope_stack
1122                .iter()
1123                .any(|l| l.declared_hashes.contains(name))
1124        {
1125            return Ok(());
1126        }
1127        Err(CompileError::Frozen {
1128            line,
1129            detail: format!(
1130                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
1131                name, name
1132            ),
1133        })
1134    }
1135
1136    fn check_scalar_mutable(&self, name: &str, line: usize) -> Result<(), CompileError> {
1137        for layer in self.scope_stack.iter().rev() {
1138            if layer.declared_scalars.contains(name) {
1139                if layer.frozen_scalars.contains(name) {
1140                    return Err(CompileError::Frozen {
1141                        line,
1142                        detail: format!("cannot assign to frozen variable `${}`", name),
1143                    });
1144                }
1145                return Ok(());
1146            }
1147        }
1148        Ok(())
1149    }
1150
1151    /// Closure-write rule (DESIGN-001): writing to an outer-scope `my $x`
1152    /// from inside a sub body silently mutates only the closure's snapshot
1153    /// — the outer scope keeps its own value, which is almost always a
1154    /// bug. Reject at compile time and point the user at `mysync $x` (for
1155    /// shared mutable state) or `--compat` (for Perl 5 shared semantics).
1156    ///
1157    /// Returns Ok(()) when:
1158    ///   - `name` is declared in the innermost sub-body layer (locally owned).
1159    ///   - `name` is `mysync` (atomic shared cell — safe to mutate).
1160    ///   - `name` is `our` (package global — explicitly cross-scope state).
1161    ///   - `--compat` mode is active (Perl 5 shared-storage semantics).
1162    ///   - `name` is undeclared (caught by strict-vars elsewhere).
1163    fn check_closure_write_to_outer_my(&self, name: &str, line: usize) -> Result<(), CompileError> {
1164        if crate::compat_mode() {
1165            return Ok(());
1166        }
1167        // Walk innermost-first. Look for the boundary: any layer between us
1168        // and the declaring layer that is a sub body (`is_sub_body`) means
1169        // the variable was declared outside the closure we're currently in.
1170        let mut crossed_sub_body = false;
1171        for layer in self.scope_stack.iter().rev() {
1172            if layer.declared_scalars.contains(name) {
1173                if !crossed_sub_body {
1174                    return Ok(());
1175                }
1176                if layer.mysync_scalars.contains(name) {
1177                    return Ok(());
1178                }
1179                if layer.declared_our_scalars.contains(name) {
1180                    return Ok(());
1181                }
1182                return Err(CompileError::Frozen {
1183                    line,
1184                    detail: format!(
1185                        "cannot modify outer-scope `my ${name}` from inside a closure — \
1186                         stryke closures capture by value to keep parallel dispatch race-free. \
1187                         Use `mysync ${name}` for shared mutable state, or `--compat` for Perl 5 \
1188                         shared-storage semantics"
1189                    ),
1190                });
1191            }
1192            if layer.is_sub_body {
1193                crossed_sub_body = true;
1194            }
1195        }
1196        Ok(())
1197    }
1198
1199    fn check_array_mutable(&self, name: &str, line: usize) -> Result<(), CompileError> {
1200        for layer in self.scope_stack.iter().rev() {
1201            if layer.declared_arrays.contains(name) {
1202                if layer.frozen_arrays.contains(name) {
1203                    return Err(CompileError::Frozen {
1204                        line,
1205                        detail: format!("cannot modify frozen array `@{}`", name),
1206                    });
1207                }
1208                return Ok(());
1209            }
1210        }
1211        Ok(())
1212    }
1213
1214    fn check_hash_mutable(&self, name: &str, line: usize) -> Result<(), CompileError> {
1215        for layer in self.scope_stack.iter().rev() {
1216            if layer.declared_hashes.contains(name) {
1217                if layer.frozen_hashes.contains(name) {
1218                    return Err(CompileError::Frozen {
1219                        line,
1220                        detail: format!("cannot modify frozen hash `%{}`", name),
1221                    });
1222                }
1223                return Ok(());
1224            }
1225        }
1226        Ok(())
1227    }
1228
1229    /// Register variables declared by `use Env qw(@PATH $HOME ...)` so the strict-vars
1230    /// compiler pass knows they exist.
1231    fn register_env_imports(layer: &mut ScopeLayer, imports: &[Expr]) {
1232        for e in imports {
1233            let mut names_owned: Vec<String> = Vec::new();
1234            match &e.kind {
1235                ExprKind::String(s) => names_owned.push(s.clone()),
1236                ExprKind::QW(ws) => names_owned.extend(ws.iter().cloned()),
1237                ExprKind::InterpolatedString(parts) => {
1238                    let mut s = String::new();
1239                    for p in parts {
1240                        match p {
1241                            StringPart::Literal(l) => s.push_str(l),
1242                            StringPart::ScalarVar(v) => {
1243                                s.push('$');
1244                                s.push_str(v);
1245                            }
1246                            StringPart::ArrayVar(v) => {
1247                                s.push('@');
1248                                s.push_str(v);
1249                            }
1250                            _ => continue,
1251                        }
1252                    }
1253                    names_owned.push(s);
1254                }
1255                _ => continue,
1256            };
1257            for raw in &names_owned {
1258                if let Some(arr) = raw.strip_prefix('@') {
1259                    layer.declared_arrays.insert(arr.to_string());
1260                } else if let Some(hash) = raw.strip_prefix('%') {
1261                    layer.declared_hashes.insert(hash.to_string());
1262                } else {
1263                    let scalar = raw.strip_prefix('$').unwrap_or(raw);
1264                    layer.declared_scalars.insert(scalar.to_string());
1265                }
1266            }
1267        }
1268    }
1269
1270    /// Emit an `Op::RuntimeErrorConst` that matches Perl's
1271    /// `Can't modify {array,hash} dereference in {pre,post}{increment,decrement} (++|--)` message.
1272    /// Used for `++@{…}`, `%{…}--`, `@$r++`, etc. — constructs that are invalid in Perl 5.
1273    /// Pushes `LoadUndef` afterwards so the rvalue position has a value on the stack for any
1274    /// surrounding `Pop` from statement-expression dispatch (the error op aborts the VM before
1275    /// the `LoadUndef` is reached, but it keeps the emitted sequence well-formed for stack tracking).
1276    fn emit_aggregate_symbolic_inc_dec_error(
1277        &mut self,
1278        kind: Sigil,
1279        is_pre: bool,
1280        is_inc: bool,
1281        line: usize,
1282        root: &Expr,
1283    ) -> Result<(), CompileError> {
1284        let agg = match kind {
1285            Sigil::Array => "array",
1286            Sigil::Hash => "hash",
1287            _ => {
1288                return Err(CompileError::Unsupported(
1289                    "internal: non-aggregate sigil passed to symbolic ++/-- error emitter".into(),
1290                ));
1291            }
1292        };
1293        let op_str = match (is_pre, is_inc) {
1294            (true, true) => "preincrement (++)",
1295            (true, false) => "predecrement (--)",
1296            (false, true) => "postincrement (++)",
1297            (false, false) => "postdecrement (--)",
1298        };
1299        let msg = format!("Can't modify {} dereference in {}", agg, op_str);
1300        let idx = self.chunk.add_constant(StrykeValue::string(msg));
1301        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
1302        // The op never returns; this LoadUndef is dead code but keeps any unreachable
1303        // `Pop` / rvalue consumer emitted by the enclosing dispatch well-formed.
1304        self.emit_op(Op::LoadUndef, line, Some(root));
1305        Ok(())
1306    }
1307
1308    /// `mysync @arr` / `mysync %h` — aggregate element updates use `atomic_*_mutate`, not yet lowered to bytecode.
1309    fn is_mysync_array(&self, array_name: &str) -> bool {
1310        let q = self.qualify_stash_array_name(array_name);
1311        self.scope_stack
1312            .iter()
1313            .rev()
1314            .any(|l| l.mysync_arrays.contains(&q))
1315    }
1316
1317    fn is_mysync_hash(&self, hash_name: &str) -> bool {
1318        self.scope_stack
1319            .iter()
1320            .rev()
1321            .any(|l| l.mysync_hashes.contains(hash_name))
1322    }
1323    /// `compile_program` — see implementation.
1324    pub fn compile_program(mut self, program: &Program) -> Result<Chunk, CompileError> {
1325        // Extract BEGIN/END blocks before compiling.
1326        for stmt in &program.statements {
1327            match &stmt.kind {
1328                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
1329                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
1330                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
1331                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
1332                StmtKind::End(block) => self.end_blocks.push(block.clone()),
1333                _ => {}
1334            }
1335        }
1336
1337        // First pass: register sub names for forward calls (qualified stash keys, same as runtime).
1338        let mut pending_pkg = String::new();
1339        for stmt in &program.statements {
1340            match &stmt.kind {
1341                StmtKind::Package { name } => pending_pkg = name.clone(),
1342                StmtKind::SubDecl { name, .. } => {
1343                    let q = Self::qualify_sub_decl_pass1(name, &pending_pkg);
1344                    let name_idx = self.chunk.intern_name(&q);
1345                    self.chunk.sub_entries.push((name_idx, 0, false));
1346                }
1347                _ => {}
1348            }
1349        }
1350
1351        // Second pass: compile main body.
1352        // The last expression statement keeps its value on the stack so the
1353        // caller can read the program's return value (like Perl's implicit return).
1354        let main_stmts: Vec<&Statement> = program
1355            .statements
1356            .iter()
1357            .filter(|s| {
1358                !matches!(
1359                    s.kind,
1360                    StmtKind::SubDecl { .. }
1361                        | StmtKind::Begin(_)
1362                        | StmtKind::UnitCheck(_)
1363                        | StmtKind::Check(_)
1364                        | StmtKind::Init(_)
1365                        | StmtKind::End(_)
1366                )
1367            })
1368            .collect();
1369        let last_idx = main_stmts.len().saturating_sub(1);
1370        self.program_last_stmt_takes_value = main_stmts
1371            .last()
1372            .map(|s| matches!(s.kind, StmtKind::TryCatch { .. }))
1373            .unwrap_or(false);
1374        // Strict-vars: a top-level `our $x` declares a package global visible to phase blocks
1375        // that appear textually AFTER it (Perl). Phase blocks compile here out of source order
1376        // (grouped by phase), so pre-register, per block, the `our` names declared before it.
1377        // Snapshot the file scope first and restore it afterward so these registrations don't
1378        // leak into the main body, where `our` must still be declared before use, in order.
1379        let [begin_pre, unitcheck_pre, check_pre, init_pre] =
1380            Self::collect_phase_our_predecls(program);
1381        let strict_snapshot = self.scope_stack.last().cloned();
1382
1383        // BEGIN blocks run before main (same order as Perl phase blocks).
1384        if !self.begin_blocks.is_empty() {
1385            self.chunk.emit(Op::SetGlobalPhase(GP_START), 0);
1386        }
1387        for (i, block) in self.begin_blocks.clone().iter().enumerate() {
1388            if let Some(pre) = begin_pre.get(i) {
1389                self.register_our_predecls(pre);
1390            }
1391            self.compile_block(block)?;
1392        }
1393        // Perl: `${^GLOBAL_PHASE}` stays **`START`** during UNITCHECK blocks.
1394        let unit_check_rev: Vec<(usize, Block)> =
1395            self.unit_check_blocks.iter().cloned().enumerate().rev().collect();
1396        for (i, block) in unit_check_rev {
1397            if let Some(pre) = unitcheck_pre.get(i) {
1398                self.register_our_predecls(pre);
1399            }
1400            self.compile_block(&block)?;
1401        }
1402        if !self.check_blocks.is_empty() {
1403            self.chunk.emit(Op::SetGlobalPhase(GP_CHECK), 0);
1404        }
1405        let check_rev: Vec<(usize, Block)> =
1406            self.check_blocks.iter().cloned().enumerate().rev().collect();
1407        for (i, block) in check_rev {
1408            if let Some(pre) = check_pre.get(i) {
1409                self.register_our_predecls(pre);
1410            }
1411            self.compile_block(&block)?;
1412        }
1413        if !self.init_blocks.is_empty() {
1414            self.chunk.emit(Op::SetGlobalPhase(GP_INIT), 0);
1415        }
1416        for (i, block) in self.init_blocks.clone().iter().enumerate() {
1417            if let Some(pre) = init_pre.get(i) {
1418                self.register_our_predecls(pre);
1419            }
1420            self.compile_block(block)?;
1421        }
1422
1423        // Restore file scope so phase-block predecls don't leak into the main body.
1424        if let Some(snap) = strict_snapshot {
1425            if let Some(last) = self.scope_stack.last_mut() {
1426                *last = snap;
1427            }
1428        }
1429        self.chunk.emit(Op::SetGlobalPhase(GP_RUN), 0);
1430        // Record where the main body starts — used by `-n`/`-p` to re-execute only the body per line.
1431        self.chunk.body_start_ip = self.chunk.ops.len();
1432
1433        // Top-level `goto LABEL` scope: labels defined on main-program statements are targetable
1434        // from `goto` statements in the same main program. Pushed before the main loop and
1435        // resolved after it (but before END blocks, which run in their own scope).
1436        self.enter_goto_scope();
1437
1438        let mut i = 0;
1439        while i < main_stmts.len() {
1440            let stmt = main_stmts[i];
1441            if i == last_idx {
1442                // The specialized `last statement leaves its value on the stack` path bypasses
1443                // `compile_statement` for Expression/If/Unless shapes, so we must record any
1444                // `LABEL:` on this statement manually before emitting its ops.
1445                if let Some(lbl) = &stmt.label {
1446                    self.record_stmt_label(lbl);
1447                }
1448                match &stmt.kind {
1449                    StmtKind::Expression(expr) => {
1450                        // Last statement of program: still not a regex *value* — bare `/pat/` matches `$_`.
1451                        if matches!(&expr.kind, ExprKind::Regex(..)) {
1452                            self.compile_boolean_rvalue_condition(expr)?;
1453                        } else {
1454                            self.compile_expr(expr)?;
1455                        }
1456                    }
1457                    StmtKind::If {
1458                        condition,
1459                        body,
1460                        elsifs,
1461                        else_block,
1462                    } => {
1463                        self.compile_boolean_rvalue_condition(condition)?;
1464                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
1465                        self.emit_block_value(body, stmt.line)?;
1466                        let mut ends = vec![self.chunk.emit(Op::Jump(0), stmt.line)];
1467                        self.chunk.patch_jump_here(j0);
1468                        for (c, blk) in elsifs {
1469                            self.compile_boolean_rvalue_condition(c)?;
1470                            let j = self.chunk.emit(Op::JumpIfFalse(0), c.line);
1471                            self.emit_block_value(blk, c.line)?;
1472                            ends.push(self.chunk.emit(Op::Jump(0), c.line));
1473                            self.chunk.patch_jump_here(j);
1474                        }
1475                        if let Some(eb) = else_block {
1476                            self.emit_block_value(eb, stmt.line)?;
1477                        } else {
1478                            self.chunk.emit(Op::LoadUndef, stmt.line);
1479                        }
1480                        for j in ends {
1481                            self.chunk.patch_jump_here(j);
1482                        }
1483                    }
1484                    StmtKind::Unless {
1485                        condition,
1486                        body,
1487                        else_block,
1488                    } => {
1489                        self.compile_boolean_rvalue_condition(condition)?;
1490                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
1491                        if let Some(eb) = else_block {
1492                            self.emit_block_value(eb, stmt.line)?;
1493                        } else {
1494                            self.chunk.emit(Op::LoadUndef, stmt.line);
1495                        }
1496                        let end = self.chunk.emit(Op::Jump(0), stmt.line);
1497                        self.chunk.patch_jump_here(j0);
1498                        self.emit_block_value(body, stmt.line)?;
1499                        self.chunk.patch_jump_here(end);
1500                    }
1501                    StmtKind::Block(block) => {
1502                        self.chunk.emit(Op::PushFrame, stmt.line);
1503                        self.emit_block_value(block, stmt.line)?;
1504                        self.chunk.emit(Op::PopFrame, stmt.line);
1505                    }
1506                    StmtKind::StmtGroup(block) => {
1507                        self.emit_block_value(block, stmt.line)?;
1508                    }
1509                    _ => self.compile_statement(stmt)?,
1510                }
1511            } else {
1512                self.compile_statement(stmt)?;
1513            }
1514            i += 1;
1515        }
1516        self.program_last_stmt_takes_value = false;
1517
1518        // Resolve all forward `goto LABEL` against labels recorded in the main scope.
1519        self.exit_goto_scope()?;
1520
1521        // Perl runs END blocks in reverse declaration order (LIFO): the last `END {}` seen runs
1522        // first, same as CHECK/UNITCHECK above. (BEGIN/INIT stay FIFO.)
1523        let end_rev: Vec<Block> = self.end_blocks.iter().rev().cloned().collect();
1524
1525        if self.line_mode {
1526            // `-n`/`-p`: end the per-line body with its own `Halt` FIRST, so per-line execution
1527            // stops here and never falls into `END`. Then place the `END` region AFTER that
1528            // `Halt`, recording its entry IP — the line driver runs it once after the loop via
1529            // `run_end_blocks`. Capturing the IP after the main `Halt` keeps it stable: the only
1530            // ops appended afterwards are deferred block/sub bodies (reached by jump, not
1531            // fall-through), so no relocation can invalidate it.
1532            self.chunk.emit(Op::Halt, 0);
1533            if !end_rev.is_empty() {
1534                self.chunk.line_mode_end_ip = Some(self.chunk.ops.len());
1535                self.chunk.emit(Op::SetGlobalPhase(GP_END), 0);
1536                for block in end_rev {
1537                    self.compile_block(&block)?;
1538                }
1539                self.chunk.emit(Op::Halt, 0);
1540            }
1541        } else {
1542            // Normal mode: END blocks run after main, before the single Halt (Perl phase order).
1543            if !self.end_blocks.is_empty() {
1544                self.chunk.emit(Op::SetGlobalPhase(GP_END), 0);
1545            }
1546            for block in end_rev {
1547                self.compile_block(&block)?;
1548            }
1549            self.chunk.emit(Op::Halt, 0);
1550        }
1551
1552        // Third pass: compile sub bodies after Halt
1553        let mut entries: Vec<(String, Vec<Statement>, String, Vec<crate::ast::SubSigParam>)> =
1554            Vec::new();
1555        let mut pending_pkg = String::new();
1556        for stmt in &program.statements {
1557            match &stmt.kind {
1558                StmtKind::Package { name } => pending_pkg = name.clone(),
1559                StmtKind::SubDecl {
1560                    name, body, params, ..
1561                } => {
1562                    entries.push((
1563                        name.clone(),
1564                        body.clone(),
1565                        pending_pkg.clone(),
1566                        params.clone(),
1567                    ));
1568                }
1569                _ => {}
1570            }
1571        }
1572
1573        for (name, body, sub_pkg, params) in &entries {
1574            let saved_pkg = self.current_package.clone();
1575            self.current_package = sub_pkg.clone();
1576            self.push_scope_layer_with_slots();
1577            // Register signature parameters in the new scope layer so the
1578            // closure-write check (DESIGN-001) recognises writes to params
1579            // as local — not outer-scope `my`. Without this, mutating a
1580            // sub parameter inside a `for` block (or any nested
1581            // statement-block) would falsely trigger the closure-write
1582            // diagnostic.
1583            for p in params {
1584                self.register_sig_param(p);
1585            }
1586            let entry_ip = self.chunk.len();
1587            let q = self.qualify_sub_decl_key(name);
1588            let name_idx = self.chunk.intern_name(&q);
1589            // Patch the entry point
1590            for e in &mut self.chunk.sub_entries {
1591                if e.0 == name_idx {
1592                    e.1 = entry_ip;
1593                }
1594            }
1595            // Each sub body gets its own `goto LABEL` scope: labels are not visible across
1596            // different subs or between a sub and the main program.
1597            self.enter_goto_scope();
1598            // Compile sub body (VM `Call` pushes a scope frame; mirror for frozen tracking).
1599            self.emit_subroutine_body_return(body)?;
1600            self.exit_goto_scope()?;
1601            self.pop_scope_layer();
1602
1603            // Peephole: convert leading `ShiftArray("_")` to `GetArg(n)` if @_ is
1604            // not referenced by any other op in this sub. This eliminates Vec
1605            // allocation + string-based @_ lookup on every call.
1606            let underscore_idx = self.chunk.intern_name("_");
1607            self.peephole_stack_args(name_idx, entry_ip, underscore_idx);
1608            self.current_package = saved_pkg;
1609        }
1610
1611        // Fourth pass: lower simple map/grep/sort block bodies to bytecode (after subs; same `ops` vec).
1612        // Each block was added via [`Self::add_deferred_block`], which captured a
1613        // snapshot of `scope_stack` at definition time. Swap that snapshot in
1614        // before compiling so references resolve against the LEXICAL scope the
1615        // block was written in, not the trailing top-level scope_stack that
1616        // exists once every sub body has been compiled and popped. Without
1617        // this, a sub-local `my $max` referenced inside a `map { … $max … }`
1618        // would silently resolve to a sibling top-level `my $max` slot.
1619        self.chunk.block_bytecode_ranges = vec![None; self.chunk.blocks.len()];
1620        for i in 0..self.chunk.blocks.len() {
1621            let b = self.chunk.blocks[i].clone();
1622            if Self::block_has_return(&b) {
1623                continue;
1624            }
1625            let saved_scope_stack = self
1626                .block_scope_snapshots
1627                .get(i)
1628                .cloned()
1629                .flatten()
1630                .map(|snap| std::mem::replace(&mut self.scope_stack, snap));
1631            // Sort/reduce blocks bind `$a`/`$b` via [`Scope::set_sort_pair`] (name-write).
1632            // Suppress slot resolution for those names while compiling the body so that
1633            // any outer `my $a`/`my $b` slot binding doesn't shadow the per-iter values.
1634            let is_sort_pair = self.sort_pair_block_indices.contains(&(i as u16));
1635            self.force_name_for_sort_pair = is_sort_pair;
1636            // Sub-body blocks (`sub { ... }` / `fn { ... }`): push a fresh
1637            // `is_sub_body: true` layer so the closure-write check
1638            // (DESIGN-001) can flag outer-scope `my` writes from inside the
1639            // body. Map/grep/sort blocks don't push this layer — they share
1640            // the enclosing scope normally.
1641            let pushed_sub_body = self.sub_body_block_indices.contains(&(i as u16));
1642            if pushed_sub_body {
1643                self.scope_stack.push(ScopeLayer {
1644                    use_slots: true,
1645                    is_sub_body: true,
1646                    ..Default::default()
1647                });
1648                // Register the closure's signature params in the new layer
1649                // so the closure-write check sees them as locally declared.
1650                if let Some(params) = self.code_ref_block_params.get(i).cloned() {
1651                    for p in &params {
1652                        self.register_sig_param(p);
1653                    }
1654                }
1655            }
1656            let result = self.try_compile_block_region(&b);
1657            self.force_name_for_sort_pair = false;
1658            if pushed_sub_body {
1659                self.scope_stack.pop();
1660            }
1661            match result {
1662                Ok(range) => {
1663                    self.chunk.block_bytecode_ranges[i] = Some(range);
1664                }
1665                Err(CompileError::Frozen { .. }) => {
1666                    // Real error (e.g. closure-write to outer-`my`,
1667                    // assignment to `frozen` binding). Restore scope and
1668                    // propagate.
1669                    if let Some(orig) = saved_scope_stack {
1670                        self.scope_stack = orig;
1671                    }
1672                    return Err(result.unwrap_err());
1673                }
1674                Err(CompileError::Unsupported(_)) => {
1675                    // Block lowering not yet supported — leave the
1676                    // bytecode_ranges entry as None; runtime will fall
1677                    // back to AST execution of the block body.
1678                }
1679            }
1680            if let Some(orig) = saved_scope_stack {
1681                self.scope_stack = orig;
1682            }
1683        }
1684
1685        // Fifth pass: `map EXPR, LIST` — list-context expression per `$_` (same `ops` vec as blocks).
1686        self.chunk.map_expr_bytecode_ranges = vec![None; self.chunk.map_expr_entries.len()];
1687        for i in 0..self.chunk.map_expr_entries.len() {
1688            let e = self.chunk.map_expr_entries[i].clone();
1689            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::List) {
1690                self.chunk.map_expr_bytecode_ranges[i] = Some(range);
1691            }
1692        }
1693
1694        // Fifth pass (a): `grep EXPR, LIST` — single-expression filter bodies (same `ops` vec as blocks).
1695        self.chunk.grep_expr_bytecode_ranges = vec![None; self.chunk.grep_expr_entries.len()];
1696        for i in 0..self.chunk.grep_expr_entries.len() {
1697            let e = self.chunk.grep_expr_entries[i].clone();
1698            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::Scalar) {
1699                self.chunk.grep_expr_bytecode_ranges[i] = Some(range);
1700            }
1701        }
1702
1703        // Fifth pass (b): regex flip-flop compound RHS — boolean context (same `ops` vec).
1704        self.chunk.regex_flip_flop_rhs_expr_bytecode_ranges =
1705            vec![None; self.chunk.regex_flip_flop_rhs_expr_entries.len()];
1706        for i in 0..self.chunk.regex_flip_flop_rhs_expr_entries.len() {
1707            let e = self.chunk.regex_flip_flop_rhs_expr_entries[i].clone();
1708            if let Ok(range) = self.try_compile_flip_flop_rhs_expr_region(&e) {
1709                self.chunk.regex_flip_flop_rhs_expr_bytecode_ranges[i] = Some(range);
1710            }
1711        }
1712
1713        // Sixth pass: `eval_timeout EXPR { ... }` — timeout expression only (body stays interpreter).
1714        self.chunk.eval_timeout_expr_bytecode_ranges =
1715            vec![None; self.chunk.eval_timeout_entries.len()];
1716        for i in 0..self.chunk.eval_timeout_entries.len() {
1717            let timeout_expr = self.chunk.eval_timeout_entries[i].0.clone();
1718            if let Ok(range) =
1719                self.try_compile_grep_expr_region(&timeout_expr, WantarrayCtx::Scalar)
1720            {
1721                self.chunk.eval_timeout_expr_bytecode_ranges[i] = Some(range);
1722            }
1723        }
1724
1725        // Seventh pass: `keys EXPR` / `values EXPR` — operand expression only.
1726        self.chunk.keys_expr_bytecode_ranges = vec![None; self.chunk.keys_expr_entries.len()];
1727        for i in 0..self.chunk.keys_expr_entries.len() {
1728            let e = self.chunk.keys_expr_entries[i].clone();
1729            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::List) {
1730                self.chunk.keys_expr_bytecode_ranges[i] = Some(range);
1731            }
1732        }
1733        self.chunk.values_expr_bytecode_ranges = vec![None; self.chunk.values_expr_entries.len()];
1734        for i in 0..self.chunk.values_expr_entries.len() {
1735            let e = self.chunk.values_expr_entries[i].clone();
1736            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::List) {
1737                self.chunk.values_expr_bytecode_ranges[i] = Some(range);
1738            }
1739        }
1740
1741        // Eighth pass: `given (TOPIC) { ... }` — topic expression only.
1742        self.chunk.given_topic_bytecode_ranges = vec![None; self.chunk.given_entries.len()];
1743        for i in 0..self.chunk.given_entries.len() {
1744            let topic = self.chunk.given_entries[i].0.clone();
1745            if let Ok(range) = self.try_compile_grep_expr_region(&topic, WantarrayCtx::Scalar) {
1746                self.chunk.given_topic_bytecode_ranges[i] = Some(range);
1747            }
1748        }
1749
1750        // Ninth pass: algebraic `match (SUBJECT) { ... }` — subject expression only.
1751        self.chunk.algebraic_match_subject_bytecode_ranges =
1752            vec![None; self.chunk.algebraic_match_entries.len()];
1753        for i in 0..self.chunk.algebraic_match_entries.len() {
1754            let subject = self.chunk.algebraic_match_entries[i].0.clone();
1755            let range: Option<(usize, usize)> = match &subject.kind {
1756                ExprKind::ArrayVar(name) => {
1757                    self.check_strict_array_access(name, subject.line)?;
1758                    let line = subject.line;
1759                    let start = self.chunk.len();
1760                    let stash = self.array_storage_name_for_ops(name);
1761                    let idx = self
1762                        .chunk
1763                        .intern_name(&self.qualify_stash_array_name(&stash));
1764                    self.chunk.emit(Op::MakeArrayBindingRef(idx), line);
1765                    self.chunk.emit(Op::BlockReturnValue, line);
1766                    Some((start, self.chunk.len()))
1767                }
1768                ExprKind::HashVar(name) => {
1769                    self.check_strict_hash_access(name, subject.line)?;
1770                    let line = subject.line;
1771                    let start = self.chunk.len();
1772                    let stash = self.hash_storage_name_for_ops(name);
1773                    let idx = self.chunk.intern_name(&stash);
1774                    self.chunk.emit(Op::MakeHashBindingRef(idx), line);
1775                    self.chunk.emit(Op::BlockReturnValue, line);
1776                    Some((start, self.chunk.len()))
1777                }
1778                _ => self
1779                    .try_compile_grep_expr_region(&subject, WantarrayCtx::Scalar)
1780                    .ok(),
1781            };
1782            self.chunk.algebraic_match_subject_bytecode_ranges[i] = range;
1783        }
1784
1785        Self::patch_static_sub_calls(&mut self.chunk);
1786        self.chunk.peephole_fuse();
1787
1788        Ok(self.chunk)
1789    }
1790
1791    /// Lower a block body to `ops` ending in [`Op::BlockReturnValue`] when possible.
1792    ///
1793    /// Matches `Interpreter::exec_block_no_scope` for blocks **without** `return`: last statement
1794    /// must be [`StmtKind::Expression`] (the value is that expression). Earlier statements use
1795    /// [`Self::compile_statement`] (void context). Any `CompileError` keeps AST fallback.
1796    fn try_compile_block_region(&mut self, block: &Block) -> Result<(usize, usize), CompileError> {
1797        let line0 = block.first().map(|s| s.line).unwrap_or(0);
1798        let start = self.chunk.len();
1799        if block.is_empty() {
1800            self.chunk.emit(Op::LoadUndef, line0);
1801            self.chunk.emit(Op::BlockReturnValue, line0);
1802            return Ok((start, self.chunk.len()));
1803        }
1804        let last = block.last().expect("non-empty block");
1805        let StmtKind::Expression(expr) = &last.kind else {
1806            return Err(CompileError::Unsupported(
1807                "block last statement must be an expression for bytecode lowering".into(),
1808            ));
1809        };
1810        for stmt in &block[..block.len() - 1] {
1811            self.compile_statement(stmt)?;
1812        }
1813        let line = last.line;
1814        self.compile_expr(expr)?;
1815        self.chunk.emit(Op::BlockReturnValue, line);
1816        Ok((start, self.chunk.len()))
1817    }
1818
1819    /// Lower a single expression to `ops` ending in [`Op::BlockReturnValue`].
1820    ///
1821    /// Used for `grep EXPR, LIST` (with `$_` set by the VM per item), `eval_timeout EXPR { ... }`,
1822    /// `keys EXPR` / `values EXPR` operands, `given (TOPIC) { ... }` topic, algebraic `match (SUBJECT)`
1823    /// subject, and similar one-shot regions matching [`VMHelper::eval_expr`].
1824    fn try_compile_grep_expr_region(
1825        &mut self,
1826        expr: &Expr,
1827        ctx: WantarrayCtx,
1828    ) -> Result<(usize, usize), CompileError> {
1829        let line = expr.line;
1830        let start = self.chunk.len();
1831        self.compile_expr_ctx(expr, ctx)?;
1832        self.chunk.emit(Op::BlockReturnValue, line);
1833        Ok((start, self.chunk.len()))
1834    }
1835
1836    /// Regex flip-flop right operand: boolean rvalue (bare `m//` is `$_ =~ m//`), like `if` / `grep EXPR`.
1837    fn try_compile_flip_flop_rhs_expr_region(
1838        &mut self,
1839        expr: &Expr,
1840    ) -> Result<(usize, usize), CompileError> {
1841        let line = expr.line;
1842        let start = self.chunk.len();
1843        self.compile_boolean_rvalue_condition(expr)?;
1844        self.chunk.emit(Op::BlockReturnValue, line);
1845        Ok((start, self.chunk.len()))
1846    }
1847
1848    /// Peephole optimization: if a compiled sub starts with `ShiftArray("_")`
1849    /// ops and `@_` is not referenced elsewhere, convert those shifts to
1850    /// `GetArg(n)` and mark the sub entry as `uses_stack_args = true`.
1851    /// This eliminates Vec allocation + string-based @_ lookup per call.
1852    fn peephole_stack_args(&mut self, sub_name_idx: u16, entry_ip: usize, underscore_idx: u16) {
1853        let ops = &self.chunk.ops;
1854        let end = ops.len();
1855
1856        // Count leading ShiftArray("_") ops
1857        let mut shift_count: u8 = 0;
1858        let mut ip = entry_ip;
1859        while ip < end {
1860            if ops[ip] == Op::ShiftArray(underscore_idx) {
1861                shift_count += 1;
1862                ip += 1;
1863            } else {
1864                break;
1865            }
1866        }
1867        if shift_count == 0 {
1868            return;
1869        }
1870
1871        // Check that @_ is not referenced by any other op in this sub
1872        let refs_underscore = |op: &Op| -> bool {
1873            match op {
1874                Op::GetArray(idx)
1875                | Op::SetArray(idx)
1876                | Op::DeclareArray(idx)
1877                | Op::DeclareArrayFrozen(idx)
1878                | Op::GetArrayElem(idx)
1879                | Op::SetArrayElem(idx)
1880                | Op::SetArrayElemKeep(idx)
1881                | Op::PushArray(idx)
1882                | Op::PopArray(idx)
1883                | Op::ShiftArray(idx)
1884                | Op::ArrayLen(idx) => *idx == underscore_idx,
1885                // `goto &sub` reads the live `@_` from the scope at runtime, so the
1886                // sub must keep the real `@_` array (no stack-args conversion).
1887                Op::GotoSub(_) => true,
1888                _ => false,
1889            }
1890        };
1891
1892        for op in ops.iter().take(end).skip(entry_ip + shift_count as usize) {
1893            if refs_underscore(op) {
1894                return; // @_ used elsewhere, can't optimize
1895            }
1896            if matches!(op, Op::Halt | Op::ReturnValue) {
1897                break; // end of this sub's bytecode
1898            }
1899        }
1900
1901        // Safe to convert: replace ShiftArray("_") with GetArg(n)
1902        for i in 0..shift_count {
1903            self.chunk.ops[entry_ip + i as usize] = Op::GetArg(i);
1904        }
1905
1906        // Mark sub entry as using stack args
1907        for e in &mut self.chunk.sub_entries {
1908            if e.0 == sub_name_idx {
1909                e.2 = true;
1910            }
1911        }
1912    }
1913
1914    fn emit_declare_scalar(&mut self, name_idx: u16, line: usize, frozen: bool) {
1915        let name = self.chunk.names[name_idx as usize].clone();
1916        self.register_declare(Sigil::Scalar, &name, frozen);
1917        // Slot-allocate for both var and val, but only INSIDE a function /
1918        // nested block. Pre-fix, frozen decls used the name-based
1919        // `DeclareScalarFrozen` op, which shared one runtime binding
1920        // across sibling blocks — two `for {…}` loops in the same
1921        // function each containing `val $e = …` both bound the same `$e`,
1922        // so the second loop read the first's last-iteration value
1923        // instead of its own initializer. Frozen-ness is enforced at
1924        // compile time via `scope_stack`'s `frozen_scalars` set, so
1925        // emitting `DeclareScalarSlot` (the mutable-slot op) is safe —
1926        // the compiler rejects reassignment via the same scope-tracking
1927        // that catches `$e = …` after `val $e = …`.
1928        //
1929        // At TOP-LEVEL script scope (scope_stack.len() == 1), `val $K`
1930        // must stay name-based (`DeclareScalarFrozen`). Top-level decls
1931        // are referenced by `fn` bodies that close over them; the closure
1932        // capture is by name, and after a `require` brings in such a fn,
1933        // the main script's subsequent `val $r = …` slot allocations
1934        // would otherwise re-use the lib's slot indices, clobbering `$K`
1935        // and making the lib's fn return 0 the second time it's called.
1936        let in_nested_scope = self.scope_stack.len() > 1;
1937        if in_nested_scope {
1938            if let Some(slot) = self.assign_scalar_slot(&name) {
1939                self.chunk.emit(Op::DeclareScalarSlot(slot, name_idx), line);
1940                return;
1941            }
1942        }
1943        if frozen {
1944            self.chunk.emit(Op::DeclareScalarFrozen(name_idx), line);
1945        } else if let Some(slot) = self.assign_scalar_slot(&name) {
1946            self.chunk.emit(Op::DeclareScalarSlot(slot, name_idx), line);
1947        } else {
1948            self.chunk.emit(Op::DeclareScalar(name_idx), line);
1949        }
1950    }
1951
1952    /// Emit the right `DeclareScalarTyped*` op for a `typed my $x : Ty`
1953    /// declaration. Handles primitive types (Int/Str/Float/…) via the
1954    /// 1-byte type encoding, and user-defined struct/class/enum types
1955    /// via the name-pool encoding (`DeclareScalarTypedUser`).
1956    fn emit_declare_scalar_typed(
1957        &mut self,
1958        name_idx: u16,
1959        ty: &crate::ast::PerlTypeName,
1960        line: usize,
1961        frozen: bool,
1962    ) {
1963        let name = self.chunk.names[name_idx as usize].clone();
1964        self.register_declare(Sigil::Scalar, &name, frozen);
1965        if let Some(ty_byte) = ty.as_byte() {
1966            if frozen {
1967                self.chunk
1968                    .emit(Op::DeclareScalarTypedFrozen(name_idx, ty_byte), line);
1969            } else {
1970                self.chunk
1971                    .emit(Op::DeclareScalarTyped(name_idx, ty_byte), line);
1972            }
1973            return;
1974        }
1975        // User-defined type — encode the name through the name pool.
1976        let (type_name, is_enum) = match ty {
1977            crate::ast::PerlTypeName::Struct(n) => (n.clone(), false),
1978            crate::ast::PerlTypeName::Enum(n) => (n.clone(), true),
1979            _ => unreachable!("non-byte non-user type slipped past as_byte()"),
1980        };
1981        let type_name_idx = self.chunk.intern_name(&type_name);
1982        let flag = (u8::from(frozen) << 1) | u8::from(is_enum);
1983        self.chunk.emit(
1984            Op::DeclareScalarTypedUser(name_idx, type_name_idx, flag),
1985            line,
1986        );
1987    }
1988
1989    fn emit_declare_array(&mut self, name_idx: u16, line: usize, frozen: bool) {
1990        let name = self.chunk.names[name_idx as usize].clone();
1991        self.register_declare(Sigil::Array, &name, frozen);
1992        if frozen {
1993            self.chunk.emit(Op::DeclareArrayFrozen(name_idx), line);
1994        } else {
1995            self.chunk.emit(Op::DeclareArray(name_idx), line);
1996        }
1997    }
1998
1999    fn emit_declare_hash(&mut self, name_idx: u16, line: usize, frozen: bool) {
2000        let name = self.chunk.names[name_idx as usize].clone();
2001        self.register_declare(Sigil::Hash, &name, frozen);
2002        if frozen {
2003            self.chunk.emit(Op::DeclareHashFrozen(name_idx), line);
2004        } else {
2005            self.chunk.emit(Op::DeclareHash(name_idx), line);
2006        }
2007    }
2008
2009    fn compile_var_declarations(
2010        &mut self,
2011        decls: &[VarDecl],
2012        line: usize,
2013        is_my: bool,
2014    ) -> Result<(), CompileError> {
2015        let allow_frozen = is_my;
2016        // List assignment: my ($a, $b) = (10, 20) — distribute elements
2017        if decls.len() > 1 && decls[0].initializer.is_some() {
2018            self.compile_expr_ctx(decls[0].initializer.as_ref().unwrap(), WantarrayCtx::List)?;
2019            let tmp_name = self.chunk.intern_name("__list_assign_tmp__");
2020            self.emit_declare_array(tmp_name, line, false);
2021            for (i, decl) in decls.iter().enumerate() {
2022                let frozen = allow_frozen && decl.frozen;
2023                match decl.sigil {
2024                    Sigil::Scalar => {
2025                        self.chunk.emit(Op::LoadInt(i as i64), line);
2026                        self.chunk.emit(Op::GetArrayElem(tmp_name), line);
2027                        if is_my {
2028                            let name_idx = self.chunk.intern_name(&decl.name);
2029                            if let Some(ref ty) = decl.type_annotation {
2030                                self.emit_declare_scalar_typed(name_idx, ty, line, frozen);
2031                            } else {
2032                                self.emit_declare_scalar(name_idx, line, frozen);
2033                            }
2034                        } else {
2035                            if decl.type_annotation.is_some() {
2036                                return Err(CompileError::Unsupported("typed our".into()));
2037                            }
2038                            self.emit_declare_our_scalar(&decl.name, line, frozen);
2039                        }
2040                    }
2041                    Sigil::Array => {
2042                        let stash = if is_my {
2043                            self.qualify_stash_array_name(&decl.name)
2044                        } else {
2045                            self.qualify_stash_array_name_full(&decl.name)
2046                        };
2047                        let name_idx = self.chunk.intern_name(&stash);
2048                        // Slurpy `@rest` at position `i` takes `tmp[i..]` (the
2049                        // tail), not the whole list. (BUG-090)
2050                        self.chunk
2051                            .emit(Op::GetArrayFromIndex(tmp_name, i as u16), line);
2052                        self.emit_declare_array(name_idx, line, frozen);
2053                        if !is_my {
2054                            if let Some(layer) = self.scope_stack.last_mut() {
2055                                layer.declared_arrays.insert(decl.name.clone());
2056                                layer.declared_our_arrays.insert(decl.name.clone());
2057                            }
2058                        }
2059                    }
2060                    Sigil::Hash => {
2061                        let stash = if is_my {
2062                            decl.name.clone()
2063                        } else {
2064                            self.qualify_stash_hash_name_full(&decl.name)
2065                        };
2066                        let name_idx = self.chunk.intern_name(&stash);
2067                        // Slurpy `%rest` at position `i` takes `tmp[i..]`
2068                        // pairs, not the whole list. (BUG-090)
2069                        self.chunk
2070                            .emit(Op::GetArrayFromIndex(tmp_name, i as u16), line);
2071                        self.emit_declare_hash(name_idx, line, frozen);
2072                        if !is_my {
2073                            if let Some(layer) = self.scope_stack.last_mut() {
2074                                layer.declared_hashes.insert(decl.name.clone());
2075                                layer.declared_our_hashes.insert(decl.name.clone());
2076                            }
2077                        }
2078                    }
2079                    Sigil::Typeglob => {
2080                        return Err(CompileError::Unsupported(
2081                            "list assignment to typeglob (my (*a, *b) = ...)".into(),
2082                        ));
2083                    }
2084                }
2085            }
2086        } else {
2087            for decl in decls {
2088                let frozen = allow_frozen && decl.frozen;
2089                // `our $x;` / `our @a;` / `our %h;` with NO initializer aliases the package
2090                // variable without resetting it: a BEGIN-phase write to the same stash must
2091                // survive (Perl — a bare `our` declaration never clobbers). Register the name
2092                // for resolution and `strict 'vars'`, but emit no runtime store. (With an
2093                // initializer, `our $x = ...;` assigns and resets, same as Perl.)
2094                if !is_my && decl.initializer.is_none() && decl.type_annotation.is_none() {
2095                    match decl.sigil {
2096                        Sigil::Scalar => {
2097                            self.register_declare_our_scalar(&decl.name);
2098                            continue;
2099                        }
2100                        Sigil::Array => {
2101                            if let Some(layer) = self.scope_stack.last_mut() {
2102                                layer.declared_arrays.insert(decl.name.clone());
2103                                layer.declared_our_arrays.insert(decl.name.clone());
2104                            }
2105                            continue;
2106                        }
2107                        Sigil::Hash => {
2108                            if let Some(layer) = self.scope_stack.last_mut() {
2109                                layer.declared_hashes.insert(decl.name.clone());
2110                                layer.declared_our_hashes.insert(decl.name.clone());
2111                            }
2112                            continue;
2113                        }
2114                        Sigil::Typeglob => {} // falls through to the typeglob error below
2115                    }
2116                }
2117                match decl.sigil {
2118                    Sigil::Scalar => {
2119                        if let Some(init) = &decl.initializer {
2120                            if decl.list_context {
2121                                // my ($x) = @a → list context, extract first element
2122                                self.compile_expr_ctx(init, WantarrayCtx::List)?;
2123                                self.chunk.emit(Op::ListFirst, line);
2124                            } else {
2125                                self.compile_expr(init)?;
2126                            }
2127                        } else {
2128                            self.chunk.emit(Op::LoadUndef, line);
2129                        }
2130                        if is_my {
2131                            let name_idx = self.chunk.intern_name(&decl.name);
2132                            if let Some(ref ty) = decl.type_annotation {
2133                                self.emit_declare_scalar_typed(name_idx, ty, line, frozen);
2134                            } else {
2135                                self.emit_declare_scalar(name_idx, line, frozen);
2136                            }
2137                        } else {
2138                            if decl.type_annotation.is_some() {
2139                                return Err(CompileError::Unsupported("typed our".into()));
2140                            }
2141                            self.emit_declare_our_scalar(&decl.name, line, false);
2142                        }
2143                    }
2144                    Sigil::Array => {
2145                        let stash = if is_my {
2146                            self.qualify_stash_array_name(&decl.name)
2147                        } else {
2148                            self.qualify_stash_array_name_full(&decl.name)
2149                        };
2150                        let name_idx = self.chunk.intern_name(&stash);
2151                        if let Some(init) = &decl.initializer {
2152                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
2153                        } else {
2154                            self.chunk.emit(Op::LoadUndef, line);
2155                        }
2156                        self.emit_declare_array(name_idx, line, frozen);
2157                        if !is_my {
2158                            if let Some(layer) = self.scope_stack.last_mut() {
2159                                layer.declared_arrays.insert(decl.name.clone());
2160                                layer.declared_our_arrays.insert(decl.name.clone());
2161                            }
2162                        }
2163                    }
2164                    Sigil::Hash => {
2165                        let stash = if is_my {
2166                            decl.name.clone()
2167                        } else {
2168                            self.qualify_stash_hash_name_full(&decl.name)
2169                        };
2170                        let name_idx = self.chunk.intern_name(&stash);
2171                        if let Some(init) = &decl.initializer {
2172                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
2173                        } else {
2174                            self.chunk.emit(Op::LoadUndef, line);
2175                        }
2176                        self.emit_declare_hash(name_idx, line, frozen);
2177                        if !is_my {
2178                            if let Some(layer) = self.scope_stack.last_mut() {
2179                                layer.declared_hashes.insert(decl.name.clone());
2180                                layer.declared_our_hashes.insert(decl.name.clone());
2181                            }
2182                        }
2183                    }
2184                    Sigil::Typeglob => {
2185                        return Err(CompileError::Unsupported("my/our *GLOB".into()));
2186                    }
2187                }
2188            }
2189        }
2190        Ok(())
2191    }
2192
2193    fn compile_state_declarations(
2194        &mut self,
2195        decls: &[VarDecl],
2196        line: usize,
2197    ) -> Result<(), CompileError> {
2198        for decl in decls {
2199            match decl.sigil {
2200                Sigil::Scalar => {
2201                    if let Some(init) = &decl.initializer {
2202                        self.compile_expr(init)?;
2203                    } else {
2204                        self.chunk.emit(Op::LoadUndef, line);
2205                    }
2206                    let name_idx = self.chunk.intern_name(&decl.name);
2207                    let name = self.chunk.names[name_idx as usize].clone();
2208                    self.register_declare(Sigil::Scalar, &name, false);
2209                    self.chunk.emit(Op::DeclareStateScalar(name_idx), line);
2210                }
2211                Sigil::Array => {
2212                    let name_idx = self
2213                        .chunk
2214                        .intern_name(&self.qualify_stash_array_name(&decl.name));
2215                    if let Some(init) = &decl.initializer {
2216                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2217                    } else {
2218                        self.chunk.emit(Op::LoadUndef, line);
2219                    }
2220                    self.chunk.emit(Op::DeclareStateArray(name_idx), line);
2221                }
2222                Sigil::Hash => {
2223                    let name_idx = self.chunk.intern_name(&decl.name);
2224                    if let Some(init) = &decl.initializer {
2225                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2226                    } else {
2227                        self.chunk.emit(Op::LoadUndef, line);
2228                    }
2229                    self.chunk.emit(Op::DeclareStateHash(name_idx), line);
2230                }
2231                Sigil::Typeglob => {
2232                    return Err(CompileError::Unsupported("state *GLOB".into()));
2233                }
2234            }
2235        }
2236        Ok(())
2237    }
2238
2239    fn compile_local_declarations(
2240        &mut self,
2241        decls: &[VarDecl],
2242        line: usize,
2243    ) -> Result<(), CompileError> {
2244        if decls.iter().any(|d| d.type_annotation.is_some()) {
2245            return Err(CompileError::Unsupported("typed local".into()));
2246        }
2247        if decls.len() > 1 && decls[0].initializer.is_some() {
2248            self.compile_expr_ctx(decls[0].initializer.as_ref().unwrap(), WantarrayCtx::List)?;
2249            let tmp_name = self.chunk.intern_name("__list_assign_tmp__");
2250            self.emit_declare_array(tmp_name, line, false);
2251            for (i, decl) in decls.iter().enumerate() {
2252                match decl.sigil {
2253                    Sigil::Scalar => {
2254                        let name_idx = self.intern_scalar_for_local(&decl.name);
2255                        self.chunk.emit(Op::LoadInt(i as i64), line);
2256                        self.chunk.emit(Op::GetArrayElem(tmp_name), line);
2257                        self.chunk.emit(Op::LocalDeclareScalar(name_idx), line);
2258                    }
2259                    Sigil::Array => {
2260                        let q = self.qualify_stash_array_name(&decl.name);
2261                        let name_idx = self.chunk.intern_name(&q);
2262                        self.chunk.emit(Op::GetArray(tmp_name), line);
2263                        self.chunk.emit(Op::LocalDeclareArray(name_idx), line);
2264                    }
2265                    Sigil::Hash => {
2266                        let name_idx = self.chunk.intern_name(&decl.name);
2267                        self.chunk.emit(Op::GetArray(tmp_name), line);
2268                        self.chunk.emit(Op::LocalDeclareHash(name_idx), line);
2269                    }
2270                    Sigil::Typeglob => {
2271                        return Err(CompileError::Unsupported(
2272                            "local (*a,*b,...) with list initializer and typeglob".into(),
2273                        ));
2274                    }
2275                }
2276            }
2277        } else {
2278            for decl in decls {
2279                match decl.sigil {
2280                    Sigil::Scalar => {
2281                        let name_idx = self.intern_scalar_for_local(&decl.name);
2282                        if let Some(init) = &decl.initializer {
2283                            self.compile_expr(init)?;
2284                        } else {
2285                            self.chunk.emit(Op::LoadUndef, line);
2286                        }
2287                        self.chunk.emit(Op::LocalDeclareScalar(name_idx), line);
2288                    }
2289                    Sigil::Array => {
2290                        let q = self.qualify_stash_array_name(&decl.name);
2291                        let name_idx = self.chunk.intern_name(&q);
2292                        if let Some(init) = &decl.initializer {
2293                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
2294                        } else {
2295                            self.chunk.emit(Op::LoadUndef, line);
2296                        }
2297                        self.chunk.emit(Op::LocalDeclareArray(name_idx), line);
2298                    }
2299                    Sigil::Hash => {
2300                        let name_idx = self.chunk.intern_name(&decl.name);
2301                        if let Some(init) = &decl.initializer {
2302                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
2303                        } else {
2304                            self.chunk.emit(Op::LoadUndef, line);
2305                        }
2306                        self.chunk.emit(Op::LocalDeclareHash(name_idx), line);
2307                    }
2308                    Sigil::Typeglob => {
2309                        let name_idx = self.chunk.intern_name(&decl.name);
2310                        if let Some(init) = &decl.initializer {
2311                            let ExprKind::Typeglob(rhs) = &init.kind else {
2312                                return Err(CompileError::Unsupported(
2313                                    "local *GLOB = non-typeglob".into(),
2314                                ));
2315                            };
2316                            let rhs_idx = self.chunk.intern_name(rhs);
2317                            self.chunk
2318                                .emit(Op::LocalDeclareTypeglob(name_idx, Some(rhs_idx)), line);
2319                        } else {
2320                            self.chunk
2321                                .emit(Op::LocalDeclareTypeglob(name_idx, None), line);
2322                        }
2323                    }
2324                }
2325            }
2326        }
2327        Ok(())
2328    }
2329
2330    /// `oursync $x` — package-global counterpart of `mysync`. Wraps the value in
2331    /// `Arc<Mutex<StrykeValue>>` (or `AtomicArray` / `AtomicHash`) like `mysync`, but
2332    /// keys the binding by the package-qualified stash name (`Pkg::x`) so all
2333    /// packages and parallel workers share one cell. Mirrors [`Self::emit_declare_our_scalar`]
2334    /// for name qualification + `register_declare_our_scalar` so later `$x` references
2335    /// rewrite to `Pkg::x` via [`Self::scalar_storage_name_for_ops`].
2336    fn compile_oursync_declarations(
2337        &mut self,
2338        decls: &[VarDecl],
2339        line: usize,
2340    ) -> Result<(), CompileError> {
2341        for decl in decls {
2342            if decl.type_annotation.is_some() {
2343                return Err(CompileError::Unsupported("typed oursync".into()));
2344            }
2345            match decl.sigil {
2346                Sigil::Typeglob => {
2347                    return Err(CompileError::Unsupported(
2348                        "`oursync` does not support typeglob variables".into(),
2349                    ));
2350                }
2351                Sigil::Scalar => {
2352                    if let Some(init) = &decl.initializer {
2353                        self.compile_expr(init)?;
2354                    } else {
2355                        self.chunk.emit(Op::LoadUndef, line);
2356                    }
2357                    let stash = self.qualify_stash_scalar_name(&decl.name);
2358                    let name_idx = self.chunk.intern_name(&stash);
2359                    self.register_declare_our_scalar(&decl.name);
2360                    if let Some(layer) = self.scope_stack.last_mut() {
2361                        layer.mysync_scalars.insert(stash);
2362                    }
2363                    self.chunk.emit(Op::DeclareOurSyncScalar(name_idx), line);
2364                }
2365                Sigil::Array => {
2366                    let stash = self.qualify_stash_array_name(&decl.name);
2367                    if let Some(init) = &decl.initializer {
2368                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2369                    } else {
2370                        self.chunk.emit(Op::LoadUndef, line);
2371                    }
2372                    let name_idx = self.chunk.intern_name(&stash);
2373                    self.register_declare(Sigil::Array, &stash, false);
2374                    if let Some(layer) = self.scope_stack.last_mut() {
2375                        layer.mysync_arrays.insert(stash);
2376                    }
2377                    self.chunk.emit(Op::DeclareOurSyncArray(name_idx), line);
2378                }
2379                Sigil::Hash => {
2380                    if let Some(init) = &decl.initializer {
2381                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2382                    } else {
2383                        self.chunk.emit(Op::LoadUndef, line);
2384                    }
2385                    // Hashes follow the existing `our %h` convention and use the
2386                    // bare name in bytecode (compiler.rs:1722). Cross-package
2387                    // hash access has separate quirks tracked elsewhere.
2388                    let name_idx = self.chunk.intern_name(&decl.name);
2389                    self.register_declare(Sigil::Hash, &decl.name, false);
2390                    if let Some(layer) = self.scope_stack.last_mut() {
2391                        layer.mysync_hashes.insert(decl.name.clone());
2392                    }
2393                    self.chunk.emit(Op::DeclareOurSyncHash(name_idx), line);
2394                }
2395            }
2396        }
2397        Ok(())
2398    }
2399
2400    fn compile_mysync_declarations(
2401        &mut self,
2402        decls: &[VarDecl],
2403        line: usize,
2404    ) -> Result<(), CompileError> {
2405        for decl in decls {
2406            if decl.type_annotation.is_some() {
2407                return Err(CompileError::Unsupported("typed mysync".into()));
2408            }
2409            match decl.sigil {
2410                Sigil::Typeglob => {
2411                    return Err(CompileError::Unsupported(
2412                        "`mysync` does not support typeglob variables".into(),
2413                    ));
2414                }
2415                Sigil::Scalar => {
2416                    if let Some(init) = &decl.initializer {
2417                        self.compile_expr(init)?;
2418                    } else {
2419                        self.chunk.emit(Op::LoadUndef, line);
2420                    }
2421                    let name_idx = self.chunk.intern_name(&decl.name);
2422                    self.register_declare(Sigil::Scalar, &decl.name, false);
2423                    self.chunk.emit(Op::DeclareMySyncScalar(name_idx), line);
2424                    if let Some(layer) = self.scope_stack.last_mut() {
2425                        layer.mysync_scalars.insert(decl.name.clone());
2426                    }
2427                }
2428                Sigil::Array => {
2429                    let stash = self.qualify_stash_array_name(&decl.name);
2430                    if let Some(init) = &decl.initializer {
2431                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2432                    } else {
2433                        self.chunk.emit(Op::LoadUndef, line);
2434                    }
2435                    let name_idx = self.chunk.intern_name(&stash);
2436                    self.register_declare(Sigil::Array, &stash, false);
2437                    self.chunk.emit(Op::DeclareMySyncArray(name_idx), line);
2438                    if let Some(layer) = self.scope_stack.last_mut() {
2439                        layer.mysync_arrays.insert(stash);
2440                    }
2441                }
2442                Sigil::Hash => {
2443                    if let Some(init) = &decl.initializer {
2444                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2445                    } else {
2446                        self.chunk.emit(Op::LoadUndef, line);
2447                    }
2448                    let name_idx = self.chunk.intern_name(&decl.name);
2449                    self.register_declare(Sigil::Hash, &decl.name, false);
2450                    self.chunk.emit(Op::DeclareMySyncHash(name_idx), line);
2451                    if let Some(layer) = self.scope_stack.last_mut() {
2452                        layer.mysync_hashes.insert(decl.name.clone());
2453                    }
2454                }
2455            }
2456        }
2457        Ok(())
2458    }
2459
2460    /// `local $h{k} = …` / `local $SIG{__WARN__}` — not plain [`StmtKind::Local`] declarations.
2461    fn compile_local_expr(
2462        &mut self,
2463        target: &Expr,
2464        initializer: Option<&Expr>,
2465        line: usize,
2466    ) -> Result<(), CompileError> {
2467        match &target.kind {
2468            ExprKind::HashElement { hash, key } => {
2469                self.check_strict_hash_access(hash, line)?;
2470                self.check_hash_mutable(hash, line)?;
2471                let hash_idx = self.chunk.intern_name(hash);
2472                if let Some(init) = initializer {
2473                    self.compile_expr(init)?;
2474                } else {
2475                    self.chunk.emit(Op::LoadUndef, line);
2476                }
2477                self.compile_expr(key)?;
2478                self.chunk.emit(Op::LocalDeclareHashElement(hash_idx), line);
2479                Ok(())
2480            }
2481            ExprKind::ArrayElement { array, index } => {
2482                self.check_strict_array_access(array, line)?;
2483                let q = self.qualify_stash_array_name(array);
2484                self.check_array_mutable(&q, line)?;
2485                let arr_idx = self.chunk.intern_name(&q);
2486                if let Some(init) = initializer {
2487                    self.compile_expr(init)?;
2488                } else {
2489                    self.chunk.emit(Op::LoadUndef, line);
2490                }
2491                self.compile_expr(index)?;
2492                self.chunk.emit(Op::LocalDeclareArrayElement(arr_idx), line);
2493                Ok(())
2494            }
2495            ExprKind::Typeglob(name) => {
2496                let lhs_idx = self.chunk.intern_name(name);
2497                if let Some(init) = initializer {
2498                    let ExprKind::Typeglob(rhs) = &init.kind else {
2499                        return Err(CompileError::Unsupported(
2500                            "local *GLOB = non-typeglob".into(),
2501                        ));
2502                    };
2503                    let rhs_idx = self.chunk.intern_name(rhs);
2504                    self.chunk
2505                        .emit(Op::LocalDeclareTypeglob(lhs_idx, Some(rhs_idx)), line);
2506                } else {
2507                    self.chunk
2508                        .emit(Op::LocalDeclareTypeglob(lhs_idx, None), line);
2509                }
2510                Ok(())
2511            }
2512            ExprKind::Deref {
2513                expr,
2514                kind: Sigil::Typeglob,
2515            } => {
2516                if let Some(init) = initializer {
2517                    let ExprKind::Typeglob(rhs) = &init.kind else {
2518                        return Err(CompileError::Unsupported(
2519                            "local *GLOB = non-typeglob".into(),
2520                        ));
2521                    };
2522                    let rhs_idx = self.chunk.intern_name(rhs);
2523                    self.compile_expr(expr)?;
2524                    self.chunk
2525                        .emit(Op::LocalDeclareTypeglobDynamic(Some(rhs_idx)), line);
2526                } else {
2527                    self.compile_expr(expr)?;
2528                    self.chunk.emit(Op::LocalDeclareTypeglobDynamic(None), line);
2529                }
2530                Ok(())
2531            }
2532            ExprKind::TypeglobExpr(expr) => {
2533                if let Some(init) = initializer {
2534                    let ExprKind::Typeglob(rhs) = &init.kind else {
2535                        return Err(CompileError::Unsupported(
2536                            "local *GLOB = non-typeglob".into(),
2537                        ));
2538                    };
2539                    let rhs_idx = self.chunk.intern_name(rhs);
2540                    self.compile_expr(expr)?;
2541                    self.chunk
2542                        .emit(Op::LocalDeclareTypeglobDynamic(Some(rhs_idx)), line);
2543                } else {
2544                    self.compile_expr(expr)?;
2545                    self.chunk.emit(Op::LocalDeclareTypeglobDynamic(None), line);
2546                }
2547                Ok(())
2548            }
2549            ExprKind::ScalarVar(name) => {
2550                let name_idx = self.intern_scalar_for_local(name);
2551                if let Some(init) = initializer {
2552                    self.compile_expr(init)?;
2553                } else {
2554                    self.chunk.emit(Op::LoadUndef, line);
2555                }
2556                self.chunk.emit(Op::LocalDeclareScalar(name_idx), line);
2557                Ok(())
2558            }
2559            ExprKind::ArrayVar(name) => {
2560                self.check_strict_array_access(name, line)?;
2561                let stash = self.array_storage_name_for_ops(name);
2562                let q = self.qualify_stash_array_name(&stash);
2563                let name_idx = self.chunk.intern_name(&q);
2564                if let Some(init) = initializer {
2565                    self.compile_expr_ctx(init, WantarrayCtx::List)?;
2566                } else {
2567                    self.chunk.emit(Op::LoadUndef, line);
2568                }
2569                self.chunk.emit(Op::LocalDeclareArray(name_idx), line);
2570                Ok(())
2571            }
2572            ExprKind::HashVar(name) => {
2573                let stash = self.hash_storage_name_for_ops(name);
2574                let name_idx = self.chunk.intern_name(&stash);
2575                if let Some(init) = initializer {
2576                    self.compile_expr_ctx(init, WantarrayCtx::List)?;
2577                } else {
2578                    self.chunk.emit(Op::LoadUndef, line);
2579                }
2580                self.chunk.emit(Op::LocalDeclareHash(name_idx), line);
2581                Ok(())
2582            }
2583            _ => Err(CompileError::Unsupported("local on this lvalue".into())),
2584        }
2585    }
2586
2587    fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
2588        // A `LABEL:` on a statement binds the label to the IP of the first op emitted for that
2589        // statement, so that `goto LABEL` can jump to the effective start of execution.
2590        if let Some(lbl) = &stmt.label {
2591            self.record_stmt_label(lbl);
2592        }
2593        let line = stmt.line;
2594        match &stmt.kind {
2595            StmtKind::FormatDecl { name, lines } => {
2596                let idx = self.chunk.add_format_decl(name.clone(), lines.clone());
2597                self.chunk.emit(Op::FormatDecl(idx), line);
2598            }
2599            StmtKind::Expression(expr) => {
2600                self.compile_expr_ctx(expr, WantarrayCtx::Void)?;
2601                self.chunk.emit(Op::Pop, line);
2602            }
2603            StmtKind::Local(decls) => self.compile_local_declarations(decls, line)?,
2604            StmtKind::LocalExpr {
2605                target,
2606                initializer,
2607            } => {
2608                self.compile_local_expr(target, initializer.as_ref(), line)?;
2609            }
2610            StmtKind::MySync(decls) => self.compile_mysync_declarations(decls, line)?,
2611            StmtKind::OurSync(decls) => self.compile_oursync_declarations(decls, line)?,
2612            StmtKind::My(decls) => self.compile_var_declarations(decls, line, true)?,
2613            StmtKind::Our(decls) => self.compile_var_declarations(decls, line, false)?,
2614            StmtKind::State(decls) => self.compile_state_declarations(decls, line)?,
2615            StmtKind::If {
2616                condition,
2617                body,
2618                elsifs,
2619                else_block,
2620            } => {
2621                self.compile_boolean_rvalue_condition(condition)?;
2622                let jump_else = self.chunk.emit(Op::JumpIfFalse(0), line);
2623                self.compile_block(body)?;
2624                let mut end_jumps = vec![self.chunk.emit(Op::Jump(0), line)];
2625                self.chunk.patch_jump_here(jump_else);
2626
2627                for (cond, blk) in elsifs {
2628                    self.compile_boolean_rvalue_condition(cond)?;
2629                    let j = self.chunk.emit(Op::JumpIfFalse(0), cond.line);
2630                    self.compile_block(blk)?;
2631                    end_jumps.push(self.chunk.emit(Op::Jump(0), cond.line));
2632                    self.chunk.patch_jump_here(j);
2633                }
2634
2635                if let Some(eb) = else_block {
2636                    self.compile_block(eb)?;
2637                }
2638                for j in end_jumps {
2639                    self.chunk.patch_jump_here(j);
2640                }
2641            }
2642            StmtKind::Unless {
2643                condition,
2644                body,
2645                else_block,
2646            } => {
2647                self.compile_boolean_rvalue_condition(condition)?;
2648                let jump_else = self.chunk.emit(Op::JumpIfTrue(0), line);
2649                self.compile_block(body)?;
2650                if let Some(eb) = else_block {
2651                    let end_j = self.chunk.emit(Op::Jump(0), line);
2652                    self.chunk.patch_jump_here(jump_else);
2653                    self.compile_block(eb)?;
2654                    self.chunk.patch_jump_here(end_j);
2655                } else {
2656                    self.chunk.patch_jump_here(jump_else);
2657                }
2658            }
2659            StmtKind::While {
2660                condition,
2661                body,
2662                label,
2663                continue_block,
2664            } => {
2665                let loop_start = self.chunk.len();
2666                self.compile_boolean_rvalue_condition(condition)?;
2667                let exit_jump = self.chunk.emit(Op::JumpIfFalse(0), line);
2668                let body_start_ip = self.chunk.len();
2669
2670                self.loop_stack.push(LoopCtx {
2671                    label: label.clone(),
2672                    entry_frame_depth: self.frame_depth,
2673                    entry_try_depth: self.try_depth,
2674                    body_start_ip,
2675                    break_jumps: vec![],
2676                    continue_jumps: vec![],
2677                });
2678                self.compile_block_no_frame(body)?;
2679                // `continue { ... }` runs both on normal fall-through from the body and on
2680                // `next` (continue_jumps). `last` still bypasses it via break_jumps.
2681                let continue_entry = self.chunk.len();
2682                let cont_jumps =
2683                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2684                for j in cont_jumps {
2685                    self.chunk.patch_jump_to(j, continue_entry);
2686                }
2687                if let Some(cb) = continue_block {
2688                    self.compile_block_no_frame(cb)?;
2689                }
2690                self.chunk.emit(Op::Jump(loop_start), line);
2691                self.chunk.patch_jump_here(exit_jump);
2692                let ctx = self.loop_stack.pop().expect("loop");
2693                for j in ctx.break_jumps {
2694                    self.chunk.patch_jump_here(j);
2695                }
2696            }
2697            StmtKind::Until {
2698                condition,
2699                body,
2700                label,
2701                continue_block,
2702            } => {
2703                let loop_start = self.chunk.len();
2704                self.compile_boolean_rvalue_condition(condition)?;
2705                let exit_jump = self.chunk.emit(Op::JumpIfTrue(0), line);
2706                let body_start_ip = self.chunk.len();
2707
2708                self.loop_stack.push(LoopCtx {
2709                    label: label.clone(),
2710                    entry_frame_depth: self.frame_depth,
2711                    entry_try_depth: self.try_depth,
2712                    body_start_ip,
2713                    break_jumps: vec![],
2714                    continue_jumps: vec![],
2715                });
2716                self.compile_block_no_frame(body)?;
2717                let continue_entry = self.chunk.len();
2718                let cont_jumps =
2719                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2720                for j in cont_jumps {
2721                    self.chunk.patch_jump_to(j, continue_entry);
2722                }
2723                if let Some(cb) = continue_block {
2724                    self.compile_block_no_frame(cb)?;
2725                }
2726                self.chunk.emit(Op::Jump(loop_start), line);
2727                self.chunk.patch_jump_here(exit_jump);
2728                let ctx = self.loop_stack.pop().expect("loop");
2729                for j in ctx.break_jumps {
2730                    self.chunk.patch_jump_here(j);
2731                }
2732            }
2733            StmtKind::For {
2734                init,
2735                condition,
2736                step,
2737                body,
2738                label,
2739                continue_block,
2740            } => {
2741                // When the enclosing scope uses scalar slots, skip PushFrame/PopFrame for the
2742                // C-style `for` so loop variables (`$i`) and outer variables (`$sum`) share the
2743                // same runtime frame and are both accessible via O(1) slot ops.  The compiler's
2744                // scope layer still tracks `my` declarations for name resolution; only the runtime
2745                // frame push is elided.
2746                let outer_has_slots = self.scope_stack.last().is_some_and(|l| l.use_slots);
2747                if !outer_has_slots {
2748                    self.emit_push_frame(line);
2749                }
2750                if let Some(init) = init {
2751                    self.compile_statement(init)?;
2752                }
2753                let loop_start = self.chunk.len();
2754                let cond_exit = if let Some(cond) = condition {
2755                    self.compile_boolean_rvalue_condition(cond)?;
2756                    Some(self.chunk.emit(Op::JumpIfFalse(0), line))
2757                } else {
2758                    None
2759                };
2760                let body_start_ip = self.chunk.len();
2761
2762                self.loop_stack.push(LoopCtx {
2763                    label: label.clone(),
2764                    entry_frame_depth: self.frame_depth,
2765                    entry_try_depth: self.try_depth,
2766                    body_start_ip,
2767                    break_jumps: cond_exit.into_iter().collect(),
2768                    continue_jumps: vec![],
2769                });
2770                self.compile_block_no_frame(body)?;
2771
2772                let continue_entry = self.chunk.len();
2773                let cont_jumps =
2774                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2775                for j in cont_jumps {
2776                    self.chunk.patch_jump_to(j, continue_entry);
2777                }
2778                if let Some(cb) = continue_block {
2779                    self.compile_block_no_frame(cb)?;
2780                }
2781                if let Some(step) = step {
2782                    self.compile_expr(step)?;
2783                    self.chunk.emit(Op::Pop, line);
2784                }
2785                self.chunk.emit(Op::Jump(loop_start), line);
2786
2787                let ctx = self.loop_stack.pop().expect("loop");
2788                for j in ctx.break_jumps {
2789                    self.chunk.patch_jump_here(j);
2790                }
2791                if !outer_has_slots {
2792                    self.emit_pop_frame(line);
2793                }
2794            }
2795            StmtKind::Foreach {
2796                var,
2797                list,
2798                body,
2799                label,
2800                continue_block,
2801            } => {
2802                // Perl `for ARRAY` aliases the loop variable to each array
2803                // element — mutations through the loop var propagate back to
2804                // the array. We approximate by detecting a bare-`@arr` source
2805                // and emitting a write-back step at the end of each iteration
2806                // (before the counter increment, after the body and any
2807                // continue block). Complex sources (lists, ranges, `keys`)
2808                // keep copy semantics, matching Perl. (BUG-019)
2809                let alias_array_name_idx: Option<u16> = match &list.kind {
2810                    ExprKind::ArrayVar(name) => Some(self.chunk.intern_name(name)),
2811                    _ => None,
2812                };
2813                // PushFrame isolates __foreach_list__ / __foreach_i__ from outer/nested loops.
2814                self.emit_push_frame(line);
2815                self.compile_expr_ctx(list, WantarrayCtx::List)?;
2816                let list_name = self.chunk.intern_name("__foreach_list__");
2817                self.chunk.emit(Op::DeclareArray(list_name), line);
2818
2819                // Counter and loop variable go in slots so the hot per-iteration ops
2820                // (`GetScalarSlot` / `PreIncSlot`) skip the linear frame-scalar scan.
2821                // We cache the slot indices before compiling the body so that any
2822                // nested foreach / inner `my` that reallocates the same name in the
2823                // shared scope layer cannot poison our post-body increment op.
2824                let counter_name = self.chunk.intern_name("__foreach_i__");
2825                self.chunk.emit(Op::LoadInt(0), line);
2826                let counter_slot_opt = self.assign_scalar_slot("__foreach_i__");
2827                if let Some(slot) = counter_slot_opt {
2828                    self.chunk
2829                        .emit(Op::DeclareScalarSlot(slot, counter_name), line);
2830                } else {
2831                    self.chunk.emit(Op::DeclareScalar(counter_name), line);
2832                }
2833
2834                let var_name = self.chunk.intern_name(var);
2835                self.register_declare(Sigil::Scalar, var, false);
2836                self.chunk.emit(Op::LoadUndef, line);
2837                // `$_` is the global topic — keep it in the frame scalars so bareword calls
2838                // and `print`/`printf` arg-defaulting still see it via the usual special-var
2839                // path. Slotting it breaks callees that read `$_` across the call boundary.
2840                let var_slot_opt = if var == "_" {
2841                    None
2842                } else {
2843                    self.assign_scalar_slot(var)
2844                };
2845                if let Some(slot) = var_slot_opt {
2846                    self.chunk.emit(Op::DeclareScalarSlot(slot, var_name), line);
2847                } else {
2848                    self.chunk.emit(Op::DeclareScalar(var_name), line);
2849                }
2850
2851                let loop_start = self.chunk.len();
2852                // Check: $i < scalar @list
2853                if let Some(s) = counter_slot_opt {
2854                    self.chunk.emit(Op::GetScalarSlot(s), line);
2855                } else {
2856                    self.emit_get_scalar(counter_name, line, None);
2857                }
2858                self.chunk.emit(Op::ArrayLen(list_name), line);
2859                self.chunk.emit(Op::NumLt, line);
2860                let exit_jump = self.chunk.emit(Op::JumpIfFalse(0), line);
2861
2862                // $var = $list[$i]
2863                if let Some(s) = counter_slot_opt {
2864                    self.chunk.emit(Op::GetScalarSlot(s), line);
2865                } else {
2866                    self.emit_get_scalar(counter_name, line, None);
2867                }
2868                self.chunk.emit(Op::GetArrayElem(list_name), line);
2869                if let Some(s) = var_slot_opt {
2870                    self.chunk.emit(Op::SetScalarSlot(s), line);
2871                } else {
2872                    self.emit_set_scalar(var_name, line, None);
2873                }
2874                let body_start_ip = self.chunk.len();
2875
2876                self.loop_stack.push(LoopCtx {
2877                    label: label.clone(),
2878                    entry_frame_depth: self.frame_depth,
2879                    entry_try_depth: self.try_depth,
2880                    body_start_ip,
2881                    break_jumps: vec![],
2882                    continue_jumps: vec![],
2883                });
2884                self.compile_block_no_frame(body)?;
2885                // `continue { ... }` on foreach runs after each iteration body (and on `next`),
2886                // before the iterator increment.
2887                let step_ip = self.chunk.len();
2888                let cont_jumps =
2889                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2890                for j in cont_jumps {
2891                    self.chunk.patch_jump_to(j, step_ip);
2892                }
2893                // Alias write-back: $arr[$i] = $loopvar; (BUG-019). Runs at
2894                // the merged step_ip target so both normal-completion and
2895                // `next` paths write the loop var's current value back to the
2896                // source array element.
2897                if let Some(arr_idx) = alias_array_name_idx {
2898                    if let Some(s) = var_slot_opt {
2899                        self.chunk.emit(Op::GetScalarSlot(s), line);
2900                    } else {
2901                        self.emit_get_scalar(var_name, line, None);
2902                    }
2903                    if let Some(s) = counter_slot_opt {
2904                        self.chunk.emit(Op::GetScalarSlot(s), line);
2905                    } else {
2906                        self.emit_get_scalar(counter_name, line, None);
2907                    }
2908                    self.chunk.emit(Op::SetArrayElem(arr_idx), line);
2909                }
2910                if let Some(cb) = continue_block {
2911                    self.compile_block_no_frame(cb)?;
2912                }
2913
2914                // $i++ — use the cached slot directly. The scope layer's scalar_slots
2915                // map may now point `__foreach_i__` at a nested foreach's slot (if any),
2916                // so we must NOT re-resolve through `emit_pre_inc(counter_name)`.
2917                if let Some(s) = counter_slot_opt {
2918                    self.chunk.emit(Op::PreIncSlot(s), line);
2919                } else {
2920                    self.emit_pre_inc(counter_name, line, None);
2921                }
2922                self.chunk.emit(Op::Pop, line);
2923                self.chunk.emit(Op::Jump(loop_start), line);
2924
2925                self.chunk.patch_jump_here(exit_jump);
2926                let ctx = self.loop_stack.pop().expect("loop");
2927                for j in ctx.break_jumps {
2928                    self.chunk.patch_jump_here(j);
2929                }
2930                self.emit_pop_frame(line);
2931                // The foreach pushed a runtime frame for `__foreach_list__`/
2932                // `__foreach_i__`/$var slot isolation, and popped it above.
2933                // Clear the compiler-side slot mappings for those names —
2934                // otherwise a sibling `val $var = …` AFTER the loop emits a
2935                // name-based `DeclareScalarFrozen($var)` (writes via
2936                // `set_scalar_raw` into the now-popped frame's slot table),
2937                // while subsequent reads still resolve through the stale
2938                // `scalar_slot($var)` cache and emit `GetScalarSlot(N)`
2939                // against the wrong frame — observed in `Bsgs::dlog`
2940                // where the inner `val $j = $tab{$gamma}` after a
2941                // `for val $j (…)` would silently leave $j undef.
2942                if let Some(layer) = self.scope_stack.last_mut() {
2943                    if var != "_" {
2944                        layer.scalar_slots.remove(var);
2945                    }
2946                    layer.scalar_slots.remove("__foreach_i__");
2947                    layer.declared_scalars.remove(var);
2948                    layer.declared_arrays.remove("__foreach_list__");
2949                }
2950            }
2951            StmtKind::DoWhile { body, condition } => {
2952                let loop_start = self.chunk.len();
2953                self.loop_stack.push(LoopCtx {
2954                    label: None,
2955                    entry_frame_depth: self.frame_depth,
2956                    entry_try_depth: self.try_depth,
2957                    body_start_ip: loop_start,
2958                    break_jumps: vec![],
2959                    continue_jumps: vec![],
2960                });
2961                self.compile_block_no_frame(body)?;
2962                let cont_jumps =
2963                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2964                for j in cont_jumps {
2965                    self.chunk.patch_jump_to(j, loop_start);
2966                }
2967                self.compile_boolean_rvalue_condition(condition)?;
2968                let exit_jump = self.chunk.emit(Op::JumpIfFalse(0), line);
2969                self.chunk.emit(Op::Jump(loop_start), line);
2970                self.chunk.patch_jump_here(exit_jump);
2971                let ctx = self.loop_stack.pop().expect("loop");
2972                for j in ctx.break_jumps {
2973                    self.chunk.patch_jump_here(j);
2974                }
2975            }
2976            StmtKind::Goto { target } => {
2977                // `goto &sub` (named target): Perl tail call — replace the current sub frame
2978                // with a call to the target, passing `@_` through unchanged. `Op::GotoSub`
2979                // performs the frame replacement at runtime; the trailing `ReturnValue` is
2980                // only reached when replacement is not possible (JIT trampoline frame), where
2981                // the VM falls back to a plain nested call + return of its value.
2982                if let ExprKind::SubroutineRef(name) = &target.kind {
2983                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
2984                    self.chunk.emit(Op::GotoSub(name_idx), line);
2985                    self.chunk.emit(Op::ReturnValue, line);
2986                    return Ok(());
2987                }
2988                // `goto LABEL` where LABEL is a compile-time-known bareword/string: emit a
2989                // forward `Jump(0)` and record it for patching when the current goto-scope
2990                // exits. `goto $expr` (dynamic target) stays Unsupported.
2991                if !self.try_emit_goto_label(target, line) {
2992                    return Err(CompileError::Unsupported(
2993                        "goto with dynamic or sub-ref target".into(),
2994                    ));
2995                }
2996            }
2997            StmtKind::Continue(block) => {
2998                // A bare `continue { ... }` statement (no attached loop) is a parser edge case:
2999                // Perl just runs the block (`Interpreter::exec_block_smart`).
3000                // Match that in the VM path.
3001                for stmt in block {
3002                    self.compile_statement(stmt)?;
3003                }
3004            }
3005            StmtKind::Return(val) => {
3006                if let Some(expr) = val {
3007                    // `return` propagates the caller's wantarray context, so the
3008                    // operand should be evaluated in list context here. Caller-
3009                    // side scalar coercion (Op::CallSub in scalar slot) takes the
3010                    // last element from the returned list — matching Perl's
3011                    // `return (1, 2, 3)` semantics. (BUG-010)
3012                    match &expr.kind {
3013                        ExprKind::Range { .. }
3014                        | ExprKind::SliceRange { .. }
3015                        | ExprKind::ArrayVar(_)
3016                        | ExprKind::List(_)
3017                        | ExprKind::HashVar(_)
3018                        | ExprKind::HashSlice { .. }
3019                        | ExprKind::HashKvSlice { .. }
3020                        | ExprKind::ArraySlice { .. } => {
3021                            self.compile_expr_ctx(expr, WantarrayCtx::List)?;
3022                        }
3023                        _ => {
3024                            self.compile_expr(expr)?;
3025                        }
3026                    }
3027                    self.chunk.emit(Op::ReturnValue, line);
3028                } else {
3029                    self.chunk.emit(Op::Return, line);
3030                }
3031            }
3032            StmtKind::Last(label) | StmtKind::Next(label) => {
3033                // Resolve the target loop via `self.loop_stack` — walk from the innermost loop
3034                // outward, picking the first one that matches the label (or the innermost if
3035                // `last`/`next` has no label). Emit `(frame_depth - entry_frame_depth)`
3036                // `PopFrame` ops first so any intervening block / if-body frames are torn down
3037                // before the jump. `try { }` crossings still bail to tree (see `entry_try_depth`).
3038                let is_last = matches!(&stmt.kind, StmtKind::Last(_));
3039                // Search the loop stack (innermost → outermost) for a matching label.
3040                let (target_idx, entry_frame_depth, entry_try_depth) = {
3041                    let mut found: Option<(usize, usize, usize)> = None;
3042                    for (i, lc) in self.loop_stack.iter().enumerate().rev() {
3043                        let matches = match (label.as_deref(), lc.label.as_deref()) {
3044                            (None, _) => true, // unlabeled `last`/`next` targets innermost loop
3045                            (Some(l), Some(lcl)) => l == lcl,
3046                            (Some(_), None) => false,
3047                        };
3048                        if matches {
3049                            found = Some((i, lc.entry_frame_depth, lc.entry_try_depth));
3050                            break;
3051                        }
3052                    }
3053                    found.ok_or_else(|| {
3054                        CompileError::Unsupported(if label.is_some() {
3055                            format!(
3056                                "last/next with label `{}` — no matching loop in compile scope",
3057                                label.as_deref().unwrap_or("")
3058                            )
3059                        } else {
3060                            "last/next outside any loop".into()
3061                        })
3062                    })?
3063                };
3064                // Cross-try-frame flow control is not modeled in bytecode.
3065                if self.try_depth != entry_try_depth {
3066                    return Err(CompileError::Unsupported(
3067                        "last/next across try { } frame".into(),
3068                    ));
3069                }
3070                // Tear down any scope frames pushed since the loop was entered.
3071                let frames_to_pop = self.frame_depth.saturating_sub(entry_frame_depth);
3072                for _ in 0..frames_to_pop {
3073                    // Emit the `PopFrame` op without decrementing `self.frame_depth` — the
3074                    // compiler is still emitting code for the enclosing block which will later
3075                    // emit its own `PopFrame`; we only need the runtime pop here for the
3076                    // `last`/`next` control path.
3077                    self.chunk.emit(Op::PopFrame, line);
3078                }
3079                let j = self.chunk.emit(Op::Jump(0), line);
3080                let slot = &mut self.loop_stack[target_idx];
3081                if is_last {
3082                    slot.break_jumps.push(j);
3083                } else {
3084                    slot.continue_jumps.push(j);
3085                }
3086            }
3087            StmtKind::Redo(label) => {
3088                let (target_idx, entry_frame_depth, entry_try_depth) = {
3089                    let mut found: Option<(usize, usize, usize)> = None;
3090                    for (i, lc) in self.loop_stack.iter().enumerate().rev() {
3091                        let matches = match (label.as_deref(), lc.label.as_deref()) {
3092                            (None, _) => true,
3093                            (Some(l), Some(lcl)) => l == lcl,
3094                            (Some(_), None) => false,
3095                        };
3096                        if matches {
3097                            found = Some((i, lc.entry_frame_depth, lc.entry_try_depth));
3098                            break;
3099                        }
3100                    }
3101                    found.ok_or_else(|| {
3102                        CompileError::Unsupported(if label.is_some() {
3103                            format!(
3104                                "redo with label `{}` — no matching loop in compile scope",
3105                                label.as_deref().unwrap_or("")
3106                            )
3107                        } else {
3108                            "redo outside any loop".into()
3109                        })
3110                    })?
3111                };
3112                if self.try_depth != entry_try_depth {
3113                    return Err(CompileError::Unsupported(
3114                        "redo across try { } frame".into(),
3115                    ));
3116                }
3117                let frames_to_pop = self.frame_depth.saturating_sub(entry_frame_depth);
3118                for _ in 0..frames_to_pop {
3119                    self.chunk.emit(Op::PopFrame, line);
3120                }
3121                let body_start = self.loop_stack[target_idx].body_start_ip;
3122                let j = self.chunk.emit(Op::Jump(0), line);
3123                self.chunk.patch_jump_to(j, body_start);
3124            }
3125            StmtKind::Block(block) => {
3126                self.chunk.emit(Op::PushFrame, line);
3127                self.compile_block_inner(block)?;
3128                self.chunk.emit(Op::PopFrame, line);
3129            }
3130            StmtKind::StmtGroup(block) => {
3131                self.compile_block_no_frame(block)?;
3132            }
3133            StmtKind::Package { name } => {
3134                self.current_package = name.clone();
3135                let val_idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
3136                let name_idx = self.chunk.intern_name("__PACKAGE__");
3137                self.chunk.emit(Op::LoadConst(val_idx), line);
3138                self.emit_set_scalar(name_idx, line, None);
3139            }
3140            StmtKind::SubDecl {
3141                name,
3142                params,
3143                body,
3144                prototype,
3145            } => {
3146                let idx = self.chunk.runtime_sub_decls.len();
3147                if idx > u16::MAX as usize {
3148                    return Err(CompileError::Unsupported(
3149                        "too many runtime sub declarations in one chunk".into(),
3150                    ));
3151                }
3152                self.chunk.runtime_sub_decls.push(RuntimeSubDecl {
3153                    name: name.clone(),
3154                    params: params.clone(),
3155                    body: body.clone(),
3156                    prototype: prototype.clone(),
3157                });
3158                self.chunk.emit(Op::RuntimeSubDecl(idx as u16), line);
3159            }
3160            StmtKind::AdviceDecl {
3161                kind,
3162                pattern,
3163                body,
3164            } => {
3165                let idx = self.chunk.runtime_advice_decls.len();
3166                if idx > u16::MAX as usize {
3167                    return Err(CompileError::Unsupported(
3168                        "too many AOP advice declarations in one chunk".into(),
3169                    ));
3170                }
3171                // Register the body as a chunk block so the fourth-pass lowering
3172                // (`Compiler::compile_program` / `block_bytecode_ranges`) emits its
3173                // bytecode and `run_block_region` can dispatch it. This keeps the
3174                // body on the VM bytecode path — the tree-walker (`exec_block`) is
3175                // banned for advice. See `tests/tree_walker_absent_aop.rs`.
3176                let body_block_idx = self.add_deferred_block(body.clone());
3177                self.chunk.runtime_advice_decls.push(RuntimeAdviceDecl {
3178                    kind: *kind,
3179                    pattern: pattern.clone(),
3180                    body: body.clone(),
3181                    body_block_idx,
3182                });
3183                self.chunk.emit(Op::RegisterAdvice(idx as u16), line);
3184            }
3185            StmtKind::StructDecl { def } => {
3186                if self.chunk.struct_defs.iter().any(|d| d.name == def.name) {
3187                    return Err(CompileError::Unsupported(format!(
3188                        "duplicate struct `{}`",
3189                        def.name
3190                    )));
3191                }
3192                self.chunk.struct_defs.push(def.clone());
3193            }
3194            StmtKind::EnumDecl { def } => {
3195                if self.chunk.enum_defs.iter().any(|d| d.name == def.name) {
3196                    return Err(CompileError::Unsupported(format!(
3197                        "duplicate enum `{}`",
3198                        def.name
3199                    )));
3200                }
3201                self.chunk.enum_defs.push(def.clone());
3202            }
3203            StmtKind::ClassDecl { def } => {
3204                if self.chunk.class_defs.iter().any(|d| d.name == def.name) {
3205                    return Err(CompileError::Unsupported(format!(
3206                        "duplicate class `{}`",
3207                        def.name
3208                    )));
3209                }
3210                self.chunk.class_defs.push(def.clone());
3211            }
3212            StmtKind::TraitDecl { def } => {
3213                if self.chunk.trait_defs.iter().any(|d| d.name == def.name) {
3214                    return Err(CompileError::Unsupported(format!(
3215                        "duplicate trait `{}`",
3216                        def.name
3217                    )));
3218                }
3219                self.chunk.trait_defs.push(def.clone());
3220            }
3221            StmtKind::TryCatch {
3222                try_block,
3223                catch_var,
3224                catch_block,
3225                finally_block,
3226            } => {
3227                let catch_var_idx = self.chunk.intern_name(catch_var);
3228                let try_push_idx = self.chunk.emit(
3229                    Op::TryPush {
3230                        catch_ip: 0,
3231                        finally_ip: None,
3232                        after_ip: 0,
3233                        catch_var_idx,
3234                    },
3235                    line,
3236                );
3237                self.chunk.emit(Op::PushFrame, line);
3238                if self.program_last_stmt_takes_value {
3239                    self.emit_block_value(try_block, line)?;
3240                } else {
3241                    self.compile_block_inner(try_block)?;
3242                }
3243                self.chunk.emit(Op::PopFrame, line);
3244                self.chunk.emit(Op::TryContinueNormal, line);
3245
3246                let catch_start = self.chunk.len();
3247                self.chunk.patch_try_push_catch(try_push_idx, catch_start);
3248
3249                self.chunk.emit(Op::CatchReceive(catch_var_idx), line);
3250                if self.program_last_stmt_takes_value {
3251                    self.emit_block_value(catch_block, line)?;
3252                } else {
3253                    self.compile_block_inner(catch_block)?;
3254                }
3255                self.chunk.emit(Op::PopFrame, line);
3256                self.chunk.emit(Op::TryContinueNormal, line);
3257
3258                if let Some(fin) = finally_block {
3259                    let finally_start = self.chunk.len();
3260                    self.chunk
3261                        .patch_try_push_finally(try_push_idx, Some(finally_start));
3262                    self.chunk.emit(Op::PushFrame, line);
3263                    self.compile_block_inner(fin)?;
3264                    self.chunk.emit(Op::PopFrame, line);
3265                    self.chunk.emit(Op::TryFinallyEnd, line);
3266                }
3267                let merge = self.chunk.len();
3268                self.chunk.patch_try_push_after(try_push_idx, merge);
3269            }
3270            StmtKind::EvalTimeout { timeout, body } => {
3271                let idx = self
3272                    .chunk
3273                    .add_eval_timeout_entry(timeout.clone(), body.clone());
3274                self.chunk.emit(Op::EvalTimeout(idx), line);
3275            }
3276            StmtKind::Given { topic, body } => {
3277                let idx = self.chunk.add_given_entry(topic.clone(), body.clone());
3278                self.chunk.emit(Op::Given(idx), line);
3279            }
3280            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => {
3281                return Err(CompileError::Unsupported(
3282                    "`when` / `default` only valid inside `given`".into(),
3283                ));
3284            }
3285            StmtKind::Tie {
3286                target,
3287                class,
3288                args,
3289            } => {
3290                self.compile_expr(class)?;
3291                for a in args {
3292                    self.compile_expr(a)?;
3293                }
3294                let (kind, name_idx) = match target {
3295                    TieTarget::Scalar(s) => (0u8, self.chunk.intern_name(s)),
3296                    TieTarget::Array(a) => (1u8, self.chunk.intern_name(a)),
3297                    TieTarget::Hash(h) => (2u8, self.chunk.intern_name(h)),
3298                };
3299                let argc = (1 + args.len()) as u8;
3300                self.chunk.emit(
3301                    Op::Tie {
3302                        target_kind: kind,
3303                        name_idx,
3304                        argc,
3305                    },
3306                    line,
3307                );
3308            }
3309            StmtKind::UseOverload { pairs } => {
3310                let idx = self.chunk.add_use_overload(pairs.clone());
3311                self.chunk.emit(Op::UseOverload(idx), line);
3312            }
3313            StmtKind::Use { module, imports, .. } => {
3314                // `use Env '@PATH'` declares variables that must be visible to strict checking.
3315                if module == "Env" {
3316                    Self::register_env_imports(
3317                        self.scope_stack.last_mut().expect("scope"),
3318                        imports,
3319                    );
3320                }
3321            }
3322            StmtKind::UsePerlVersion { .. }
3323            | StmtKind::No { .. }
3324            | StmtKind::Begin(_)
3325            | StmtKind::UnitCheck(_)
3326            | StmtKind::Check(_)
3327            | StmtKind::Init(_)
3328            | StmtKind::End(_)
3329            | StmtKind::Empty => {
3330                // No-ops or handled elsewhere
3331            }
3332        }
3333        Ok(())
3334    }
3335
3336    /// Returns true if the block contains a Return statement (directly, not in nested subs).
3337    fn block_has_return(block: &Block) -> bool {
3338        for stmt in block {
3339            match &stmt.kind {
3340                StmtKind::Return(_) => return true,
3341                StmtKind::If {
3342                    body,
3343                    elsifs,
3344                    else_block,
3345                    ..
3346                } => {
3347                    if Self::block_has_return(body) {
3348                        return true;
3349                    }
3350                    for (_, blk) in elsifs {
3351                        if Self::block_has_return(blk) {
3352                            return true;
3353                        }
3354                    }
3355                    if let Some(eb) = else_block {
3356                        if Self::block_has_return(eb) {
3357                            return true;
3358                        }
3359                    }
3360                }
3361                StmtKind::Unless {
3362                    body, else_block, ..
3363                } => {
3364                    if Self::block_has_return(body) {
3365                        return true;
3366                    }
3367                    if let Some(eb) = else_block {
3368                        if Self::block_has_return(eb) {
3369                            return true;
3370                        }
3371                    }
3372                }
3373                StmtKind::While { body, .. }
3374                | StmtKind::Until { body, .. }
3375                | StmtKind::Foreach { body, .. }
3376                    if Self::block_has_return(body) =>
3377                {
3378                    return true;
3379                }
3380                StmtKind::For { body, .. } if Self::block_has_return(body) => {
3381                    return true;
3382                }
3383                StmtKind::Block(blk) if Self::block_has_return(blk) => {
3384                    return true;
3385                }
3386                StmtKind::DoWhile { body, .. } if Self::block_has_return(body) => {
3387                    return true;
3388                }
3389                _ => {}
3390            }
3391        }
3392        false
3393    }
3394
3395    /// Returns true if the block contains a local declaration.
3396    fn block_has_local(block: &Block) -> bool {
3397        block.iter().any(|s| match &s.kind {
3398            StmtKind::Local(_) | StmtKind::LocalExpr { .. } => true,
3399            StmtKind::StmtGroup(inner) => Self::block_has_local(inner),
3400            _ => false,
3401        })
3402    }
3403
3404    fn compile_block(&mut self, block: &Block) -> Result<(), CompileError> {
3405        if Self::block_has_return(block) {
3406            self.compile_block_inner(block)?;
3407        } else if self.scope_stack.last().is_some_and(|l| l.use_slots)
3408            && !Self::block_has_local(block)
3409        {
3410            // When scalar slots are active, skip PushFrame/PopFrame so slot indices keep
3411            // addressing the same runtime frame. New `my` / `val` / `var` decls still get
3412            // fresh slot indices via `assign_scalar_slot` in `emit_declare_scalar`.
3413            self.compile_block_inner(block)?;
3414        } else {
3415            self.push_scope_layer();
3416            self.chunk.emit(Op::PushFrame, 0);
3417            self.compile_block_inner(block)?;
3418            self.chunk.emit(Op::PopFrame, 0);
3419            self.pop_scope_layer();
3420        }
3421        Ok(())
3422    }
3423
3424    fn compile_block_inner(&mut self, block: &Block) -> Result<(), CompileError> {
3425        for stmt in block {
3426            self.compile_statement(stmt)?;
3427        }
3428        Ok(())
3429    }
3430
3431    /// Compile a block that leaves its last expression's value on the stack.
3432    /// Used for if/unless as the last statement (implicit return).
3433    fn emit_block_value(&mut self, block: &Block, line: usize) -> Result<(), CompileError> {
3434        if block.is_empty() {
3435            self.chunk.emit(Op::LoadUndef, line);
3436            return Ok(());
3437        }
3438        let last_idx = block.len() - 1;
3439        for (i, stmt) in block.iter().enumerate() {
3440            if i == last_idx {
3441                match &stmt.kind {
3442                    StmtKind::Expression(expr) => {
3443                        self.compile_expr(expr)?;
3444                    }
3445                    StmtKind::Block(inner) => {
3446                        self.chunk.emit(Op::PushFrame, stmt.line);
3447                        self.emit_block_value(inner, stmt.line)?;
3448                        self.chunk.emit(Op::PopFrame, stmt.line);
3449                    }
3450                    StmtKind::StmtGroup(inner) => {
3451                        self.emit_block_value(inner, stmt.line)?;
3452                    }
3453                    StmtKind::If {
3454                        condition,
3455                        body,
3456                        elsifs,
3457                        else_block,
3458                    } => {
3459                        self.compile_boolean_rvalue_condition(condition)?;
3460                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
3461                        self.emit_block_value(body, stmt.line)?;
3462                        let mut ends = vec![self.chunk.emit(Op::Jump(0), stmt.line)];
3463                        self.chunk.patch_jump_here(j0);
3464                        for (c, blk) in elsifs {
3465                            self.compile_boolean_rvalue_condition(c)?;
3466                            let j = self.chunk.emit(Op::JumpIfFalse(0), c.line);
3467                            self.emit_block_value(blk, c.line)?;
3468                            ends.push(self.chunk.emit(Op::Jump(0), c.line));
3469                            self.chunk.patch_jump_here(j);
3470                        }
3471                        if let Some(eb) = else_block {
3472                            self.emit_block_value(eb, stmt.line)?;
3473                        } else {
3474                            self.chunk.emit(Op::LoadUndef, stmt.line);
3475                        }
3476                        for j in ends {
3477                            self.chunk.patch_jump_here(j);
3478                        }
3479                    }
3480                    StmtKind::Unless {
3481                        condition,
3482                        body,
3483                        else_block,
3484                    } => {
3485                        self.compile_boolean_rvalue_condition(condition)?;
3486                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
3487                        if let Some(eb) = else_block {
3488                            self.emit_block_value(eb, stmt.line)?;
3489                        } else {
3490                            self.chunk.emit(Op::LoadUndef, stmt.line);
3491                        }
3492                        let end = self.chunk.emit(Op::Jump(0), stmt.line);
3493                        self.chunk.patch_jump_here(j0);
3494                        self.emit_block_value(body, stmt.line)?;
3495                        self.chunk.patch_jump_here(end);
3496                    }
3497                    _ => self.compile_statement(stmt)?,
3498                }
3499            } else {
3500                self.compile_statement(stmt)?;
3501            }
3502        }
3503        Ok(())
3504    }
3505
3506    /// Compile a subroutine body so the return value matches Perl: the last statement's value is
3507    /// returned when it is an expression or a trailing `if`/`unless` (same shape as the main
3508    /// program's last-statement value rule). Otherwise falls through with `undef` after the last
3509    /// statement unless it already executed `return`.
3510    fn emit_subroutine_body_return(&mut self, body: &Block) -> Result<(), CompileError> {
3511        if body.is_empty() {
3512            self.chunk.emit(Op::LoadUndef, 0);
3513            self.chunk.emit(Op::ReturnValue, 0);
3514            return Ok(());
3515        }
3516        let last_idx = body.len() - 1;
3517        let last = &body[last_idx];
3518        match &last.kind {
3519            StmtKind::Return(_) => {
3520                for stmt in body {
3521                    self.compile_statement(stmt)?;
3522                }
3523            }
3524            StmtKind::Expression(expr) => {
3525                for stmt in &body[..last_idx] {
3526                    self.compile_statement(stmt)?;
3527                }
3528                // Compile tail expression in List context so @array returns
3529                // the array contents, not the count. The caller's ReturnValue
3530                // handler will adapt to the actual wantarray context.
3531                self.compile_expr_ctx(expr, WantarrayCtx::List)?;
3532                self.chunk.emit(Op::ReturnValue, last.line);
3533            }
3534            StmtKind::If {
3535                condition,
3536                body: if_body,
3537                elsifs,
3538                else_block,
3539            } => {
3540                for stmt in &body[..last_idx] {
3541                    self.compile_statement(stmt)?;
3542                }
3543                self.compile_boolean_rvalue_condition(condition)?;
3544                let j0 = self.chunk.emit(Op::JumpIfFalse(0), last.line);
3545                self.emit_block_value(if_body, last.line)?;
3546                let mut ends = vec![self.chunk.emit(Op::Jump(0), last.line)];
3547                self.chunk.patch_jump_here(j0);
3548                for (c, blk) in elsifs {
3549                    self.compile_boolean_rvalue_condition(c)?;
3550                    let j = self.chunk.emit(Op::JumpIfFalse(0), c.line);
3551                    self.emit_block_value(blk, c.line)?;
3552                    ends.push(self.chunk.emit(Op::Jump(0), c.line));
3553                    self.chunk.patch_jump_here(j);
3554                }
3555                if let Some(eb) = else_block {
3556                    self.emit_block_value(eb, last.line)?;
3557                } else {
3558                    self.chunk.emit(Op::LoadUndef, last.line);
3559                }
3560                for j in ends {
3561                    self.chunk.patch_jump_here(j);
3562                }
3563                self.chunk.emit(Op::ReturnValue, last.line);
3564            }
3565            StmtKind::Unless {
3566                condition,
3567                body: unless_body,
3568                else_block,
3569            } => {
3570                for stmt in &body[..last_idx] {
3571                    self.compile_statement(stmt)?;
3572                }
3573                self.compile_boolean_rvalue_condition(condition)?;
3574                let j0 = self.chunk.emit(Op::JumpIfFalse(0), last.line);
3575                if let Some(eb) = else_block {
3576                    self.emit_block_value(eb, last.line)?;
3577                } else {
3578                    self.chunk.emit(Op::LoadUndef, last.line);
3579                }
3580                let end = self.chunk.emit(Op::Jump(0), last.line);
3581                self.chunk.patch_jump_here(j0);
3582                self.emit_block_value(unless_body, last.line)?;
3583                self.chunk.patch_jump_here(end);
3584                self.chunk.emit(Op::ReturnValue, last.line);
3585            }
3586            _ => {
3587                for stmt in body {
3588                    self.compile_statement(stmt)?;
3589                }
3590                self.chunk.emit(Op::LoadUndef, 0);
3591                self.chunk.emit(Op::ReturnValue, 0);
3592            }
3593        }
3594        Ok(())
3595    }
3596
3597    /// Compile a loop body as a sequence of statements. `last`/`next` (including those nested
3598    /// inside `if`/`unless`/block statements) are handled by `compile_statement` via the
3599    /// [`Compiler::loop_stack`] — the innermost loop frame owns their break/continue patches.
3600    fn compile_block_no_frame(&mut self, block: &Block) -> Result<(), CompileError> {
3601        for stmt in block {
3602            self.compile_statement(stmt)?;
3603        }
3604        Ok(())
3605    }
3606
3607    fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
3608        self.compile_expr_ctx(expr, WantarrayCtx::Scalar)
3609    }
3610
3611    fn compile_expr_ctx(&mut self, root: &Expr, ctx: WantarrayCtx) -> Result<(), CompileError> {
3612        let line = root.line;
3613        match &root.kind {
3614            ExprKind::Integer(n) => {
3615                self.emit_op(Op::LoadInt(*n), line, Some(root));
3616            }
3617            ExprKind::Float(f) => {
3618                self.emit_op(Op::LoadFloat(*f), line, Some(root));
3619            }
3620            ExprKind::String(s) => {
3621                let processed = VMHelper::process_case_escapes(s);
3622                let idx = self.chunk.add_constant(StrykeValue::string(processed));
3623                self.emit_op(Op::LoadConst(idx), line, Some(root));
3624            }
3625            ExprKind::Bareword(name) => {
3626                // `BAREWORD` as an rvalue: run-time lookup via `Op::BarewordRvalue` — if a sub
3627                // with this name exists at run time, call it nullary; otherwise push the name
3628                // as a string. Mirrors Perl's `ExprKind::Bareword` eval path.
3629                let idx = self.chunk.intern_name(name);
3630                self.emit_op(Op::BarewordRvalue(idx), line, Some(root));
3631            }
3632            ExprKind::Undef => {
3633                self.emit_op(Op::LoadUndef, line, Some(root));
3634            }
3635            ExprKind::MagicConst(crate::ast::MagicConstKind::File) => {
3636                let idx = self
3637                    .chunk
3638                    .add_constant(StrykeValue::string(self.source_file.clone()));
3639                self.emit_op(Op::LoadConst(idx), line, Some(root));
3640            }
3641            ExprKind::MagicConst(crate::ast::MagicConstKind::Line) => {
3642                let idx = self
3643                    .chunk
3644                    .add_constant(StrykeValue::integer(root.line as i64));
3645                self.emit_op(Op::LoadConst(idx), line, Some(root));
3646            }
3647            ExprKind::MagicConst(crate::ast::MagicConstKind::Sub) => {
3648                self.emit_op(Op::LoadCurrentSub, line, Some(root));
3649            }
3650            ExprKind::ScalarVar(name) => {
3651                self.check_strict_scalar_access(name, line)?;
3652                let idx = self.intern_scalar_var_for_ops(name);
3653                self.emit_get_scalar(idx, line, Some(root));
3654            }
3655            ExprKind::ArrayVar(name) => {
3656                self.check_strict_array_access(name, line)?;
3657                let stash = self.array_storage_name_for_ops(name);
3658                let idx = self
3659                    .chunk
3660                    .intern_name(&self.qualify_stash_array_name(&stash));
3661                if ctx == WantarrayCtx::List {
3662                    self.emit_op(Op::GetArray(idx), line, Some(root));
3663                } else {
3664                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
3665                }
3666            }
3667            ExprKind::HashVar(name) => {
3668                self.check_strict_hash_access(name, line)?;
3669                let stash = self.hash_storage_name_for_ops(name);
3670                let idx = self.chunk.intern_name(&stash);
3671                self.emit_op(Op::GetHash(idx), line, Some(root));
3672                if ctx != WantarrayCtx::List {
3673                    self.emit_op(Op::ValueScalarContext, line, Some(root));
3674                }
3675            }
3676            ExprKind::Typeglob(name) => {
3677                let idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
3678                self.emit_op(Op::LoadConst(idx), line, Some(root));
3679            }
3680            ExprKind::TypeglobExpr(expr) => {
3681                self.compile_expr(expr)?;
3682                self.emit_op(Op::LoadDynamicTypeglob, line, Some(root));
3683            }
3684            ExprKind::ArrayElement { array, index } => {
3685                self.check_strict_array_access(array, line)?;
3686                let idx = self
3687                    .chunk
3688                    .intern_name(&self.qualify_stash_array_name(array));
3689                // Stryke string-slice sugar: `$s[from:to]` / `$s[from:to:step]`
3690                // returns a substring (or stepped pick) when the target is a
3691                // scalar string. Compile to ArraySliceRange — the VM handles
3692                // the scalar-string case when the array is empty.
3693                if let ExprKind::Range {
3694                    from,
3695                    to,
3696                    exclusive,
3697                    step,
3698                } = &index.kind
3699                {
3700                    self.compile_expr(from)?;
3701                    self.compile_expr(to)?;
3702                    self.compile_optional_or_undef(step.as_deref())?;
3703                    let _ = exclusive; // ArraySliceRange semantics treat to as inclusive
3704                    self.emit_op(Op::ArraySliceRange(idx), line, Some(root));
3705                    return Ok(());
3706                }
3707                // Open-ended slices like `$s[1:]`, `$s[:5]`, `$s[::2]`
3708                if let ExprKind::SliceRange { from, to, step } = &index.kind {
3709                    self.compile_optional_or_undef(from.as_deref())?;
3710                    self.compile_optional_or_undef(to.as_deref())?;
3711                    self.compile_optional_or_undef(step.as_deref())?;
3712                    self.emit_op(Op::ArraySliceRange(idx), line, Some(root));
3713                    return Ok(());
3714                }
3715                self.compile_expr(index)?;
3716                self.emit_op(Op::GetArrayElem(idx), line, Some(root));
3717            }
3718            ExprKind::HashElement { hash, key } => {
3719                self.check_strict_hash_access(hash, line)?;
3720                let stash = self.hash_storage_name_for_ops(hash);
3721                let idx = self.chunk.intern_name(&stash);
3722                self.compile_expr(key)?;
3723                self.emit_op(Op::GetHashElem(idx), line, Some(root));
3724            }
3725            ExprKind::ArraySlice { array, indices } => {
3726                let stash = self.array_storage_name_for_ops(array);
3727                let arr_idx = self
3728                    .chunk
3729                    .intern_name(&self.qualify_stash_array_name(&stash));
3730                if indices.is_empty() {
3731                    self.emit_op(Op::MakeArray(0), line, Some(root));
3732                } else if indices.len() == 1 {
3733                    match &indices[0].kind {
3734                        ExprKind::SliceRange { from, to, step } => {
3735                            // Open-ended slice — push (from?, to?, step?); VM resolves
3736                            // defaults from array length. Integer-strict; aborts on
3737                            // non-integer endpoints.
3738                            self.compile_optional_or_undef(from.as_deref())?;
3739                            self.compile_optional_or_undef(to.as_deref())?;
3740                            self.compile_optional_or_undef(step.as_deref())?;
3741                            self.emit_op(Op::ArraySliceRange(arr_idx), line, Some(root));
3742                        }
3743                        ExprKind::Range { from, to, step, .. } => {
3744                            // Closed colon range like `1:3` or `1:3:2` — also strict
3745                            // integer (rejects `"a":"c"`, `1.5:5`, etc.).
3746                            self.compile_expr(from)?;
3747                            self.compile_expr(to)?;
3748                            self.compile_optional_or_undef(step.as_deref())?;
3749                            self.emit_op(Op::ArraySliceRange(arr_idx), line, Some(root));
3750                        }
3751                        _ => {
3752                            self.compile_array_slice_index_expr(&indices[0])?;
3753                            self.emit_op(Op::ArraySlicePart(arr_idx), line, Some(root));
3754                        }
3755                    }
3756                } else {
3757                    for (ix, index_expr) in indices.iter().enumerate() {
3758                        self.compile_array_slice_index_expr(index_expr)?;
3759                        self.emit_op(Op::ArraySlicePart(arr_idx), line, Some(root));
3760                        if ix > 0 {
3761                            self.emit_op(Op::ArrayConcatTwo, line, Some(root));
3762                        }
3763                    }
3764                }
3765            }
3766            ExprKind::HashSlice { hash, keys } => {
3767                let hash_idx = self.chunk.intern_name(hash);
3768                if keys.len() == 1 {
3769                    match &keys[0].kind {
3770                        ExprKind::SliceRange { from, to, step } => {
3771                            // Open-ended hash slice — VM aborts (no "all keys" in
3772                            // unordered hash).
3773                            self.compile_optional_or_undef(from.as_deref())?;
3774                            self.compile_optional_or_undef(to.as_deref())?;
3775                            self.compile_optional_or_undef(step.as_deref())?;
3776                            self.emit_op(Op::HashSliceRange(hash_idx), line, Some(root));
3777                            return Ok(());
3778                        }
3779                        ExprKind::Range { from, to, step, .. } => {
3780                            // Closed colon range like `a:c:1` or `1:3` — endpoints
3781                            // stringify to hash keys (auto-quoted barewords already
3782                            // resolved during parsing).
3783                            self.compile_expr(from)?;
3784                            self.compile_expr(to)?;
3785                            self.compile_optional_or_undef(step.as_deref())?;
3786                            self.emit_op(Op::HashSliceRange(hash_idx), line, Some(root));
3787                            return Ok(());
3788                        }
3789                        _ => {}
3790                    }
3791                }
3792                // If any key expression yields more than one value at runtime
3793                // (range, array variable, arrayref deref, slurpy function call),
3794                // we need runtime flattening via `GetHashSlice` — the per-key
3795                // `GetHashElem` path treats each operand as a single key and
3796                // would scalarize `@arr` to its count (BUG-028).
3797                let has_dynamic_keys = keys.iter().any(|k| {
3798                    matches!(
3799                        &k.kind,
3800                        ExprKind::Range { .. }
3801                            | ExprKind::ArrayVar(_)
3802                            | ExprKind::Deref {
3803                                kind: Sigil::Array,
3804                                ..
3805                            }
3806                            | ExprKind::ArraySlice { .. }
3807                    )
3808                });
3809                if has_dynamic_keys {
3810                    for key_expr in keys {
3811                        self.compile_hash_slice_key_expr(key_expr)?;
3812                    }
3813                    self.emit_op(
3814                        Op::GetHashSlice(hash_idx, keys.len() as u16),
3815                        line,
3816                        Some(root),
3817                    );
3818                } else {
3819                    // Flatten multi-key subscripts (qw, lists) into individual GetHashElem ops
3820                    let mut total_keys = 0u16;
3821                    for key_expr in keys {
3822                        match &key_expr.kind {
3823                            ExprKind::QW(words) => {
3824                                for w in words {
3825                                    let cidx =
3826                                        self.chunk.add_constant(StrykeValue::string(w.clone()));
3827                                    self.emit_op(Op::LoadConst(cidx), line, Some(root));
3828                                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3829                                    total_keys += 1;
3830                                }
3831                            }
3832                            ExprKind::List(elems) => {
3833                                for e in elems {
3834                                    self.compile_expr(e)?;
3835                                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3836                                    total_keys += 1;
3837                                }
3838                            }
3839                            _ => {
3840                                self.compile_expr(key_expr)?;
3841                                self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3842                                total_keys += 1;
3843                            }
3844                        }
3845                    }
3846                    self.emit_op(Op::MakeArray(total_keys), line, Some(root));
3847                }
3848            }
3849            ExprKind::HashKvSlice { hash, keys } => {
3850                // `%h{KEYS}` — Perl 5.20+ kv-slice. Emit key-then-value
3851                // pairs, then MakeArray for a flat list. (BUG-008)
3852                let hash_idx = self.chunk.intern_name(hash);
3853                let mut total_pairs = 0u16;
3854                for key_expr in keys {
3855                    match &key_expr.kind {
3856                        ExprKind::QW(words) => {
3857                            for w in words {
3858                                let kidx = self.chunk.add_constant(StrykeValue::string(w.clone()));
3859                                self.emit_op(Op::LoadConst(kidx), line, Some(root));
3860                                let kidx2 = self.chunk.add_constant(StrykeValue::string(w.clone()));
3861                                self.emit_op(Op::LoadConst(kidx2), line, Some(root));
3862                                self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3863                                total_pairs += 1;
3864                            }
3865                        }
3866                        ExprKind::List(elems) => {
3867                            for e in elems {
3868                                self.compile_expr(e)?;
3869                                self.emit_op(Op::Dup, line, Some(root));
3870                                self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3871                                total_pairs += 1;
3872                            }
3873                        }
3874                        _ => {
3875                            self.compile_expr(key_expr)?;
3876                            self.emit_op(Op::Dup, line, Some(root));
3877                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3878                            total_pairs += 1;
3879                        }
3880                    }
3881                }
3882                self.emit_op(Op::MakeArray(total_pairs * 2), line, Some(root));
3883            }
3884            ExprKind::HashSliceDeref { container, keys } => {
3885                self.compile_expr(container)?;
3886                for key_expr in keys {
3887                    self.compile_hash_slice_key_expr(key_expr)?;
3888                }
3889                self.emit_op(Op::HashSliceDeref(keys.len() as u16), line, Some(root));
3890            }
3891            ExprKind::AnonymousListSlice { source, indices } => {
3892                if indices.is_empty() {
3893                    self.compile_expr_ctx(source, WantarrayCtx::List)?;
3894                    self.emit_op(Op::MakeArray(0), line, Some(root));
3895                } else {
3896                    self.compile_expr_ctx(source, WantarrayCtx::List)?;
3897                    for index_expr in indices {
3898                        self.compile_array_slice_index_expr(index_expr)?;
3899                    }
3900                    self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, Some(root));
3901                }
3902                if ctx != WantarrayCtx::List {
3903                    self.emit_op(Op::ListSliceToScalar, line, Some(root));
3904                }
3905            }
3906
3907            // ── Operators ──
3908            ExprKind::BinOp { left, op, right } => {
3909                // Short-circuit operators
3910                match op {
3911                    BinOp::LogAnd | BinOp::LogAndWord => {
3912                        if matches!(left.kind, ExprKind::Regex(..)) {
3913                            self.compile_boolean_rvalue_condition(left)?;
3914                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3915                        } else {
3916                            self.compile_expr(left)?;
3917                        }
3918                        let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
3919                        // JumpIfFalseKeep already pops on fall-through, so no explicit Pop needed
3920                        if matches!(right.kind, ExprKind::Regex(..)) {
3921                            self.compile_boolean_rvalue_condition(right)?;
3922                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3923                        } else {
3924                            self.compile_expr(right)?;
3925                        }
3926                        self.chunk.patch_jump_here(j);
3927                        return Ok(());
3928                    }
3929                    BinOp::LogOr | BinOp::LogOrWord => {
3930                        if matches!(left.kind, ExprKind::Regex(..)) {
3931                            self.compile_boolean_rvalue_condition(left)?;
3932                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3933                        } else {
3934                            self.compile_expr(left)?;
3935                        }
3936                        let j = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
3937                        // JumpIfTrueKeep already pops on fall-through, so no explicit Pop needed
3938                        if matches!(right.kind, ExprKind::Regex(..)) {
3939                            self.compile_boolean_rvalue_condition(right)?;
3940                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3941                        } else {
3942                            self.compile_expr(right)?;
3943                        }
3944                        self.chunk.patch_jump_here(j);
3945                        return Ok(());
3946                    }
3947                    BinOp::DefinedOr => {
3948                        self.compile_expr(left)?;
3949                        let j = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
3950                        // JumpIfDefinedKeep already pops on fall-through, so no explicit Pop needed
3951                        self.compile_expr(right)?;
3952                        self.chunk.patch_jump_here(j);
3953                        return Ok(());
3954                    }
3955                    BinOp::BindMatch => {
3956                        self.compile_expr(left)?;
3957                        self.compile_expr(right)?;
3958                        self.emit_op(Op::RegexMatchDyn(false), line, Some(root));
3959                        return Ok(());
3960                    }
3961                    BinOp::BindNotMatch => {
3962                        self.compile_expr(left)?;
3963                        self.compile_expr(right)?;
3964                        self.emit_op(Op::RegexMatchDyn(true), line, Some(root));
3965                        return Ok(());
3966                    }
3967                    _ => {}
3968                }
3969
3970                self.compile_expr(left)?;
3971                self.compile_expr(right)?;
3972                let op_code = match op {
3973                    BinOp::Add => Op::Add,
3974                    BinOp::Sub => Op::Sub,
3975                    BinOp::Mul => Op::Mul,
3976                    BinOp::Div => Op::Div,
3977                    BinOp::Mod => Op::Mod,
3978                    BinOp::Pow => Op::Pow,
3979                    BinOp::Concat => Op::Concat,
3980                    BinOp::NumEq => Op::NumEq,
3981                    BinOp::NumNe => Op::NumNe,
3982                    BinOp::NumLt => Op::NumLt,
3983                    BinOp::NumGt => Op::NumGt,
3984                    BinOp::NumLe => Op::NumLe,
3985                    BinOp::NumGe => Op::NumGe,
3986                    BinOp::Spaceship => Op::Spaceship,
3987                    BinOp::StrEq => Op::StrEq,
3988                    BinOp::StrNe => Op::StrNe,
3989                    BinOp::StrLt => Op::StrLt,
3990                    BinOp::StrGt => Op::StrGt,
3991                    BinOp::StrLe => Op::StrLe,
3992                    BinOp::StrGe => Op::StrGe,
3993                    BinOp::StrCmp => Op::StrCmp,
3994                    BinOp::BitAnd => Op::BitAnd,
3995                    BinOp::BitOr => Op::BitOr,
3996                    BinOp::BitXor => Op::BitXor,
3997                    BinOp::ShiftLeft => Op::Shl,
3998                    BinOp::ShiftRight => Op::Shr,
3999                    // Short-circuit and regex bind handled above
4000                    BinOp::LogAnd
4001                    | BinOp::LogOr
4002                    | BinOp::DefinedOr
4003                    | BinOp::LogAndWord
4004                    | BinOp::LogOrWord
4005                    | BinOp::BindMatch
4006                    | BinOp::BindNotMatch => unreachable!(),
4007                };
4008                self.emit_op(op_code, line, Some(root));
4009            }
4010
4011            ExprKind::UnaryOp { op, expr } => match op {
4012                UnaryOp::PreIncrement => {
4013                    if let ExprKind::ScalarVar(name) = &expr.kind {
4014                        self.check_scalar_mutable(name, line)?;
4015                        self.check_closure_write_to_outer_my(name, line)?;
4016                        let idx = self.intern_scalar_var_for_ops(name);
4017                        self.emit_pre_inc(idx, line, Some(root));
4018                    } else if let ExprKind::ArrayElement { array, index } = &expr.kind {
4019                        if self.is_mysync_array(array) {
4020                            return Err(CompileError::Unsupported(
4021                                "mysync array element update".into(),
4022                            ));
4023                        }
4024                        let q = self.qualify_stash_array_name(array);
4025                        self.check_array_mutable(&q, line)?;
4026                        let arr_idx = self.chunk.intern_name(&q);
4027                        self.compile_expr(index)?;
4028                        self.emit_op(Op::Dup, line, Some(root));
4029                        self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4030                        self.emit_op(Op::LoadInt(1), line, Some(root));
4031                        self.emit_op(Op::Add, line, Some(root));
4032                        self.emit_op(Op::Dup, line, Some(root));
4033                        self.emit_op(Op::Rot, line, Some(root));
4034                        self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
4035                    } else if let ExprKind::ArraySlice { array, indices } = &expr.kind {
4036                        if self.is_mysync_array(array) {
4037                            return Err(CompileError::Unsupported(
4038                                "mysync array element update".into(),
4039                            ));
4040                        }
4041                        self.check_strict_array_access(array, line)?;
4042                        let q = self.qualify_stash_array_name(array);
4043                        self.check_array_mutable(&q, line)?;
4044                        let arr_idx = self.chunk.intern_name(&q);
4045                        for ix in indices {
4046                            self.compile_array_slice_index_expr(ix)?;
4047                        }
4048                        self.emit_op(
4049                            Op::NamedArraySliceIncDec(0, arr_idx, indices.len() as u16),
4050                            line,
4051                            Some(root),
4052                        );
4053                    } else if let ExprKind::HashElement { hash, key } = &expr.kind {
4054                        if self.is_mysync_hash(hash) {
4055                            return Err(CompileError::Unsupported(
4056                                "mysync hash element update".into(),
4057                            ));
4058                        }
4059                        self.check_hash_mutable(hash, line)?;
4060                        let hash_idx = self.chunk.intern_name(hash);
4061                        self.compile_expr(key)?;
4062                        self.emit_op(Op::Dup, line, Some(root));
4063                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4064                        self.emit_op(Op::LoadInt(1), line, Some(root));
4065                        self.emit_op(Op::Add, line, Some(root));
4066                        self.emit_op(Op::Dup, line, Some(root));
4067                        self.emit_op(Op::Rot, line, Some(root));
4068                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4069                    } else if let ExprKind::HashSlice { hash, keys } = &expr.kind {
4070                        if self.is_mysync_hash(hash) {
4071                            return Err(CompileError::Unsupported(
4072                                "mysync hash element update".into(),
4073                            ));
4074                        }
4075                        self.check_hash_mutable(hash, line)?;
4076                        let hash_idx = self.chunk.intern_name(hash);
4077                        if hash_slice_needs_slice_ops(keys) {
4078                            for hk in keys {
4079                                self.compile_expr(hk)?;
4080                            }
4081                            self.emit_op(
4082                                Op::NamedHashSliceIncDec(0, hash_idx, keys.len() as u16),
4083                                line,
4084                                Some(root),
4085                            );
4086                            return Ok(());
4087                        }
4088                        let hk = &keys[0];
4089                        self.compile_expr(hk)?;
4090                        self.emit_op(Op::Dup, line, Some(root));
4091                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4092                        self.emit_op(Op::LoadInt(1), line, Some(root));
4093                        self.emit_op(Op::Add, line, Some(root));
4094                        self.emit_op(Op::Dup, line, Some(root));
4095                        self.emit_op(Op::Rot, line, Some(root));
4096                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4097                    } else if let ExprKind::ArrowDeref {
4098                        expr,
4099                        index,
4100                        kind: DerefKind::Array,
4101                    } = &expr.kind
4102                    {
4103                        if let ExprKind::List(indices) = &index.kind {
4104                            // Multi-index `++@$aref[i1,i2,...]` — delegates to VM slice inc-dec.
4105                            self.compile_arrow_array_base_expr(expr)?;
4106                            for ix in indices {
4107                                self.compile_array_slice_index_expr(ix)?;
4108                            }
4109                            self.emit_op(
4110                                Op::ArrowArraySliceIncDec(0, indices.len() as u16),
4111                                line,
4112                                Some(root),
4113                            );
4114                            return Ok(());
4115                        }
4116                        self.compile_arrow_array_base_expr(expr)?;
4117                        self.compile_array_slice_index_expr(index)?;
4118                        self.emit_op(Op::ArrowArraySliceIncDec(0, 1), line, Some(root));
4119                    } else if let ExprKind::AnonymousListSlice { source, indices } = &expr.kind {
4120                        if let ExprKind::Deref {
4121                            expr: inner,
4122                            kind: Sigil::Array,
4123                        } = &source.kind
4124                        {
4125                            self.compile_arrow_array_base_expr(inner)?;
4126                            for ix in indices {
4127                                self.compile_array_slice_index_expr(ix)?;
4128                            }
4129                            self.emit_op(
4130                                Op::ArrowArraySliceIncDec(0, indices.len() as u16),
4131                                line,
4132                                Some(root),
4133                            );
4134                            return Ok(());
4135                        }
4136                    } else if let ExprKind::ArrowDeref {
4137                        expr,
4138                        index,
4139                        kind: DerefKind::Hash,
4140                    } = &expr.kind
4141                    {
4142                        self.compile_arrow_hash_base_expr(expr)?;
4143                        self.compile_expr(index)?;
4144                        self.emit_op(Op::Dup2, line, Some(root));
4145                        self.emit_op(Op::ArrowHash, line, Some(root));
4146                        self.emit_op(Op::LoadInt(1), line, Some(root));
4147                        self.emit_op(Op::Add, line, Some(root));
4148                        self.emit_op(Op::Dup, line, Some(root));
4149                        self.emit_op(Op::Pop, line, Some(root));
4150                        self.emit_op(Op::Swap, line, Some(root));
4151                        self.emit_op(Op::Rot, line, Some(root));
4152                        self.emit_op(Op::Swap, line, Some(root));
4153                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4154                    } else if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
4155                        if hash_slice_needs_slice_ops(keys) {
4156                            // Multi-key: matches generic PreIncrement fallback
4157                            // (list → int → ±1 → slice assign). Dedicated op in VM delegates to
4158                            // Interpreter::hash_slice_deref_inc_dec.
4159                            self.compile_expr(container)?;
4160                            for hk in keys {
4161                                self.compile_expr(hk)?;
4162                            }
4163                            self.emit_op(
4164                                Op::HashSliceDerefIncDec(0, keys.len() as u16),
4165                                line,
4166                                Some(root),
4167                            );
4168                            return Ok(());
4169                        }
4170                        let hk = &keys[0];
4171                        self.compile_expr(container)?;
4172                        self.compile_expr(hk)?;
4173                        self.emit_op(Op::Dup2, line, Some(root));
4174                        self.emit_op(Op::ArrowHash, line, Some(root));
4175                        self.emit_op(Op::LoadInt(1), line, Some(root));
4176                        self.emit_op(Op::Add, line, Some(root));
4177                        self.emit_op(Op::Dup, line, Some(root));
4178                        self.emit_op(Op::Pop, line, Some(root));
4179                        self.emit_op(Op::Swap, line, Some(root));
4180                        self.emit_op(Op::Rot, line, Some(root));
4181                        self.emit_op(Op::Swap, line, Some(root));
4182                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4183                    } else if let ExprKind::Deref {
4184                        expr,
4185                        kind: Sigil::Scalar,
4186                    } = &expr.kind
4187                    {
4188                        self.compile_expr(expr)?;
4189                        self.emit_op(Op::Dup, line, Some(root));
4190                        self.emit_op(Op::SymbolicDeref(0), line, Some(root));
4191                        self.emit_op(Op::LoadInt(1), line, Some(root));
4192                        self.emit_op(Op::Add, line, Some(root));
4193                        self.emit_op(Op::Swap, line, Some(root));
4194                        self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
4195                    } else if let ExprKind::Deref { kind, .. } = &expr.kind {
4196                        // `++@{…}` / `++%{…}` (and `++@$r` / `++%$r`) are invalid in Perl 5.
4197                        // Emit a runtime error directly so the VM produces the same error
4198                        // Perl would.
4199                        self.emit_aggregate_symbolic_inc_dec_error(*kind, true, true, line, root)?;
4200                    } else {
4201                        return Err(CompileError::Unsupported("PreInc on non-scalar".into()));
4202                    }
4203                }
4204                UnaryOp::PreDecrement => {
4205                    if let ExprKind::ScalarVar(name) = &expr.kind {
4206                        self.check_scalar_mutable(name, line)?;
4207                        self.check_closure_write_to_outer_my(name, line)?;
4208                        let idx = self.intern_scalar_var_for_ops(name);
4209                        self.emit_pre_dec(idx, line, Some(root));
4210                    } else if let ExprKind::ArrayElement { array, index } = &expr.kind {
4211                        if self.is_mysync_array(array) {
4212                            return Err(CompileError::Unsupported(
4213                                "mysync array element update".into(),
4214                            ));
4215                        }
4216                        let q = self.qualify_stash_array_name(array);
4217                        self.check_array_mutable(&q, line)?;
4218                        let arr_idx = self.chunk.intern_name(&q);
4219                        self.compile_expr(index)?;
4220                        self.emit_op(Op::Dup, line, Some(root));
4221                        self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4222                        self.emit_op(Op::LoadInt(1), line, Some(root));
4223                        self.emit_op(Op::Sub, line, Some(root));
4224                        self.emit_op(Op::Dup, line, Some(root));
4225                        self.emit_op(Op::Rot, line, Some(root));
4226                        self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
4227                    } else if let ExprKind::ArraySlice { array, indices } = &expr.kind {
4228                        if self.is_mysync_array(array) {
4229                            return Err(CompileError::Unsupported(
4230                                "mysync array element update".into(),
4231                            ));
4232                        }
4233                        self.check_strict_array_access(array, line)?;
4234                        let q = self.qualify_stash_array_name(array);
4235                        self.check_array_mutable(&q, line)?;
4236                        let arr_idx = self.chunk.intern_name(&q);
4237                        for ix in indices {
4238                            self.compile_array_slice_index_expr(ix)?;
4239                        }
4240                        self.emit_op(
4241                            Op::NamedArraySliceIncDec(1, arr_idx, indices.len() as u16),
4242                            line,
4243                            Some(root),
4244                        );
4245                    } else if let ExprKind::HashElement { hash, key } = &expr.kind {
4246                        if self.is_mysync_hash(hash) {
4247                            return Err(CompileError::Unsupported(
4248                                "mysync hash element update".into(),
4249                            ));
4250                        }
4251                        self.check_hash_mutable(hash, line)?;
4252                        let hash_idx = self.chunk.intern_name(hash);
4253                        self.compile_expr(key)?;
4254                        self.emit_op(Op::Dup, line, Some(root));
4255                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4256                        self.emit_op(Op::LoadInt(1), line, Some(root));
4257                        self.emit_op(Op::Sub, line, Some(root));
4258                        self.emit_op(Op::Dup, line, Some(root));
4259                        self.emit_op(Op::Rot, line, Some(root));
4260                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4261                    } else if let ExprKind::HashSlice { hash, keys } = &expr.kind {
4262                        if self.is_mysync_hash(hash) {
4263                            return Err(CompileError::Unsupported(
4264                                "mysync hash element update".into(),
4265                            ));
4266                        }
4267                        self.check_hash_mutable(hash, line)?;
4268                        let hash_idx = self.chunk.intern_name(hash);
4269                        if hash_slice_needs_slice_ops(keys) {
4270                            for hk in keys {
4271                                self.compile_expr(hk)?;
4272                            }
4273                            self.emit_op(
4274                                Op::NamedHashSliceIncDec(1, hash_idx, keys.len() as u16),
4275                                line,
4276                                Some(root),
4277                            );
4278                            return Ok(());
4279                        }
4280                        let hk = &keys[0];
4281                        self.compile_expr(hk)?;
4282                        self.emit_op(Op::Dup, line, Some(root));
4283                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4284                        self.emit_op(Op::LoadInt(1), line, Some(root));
4285                        self.emit_op(Op::Sub, line, Some(root));
4286                        self.emit_op(Op::Dup, line, Some(root));
4287                        self.emit_op(Op::Rot, line, Some(root));
4288                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4289                    } else if let ExprKind::ArrowDeref {
4290                        expr,
4291                        index,
4292                        kind: DerefKind::Array,
4293                    } = &expr.kind
4294                    {
4295                        if let ExprKind::List(indices) = &index.kind {
4296                            self.compile_arrow_array_base_expr(expr)?;
4297                            for ix in indices {
4298                                self.compile_array_slice_index_expr(ix)?;
4299                            }
4300                            self.emit_op(
4301                                Op::ArrowArraySliceIncDec(1, indices.len() as u16),
4302                                line,
4303                                Some(root),
4304                            );
4305                            return Ok(());
4306                        }
4307                        self.compile_arrow_array_base_expr(expr)?;
4308                        self.compile_array_slice_index_expr(index)?;
4309                        self.emit_op(Op::ArrowArraySliceIncDec(1, 1), line, Some(root));
4310                    } else if let ExprKind::AnonymousListSlice { source, indices } = &expr.kind {
4311                        if let ExprKind::Deref {
4312                            expr: inner,
4313                            kind: Sigil::Array,
4314                        } = &source.kind
4315                        {
4316                            self.compile_arrow_array_base_expr(inner)?;
4317                            for ix in indices {
4318                                self.compile_array_slice_index_expr(ix)?;
4319                            }
4320                            self.emit_op(
4321                                Op::ArrowArraySliceIncDec(1, indices.len() as u16),
4322                                line,
4323                                Some(root),
4324                            );
4325                            return Ok(());
4326                        }
4327                    } else if let ExprKind::ArrowDeref {
4328                        expr,
4329                        index,
4330                        kind: DerefKind::Hash,
4331                    } = &expr.kind
4332                    {
4333                        self.compile_arrow_hash_base_expr(expr)?;
4334                        self.compile_expr(index)?;
4335                        self.emit_op(Op::Dup2, line, Some(root));
4336                        self.emit_op(Op::ArrowHash, line, Some(root));
4337                        self.emit_op(Op::LoadInt(1), line, Some(root));
4338                        self.emit_op(Op::Sub, line, Some(root));
4339                        self.emit_op(Op::Dup, line, Some(root));
4340                        self.emit_op(Op::Pop, line, Some(root));
4341                        self.emit_op(Op::Swap, line, Some(root));
4342                        self.emit_op(Op::Rot, line, Some(root));
4343                        self.emit_op(Op::Swap, line, Some(root));
4344                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4345                    } else if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
4346                        if hash_slice_needs_slice_ops(keys) {
4347                            self.compile_expr(container)?;
4348                            for hk in keys {
4349                                self.compile_expr(hk)?;
4350                            }
4351                            self.emit_op(
4352                                Op::HashSliceDerefIncDec(1, keys.len() as u16),
4353                                line,
4354                                Some(root),
4355                            );
4356                            return Ok(());
4357                        }
4358                        let hk = &keys[0];
4359                        self.compile_expr(container)?;
4360                        self.compile_expr(hk)?;
4361                        self.emit_op(Op::Dup2, line, Some(root));
4362                        self.emit_op(Op::ArrowHash, line, Some(root));
4363                        self.emit_op(Op::LoadInt(1), line, Some(root));
4364                        self.emit_op(Op::Sub, line, Some(root));
4365                        self.emit_op(Op::Dup, line, Some(root));
4366                        self.emit_op(Op::Pop, line, Some(root));
4367                        self.emit_op(Op::Swap, line, Some(root));
4368                        self.emit_op(Op::Rot, line, Some(root));
4369                        self.emit_op(Op::Swap, line, Some(root));
4370                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4371                    } else if let ExprKind::Deref {
4372                        expr,
4373                        kind: Sigil::Scalar,
4374                    } = &expr.kind
4375                    {
4376                        self.compile_expr(expr)?;
4377                        self.emit_op(Op::Dup, line, Some(root));
4378                        self.emit_op(Op::SymbolicDeref(0), line, Some(root));
4379                        self.emit_op(Op::LoadInt(1), line, Some(root));
4380                        self.emit_op(Op::Sub, line, Some(root));
4381                        self.emit_op(Op::Swap, line, Some(root));
4382                        self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
4383                    } else if let ExprKind::Deref { kind, .. } = &expr.kind {
4384                        self.emit_aggregate_symbolic_inc_dec_error(*kind, true, false, line, root)?;
4385                    } else {
4386                        return Err(CompileError::Unsupported("PreDec on non-scalar".into()));
4387                    }
4388                }
4389                UnaryOp::Ref => {
4390                    self.compile_expr(expr)?;
4391                    self.emit_op(Op::MakeScalarRef, line, Some(root));
4392                }
4393                _ => match op {
4394                    UnaryOp::LogNot | UnaryOp::LogNotWord => {
4395                        if matches!(expr.kind, ExprKind::Regex(..)) {
4396                            self.compile_boolean_rvalue_condition(expr)?;
4397                        } else {
4398                            self.compile_expr(expr)?;
4399                        }
4400                        self.emit_op(Op::LogNot, line, Some(root));
4401                    }
4402                    UnaryOp::Negate => {
4403                        self.compile_expr(expr)?;
4404                        self.emit_op(Op::Negate, line, Some(root));
4405                    }
4406                    UnaryOp::BitNot => {
4407                        self.compile_expr(expr)?;
4408                        self.emit_op(Op::BitNot, line, Some(root));
4409                    }
4410                    _ => unreachable!(),
4411                },
4412            },
4413            ExprKind::PostfixOp { expr, op } => {
4414                if let ExprKind::ScalarVar(name) = &expr.kind {
4415                    self.check_scalar_mutable(name, line)?;
4416                    self.check_closure_write_to_outer_my(name, line)?;
4417                    let idx = self.intern_scalar_var_for_ops(name);
4418                    match op {
4419                        PostfixOp::Increment => {
4420                            self.emit_post_inc(idx, line, Some(root));
4421                        }
4422                        PostfixOp::Decrement => {
4423                            self.emit_post_dec(idx, line, Some(root));
4424                        }
4425                    }
4426                } else if let ExprKind::ArrayElement { array, index } = &expr.kind {
4427                    if self.is_mysync_array(array) {
4428                        return Err(CompileError::Unsupported(
4429                            "mysync array element update".into(),
4430                        ));
4431                    }
4432                    let q = self.qualify_stash_array_name(array);
4433                    self.check_array_mutable(&q, line)?;
4434                    let arr_idx = self.chunk.intern_name(&q);
4435                    self.compile_expr(index)?;
4436                    self.emit_op(Op::Dup, line, Some(root));
4437                    self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4438                    self.emit_op(Op::Dup, line, Some(root));
4439                    self.emit_op(Op::LoadInt(1), line, Some(root));
4440                    match op {
4441                        PostfixOp::Increment => {
4442                            self.emit_op(Op::Add, line, Some(root));
4443                        }
4444                        PostfixOp::Decrement => {
4445                            self.emit_op(Op::Sub, line, Some(root));
4446                        }
4447                    }
4448                    self.emit_op(Op::Rot, line, Some(root));
4449                    self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
4450                } else if let ExprKind::ArraySlice { array, indices } = &expr.kind {
4451                    if self.is_mysync_array(array) {
4452                        return Err(CompileError::Unsupported(
4453                            "mysync array element update".into(),
4454                        ));
4455                    }
4456                    self.check_strict_array_access(array, line)?;
4457                    let q = self.qualify_stash_array_name(array);
4458                    self.check_array_mutable(&q, line)?;
4459                    let arr_idx = self.chunk.intern_name(&q);
4460                    let kind_byte: u8 = match op {
4461                        PostfixOp::Increment => 2,
4462                        PostfixOp::Decrement => 3,
4463                    };
4464                    for ix in indices {
4465                        self.compile_array_slice_index_expr(ix)?;
4466                    }
4467                    self.emit_op(
4468                        Op::NamedArraySliceIncDec(kind_byte, arr_idx, indices.len() as u16),
4469                        line,
4470                        Some(root),
4471                    );
4472                } else if let ExprKind::HashElement { hash, key } = &expr.kind {
4473                    if self.is_mysync_hash(hash) {
4474                        return Err(CompileError::Unsupported(
4475                            "mysync hash element update".into(),
4476                        ));
4477                    }
4478                    self.check_hash_mutable(hash, line)?;
4479                    let hash_idx = self.chunk.intern_name(hash);
4480                    self.compile_expr(key)?;
4481                    self.emit_op(Op::Dup, line, Some(root));
4482                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4483                    self.emit_op(Op::Dup, line, Some(root));
4484                    self.emit_op(Op::LoadInt(1), line, Some(root));
4485                    match op {
4486                        PostfixOp::Increment => {
4487                            self.emit_op(Op::Add, line, Some(root));
4488                        }
4489                        PostfixOp::Decrement => {
4490                            self.emit_op(Op::Sub, line, Some(root));
4491                        }
4492                    }
4493                    self.emit_op(Op::Rot, line, Some(root));
4494                    self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4495                } else if let ExprKind::HashSlice { hash, keys } = &expr.kind {
4496                    if self.is_mysync_hash(hash) {
4497                        return Err(CompileError::Unsupported(
4498                            "mysync hash element update".into(),
4499                        ));
4500                    }
4501                    self.check_hash_mutable(hash, line)?;
4502                    let hash_idx = self.chunk.intern_name(hash);
4503                    if hash_slice_needs_slice_ops(keys) {
4504                        let kind_byte: u8 = match op {
4505                            PostfixOp::Increment => 2,
4506                            PostfixOp::Decrement => 3,
4507                        };
4508                        for hk in keys {
4509                            self.compile_expr(hk)?;
4510                        }
4511                        self.emit_op(
4512                            Op::NamedHashSliceIncDec(kind_byte, hash_idx, keys.len() as u16),
4513                            line,
4514                            Some(root),
4515                        );
4516                        return Ok(());
4517                    }
4518                    let hk = &keys[0];
4519                    self.compile_expr(hk)?;
4520                    self.emit_op(Op::Dup, line, Some(root));
4521                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4522                    self.emit_op(Op::Dup, line, Some(root));
4523                    self.emit_op(Op::LoadInt(1), line, Some(root));
4524                    match op {
4525                        PostfixOp::Increment => {
4526                            self.emit_op(Op::Add, line, Some(root));
4527                        }
4528                        PostfixOp::Decrement => {
4529                            self.emit_op(Op::Sub, line, Some(root));
4530                        }
4531                    }
4532                    self.emit_op(Op::Rot, line, Some(root));
4533                    self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4534                } else if let ExprKind::ArrowDeref {
4535                    expr: inner,
4536                    index,
4537                    kind: DerefKind::Array,
4538                } = &expr.kind
4539                {
4540                    if let ExprKind::List(indices) = &index.kind {
4541                        let kind_byte: u8 = match op {
4542                            PostfixOp::Increment => 2,
4543                            PostfixOp::Decrement => 3,
4544                        };
4545                        self.compile_arrow_array_base_expr(inner)?;
4546                        for ix in indices {
4547                            self.compile_array_slice_index_expr(ix)?;
4548                        }
4549                        self.emit_op(
4550                            Op::ArrowArraySliceIncDec(kind_byte, indices.len() as u16),
4551                            line,
4552                            Some(root),
4553                        );
4554                        return Ok(());
4555                    }
4556                    self.compile_arrow_array_base_expr(inner)?;
4557                    self.compile_array_slice_index_expr(index)?;
4558                    let kind_byte: u8 = match op {
4559                        PostfixOp::Increment => 2,
4560                        PostfixOp::Decrement => 3,
4561                    };
4562                    self.emit_op(Op::ArrowArraySliceIncDec(kind_byte, 1), line, Some(root));
4563                } else if let ExprKind::AnonymousListSlice { source, indices } = &expr.kind {
4564                    let ExprKind::Deref {
4565                        expr: inner,
4566                        kind: Sigil::Array,
4567                    } = &source.kind
4568                    else {
4569                        return Err(CompileError::Unsupported(
4570                            "PostfixOp on list slice (non-array deref)".into(),
4571                        ));
4572                    };
4573                    if indices.is_empty() {
4574                        return Err(CompileError::Unsupported(
4575                            "postfix ++/-- on empty list slice (internal)".into(),
4576                        ));
4577                    }
4578                    let kind_byte: u8 = match op {
4579                        PostfixOp::Increment => 2,
4580                        PostfixOp::Decrement => 3,
4581                    };
4582                    self.compile_arrow_array_base_expr(inner)?;
4583                    if indices.len() > 1 {
4584                        for ix in indices {
4585                            self.compile_array_slice_index_expr(ix)?;
4586                        }
4587                        self.emit_op(
4588                            Op::ArrowArraySliceIncDec(kind_byte, indices.len() as u16),
4589                            line,
4590                            Some(root),
4591                        );
4592                    } else {
4593                        self.compile_array_slice_index_expr(&indices[0])?;
4594                        self.emit_op(Op::ArrowArraySliceIncDec(kind_byte, 1), line, Some(root));
4595                    }
4596                } else if let ExprKind::ArrowDeref {
4597                    expr: inner,
4598                    index,
4599                    kind: DerefKind::Hash,
4600                } = &expr.kind
4601                {
4602                    self.compile_arrow_hash_base_expr(inner)?;
4603                    self.compile_expr(index)?;
4604                    let b = match op {
4605                        PostfixOp::Increment => 0u8,
4606                        PostfixOp::Decrement => 1u8,
4607                    };
4608                    self.emit_op(Op::ArrowHashPostfix(b), line, Some(root));
4609                } else if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
4610                    if hash_slice_needs_slice_ops(keys) {
4611                        // Multi-key postfix ++/--: matches generic PostfixOp fallback
4612                        // (reads slice list, assigns scalar back, returns old list).
4613                        let kind_byte: u8 = match op {
4614                            PostfixOp::Increment => 2,
4615                            PostfixOp::Decrement => 3,
4616                        };
4617                        self.compile_expr(container)?;
4618                        for hk in keys {
4619                            self.compile_expr(hk)?;
4620                        }
4621                        self.emit_op(
4622                            Op::HashSliceDerefIncDec(kind_byte, keys.len() as u16),
4623                            line,
4624                            Some(root),
4625                        );
4626                        return Ok(());
4627                    }
4628                    let hk = &keys[0];
4629                    self.compile_expr(container)?;
4630                    self.compile_expr(hk)?;
4631                    let b = match op {
4632                        PostfixOp::Increment => 0u8,
4633                        PostfixOp::Decrement => 1u8,
4634                    };
4635                    self.emit_op(Op::ArrowHashPostfix(b), line, Some(root));
4636                } else if let ExprKind::Deref {
4637                    expr,
4638                    kind: Sigil::Scalar,
4639                } = &expr.kind
4640                {
4641                    self.compile_expr(expr)?;
4642                    let b = match op {
4643                        PostfixOp::Increment => 0u8,
4644                        PostfixOp::Decrement => 1u8,
4645                    };
4646                    self.emit_op(Op::SymbolicScalarRefPostfix(b), line, Some(root));
4647                } else if let ExprKind::Deref { kind, .. } = &expr.kind {
4648                    let is_inc = matches!(op, PostfixOp::Increment);
4649                    self.emit_aggregate_symbolic_inc_dec_error(*kind, false, is_inc, line, root)?;
4650                } else {
4651                    return Err(CompileError::Unsupported("PostfixOp on non-scalar".into()));
4652                }
4653            }
4654
4655            ExprKind::Assign { target, value } => {
4656                // `substr($s, $o, $l) = $rhs` is equivalent to the 4-arg
4657                // form `substr($s, $o, $l, $rhs)`. Rewrite + compile as
4658                // a function call so the dedicated lvalue path isn't
4659                // needed.
4660                if let ExprKind::Substr {
4661                    string,
4662                    offset,
4663                    length,
4664                    replacement: None,
4665                } = &target.kind
4666                {
4667                    let rewritten = Expr {
4668                        kind: ExprKind::Substr {
4669                            string: string.clone(),
4670                            offset: offset.clone(),
4671                            length: length.clone(),
4672                            replacement: Some(value.clone()),
4673                        },
4674                        line: target.line,
4675                    };
4676                    self.compile_expr(&rewritten)?;
4677                    return Ok(());
4678                }
4679                // `vec($s, $o, $b) = $rhs` — set the requested bit field
4680                // in $s and write back. Lower to:
4681                //     $s = vec_set_value($s, $o, $b, $rhs);
4682                // where `vec_set_value` is a 4-arg pure builtin that
4683                // returns the modified string. This lets every supported
4684                // lvalue shape for `$s` (plain scalar, array element,
4685                // hash element, etc.) reuse its existing assign path.
4686                if let ExprKind::FuncCall { name, args } = &target.kind {
4687                    if name == "vec" && args.len() == 3 {
4688                        let new_call = Expr {
4689                            kind: ExprKind::FuncCall {
4690                                name: "vec_set_value".to_string(),
4691                                args: vec![
4692                                    args[0].clone(),
4693                                    args[1].clone(),
4694                                    args[2].clone(),
4695                                    (**value).clone(),
4696                                ],
4697                            },
4698                            line: target.line,
4699                        };
4700                        let rewritten = Expr {
4701                            kind: ExprKind::Assign {
4702                                target: Box::new(args[0].clone()),
4703                                value: Box::new(new_call),
4704                            },
4705                            line,
4706                        };
4707                        self.compile_expr(&rewritten)?;
4708                        return Ok(());
4709                    }
4710                }
4711                if let (ExprKind::Typeglob(lhs), ExprKind::Typeglob(rhs)) =
4712                    (&target.kind, &value.kind)
4713                {
4714                    let lhs_idx = self.chunk.intern_name(lhs);
4715                    let rhs_idx = self.chunk.intern_name(rhs);
4716                    self.emit_op(Op::CopyTypeglobSlots(lhs_idx, rhs_idx), line, Some(root));
4717                    self.compile_expr(value)?;
4718                    return Ok(());
4719                }
4720                if let ExprKind::TypeglobExpr(expr) = &target.kind {
4721                    if let ExprKind::Typeglob(rhs) = &value.kind {
4722                        self.compile_expr(expr)?;
4723                        let rhs_idx = self.chunk.intern_name(rhs);
4724                        self.emit_op(Op::CopyTypeglobSlotsDynamicLhs(rhs_idx), line, Some(root));
4725                        self.compile_expr(value)?;
4726                        return Ok(());
4727                    }
4728                    self.compile_expr(expr)?;
4729                    self.compile_expr(value)?;
4730                    self.emit_op(Op::TypeglobAssignFromValueDynamic, line, Some(root));
4731                    return Ok(());
4732                }
4733                // Braced `*{EXPR}` parses as `Deref { kind: Typeglob }` (same VM lowering as `TypeglobExpr`).
4734                if let ExprKind::Deref {
4735                    expr,
4736                    kind: Sigil::Typeglob,
4737                } = &target.kind
4738                {
4739                    if let ExprKind::Typeglob(rhs) = &value.kind {
4740                        self.compile_expr(expr)?;
4741                        let rhs_idx = self.chunk.intern_name(rhs);
4742                        self.emit_op(Op::CopyTypeglobSlotsDynamicLhs(rhs_idx), line, Some(root));
4743                        self.compile_expr(value)?;
4744                        return Ok(());
4745                    }
4746                    self.compile_expr(expr)?;
4747                    self.compile_expr(value)?;
4748                    self.emit_op(Op::TypeglobAssignFromValueDynamic, line, Some(root));
4749                    return Ok(());
4750                }
4751                if let ExprKind::ArrowDeref {
4752                    expr,
4753                    index,
4754                    kind: DerefKind::Array,
4755                } = &target.kind
4756                {
4757                    if let ExprKind::List(indices) = &index.kind {
4758                        if let ExprKind::Deref {
4759                            expr: inner,
4760                            kind: Sigil::Array,
4761                        } = &expr.kind
4762                        {
4763                            if let ExprKind::List(vals) = &value.kind {
4764                                if !indices.is_empty() && indices.len() == vals.len() {
4765                                    for (idx_e, val_e) in indices.iter().zip(vals.iter()) {
4766                                        self.compile_expr(val_e)?;
4767                                        self.compile_expr(inner)?;
4768                                        self.compile_expr(idx_e)?;
4769                                        self.emit_op(Op::SetArrowArray, line, Some(root));
4770                                    }
4771                                    return Ok(());
4772                                }
4773                            }
4774                        }
4775                    }
4776                }
4777                // Fuse `$x = $x OP $y` / `$x = $x + 1` into slot ops when possible.
4778                if let ExprKind::ScalarVar(tgt_name) = &target.kind {
4779                    if let Some(dst_slot) = self.scalar_slot(tgt_name) {
4780                        if let ExprKind::BinOp { left, op, right } = &value.kind {
4781                            if let ExprKind::ScalarVar(lv) = &left.kind {
4782                                if lv == tgt_name {
4783                                    // $x = $x + SCALAR_VAR → AddAssignSlotSlot etc.
4784                                    if let ExprKind::ScalarVar(rv) = &right.kind {
4785                                        if let Some(src_slot) = self.scalar_slot(rv) {
4786                                            let fused = match op {
4787                                                BinOp::Add => {
4788                                                    Some(Op::AddAssignSlotSlot(dst_slot, src_slot))
4789                                                }
4790                                                BinOp::Sub => {
4791                                                    Some(Op::SubAssignSlotSlot(dst_slot, src_slot))
4792                                                }
4793                                                BinOp::Mul => {
4794                                                    Some(Op::MulAssignSlotSlot(dst_slot, src_slot))
4795                                                }
4796                                                _ => None,
4797                                            };
4798                                            if let Some(fop) = fused {
4799                                                self.emit_op(fop, line, Some(root));
4800                                                return Ok(());
4801                                            }
4802                                        }
4803                                    }
4804                                    // $x = $x + 1 → PreIncSlot, $x = $x - 1 → PreDecSlot
4805                                    if let ExprKind::Integer(1) = &right.kind {
4806                                        match op {
4807                                            BinOp::Add => {
4808                                                self.emit_op(
4809                                                    Op::PreIncSlot(dst_slot),
4810                                                    line,
4811                                                    Some(root),
4812                                                );
4813                                                return Ok(());
4814                                            }
4815                                            BinOp::Sub => {
4816                                                self.emit_op(
4817                                                    Op::PreDecSlot(dst_slot),
4818                                                    line,
4819                                                    Some(root),
4820                                                );
4821                                                return Ok(());
4822                                            }
4823                                            _ => {}
4824                                        }
4825                                    }
4826                                }
4827                            }
4828                        }
4829                    }
4830                }
4831                self.compile_expr_ctx(value, assign_rhs_wantarray(target))?;
4832                self.compile_assign(target, line, true, Some(root))?;
4833            }
4834            ExprKind::CompoundAssign { target, op, value } => {
4835                if let ExprKind::ScalarVar(name) = &target.kind {
4836                    self.check_scalar_mutable(name, line)?;
4837                    let idx = self.intern_scalar_var_for_ops(name);
4838                    // Fast path: `.=` on scalar → in-place append (no clone)
4839                    if *op == BinOp::Concat {
4840                        self.compile_expr(value)?;
4841                        if let Some(slot) = self.scalar_slot(name) {
4842                            self.emit_op(Op::ConcatAppendSlot(slot), line, Some(root));
4843                        } else {
4844                            self.emit_op(Op::ConcatAppend(idx), line, Some(root));
4845                        }
4846                        return Ok(());
4847                    }
4848                    // Fused slot+slot arithmetic: $slot_a += $slot_b (no stack traffic)
4849                    if let Some(dst_slot) = self.scalar_slot(name) {
4850                        if let ExprKind::ScalarVar(rhs_name) = &value.kind {
4851                            if let Some(src_slot) = self.scalar_slot(rhs_name) {
4852                                let fused = match op {
4853                                    BinOp::Add => Some(Op::AddAssignSlotSlot(dst_slot, src_slot)),
4854                                    BinOp::Sub => Some(Op::SubAssignSlotSlot(dst_slot, src_slot)),
4855                                    BinOp::Mul => Some(Op::MulAssignSlotSlot(dst_slot, src_slot)),
4856                                    _ => None,
4857                                };
4858                                if let Some(fop) = fused {
4859                                    self.emit_op(fop, line, Some(root));
4860                                    return Ok(());
4861                                }
4862                            }
4863                        }
4864                    }
4865                    if *op == BinOp::DefinedOr {
4866                        // `$x //=` — short-circuit when LHS is defined.
4867                        // Slot-aware: use GetScalarSlot/SetScalarSlot if available.
4868                        if let Some(slot) = self.scalar_slot(name) {
4869                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4870                            let j_def = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
4871                            self.compile_expr(value)?;
4872                            self.emit_op(Op::Dup, line, Some(root));
4873                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4874                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4875                            self.chunk.patch_jump_here(j_def);
4876                            self.chunk.patch_jump_here(j_end);
4877                        } else {
4878                            self.emit_get_scalar(idx, line, Some(root));
4879                            let j_def = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
4880                            self.compile_expr(value)?;
4881                            self.emit_set_scalar_keep(idx, line, Some(root));
4882                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4883                            self.chunk.patch_jump_here(j_def);
4884                            self.chunk.patch_jump_here(j_end);
4885                        }
4886                        return Ok(());
4887                    }
4888                    if *op == BinOp::LogOr {
4889                        // `$x ||=` — short-circuit when LHS is true.
4890                        if let Some(slot) = self.scalar_slot(name) {
4891                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4892                            let j_true = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
4893                            self.compile_expr(value)?;
4894                            self.emit_op(Op::Dup, line, Some(root));
4895                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4896                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4897                            self.chunk.patch_jump_here(j_true);
4898                            self.chunk.patch_jump_here(j_end);
4899                        } else {
4900                            self.emit_get_scalar(idx, line, Some(root));
4901                            let j_true = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
4902                            self.compile_expr(value)?;
4903                            self.emit_set_scalar_keep(idx, line, Some(root));
4904                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4905                            self.chunk.patch_jump_here(j_true);
4906                            self.chunk.patch_jump_here(j_end);
4907                        }
4908                        return Ok(());
4909                    }
4910                    if *op == BinOp::LogAnd {
4911                        // `$x &&=` — short-circuit when LHS is false.
4912                        if let Some(slot) = self.scalar_slot(name) {
4913                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4914                            let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
4915                            self.compile_expr(value)?;
4916                            self.emit_op(Op::Dup, line, Some(root));
4917                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4918                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4919                            self.chunk.patch_jump_here(j);
4920                            self.chunk.patch_jump_here(j_end);
4921                        } else {
4922                            self.emit_get_scalar(idx, line, Some(root));
4923                            let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
4924                            self.compile_expr(value)?;
4925                            self.emit_set_scalar_keep(idx, line, Some(root));
4926                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4927                            self.chunk.patch_jump_here(j);
4928                            self.chunk.patch_jump_here(j_end);
4929                        }
4930                        return Ok(());
4931                    }
4932                    if let Some(op_b) = scalar_compound_op_to_byte(*op) {
4933                        // Slot-aware path: `my $x` inside a sub body lives in a local slot.
4934                        if let Some(slot) = self.scalar_slot(name) {
4935                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4936                                CompileError::Unsupported("CompoundAssign op (slot)".into())
4937                            })?;
4938                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4939                            self.compile_expr(value)?;
4940                            self.emit_op(vm_op, line, Some(root));
4941                            self.emit_op(Op::Dup, line, Some(root));
4942                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4943                            return Ok(());
4944                        }
4945                        self.compile_expr(value)?;
4946                        self.emit_op(
4947                            Op::ScalarCompoundAssign {
4948                                name_idx: idx,
4949                                op: op_b,
4950                            },
4951                            line,
4952                            Some(root),
4953                        );
4954                    } else {
4955                        return Err(CompileError::Unsupported("CompoundAssign op".into()));
4956                    }
4957                } else if let ExprKind::ArrayElement { array, index } = &target.kind {
4958                    if self.is_mysync_array(array) {
4959                        return Err(CompileError::Unsupported(
4960                            "mysync array element update".into(),
4961                        ));
4962                    }
4963                    let q = self.qualify_stash_array_name(array);
4964                    self.check_array_mutable(&q, line)?;
4965                    let arr_idx = self.chunk.intern_name(&q);
4966                    match op {
4967                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4968                            self.compile_expr(index)?;
4969                            self.emit_op(Op::Dup, line, Some(root));
4970                            self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4971                            let j = match *op {
4972                                BinOp::DefinedOr => {
4973                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4974                                }
4975                                BinOp::LogOr => {
4976                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4977                                }
4978                                BinOp::LogAnd => {
4979                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4980                                }
4981                                _ => unreachable!(),
4982                            };
4983                            self.compile_expr(value)?;
4984                            self.emit_op(Op::Swap, line, Some(root));
4985                            self.emit_op(Op::SetArrayElemKeep(arr_idx), line, Some(root));
4986                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4987                            self.chunk.patch_jump_here(j);
4988                            self.emit_op(Op::Swap, line, Some(root));
4989                            self.emit_op(Op::Pop, line, Some(root));
4990                            self.chunk.patch_jump_here(j_end);
4991                        }
4992                        _ => {
4993                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4994                                CompileError::Unsupported("CompoundAssign op".into())
4995                            })?;
4996                            self.compile_expr(index)?;
4997                            self.emit_op(Op::Dup, line, Some(root));
4998                            self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4999                            self.compile_expr(value)?;
5000                            self.emit_op(vm_op, line, Some(root));
5001                            self.emit_op(Op::Dup, line, Some(root));
5002                            self.emit_op(Op::Rot, line, Some(root));
5003                            self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
5004                        }
5005                    }
5006                } else if let ExprKind::HashElement { hash, key } = &target.kind {
5007                    if self.is_mysync_hash(hash) {
5008                        return Err(CompileError::Unsupported(
5009                            "mysync hash element update".into(),
5010                        ));
5011                    }
5012                    self.check_hash_mutable(hash, line)?;
5013                    let hash_idx = self.chunk.intern_name(hash);
5014                    match op {
5015                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5016                            self.compile_expr(key)?;
5017                            self.emit_op(Op::Dup, line, Some(root));
5018                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
5019                            let j = match *op {
5020                                BinOp::DefinedOr => {
5021                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5022                                }
5023                                BinOp::LogOr => {
5024                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5025                                }
5026                                BinOp::LogAnd => {
5027                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5028                                }
5029                                _ => unreachable!(),
5030                            };
5031                            self.compile_expr(value)?;
5032                            self.emit_op(Op::Swap, line, Some(root));
5033                            self.emit_op(Op::SetHashElemKeep(hash_idx), line, Some(root));
5034                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5035                            self.chunk.patch_jump_here(j);
5036                            self.emit_op(Op::Swap, line, Some(root));
5037                            self.emit_op(Op::Pop, line, Some(root));
5038                            self.chunk.patch_jump_here(j_end);
5039                        }
5040                        _ => {
5041                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
5042                                CompileError::Unsupported("CompoundAssign op".into())
5043                            })?;
5044                            self.compile_expr(key)?;
5045                            self.emit_op(Op::Dup, line, Some(root));
5046                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
5047                            self.compile_expr(value)?;
5048                            self.emit_op(vm_op, line, Some(root));
5049                            self.emit_op(Op::Dup, line, Some(root));
5050                            self.emit_op(Op::Rot, line, Some(root));
5051                            self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
5052                        }
5053                    }
5054                } else if let ExprKind::Deref {
5055                    expr,
5056                    kind: Sigil::Scalar,
5057                } = &target.kind
5058                {
5059                    match op {
5060                        BinOp::DefinedOr => {
5061                            // `$$r //=` — unlike binary `//`, no `Pop` after `JumpIfDefinedKeep`
5062                            // (the ref must stay under the deref); `Swap` before set (ref on TOS).
5063                            self.compile_expr(expr)?;
5064                            self.emit_op(Op::Dup, line, Some(root));
5065                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
5066                            let j_def = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
5067                            self.compile_expr(value)?;
5068                            self.emit_op(Op::Swap, line, Some(root));
5069                            self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
5070                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5071                            self.chunk.patch_jump_here(j_def);
5072                            self.emit_op(Op::Swap, line, Some(root));
5073                            self.emit_op(Op::Pop, line, Some(root));
5074                            self.chunk.patch_jump_here(j_end);
5075                        }
5076                        BinOp::LogOr => {
5077                            // `$$r ||=` — same idea as `//=`: no `Pop` after `JumpIfTrueKeep`.
5078                            self.compile_expr(expr)?;
5079                            self.emit_op(Op::Dup, line, Some(root));
5080                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
5081                            let j_true = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
5082                            self.compile_expr(value)?;
5083                            self.emit_op(Op::Swap, line, Some(root));
5084                            self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
5085                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5086                            self.chunk.patch_jump_here(j_true);
5087                            self.emit_op(Op::Swap, line, Some(root));
5088                            self.emit_op(Op::Pop, line, Some(root));
5089                            self.chunk.patch_jump_here(j_end);
5090                        }
5091                        BinOp::LogAnd => {
5092                            // `$$r &&=` — no `Pop` after `JumpIfFalseKeep` (ref under LHS).
5093                            self.compile_expr(expr)?;
5094                            self.emit_op(Op::Dup, line, Some(root));
5095                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
5096                            let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
5097                            self.compile_expr(value)?;
5098                            self.emit_op(Op::Swap, line, Some(root));
5099                            self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
5100                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5101                            self.chunk.patch_jump_here(j);
5102                            self.emit_op(Op::Swap, line, Some(root));
5103                            self.emit_op(Op::Pop, line, Some(root));
5104                            self.chunk.patch_jump_here(j_end);
5105                        }
5106                        _ => {
5107                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
5108                                CompileError::Unsupported("CompoundAssign op".into())
5109                            })?;
5110                            self.compile_expr(expr)?;
5111                            self.emit_op(Op::Dup, line, Some(root));
5112                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
5113                            self.compile_expr(value)?;
5114                            self.emit_op(vm_op, line, Some(root));
5115                            self.emit_op(Op::Swap, line, Some(root));
5116                            self.emit_op(Op::SetSymbolicScalarRef, line, Some(root));
5117                        }
5118                    }
5119                } else if let ExprKind::ArrowDeref {
5120                    expr,
5121                    index,
5122                    kind: DerefKind::Hash,
5123                } = &target.kind
5124                {
5125                    match op {
5126                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5127                            self.compile_arrow_hash_base_expr(expr)?;
5128                            self.compile_expr(index)?;
5129                            self.emit_op(Op::Dup2, line, Some(root));
5130                            self.emit_op(Op::ArrowHash, line, Some(root));
5131                            let j = match *op {
5132                                BinOp::DefinedOr => {
5133                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5134                                }
5135                                BinOp::LogOr => {
5136                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5137                                }
5138                                BinOp::LogAnd => {
5139                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5140                                }
5141                                _ => unreachable!(),
5142                            };
5143                            self.compile_expr(value)?;
5144                            self.emit_op(Op::Swap, line, Some(root));
5145                            self.emit_op(Op::Rot, line, Some(root));
5146                            self.emit_op(Op::Swap, line, Some(root));
5147                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
5148                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5149                            self.chunk.patch_jump_here(j);
5150                            // Stack: ref, key, cur — leave `cur` as the expression value.
5151                            self.emit_op(Op::Swap, line, Some(root));
5152                            self.emit_op(Op::Pop, line, Some(root));
5153                            self.emit_op(Op::Swap, line, Some(root));
5154                            self.emit_op(Op::Pop, line, Some(root));
5155                            self.chunk.patch_jump_here(j_end);
5156                        }
5157                        _ => {
5158                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
5159                                CompileError::Unsupported("CompoundAssign op".into())
5160                            })?;
5161                            self.compile_arrow_hash_base_expr(expr)?;
5162                            self.compile_expr(index)?;
5163                            self.emit_op(Op::Dup2, line, Some(root));
5164                            self.emit_op(Op::ArrowHash, line, Some(root));
5165                            self.compile_expr(value)?;
5166                            self.emit_op(vm_op, line, Some(root));
5167                            self.emit_op(Op::Swap, line, Some(root));
5168                            self.emit_op(Op::Rot, line, Some(root));
5169                            self.emit_op(Op::Swap, line, Some(root));
5170                            // Use ...Keep so the new value remains on the stack
5171                            // as the expression's value — the statement-level
5172                            // `Pop` emitted by `StmtKind::Expression` will discard
5173                            // it. Previously emitted `SetArrowHash` (no-keep)
5174                            // left nothing, and that `Pop` then popped a slot
5175                            // from the CALLER's stack frame — silently corrupting
5176                            // multi-call expressions like `dec($n) + dec($n)`.
5177                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
5178                        }
5179                    }
5180                } else if let ExprKind::ArrowDeref {
5181                    expr,
5182                    index,
5183                    kind: DerefKind::Array,
5184                } = &target.kind
5185                {
5186                    if let ExprKind::List(indices) = &index.kind {
5187                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5188                            let k = indices.len() as u16;
5189                            self.compile_arrow_array_base_expr(expr)?;
5190                            for ix in indices {
5191                                self.compile_array_slice_index_expr(ix)?;
5192                            }
5193                            self.emit_op(Op::ArrowArraySlicePeekLast(k), line, Some(root));
5194                            let j = match *op {
5195                                BinOp::DefinedOr => {
5196                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5197                                }
5198                                BinOp::LogOr => {
5199                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5200                                }
5201                                BinOp::LogAnd => {
5202                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5203                                }
5204                                _ => unreachable!(),
5205                            };
5206                            self.compile_expr(value)?;
5207                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(k), line, Some(root));
5208                            self.emit_op(Op::SetArrowArraySliceLastKeep(k), line, Some(root));
5209                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5210                            self.chunk.patch_jump_here(j);
5211                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(k), line, Some(root));
5212                            self.chunk.patch_jump_here(j_end);
5213                            return Ok(());
5214                        }
5215                        // Multi-index `@$aref[i1,i2,...] OP= EXPR` — Perl applies the op only to the
5216                        // last index (see `Interpreter::compound_assign_arrow_array_slice`).
5217                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5218                            CompileError::Unsupported(
5219                                "CompoundAssign op on multi-index array slice".into(),
5220                            )
5221                        })?;
5222                        self.compile_expr(value)?;
5223                        self.compile_arrow_array_base_expr(expr)?;
5224                        for ix in indices {
5225                            self.compile_array_slice_index_expr(ix)?;
5226                        }
5227                        self.emit_op(
5228                            Op::ArrowArraySliceCompound(op_byte, indices.len() as u16),
5229                            line,
5230                            Some(root),
5231                        );
5232                        return Ok(());
5233                    }
5234                    match op {
5235                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5236                            // Same last-slot short-circuit semantics as `@$r[i,j] //=` but with one
5237                            // subscript slot (`..` / list / `qw` flatten to multiple indices).
5238                            self.compile_arrow_array_base_expr(expr)?;
5239                            self.compile_array_slice_index_expr(index)?;
5240                            self.emit_op(Op::ArrowArraySlicePeekLast(1), line, Some(root));
5241                            let j = match *op {
5242                                BinOp::DefinedOr => {
5243                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5244                                }
5245                                BinOp::LogOr => {
5246                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5247                                }
5248                                BinOp::LogAnd => {
5249                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5250                                }
5251                                _ => unreachable!(),
5252                            };
5253                            self.compile_expr(value)?;
5254                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(1), line, Some(root));
5255                            self.emit_op(Op::SetArrowArraySliceLastKeep(1), line, Some(root));
5256                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5257                            self.chunk.patch_jump_here(j);
5258                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(1), line, Some(root));
5259                            self.chunk.patch_jump_here(j_end);
5260                        }
5261                        _ => {
5262                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5263                                CompileError::Unsupported("CompoundAssign op".into())
5264                            })?;
5265                            self.compile_expr(value)?;
5266                            self.compile_arrow_array_base_expr(expr)?;
5267                            self.compile_array_slice_index_expr(index)?;
5268                            self.emit_op(Op::ArrowArraySliceCompound(op_byte, 1), line, Some(root));
5269                        }
5270                    }
5271                } else if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
5272                    // Single-key `@$href{"k"} OP= EXPR` matches `$href->{"k"} OP= EXPR` (ArrowHash).
5273                    // Multi-key `@$href{k1,k2} OP= EXPR` — Perl applies the op only to the last key.
5274                    if keys.is_empty() {
5275                        // Mirror `@h{} OP= EXPR`: evaluate invocant and RHS, then error (matches
5276                        // [`ExprKind::HashSlice`] empty `keys` compound path).
5277                        self.compile_expr(container)?;
5278                        self.emit_op(Op::Pop, line, Some(root));
5279                        self.compile_expr(value)?;
5280                        self.emit_op(Op::Pop, line, Some(root));
5281                        let idx = self
5282                            .chunk
5283                            .add_constant(StrykeValue::string("assign to empty hash slice".into()));
5284                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5285                        self.emit_op(Op::LoadUndef, line, Some(root));
5286                        return Ok(());
5287                    }
5288                    if hash_slice_needs_slice_ops(keys) {
5289                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5290                            let k = keys.len() as u16;
5291                            self.compile_expr(container)?;
5292                            for hk in keys {
5293                                self.compile_expr(hk)?;
5294                            }
5295                            self.emit_op(Op::HashSliceDerefPeekLast(k), line, Some(root));
5296                            let j = match *op {
5297                                BinOp::DefinedOr => {
5298                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5299                                }
5300                                BinOp::LogOr => {
5301                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5302                                }
5303                                BinOp::LogAnd => {
5304                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5305                                }
5306                                _ => unreachable!(),
5307                            };
5308                            self.compile_expr(value)?;
5309                            self.emit_op(Op::HashSliceDerefRollValUnderKeys(k), line, Some(root));
5310                            self.emit_op(Op::HashSliceDerefSetLastKeep(k), line, Some(root));
5311                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5312                            self.chunk.patch_jump_here(j);
5313                            self.emit_op(Op::HashSliceDerefDropKeysKeepCur(k), line, Some(root));
5314                            self.chunk.patch_jump_here(j_end);
5315                            return Ok(());
5316                        }
5317                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5318                            CompileError::Unsupported(
5319                                "CompoundAssign op on multi-key hash slice".into(),
5320                            )
5321                        })?;
5322                        self.compile_expr(value)?;
5323                        self.compile_expr(container)?;
5324                        for hk in keys {
5325                            self.compile_expr(hk)?;
5326                        }
5327                        self.emit_op(
5328                            Op::HashSliceDerefCompound(op_byte, keys.len() as u16),
5329                            line,
5330                            Some(root),
5331                        );
5332                        return Ok(());
5333                    }
5334                    let hk = &keys[0];
5335                    match op {
5336                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5337                            self.compile_expr(container)?;
5338                            self.compile_expr(hk)?;
5339                            self.emit_op(Op::Dup2, line, Some(root));
5340                            self.emit_op(Op::ArrowHash, line, Some(root));
5341                            let j = match *op {
5342                                BinOp::DefinedOr => {
5343                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5344                                }
5345                                BinOp::LogOr => {
5346                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5347                                }
5348                                BinOp::LogAnd => {
5349                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5350                                }
5351                                _ => unreachable!(),
5352                            };
5353                            self.compile_expr(value)?;
5354                            self.emit_op(Op::Swap, line, Some(root));
5355                            self.emit_op(Op::Rot, line, Some(root));
5356                            self.emit_op(Op::Swap, line, Some(root));
5357                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
5358                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5359                            self.chunk.patch_jump_here(j);
5360                            self.emit_op(Op::Swap, line, Some(root));
5361                            self.emit_op(Op::Pop, line, Some(root));
5362                            self.emit_op(Op::Swap, line, Some(root));
5363                            self.emit_op(Op::Pop, line, Some(root));
5364                            self.chunk.patch_jump_here(j_end);
5365                        }
5366                        _ => {
5367                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
5368                                CompileError::Unsupported("CompoundAssign op".into())
5369                            })?;
5370                            self.compile_expr(container)?;
5371                            self.compile_expr(hk)?;
5372                            self.emit_op(Op::Dup2, line, Some(root));
5373                            self.emit_op(Op::ArrowHash, line, Some(root));
5374                            self.compile_expr(value)?;
5375                            self.emit_op(vm_op, line, Some(root));
5376                            self.emit_op(Op::Swap, line, Some(root));
5377                            self.emit_op(Op::Rot, line, Some(root));
5378                            self.emit_op(Op::Swap, line, Some(root));
5379                            self.emit_op(Op::SetArrowHash, line, Some(root));
5380                        }
5381                    }
5382                } else if let ExprKind::HashSlice { hash, keys } = &target.kind {
5383                    if keys.is_empty() {
5384                        if self.is_mysync_hash(hash) {
5385                            return Err(CompileError::Unsupported(
5386                                "mysync hash slice update".into(),
5387                            ));
5388                        }
5389                        self.check_strict_hash_access(hash, line)?;
5390                        self.check_hash_mutable(hash, line)?;
5391                        self.compile_expr(value)?;
5392                        self.emit_op(Op::Pop, line, Some(root));
5393                        let idx = self
5394                            .chunk
5395                            .add_constant(StrykeValue::string("assign to empty hash slice".into()));
5396                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5397                        self.emit_op(Op::LoadUndef, line, Some(root));
5398                        return Ok(());
5399                    }
5400                    if self.is_mysync_hash(hash) {
5401                        return Err(CompileError::Unsupported("mysync hash slice update".into()));
5402                    }
5403                    self.check_strict_hash_access(hash, line)?;
5404                    self.check_hash_mutable(hash, line)?;
5405                    let hash_idx = self.chunk.intern_name(hash);
5406                    if hash_slice_needs_slice_ops(keys) {
5407                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5408                            let k = keys.len() as u16;
5409                            for hk in keys {
5410                                self.compile_expr(hk)?;
5411                            }
5412                            self.emit_op(Op::NamedHashSlicePeekLast(hash_idx, k), line, Some(root));
5413                            let j = match *op {
5414                                BinOp::DefinedOr => {
5415                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5416                                }
5417                                BinOp::LogOr => {
5418                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5419                                }
5420                                BinOp::LogAnd => {
5421                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5422                                }
5423                                _ => unreachable!(),
5424                            };
5425                            self.compile_expr(value)?;
5426                            self.emit_op(Op::NamedArraySliceRollValUnderSpecs(k), line, Some(root));
5427                            self.emit_op(
5428                                Op::SetNamedHashSliceLastKeep(hash_idx, k),
5429                                line,
5430                                Some(root),
5431                            );
5432                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5433                            self.chunk.patch_jump_here(j);
5434                            self.emit_op(Op::NamedHashSliceDropKeysKeepCur(k), line, Some(root));
5435                            self.chunk.patch_jump_here(j_end);
5436                            return Ok(());
5437                        }
5438                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5439                            CompileError::Unsupported(
5440                                "CompoundAssign op on multi-key hash slice".into(),
5441                            )
5442                        })?;
5443                        self.compile_expr(value)?;
5444                        for hk in keys {
5445                            self.compile_expr(hk)?;
5446                        }
5447                        self.emit_op(
5448                            Op::NamedHashSliceCompound(op_byte, hash_idx, keys.len() as u16),
5449                            line,
5450                            Some(root),
5451                        );
5452                        return Ok(());
5453                    }
5454                    let hk = &keys[0];
5455                    match op {
5456                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5457                            self.compile_expr(hk)?;
5458                            self.emit_op(Op::Dup, line, Some(root));
5459                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
5460                            let j = match *op {
5461                                BinOp::DefinedOr => {
5462                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5463                                }
5464                                BinOp::LogOr => {
5465                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5466                                }
5467                                BinOp::LogAnd => {
5468                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5469                                }
5470                                _ => unreachable!(),
5471                            };
5472                            self.compile_expr(value)?;
5473                            self.emit_op(Op::Swap, line, Some(root));
5474                            self.emit_op(Op::SetHashElemKeep(hash_idx), line, Some(root));
5475                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5476                            self.chunk.patch_jump_here(j);
5477                            self.emit_op(Op::Swap, line, Some(root));
5478                            self.emit_op(Op::Pop, line, Some(root));
5479                            self.chunk.patch_jump_here(j_end);
5480                        }
5481                        _ => {
5482                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5483                                CompileError::Unsupported("CompoundAssign op".into())
5484                            })?;
5485                            self.compile_expr(value)?;
5486                            self.compile_expr(hk)?;
5487                            self.emit_op(
5488                                Op::NamedHashSliceCompound(op_byte, hash_idx, 1),
5489                                line,
5490                                Some(root),
5491                            );
5492                        }
5493                    }
5494                } else if let ExprKind::ArraySlice { array, indices } = &target.kind {
5495                    if indices.is_empty() {
5496                        if self.is_mysync_array(array) {
5497                            return Err(CompileError::Unsupported(
5498                                "mysync array slice update".into(),
5499                            ));
5500                        }
5501                        let q = self.qualify_stash_array_name(array);
5502                        self.check_array_mutable(&q, line)?;
5503                        let arr_idx = self.chunk.intern_name(&q);
5504                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5505                            self.compile_expr(value)?;
5506                            self.emit_op(Op::Pop, line, Some(root));
5507                            let idx = self.chunk.add_constant(StrykeValue::string(
5508                                "assign to empty array slice".into(),
5509                            ));
5510                            self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5511                            self.emit_op(Op::LoadUndef, line, Some(root));
5512                            return Ok(());
5513                        }
5514                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5515                            CompileError::Unsupported(
5516                                "CompoundAssign op on named array slice".into(),
5517                            )
5518                        })?;
5519                        self.compile_expr(value)?;
5520                        self.emit_op(
5521                            Op::NamedArraySliceCompound(op_byte, arr_idx, 0),
5522                            line,
5523                            Some(root),
5524                        );
5525                        return Ok(());
5526                    }
5527                    if self.is_mysync_array(array) {
5528                        return Err(CompileError::Unsupported(
5529                            "mysync array slice update".into(),
5530                        ));
5531                    }
5532                    let q = self.qualify_stash_array_name(array);
5533                    self.check_array_mutable(&q, line)?;
5534                    let arr_idx = self.chunk.intern_name(&q);
5535                    if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5536                        let k = indices.len() as u16;
5537                        for ix in indices {
5538                            self.compile_array_slice_index_expr(ix)?;
5539                        }
5540                        self.emit_op(Op::NamedArraySlicePeekLast(arr_idx, k), line, Some(root));
5541                        let j = match *op {
5542                            BinOp::DefinedOr => {
5543                                self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5544                            }
5545                            BinOp::LogOr => self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root)),
5546                            BinOp::LogAnd => self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root)),
5547                            _ => unreachable!(),
5548                        };
5549                        self.compile_expr(value)?;
5550                        self.emit_op(Op::NamedArraySliceRollValUnderSpecs(k), line, Some(root));
5551                        self.emit_op(Op::SetNamedArraySliceLastKeep(arr_idx, k), line, Some(root));
5552                        let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5553                        self.chunk.patch_jump_here(j);
5554                        self.emit_op(Op::NamedArraySliceDropKeysKeepCur(k), line, Some(root));
5555                        self.chunk.patch_jump_here(j_end);
5556                        return Ok(());
5557                    }
5558                    let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5559                        CompileError::Unsupported("CompoundAssign op on named array slice".into())
5560                    })?;
5561                    self.compile_expr(value)?;
5562                    for ix in indices {
5563                        self.compile_array_slice_index_expr(ix)?;
5564                    }
5565                    self.emit_op(
5566                        Op::NamedArraySliceCompound(op_byte, arr_idx, indices.len() as u16),
5567                        line,
5568                        Some(root),
5569                    );
5570                    return Ok(());
5571                } else if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
5572                    let ExprKind::Deref {
5573                        expr: inner,
5574                        kind: Sigil::Array,
5575                    } = &source.kind
5576                    else {
5577                        return Err(CompileError::Unsupported(
5578                            "CompoundAssign on AnonymousListSlice (non-array deref)".into(),
5579                        ));
5580                    };
5581                    if indices.is_empty() {
5582                        self.compile_arrow_array_base_expr(inner)?;
5583                        self.emit_op(Op::Pop, line, Some(root));
5584                        self.compile_expr(value)?;
5585                        self.emit_op(Op::Pop, line, Some(root));
5586                        let idx = self.chunk.add_constant(StrykeValue::string(
5587                            "assign to empty array slice".into(),
5588                        ));
5589                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5590                        self.emit_op(Op::LoadUndef, line, Some(root));
5591                        return Ok(());
5592                    }
5593                    if indices.len() > 1 {
5594                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5595                            let k = indices.len() as u16;
5596                            self.compile_arrow_array_base_expr(inner)?;
5597                            for ix in indices {
5598                                self.compile_array_slice_index_expr(ix)?;
5599                            }
5600                            self.emit_op(Op::ArrowArraySlicePeekLast(k), line, Some(root));
5601                            let j = match *op {
5602                                BinOp::DefinedOr => {
5603                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5604                                }
5605                                BinOp::LogOr => {
5606                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5607                                }
5608                                BinOp::LogAnd => {
5609                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5610                                }
5611                                _ => unreachable!(),
5612                            };
5613                            self.compile_expr(value)?;
5614                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(k), line, Some(root));
5615                            self.emit_op(Op::SetArrowArraySliceLastKeep(k), line, Some(root));
5616                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5617                            self.chunk.patch_jump_here(j);
5618                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(k), line, Some(root));
5619                            self.chunk.patch_jump_here(j_end);
5620                            return Ok(());
5621                        }
5622                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5623                            CompileError::Unsupported(
5624                                "CompoundAssign op on multi-index array slice".into(),
5625                            )
5626                        })?;
5627                        self.compile_expr(value)?;
5628                        self.compile_arrow_array_base_expr(inner)?;
5629                        for ix in indices {
5630                            self.compile_array_slice_index_expr(ix)?;
5631                        }
5632                        self.emit_op(
5633                            Op::ArrowArraySliceCompound(op_byte, indices.len() as u16),
5634                            line,
5635                            Some(root),
5636                        );
5637                        return Ok(());
5638                    }
5639                    let ix0 = &indices[0];
5640                    match op {
5641                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5642                            self.compile_arrow_array_base_expr(inner)?;
5643                            self.compile_array_slice_index_expr(ix0)?;
5644                            self.emit_op(Op::ArrowArraySlicePeekLast(1), line, Some(root));
5645                            let j = match *op {
5646                                BinOp::DefinedOr => {
5647                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5648                                }
5649                                BinOp::LogOr => {
5650                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5651                                }
5652                                BinOp::LogAnd => {
5653                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5654                                }
5655                                _ => unreachable!(),
5656                            };
5657                            self.compile_expr(value)?;
5658                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(1), line, Some(root));
5659                            self.emit_op(Op::SetArrowArraySliceLastKeep(1), line, Some(root));
5660                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5661                            self.chunk.patch_jump_here(j);
5662                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(1), line, Some(root));
5663                            self.chunk.patch_jump_here(j_end);
5664                        }
5665                        _ => {
5666                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5667                                CompileError::Unsupported("CompoundAssign op".into())
5668                            })?;
5669                            self.compile_expr(value)?;
5670                            self.compile_arrow_array_base_expr(inner)?;
5671                            self.compile_array_slice_index_expr(ix0)?;
5672                            self.emit_op(Op::ArrowArraySliceCompound(op_byte, 1), line, Some(root));
5673                        }
5674                    }
5675                } else {
5676                    return Err(CompileError::Unsupported(
5677                        "CompoundAssign on non-scalar".into(),
5678                    ));
5679                }
5680            }
5681
5682            ExprKind::Ternary {
5683                condition,
5684                then_expr,
5685                else_expr,
5686            } => {
5687                self.compile_boolean_rvalue_condition(condition)?;
5688                let jump_else = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
5689                self.compile_expr_ctx(then_expr, ctx)?;
5690                let jump_end = self.emit_op(Op::Jump(0), line, Some(root));
5691                self.chunk.patch_jump_here(jump_else);
5692                self.compile_expr_ctx(else_expr, ctx)?;
5693                self.chunk.patch_jump_here(jump_end);
5694            }
5695
5696            ExprKind::Range {
5697                from,
5698                to,
5699                exclusive,
5700                step,
5701            } => {
5702                if ctx == WantarrayCtx::List {
5703                    self.compile_expr_ctx(from, WantarrayCtx::Scalar)?;
5704                    self.compile_expr_ctx(to, WantarrayCtx::Scalar)?;
5705                    if let Some(s) = step {
5706                        self.compile_expr_ctx(s, WantarrayCtx::Scalar)?;
5707                        self.emit_op(Op::RangeStep, line, Some(root));
5708                    } else {
5709                        self.emit_op(Op::Range, line, Some(root));
5710                    }
5711                } else if let (ExprKind::Regex(lp, lf), ExprKind::Regex(rp, rf)) =
5712                    (&from.kind, &to.kind)
5713                {
5714                    let slot = self.chunk.alloc_flip_flop_slot();
5715                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5716                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5717                    let rp_idx = self.chunk.add_constant(StrykeValue::string(rp.clone()));
5718                    let rf_idx = self.chunk.add_constant(StrykeValue::string(rf.clone()));
5719                    self.emit_op(
5720                        Op::RegexFlipFlop(
5721                            slot,
5722                            u8::from(*exclusive),
5723                            lp_idx,
5724                            lf_idx,
5725                            rp_idx,
5726                            rf_idx,
5727                        ),
5728                        line,
5729                        Some(root),
5730                    );
5731                } else if let (ExprKind::Regex(lp, lf), ExprKind::Eof(None)) =
5732                    (&from.kind, &to.kind)
5733                {
5734                    let slot = self.chunk.alloc_flip_flop_slot();
5735                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5736                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5737                    self.emit_op(
5738                        Op::RegexEofFlipFlop(slot, u8::from(*exclusive), lp_idx, lf_idx),
5739                        line,
5740                        Some(root),
5741                    );
5742                } else if matches!(
5743                    (&from.kind, &to.kind),
5744                    (ExprKind::Regex(_, _), ExprKind::Eof(Some(_)))
5745                ) {
5746                    return Err(CompileError::Unsupported(
5747                        "regex flip-flop with eof(HANDLE) is not supported".into(),
5748                    ));
5749                } else if let ExprKind::Regex(lp, lf) = &from.kind {
5750                    let slot = self.chunk.alloc_flip_flop_slot();
5751                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5752                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5753                    if matches!(to.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
5754                        let line_target = match &to.kind {
5755                            ExprKind::Integer(n) => *n,
5756                            ExprKind::Float(f) => *f as i64,
5757                            _ => unreachable!(),
5758                        };
5759                        let line_cidx = self.chunk.add_constant(StrykeValue::integer(line_target));
5760                        self.emit_op(
5761                            Op::RegexFlipFlopDotLineRhs(
5762                                slot,
5763                                u8::from(*exclusive),
5764                                lp_idx,
5765                                lf_idx,
5766                                line_cidx,
5767                            ),
5768                            line,
5769                            Some(root),
5770                        );
5771                    } else {
5772                        let rhs_idx = self
5773                            .chunk
5774                            .add_regex_flip_flop_rhs_expr_entry((**to).clone());
5775                        self.emit_op(
5776                            Op::RegexFlipFlopExprRhs(
5777                                slot,
5778                                u8::from(*exclusive),
5779                                lp_idx,
5780                                lf_idx,
5781                                rhs_idx,
5782                            ),
5783                            line,
5784                            Some(root),
5785                        );
5786                    }
5787                } else {
5788                    self.compile_expr(from)?;
5789                    self.compile_expr(to)?;
5790                    let slot = self.chunk.alloc_flip_flop_slot();
5791                    self.emit_op(
5792                        Op::ScalarFlipFlop(slot, u8::from(*exclusive)),
5793                        line,
5794                        Some(root),
5795                    );
5796                }
5797            }
5798
5799            ExprKind::SliceRange { .. } => {
5800                // Open-ended slice ranges (`:N`, `N:`, `::-1`, `::`) only have meaning
5801                // inside slice subscripts (`@arr[...]`, `@h{...}`), where they are
5802                // intercepted by the slice arms above. Anywhere else is a hard error —
5803                // we have no container length context to resolve open ends.
5804                return Err(CompileError::Unsupported(
5805                    "open-ended slice range (`:N`/`N:`/`::-1`) is only valid inside `@arr[...]` or `@h{...}` subscripts"
5806                        .into(),
5807                ));
5808            }
5809
5810            ExprKind::Repeat {
5811                expr,
5812                count,
5813                list_repeat,
5814            } => {
5815                if *list_repeat {
5816                    // List context for the LHS so `(EXPR)` and `qw(...)` flatten
5817                    // into the array we'll replicate.
5818                    self.compile_expr_ctx(expr, WantarrayCtx::List)?;
5819                    self.compile_expr(count)?;
5820                    self.emit_op(Op::ListRepeat, line, Some(root));
5821                } else {
5822                    self.compile_expr(expr)?;
5823                    self.compile_expr(count)?;
5824                    self.emit_op(Op::StringRepeat, line, Some(root));
5825                }
5826            }
5827
5828            // ── Function calls ──
5829            ExprKind::FuncCall { name, args } => {
5830                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
5831                // bare-name fast path so the arms below stay flat.
5832                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
5833                match dispatch_name {
5834                    // read(FH, $buf, LEN) — emit ReadIntoVar with the buffer variable's name index
5835                    "read" => {
5836                        if args.len() < 3 {
5837                            return Err(CompileError::Unsupported(
5838                                "read() needs at least 3 args".into(),
5839                            ));
5840                        }
5841                        // Extract buffer variable name from 2nd arg
5842                        let buf_name =
5843                            match &args[1].kind {
5844                                ExprKind::ScalarVar(n) => n.clone(),
5845                                _ => return Err(CompileError::Unsupported(
5846                                    "read() buffer must be a simple scalar variable for bytecode"
5847                                        .into(),
5848                                )),
5849                            };
5850                        let buf_idx = self.chunk.intern_name(&buf_name);
5851                        // Stack: [filehandle, length]
5852                        self.compile_expr(&args[0])?; // filehandle
5853                        self.compile_expr(&args[2])?; // length
5854                        self.emit_op(Op::ReadIntoVar(buf_idx), line, Some(root));
5855                    }
5856                    // `defer { BLOCK }` — desugared by parser to `defer__internal(fn { BLOCK })`
5857                    "defer__internal" => {
5858                        if args.len() != 1 {
5859                            return Err(CompileError::Unsupported(
5860                                "defer__internal expects exactly one argument".into(),
5861                            ));
5862                        }
5863                        // Compile the coderef argument; afterwards, un-mark
5864                        // the defer block as a sub-body so the closure-write
5865                        // check (DESIGN-001) doesn't flag mutations of outer
5866                        // `my` vars from inside `defer { ... }`. defer runs
5867                        // synchronously at scope exit and is intentionally
5868                        // shared-state with the enclosing scope.
5869                        self.compile_expr(&args[0])?;
5870                        if let ExprKind::CodeRef { .. } = &args[0].kind {
5871                            // The most-recently-pushed CodeRef block index is
5872                            // the highest one in `sub_body_block_indices`.
5873                            if let Some(max_idx) = self.sub_body_block_indices.iter().copied().max()
5874                            {
5875                                self.sub_body_block_indices.remove(&max_idx);
5876                            }
5877                        }
5878                        self.emit_op(Op::DeferBlock, line, Some(root));
5879                    }
5880                    "deque" => {
5881                        if !args.is_empty() {
5882                            return Err(CompileError::Unsupported(
5883                                "deque() takes no arguments".into(),
5884                            ));
5885                        }
5886                        self.emit_op(
5887                            Op::CallBuiltin(BuiltinId::DequeNew as u16, 0),
5888                            line,
5889                            Some(root),
5890                        );
5891                    }
5892                    "inc" => {
5893                        let arg = args.first().cloned().unwrap_or_else(|| Expr {
5894                            kind: ExprKind::ScalarVar("_".into()),
5895                            line,
5896                        });
5897                        self.compile_expr(&arg)?;
5898                        self.emit_op(Op::Inc, line, Some(root));
5899                    }
5900                    "dec" => {
5901                        let arg = args.first().cloned().unwrap_or_else(|| Expr {
5902                            kind: ExprKind::ScalarVar("_".into()),
5903                            line,
5904                        });
5905                        self.compile_expr(&arg)?;
5906                        self.emit_op(Op::Dec, line, Some(root));
5907                    }
5908                    "heap" => {
5909                        if args.len() != 1 {
5910                            return Err(CompileError::Unsupported(
5911                                "heap() expects one comparator sub".into(),
5912                            ));
5913                        }
5914                        self.compile_expr(&args[0])?;
5915                        self.emit_op(
5916                            Op::CallBuiltin(BuiltinId::HeapNew as u16, 1),
5917                            line,
5918                            Some(root),
5919                        );
5920                    }
5921                    "pipeline" => {
5922                        for arg in args {
5923                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5924                        }
5925                        self.emit_op(
5926                            Op::CallBuiltin(BuiltinId::Pipeline as u16, args.len() as u8),
5927                            line,
5928                            Some(root),
5929                        );
5930                    }
5931                    "par_pipeline" => {
5932                        for arg in args {
5933                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5934                        }
5935                        self.emit_op(
5936                            Op::CallBuiltin(BuiltinId::ParPipeline as u16, args.len() as u8),
5937                            line,
5938                            Some(root),
5939                        );
5940                    }
5941                    "par_pipeline_stream" => {
5942                        for arg in args {
5943                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5944                        }
5945                        self.emit_op(
5946                            Op::CallBuiltin(BuiltinId::ParPipelineStream as u16, args.len() as u8),
5947                            line,
5948                            Some(root),
5949                        );
5950                    }
5951                    // `collect(EXPR)` — compile the argument in list context so nested
5952                    // `map { }` / `grep { }` keep a pipeline handle (scalar context adds
5953                    // `StackArrayLen`, which turns a pipeline into `1`). At runtime, a
5954                    // pipeline runs staged ops; any other value is materialized as an array
5955                    // (`|> … |> collect()`).
5956                    "collect" => {
5957                        for arg in args {
5958                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5959                        }
5960                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5961                        self.emit_op(
5962                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5963                            line,
5964                            Some(root),
5965                        );
5966                    }
5967                    "ppool" => {
5968                        if args.len() != 1 {
5969                            return Err(CompileError::Unsupported(
5970                                "ppool() expects one argument (worker count)".into(),
5971                            ));
5972                        }
5973                        self.compile_expr(&args[0])?;
5974                        self.emit_op(
5975                            Op::CallBuiltin(BuiltinId::Ppool as u16, 1),
5976                            line,
5977                            Some(root),
5978                        );
5979                    }
5980                    "barrier" => {
5981                        if args.len() != 1 {
5982                            return Err(CompileError::Unsupported(
5983                                "barrier() expects one argument (party count)".into(),
5984                            ));
5985                        }
5986                        self.compile_expr(&args[0])?;
5987                        self.emit_op(
5988                            Op::CallBuiltin(BuiltinId::BarrierNew as u16, 1),
5989                            line,
5990                            Some(root),
5991                        );
5992                    }
5993                    "cluster" => {
5994                        // Each arg pushed in list context so an `@hosts`
5995                        // operand flattens into individual slot specs and
5996                        // a single bareword `cluster("host1:4")` arrives
5997                        // as one string. `ClusterNew` mirrors the
5998                        // tree-walker arm in `vm_helper::call_named_sub`.
5999                        if args.is_empty() {
6000                            return Err(CompileError::Unsupported(
6001                                "cluster() expects at least one host/slot specifier".into(),
6002                            ));
6003                        }
6004                        for arg in args {
6005                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6006                        }
6007                        self.emit_op(
6008                            Op::CallBuiltin(BuiltinId::ClusterNew as u16, args.len() as u8),
6009                            line,
6010                            Some(root),
6011                        );
6012                    }
6013                    "pselect" => {
6014                        if args.is_empty() {
6015                            return Err(CompileError::Unsupported(
6016                                "pselect() expects at least one pchannel receiver".into(),
6017                            ));
6018                        }
6019                        for arg in args {
6020                            self.compile_expr(arg)?;
6021                        }
6022                        self.emit_op(
6023                            Op::CallBuiltin(BuiltinId::Pselect as u16, args.len() as u8),
6024                            line,
6025                            Some(root),
6026                        );
6027                    }
6028                    "ssh" => {
6029                        for arg in args {
6030                            self.compile_expr(arg)?;
6031                        }
6032                        self.emit_op(
6033                            Op::CallBuiltin(BuiltinId::Ssh as u16, args.len() as u8),
6034                            line,
6035                            Some(root),
6036                        );
6037                    }
6038                    "rmdir" => {
6039                        for arg in args {
6040                            self.compile_expr(arg)?;
6041                        }
6042                        self.emit_op(
6043                            Op::CallBuiltin(BuiltinId::Rmdir as u16, args.len() as u8),
6044                            line,
6045                            Some(root),
6046                        );
6047                    }
6048                    "utime" => {
6049                        for arg in args {
6050                            self.compile_expr(arg)?;
6051                        }
6052                        self.emit_op(
6053                            Op::CallBuiltin(BuiltinId::Utime as u16, args.len() as u8),
6054                            line,
6055                            Some(root),
6056                        );
6057                    }
6058                    "umask" => {
6059                        for arg in args {
6060                            self.compile_expr(arg)?;
6061                        }
6062                        self.emit_op(
6063                            Op::CallBuiltin(BuiltinId::Umask as u16, args.len() as u8),
6064                            line,
6065                            Some(root),
6066                        );
6067                    }
6068                    "getcwd" => {
6069                        for arg in args {
6070                            self.compile_expr(arg)?;
6071                        }
6072                        self.emit_op(
6073                            Op::CallBuiltin(BuiltinId::Getcwd as u16, args.len() as u8),
6074                            line,
6075                            Some(root),
6076                        );
6077                    }
6078                    "pipe" => {
6079                        if args.len() != 2 {
6080                            return Err(CompileError::Unsupported(
6081                                "pipe requires exactly two arguments".into(),
6082                            ));
6083                        }
6084                        for arg in args {
6085                            self.compile_expr(arg)?;
6086                        }
6087                        self.emit_op(Op::CallBuiltin(BuiltinId::Pipe as u16, 2), line, Some(root));
6088                    }
6089                    "uniq" | "distinct" | "flatten" | "set" | "with_index" | "list_count"
6090                    | "list_size" | "count" | "size" | "cnt" | "len" | "sum" | "sum0"
6091                    | "product" | "min" | "max" | "mean" | "median" | "mode" | "stddev"
6092                    | "variance" => {
6093                        // Fast path for `len @arr` / `cnt @arr` / `count @arr` and the deref
6094                        // variants `len @$ref` / `len @{$ref}`: emit the same direct length op
6095                        // (`ArrayLen` / `ArrayDerefLen`) that `scalar @arr` uses, so the
6096                        // idiomatic stryke spelling is no slower than the Perl-compat one.
6097                        if matches!(
6098                            name.as_str(),
6099                            "count" | "cnt" | "size" | "len" | "list_count" | "list_size"
6100                        ) && args.len() == 1
6101                        {
6102                            match &args[0].kind {
6103                                ExprKind::ArrayVar(arr_name) => {
6104                                    self.check_strict_array_access(arr_name, line)?;
6105                                    let idx = self
6106                                        .chunk
6107                                        .intern_name(&self.qualify_stash_array_name(arr_name));
6108                                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
6109                                    return Ok(());
6110                                }
6111                                ExprKind::Deref {
6112                                    expr,
6113                                    kind: Sigil::Array,
6114                                } => {
6115                                    self.compile_expr(expr)?;
6116                                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
6117                                    return Ok(());
6118                                }
6119                                _ => {}
6120                            }
6121                        }
6122                        for arg in args {
6123                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6124                        }
6125                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
6126                        self.emit_op(
6127                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6128                            line,
6129                            Some(root),
6130                        );
6131                    }
6132                    "shuffle" => {
6133                        for arg in args {
6134                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6135                        }
6136                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
6137                        self.emit_op(
6138                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6139                            line,
6140                            Some(root),
6141                        );
6142                    }
6143                    "chunked" | "windowed" => {
6144                        match args.len() {
6145                            0 => {
6146                                return Err(CompileError::Unsupported(
6147                                "chunked/windowed need (LIST, N) or unary N (e.g. `|> chunked(2)`)"
6148                                    .into(),
6149                            ));
6150                            }
6151                            1 => {
6152                                // chunked @l / windowed @l — compile in list context, default size
6153                                self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
6154                            }
6155                            2 => {
6156                                self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
6157                                self.compile_expr(&args[1])?;
6158                            }
6159                            _ => {
6160                                return Err(CompileError::Unsupported(
6161                                "chunked/windowed expect exactly two arguments (LIST, N); use a single list expression for the first operand".into(),
6162                            ));
6163                            }
6164                        }
6165                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
6166                        self.emit_op(
6167                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6168                            line,
6169                            Some(root),
6170                        );
6171                    }
6172                    "take" | "head" | "tail" | "drop" => {
6173                        if args.is_empty() {
6174                            return Err(CompileError::Unsupported(
6175                                "take/head/tail/drop expect LIST..., N or unary N".into(),
6176                            ));
6177                        }
6178                        if args.len() == 1 {
6179                            // head @l == head @l, 1 — evaluate in list context
6180                            self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
6181                        } else {
6182                            for a in &args[..args.len() - 1] {
6183                                self.compile_expr_ctx(a, WantarrayCtx::List)?;
6184                            }
6185                            self.compile_expr(&args[args.len() - 1])?;
6186                        }
6187                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
6188                        self.emit_op(
6189                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6190                            line,
6191                            Some(root),
6192                        );
6193                    }
6194                    "any" | "all" | "none" | "first" | "take_while" | "drop_while" | "tap"
6195                    | "peek" => {
6196                        // Three shapes:
6197                        //   `any { BLOCK } @list`           — block form
6198                        //   `any(fn { ... }, 1, 2, 3)`      — slurpy `(&@)` form
6199                        //   `any($coderef, @list)`          — coderef-in-block-position
6200                        //   `any($f, @list)` (no parens)    — same, runtime dispatch
6201                        // Builtin runtime checks `as_code_ref()` and dispatches; if
6202                        // the first arg isn't a coderef the builtin uses its
6203                        // value-shape semantics. Compiler stays out of the way.
6204                        if args.is_empty() {
6205                            return Err(CompileError::Unsupported(
6206                            "any/all/none/first/take_while/drop_while/tap/peek expect BLOCK, LIST"
6207                                .into(),
6208                        ));
6209                        }
6210                        self.compile_expr(&args[0])?;
6211                        for arg in &args[1..] {
6212                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6213                        }
6214                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
6215                        self.emit_op(
6216                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6217                            line,
6218                            Some(root),
6219                        );
6220                    }
6221                    "group_by" | "chunk_by" => {
6222                        if args.len() != 2 {
6223                            return Err(CompileError::Unsupported(
6224                                "group_by/chunk_by expect { BLOCK } or EXPR, LIST".into(),
6225                            ));
6226                        }
6227                        self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
6228                        match &args[0].kind {
6229                            ExprKind::CodeRef { body, .. } => {
6230                                let block_idx = self.add_deferred_block(body.clone());
6231                                self.emit_op(Op::ChunkByWithBlock(block_idx), line, Some(root));
6232                            }
6233                            _ => {
6234                                let idx = self.chunk.add_map_expr_entry(args[0].clone());
6235                                self.emit_op(Op::ChunkByWithExpr(idx), line, Some(root));
6236                            }
6237                        }
6238                        if ctx != WantarrayCtx::List {
6239                            self.emit_op(Op::StackArrayLen, line, Some(root));
6240                        }
6241                    }
6242                    "zip" | "zip_longest" => {
6243                        for arg in args {
6244                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6245                        }
6246                        // Both forms are stryke bare-name builtins; the VM slow path strips the
6247                        // `main::` qualifier and routes through `try_builtin` → `dispatch_by_name`.
6248                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(dispatch_name));
6249                        self.emit_op(
6250                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6251                            line,
6252                            Some(root),
6253                        );
6254                    }
6255                    "puniq" => {
6256                        if args.is_empty() || args.len() > 2 {
6257                            return Err(CompileError::Unsupported(
6258                                "puniq expects LIST [, progress => EXPR]".into(),
6259                            ));
6260                        }
6261                        if args.len() == 2 {
6262                            self.compile_expr(&args[1])?;
6263                        } else {
6264                            self.emit_op(Op::LoadInt(0), line, Some(root));
6265                        }
6266                        self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
6267                        self.emit_op(Op::Puniq, line, Some(root));
6268                        if ctx != WantarrayCtx::List {
6269                            self.emit_op(Op::StackArrayLen, line, Some(root));
6270                        }
6271                    }
6272                    "pfirst" | "pany" => {
6273                        if args.len() < 2 || args.len() > 3 {
6274                            return Err(CompileError::Unsupported(
6275                                "pfirst/pany expect BLOCK, LIST [, progress => EXPR]".into(),
6276                            ));
6277                        }
6278                        let body = match &args[0].kind {
6279                            ExprKind::CodeRef { body, .. } => body,
6280                            _ => {
6281                                return Err(CompileError::Unsupported(
6282                                    "pfirst/pany: first argument must be a { BLOCK }".into(),
6283                                ));
6284                            }
6285                        };
6286                        if args.len() == 3 {
6287                            self.compile_expr(&args[2])?;
6288                        } else {
6289                            self.emit_op(Op::LoadInt(0), line, Some(root));
6290                        }
6291                        self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
6292                        let block_idx = self.add_deferred_block(body.clone());
6293                        let op = if name == "pfirst" {
6294                            Op::PFirstWithBlock(block_idx)
6295                        } else {
6296                            Op::PAnyWithBlock(block_idx)
6297                        };
6298                        self.emit_op(op, line, Some(root));
6299                    }
6300                    // Math builtins lowered to fusevm Op::*Float for JIT disk-cache
6301                    // coverage. Each is a unary float→float fn; the bytecode shape
6302                    // (CallBuiltin(N, 1)) is recognized by `is_float_unary_builtin`
6303                    // and translated by `float_builtin_fusevm_op` in fusevm_bridge.
6304                    "tan" if args.len() == 1 => {
6305                        self.compile_expr(&args[0])?;
6306                        self.emit_op(Op::CallBuiltin(BuiltinId::Tan as u16, 1), line, Some(root));
6307                    }
6308                    "asin" if args.len() == 1 => {
6309                        self.compile_expr(&args[0])?;
6310                        self.emit_op(Op::CallBuiltin(BuiltinId::Asin as u16, 1), line, Some(root));
6311                    }
6312                    "acos" if args.len() == 1 => {
6313                        self.compile_expr(&args[0])?;
6314                        self.emit_op(Op::CallBuiltin(BuiltinId::Acos as u16, 1), line, Some(root));
6315                    }
6316                    "atan" if args.len() == 1 => {
6317                        self.compile_expr(&args[0])?;
6318                        self.emit_op(Op::CallBuiltin(BuiltinId::Atan as u16, 1), line, Some(root));
6319                    }
6320                    "sinh" if args.len() == 1 => {
6321                        self.compile_expr(&args[0])?;
6322                        self.emit_op(Op::CallBuiltin(BuiltinId::Sinh as u16, 1), line, Some(root));
6323                    }
6324                    "cosh" if args.len() == 1 => {
6325                        self.compile_expr(&args[0])?;
6326                        self.emit_op(Op::CallBuiltin(BuiltinId::Cosh as u16, 1), line, Some(root));
6327                    }
6328                    "tanh" if args.len() == 1 => {
6329                        self.compile_expr(&args[0])?;
6330                        self.emit_op(Op::CallBuiltin(BuiltinId::Tanh as u16, 1), line, Some(root));
6331                    }
6332                    "log2" if args.len() == 1 => {
6333                        self.compile_expr(&args[0])?;
6334                        self.emit_op(Op::CallBuiltin(BuiltinId::Log2 as u16, 1), line, Some(root));
6335                    }
6336                    "log10" if args.len() == 1 => {
6337                        self.compile_expr(&args[0])?;
6338                        self.emit_op(Op::CallBuiltin(BuiltinId::Log10 as u16, 1), line, Some(root));
6339                    }
6340                    "ceil" | "ceiling" if args.len() == 1 => {
6341                        self.compile_expr(&args[0])?;
6342                        self.emit_op(Op::CallBuiltin(BuiltinId::Ceil as u16, 1), line, Some(root));
6343                    }
6344                    "floor" if args.len() == 1 => {
6345                        self.compile_expr(&args[0])?;
6346                        self.emit_op(Op::CallBuiltin(BuiltinId::Floor as u16, 1), line, Some(root));
6347                    }
6348                    // round($x) (1-arg, no precision) — lowers to STK_VAL_ROUND
6349                    // via BuiltinId::Round. round($x, $n) (2-arg) keeps the
6350                    // generic name-dispatch path (returns float, different shape).
6351                    "round" if args.len() == 1 => {
6352                        self.compile_expr(&args[0])?;
6353                        self.emit_op(Op::CallBuiltin(BuiltinId::Round as u16, 1), line, Some(root));
6354                    }
6355                    _ => {
6356                        // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
6357                        // `f(reverse LIST)` etc. flatten into `@_`. [`Self::pop_call_operands_flattened`]
6358                        // splats any array value at runtime, matching Perl's `@_` semantics.
6359                        for arg in args {
6360                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6361                        }
6362                        let q = self.qualify_sub_key(name);
6363                        let name_idx = self.chunk.intern_name(&q);
6364                        self.emit_op(
6365                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
6366                            line,
6367                            Some(root),
6368                        );
6369                    }
6370                }
6371            }
6372
6373            // ── Method calls ──
6374            ExprKind::MethodCall {
6375                object,
6376                method,
6377                args,
6378                super_call,
6379            } => {
6380                self.compile_expr(object)?;
6381                for arg in args {
6382                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6383                }
6384                let name_idx = self.chunk.intern_name(method);
6385                if *super_call {
6386                    self.emit_op(
6387                        Op::MethodCallSuper(name_idx, args.len() as u8, ctx.as_byte()),
6388                        line,
6389                        Some(root),
6390                    );
6391                } else {
6392                    self.emit_op(
6393                        Op::MethodCall(name_idx, args.len() as u8, ctx.as_byte()),
6394                        line,
6395                        Some(root),
6396                    );
6397                }
6398            }
6399            ExprKind::IndirectCall {
6400                target,
6401                args,
6402                ampersand: _,
6403                pass_caller_arglist,
6404            } => {
6405                self.compile_expr(target)?;
6406                // When any arg is a bare `@arr` / `%hash`, route through
6407                // ArrowCall so the array / hash flattens into call args
6408                // the same way `$f->(@xs)` does. The plain IndirectCall
6409                // op pops `args.len()` items, which collapses `@xs` to a
6410                // single scalar — `Fn::apply($f, 5)` calling `$f(@xs)`
6411                // reached `$f` with one arg (the array value) instead of
6412                // flattened elements. For all-scalar args we keep the
6413                // tighter IndirectCall path so `~> LHS head(3)` thread-
6414                // first stages and `>{ … }` IIFEs keep their existing
6415                // arg shapes (those depend on `argc` being the static
6416                // arg count, not a flattened total).
6417                let has_aggregate_arg = !pass_caller_arglist
6418                    && args
6419                        .iter()
6420                        .any(|a| matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)));
6421                if *pass_caller_arglist {
6422                    self.emit_op(Op::IndirectCall(0, ctx.as_byte(), 1), line, Some(root));
6423                } else if has_aggregate_arg {
6424                    let list_expr = Expr {
6425                        kind: ExprKind::List(args.clone()),
6426                        line,
6427                    };
6428                    self.compile_expr_ctx(&list_expr, WantarrayCtx::List)?;
6429                    self.emit_op(Op::ArrowCall(ctx.as_byte()), line, Some(root));
6430                } else {
6431                    for a in args {
6432                        self.compile_expr_ctx(a, WantarrayCtx::List)?;
6433                    }
6434                    self.emit_op(
6435                        Op::IndirectCall(args.len() as u8, ctx.as_byte(), 0),
6436                        line,
6437                        Some(root),
6438                    );
6439                }
6440            }
6441
6442            // ── Print / Say / Printf ──
6443            ExprKind::Print { handle, args } => {
6444                for arg in args {
6445                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6446                }
6447                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
6448                self.emit_op(Op::Print(h, args.len() as u8), line, Some(root));
6449            }
6450            ExprKind::Say { handle, args } => {
6451                for arg in args {
6452                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6453                }
6454                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
6455                self.emit_op(Op::Say(h, args.len() as u8), line, Some(root));
6456            }
6457            ExprKind::Printf { handle, args } => {
6458                // printf's format + arg list is Perl list context — ranges, arrays, and
6459                // `reverse`/`sort`/`grep` flatten into format argument positions.
6460                for arg in args {
6461                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6462                }
6463                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
6464                self.emit_op(Op::Printf(h, args.len() as u8), line, Some(root));
6465            }
6466
6467            // ── Die / Warn ──
6468            ExprKind::Die(args) => {
6469                // die / warn take a list that gets stringified and concatenated — list context
6470                // so `die 1..5` matches Perl's "12345" stringification.
6471                for arg in args {
6472                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6473                }
6474                self.emit_op(
6475                    Op::CallBuiltin(BuiltinId::Die as u16, args.len() as u8),
6476                    line,
6477                    Some(root),
6478                );
6479            }
6480            ExprKind::Warn(args) => {
6481                for arg in args {
6482                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6483                }
6484                self.emit_op(
6485                    Op::CallBuiltin(BuiltinId::Warn as u16, args.len() as u8),
6486                    line,
6487                    Some(root),
6488                );
6489            }
6490            ExprKind::Exit(code) => {
6491                if let Some(c) = code {
6492                    self.compile_expr(c)?;
6493                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
6494                } else {
6495                    self.emit_op(Op::LoadInt(0), line, Some(root));
6496                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
6497                }
6498            }
6499
6500            // ── Array ops ──
6501            ExprKind::Push { array, values } => {
6502                if let ExprKind::ArrayVar(name) = &array.kind {
6503                    let stash = self.array_storage_name_for_ops(name);
6504                    let idx = self
6505                        .chunk
6506                        .intern_name(&self.qualify_stash_array_name(&stash));
6507                    for v in values {
6508                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
6509                        self.emit_op(Op::PushArray(idx), line, Some(root));
6510                    }
6511                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
6512                } else if let ExprKind::Deref {
6513                    expr: aref_expr,
6514                    kind: Sigil::Array,
6515                } = &array.kind
6516                {
6517                    // Autovivifiable inner shapes (`$x`, `$h{k}`, `$a[i]`) need lvalue
6518                    // resolution: when the slot is undef, `push @{...}` must create a new
6519                    // arrayref and store it back. Routed through PushExpr where
6520                    // `try_eval_array_deref_container` handles autoviv.
6521                    let needs_autoviv = matches!(
6522                        &aref_expr.kind,
6523                        ExprKind::ScalarVar(_)
6524                            | ExprKind::HashElement { .. }
6525                            | ExprKind::ArrayElement { .. }
6526                    );
6527                    if needs_autoviv {
6528                        let pool = self
6529                            .chunk
6530                            .add_push_expr_entry(array.as_ref().clone(), values.clone());
6531                        self.emit_op(Op::PushExpr(pool), line, Some(root));
6532                    } else {
6533                        self.compile_expr(aref_expr)?;
6534                        for v in values {
6535                            self.emit_op(Op::Dup, line, Some(root));
6536                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
6537                            self.emit_op(Op::PushArrayDeref, line, Some(root));
6538                        }
6539                        self.emit_op(Op::ArrayDerefLen, line, Some(root));
6540                    }
6541                } else {
6542                    let pool = self
6543                        .chunk
6544                        .add_push_expr_entry(array.as_ref().clone(), values.clone());
6545                    self.emit_op(Op::PushExpr(pool), line, Some(root));
6546                }
6547            }
6548            ExprKind::Pop(array) => {
6549                if let ExprKind::ArrayVar(name) = &array.kind {
6550                    let stash = self.array_storage_name_for_ops(name);
6551                    let idx = self
6552                        .chunk
6553                        .intern_name(&self.qualify_stash_array_name(&stash));
6554                    self.emit_op(Op::PopArray(idx), line, Some(root));
6555                } else if let ExprKind::Deref {
6556                    expr: aref_expr,
6557                    kind: Sigil::Array,
6558                } = &array.kind
6559                {
6560                    let needs_autoviv = matches!(
6561                        &aref_expr.kind,
6562                        ExprKind::ScalarVar(_)
6563                            | ExprKind::HashElement { .. }
6564                            | ExprKind::ArrayElement { .. }
6565                    );
6566                    if needs_autoviv {
6567                        let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
6568                        self.emit_op(Op::PopExpr(pool), line, Some(root));
6569                    } else {
6570                        self.compile_expr(aref_expr)?;
6571                        self.emit_op(Op::PopArrayDeref, line, Some(root));
6572                    }
6573                } else {
6574                    let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
6575                    self.emit_op(Op::PopExpr(pool), line, Some(root));
6576                }
6577            }
6578            ExprKind::Shift(array) => {
6579                if let ExprKind::ArrayVar(name) = &array.kind {
6580                    let stash = self.array_storage_name_for_ops(name);
6581                    let idx = self
6582                        .chunk
6583                        .intern_name(&self.qualify_stash_array_name(&stash));
6584                    self.emit_op(Op::ShiftArray(idx), line, Some(root));
6585                } else if let ExprKind::Deref {
6586                    expr: aref_expr,
6587                    kind: Sigil::Array,
6588                } = &array.kind
6589                {
6590                    let needs_autoviv = matches!(
6591                        &aref_expr.kind,
6592                        ExprKind::ScalarVar(_)
6593                            | ExprKind::HashElement { .. }
6594                            | ExprKind::ArrayElement { .. }
6595                    );
6596                    if needs_autoviv {
6597                        let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
6598                        self.emit_op(Op::ShiftExpr(pool), line, Some(root));
6599                    } else {
6600                        self.compile_expr(aref_expr)?;
6601                        self.emit_op(Op::ShiftArrayDeref, line, Some(root));
6602                    }
6603                } else {
6604                    let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
6605                    self.emit_op(Op::ShiftExpr(pool), line, Some(root));
6606                }
6607            }
6608            ExprKind::Unshift { array, values } => {
6609                if let ExprKind::ArrayVar(name) = &array.kind {
6610                    let stash = self.array_storage_name_for_ops(name);
6611                    let q = self.qualify_stash_array_name(&stash);
6612                    let name_const = self.chunk.add_constant(StrykeValue::string(q));
6613                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
6614                    for v in values {
6615                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
6616                    }
6617                    let nargs = (1 + values.len()) as u8;
6618                    self.emit_op(
6619                        Op::CallBuiltin(BuiltinId::Unshift as u16, nargs),
6620                        line,
6621                        Some(root),
6622                    );
6623                } else if let ExprKind::Deref {
6624                    expr: aref_expr,
6625                    kind: Sigil::Array,
6626                } = &array.kind
6627                {
6628                    let needs_autoviv = matches!(
6629                        &aref_expr.kind,
6630                        ExprKind::ScalarVar(_)
6631                            | ExprKind::HashElement { .. }
6632                            | ExprKind::ArrayElement { .. }
6633                    );
6634                    if needs_autoviv || values.len() > u8::MAX as usize {
6635                        let pool = self
6636                            .chunk
6637                            .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
6638                        self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
6639                    } else {
6640                        self.compile_expr(aref_expr)?;
6641                        for v in values {
6642                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
6643                        }
6644                        self.emit_op(Op::UnshiftArrayDeref(values.len() as u8), line, Some(root));
6645                    }
6646                } else {
6647                    let pool = self
6648                        .chunk
6649                        .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
6650                    self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
6651                }
6652            }
6653            ExprKind::Splice {
6654                array,
6655                offset,
6656                length,
6657                replacement,
6658            } => {
6659                self.emit_op(Op::WantarrayPush(ctx.as_byte()), line, Some(root));
6660                if let ExprKind::ArrayVar(name) = &array.kind {
6661                    let stash = self.array_storage_name_for_ops(name);
6662                    let q = self.qualify_stash_array_name(&stash);
6663                    let name_const = self.chunk.add_constant(StrykeValue::string(q));
6664                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
6665                    if let Some(o) = offset {
6666                        self.compile_expr(o)?;
6667                    } else {
6668                        self.emit_op(Op::LoadInt(0), line, Some(root));
6669                    }
6670                    if let Some(l) = length {
6671                        self.compile_expr(l)?;
6672                    } else {
6673                        self.emit_op(Op::LoadUndef, line, Some(root));
6674                    }
6675                    // Replacement LIST is Perl list context — arrays splat
6676                    // their elements instead of being scalarized to a count.
6677                    for r in replacement {
6678                        self.compile_expr_ctx(r, WantarrayCtx::List)?;
6679                    }
6680                    let nargs = (3 + replacement.len()) as u8;
6681                    self.emit_op(
6682                        Op::CallBuiltin(BuiltinId::Splice as u16, nargs),
6683                        line,
6684                        Some(root),
6685                    );
6686                } else if let ExprKind::Deref {
6687                    expr: aref_expr,
6688                    kind: Sigil::Array,
6689                } = &array.kind
6690                {
6691                    if replacement.len() > u8::MAX as usize {
6692                        let pool = self.chunk.add_splice_expr_entry(
6693                            array.as_ref().clone(),
6694                            offset.as_deref().cloned(),
6695                            length.as_deref().cloned(),
6696                            replacement.clone(),
6697                        );
6698                        self.emit_op(Op::SpliceExpr(pool), line, Some(root));
6699                    } else {
6700                        self.compile_expr(aref_expr)?;
6701                        if let Some(o) = offset {
6702                            self.compile_expr(o)?;
6703                        } else {
6704                            self.emit_op(Op::LoadInt(0), line, Some(root));
6705                        }
6706                        if let Some(l) = length {
6707                            self.compile_expr(l)?;
6708                        } else {
6709                            self.emit_op(Op::LoadUndef, line, Some(root));
6710                        }
6711                        for r in replacement {
6712                            self.compile_expr(r)?;
6713                        }
6714                        self.emit_op(
6715                            Op::SpliceArrayDeref(replacement.len() as u8),
6716                            line,
6717                            Some(root),
6718                        );
6719                    }
6720                } else {
6721                    let pool = self.chunk.add_splice_expr_entry(
6722                        array.as_ref().clone(),
6723                        offset.as_deref().cloned(),
6724                        length.as_deref().cloned(),
6725                        replacement.clone(),
6726                    );
6727                    self.emit_op(Op::SpliceExpr(pool), line, Some(root));
6728                }
6729                self.emit_op(Op::WantarrayPop, line, Some(root));
6730            }
6731            ExprKind::ScalarContext(inner) => {
6732                // `scalar EXPR` forces scalar context on EXPR regardless of the outer context
6733                // (e.g. `print scalar grep { } @x` — grep's result is a count, not a list).
6734                self.compile_expr_ctx(inner, WantarrayCtx::Scalar)?;
6735                // Then apply aggregate scalar semantics (set size, pipeline source len, …) —
6736                // same as [`Op::ValueScalarContext`] / [`StrykeValue::scalar_context`].
6737                self.emit_op(Op::ValueScalarContext, line, Some(root));
6738            }
6739
6740            // ── Hash ops ──
6741            ExprKind::Delete(inner) => {
6742                if let ExprKind::HashElement { hash, key } = &inner.kind {
6743                    self.check_hash_mutable(hash, line)?;
6744                    let idx = self.chunk.intern_name(hash);
6745                    self.compile_expr(key)?;
6746                    self.emit_op(Op::DeleteHashElem(idx), line, Some(root));
6747                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
6748                    self.check_strict_array_access(array, line)?;
6749                    let q = self.qualify_stash_array_name(array);
6750                    self.check_array_mutable(&q, line)?;
6751                    let arr_idx = self.chunk.intern_name(&q);
6752                    self.compile_expr(index)?;
6753                    self.emit_op(Op::DeleteArrayElem(arr_idx), line, Some(root));
6754                } else if let ExprKind::ArrowDeref {
6755                    expr: container,
6756                    index,
6757                    kind: DerefKind::Hash,
6758                } = &inner.kind
6759                {
6760                    self.compile_arrow_hash_base_expr(container)?;
6761                    self.compile_expr(index)?;
6762                    self.emit_op(Op::DeleteArrowHashElem, line, Some(root));
6763                } else if let ExprKind::ArrowDeref {
6764                    expr: container,
6765                    index,
6766                    kind: DerefKind::Array,
6767                } = &inner.kind
6768                {
6769                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
6770                        self.compile_expr(container)?;
6771                        self.compile_expr(index)?;
6772                        self.emit_op(Op::DeleteArrowArrayElem, line, Some(root));
6773                    } else {
6774                        let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
6775                        self.emit_op(Op::DeleteExpr(pool), line, Some(root));
6776                    }
6777                } else {
6778                    let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
6779                    self.emit_op(Op::DeleteExpr(pool), line, Some(root));
6780                }
6781            }
6782            ExprKind::Exists(inner) => {
6783                if let ExprKind::HashElement { hash, key } = &inner.kind {
6784                    let idx = self.chunk.intern_name(hash);
6785                    self.compile_expr(key)?;
6786                    self.emit_op(Op::ExistsHashElem(idx), line, Some(root));
6787                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
6788                    self.check_strict_array_access(array, line)?;
6789                    let arr_idx = self
6790                        .chunk
6791                        .intern_name(&self.qualify_stash_array_name(array));
6792                    self.compile_expr(index)?;
6793                    self.emit_op(Op::ExistsArrayElem(arr_idx), line, Some(root));
6794                } else if let ExprKind::ArrowDeref {
6795                    expr: container,
6796                    index,
6797                    kind: DerefKind::Hash,
6798                } = &inner.kind
6799                {
6800                    // Multi-level chains (e.g. `exists $h{x}{y}{z}`) need
6801                    // undef-tolerant intermediate eval — route through
6802                    // `ExistsExpr` (which calls `eval_exists_operand` and
6803                    // soft-fails on undef intermediates). (BUG-009)
6804                    if matches!(container.kind, ExprKind::ArrowDeref { .. }) {
6805                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6806                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6807                    } else {
6808                        self.compile_arrow_hash_base_expr(container)?;
6809                        self.compile_expr(index)?;
6810                        self.emit_op(Op::ExistsArrowHashElem, line, Some(root));
6811                    }
6812                } else if let ExprKind::ArrowDeref {
6813                    expr: container,
6814                    index,
6815                    kind: DerefKind::Array,
6816                } = &inner.kind
6817                {
6818                    if !arrow_deref_arrow_subscript_is_plain_scalar_index(index)
6819                        || matches!(container.kind, ExprKind::ArrowDeref { .. })
6820                    {
6821                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6822                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6823                    } else {
6824                        self.compile_expr(container)?;
6825                        self.compile_expr(index)?;
6826                        self.emit_op(Op::ExistsArrowArrayElem, line, Some(root));
6827                    }
6828                } else {
6829                    let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6830                    self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6831                }
6832            }
6833            ExprKind::Keys(inner) => {
6834                if let ExprKind::HashVar(name) = &inner.kind {
6835                    let stash = self.hash_storage_name_for_ops(name);
6836                    let idx = self.chunk.intern_name(&stash);
6837                    if ctx == WantarrayCtx::List {
6838                        self.emit_op(Op::HashKeys(idx), line, Some(root));
6839                    } else {
6840                        self.emit_op(Op::HashKeysScalar(idx), line, Some(root));
6841                    }
6842                } else {
6843                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
6844                    if ctx == WantarrayCtx::List {
6845                        self.emit_op(Op::KeysFromValue, line, Some(root));
6846                    } else {
6847                        self.emit_op(Op::KeysFromValueScalar, line, Some(root));
6848                    }
6849                }
6850            }
6851            ExprKind::Values(inner) => {
6852                if let ExprKind::HashVar(name) = &inner.kind {
6853                    let stash = self.hash_storage_name_for_ops(name);
6854                    let idx = self.chunk.intern_name(&stash);
6855                    if ctx == WantarrayCtx::List {
6856                        self.emit_op(Op::HashValues(idx), line, Some(root));
6857                    } else {
6858                        self.emit_op(Op::HashValuesScalar(idx), line, Some(root));
6859                    }
6860                } else {
6861                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
6862                    if ctx == WantarrayCtx::List {
6863                        self.emit_op(Op::ValuesFromValue, line, Some(root));
6864                    } else {
6865                        self.emit_op(Op::ValuesFromValueScalar, line, Some(root));
6866                    }
6867                }
6868            }
6869            ExprKind::Each(e) => {
6870                self.compile_expr(e)?;
6871                self.emit_op(Op::CallBuiltin(BuiltinId::Each as u16, 1), line, Some(root));
6872            }
6873
6874            // ── Builtins that map to CallBuiltin ──
6875            ExprKind::Length(e) => {
6876                self.compile_expr(e)?;
6877                self.emit_op(
6878                    Op::CallBuiltin(BuiltinId::Length as u16, 1),
6879                    line,
6880                    Some(root),
6881                );
6882            }
6883            ExprKind::Chomp(e) => {
6884                self.compile_expr(e)?;
6885                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
6886                self.emit_op(Op::ChompInPlace(lv), line, Some(root));
6887            }
6888            ExprKind::Chop(e) => {
6889                self.compile_expr(e)?;
6890                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
6891                self.emit_op(Op::ChopInPlace(lv), line, Some(root));
6892            }
6893            ExprKind::Defined(e) => {
6894                self.compile_expr(e)?;
6895                self.emit_op(
6896                    Op::CallBuiltin(BuiltinId::Defined as u16, 1),
6897                    line,
6898                    Some(root),
6899                );
6900            }
6901            ExprKind::Abs(e) => {
6902                self.compile_expr(e)?;
6903                self.emit_op(Op::CallBuiltin(BuiltinId::Abs as u16, 1), line, Some(root));
6904            }
6905            ExprKind::Int(e) => {
6906                self.compile_expr(e)?;
6907                self.emit_op(Op::CallBuiltin(BuiltinId::Int as u16, 1), line, Some(root));
6908            }
6909            ExprKind::Sqrt(e) => {
6910                self.compile_expr(e)?;
6911                self.emit_op(Op::CallBuiltin(BuiltinId::Sqrt as u16, 1), line, Some(root));
6912            }
6913            ExprKind::Sin(e) => {
6914                self.compile_expr(e)?;
6915                self.emit_op(Op::CallBuiltin(BuiltinId::Sin as u16, 1), line, Some(root));
6916            }
6917            ExprKind::Cos(e) => {
6918                self.compile_expr(e)?;
6919                self.emit_op(Op::CallBuiltin(BuiltinId::Cos as u16, 1), line, Some(root));
6920            }
6921            ExprKind::Atan2 { y, x } => {
6922                self.compile_expr(y)?;
6923                self.compile_expr(x)?;
6924                self.emit_op(
6925                    Op::CallBuiltin(BuiltinId::Atan2 as u16, 2),
6926                    line,
6927                    Some(root),
6928                );
6929            }
6930            ExprKind::Exp(e) => {
6931                self.compile_expr(e)?;
6932                self.emit_op(Op::CallBuiltin(BuiltinId::Exp as u16, 1), line, Some(root));
6933            }
6934            ExprKind::Log(e) => {
6935                self.compile_expr(e)?;
6936                self.emit_op(Op::CallBuiltin(BuiltinId::Log as u16, 1), line, Some(root));
6937            }
6938            ExprKind::Rand(upper) => {
6939                if let Some(e) = upper {
6940                    self.compile_expr(e)?;
6941                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 1), line, Some(root));
6942                } else {
6943                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 0), line, Some(root));
6944                }
6945            }
6946            ExprKind::Srand(seed) => {
6947                if let Some(e) = seed {
6948                    self.compile_expr(e)?;
6949                    self.emit_op(
6950                        Op::CallBuiltin(BuiltinId::Srand as u16, 1),
6951                        line,
6952                        Some(root),
6953                    );
6954                } else {
6955                    self.emit_op(
6956                        Op::CallBuiltin(BuiltinId::Srand as u16, 0),
6957                        line,
6958                        Some(root),
6959                    );
6960                }
6961            }
6962            ExprKind::Chr(e) => {
6963                self.compile_expr(e)?;
6964                self.emit_op(Op::CallBuiltin(BuiltinId::Chr as u16, 1), line, Some(root));
6965            }
6966            ExprKind::Ord(e) => {
6967                self.compile_expr(e)?;
6968                self.emit_op(Op::CallBuiltin(BuiltinId::Ord as u16, 1), line, Some(root));
6969            }
6970            ExprKind::Hex(e) => {
6971                self.compile_expr(e)?;
6972                self.emit_op(Op::CallBuiltin(BuiltinId::Hex as u16, 1), line, Some(root));
6973            }
6974            ExprKind::Oct(e) => {
6975                self.compile_expr(e)?;
6976                self.emit_op(Op::CallBuiltin(BuiltinId::Oct as u16, 1), line, Some(root));
6977            }
6978            ExprKind::Uc(e) => {
6979                self.compile_expr(e)?;
6980                self.emit_op(Op::CallBuiltin(BuiltinId::Uc as u16, 1), line, Some(root));
6981            }
6982            ExprKind::Lc(e) => {
6983                self.compile_expr(e)?;
6984                self.emit_op(Op::CallBuiltin(BuiltinId::Lc as u16, 1), line, Some(root));
6985            }
6986            ExprKind::Ucfirst(e) => {
6987                self.compile_expr(e)?;
6988                self.emit_op(
6989                    Op::CallBuiltin(BuiltinId::Ucfirst as u16, 1),
6990                    line,
6991                    Some(root),
6992                );
6993            }
6994            ExprKind::Lcfirst(e) => {
6995                self.compile_expr(e)?;
6996                self.emit_op(
6997                    Op::CallBuiltin(BuiltinId::Lcfirst as u16, 1),
6998                    line,
6999                    Some(root),
7000                );
7001            }
7002            ExprKind::Fc(e) => {
7003                self.compile_expr(e)?;
7004                self.emit_op(Op::CallBuiltin(BuiltinId::Fc as u16, 1), line, Some(root));
7005            }
7006            ExprKind::Quotemeta(e) => {
7007                self.compile_expr(e)?;
7008                self.emit_op(
7009                    Op::CallBuiltin(BuiltinId::Quotemeta as u16, 1),
7010                    line,
7011                    Some(root),
7012                );
7013            }
7014            ExprKind::Crypt { plaintext, salt } => {
7015                self.compile_expr(plaintext)?;
7016                self.compile_expr(salt)?;
7017                self.emit_op(
7018                    Op::CallBuiltin(BuiltinId::Crypt as u16, 2),
7019                    line,
7020                    Some(root),
7021                );
7022            }
7023            ExprKind::Pos(e) => match e {
7024                None => {
7025                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 0), line, Some(root));
7026                }
7027                Some(pos_arg) => {
7028                    if let ExprKind::ScalarVar(name) = &pos_arg.kind {
7029                        let stor = self.scalar_storage_name_for_ops(name);
7030                        let idx = self.chunk.add_constant(StrykeValue::string(stor));
7031                        self.emit_op(Op::LoadConst(idx), line, Some(root));
7032                    } else {
7033                        self.compile_expr(pos_arg)?;
7034                    }
7035                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 1), line, Some(root));
7036                }
7037            },
7038            ExprKind::Study(e) => {
7039                self.compile_expr(e)?;
7040                self.emit_op(
7041                    Op::CallBuiltin(BuiltinId::Study as u16, 1),
7042                    line,
7043                    Some(root),
7044                );
7045            }
7046            ExprKind::Ref(e) => {
7047                self.compile_expr(e)?;
7048                self.emit_op(Op::CallBuiltin(BuiltinId::Ref as u16, 1), line, Some(root));
7049            }
7050            ExprKind::Rev(e) => {
7051                // Compile in list context to get arrays/hashes as collections
7052                self.compile_expr_ctx(e, WantarrayCtx::List)?;
7053                if ctx == WantarrayCtx::List {
7054                    self.emit_op(Op::RevListOp, line, Some(root));
7055                } else {
7056                    self.emit_op(Op::RevScalarOp, line, Some(root));
7057                }
7058            }
7059            ExprKind::ReverseExpr(e) => {
7060                self.compile_expr_ctx(e, WantarrayCtx::List)?;
7061                if ctx == WantarrayCtx::List {
7062                    self.emit_op(Op::ReverseListOp, line, Some(root));
7063                } else {
7064                    self.emit_op(Op::ReverseScalarOp, line, Some(root));
7065                }
7066            }
7067            ExprKind::System(args) => {
7068                for a in args {
7069                    self.compile_expr(a)?;
7070                }
7071                self.emit_op(
7072                    Op::CallBuiltin(BuiltinId::System as u16, args.len() as u8),
7073                    line,
7074                    Some(root),
7075                );
7076            }
7077            ExprKind::Exec(args) => {
7078                for a in args {
7079                    self.compile_expr(a)?;
7080                }
7081                self.emit_op(
7082                    Op::CallBuiltin(BuiltinId::Exec as u16, args.len() as u8),
7083                    line,
7084                    Some(root),
7085                );
7086            }
7087
7088            // ── String builtins ──
7089            ExprKind::Substr {
7090                string,
7091                offset,
7092                length,
7093                replacement,
7094            } => {
7095                if let Some(rep) = replacement {
7096                    let idx = self.chunk.add_substr_four_arg_entry(
7097                        string.as_ref().clone(),
7098                        offset.as_ref().clone(),
7099                        length.as_ref().map(|b| b.as_ref().clone()),
7100                        rep.as_ref().clone(),
7101                    );
7102                    self.emit_op(Op::SubstrFourArg(idx), line, Some(root));
7103                } else {
7104                    self.compile_expr(string)?;
7105                    self.compile_expr(offset)?;
7106                    let mut argc: u8 = 2;
7107                    if let Some(len) = length {
7108                        self.compile_expr(len)?;
7109                        argc = 3;
7110                    }
7111                    self.emit_op(
7112                        Op::CallBuiltin(BuiltinId::Substr as u16, argc),
7113                        line,
7114                        Some(root),
7115                    );
7116                }
7117            }
7118            ExprKind::Index {
7119                string,
7120                substr,
7121                position,
7122            } => {
7123                self.compile_expr(string)?;
7124                self.compile_expr(substr)?;
7125                if let Some(pos) = position {
7126                    self.compile_expr(pos)?;
7127                    self.emit_op(
7128                        Op::CallBuiltin(BuiltinId::Index as u16, 3),
7129                        line,
7130                        Some(root),
7131                    );
7132                } else {
7133                    self.emit_op(
7134                        Op::CallBuiltin(BuiltinId::Index as u16, 2),
7135                        line,
7136                        Some(root),
7137                    );
7138                }
7139            }
7140            ExprKind::Rindex {
7141                string,
7142                substr,
7143                position,
7144            } => {
7145                self.compile_expr(string)?;
7146                self.compile_expr(substr)?;
7147                if let Some(pos) = position {
7148                    self.compile_expr(pos)?;
7149                    self.emit_op(
7150                        Op::CallBuiltin(BuiltinId::Rindex as u16, 3),
7151                        line,
7152                        Some(root),
7153                    );
7154                } else {
7155                    self.emit_op(
7156                        Op::CallBuiltin(BuiltinId::Rindex as u16, 2),
7157                        line,
7158                        Some(root),
7159                    );
7160                }
7161            }
7162
7163            ExprKind::JoinExpr { separator, list } => {
7164                self.compile_expr(separator)?;
7165                // Arguments after the separator are evaluated in list context (Perl 5).
7166                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7167                self.emit_op(Op::CallBuiltin(BuiltinId::Join as u16, 2), line, Some(root));
7168            }
7169            ExprKind::SplitExpr {
7170                pattern,
7171                string,
7172                limit,
7173            } => {
7174                self.compile_expr(pattern)?;
7175                self.compile_expr(string)?;
7176                if let Some(l) = limit {
7177                    self.compile_expr(l)?;
7178                    self.emit_op(
7179                        Op::CallBuiltin(BuiltinId::Split as u16, 3),
7180                        line,
7181                        Some(root),
7182                    );
7183                } else {
7184                    self.emit_op(
7185                        Op::CallBuiltin(BuiltinId::Split as u16, 2),
7186                        line,
7187                        Some(root),
7188                    );
7189                }
7190            }
7191            ExprKind::Sprintf { format, args } => {
7192                // sprintf's arg list after the format is Perl list context — ranges, arrays,
7193                // and `reverse`/`sort`/`grep` flatten into the format argument positions.
7194                self.compile_expr(format)?;
7195                for a in args {
7196                    self.compile_expr_ctx(a, WantarrayCtx::List)?;
7197                }
7198                self.emit_op(
7199                    Op::CallBuiltin(BuiltinId::Sprintf as u16, (1 + args.len()) as u8),
7200                    line,
7201                    Some(root),
7202                );
7203            }
7204
7205            // ── I/O ──
7206            ExprKind::Open { handle, mode, file } => {
7207                if let ExprKind::OpenMyHandle { name } = &handle.kind {
7208                    // Same fix as Opendir below: $fh must hold the handle
7209                    // NAME STRING so subsequent close($fh)/print $fh,…
7210                    // resolve to the runtime's open-handle table. Pre-fix
7211                    // SetScalarKeepPlain clobbered $fh with open's bool
7212                    // return — handle was orphaned in the runtime table.
7213                    let name_idx = self.chunk.intern_name(name);
7214                    let h_idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
7215                    // $fh = "name"
7216                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
7217                    self.emit_declare_scalar(name_idx, line, false);
7218                    // open("name", mode, [file]) → bool, remains on stack
7219                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
7220                    self.compile_expr(mode)?;
7221                    if let Some(f) = file {
7222                        self.compile_expr(f)?;
7223                        self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
7224                    } else {
7225                        self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
7226                    }
7227                    return Ok(());
7228                }
7229                self.compile_expr(handle)?;
7230                self.compile_expr(mode)?;
7231                if let Some(f) = file {
7232                    self.compile_expr(f)?;
7233                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
7234                } else {
7235                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
7236                }
7237            }
7238            ExprKind::OpenMyHandle { .. } => {
7239                return Err(CompileError::Unsupported(
7240                    "open my $fh handle expression".into(),
7241                ));
7242            }
7243            ExprKind::Close(e) => {
7244                self.compile_expr(e)?;
7245                self.emit_op(
7246                    Op::CallBuiltin(BuiltinId::Close as u16, 1),
7247                    line,
7248                    Some(root),
7249                );
7250            }
7251            ExprKind::ReadLine(handle) => {
7252                let bid = if ctx == WantarrayCtx::List {
7253                    BuiltinId::ReadLineList
7254                } else {
7255                    BuiltinId::ReadLine
7256                };
7257                if let Some(h) = handle {
7258                    let idx = self.chunk.add_constant(StrykeValue::string(h.clone()));
7259                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7260                    self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
7261                } else {
7262                    self.emit_op(Op::CallBuiltin(bid as u16, 0), line, Some(root));
7263                }
7264            }
7265            ExprKind::Eof(e) => {
7266                if let Some(inner) = e {
7267                    self.compile_expr(inner)?;
7268                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 1), line, Some(root));
7269                } else {
7270                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 0), line, Some(root));
7271                }
7272            }
7273            ExprKind::Opendir { handle, path } => {
7274                if let ExprKind::OpendirMyHandle { name } = &handle.kind {
7275                    // `opendir(my|var|val $dh, $path)` semantics: declare $dh,
7276                    // initialize it to the literal handle-name string, then
7277                    // call the opendir builtin with that same string as its
7278                    // first arg. The runtime keys its open-dir table by that
7279                    // string, so subsequent `readdir($dh)` / `closedir($dh)`
7280                    // (which to_string $dh) resolve to the right handle.
7281                    //
7282                    // Pre-fix this emitted `SetScalarKeepPlain` AFTER the
7283                    // CallBuiltin — which clobbered $dh with opendir's bool
7284                    // return value, so the dir handle was orphaned in the
7285                    // runtime table and readdir($dh)/readdir("1") found
7286                    // nothing. The bool return must REMAIN as the expression
7287                    // value so `opendir(...) or die` still works.
7288                    let name_idx = self.chunk.intern_name(name);
7289                    let h_idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
7290                    // $dh = "name"
7291                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
7292                    self.emit_declare_scalar(name_idx, line, false);
7293                    // opendir("name", path) → bool, remains on stack
7294                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
7295                    self.compile_expr(path)?;
7296                    self.emit_op(
7297                        Op::CallBuiltin(BuiltinId::Opendir as u16, 2),
7298                        line,
7299                        Some(root),
7300                    );
7301                    return Ok(());
7302                }
7303                self.compile_expr(handle)?;
7304                self.compile_expr(path)?;
7305                self.emit_op(
7306                    Op::CallBuiltin(BuiltinId::Opendir as u16, 2),
7307                    line,
7308                    Some(root),
7309                );
7310            }
7311            ExprKind::OpendirMyHandle { .. } => {
7312                return Err(CompileError::Unsupported(
7313                    "opendir my $dh handle expression".into(),
7314                ));
7315            }
7316            ExprKind::Readdir(e) => {
7317                let bid = if ctx == WantarrayCtx::List {
7318                    BuiltinId::ReaddirList
7319                } else {
7320                    BuiltinId::Readdir
7321                };
7322                self.compile_expr(e)?;
7323                self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
7324            }
7325            ExprKind::Closedir(e) => {
7326                self.compile_expr(e)?;
7327                self.emit_op(
7328                    Op::CallBuiltin(BuiltinId::Closedir as u16, 1),
7329                    line,
7330                    Some(root),
7331                );
7332            }
7333            ExprKind::Rewinddir(e) => {
7334                self.compile_expr(e)?;
7335                self.emit_op(
7336                    Op::CallBuiltin(BuiltinId::Rewinddir as u16, 1),
7337                    line,
7338                    Some(root),
7339                );
7340            }
7341            ExprKind::Telldir(e) => {
7342                self.compile_expr(e)?;
7343                self.emit_op(
7344                    Op::CallBuiltin(BuiltinId::Telldir as u16, 1),
7345                    line,
7346                    Some(root),
7347                );
7348            }
7349            ExprKind::Seekdir { handle, position } => {
7350                self.compile_expr(handle)?;
7351                self.compile_expr(position)?;
7352                self.emit_op(
7353                    Op::CallBuiltin(BuiltinId::Seekdir as u16, 2),
7354                    line,
7355                    Some(root),
7356                );
7357            }
7358
7359            // ── File tests ──
7360            ExprKind::FileTest { op, expr } => {
7361                self.compile_expr(expr)?;
7362                self.emit_op(Op::FileTestOp(*op as u8), line, Some(root));
7363            }
7364
7365            // ── Eval / Do / Require ──
7366            ExprKind::Eval(e) => {
7367                self.compile_expr(e)?;
7368                // `eval { BLOCK }` runs synchronously and is intentionally
7369                // shared-state with the enclosing scope (it's used for error
7370                // catching, not stored as a closure value). Un-mark the
7371                // CodeRef's block so the closure-write check (DESIGN-001)
7372                // doesn't fire on writes to outer-scope `my` from inside
7373                // the eval body.
7374                if let ExprKind::CodeRef { .. } = &e.kind {
7375                    if let Some(max_idx) = self.sub_body_block_indices.iter().copied().max() {
7376                        self.sub_body_block_indices.remove(&max_idx);
7377                    }
7378                }
7379                self.emit_op(Op::CallBuiltin(BuiltinId::Eval as u16, 1), line, Some(root));
7380            }
7381            ExprKind::Do(e) => {
7382                // do { BLOCK } executes the block; do "file" loads a file
7383                if let ExprKind::CodeRef { body, .. } = &e.kind {
7384                    let block_idx = self.add_deferred_block(body.clone());
7385                    self.emit_op(Op::EvalBlock(block_idx, ctx.as_byte()), line, Some(root));
7386                } else {
7387                    self.compile_expr(e)?;
7388                    self.emit_op(Op::CallBuiltin(BuiltinId::Do as u16, 1), line, Some(root));
7389                }
7390            }
7391            ExprKind::Require(e) => {
7392                self.compile_expr(e)?;
7393                self.emit_op(
7394                    Op::CallBuiltin(BuiltinId::Require as u16, 1),
7395                    line,
7396                    Some(root),
7397                );
7398            }
7399
7400            // ── Filesystem ──
7401            ExprKind::Chdir(e) => {
7402                self.compile_expr(e)?;
7403                self.emit_op(
7404                    Op::CallBuiltin(BuiltinId::Chdir as u16, 1),
7405                    line,
7406                    Some(root),
7407                );
7408            }
7409            ExprKind::Mkdir { path, mode } => {
7410                self.compile_expr(path)?;
7411                if let Some(m) = mode {
7412                    self.compile_expr(m)?;
7413                    self.emit_op(
7414                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 2),
7415                        line,
7416                        Some(root),
7417                    );
7418                } else {
7419                    self.emit_op(
7420                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 1),
7421                        line,
7422                        Some(root),
7423                    );
7424                }
7425            }
7426            ExprKind::Unlink(args) => {
7427                for a in args {
7428                    self.compile_expr(a)?;
7429                }
7430                self.emit_op(
7431                    Op::CallBuiltin(BuiltinId::Unlink as u16, args.len() as u8),
7432                    line,
7433                    Some(root),
7434                );
7435            }
7436            ExprKind::Rename { old, new } => {
7437                self.compile_expr(old)?;
7438                self.compile_expr(new)?;
7439                self.emit_op(
7440                    Op::CallBuiltin(BuiltinId::Rename as u16, 2),
7441                    line,
7442                    Some(root),
7443                );
7444            }
7445            ExprKind::Chmod(args) => {
7446                for a in args {
7447                    self.compile_expr(a)?;
7448                }
7449                self.emit_op(
7450                    Op::CallBuiltin(BuiltinId::Chmod as u16, args.len() as u8),
7451                    line,
7452                    Some(root),
7453                );
7454            }
7455            ExprKind::Chown(args) => {
7456                for a in args {
7457                    self.compile_expr(a)?;
7458                }
7459                self.emit_op(
7460                    Op::CallBuiltin(BuiltinId::Chown as u16, args.len() as u8),
7461                    line,
7462                    Some(root),
7463                );
7464            }
7465            ExprKind::Stat(e) => {
7466                self.compile_expr(e)?;
7467                self.emit_op(Op::CallBuiltin(BuiltinId::Stat as u16, 1), line, Some(root));
7468            }
7469            ExprKind::Lstat(e) => {
7470                self.compile_expr(e)?;
7471                self.emit_op(
7472                    Op::CallBuiltin(BuiltinId::Lstat as u16, 1),
7473                    line,
7474                    Some(root),
7475                );
7476            }
7477            ExprKind::Link { old, new } => {
7478                self.compile_expr(old)?;
7479                self.compile_expr(new)?;
7480                self.emit_op(Op::CallBuiltin(BuiltinId::Link as u16, 2), line, Some(root));
7481            }
7482            ExprKind::Symlink { old, new } => {
7483                self.compile_expr(old)?;
7484                self.compile_expr(new)?;
7485                self.emit_op(
7486                    Op::CallBuiltin(BuiltinId::Symlink as u16, 2),
7487                    line,
7488                    Some(root),
7489                );
7490            }
7491            ExprKind::Readlink(e) => {
7492                self.compile_expr(e)?;
7493                self.emit_op(
7494                    Op::CallBuiltin(BuiltinId::Readlink as u16, 1),
7495                    line,
7496                    Some(root),
7497                );
7498            }
7499            ExprKind::Files(args) => {
7500                for a in args {
7501                    self.compile_expr(a)?;
7502                }
7503                self.emit_op(
7504                    Op::CallBuiltin(BuiltinId::Files as u16, args.len() as u8),
7505                    line,
7506                    Some(root),
7507                );
7508            }
7509            ExprKind::Filesf(args) => {
7510                for a in args {
7511                    self.compile_expr(a)?;
7512                }
7513                self.emit_op(
7514                    Op::CallBuiltin(BuiltinId::Filesf as u16, args.len() as u8),
7515                    line,
7516                    Some(root),
7517                );
7518            }
7519            ExprKind::FilesfRecursive(args) => {
7520                for a in args {
7521                    self.compile_expr(a)?;
7522                }
7523                self.emit_op(
7524                    Op::CallBuiltin(BuiltinId::FilesfRecursive as u16, args.len() as u8),
7525                    line,
7526                    Some(root),
7527                );
7528            }
7529            ExprKind::Dirs(args) => {
7530                for a in args {
7531                    self.compile_expr(a)?;
7532                }
7533                self.emit_op(
7534                    Op::CallBuiltin(BuiltinId::Dirs as u16, args.len() as u8),
7535                    line,
7536                    Some(root),
7537                );
7538            }
7539            ExprKind::DirsRecursive(args) => {
7540                for a in args {
7541                    self.compile_expr(a)?;
7542                }
7543                self.emit_op(
7544                    Op::CallBuiltin(BuiltinId::DirsRecursive as u16, args.len() as u8),
7545                    line,
7546                    Some(root),
7547                );
7548            }
7549            ExprKind::SymLinks(args) => {
7550                for a in args {
7551                    self.compile_expr(a)?;
7552                }
7553                self.emit_op(
7554                    Op::CallBuiltin(BuiltinId::SymLinks as u16, args.len() as u8),
7555                    line,
7556                    Some(root),
7557                );
7558            }
7559            ExprKind::Sockets(args) => {
7560                for a in args {
7561                    self.compile_expr(a)?;
7562                }
7563                self.emit_op(
7564                    Op::CallBuiltin(BuiltinId::Sockets as u16, args.len() as u8),
7565                    line,
7566                    Some(root),
7567                );
7568            }
7569            ExprKind::Pipes(args) => {
7570                for a in args {
7571                    self.compile_expr(a)?;
7572                }
7573                self.emit_op(
7574                    Op::CallBuiltin(BuiltinId::Pipes as u16, args.len() as u8),
7575                    line,
7576                    Some(root),
7577                );
7578            }
7579            ExprKind::BlockDevices(args) => {
7580                for a in args {
7581                    self.compile_expr(a)?;
7582                }
7583                self.emit_op(
7584                    Op::CallBuiltin(BuiltinId::BlockDevices as u16, args.len() as u8),
7585                    line,
7586                    Some(root),
7587                );
7588            }
7589            ExprKind::CharDevices(args) => {
7590                for a in args {
7591                    self.compile_expr(a)?;
7592                }
7593                self.emit_op(
7594                    Op::CallBuiltin(BuiltinId::CharDevices as u16, args.len() as u8),
7595                    line,
7596                    Some(root),
7597                );
7598            }
7599            ExprKind::Executables(args) => {
7600                for a in args {
7601                    self.compile_expr(a)?;
7602                }
7603                self.emit_op(
7604                    Op::CallBuiltin(BuiltinId::Executables as u16, args.len() as u8),
7605                    line,
7606                    Some(root),
7607                );
7608            }
7609            ExprKind::Glob(args) => {
7610                for a in args {
7611                    self.compile_expr(a)?;
7612                }
7613                self.emit_op(
7614                    Op::CallBuiltin(BuiltinId::Glob as u16, args.len() as u8),
7615                    line,
7616                    Some(root),
7617                );
7618            }
7619            ExprKind::GlobPar { args, progress } => {
7620                for a in args {
7621                    self.compile_expr(a)?;
7622                }
7623                match progress {
7624                    None => {
7625                        self.emit_op(
7626                            Op::CallBuiltin(BuiltinId::GlobPar as u16, args.len() as u8),
7627                            line,
7628                            Some(root),
7629                        );
7630                    }
7631                    Some(p) => {
7632                        self.compile_expr(p)?;
7633                        self.emit_op(
7634                            Op::CallBuiltin(
7635                                BuiltinId::GlobParProgress as u16,
7636                                (args.len() + 1) as u8,
7637                            ),
7638                            line,
7639                            Some(root),
7640                        );
7641                    }
7642                }
7643            }
7644            ExprKind::ParSed { args, progress } => {
7645                for a in args {
7646                    self.compile_expr(a)?;
7647                }
7648                match progress {
7649                    None => {
7650                        self.emit_op(
7651                            Op::CallBuiltin(BuiltinId::ParSed as u16, args.len() as u8),
7652                            line,
7653                            Some(root),
7654                        );
7655                    }
7656                    Some(p) => {
7657                        self.compile_expr(p)?;
7658                        self.emit_op(
7659                            Op::CallBuiltin(
7660                                BuiltinId::ParSedProgress as u16,
7661                                (args.len() + 1) as u8,
7662                            ),
7663                            line,
7664                            Some(root),
7665                        );
7666                    }
7667                }
7668            }
7669
7670            // ── OOP ──
7671            ExprKind::Bless { ref_expr, class } => {
7672                self.compile_expr(ref_expr)?;
7673                if let Some(c) = class {
7674                    self.compile_expr(c)?;
7675                    self.emit_op(
7676                        Op::CallBuiltin(BuiltinId::Bless as u16, 2),
7677                        line,
7678                        Some(root),
7679                    );
7680                } else {
7681                    self.emit_op(
7682                        Op::CallBuiltin(BuiltinId::Bless as u16, 1),
7683                        line,
7684                        Some(root),
7685                    );
7686                }
7687            }
7688            ExprKind::Caller(e) => {
7689                if let Some(inner) = e {
7690                    self.compile_expr(inner)?;
7691                    self.emit_op(
7692                        Op::CallBuiltin(BuiltinId::Caller as u16, 1),
7693                        line,
7694                        Some(root),
7695                    );
7696                } else {
7697                    self.emit_op(
7698                        Op::CallBuiltin(BuiltinId::Caller as u16, 0),
7699                        line,
7700                        Some(root),
7701                    );
7702                }
7703            }
7704            ExprKind::Wantarray => {
7705                self.emit_op(
7706                    Op::CallBuiltin(BuiltinId::Wantarray as u16, 0),
7707                    line,
7708                    Some(root),
7709                );
7710            }
7711
7712            // ── References ──
7713            ExprKind::ScalarRef(e) => match &e.kind {
7714                ExprKind::ScalarVar(name) => {
7715                    let idx = self.intern_scalar_var_for_ops(name);
7716                    self.emit_op(Op::MakeScalarBindingRef(idx), line, Some(root));
7717                }
7718                ExprKind::ArrayVar(name) => {
7719                    self.check_strict_array_access(name, line)?;
7720                    let stash = self.array_storage_name_for_ops(name);
7721                    let idx = self
7722                        .chunk
7723                        .intern_name(&self.qualify_stash_array_name(&stash));
7724                    self.emit_op(Op::MakeArrayBindingRef(idx), line, Some(root));
7725                }
7726                ExprKind::HashVar(name) => {
7727                    self.check_strict_hash_access(name, line)?;
7728                    let stash = self.hash_storage_name_for_ops(name);
7729                    let idx = self.chunk.intern_name(&stash);
7730                    self.emit_op(Op::MakeHashBindingRef(idx), line, Some(root));
7731                }
7732                ExprKind::Deref {
7733                    expr: inner,
7734                    kind: Sigil::Array,
7735                } => {
7736                    self.compile_expr(inner)?;
7737                    self.emit_op(Op::MakeArrayRefAlias, line, Some(root));
7738                }
7739                ExprKind::Deref {
7740                    expr: inner,
7741                    kind: Sigil::Hash,
7742                } => {
7743                    self.compile_expr(inner)?;
7744                    self.emit_op(Op::MakeHashRefAlias, line, Some(root));
7745                }
7746                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
7747                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7748                    self.emit_op(Op::MakeArrayRef, line, Some(root));
7749                }
7750                ExprKind::AnonymousListSlice { .. } | ExprKind::HashSliceDeref { .. } => {
7751                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7752                    self.emit_op(Op::MakeArrayRef, line, Some(root));
7753                }
7754                _ => {
7755                    self.compile_expr(e)?;
7756                    self.emit_op(Op::MakeScalarRef, line, Some(root));
7757                }
7758            },
7759            ExprKind::ArrayRef(elems) => {
7760                // `[ LIST ]` — each element is in list context so `1..5`, `reverse`, `grep`
7761                // and array variables flatten through [`Op::MakeArray`], which already splats
7762                // nested arrays.
7763                for e in elems {
7764                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7765                }
7766                self.emit_op(Op::MakeArray(elems.len() as u16), line, Some(root));
7767                self.emit_op(Op::MakeArrayRef, line, Some(root));
7768            }
7769            ExprKind::HashRef(pairs) => {
7770                // `{ K => V, ... }` — keys are scalar, values are list context so ranges and
7771                // slurpy constructs on the value side flatten into the built hash.
7772                // Special case: a single pair with the `__HASH_SPREAD__` sentinel key is
7773                // emitted by `parse_forced_hashref_body` (`+{ EXPR }`) and by the `{ %h }`
7774                // hash-spread short-form. Compile the value in list context and let
7775                // [`Op::MakeHashRef`] pair up the resulting list at runtime — the slow
7776                // path's [`Interpreter::eval_expr`] HashRef arm handles this too via the
7777                // same sentinel string.
7778                if pairs.len() == 1 {
7779                    if let ExprKind::String(ref k) = pairs[0].0.kind {
7780                        if k == "__HASH_SPREAD__" {
7781                            self.compile_expr_ctx(&pairs[0].1, WantarrayCtx::List)?;
7782                            self.emit_op(Op::MakeHashRef, line, Some(root));
7783                            return Ok(());
7784                        }
7785                    }
7786                }
7787                for (k, v) in pairs {
7788                    self.compile_expr(k)?;
7789                    self.compile_expr_ctx(v, WantarrayCtx::List)?;
7790                }
7791                self.emit_op(Op::MakeHash((pairs.len() * 2) as u16), line, Some(root));
7792                self.emit_op(Op::MakeHashRef, line, Some(root));
7793            }
7794            ExprKind::CodeRef { body, params } => {
7795                let block_idx = self.add_deferred_block(body.clone());
7796                self.sub_body_block_indices.insert(block_idx);
7797                // Stash params alongside the block index so the 4th-pass
7798                // compile can register them in the new sub-body layer (so
7799                // the closure-write check sees them as locally declared).
7800                while self.code_ref_block_params.len() <= block_idx as usize {
7801                    self.code_ref_block_params.push(Vec::new());
7802                }
7803                self.code_ref_block_params[block_idx as usize] = params.clone();
7804                let sig_idx = self.chunk.add_code_ref_sig(params.clone());
7805                self.emit_op(Op::MakeCodeRef(block_idx, sig_idx), line, Some(root));
7806            }
7807            ExprKind::SubroutineRef(name) => {
7808                // Unary `&name` — invoke subroutine with no explicit args (same as tree `call_named_sub`).
7809                let q = self.qualify_sub_key(name);
7810                let name_idx = self.chunk.intern_name(&q);
7811                self.emit_op(Op::Call(name_idx, 0, ctx.as_byte()), line, Some(root));
7812            }
7813            ExprKind::SubroutineCodeRef(name) => {
7814                // `\&name` — coderef (must exist at run time).
7815                let name_idx = self.chunk.intern_name(name);
7816                self.emit_op(Op::LoadNamedSubRef(name_idx), line, Some(root));
7817            }
7818            ExprKind::DynamicSubCodeRef(expr) => {
7819                self.compile_expr(expr)?;
7820                self.emit_op(Op::LoadDynamicSubRef, line, Some(root));
7821            }
7822
7823            // ── Derefs ──
7824            ExprKind::ArrowDeref { expr, index, kind } => match kind {
7825                DerefKind::Array => {
7826                    self.compile_arrow_array_base_expr(expr)?;
7827                    let mut used_arrow_slice = false;
7828                    // `$r->[$i]` with a single plain-scalar subscript is element
7829                    // access, not a slice — even when the parser wraps it in a
7830                    // 1-element `List`. Returning a 1-element list here breaks
7831                    // arithmetic (`$a + $b` numifies via length to `1+1=2`).
7832                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
7833                        let inner = match &index.kind {
7834                            ExprKind::List(el) if el.len() == 1 => &el[0],
7835                            _ => index.as_ref(),
7836                        };
7837                        self.compile_expr(inner)?;
7838                        self.emit_op(Op::ArrowArray, line, Some(root));
7839                    } else if let ExprKind::List(indices) = &index.kind {
7840                        for ix in indices {
7841                            self.compile_array_slice_index_expr(ix)?;
7842                        }
7843                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, Some(root));
7844                        used_arrow_slice = true;
7845                    } else {
7846                        // One subscript expr may expand to multiple indices (`$r->[0..1]`, `[(0,1)]`).
7847                        self.compile_array_slice_index_expr(index)?;
7848                        self.emit_op(Op::ArrowArraySlice(1), line, Some(root));
7849                        used_arrow_slice = true;
7850                    }
7851                    if used_arrow_slice && ctx != WantarrayCtx::List {
7852                        self.emit_op(Op::ListSliceToScalar, line, Some(root));
7853                    }
7854                }
7855                DerefKind::Hash => {
7856                    self.compile_arrow_hash_base_expr(expr)?;
7857                    self.compile_expr(index)?;
7858                    self.emit_op(Op::ArrowHash, line, Some(root));
7859                }
7860                DerefKind::Call => {
7861                    self.compile_expr(expr)?;
7862                    // Always compile args in list context to preserve all arguments
7863                    self.compile_expr_ctx(index, WantarrayCtx::List)?;
7864                    self.emit_op(Op::ArrowCall(ctx.as_byte()), line, Some(root));
7865                }
7866            },
7867            ExprKind::Deref { expr, kind } => {
7868                // Perl: `scalar @{EXPR}` / `scalar @$r` is the array length (not a copy of the list).
7869                // `scalar %{EXPR}` uses hash fill metrics like `%h` in scalar context.
7870                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
7871                    self.compile_expr(expr)?;
7872                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
7873                } else if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
7874                    self.compile_expr(expr)?;
7875                    self.emit_op(Op::SymbolicDeref(2), line, Some(root));
7876                    self.emit_op(Op::ValueScalarContext, line, Some(root));
7877                } else {
7878                    self.compile_expr(expr)?;
7879                    let b = match kind {
7880                        Sigil::Scalar => 0u8,
7881                        Sigil::Array => 1,
7882                        Sigil::Hash => 2,
7883                        Sigil::Typeglob => 3,
7884                    };
7885                    self.emit_op(Op::SymbolicDeref(b), line, Some(root));
7886                }
7887            }
7888
7889            // ── Interpolated strings ──
7890            ExprKind::InterpolatedString(parts) => {
7891                // Check if any literal part contains case-escape sequences.
7892                let has_case_escapes = parts.iter().any(|p| {
7893                    if let StringPart::Literal(s) = p {
7894                        s.contains('\\')
7895                            && (s.contains("\\U")
7896                                || s.contains("\\L")
7897                                || s.contains("\\u")
7898                                || s.contains("\\l")
7899                                || s.contains("\\Q")
7900                                || s.contains("\\E"))
7901                    } else {
7902                        false
7903                    }
7904                });
7905                if parts.is_empty() {
7906                    let idx = self.chunk.add_constant(StrykeValue::string(String::new()));
7907                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7908                } else {
7909                    // `"$x"` is a single [`StringPart`] — still string context; must go through
7910                    // [`Op::Concat`] so operands are stringified (`use overload '""'`, etc.).
7911                    if !matches!(&parts[0], StringPart::Literal(_)) {
7912                        let idx = self.chunk.add_constant(StrykeValue::string(String::new()));
7913                        self.emit_op(Op::LoadConst(idx), line, Some(root));
7914                    }
7915                    self.compile_string_part(&parts[0], line, Some(root))?;
7916                    for part in &parts[1..] {
7917                        self.compile_string_part(part, line, Some(root))?;
7918                        self.emit_op(Op::Concat, line, Some(root));
7919                    }
7920                    if !matches!(&parts[0], StringPart::Literal(_)) {
7921                        self.emit_op(Op::Concat, line, Some(root));
7922                    }
7923                }
7924                if has_case_escapes {
7925                    self.emit_op(Op::ProcessCaseEscapes, line, Some(root));
7926                }
7927            }
7928
7929            // ── List ──
7930            ExprKind::List(exprs) => {
7931                if ctx == WantarrayCtx::Scalar {
7932                    // Perl: comma-list in scalar context evaluates to the **last** element (`(1,2)` → 2).
7933                    if let Some(last) = exprs.last() {
7934                        self.compile_expr_ctx(last, WantarrayCtx::Scalar)?;
7935                    } else {
7936                        self.emit_op(Op::LoadUndef, line, Some(root));
7937                    }
7938                } else {
7939                    for e in exprs {
7940                        self.compile_expr_ctx(e, ctx)?;
7941                    }
7942                    if exprs.len() != 1 {
7943                        self.emit_op(Op::MakeArray(exprs.len() as u16), line, Some(root));
7944                    }
7945                }
7946            }
7947
7948            // ── QW ──
7949            ExprKind::QW(words) => {
7950                for w in words {
7951                    let idx = self.chunk.add_constant(StrykeValue::string(w.clone()));
7952                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7953                }
7954                self.emit_op(Op::MakeArray(words.len() as u16), line, Some(root));
7955            }
7956
7957            // ── Postfix if/unless ──
7958            ExprKind::PostfixIf { expr, condition } => {
7959                self.compile_boolean_rvalue_condition(condition)?;
7960                let j = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7961                self.compile_expr(expr)?;
7962                let end = self.emit_op(Op::Jump(0), line, Some(root));
7963                self.chunk.patch_jump_here(j);
7964                self.emit_op(Op::LoadUndef, line, Some(root));
7965                self.chunk.patch_jump_here(end);
7966            }
7967            ExprKind::PostfixUnless { expr, condition } => {
7968                self.compile_boolean_rvalue_condition(condition)?;
7969                let j = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
7970                self.compile_expr(expr)?;
7971                let end = self.emit_op(Op::Jump(0), line, Some(root));
7972                self.chunk.patch_jump_here(j);
7973                self.emit_op(Op::LoadUndef, line, Some(root));
7974                self.chunk.patch_jump_here(end);
7975            }
7976
7977            // ── Postfix while/until/foreach ──
7978            ExprKind::PostfixWhile { expr, condition } => {
7979                // Detect `do { BLOCK } while (COND)` pattern
7980                let is_do_block = matches!(
7981                    &expr.kind,
7982                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
7983                );
7984                if is_do_block {
7985                    // do-while: body executes before first condition check
7986                    let loop_start = self.chunk.len();
7987                    self.compile_expr(expr)?;
7988                    self.emit_op(Op::Pop, line, Some(root));
7989                    self.compile_boolean_rvalue_condition(condition)?;
7990                    self.emit_op(Op::JumpIfTrue(loop_start), line, Some(root));
7991                    self.emit_op(Op::LoadUndef, line, Some(root));
7992                } else {
7993                    // Regular postfix while: condition checked first
7994                    let loop_start = self.chunk.len();
7995                    self.compile_boolean_rvalue_condition(condition)?;
7996                    let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7997                    self.compile_expr(expr)?;
7998                    self.emit_op(Op::Pop, line, Some(root));
7999                    self.emit_op(Op::Jump(loop_start), line, Some(root));
8000                    self.chunk.patch_jump_here(exit_jump);
8001                    self.emit_op(Op::LoadUndef, line, Some(root));
8002                }
8003            }
8004            ExprKind::PostfixUntil { expr, condition } => {
8005                let is_do_block = matches!(
8006                    &expr.kind,
8007                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
8008                );
8009                if is_do_block {
8010                    let loop_start = self.chunk.len();
8011                    self.compile_expr(expr)?;
8012                    self.emit_op(Op::Pop, line, Some(root));
8013                    self.compile_boolean_rvalue_condition(condition)?;
8014                    self.emit_op(Op::JumpIfFalse(loop_start), line, Some(root));
8015                    self.emit_op(Op::LoadUndef, line, Some(root));
8016                } else {
8017                    let loop_start = self.chunk.len();
8018                    self.compile_boolean_rvalue_condition(condition)?;
8019                    let exit_jump = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
8020                    self.compile_expr(expr)?;
8021                    self.emit_op(Op::Pop, line, Some(root));
8022                    self.emit_op(Op::Jump(loop_start), line, Some(root));
8023                    self.chunk.patch_jump_here(exit_jump);
8024                    self.emit_op(Op::LoadUndef, line, Some(root));
8025                }
8026            }
8027            ExprKind::PostfixForeach { expr, list } => {
8028                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8029                let list_name = self.chunk.intern_name("__pf_foreach_list__");
8030                self.emit_op(Op::DeclareArray(list_name), line, Some(root));
8031                let counter = self.chunk.intern_name("__pf_foreach_i__");
8032                self.emit_op(Op::LoadInt(0), line, Some(root));
8033                self.emit_op(Op::DeclareScalar(counter), line, Some(root));
8034                let underscore = self.chunk.intern_name("_");
8035
8036                let loop_start = self.chunk.len();
8037                self.emit_get_scalar(counter, line, Some(root));
8038                self.emit_op(Op::ArrayLen(list_name), line, Some(root));
8039                self.emit_op(Op::NumLt, line, Some(root));
8040                let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
8041
8042                self.emit_get_scalar(counter, line, Some(root));
8043                self.emit_op(Op::GetArrayElem(list_name), line, Some(root));
8044                self.emit_set_scalar(underscore, line, Some(root));
8045
8046                self.compile_expr(expr)?;
8047                self.emit_op(Op::Pop, line, Some(root));
8048
8049                self.emit_pre_inc(counter, line, Some(root));
8050                self.emit_op(Op::Pop, line, Some(root));
8051                self.emit_op(Op::Jump(loop_start), line, Some(root));
8052                self.chunk.patch_jump_here(exit_jump);
8053                self.emit_op(Op::LoadUndef, line, Some(root));
8054            }
8055
8056            ExprKind::AlgebraicMatch { subject, arms } => {
8057                let idx = self
8058                    .chunk
8059                    .add_algebraic_match_entry(subject.as_ref().clone(), arms.clone());
8060                self.emit_op(Op::AlgebraicMatch(idx), line, Some(root));
8061            }
8062
8063            // ── Match (regex) ──
8064            ExprKind::Match {
8065                expr,
8066                pattern,
8067                flags,
8068                scalar_g,
8069                delim: _,
8070            } => {
8071                self.compile_expr(expr)?;
8072                let pat_idx = self
8073                    .chunk
8074                    .add_constant(StrykeValue::string(pattern.clone()));
8075                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
8076                let pos_key_idx = if *scalar_g && flags.contains('g') {
8077                    if let ExprKind::ScalarVar(n) = &expr.kind {
8078                        let stor = self.scalar_storage_name_for_ops(n);
8079                        self.chunk.add_constant(StrykeValue::string(stor))
8080                    } else {
8081                        u16::MAX
8082                    }
8083                } else {
8084                    u16::MAX
8085                };
8086                // The match runtime reads `wantarray_kind` to decide whether
8087                // to return captures as a list (BUG-258). Push the outer
8088                // context around the op so destructuring assignments like
8089                // `my ($k, $v) = $s =~ /^(\w+)=(\d+)/` see the captures.
8090                let push_wa = ctx == WantarrayCtx::List;
8091                if push_wa {
8092                    self.emit_op(Op::WantarrayPush(ctx.as_byte()), line, Some(root));
8093                }
8094                self.emit_op(
8095                    Op::RegexMatch(pat_idx, flags_idx, *scalar_g, pos_key_idx),
8096                    line,
8097                    Some(root),
8098                );
8099                if push_wa {
8100                    self.emit_op(Op::WantarrayPop, line, Some(root));
8101                }
8102            }
8103
8104            ExprKind::Substitution {
8105                expr,
8106                pattern,
8107                replacement,
8108                flags,
8109                delim: _,
8110            } => {
8111                self.compile_expr(expr)?;
8112                let pat_idx = self
8113                    .chunk
8114                    .add_constant(StrykeValue::string(pattern.clone()));
8115                let repl_idx = self
8116                    .chunk
8117                    .add_constant(StrykeValue::string(replacement.clone()));
8118                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
8119                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
8120                self.emit_op(
8121                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lv_idx),
8122                    line,
8123                    Some(root),
8124                );
8125            }
8126            ExprKind::Transliterate {
8127                expr,
8128                from,
8129                to,
8130                flags,
8131                delim: _,
8132            } => {
8133                self.compile_expr(expr)?;
8134                let from_idx = self.chunk.add_constant(StrykeValue::string(from.clone()));
8135                let to_idx = self.chunk.add_constant(StrykeValue::string(to.clone()));
8136                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
8137                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
8138                self.emit_op(
8139                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lv_idx),
8140                    line,
8141                    Some(root),
8142                );
8143            }
8144
8145            // ── Regex literal ──
8146            ExprKind::Regex(pattern, flags) => {
8147                if ctx == WantarrayCtx::Void {
8148                    // Statement context: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a discarded regex object.
8149                    self.compile_boolean_rvalue_condition(root)?;
8150                } else {
8151                    let pat_idx = self
8152                        .chunk
8153                        .add_constant(StrykeValue::string(pattern.clone()));
8154                    let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
8155                    self.emit_op(Op::LoadRegex(pat_idx, flags_idx), line, Some(root));
8156                }
8157            }
8158
8159            // ── Map/Grep/Sort with blocks ──
8160            ExprKind::MapExpr {
8161                block,
8162                list,
8163                flatten_array_refs,
8164                stream,
8165            } => {
8166                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8167                if *stream {
8168                    let block_idx = self.add_deferred_block(block.clone());
8169                    if *flatten_array_refs {
8170                        self.emit_op(Op::MapsFlatMapWithBlock(block_idx), line, Some(root));
8171                    } else {
8172                        self.emit_op(Op::MapsWithBlock(block_idx), line, Some(root));
8173                    }
8174                } else if let Some(k) = crate::map_grep_fast::detect_map_int_mul(block) {
8175                    self.emit_op(Op::MapIntMul(k), line, Some(root));
8176                } else {
8177                    let block_idx = self.add_deferred_block(block.clone());
8178                    if *flatten_array_refs {
8179                        self.emit_op(Op::FlatMapWithBlock(block_idx), line, Some(root));
8180                    } else {
8181                        self.emit_op(Op::MapWithBlock(block_idx), line, Some(root));
8182                    }
8183                }
8184                if ctx != WantarrayCtx::List {
8185                    self.emit_op(Op::StackArrayLen, line, Some(root));
8186                }
8187            }
8188            ExprKind::MapExprComma {
8189                expr,
8190                list,
8191                flatten_array_refs,
8192                stream,
8193            } => {
8194                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8195                let idx = self.chunk.add_map_expr_entry(*expr.clone());
8196                if *stream {
8197                    if *flatten_array_refs {
8198                        self.emit_op(Op::MapsFlatMapWithExpr(idx), line, Some(root));
8199                    } else {
8200                        self.emit_op(Op::MapsWithExpr(idx), line, Some(root));
8201                    }
8202                } else if *flatten_array_refs {
8203                    self.emit_op(Op::FlatMapWithExpr(idx), line, Some(root));
8204                } else {
8205                    self.emit_op(Op::MapWithExpr(idx), line, Some(root));
8206                }
8207                if ctx != WantarrayCtx::List {
8208                    self.emit_op(Op::StackArrayLen, line, Some(root));
8209                }
8210            }
8211            ExprKind::ForEachExpr { block, list } => {
8212                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8213                let block_idx = self.add_deferred_block(block.clone());
8214                self.emit_op(Op::ForEachWithBlock(block_idx), line, Some(root));
8215            }
8216            ExprKind::GrepExpr {
8217                block,
8218                list,
8219                keyword,
8220            } => {
8221                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8222                if keyword.is_stream() {
8223                    let block_idx = self.add_deferred_block(block.clone());
8224                    self.emit_op(Op::FilterWithBlock(block_idx), line, Some(root));
8225                } else if let Some((m, r)) = crate::map_grep_fast::detect_grep_int_mod_eq(block) {
8226                    self.emit_op(Op::GrepIntModEq(m, r), line, Some(root));
8227                } else {
8228                    let block_idx = self.add_deferred_block(block.clone());
8229                    self.emit_op(Op::GrepWithBlock(block_idx), line, Some(root));
8230                }
8231                if ctx != WantarrayCtx::List {
8232                    self.emit_op(Op::StackArrayLen, line, Some(root));
8233                }
8234            }
8235            ExprKind::GrepExprComma {
8236                expr,
8237                list,
8238                keyword,
8239            } => {
8240                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8241                let idx = self.chunk.add_grep_expr_entry(*expr.clone());
8242                if keyword.is_stream() {
8243                    self.emit_op(Op::FilterWithExpr(idx), line, Some(root));
8244                } else {
8245                    self.emit_op(Op::GrepWithExpr(idx), line, Some(root));
8246                }
8247                if ctx != WantarrayCtx::List {
8248                    self.emit_op(Op::StackArrayLen, line, Some(root));
8249                }
8250            }
8251            ExprKind::SortExpr { cmp, list } => {
8252                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8253                match cmp {
8254                    Some(crate::ast::SortComparator::Block(block)) => {
8255                        if let Some(mode) = detect_sort_block_fast(block) {
8256                            let tag = match mode {
8257                                crate::sort_fast::SortBlockFast::Numeric => 0u8,
8258                                crate::sort_fast::SortBlockFast::String => 1u8,
8259                                crate::sort_fast::SortBlockFast::NumericRev => 2u8,
8260                                crate::sort_fast::SortBlockFast::StringRev => 3u8,
8261                            };
8262                            self.emit_op(Op::SortWithBlockFast(tag), line, Some(root));
8263                        } else {
8264                            let block_idx = self.add_deferred_block(block.clone());
8265                            self.register_sort_pair_block(block_idx);
8266                            self.emit_op(Op::SortWithBlock(block_idx), line, Some(root));
8267                        }
8268                    }
8269                    Some(crate::ast::SortComparator::Code(code_expr)) => {
8270                        self.compile_expr(code_expr)?;
8271                        self.emit_op(Op::SortWithCodeComparator(ctx.as_byte()), line, Some(root));
8272                    }
8273                    None => {
8274                        self.emit_op(Op::SortNoBlock, line, Some(root));
8275                    }
8276                }
8277            }
8278
8279            // ── Parallel extensions ──
8280            ExprKind::ParExpr { .. }
8281            | ExprKind::ParReduceExpr { .. }
8282            | ExprKind::DistReduceExpr { .. } => {
8283                // `par { BLOCK }`, `par_reduce { extract } [{ merge }]`, and
8284                // `~d>` (distributed thread macro) — bytecode VM has no
8285                // dedicated opcodes; defer AST eval to the interpreter via
8286                // `Op::EvalAstExpr`.
8287                let idx = self.chunk.ast_eval_exprs.len() as u16;
8288                self.chunk.ast_eval_exprs.push(root.clone());
8289                self.emit_op(Op::EvalAstExpr(idx), line, Some(root));
8290            }
8291            ExprKind::PMapExpr {
8292                block,
8293                list,
8294                progress,
8295                flat_outputs,
8296                on_cluster,
8297                stream,
8298            } => {
8299                if *stream {
8300                    // Streaming: no progress flag needed, just list + block.
8301                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
8302                    let block_idx = self.add_deferred_block(block.clone());
8303                    if *flat_outputs {
8304                        self.emit_op(Op::PFlatMapsWithBlock(block_idx), line, Some(root));
8305                    } else {
8306                        self.emit_op(Op::PMapsWithBlock(block_idx), line, Some(root));
8307                    }
8308                } else {
8309                    if let Some(p) = progress {
8310                        self.compile_expr(p)?;
8311                    } else {
8312                        self.emit_op(Op::LoadInt(0), line, Some(root));
8313                    }
8314                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
8315                    if let Some(cluster_e) = on_cluster {
8316                        self.compile_expr(cluster_e)?;
8317                        let block_idx = self.add_deferred_block(block.clone());
8318                        self.emit_op(
8319                            Op::PMapRemote {
8320                                block_idx,
8321                                flat: u8::from(*flat_outputs),
8322                            },
8323                            line,
8324                            Some(root),
8325                        );
8326                    } else {
8327                        let block_idx = self.add_deferred_block(block.clone());
8328                        if *flat_outputs {
8329                            self.emit_op(Op::PFlatMapWithBlock(block_idx), line, Some(root));
8330                        } else {
8331                            self.emit_op(Op::PMapWithBlock(block_idx), line, Some(root));
8332                        }
8333                    }
8334                }
8335            }
8336            ExprKind::PMapChunkedExpr {
8337                chunk_size,
8338                block,
8339                list,
8340                progress,
8341            } => {
8342                if let Some(p) = progress {
8343                    self.compile_expr(p)?;
8344                } else {
8345                    self.emit_op(Op::LoadInt(0), line, Some(root));
8346                }
8347                self.compile_expr(chunk_size)?;
8348                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8349                let block_idx = self.add_deferred_block(block.clone());
8350                self.emit_op(Op::PMapChunkedWithBlock(block_idx), line, Some(root));
8351            }
8352            ExprKind::PGrepExpr {
8353                block,
8354                list,
8355                progress,
8356                stream,
8357            } => {
8358                if *stream {
8359                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
8360                    let block_idx = self.add_deferred_block(block.clone());
8361                    self.emit_op(Op::PGrepsWithBlock(block_idx), line, Some(root));
8362                } else {
8363                    if let Some(p) = progress {
8364                        self.compile_expr(p)?;
8365                    } else {
8366                        self.emit_op(Op::LoadInt(0), line, Some(root));
8367                    }
8368                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
8369                    let block_idx = self.add_deferred_block(block.clone());
8370                    self.emit_op(Op::PGrepWithBlock(block_idx), line, Some(root));
8371                }
8372            }
8373            ExprKind::PForExpr {
8374                block,
8375                list,
8376                progress,
8377            } => {
8378                if let Some(p) = progress {
8379                    self.compile_expr(p)?;
8380                } else {
8381                    self.emit_op(Op::LoadInt(0), line, Some(root));
8382                }
8383                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8384                let block_idx = self.add_deferred_block(block.clone());
8385                self.emit_op(Op::PForWithBlock(block_idx), line, Some(root));
8386            }
8387            ExprKind::ParLinesExpr {
8388                path,
8389                callback,
8390                progress,
8391            } => {
8392                let idx = self.chunk.add_par_lines_entry(
8393                    path.as_ref().clone(),
8394                    callback.as_ref().clone(),
8395                    progress.as_ref().map(|p| p.as_ref().clone()),
8396                );
8397                self.emit_op(Op::ParLines(idx), line, Some(root));
8398            }
8399            ExprKind::ParWalkExpr {
8400                path,
8401                callback,
8402                progress,
8403            } => {
8404                let idx = self.chunk.add_par_walk_entry(
8405                    path.as_ref().clone(),
8406                    callback.as_ref().clone(),
8407                    progress.as_ref().map(|p| p.as_ref().clone()),
8408                );
8409                self.emit_op(Op::ParWalk(idx), line, Some(root));
8410            }
8411            ExprKind::PwatchExpr { path, callback } => {
8412                let idx = self
8413                    .chunk
8414                    .add_pwatch_entry(path.as_ref().clone(), callback.as_ref().clone());
8415                self.emit_op(Op::Pwatch(idx), line, Some(root));
8416            }
8417            ExprKind::PSortExpr {
8418                cmp,
8419                list,
8420                progress,
8421            } => {
8422                if let Some(p) = progress {
8423                    self.compile_expr(p)?;
8424                } else {
8425                    self.emit_op(Op::LoadInt(0), line, Some(root));
8426                }
8427                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8428                if let Some(block) = cmp {
8429                    if let Some(mode) = detect_sort_block_fast(block) {
8430                        let tag = match mode {
8431                            crate::sort_fast::SortBlockFast::Numeric => 0u8,
8432                            crate::sort_fast::SortBlockFast::String => 1u8,
8433                            crate::sort_fast::SortBlockFast::NumericRev => 2u8,
8434                            crate::sort_fast::SortBlockFast::StringRev => 3u8,
8435                        };
8436                        self.emit_op(Op::PSortWithBlockFast(tag), line, Some(root));
8437                    } else {
8438                        let block_idx = self.add_deferred_block(block.clone());
8439                        self.register_sort_pair_block(block_idx);
8440                        self.emit_op(Op::PSortWithBlock(block_idx), line, Some(root));
8441                    }
8442                } else {
8443                    self.emit_op(Op::PSortNoBlockParallel, line, Some(root));
8444                }
8445            }
8446            ExprKind::ReduceExpr { block, list } => {
8447                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8448                let block_idx = self.add_deferred_block(block.clone());
8449                self.register_sort_pair_block(block_idx);
8450                self.emit_op(Op::ReduceWithBlock(block_idx), line, Some(root));
8451            }
8452            ExprKind::PReduceExpr {
8453                block,
8454                list,
8455                progress,
8456            } => {
8457                if let Some(p) = progress {
8458                    self.compile_expr(p)?;
8459                } else {
8460                    self.emit_op(Op::LoadInt(0), line, Some(root));
8461                }
8462                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8463                let block_idx = self.add_deferred_block(block.clone());
8464                self.register_sort_pair_block(block_idx);
8465                self.emit_op(Op::PReduceWithBlock(block_idx), line, Some(root));
8466            }
8467            ExprKind::PReduceInitExpr {
8468                init,
8469                block,
8470                list,
8471                progress,
8472            } => {
8473                if let Some(p) = progress {
8474                    self.compile_expr(p)?;
8475                } else {
8476                    self.emit_op(Op::LoadInt(0), line, Some(root));
8477                }
8478                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8479                self.compile_expr(init)?;
8480                let block_idx = self.add_deferred_block(block.clone());
8481                self.emit_op(Op::PReduceInitWithBlock(block_idx), line, Some(root));
8482            }
8483            ExprKind::PMapReduceExpr {
8484                map_block,
8485                reduce_block,
8486                list,
8487                progress,
8488            } => {
8489                if let Some(p) = progress {
8490                    self.compile_expr(p)?;
8491                } else {
8492                    self.emit_op(Op::LoadInt(0), line, Some(root));
8493                }
8494                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8495                let map_idx = self.add_deferred_block(map_block.clone());
8496                let reduce_idx = self.add_deferred_block(reduce_block.clone());
8497                self.emit_op(
8498                    Op::PMapReduceWithBlocks(map_idx, reduce_idx),
8499                    line,
8500                    Some(root),
8501                );
8502            }
8503            ExprKind::PcacheExpr {
8504                block,
8505                list,
8506                progress,
8507            } => {
8508                if let Some(p) = progress {
8509                    self.compile_expr(p)?;
8510                } else {
8511                    self.emit_op(Op::LoadInt(0), line, Some(root));
8512                }
8513                self.compile_expr_ctx(list, WantarrayCtx::List)?;
8514                let block_idx = self.add_deferred_block(block.clone());
8515                self.emit_op(Op::PcacheWithBlock(block_idx), line, Some(root));
8516            }
8517            ExprKind::PselectExpr { receivers, timeout } => {
8518                let n = receivers.len();
8519                if n > u8::MAX as usize {
8520                    return Err(CompileError::Unsupported(
8521                        "pselect: too many receivers".into(),
8522                    ));
8523                }
8524                for r in receivers {
8525                    self.compile_expr(r)?;
8526                }
8527                let has_timeout = timeout.is_some();
8528                if let Some(t) = timeout {
8529                    self.compile_expr(t)?;
8530                }
8531                self.emit_op(
8532                    Op::Pselect {
8533                        n_rx: n as u8,
8534                        has_timeout,
8535                    },
8536                    line,
8537                    Some(root),
8538                );
8539            }
8540            ExprKind::FanExpr {
8541                count,
8542                block,
8543                progress,
8544                capture,
8545            } => {
8546                if let Some(p) = progress {
8547                    self.compile_expr(p)?;
8548                } else {
8549                    self.emit_op(Op::LoadInt(0), line, Some(root));
8550                }
8551                let block_idx = self.add_deferred_block(block.clone());
8552                match (count, capture) {
8553                    (Some(c), false) => {
8554                        self.compile_expr(c)?;
8555                        self.emit_op(Op::FanWithBlock(block_idx), line, Some(root));
8556                    }
8557                    (None, false) => {
8558                        self.emit_op(Op::FanWithBlockAuto(block_idx), line, Some(root));
8559                    }
8560                    (Some(c), true) => {
8561                        self.compile_expr(c)?;
8562                        self.emit_op(Op::FanCapWithBlock(block_idx), line, Some(root));
8563                    }
8564                    (None, true) => {
8565                        self.emit_op(Op::FanCapWithBlockAuto(block_idx), line, Some(root));
8566                    }
8567                }
8568            }
8569            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
8570                let block_idx = self.add_deferred_block(body.clone());
8571                self.emit_op(Op::AsyncBlock(block_idx), line, Some(root));
8572            }
8573            ExprKind::Trace { body } => {
8574                let block_idx = self.add_deferred_block(body.clone());
8575                self.emit_op(Op::TraceBlock(block_idx), line, Some(root));
8576            }
8577            ExprKind::Timer { body } => {
8578                let block_idx = self.add_deferred_block(body.clone());
8579                self.emit_op(Op::TimerBlock(block_idx), line, Some(root));
8580            }
8581            ExprKind::Bench { body, times } => {
8582                self.compile_expr(times)?;
8583                let block_idx = self.add_deferred_block(body.clone());
8584                self.emit_op(Op::BenchBlock(block_idx), line, Some(root));
8585            }
8586            ExprKind::Await(e) => {
8587                self.compile_expr(e)?;
8588                self.emit_op(Op::Await, line, Some(root));
8589            }
8590            ExprKind::Slurp(e) => {
8591                self.compile_expr(e)?;
8592                self.emit_op(
8593                    Op::CallBuiltin(BuiltinId::Slurp as u16, 1),
8594                    line,
8595                    Some(root),
8596                );
8597            }
8598            ExprKind::Swallow(e) => {
8599                self.compile_expr(e)?;
8600                self.emit_op(
8601                    Op::CallBuiltin(BuiltinId::Swallow as u16, 1),
8602                    line,
8603                    Some(root),
8604                );
8605            }
8606            ExprKind::Ingest(e) => {
8607                self.compile_expr(e)?;
8608                self.emit_op(
8609                    Op::CallBuiltin(BuiltinId::Ingest as u16, 1),
8610                    line,
8611                    Some(root),
8612                );
8613            }
8614            ExprKind::Burp(e) => {
8615                self.compile_expr(e)?;
8616                self.emit_op(Op::CallBuiltin(BuiltinId::Burp as u16, 1), line, Some(root));
8617            }
8618            ExprKind::God(e) => {
8619                self.compile_expr(e)?;
8620                self.emit_op(Op::CallBuiltin(BuiltinId::God as u16, 1), line, Some(root));
8621            }
8622            ExprKind::Capture(e) => {
8623                self.compile_expr(e)?;
8624                self.emit_op(
8625                    Op::CallBuiltin(BuiltinId::Capture as u16, 1),
8626                    line,
8627                    Some(root),
8628                );
8629            }
8630            ExprKind::Qx(e) => {
8631                self.compile_expr(e)?;
8632                // Perl `qx`/backticks in list context yield one element per
8633                // line; the dedicated list-context op handles the split.
8634                let id = if ctx == WantarrayCtx::List {
8635                    BuiltinId::ReadpipeList
8636                } else {
8637                    BuiltinId::Readpipe
8638                };
8639                self.emit_op(Op::CallBuiltin(id as u16, 1), line, Some(root));
8640            }
8641            ExprKind::FetchUrl(e) => {
8642                self.compile_expr(e)?;
8643                self.emit_op(
8644                    Op::CallBuiltin(BuiltinId::FetchUrl as u16, 1),
8645                    line,
8646                    Some(root),
8647                );
8648            }
8649            ExprKind::Pchannel { capacity } => {
8650                if let Some(c) = capacity {
8651                    self.compile_expr(c)?;
8652                    self.emit_op(
8653                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 1),
8654                        line,
8655                        Some(root),
8656                    );
8657                } else {
8658                    self.emit_op(
8659                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 0),
8660                        line,
8661                        Some(root),
8662                    );
8663                }
8664            }
8665            ExprKind::RetryBlock { .. }
8666            | ExprKind::RateLimitBlock { .. }
8667            | ExprKind::EveryBlock { .. }
8668            | ExprKind::GenBlock { .. }
8669            | ExprKind::Yield(_)
8670            | ExprKind::Spinner { .. } => {
8671                let idx = self.chunk.ast_eval_exprs.len() as u16;
8672                self.chunk.ast_eval_exprs.push(root.clone());
8673                self.emit_op(Op::EvalAstExpr(idx), line, Some(root));
8674            }
8675            ExprKind::MyExpr { keyword, decls } => {
8676                // `my $x = EXPR` in expression context (e.g. `while (my $line = <$fh>)`)
8677                // Compile the declaration, then leave the value on the stack for the caller.
8678                if decls.len() == 1 && decls[0].sigil == Sigil::Scalar {
8679                    let decl = &decls[0];
8680                    if let Some(init) = &decl.initializer {
8681                        self.compile_expr(init)?;
8682                    } else {
8683                        self.chunk.emit(Op::LoadUndef, line);
8684                    }
8685                    // Dup so the value stays on stack after DeclareScalar consumes one copy
8686                    self.emit_op(Op::Dup, line, Some(root));
8687                    let name_idx = self.chunk.intern_name(&decl.name);
8688                    match keyword.as_str() {
8689                        "state" => {
8690                            let name = self.chunk.names[name_idx as usize].clone();
8691                            self.register_declare(Sigil::Scalar, &name, false);
8692                            self.chunk.emit(Op::DeclareStateScalar(name_idx), line);
8693                        }
8694                        _ => {
8695                            self.emit_declare_scalar(name_idx, line, false);
8696                        }
8697                    }
8698                } else {
8699                    return Err(CompileError::Unsupported(
8700                        "my/our/state/local in expression context with multiple or non-scalar decls".into(),
8701                    ));
8702                }
8703            }
8704        }
8705        Ok(())
8706    }
8707
8708    fn compile_string_part(
8709        &mut self,
8710        part: &StringPart,
8711        line: usize,
8712        parent: Option<&Expr>,
8713    ) -> Result<(), CompileError> {
8714        match part {
8715            StringPart::Literal(s) => {
8716                let idx = self.chunk.add_constant(StrykeValue::string(s.clone()));
8717                self.emit_op(Op::LoadConst(idx), line, parent);
8718            }
8719            StringPart::ScalarVar(name) => {
8720                let idx = self.intern_scalar_var_for_ops(name);
8721                self.emit_get_scalar(idx, line, parent);
8722            }
8723            StringPart::ArrayVar(name) => {
8724                let stash = self.array_storage_name_for_ops(name);
8725                let idx = self
8726                    .chunk
8727                    .intern_name(&self.qualify_stash_array_name(&stash));
8728                self.emit_op(Op::GetArray(idx), line, parent);
8729                self.emit_op(Op::ArrayStringifyListSep, line, parent);
8730            }
8731            StringPart::Expr(e) => {
8732                // Interpolation uses list/array values (`$"`), not Perl scalar(@arr) length.
8733                if matches!(&e.kind, ExprKind::ArraySlice { .. })
8734                    || matches!(
8735                        &e.kind,
8736                        ExprKind::Deref {
8737                            kind: Sigil::Array,
8738                            ..
8739                        }
8740                    )
8741                {
8742                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
8743                    self.emit_op(Op::ArrayStringifyListSep, line, parent);
8744                } else {
8745                    self.compile_expr(e)?;
8746                }
8747            }
8748        }
8749        Ok(())
8750    }
8751
8752    fn compile_assign(
8753        &mut self,
8754        target: &Expr,
8755        line: usize,
8756        keep: bool,
8757        ast: Option<&Expr>,
8758    ) -> Result<(), CompileError> {
8759        match &target.kind {
8760            ExprKind::ScalarVar(name) => {
8761                self.check_strict_scalar_access(name, line)?;
8762                self.check_scalar_mutable(name, line)?;
8763                self.check_closure_write_to_outer_my(name, line)?;
8764                let idx = self.intern_scalar_var_for_ops(name);
8765                if keep {
8766                    self.emit_set_scalar_keep(idx, line, ast);
8767                } else {
8768                    self.emit_set_scalar(idx, line, ast);
8769                }
8770            }
8771            ExprKind::ArrayVar(name) => {
8772                self.check_strict_array_access(name, line)?;
8773                let stash = self.array_storage_name_for_ops(name);
8774                let q = self.qualify_stash_array_name(&stash);
8775                self.check_array_mutable(&q, line)?;
8776                let idx = self.chunk.intern_name(&q);
8777                self.emit_op(Op::SetArray(idx), line, ast);
8778                if keep {
8779                    self.emit_op(Op::GetArray(idx), line, ast);
8780                }
8781            }
8782            ExprKind::HashVar(name) => {
8783                self.check_strict_hash_access(name, line)?;
8784                self.check_hash_mutable(name, line)?;
8785                let idx = self.chunk.intern_name(name);
8786                self.emit_op(Op::SetHash(idx), line, ast);
8787                if keep {
8788                    self.emit_op(Op::GetHash(idx), line, ast);
8789                }
8790            }
8791            ExprKind::ArrayElement { array, index } => {
8792                self.check_strict_array_access(array, line)?;
8793                let q = self.qualify_stash_array_name(array);
8794                self.check_array_mutable(&q, line)?;
8795                let idx = self.chunk.intern_name(&q);
8796                self.compile_expr(index)?;
8797                if keep {
8798                    self.emit_op(Op::SetArrayElemKeep(idx), line, ast);
8799                } else {
8800                    self.emit_op(Op::SetArrayElem(idx), line, ast);
8801                }
8802            }
8803            ExprKind::ArraySlice { array, indices } => {
8804                if indices.is_empty() {
8805                    if self.is_mysync_array(array) {
8806                        return Err(CompileError::Unsupported(
8807                            "mysync array slice assign".into(),
8808                        ));
8809                    }
8810                    self.check_strict_array_access(array, line)?;
8811                    let q = self.qualify_stash_array_name(array);
8812                    self.check_array_mutable(&q, line)?;
8813                    let arr_idx = self.chunk.intern_name(&q);
8814                    self.emit_op(Op::SetNamedArraySlice(arr_idx, 0), line, ast);
8815                    if keep {
8816                        self.emit_op(Op::MakeArray(0), line, ast);
8817                    }
8818                    return Ok(());
8819                }
8820                if self.is_mysync_array(array) {
8821                    return Err(CompileError::Unsupported(
8822                        "mysync array slice assign".into(),
8823                    ));
8824                }
8825                self.check_strict_array_access(array, line)?;
8826                let q = self.qualify_stash_array_name(array);
8827                self.check_array_mutable(&q, line)?;
8828                let arr_idx = self.chunk.intern_name(&q);
8829                for ix in indices {
8830                    self.compile_array_slice_index_expr(ix)?;
8831                }
8832                self.emit_op(
8833                    Op::SetNamedArraySlice(arr_idx, indices.len() as u16),
8834                    line,
8835                    ast,
8836                );
8837                if keep {
8838                    for (ix, index_expr) in indices.iter().enumerate() {
8839                        self.compile_array_slice_index_expr(index_expr)?;
8840                        self.emit_op(Op::ArraySlicePart(arr_idx), line, ast);
8841                        if ix > 0 {
8842                            self.emit_op(Op::ArrayConcatTwo, line, ast);
8843                        }
8844                    }
8845                }
8846                return Ok(());
8847            }
8848            ExprKind::HashElement { hash, key } => {
8849                self.check_strict_hash_access(hash, line)?;
8850                self.check_hash_mutable(hash, line)?;
8851                let stash = self.hash_storage_name_for_ops(hash);
8852                let idx = self.chunk.intern_name(&stash);
8853                self.compile_expr(key)?;
8854                if keep {
8855                    self.emit_op(Op::SetHashElemKeep(idx), line, ast);
8856                } else {
8857                    self.emit_op(Op::SetHashElem(idx), line, ast);
8858                }
8859            }
8860            ExprKind::HashSlice { hash, keys } => {
8861                if keys.is_empty() {
8862                    if self.is_mysync_hash(hash) {
8863                        return Err(CompileError::Unsupported("mysync hash slice assign".into()));
8864                    }
8865                    self.check_strict_hash_access(hash, line)?;
8866                    self.check_hash_mutable(hash, line)?;
8867                    let hash_idx = self.chunk.intern_name(hash);
8868                    self.emit_op(Op::SetHashSlice(hash_idx, 0), line, ast);
8869                    if keep {
8870                        self.emit_op(Op::MakeArray(0), line, ast);
8871                    }
8872                    return Ok(());
8873                }
8874                if self.is_mysync_hash(hash) {
8875                    return Err(CompileError::Unsupported("mysync hash slice assign".into()));
8876                }
8877                self.check_strict_hash_access(hash, line)?;
8878                self.check_hash_mutable(hash, line)?;
8879                let hash_idx = self.chunk.intern_name(hash);
8880                // Multi-key entries (`'a'..'c'`, `qw/a b/`, list literals) push an array value;
8881                // [`Self::assign_named_hash_slice`] / [`crate::bytecode::Op::SetHashSlice`]
8882                // flattens it at runtime, so compile in list context (scalar context collapses
8883                // `..` to a flip-flop).
8884                for key_expr in keys {
8885                    self.compile_hash_slice_key_expr(key_expr)?;
8886                }
8887                self.emit_op(Op::SetHashSlice(hash_idx, keys.len() as u16), line, ast);
8888                if keep {
8889                    for key_expr in keys {
8890                        self.compile_expr(key_expr)?;
8891                        self.emit_op(Op::GetHashElem(hash_idx), line, ast);
8892                    }
8893                    self.emit_op(Op::MakeArray(keys.len() as u16), line, ast);
8894                }
8895                return Ok(());
8896            }
8897            ExprKind::Deref {
8898                expr,
8899                kind: Sigil::Scalar,
8900            } => {
8901                self.compile_expr(expr)?;
8902                if keep {
8903                    self.emit_op(Op::SetSymbolicScalarRefKeep, line, ast);
8904                } else {
8905                    self.emit_op(Op::SetSymbolicScalarRef, line, ast);
8906                }
8907            }
8908            ExprKind::Deref {
8909                expr,
8910                kind: Sigil::Array,
8911            } => {
8912                self.compile_expr(expr)?;
8913                self.emit_op(Op::SetSymbolicArrayRef, line, ast);
8914            }
8915            ExprKind::Deref {
8916                expr,
8917                kind: Sigil::Hash,
8918            } => {
8919                self.compile_expr(expr)?;
8920                self.emit_op(Op::SetSymbolicHashRef, line, ast);
8921            }
8922            ExprKind::Deref {
8923                expr,
8924                kind: Sigil::Typeglob,
8925            } => {
8926                self.compile_expr(expr)?;
8927                self.emit_op(Op::SetSymbolicTypeglobRef, line, ast);
8928            }
8929            ExprKind::Typeglob(name) => {
8930                let idx = self.chunk.intern_name(name);
8931                if keep {
8932                    self.emit_op(Op::TypeglobAssignFromValue(idx), line, ast);
8933                } else {
8934                    return Err(CompileError::Unsupported(
8935                        "typeglob assign without keep (internal)".into(),
8936                    ));
8937                }
8938            }
8939            ExprKind::AnonymousListSlice { source, indices } => {
8940                if let ExprKind::Deref {
8941                    expr: inner,
8942                    kind: Sigil::Array,
8943                } = &source.kind
8944                {
8945                    if indices.is_empty() {
8946                        return Err(CompileError::Unsupported(
8947                            "assign to empty list slice (internal)".into(),
8948                        ));
8949                    }
8950                    self.compile_arrow_array_base_expr(inner)?;
8951                    for ix in indices {
8952                        self.compile_array_slice_index_expr(ix)?;
8953                    }
8954                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
8955                    if keep {
8956                        self.compile_arrow_array_base_expr(inner)?;
8957                        for ix in indices {
8958                            self.compile_array_slice_index_expr(ix)?;
8959                        }
8960                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
8961                    }
8962                    return Ok(());
8963                }
8964                return Err(CompileError::Unsupported(
8965                    "assign to anonymous list slice (non-@array-deref base)".into(),
8966                ));
8967            }
8968            ExprKind::ArrowDeref {
8969                expr,
8970                index,
8971                kind: DerefKind::Hash,
8972            } => {
8973                self.compile_arrow_hash_base_expr(expr)?;
8974                self.compile_expr(index)?;
8975                if keep {
8976                    self.emit_op(Op::SetArrowHashKeep, line, ast);
8977                } else {
8978                    self.emit_op(Op::SetArrowHash, line, ast);
8979                }
8980            }
8981            ExprKind::ArrowDeref {
8982                expr,
8983                index,
8984                kind: DerefKind::Array,
8985            } => {
8986                if let ExprKind::List(indices) = &index.kind {
8987                    // Multi-index slice assignment: RHS value is already on the stack (pushed
8988                    // by the enclosing `compile_expr(value)` before `compile_assign` was called
8989                    // with keep = true). `SetArrowArraySlice` delegates to
8990                    // `Interpreter::assign_arrow_array_slice` for element-wise write.
8991                    self.compile_arrow_array_base_expr(expr)?;
8992                    for ix in indices {
8993                        self.compile_array_slice_index_expr(ix)?;
8994                    }
8995                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
8996                    if keep {
8997                        // The Set op pops the value; keep callers re-read via a fresh slice read.
8998                        self.compile_arrow_array_base_expr(expr)?;
8999                        for ix in indices {
9000                            self.compile_array_slice_index_expr(ix)?;
9001                        }
9002                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
9003                    }
9004                    return Ok(());
9005                }
9006                if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
9007                    self.compile_arrow_array_base_expr(expr)?;
9008                    self.compile_expr(index)?;
9009                    if keep {
9010                        self.emit_op(Op::SetArrowArrayKeep, line, ast);
9011                    } else {
9012                        self.emit_op(Op::SetArrowArray, line, ast);
9013                    }
9014                } else {
9015                    self.compile_arrow_array_base_expr(expr)?;
9016                    self.compile_array_slice_index_expr(index)?;
9017                    self.emit_op(Op::SetArrowArraySlice(1), line, ast);
9018                    if keep {
9019                        self.compile_arrow_array_base_expr(expr)?;
9020                        self.compile_array_slice_index_expr(index)?;
9021                        self.emit_op(Op::ArrowArraySlice(1), line, ast);
9022                    }
9023                }
9024            }
9025            ExprKind::ArrowDeref {
9026                kind: DerefKind::Call,
9027                ..
9028            } => {
9029                return Err(CompileError::Unsupported(
9030                    "Assign to arrow call deref".into(),
9031                ));
9032            }
9033            ExprKind::HashSliceDeref { container, keys } => {
9034                self.compile_expr(container)?;
9035                for key_expr in keys {
9036                    self.compile_hash_slice_key_expr(key_expr)?;
9037                }
9038                self.emit_op(Op::SetHashSliceDeref(keys.len() as u16), line, ast);
9039            }
9040            ExprKind::Pos(inner) => {
9041                let Some(inner_e) = inner.as_ref() else {
9042                    return Err(CompileError::Unsupported(
9043                        "assign to pos() without scalar".into(),
9044                    ));
9045                };
9046                if keep {
9047                    self.emit_op(Op::Dup, line, ast);
9048                }
9049                match &inner_e.kind {
9050                    ExprKind::ScalarVar(name) => {
9051                        let stor = self.scalar_storage_name_for_ops(name);
9052                        let idx = self.chunk.add_constant(StrykeValue::string(stor));
9053                        self.emit_op(Op::LoadConst(idx), line, ast);
9054                    }
9055                    _ => {
9056                        self.compile_expr(inner_e)?;
9057                    }
9058                }
9059                self.emit_op(Op::SetRegexPos, line, ast);
9060            }
9061            // List assignment: `($a, $b) = (val1, val2)` — RHS is on stack as array,
9062            // store into temp, then distribute elements to each target.
9063            ExprKind::List(targets) => {
9064                let tmp = self.chunk.intern_name("__list_assign_swap__");
9065                self.emit_op(Op::DeclareArray(tmp), line, ast);
9066                for (i, t) in targets.iter().enumerate() {
9067                    self.emit_op(Op::LoadInt(i as i64), line, ast);
9068                    self.emit_op(Op::GetArrayElem(tmp), line, ast);
9069                    self.compile_assign(t, line, false, ast)?;
9070                }
9071                if keep {
9072                    self.emit_op(Op::GetArray(tmp), line, ast);
9073                }
9074            }
9075            _ => {
9076                return Err(CompileError::Unsupported("Assign to complex lvalue".into()));
9077            }
9078        }
9079        Ok(())
9080    }
9081}
9082
9083/// Map a binary op to its stack opcode for compound assignment on aggregates (`$a[$i]`, `$h{$k}`).
9084pub(crate) fn binop_to_vm_op(op: BinOp) -> Option<Op> {
9085    Some(match op {
9086        BinOp::Add => Op::Add,
9087        BinOp::Sub => Op::Sub,
9088        BinOp::Mul => Op::Mul,
9089        BinOp::Div => Op::Div,
9090        BinOp::Mod => Op::Mod,
9091        BinOp::Pow => Op::Pow,
9092        BinOp::Concat => Op::Concat,
9093        BinOp::BitAnd => Op::BitAnd,
9094        BinOp::BitOr => Op::BitOr,
9095        BinOp::BitXor => Op::BitXor,
9096        BinOp::ShiftLeft => Op::Shl,
9097        BinOp::ShiftRight => Op::Shr,
9098        _ => return None,
9099    })
9100}
9101
9102/// Encode/decode scalar compound ops for [`Op::ScalarCompoundAssign`].
9103pub(crate) fn scalar_compound_op_to_byte(op: BinOp) -> Option<u8> {
9104    Some(match op {
9105        BinOp::Add => 0,
9106        BinOp::Sub => 1,
9107        BinOp::Mul => 2,
9108        BinOp::Div => 3,
9109        BinOp::Mod => 4,
9110        BinOp::Pow => 5,
9111        BinOp::Concat => 6,
9112        BinOp::BitAnd => 7,
9113        BinOp::BitOr => 8,
9114        BinOp::BitXor => 9,
9115        BinOp::ShiftLeft => 10,
9116        BinOp::ShiftRight => 11,
9117        _ => return None,
9118    })
9119}
9120
9121pub(crate) fn scalar_compound_op_from_byte(b: u8) -> Option<BinOp> {
9122    Some(match b {
9123        0 => BinOp::Add,
9124        1 => BinOp::Sub,
9125        2 => BinOp::Mul,
9126        3 => BinOp::Div,
9127        4 => BinOp::Mod,
9128        5 => BinOp::Pow,
9129        6 => BinOp::Concat,
9130        7 => BinOp::BitAnd,
9131        8 => BinOp::BitOr,
9132        9 => BinOp::BitXor,
9133        10 => BinOp::ShiftLeft,
9134        11 => BinOp::ShiftRight,
9135        _ => return None,
9136    })
9137}
9138
9139#[cfg(test)]
9140mod tests {
9141    use super::*;
9142    use crate::bytecode::{BuiltinId, Op, GP_RUN};
9143    use crate::parse;
9144
9145    fn compile_snippet(code: &str) -> Result<Chunk, CompileError> {
9146        let program = parse(code).expect("parse snippet");
9147        Compiler::new().compile_program(&program)
9148    }
9149
9150    fn assert_last_halt(chunk: &Chunk) {
9151        assert!(
9152            matches!(chunk.ops.last(), Some(Op::Halt)),
9153            "expected Halt last, got {:?}",
9154            chunk.ops.last()
9155        );
9156    }
9157
9158    #[test]
9159    fn compile_empty_program_emits_run_phase_then_halt() {
9160        let chunk = compile_snippet("").expect("compile");
9161        assert_eq!(chunk.ops.len(), 2);
9162        assert!(matches!(chunk.ops[0], Op::SetGlobalPhase(p) if p == GP_RUN));
9163        assert!(matches!(chunk.ops[1], Op::Halt));
9164    }
9165
9166    #[test]
9167    fn compile_struct_method_fn_eq_shorthand() {
9168        compile_snippet("struct CmpS { fn triple($n) = $n * 3 }").expect("compile");
9169    }
9170
9171    #[test]
9172    fn compile_integer_literal_statement() {
9173        let chunk = compile_snippet("42;").expect("compile");
9174        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadInt(42))));
9175        assert_last_halt(&chunk);
9176    }
9177
9178    #[test]
9179    fn compile_pos_assign_emits_set_regex_pos() {
9180        let chunk = compile_snippet(r#"$_ = ""; pos = 3;"#).expect("compile");
9181        assert!(
9182            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
9183            "expected SetRegexPos in {:?}",
9184            chunk.ops
9185        );
9186    }
9187
9188    #[test]
9189    fn compile_pos_deref_scalar_assign_emits_set_regex_pos() {
9190        let chunk = compile_snippet(
9191            r#"no strict 'vars';
9192            my $s;
9193            my $r = \$s;
9194            pos $$r = 0;"#,
9195        )
9196        .expect("compile");
9197        assert!(
9198            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
9199            r"expected SetRegexPos for pos $$r =, got {:?}",
9200            chunk.ops
9201        );
9202    }
9203
9204    #[test]
9205    fn compile_map_expr_comma_emits_map_with_expr() {
9206        let chunk = compile_snippet(
9207            r#"no strict 'vars';
9208            (map $_ + 1, (4, 5)) |> join ','"#,
9209        )
9210        .expect("compile");
9211        assert!(
9212            chunk.ops.iter().any(|o| matches!(o, Op::MapWithExpr(_))),
9213            "expected MapWithExpr, got {:?}",
9214            chunk.ops
9215        );
9216    }
9217
9218    #[test]
9219    fn compile_hash_slice_deref_assign_emits_set_op() {
9220        let code = r#"no strict 'vars';
9221        my $h = { "a" => 1, "b" => 2 };
9222        my $r = $h;
9223        @$r{"a", "b"} = (10, 20);
9224        $r->{"a"} . "," . $r->{"b"};"#;
9225        let chunk = compile_snippet(code).expect("compile");
9226        assert!(
9227            chunk
9228                .ops
9229                .iter()
9230                .any(|o| matches!(o, Op::SetHashSliceDeref(n) if *n == 2)),
9231            "expected SetHashSliceDeref(2), got {:?}",
9232            chunk.ops
9233        );
9234    }
9235
9236    #[test]
9237    fn compile_bare_array_assign_diamond_uses_readline_list() {
9238        let chunk = compile_snippet("@a = <>;").expect("compile");
9239        assert!(
9240            chunk.ops.iter().any(|o| matches!(
9241                o,
9242                Op::CallBuiltin(bid, 0) if *bid == BuiltinId::ReadLineList as u16
9243            )),
9244            "expected ReadLineList for bare @a = <>, got {:?}",
9245            chunk.ops
9246        );
9247    }
9248
9249    #[test]
9250    fn compile_float_literal() {
9251        let chunk = compile_snippet("3.25;").expect("compile");
9252        assert!(chunk
9253            .ops
9254            .iter()
9255            .any(|o| matches!(o, Op::LoadFloat(f) if (*f - 3.25).abs() < 1e-9)));
9256        assert_last_halt(&chunk);
9257    }
9258
9259    #[test]
9260    fn compile_addition() {
9261        let chunk = compile_snippet("1 + 2;").expect("compile");
9262        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Add)));
9263        assert_last_halt(&chunk);
9264    }
9265
9266    #[test]
9267    fn compile_sub_mul_div_mod_pow() {
9268        for (src, op) in [
9269            ("10 - 3;", "Sub"),
9270            ("6 * 7;", "Mul"),
9271            ("8 / 2;", "Div"),
9272            ("9 % 4;", "Mod"),
9273            ("2 ** 8;", "Pow"),
9274        ] {
9275            let chunk = compile_snippet(src).expect(src);
9276            assert!(
9277                chunk.ops.iter().any(|o| std::mem::discriminant(o) == {
9278                    let dummy = match op {
9279                        "Sub" => Op::Sub,
9280                        "Mul" => Op::Mul,
9281                        "Div" => Op::Div,
9282                        "Mod" => Op::Mod,
9283                        "Pow" => Op::Pow,
9284                        _ => unreachable!(),
9285                    };
9286                    std::mem::discriminant(&dummy)
9287                }),
9288                "{} missing {:?}",
9289                src,
9290                op
9291            );
9292            assert_last_halt(&chunk);
9293        }
9294    }
9295
9296    #[test]
9297    fn compile_string_literal_uses_constant_pool() {
9298        let chunk = compile_snippet(r#""hello";"#).expect("compile");
9299        assert!(chunk
9300            .constants
9301            .iter()
9302            .any(|c| c.as_str().as_deref() == Some("hello")));
9303        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadConst(_))));
9304        assert_last_halt(&chunk);
9305    }
9306
9307    #[test]
9308    fn compile_substitution_bind_emits_regex_subst() {
9309        let chunk = compile_snippet(r#"my $s = "aa"; $s =~ s/a/b/g;"#).expect("compile");
9310        assert!(
9311            chunk
9312                .ops
9313                .iter()
9314                .any(|o| matches!(o, Op::RegexSubst(_, _, _, _))),
9315            "expected RegexSubst in {:?}",
9316            chunk.ops
9317        );
9318        assert!(!chunk.lvalues.is_empty());
9319    }
9320
9321    #[test]
9322    fn compile_chomp_emits_chomp_in_place() {
9323        let chunk = compile_snippet(r#"my $s = "x\n"; chomp $s;"#).expect("compile");
9324        assert!(
9325            chunk.ops.iter().any(|o| matches!(o, Op::ChompInPlace(_))),
9326            "expected ChompInPlace, got {:?}",
9327            chunk.ops
9328        );
9329    }
9330
9331    #[test]
9332    fn compile_transliterate_bind_emits_regex_transliterate() {
9333        let chunk = compile_snippet(r#"my $u = "abc"; $u =~ tr/a-z/A-Z/;"#).expect("compile");
9334        assert!(
9335            chunk
9336                .ops
9337                .iter()
9338                .any(|o| matches!(o, Op::RegexTransliterate(_, _, _, _))),
9339            "expected RegexTransliterate in {:?}",
9340            chunk.ops
9341        );
9342        assert!(!chunk.lvalues.is_empty());
9343    }
9344
9345    #[test]
9346    fn compile_negation() {
9347        let chunk = compile_snippet("-7;").expect("compile");
9348        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Negate)));
9349        assert_last_halt(&chunk);
9350    }
9351
9352    #[test]
9353    fn compile_my_scalar_declares() {
9354        let chunk = compile_snippet("my $x = 1;").expect("compile");
9355        assert!(chunk
9356            .ops
9357            .iter()
9358            .any(|o| matches!(o, Op::DeclareScalar(_) | Op::DeclareScalarSlot(_, _))));
9359        assert_last_halt(&chunk);
9360    }
9361
9362    #[test]
9363    fn compile_scalar_fetch_and_assign() {
9364        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
9365        assert!(
9366            chunk
9367                .ops
9368                .iter()
9369                .filter(|o| matches!(
9370                    o,
9371                    Op::GetScalar(_) | Op::GetScalarPlain(_) | Op::GetScalarSlot(_)
9372                ))
9373                .count()
9374                >= 1
9375        );
9376        assert_last_halt(&chunk);
9377    }
9378
9379    #[test]
9380    fn compile_plain_scalar_read_emits_get_scalar_plain() {
9381        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
9382        assert!(
9383            chunk
9384                .ops
9385                .iter()
9386                .any(|o| matches!(o, Op::GetScalarPlain(_) | Op::GetScalarSlot(_))),
9387            "expected GetScalarPlain or GetScalarSlot for non-special $a, ops={:?}",
9388            chunk.ops
9389        );
9390    }
9391
9392    #[test]
9393    fn compile_sub_postfix_inc_emits_post_inc_slot() {
9394        let chunk = compile_snippet("fn foo { my $x = 0; $x++; return $x; }").expect("compile");
9395        assert!(
9396            chunk.ops.iter().any(|o| matches!(o, Op::PostIncSlot(_))),
9397            "expected PostIncSlot in compiled sub body, ops={:?}",
9398            chunk.ops
9399        );
9400    }
9401
9402    #[test]
9403    fn compile_comparison_ops_numeric() {
9404        for src in [
9405            "1 < 2;", "1 > 2;", "1 <= 2;", "1 >= 2;", "1 == 2;", "1 != 2;",
9406        ] {
9407            let chunk = compile_snippet(src).expect(src);
9408            assert!(
9409                chunk.ops.iter().any(|o| {
9410                    matches!(
9411                        o,
9412                        Op::NumLt | Op::NumGt | Op::NumLe | Op::NumGe | Op::NumEq | Op::NumNe
9413                    )
9414                }),
9415                "{}",
9416                src
9417            );
9418            assert_last_halt(&chunk);
9419        }
9420    }
9421
9422    #[test]
9423    fn compile_string_compare_ops() {
9424        for src in [
9425            r#"'a' lt 'b';"#,
9426            r#"'a' gt 'b';"#,
9427            r#"'a' le 'b';"#,
9428            r#"'a' ge 'b';"#,
9429        ] {
9430            let chunk = compile_snippet(src).expect(src);
9431            assert!(
9432                chunk
9433                    .ops
9434                    .iter()
9435                    .any(|o| matches!(o, Op::StrLt | Op::StrGt | Op::StrLe | Op::StrGe)),
9436                "{}",
9437                src
9438            );
9439            assert_last_halt(&chunk);
9440        }
9441    }
9442
9443    #[test]
9444    fn compile_concat() {
9445        let chunk = compile_snippet(r#"'a' . 'b';"#).expect("compile");
9446        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Concat)));
9447        assert_last_halt(&chunk);
9448    }
9449
9450    #[test]
9451    fn compile_bitwise_ops() {
9452        let chunk = compile_snippet("1 & 2 | 3 ^ 4;").expect("compile");
9453        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitAnd)));
9454        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitOr)));
9455        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitXor)));
9456        assert_last_halt(&chunk);
9457    }
9458
9459    #[test]
9460    fn compile_shift_right() {
9461        let chunk = compile_snippet("8 >> 1;").expect("compile");
9462        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shr)));
9463        assert_last_halt(&chunk);
9464    }
9465
9466    #[test]
9467    fn compile_shift_left() {
9468        let chunk = compile_snippet("1 << 4;").expect("compile");
9469        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shl)));
9470        assert_last_halt(&chunk);
9471    }
9472
9473    #[test]
9474    fn compile_log_not_and_bit_not() {
9475        let c1 = compile_snippet("!0;").expect("compile");
9476        assert!(c1.ops.iter().any(|o| matches!(o, Op::LogNot)));
9477        let c2 = compile_snippet("~0;").expect("compile");
9478        assert!(c2.ops.iter().any(|o| matches!(o, Op::BitNot)));
9479    }
9480
9481    #[test]
9482    fn compile_sub_registers_name_and_entry() {
9483        let chunk = compile_snippet("fn foo { return 1; }").expect("compile");
9484        assert!(chunk.names.iter().any(|n| n == "foo"));
9485        assert!(chunk
9486            .sub_entries
9487            .iter()
9488            .any(|&(idx, ip, _)| chunk.names[idx as usize] == "foo" && ip > 0));
9489        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Halt)));
9490        assert!(chunk.ops.iter().any(|o| matches!(o, Op::ReturnValue)));
9491    }
9492
9493    #[test]
9494    fn compile_postinc_scalar() {
9495        let chunk = compile_snippet("my $n = 1; $n++;").expect("compile");
9496        assert!(chunk
9497            .ops
9498            .iter()
9499            .any(|o| matches!(o, Op::PostInc(_) | Op::PostIncSlot(_))));
9500        assert_last_halt(&chunk);
9501    }
9502
9503    #[test]
9504    fn compile_preinc_scalar() {
9505        let chunk = compile_snippet("my $n = 1; ++$n;").expect("compile");
9506        assert!(chunk
9507            .ops
9508            .iter()
9509            .any(|o| matches!(o, Op::PreInc(_) | Op::PreIncSlot(_))));
9510        assert_last_halt(&chunk);
9511    }
9512
9513    #[test]
9514    fn compile_if_expression_value() {
9515        let chunk = compile_snippet("if (1) { 2 } else { 3 }").expect("compile");
9516        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
9517        assert_last_halt(&chunk);
9518    }
9519
9520    #[test]
9521    fn compile_unless_expression_value() {
9522        let chunk = compile_snippet("unless (0) { 1 } else { 2 }").expect("compile");
9523        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
9524        assert_last_halt(&chunk);
9525    }
9526
9527    #[test]
9528    fn compile_array_declare_and_push() {
9529        let chunk = compile_snippet("my @a; push @a, 1;").expect("compile");
9530        assert!(chunk.ops.iter().any(|o| matches!(o, Op::DeclareArray(_))));
9531        assert_last_halt(&chunk);
9532    }
9533
9534    #[test]
9535    fn compile_ternary() {
9536        let chunk = compile_snippet("1 ? 2 : 3;").expect("compile");
9537        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
9538        assert_last_halt(&chunk);
9539    }
9540
9541    #[test]
9542    fn compile_repeat_operator() {
9543        let chunk = compile_snippet(r#"'ab' x 3;"#).expect("compile");
9544        assert!(chunk.ops.iter().any(|o| matches!(o, Op::StringRepeat)));
9545        assert_last_halt(&chunk);
9546    }
9547
9548    #[test]
9549    fn compile_range_to_array() {
9550        let chunk = compile_snippet("my @a = (1..3);").expect("compile");
9551        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Range)));
9552        assert_last_halt(&chunk);
9553    }
9554
9555    /// Scalar `..` / `...` in a boolean condition must be the flip-flop (`$.`), not a list range.
9556    #[test]
9557    fn compile_print_if_uses_scalar_flipflop_not_range_list() {
9558        let chunk = compile_snippet("print if 1..2;").expect("compile");
9559        assert!(
9560            chunk
9561                .ops
9562                .iter()
9563                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 0))),
9564            "expected ScalarFlipFlop in bytecode, got:\n{}",
9565            chunk.disassemble()
9566        );
9567        assert!(
9568            !chunk.ops.iter().any(|o| matches!(o, Op::Range)),
9569            "did not expect list Range op in scalar if-condition:\n{}",
9570            chunk.disassemble()
9571        );
9572    }
9573
9574    #[test]
9575    fn compile_print_if_three_dot_scalar_flipflop_sets_exclusive_flag() {
9576        let chunk = compile_snippet("print if 1...2;").expect("compile");
9577        assert!(
9578            chunk
9579                .ops
9580                .iter()
9581                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 1))),
9582            "expected ScalarFlipFlop(..., exclusive=1), got:\n{}",
9583            chunk.disassemble()
9584        );
9585    }
9586
9587    #[test]
9588    fn compile_regex_flipflop_two_dot_emits_regex_flipflop_op() {
9589        let chunk = compile_snippet(r#"print if /a/../b/;"#).expect("compile");
9590        assert!(
9591            chunk
9592                .ops
9593                .iter()
9594                .any(|o| matches!(o, Op::RegexFlipFlop(_, 0, _, _, _, _))),
9595            "expected RegexFlipFlop(.., exclusive=0), got:\n{}",
9596            chunk.disassemble()
9597        );
9598        assert!(
9599            !chunk
9600                .ops
9601                .iter()
9602                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9603            "regex flip-flop must not use ScalarFlipFlop:\n{}",
9604            chunk.disassemble()
9605        );
9606    }
9607
9608    #[test]
9609    fn compile_regex_flipflop_three_dot_sets_exclusive_flag() {
9610        let chunk = compile_snippet(r#"print if /a/.../b/;"#).expect("compile");
9611        assert!(
9612            chunk
9613                .ops
9614                .iter()
9615                .any(|o| matches!(o, Op::RegexFlipFlop(_, 1, _, _, _, _))),
9616            "expected RegexFlipFlop(..., exclusive=1), got:\n{}",
9617            chunk.disassemble()
9618        );
9619    }
9620
9621    #[test]
9622    fn compile_regex_eof_flipflop_emits_regex_eof_flipflop_op() {
9623        let chunk = compile_snippet(r#"print if /a/..eof;"#).expect("compile");
9624        assert!(
9625            chunk
9626                .ops
9627                .iter()
9628                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 0, _, _))),
9629            "expected RegexEofFlipFlop(.., exclusive=0), got:\n{}",
9630            chunk.disassemble()
9631        );
9632        assert!(
9633            !chunk
9634                .ops
9635                .iter()
9636                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9637            "regex/eof flip-flop must not use ScalarFlipFlop:\n{}",
9638            chunk.disassemble()
9639        );
9640    }
9641
9642    #[test]
9643    fn compile_regex_eof_flipflop_three_dot_sets_exclusive_flag() {
9644        let chunk = compile_snippet(r#"print if /a/...eof;"#).expect("compile");
9645        assert!(
9646            chunk
9647                .ops
9648                .iter()
9649                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 1, _, _))),
9650            "expected RegexEofFlipFlop(..., exclusive=1), got:\n{}",
9651            chunk.disassemble()
9652        );
9653    }
9654
9655    #[test]
9656    fn compile_regex_flipflop_compound_rhs_emits_regex_flip_flop_expr_rhs() {
9657        let chunk = compile_snippet(r#"print if /a/...(/b/ or /c/);"#).expect("compile");
9658        assert!(
9659            chunk
9660                .ops
9661                .iter()
9662                .any(|o| matches!(o, Op::RegexFlipFlopExprRhs(_, _, _, _, _))),
9663            "expected RegexFlipFlopExprRhs for compound RHS, got:\n{}",
9664            chunk.disassemble()
9665        );
9666        assert!(
9667            !chunk
9668                .ops
9669                .iter()
9670                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9671            "compound regex flip-flop must not use ScalarFlipFlop:\n{}",
9672            chunk.disassemble()
9673        );
9674    }
9675
9676    #[test]
9677    fn compile_print_statement() {
9678        let chunk = compile_snippet("print 1;").expect("compile");
9679        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Print(_, _))));
9680        assert_last_halt(&chunk);
9681    }
9682
9683    #[test]
9684    fn compile_defined_builtin() {
9685        let chunk = compile_snippet("defined 1;").expect("compile");
9686        assert!(chunk
9687            .ops
9688            .iter()
9689            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Defined as u16)));
9690        assert_last_halt(&chunk);
9691    }
9692
9693    #[test]
9694    fn compile_length_builtin() {
9695        let chunk = compile_snippet("length 'abc';").expect("compile");
9696        assert!(chunk
9697            .ops
9698            .iter()
9699            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Length as u16)));
9700        assert_last_halt(&chunk);
9701    }
9702
9703    #[test]
9704    fn compile_complex_expr_parentheses() {
9705        let chunk = compile_snippet("(1 + 2) * (3 + 4);").expect("compile");
9706        assert!(chunk.ops.iter().filter(|o| matches!(o, Op::Add)).count() >= 2);
9707        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Mul)));
9708        assert_last_halt(&chunk);
9709    }
9710
9711    #[test]
9712    fn compile_undef_literal() {
9713        let chunk = compile_snippet("undef;").expect("compile");
9714        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadUndef)));
9715        assert_last_halt(&chunk);
9716    }
9717
9718    #[test]
9719    fn compile_empty_statement_semicolons() {
9720        let chunk = compile_snippet(";;;").expect("compile");
9721        assert_last_halt(&chunk);
9722    }
9723
9724    #[test]
9725    fn compile_array_elem_preinc_uses_rot_and_set_elem() {
9726        let chunk = compile_snippet("my @a; $a[0] = 0; ++$a[0];").expect("compile");
9727        assert!(
9728            chunk.ops.iter().any(|o| matches!(o, Op::Rot)),
9729            "expected Rot in {:?}",
9730            chunk.ops
9731        );
9732        assert!(
9733            chunk.ops.iter().any(|o| matches!(o, Op::SetArrayElem(_))),
9734            "expected SetArrayElem in {:?}",
9735            chunk.ops
9736        );
9737        assert_last_halt(&chunk);
9738    }
9739
9740    #[test]
9741    fn compile_hash_elem_compound_assign_uses_rot() {
9742        let chunk = compile_snippet("my %h; $h{0} = 1; $h{0} += 2;").expect("compile");
9743        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
9744        assert!(
9745            chunk.ops.iter().any(|o| matches!(o, Op::SetHashElem(_))),
9746            "expected SetHashElem"
9747        );
9748        assert_last_halt(&chunk);
9749    }
9750
9751    #[test]
9752    fn compile_postfix_inc_array_elem_emits_rot() {
9753        let chunk = compile_snippet("my @a; $a[1] = 5; $a[1]++;").expect("compile");
9754        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
9755        assert_last_halt(&chunk);
9756    }
9757
9758    #[test]
9759    fn compile_tie_stmt_emits_op_tie() {
9760        let chunk = compile_snippet("tie %h, 'Pkg';").expect("compile");
9761        assert!(
9762            chunk.ops.iter().any(|o| matches!(o, Op::Tie { .. })),
9763            "expected Op::Tie in {:?}",
9764            chunk.ops
9765        );
9766        assert_last_halt(&chunk);
9767    }
9768
9769    #[test]
9770    fn compile_format_decl_emits_format_decl_op() {
9771        let chunk = compile_snippet(
9772            r#"
9773format FMT =
9774literal line
9775.
97761;
9777"#,
9778        )
9779        .expect("compile");
9780        assert!(
9781            chunk.ops.iter().any(|o| matches!(o, Op::FormatDecl(0))),
9782            "expected Op::FormatDecl(0), got {:?}",
9783            chunk.ops
9784        );
9785        assert_eq!(chunk.format_decls.len(), 1);
9786        assert_eq!(chunk.format_decls[0].0, "FMT");
9787        assert_eq!(chunk.format_decls[0].1, vec!["literal line".to_string()]);
9788        assert_last_halt(&chunk);
9789    }
9790
9791    #[test]
9792    fn compile_interpolated_string_scalar_only_emits_empty_prefix_and_concat() {
9793        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "$x";"#).expect("compile");
9794        let empty_idx = chunk
9795            .constants
9796            .iter()
9797            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9798            .expect("empty string in pool") as u16;
9799        assert!(
9800            chunk
9801                .ops
9802                .iter()
9803                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9804            "expected LoadConst(\"\"), ops={:?}",
9805            chunk.ops
9806        );
9807        assert!(
9808            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9809            "expected Op::Concat for qq with only a scalar part, ops={:?}",
9810            chunk.ops
9811        );
9812        assert_last_halt(&chunk);
9813    }
9814
9815    #[test]
9816    fn compile_interpolated_string_array_only_emits_stringify_and_concat() {
9817        let chunk = compile_snippet(r#"no strict 'vars'; my @a = (1, 2); "@a";"#).expect("compile");
9818        let empty_idx = chunk
9819            .constants
9820            .iter()
9821            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9822            .expect("empty string in pool") as u16;
9823        assert!(
9824            chunk
9825                .ops
9826                .iter()
9827                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9828            "expected LoadConst(\"\"), ops={:?}",
9829            chunk.ops
9830        );
9831        assert!(
9832            chunk
9833                .ops
9834                .iter()
9835                .any(|o| matches!(o, Op::ArrayStringifyListSep)),
9836            "expected ArrayStringifyListSep for array var in qq, ops={:?}",
9837            chunk.ops
9838        );
9839        assert!(
9840            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9841            "expected Op::Concat after array stringify, ops={:?}",
9842            chunk.ops
9843        );
9844        assert_last_halt(&chunk);
9845    }
9846
9847    #[test]
9848    fn compile_interpolated_string_hash_element_only_emits_empty_prefix_and_concat() {
9849        let chunk =
9850            compile_snippet(r#"no strict 'vars'; my %h = (k => 1); "$h{k}";"#).expect("compile");
9851        let empty_idx = chunk
9852            .constants
9853            .iter()
9854            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9855            .expect("empty string in pool") as u16;
9856        assert!(
9857            chunk
9858                .ops
9859                .iter()
9860                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9861            "expected LoadConst(\"\"), ops={:?}",
9862            chunk.ops
9863        );
9864        assert!(
9865            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9866            "expected Op::Concat for qq with only an expr part, ops={:?}",
9867            chunk.ops
9868        );
9869        assert_last_halt(&chunk);
9870    }
9871
9872    #[test]
9873    fn compile_interpolated_string_leading_literal_has_no_empty_string_prefix() {
9874        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "a$x";"#).expect("compile");
9875        assert!(
9876            !chunk
9877                .constants
9878                .iter()
9879                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
9880            "literal-first qq must not intern \"\" (only non-literal first parts need it), ops={:?}",
9881            chunk.ops
9882        );
9883        assert!(
9884            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9885            "expected Op::Concat after literal + scalar, ops={:?}",
9886            chunk.ops
9887        );
9888        assert_last_halt(&chunk);
9889    }
9890
9891    #[test]
9892    fn compile_interpolated_string_two_scalars_empty_prefix_and_two_concats() {
9893        let chunk =
9894            compile_snippet(r#"no strict 'vars'; my $a = 1; my $b = 2; "$a$b";"#).expect("compile");
9895        let empty_idx = chunk
9896            .constants
9897            .iter()
9898            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9899            .expect("empty string in pool") as u16;
9900        assert!(
9901            chunk
9902                .ops
9903                .iter()
9904                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9905            "expected LoadConst(\"\") before first scalar qq part, ops={:?}",
9906            chunk.ops
9907        );
9908        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9909        assert!(
9910            n_concat >= 2,
9911            "expected at least two Op::Concat for two scalar qq parts, got {} in {:?}",
9912            n_concat,
9913            chunk.ops
9914        );
9915        assert_last_halt(&chunk);
9916    }
9917
9918    #[test]
9919    fn compile_interpolated_string_literal_then_two_scalars_has_no_empty_prefix() {
9920        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 7; my $y = 8; "p$x$y";"#)
9921            .expect("compile");
9922        assert!(
9923            !chunk
9924                .constants
9925                .iter()
9926                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
9927            "literal-first qq must not intern empty string, ops={:?}",
9928            chunk.ops
9929        );
9930        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9931        assert!(
9932            n_concat >= 2,
9933            "expected two Concats for literal + two scalars, got {} in {:?}",
9934            n_concat,
9935            chunk.ops
9936        );
9937        assert_last_halt(&chunk);
9938    }
9939
9940    #[test]
9941    fn compile_interpolated_string_braced_scalar_trailing_literal_emits_concats() {
9942        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "a${u}z";"#).expect("compile");
9943        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9944        assert!(
9945            n_concat >= 2,
9946            "expected braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
9947            n_concat,
9948            chunk.ops
9949        );
9950        assert_last_halt(&chunk);
9951    }
9952
9953    #[test]
9954    fn compile_interpolated_string_braced_scalar_sandwiched_emits_concats() {
9955        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "L${u}R";"#).expect("compile");
9956        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9957        assert!(
9958            n_concat >= 2,
9959            "expected leading literal + braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
9960            n_concat,
9961            chunk.ops
9962        );
9963        assert_last_halt(&chunk);
9964    }
9965
9966    #[test]
9967    fn compile_interpolated_string_mixed_braced_and_plain_scalars_emits_concats() {
9968        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; my $y = 2; "a${x}b$y";"#)
9969            .expect("compile");
9970        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9971        assert!(
9972            n_concat >= 3,
9973            "expected literal/braced/plain qq mix to use at least three Concats, got {} in {:?}",
9974            n_concat,
9975            chunk.ops
9976        );
9977        assert_last_halt(&chunk);
9978    }
9979
9980    #[test]
9981    fn compile_use_overload_emits_use_overload_op() {
9982        let chunk = compile_snippet(r#"use overload '""' => 'as_string';"#).expect("compile");
9983        assert!(
9984            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9985            "expected Op::UseOverload(0), got {:?}",
9986            chunk.ops
9987        );
9988        assert_eq!(chunk.use_overload_entries.len(), 1);
9989        // Perl `'""'` is a single-quoted string whose contents are two `"` characters — the
9990        // overload table key for stringify (see [`Interpreter::overload_stringify_method`]).
9991        let stringify_key: String = ['"', '"'].iter().collect();
9992        assert_eq!(
9993            chunk.use_overload_entries[0],
9994            vec![(stringify_key, "as_string".to_string())]
9995        );
9996        assert_last_halt(&chunk);
9997    }
9998
9999    #[test]
10000    fn compile_use_overload_empty_list_emits_use_overload_with_no_pairs() {
10001        let chunk = compile_snippet(r#"use overload ();"#).expect("compile");
10002        assert!(
10003            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
10004            "expected Op::UseOverload(0), got {:?}",
10005            chunk.ops
10006        );
10007        assert_eq!(chunk.use_overload_entries.len(), 1);
10008        assert!(chunk.use_overload_entries[0].is_empty());
10009        assert_last_halt(&chunk);
10010    }
10011
10012    #[test]
10013    fn compile_use_overload_multiple_pairs_single_op() {
10014        let chunk =
10015            compile_snippet(r#"use overload '+' => 'p_add', '-' => 'p_sub';"#).expect("compile");
10016        assert!(
10017            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
10018            "expected Op::UseOverload(0), got {:?}",
10019            chunk.ops
10020        );
10021        assert_eq!(chunk.use_overload_entries.len(), 1);
10022        assert_eq!(
10023            chunk.use_overload_entries[0],
10024            vec![
10025                ("+".to_string(), "p_add".to_string()),
10026                ("-".to_string(), "p_sub".to_string()),
10027            ]
10028        );
10029        assert_last_halt(&chunk);
10030    }
10031
10032    #[test]
10033    fn compile_open_my_fh_emits_declare_open_set() {
10034        let chunk = compile_snippet(r#"open my $fh, "<", "/dev/null";"#).expect("compile");
10035        assert!(
10036            chunk.ops.iter().any(|o| matches!(
10037                o,
10038                Op::CallBuiltin(b, 3) if *b == BuiltinId::Open as u16
10039            )),
10040            "expected Open builtin 3-arg, got {:?}",
10041            chunk.ops
10042        );
10043        // Post-fix: `open my $fh, …` initializes `$fh` to the literal handle
10044        // NAME via DeclareScalar*/DeclareScalarSlot BEFORE the Open call,
10045        // not via SetScalarKeepPlain AFTER. The bool return of `open` stays
10046        // on stack as the expression value (so `open(...) or die` works).
10047        // SetScalarKeepPlain is no longer expected.
10048        assert!(
10049            !chunk
10050                .ops
10051                .iter()
10052                .any(|o| matches!(o, Op::SetScalarKeepPlain(_))),
10053            "post-fix: SetScalarKeepPlain must NOT appear — $fh is set BEFORE open(name, mode, file). got {:?}",
10054            chunk.ops
10055        );
10056        assert!(
10057            chunk.ops.iter().any(|o| matches!(
10058                o,
10059                Op::DeclareScalarSlot(..) | Op::DeclareScalar(_) | Op::DeclareScalarFrozen(_)
10060            )),
10061            "expected $fh to be declared (slot or named), got {:?}",
10062            chunk.ops
10063        );
10064        assert_last_halt(&chunk);
10065    }
10066
10067    #[test]
10068    fn compile_local_hash_element_emits_local_declare_hash_element() {
10069        let chunk = compile_snippet(r#"local $SIG{__WARN__} = 0;"#).expect("compile");
10070        assert!(
10071            chunk
10072                .ops
10073                .iter()
10074                .any(|o| matches!(o, Op::LocalDeclareHashElement(_))),
10075            "expected LocalDeclareHashElement in {:?}",
10076            chunk.ops
10077        );
10078        assert_last_halt(&chunk);
10079    }
10080
10081    #[test]
10082    fn compile_local_array_element_emits_local_declare_array_element() {
10083        let chunk = compile_snippet(r#"local $a[2] = 9;"#).expect("compile");
10084        assert!(
10085            chunk
10086                .ops
10087                .iter()
10088                .any(|o| matches!(o, Op::LocalDeclareArrayElement(_))),
10089            "expected LocalDeclareArrayElement in {:?}",
10090            chunk.ops
10091        );
10092        assert_last_halt(&chunk);
10093    }
10094
10095    #[test]
10096    fn compile_local_typeglob_emits_local_declare_typeglob() {
10097        let chunk = compile_snippet(r#"local *STDOUT;"#).expect("compile");
10098        assert!(
10099            chunk
10100                .ops
10101                .iter()
10102                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, None))),
10103            "expected LocalDeclareTypeglob(_, None) in {:?}",
10104            chunk.ops
10105        );
10106        assert_last_halt(&chunk);
10107    }
10108
10109    #[test]
10110    fn compile_local_typeglob_alias_emits_local_declare_typeglob_some_rhs() {
10111        let chunk = compile_snippet(r#"local *FOO = *STDOUT;"#).expect("compile");
10112        assert!(
10113            chunk
10114                .ops
10115                .iter()
10116                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, Some(_)))),
10117            "expected LocalDeclareTypeglob with rhs in {:?}",
10118            chunk.ops
10119        );
10120        assert_last_halt(&chunk);
10121    }
10122
10123    #[test]
10124    fn compile_local_braced_typeglob_emits_local_declare_typeglob_dynamic() {
10125        let chunk = compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *{ $g };"#)
10126            .expect("compile");
10127        assert!(
10128            chunk
10129                .ops
10130                .iter()
10131                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
10132            "expected LocalDeclareTypeglobDynamic(None) in {:?}",
10133            chunk.ops
10134        );
10135        assert_last_halt(&chunk);
10136    }
10137
10138    #[test]
10139    fn compile_local_star_deref_typeglob_emits_local_declare_typeglob_dynamic() {
10140        let chunk =
10141            compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *$g;"#).expect("compile");
10142        assert!(
10143            chunk
10144                .ops
10145                .iter()
10146                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
10147            "expected LocalDeclareTypeglobDynamic(None) for local *scalar glob in {:?}",
10148            chunk.ops
10149        );
10150        assert_last_halt(&chunk);
10151    }
10152
10153    #[test]
10154    fn compile_braced_glob_assign_to_named_glob_emits_copy_dynamic_lhs() {
10155        // `*{EXPR} = *FOO` — dynamic lhs name + static rhs glob → `CopyTypeglobSlotsDynamicLhs`.
10156        let chunk = compile_snippet(r#"no strict 'refs'; my $n = "x"; *{ $n } = *STDOUT;"#)
10157            .expect("compile");
10158        assert!(
10159            chunk
10160                .ops
10161                .iter()
10162                .any(|o| matches!(o, Op::CopyTypeglobSlotsDynamicLhs(_))),
10163            "expected CopyTypeglobSlotsDynamicLhs in {:?}",
10164            chunk.ops
10165        );
10166        assert_last_halt(&chunk);
10167    }
10168
10169    /// Regression for the class-field line-drift bug at the compiler level.
10170    /// A class body with a typed field used to push every subsequent
10171    /// statement's bytecode `lines[i]` off by +1 per field, so DAP
10172    /// breakpoints set on the post-class lines silently dropped. With the
10173    /// lexer's fat-arrow / qw / ... lookaheads correctly restoring
10174    /// `self.line`, the compiled chunk's `lines[]` matches the source.
10175    #[test]
10176    fn class_field_emits_correct_op_lines_for_subsequent_statements() {
10177        let chunk =
10178            compile_snippet("class Foo {\n    x: Int\n}\nmy $y = 1\np $y\n").expect("compile");
10179        // `my $y = 1` is at source line 4. Find any op with line 4 — it
10180        // proves the compiler emitted bytecode for that statement on the
10181        // correct source line. Before the fix, line 4 would not appear
10182        // (the ops would carry line 5 instead).
10183        let has_line_4 = chunk.lines.contains(&4);
10184        assert!(
10185            has_line_4,
10186            "expected at least one op tagged with source line 4 (my $y = 1), got lines {:?}",
10187            chunk.lines
10188        );
10189        // And `p $y` is at line 5; same regression catch.
10190        let has_line_5 = chunk.lines.contains(&5);
10191        assert!(
10192            has_line_5,
10193            "expected at least one op tagged with source line 5 (p $y), got lines {:?}",
10194            chunk.lines
10195        );
10196        // And the inverse — *no* op should be on line 6 (file only has
10197        // 5 logical lines plus a trailing newline). A line-6 op means the
10198        // counter drifted past the file.
10199        let has_line_6 = chunk.lines.contains(&6);
10200        assert!(
10201            !has_line_6,
10202            "no op should report line 6 in a 5-line file; lines {:?}",
10203            chunk.lines
10204        );
10205    }
10206
10207    /// Same check but for class with *two* typed fields (the original
10208    /// reproduction had 2 fields giving a +2 drift on subsequent ops).
10209    #[test]
10210    fn class_with_two_fields_does_not_drift_op_lines() {
10211        let chunk = compile_snippet(
10212            "class Foo {\n    x: Int\n    y: Int\n}\nmy $a = 1\nmy $b = 2\np $a\np $b\n",
10213        )
10214        .expect("compile");
10215        // `my $a = 1` is at source line 5; `my $b = 2` at line 6;
10216        // `p $a` at line 7; `p $b` at line 8.
10217        for expected in [5, 6, 7, 8] {
10218            assert!(
10219                chunk.lines.contains(&expected),
10220                "expected an op on line {expected}, got {:?}",
10221                chunk.lines
10222            );
10223        }
10224        // And no op past the end of the file.
10225        let max_line = chunk.lines.iter().copied().max().unwrap_or(0);
10226        assert!(
10227            max_line <= 8,
10228            "no op past source line 8: max was {max_line}"
10229        );
10230    }
10231}