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(String),
109    /// Immutable binding reassignment (e.g. `frozen my $x` then `$x = 1`).
110    Frozen {
111        line: usize,
112        detail: String,
113    },
114}
115
116#[derive(Default, Clone)]
117struct ScopeLayer {
118    declared_scalars: HashSet<String>,
119    /// Bare names from `our $x` — rvalue/lvalue ops must use the package stash key (`main::x`).
120    declared_our_scalars: HashSet<String>,
121    declared_arrays: HashSet<String>,
122    declared_hashes: HashSet<String>,
123    frozen_scalars: HashSet<String>,
124    frozen_arrays: HashSet<String>,
125    frozen_hashes: HashSet<String>,
126    /// Slot-index mapping for `my` scalars in compiled subroutines.
127    /// When `use_slots` is true, `my $x` is assigned a u8 slot index
128    /// and the VM accesses it via `GetScalarSlot(idx)` — O(1).
129    scalar_slots: HashMap<String, u8>,
130    next_scalar_slot: u8,
131    /// True when compiling a subroutine body (enables slot assignment).
132    use_slots: bool,
133    /// `mysync @name` — element `++`/`--`/compound assign not yet lowered to bytecode (atomic RMW).
134    mysync_arrays: HashSet<String>,
135    /// `mysync %name` — same as [`Self::mysync_arrays`].
136    mysync_hashes: HashSet<String>,
137    /// `mysync $name` — opt-in shared closure capture (Arc-cell). Closures may
138    /// freely mutate these; default `my` scalars trigger a compile-time error
139    /// if a closure writes to them from inside a sub body. (DESIGN-001)
140    mysync_scalars: HashSet<String>,
141    /// True when this layer was entered for an anonymous-or-named sub body
142    /// (`sub { ... }` / `fn { ... }` / `sub foo { ... }`). Used by the
143    /// closure-write check to detect when a write crosses a sub-body
144    /// boundary (DESIGN-001). False for `map`/`grep`/`sort` block layers
145    /// (those are not closures in the value-snapshot sense).
146    is_sub_body: bool,
147}
148
149/// Loop context for resolving `last`/`next` jumps.
150///
151/// Pushed onto [`Compiler::loop_stack`] at every loop entry so `last`/`next` (including those
152/// nested inside `if`/`unless`/`{ }` blocks) can find the matching loop and patch their jumps.
153///
154/// `entry_frame_depth` is [`Compiler::frame_depth`] at loop entry — `last`/`next` from inside
155/// emits `(frame_depth - entry_frame_depth)` `Op::PopFrame` instructions before jumping so any
156/// `if`/block-pushed scope frames are torn down.
157///
158/// `entry_try_depth` mirrors `try { }` nesting; if a `last`/`next` would have to cross a try
159/// frame the compiler bails to `Unsupported` (try-frame unwind on flow control is not yet
160/// modeled in bytecode — the catch handler would still see the next exception).
161struct LoopCtx {
162    label: Option<String>,
163    entry_frame_depth: usize,
164    entry_try_depth: usize,
165    /// First bytecode IP of the loop **body** (after `while`/`until` condition, after `for` condition,
166    /// after `foreach` assigns `$var` from the list, or `do` body start) — target for `redo`.
167    body_start_ip: usize,
168    /// Positions of `last`/`next` jumps to patch after the loop body is fully compiled.
169    break_jumps: Vec<usize>,
170    /// `Op::Jump(0)` placeholders for `next` — patched to the loop increment / condition entry.
171    continue_jumps: Vec<usize>,
172}
173
174pub struct Compiler {
175    pub chunk: Chunk,
176    /// During compilation: stable [`Expr`] pointer → [`Chunk::ast_expr_pool`] index.
177    ast_expr_intern: HashMap<usize, u32>,
178    pub begin_blocks: Vec<Block>,
179    pub unit_check_blocks: Vec<Block>,
180    pub check_blocks: Vec<Block>,
181    pub init_blocks: Vec<Block>,
182    pub end_blocks: Vec<Block>,
183    /// Lexical `my` declarations per scope frame (mirrors `PushFrame` / sub bodies).
184    scope_stack: Vec<ScopeLayer>,
185    /// Current `package` for stash qualification (`@ISA`, `@EXPORT`, …), matching [`VMHelper::stash_array_name_for_package`].
186    current_package: String,
187    /// Set while compiling the main program body when the last statement must leave its value on the
188    /// stack (implicit return). Enables `try`/`catch` blocks to match `emit_block_value` semantics.
189    program_last_stmt_takes_value: bool,
190    /// Source path for `__FILE__` in bytecode (must match the interpreter's notion of current file when using the VM).
191    pub source_file: String,
192    /// Runtime activation depth — `Op::PushFrame` count minus `Op::PopFrame` count emitted so far.
193    /// Used by `last`/`next` to compute how many frames to pop before jumping.
194    frame_depth: usize,
195    /// `try { }` nesting depth — `last`/`next` cannot currently cross a try-frame in bytecode.
196    try_depth: usize,
197    /// Active loops, innermost at the back. `last`/`next` consult this stack.
198    loop_stack: Vec<LoopCtx>,
199    /// Per-function (top-level program or sub body) `goto LABEL` tracking. Top of the stack holds
200    /// the label→IP map and forward-goto patch list for the innermost enclosing label-scoped
201    /// region. `goto` is only resolved against the top frame (matches Perl's "goto must target a
202    /// label in the same lexical context" intuition).
203    goto_ctx_stack: Vec<GotoCtx>,
204    /// `use strict 'vars'` — reject access to undeclared globals at compile time (mirrors
205    /// `Interpreter::check_strict_*_var` runtime checks). Set via
206    /// [`Self::with_strict_vars`] before `compile_program` runs; stable throughout a single
207    /// compile because `use strict` is resolved in `prepare_program_top_level` before the VM
208    /// compile begins.
209    strict_vars: bool,
210    /// True while compiling a deferred sort/reduce block in the 4th pass. `$a` and `$b`
211    /// inside such blocks must use name-based access — `set_sort_pair` writes by name,
212    /// so a slot allocation from any outer `my $a`/`my $b` (which the deferred pass sees
213    /// because it runs after the whole program is compiled) would silently miss the
214    /// per-iteration value. Other variables continue to use slots so outer `my`
215    /// captures remain O(1).
216    force_name_for_sort_pair: bool,
217    /// Block indices registered via [`Self::register_sort_pair_block`] — sort/reduce
218    /// comparator bodies that bind `$a`/`$b` magic globals. The deferred 4th pass
219    /// turns on [`Self::force_name_for_sort_pair`] only for these blocks.
220    sort_pair_block_indices: std::collections::HashSet<u16>,
221    /// Block indices that came from an anonymous-or-named sub-body
222    /// (`sub { ... }` / `fn { ... }`). The 4th-pass compile pushes a
223    /// `is_sub_body: true` scope layer before compiling these so the
224    /// closure-write check (DESIGN-001) can detect outer-scope `my`
225    /// writes. Map/grep/sort blocks are NOT in this set.
226    sub_body_block_indices: std::collections::HashSet<u16>,
227    /// Per-block signature params (parallel to `chunk.blocks`) used by the
228    /// 4th-pass compile to register CodeRef params in the new sub-body
229    /// scope layer.
230    code_ref_block_params: Vec<Vec<crate::ast::SubSigParam>>,
231    /// Snapshot of `scope_stack` taken when each block was added. The 4th pass
232    /// (`compile_program`) defers map/grep/sort block compilation until after
233    /// every sub body has finished, by which point the parent scope_stack has
234    /// already been popped. Without these snapshots, references like the
235    /// fn-local `my $max` inside `map { … $max … }` would resolve against the
236    /// trailing top-level scope_stack and silently bind to a sibling top-level
237    /// `my $max` slot. The 4th pass swaps the matching snapshot in before
238    /// compiling each block. Index lines up with `chunk.blocks`; entries are
239    /// `None` for blocks added through code paths that don't go through the
240    /// compiler's [`Self::add_deferred_block`] wrapper.
241    block_scope_snapshots: Vec<Option<Vec<ScopeLayer>>>,
242}
243
244/// Label tracking for `goto LABEL` within a single label-scoped region (top-level main program
245/// or subroutine body). See [`Compiler::enter_goto_scope`] / [`Compiler::exit_goto_scope`].
246#[derive(Default)]
247struct GotoCtx {
248    /// `label_name → (bytecode IP of the labeled statement's first op, frame_depth at label)`
249    labels: HashMap<String, (usize, usize)>,
250    /// `(jump_op_ip, label_name, source_line, frame_depth_at_goto)` for forward `goto LABEL`.
251    pending: Vec<(usize, String, usize, usize)>,
252}
253
254impl Default for Compiler {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl Compiler {
261    /// Array/hash slice subscripts are list context: `@a[LIST]` flattens ranges, `reverse`,
262    /// `sort`, `grep`, `map`, and array variables the same way `@h{LIST}` does. Scalar
263    /// subscripts are unaffected because list context on a plain scalar is still the scalar.
264    fn compile_array_slice_index_expr(&mut self, index_expr: &Expr) -> Result<(), CompileError> {
265        self.compile_expr_ctx(index_expr, WantarrayCtx::List)
266    }
267
268    /// Hash-slice key component: `'a'..'c'` inside `@h{...}` / `@$h{...}` is a list-context
269    /// range so the VM's hash-slice helpers receive an array to flatten into individual keys.
270    fn compile_hash_slice_key_expr(&mut self, key_expr: &Expr) -> Result<(), CompileError> {
271        if matches!(
272            &key_expr.kind,
273            ExprKind::Range { .. } | ExprKind::SliceRange { .. }
274        ) {
275            self.compile_expr_ctx(key_expr, WantarrayCtx::List)
276        } else {
277            self.compile_expr(key_expr)
278        }
279    }
280
281    /// Push the value of `expr` if present, or `Undef` (the omitted-endpoint sentinel
282    /// used by [`Op::ArraySliceRange`] / [`Op::HashSliceRange`]) if `None`.
283    fn compile_optional_or_undef(&mut self, expr: Option<&Expr>) -> Result<(), CompileError> {
284        match expr {
285            Some(e) => self.compile_expr(e),
286            None => {
287                self.emit_op(Op::LoadUndef, 0, None);
288                Ok(())
289            }
290        }
291    }
292
293    pub fn new() -> Self {
294        Self {
295            chunk: Chunk::new(),
296            ast_expr_intern: HashMap::new(),
297            begin_blocks: Vec::new(),
298            unit_check_blocks: Vec::new(),
299            check_blocks: Vec::new(),
300            init_blocks: Vec::new(),
301            end_blocks: Vec::new(),
302            // Main program `my $x` uses [`Op::GetScalarSlot`] / [`Op::SetScalarSlot`] like subs,
303            // so hot loops are not stuck on [`Op::GetScalarPlain`] (linear scan per access).
304            scope_stack: vec![ScopeLayer {
305                use_slots: true,
306                ..Default::default()
307            }],
308            current_package: String::new(),
309            program_last_stmt_takes_value: false,
310            source_file: String::new(),
311            frame_depth: 0,
312            try_depth: 0,
313            loop_stack: Vec::new(),
314            goto_ctx_stack: Vec::new(),
315            strict_vars: false,
316            force_name_for_sort_pair: false,
317            sort_pair_block_indices: std::collections::HashSet::new(),
318            sub_body_block_indices: std::collections::HashSet::new(),
319            code_ref_block_params: Vec::new(),
320            block_scope_snapshots: Vec::new(),
321        }
322    }
323
324    /// Add a deferred-compilation block (map/grep/sort/reduce/…) to the chunk
325    /// AND snapshot the current `scope_stack` so the 4th pass can compile the
326    /// block under the lexical scope it was originally defined in. Replaces
327    /// every former `self.add_deferred_block(…)` call site inside the compiler so
328    /// no block slips through unsnapshotted. See [`Self::block_scope_snapshots`].
329    fn add_deferred_block(&mut self, block: Block) -> u16 {
330        let idx = self.chunk.add_block(block);
331        let snap = self.scope_stack.clone();
332        while self.block_scope_snapshots.len() < idx as usize {
333            self.block_scope_snapshots.push(None);
334        }
335        self.block_scope_snapshots.push(Some(snap));
336        idx
337    }
338
339    /// Mark a block index as a sort/reduce comparator that binds `$a`/`$b`.
340    /// The deferred 4th pass enables [`Self::force_name_for_sort_pair`] before
341    /// compiling registered indices so `$a`/`$b` references resolve via name
342    /// instead of any outer `my` slot allocation.
343    fn register_sort_pair_block(&mut self, idx: u16) {
344        self.sort_pair_block_indices.insert(idx);
345    }
346
347    /// Set `use strict 'vars'` at compile time. When enabled, [`compile_expr`] rejects any read
348    /// or write of an undeclared global scalar / array / hash with `CompileError::Frozen` — the
349    /// same diagnostic emitted at runtime (`Global symbol "$name" requires
350    /// explicit package name`). `try_vm_execute` pulls the flag from `Interpreter::strict_vars`
351    /// before constructing the compiler, matching the timing of
352    /// `prepare_program_top_level` (which processes `use strict` before main body execution).
353    pub fn with_strict_vars(mut self, v: bool) -> Self {
354        self.strict_vars = v;
355        self
356    }
357
358    /// Enter a `goto LABEL` scope (called when compiling the top-level main program or a sub
359    /// body). Labels defined inside can be targeted from any `goto` inside the same scope;
360    /// labels are *not* shared across nested functions.
361    fn enter_goto_scope(&mut self) {
362        self.goto_ctx_stack.push(GotoCtx::default());
363    }
364
365    /// Resolve all pending forward gotos and pop the scope. Returns `CompileError::Frozen` if a
366    /// `goto` targets a label that was never defined in this scope (same diagnostic as
367    /// runtime: `goto: unknown label NAME`). Returns `Unsupported` if a
368    /// `goto` crosses a frame boundary (e.g. from inside an `if` body out to an outer label) —
369    /// crossing frames would skip `PopFrame` ops and corrupt the scope stack. That case is
370    /// not yet lowered to bytecode.
371    fn exit_goto_scope(&mut self) -> Result<(), CompileError> {
372        let ctx = self
373            .goto_ctx_stack
374            .pop()
375            .expect("exit_goto_scope called without matching enter");
376        for (jump_ip, label, line, goto_frame_depth) in ctx.pending {
377            if let Some(&(target_ip, label_frame_depth)) = ctx.labels.get(&label) {
378                if label_frame_depth != goto_frame_depth {
379                    return Err(CompileError::Unsupported(format!(
380                        "goto LABEL crosses a scope frame (label `{}` at depth {} vs goto at depth {})",
381                        label, label_frame_depth, goto_frame_depth
382                    )));
383                }
384                self.chunk.patch_jump_to(jump_ip, target_ip);
385            } else {
386                return Err(CompileError::Frozen {
387                    line,
388                    detail: format!("goto: unknown label {}", label),
389                });
390            }
391        }
392        Ok(())
393    }
394
395    /// Record `label → current IP` if a goto-scope is active. Called before each labeled
396    /// statement is emitted; the label points to the first op of the statement.
397    fn record_stmt_label(&mut self, label: &str) {
398        if let Some(top) = self.goto_ctx_stack.last_mut() {
399            top.labels
400                .insert(label.to_string(), (self.chunk.len(), self.frame_depth));
401        }
402    }
403
404    /// If `target` is a compile-time-known label name (bareword or literal string), emit a
405    /// forward `Jump(0)` and record it for patching on goto-scope exit. Returns `true` if the
406    /// goto was handled (so the caller should not emit a fallback). Returns `false` if the target
407    /// is dynamic — the caller should bail to `CompileError::Unsupported` so the tree path can
408    /// still handle it in future.
409    fn try_emit_goto_label(&mut self, target: &Expr, line: usize) -> bool {
410        let name = match &target.kind {
411            ExprKind::Bareword(n) => n.clone(),
412            ExprKind::String(s) => s.clone(),
413            // Parser may produce a zero-arg FuncCall for a bare label name
414            ExprKind::FuncCall { name, args } if args.is_empty() => name.clone(),
415            _ => return false,
416        };
417        if self.goto_ctx_stack.is_empty() {
418            return false;
419        }
420        let jump_ip = self.chunk.emit(Op::Jump(0), line);
421        let frame_depth = self.frame_depth;
422        self.goto_ctx_stack
423            .last_mut()
424            .expect("goto scope must be active")
425            .pending
426            .push((jump_ip, name, line, frame_depth));
427        true
428    }
429
430    /// Emit `Op::PushFrame` and bump [`Self::frame_depth`].
431    fn emit_push_frame(&mut self, line: usize) {
432        self.chunk.emit(Op::PushFrame, line);
433        self.frame_depth += 1;
434    }
435
436    /// Emit `Op::PopFrame` and decrement [`Self::frame_depth`] (saturating).
437    fn emit_pop_frame(&mut self, line: usize) {
438        self.chunk.emit(Op::PopFrame, line);
439        self.frame_depth = self.frame_depth.saturating_sub(1);
440    }
441
442    pub fn with_source_file(mut self, path: String) -> Self {
443        self.source_file = path;
444        self
445    }
446
447    /// `@ISA` / `@EXPORT` / `@EXPORT_OK` outside `main` → `Pkg::NAME` (see interpreter stash rules).
448    fn qualify_stash_array_name(&self, name: &str) -> String {
449        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
450            let pkg = &self.current_package;
451            if !pkg.is_empty() && pkg != "main" {
452                return format!("{}::{}", pkg, name);
453            }
454        }
455        name.to_string()
456    }
457
458    /// Package stash key for `our $name` (matches [`VMHelper::stash_scalar_name_for_package`]).
459    fn qualify_stash_scalar_name(&self, name: &str) -> String {
460        if name.contains("::") {
461            return name.to_string();
462        }
463        let pkg = &self.current_package;
464        if pkg.is_empty() || pkg == "main" {
465            format!("main::{}", name)
466        } else {
467            format!("{}::{}", pkg, name)
468        }
469    }
470
471    /// Runtime name for `$x` in bytecode after `my`/`our` resolution (`our` → qualified stash).
472    fn scalar_storage_name_for_ops(&self, bare: &str) -> String {
473        if bare.contains("::") {
474            return bare.to_string();
475        }
476        for layer in self.scope_stack.iter().rev() {
477            if layer.declared_scalars.contains(bare) {
478                if layer.declared_our_scalars.contains(bare) {
479                    return self.qualify_stash_scalar_name(bare);
480                }
481                return bare.to_string();
482            }
483        }
484        bare.to_string()
485    }
486
487    #[inline]
488    fn intern_scalar_var_for_ops(&mut self, bare: &str) -> u16 {
489        let s = self.scalar_storage_name_for_ops(bare);
490        self.chunk.intern_name(&s)
491    }
492
493    /// For `local $x`, qualify to package stash since local only works on package variables.
494    /// Special vars (like `$/`, `$\`, `$,`, `$"`, or `^X` caret vars) are not qualified.
495    fn intern_scalar_for_local(&mut self, bare: &str) -> u16 {
496        if VMHelper::is_special_scalar_name_for_set(bare) || bare.starts_with('^') {
497            self.chunk.intern_name(bare)
498        } else {
499            let s = self.qualify_stash_scalar_name(bare);
500            self.chunk.intern_name(&s)
501        }
502    }
503
504    fn register_declare_our_scalar(&mut self, bare_name: &str) {
505        let layer = self.scope_stack.last_mut().expect("scope stack");
506        layer.declared_scalars.insert(bare_name.to_string());
507        layer.declared_our_scalars.insert(bare_name.to_string());
508    }
509
510    /// `our $x` — package stash binding; no slot indices (bare `$x` maps to `main::x` / `Pkg::x`).
511    fn emit_declare_our_scalar(&mut self, bare_name: &str, line: usize, frozen: bool) {
512        let stash = self.qualify_stash_scalar_name(bare_name);
513        let stash_idx = self.chunk.intern_name(&stash);
514        self.register_declare_our_scalar(bare_name);
515        if frozen {
516            self.chunk.emit(Op::DeclareScalarFrozen(stash_idx), line);
517        } else {
518            self.chunk.emit(Op::DeclareScalar(stash_idx), line);
519        }
520    }
521
522    /// Stash key for a subroutine name in the current package (matches [`VMHelper::qualify_sub_key`]).
523    fn qualify_sub_key(&self, name: &str) -> String {
524        if name.contains("::") {
525            return name.to_string();
526        }
527        let pkg = &self.current_package;
528        if pkg.is_empty() || pkg == "main" {
529            name.to_string()
530        } else {
531            format!("{}::{}", pkg, name)
532        }
533    }
534
535    /// First-pass sub registration: walk `package` statements like [`Self::compile_program`] does for
536    /// sub bodies so forward `sub` entries use the same stash key as runtime registration.
537    fn qualify_sub_decl_pass1(name: &str, pending_pkg: &str) -> String {
538        if name.contains("::") {
539            return name.to_string();
540        }
541        if pending_pkg.is_empty() || pending_pkg == "main" {
542            name.to_string()
543        } else {
544            format!("{}::{}", pending_pkg, name)
545        }
546    }
547
548    /// After all `sub` bodies are lowered, replace [`Op::Call`] with [`Op::CallStaticSubId`] when the
549    /// callee has a compiled entry (avoids linear `sub_entries` scan + extra stash work per call).
550    fn patch_static_sub_calls(chunk: &mut Chunk) {
551        for i in 0..chunk.ops.len() {
552            if let Op::Call(name_idx, argc, wa) = chunk.ops[i] {
553                if let Some((entry_ip, stack_args)) = chunk.find_sub_entry(name_idx) {
554                    if chunk.static_sub_calls.len() < u16::MAX as usize {
555                        let sid = chunk.static_sub_calls.len() as u16;
556                        chunk
557                            .static_sub_calls
558                            .push((entry_ip, stack_args, name_idx));
559                        chunk.ops[i] = Op::CallStaticSubId(sid, name_idx, argc, wa);
560                    }
561                }
562            }
563        }
564    }
565
566    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the stack must hold the **array reference**
567    /// (scalar), not `@{...}` / `@$r` expansion (which would push a cloned plain array).
568    fn compile_arrow_array_base_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
569        if let ExprKind::Deref {
570            expr: inner,
571            kind: Sigil::Array | Sigil::Scalar,
572        } = &expr.kind
573        {
574            self.compile_expr(inner)
575        } else {
576            self.compile_expr(expr)
577        }
578    }
579
580    /// For `$href->{k}` / `$$r{k}`: stack holds the hash **reference** scalar, not a copied `%` value.
581    fn compile_arrow_hash_base_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
582        if let ExprKind::Deref {
583            expr: inner,
584            kind: Sigil::Scalar,
585        } = &expr.kind
586        {
587            self.compile_expr(inner)
588        } else {
589            self.compile_expr(expr)
590        }
591    }
592
593    fn push_scope_layer(&mut self) {
594        self.scope_stack.push(ScopeLayer::default());
595    }
596
597    /// Push a scope layer with slot assignment enabled (for subroutine bodies).
598    fn push_scope_layer_with_slots(&mut self) {
599        self.scope_stack.push(ScopeLayer {
600            use_slots: true,
601            is_sub_body: true,
602            ..Default::default()
603        });
604    }
605
606    /// Register a signature parameter (`fn foo($x, @args, %opts) { ... }`) in
607    /// the current scope layer so the closure-write check (DESIGN-001)
608    /// recognises writes to the param as local — not outer-scope `my`.
609    fn register_sig_param(&mut self, p: &crate::ast::SubSigParam) {
610        use crate::ast::SubSigParam;
611        let layer = self.scope_stack.last_mut().expect("scope stack");
612        match p {
613            SubSigParam::Scalar(name, _, _) => {
614                layer.declared_scalars.insert(name.clone());
615            }
616            SubSigParam::Array(name, _) => {
617                layer.declared_arrays.insert(name.clone());
618            }
619            SubSigParam::Hash(name, _) => {
620                layer.declared_hashes.insert(name.clone());
621            }
622            SubSigParam::ArrayDestruct(elems) => {
623                for el in elems {
624                    match el {
625                        crate::ast::MatchArrayElem::CaptureScalar(n) => {
626                            layer.declared_scalars.insert(n.clone());
627                        }
628                        crate::ast::MatchArrayElem::RestBind(n) => {
629                            layer.declared_arrays.insert(n.clone());
630                        }
631                        _ => {}
632                    }
633                }
634            }
635            SubSigParam::HashDestruct(pairs) => {
636                for (_, var) in pairs {
637                    layer.declared_scalars.insert(var.clone());
638                }
639            }
640        }
641    }
642
643    fn pop_scope_layer(&mut self) {
644        if self.scope_stack.len() > 1 {
645            self.scope_stack.pop();
646        }
647    }
648
649    /// Look up a scalar's slot index in the current scope layer (if slots are enabled).
650    fn scalar_slot(&self, name: &str) -> Option<u8> {
651        // Deferred sort-block compilation: `$a`/`$b` must be name-based so the
652        // runtime `set_sort_pair` (which writes by name) and the comparator's
653        // reads agree. Slot allocation from any outer `my $a`/`my $b` is
654        // visible here because the 4th pass runs after the whole program is
655        // compiled. See [`Self::force_name_for_sort_pair`].
656        if self.force_name_for_sort_pair && (name == "a" || name == "b") {
657            return None;
658        }
659        if let Some(layer) = self.scope_stack.last() {
660            if layer.use_slots {
661                return layer.scalar_slots.get(name).copied();
662            }
663        }
664        None
665    }
666
667    /// Intern an [`Expr`] for [`Chunk::op_ast_expr`] (pointer-stable during compile).
668    fn intern_ast_expr(&mut self, expr: &Expr) -> u32 {
669        let p = expr as *const Expr as usize;
670        if let Some(&id) = self.ast_expr_intern.get(&p) {
671            return id;
672        }
673        let id = self.chunk.ast_expr_pool.len() as u32;
674        self.chunk.ast_expr_pool.push(expr.clone());
675        self.ast_expr_intern.insert(p, id);
676        id
677    }
678
679    /// Emit one opcode with optional link to the originating expression (expression compiler path).
680    #[inline]
681    fn emit_op(&mut self, op: Op, line: usize, ast: Option<&Expr>) -> usize {
682        let idx = ast.map(|e| self.intern_ast_expr(e));
683        self.chunk.emit_with_ast_idx(op, line, idx)
684    }
685
686    /// Emit GetScalar or GetScalarSlot depending on whether the variable has a slot.
687    fn emit_get_scalar(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
688        let name = &self.chunk.names[name_idx as usize];
689        if let Some(slot) = self.scalar_slot(name) {
690            self.emit_op(Op::GetScalarSlot(slot), line, ast);
691        } else if VMHelper::is_special_scalar_name_for_get(name) {
692            self.emit_op(Op::GetScalar(name_idx), line, ast);
693        } else {
694            self.emit_op(Op::GetScalarPlain(name_idx), line, ast);
695        }
696    }
697
698    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl), not “regex object is truthy”.
699    /// Emits `$_` + pattern and [`Op::RegexMatchDyn`] so match vars and truthy 0/1 match `=~`.
700    fn compile_boolean_rvalue_condition(&mut self, cond: &Expr) -> Result<(), CompileError> {
701        let line = cond.line;
702        if let ExprKind::Regex(pattern, flags) = &cond.kind {
703            let name_idx = self.chunk.intern_name("_");
704            self.emit_get_scalar(name_idx, line, Some(cond));
705            let pat_idx = self
706                .chunk
707                .add_constant(StrykeValue::string(pattern.clone()));
708            let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
709            self.emit_op(Op::LoadRegex(pat_idx, flags_idx), line, Some(cond));
710            self.emit_op(Op::RegexMatchDyn(false), line, Some(cond));
711            Ok(())
712        } else if matches!(&cond.kind, ExprKind::ReadLine(_)) {
713            // `while (<STDIN>)` — assign line to `$_` then test definedness (Perl).
714            self.compile_expr(cond)?;
715            let name_idx = self.chunk.intern_name("_");
716            self.emit_set_scalar_keep(name_idx, line, Some(cond));
717            self.emit_op(
718                Op::CallBuiltin(BuiltinId::Defined as u16, 1),
719                line,
720                Some(cond),
721            );
722            Ok(())
723        } else {
724            self.compile_expr(cond)
725        }
726    }
727
728    /// Emit SetScalar or SetScalarSlot depending on slot availability.
729    fn emit_set_scalar(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
730        let name = &self.chunk.names[name_idx as usize];
731        if let Some(slot) = self.scalar_slot(name) {
732            self.emit_op(Op::SetScalarSlot(slot), line, ast);
733        } else if VMHelper::is_special_scalar_name_for_set(name) {
734            self.emit_op(Op::SetScalar(name_idx), line, ast);
735        } else {
736            self.emit_op(Op::SetScalarPlain(name_idx), line, ast);
737        }
738    }
739
740    /// Emit SetScalarKeep or SetScalarSlotKeep depending on slot availability.
741    fn emit_set_scalar_keep(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
742        let name = &self.chunk.names[name_idx as usize];
743        if let Some(slot) = self.scalar_slot(name) {
744            self.emit_op(Op::SetScalarSlotKeep(slot), line, ast);
745        } else if VMHelper::is_special_scalar_name_for_set(name) {
746            self.emit_op(Op::SetScalarKeep(name_idx), line, ast);
747        } else {
748            self.emit_op(Op::SetScalarKeepPlain(name_idx), line, ast);
749        }
750    }
751
752    fn emit_pre_inc(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
753        let name = &self.chunk.names[name_idx as usize];
754        if let Some(slot) = self.scalar_slot(name) {
755            self.emit_op(Op::PreIncSlot(slot), line, ast);
756        } else {
757            self.emit_op(Op::PreInc(name_idx), line, ast);
758        }
759    }
760
761    fn emit_pre_dec(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
762        let name = &self.chunk.names[name_idx as usize];
763        if let Some(slot) = self.scalar_slot(name) {
764            self.emit_op(Op::PreDecSlot(slot), line, ast);
765        } else {
766            self.emit_op(Op::PreDec(name_idx), line, ast);
767        }
768    }
769
770    fn emit_post_inc(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
771        let name = &self.chunk.names[name_idx as usize];
772        if let Some(slot) = self.scalar_slot(name) {
773            self.emit_op(Op::PostIncSlot(slot), line, ast);
774        } else {
775            self.emit_op(Op::PostInc(name_idx), line, ast);
776        }
777    }
778
779    fn emit_post_dec(&mut self, name_idx: u16, line: usize, ast: Option<&Expr>) {
780        let name = &self.chunk.names[name_idx as usize];
781        if let Some(slot) = self.scalar_slot(name) {
782            self.emit_op(Op::PostDecSlot(slot), line, ast);
783        } else {
784            self.emit_op(Op::PostDec(name_idx), line, ast);
785        }
786    }
787
788    /// Assign a new slot index for a scalar in the current scope layer.
789    /// Returns the slot index if slots are enabled, None otherwise.
790    fn assign_scalar_slot(&mut self, name: &str) -> Option<u8> {
791        if let Some(layer) = self.scope_stack.last_mut() {
792            if layer.use_slots && layer.next_scalar_slot < 255 {
793                let slot = layer.next_scalar_slot;
794                layer.scalar_slots.insert(name.to_string(), slot);
795                layer.next_scalar_slot += 1;
796                return Some(slot);
797            }
798        }
799        None
800    }
801
802    fn register_declare(&mut self, sigil: Sigil, name: &str, frozen: bool) {
803        let layer = self.scope_stack.last_mut().expect("scope stack");
804        match sigil {
805            Sigil::Scalar => {
806                layer.declared_scalars.insert(name.to_string());
807                if frozen {
808                    layer.frozen_scalars.insert(name.to_string());
809                }
810            }
811            Sigil::Array => {
812                layer.declared_arrays.insert(name.to_string());
813                if frozen {
814                    layer.frozen_arrays.insert(name.to_string());
815                }
816            }
817            Sigil::Hash => {
818                layer.declared_hashes.insert(name.to_string());
819                if frozen {
820                    layer.frozen_hashes.insert(name.to_string());
821                }
822            }
823            Sigil::Typeglob => {
824                layer.declared_scalars.insert(name.to_string());
825            }
826        }
827    }
828
829    /// `use strict 'vars'` check for a scalar `$name`. Mirrors [`VMHelper::check_strict_scalar_var`]:
830    /// ok if strict is off, the name contains `::` (package-qualified), the name is a Perl special
831    /// scalar, or the name is declared via `my`/`our` in any enclosing compiler scope layer.
832    /// Otherwise errors with the exact diagnostic message so the user sees a consistent
833    /// error from the VM.
834    fn check_strict_scalar_access(&self, name: &str, line: usize) -> Result<(), CompileError> {
835        if !self.strict_vars
836            || name.contains("::")
837            || VMHelper::strict_scalar_exempt(name)
838            || VMHelper::is_special_scalar_name_for_get(name)
839            || self
840                .scope_stack
841                .iter()
842                .any(|l| l.declared_scalars.contains(name))
843        {
844            return Ok(());
845        }
846        Err(CompileError::Frozen {
847            line,
848            detail: format!(
849                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
850                name, name
851            ),
852        })
853    }
854
855    /// Array names that are always bound at runtime (Perl built-ins) and must not trigger a
856    /// `use strict 'vars'` compile error even though they're never `my`-declared.
857    /// Stryke topic-var arrays (`@_0`, `@_1`, …) are auto-bound inside any
858    /// block that takes positional arguments — exempt them too so
859    /// `$_1[0]` / `@_1[0]` / etc. compile under `use strict 'vars'`.
860    fn strict_array_exempt(name: &str) -> bool {
861        if matches!(
862            name,
863            "_" | "ARGV" | "INC" | "ENV" | "ISA" | "EXPORT" | "EXPORT_OK" | "EXPORT_FAIL"
864        ) {
865            return true;
866        }
867        name.starts_with('_') && name.len() > 1 && name[1..].chars().all(|c| c.is_ascii_digit())
868    }
869
870    /// Hash names that are always bound at runtime. Also exempts stryke
871    /// topic-var hashes (`%_0`, `%_1`, …) for symmetry with the scalar
872    /// and array forms.
873    fn strict_hash_exempt(name: &str) -> bool {
874        if matches!(
875            name,
876            "ENV" | "INC" | "SIG" | "EXPORT_TAGS" | "ISA" | "OVERLOAD" | "+" | "-" | "!" | "^H"
877        ) {
878            return true;
879        }
880        name.starts_with('_') && name.len() > 1 && name[1..].chars().all(|c| c.is_ascii_digit())
881    }
882
883    fn check_strict_array_access(&self, name: &str, line: usize) -> Result<(), CompileError> {
884        if !self.strict_vars
885            || name.contains("::")
886            || Self::strict_array_exempt(name)
887            || self
888                .scope_stack
889                .iter()
890                .any(|l| l.declared_arrays.contains(name))
891        {
892            return Ok(());
893        }
894        Err(CompileError::Frozen {
895            line,
896            detail: format!(
897                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
898                name, name
899            ),
900        })
901    }
902
903    fn check_strict_hash_access(&self, name: &str, line: usize) -> Result<(), CompileError> {
904        if !self.strict_vars
905            || name.contains("::")
906            || Self::strict_hash_exempt(name)
907            || self
908                .scope_stack
909                .iter()
910                .any(|l| l.declared_hashes.contains(name))
911        {
912            return Ok(());
913        }
914        Err(CompileError::Frozen {
915            line,
916            detail: format!(
917                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
918                name, name
919            ),
920        })
921    }
922
923    fn check_scalar_mutable(&self, name: &str, line: usize) -> Result<(), CompileError> {
924        for layer in self.scope_stack.iter().rev() {
925            if layer.declared_scalars.contains(name) {
926                if layer.frozen_scalars.contains(name) {
927                    return Err(CompileError::Frozen {
928                        line,
929                        detail: format!("cannot assign to frozen variable `${}`", name),
930                    });
931                }
932                return Ok(());
933            }
934        }
935        Ok(())
936    }
937
938    /// Closure-write rule (DESIGN-001): writing to an outer-scope `my $x`
939    /// from inside a sub body silently mutates only the closure's snapshot
940    /// — the outer scope keeps its own value, which is almost always a
941    /// bug. Reject at compile time and point the user at `mysync $x` (for
942    /// shared mutable state) or `--compat` (for Perl 5 shared semantics).
943    ///
944    /// Returns Ok(()) when:
945    ///   - `name` is declared in the innermost sub-body layer (locally owned).
946    ///   - `name` is `mysync` (atomic shared cell — safe to mutate).
947    ///   - `name` is `our` (package global — explicitly cross-scope state).
948    ///   - `--compat` mode is active (Perl 5 shared-storage semantics).
949    ///   - `name` is undeclared (caught by strict-vars elsewhere).
950    fn check_closure_write_to_outer_my(&self, name: &str, line: usize) -> Result<(), CompileError> {
951        if crate::compat_mode() {
952            return Ok(());
953        }
954        // Walk innermost-first. Look for the boundary: any layer between us
955        // and the declaring layer that is a sub body (`is_sub_body`) means
956        // the variable was declared outside the closure we're currently in.
957        let mut crossed_sub_body = false;
958        for layer in self.scope_stack.iter().rev() {
959            if layer.declared_scalars.contains(name) {
960                if !crossed_sub_body {
961                    return Ok(());
962                }
963                if layer.mysync_scalars.contains(name) {
964                    return Ok(());
965                }
966                if layer.declared_our_scalars.contains(name) {
967                    return Ok(());
968                }
969                return Err(CompileError::Frozen {
970                    line,
971                    detail: format!(
972                        "cannot modify outer-scope `my ${name}` from inside a closure — \
973                         stryke closures capture by value to keep parallel dispatch race-free. \
974                         Use `mysync ${name}` for shared mutable state, or `--compat` for Perl 5 \
975                         shared-storage semantics"
976                    ),
977                });
978            }
979            if layer.is_sub_body {
980                crossed_sub_body = true;
981            }
982        }
983        Ok(())
984    }
985
986    fn check_array_mutable(&self, name: &str, line: usize) -> Result<(), CompileError> {
987        for layer in self.scope_stack.iter().rev() {
988            if layer.declared_arrays.contains(name) {
989                if layer.frozen_arrays.contains(name) {
990                    return Err(CompileError::Frozen {
991                        line,
992                        detail: format!("cannot modify frozen array `@{}`", name),
993                    });
994                }
995                return Ok(());
996            }
997        }
998        Ok(())
999    }
1000
1001    fn check_hash_mutable(&self, name: &str, line: usize) -> Result<(), CompileError> {
1002        for layer in self.scope_stack.iter().rev() {
1003            if layer.declared_hashes.contains(name) {
1004                if layer.frozen_hashes.contains(name) {
1005                    return Err(CompileError::Frozen {
1006                        line,
1007                        detail: format!("cannot modify frozen hash `%{}`", name),
1008                    });
1009                }
1010                return Ok(());
1011            }
1012        }
1013        Ok(())
1014    }
1015
1016    /// Register variables declared by `use Env qw(@PATH $HOME ...)` so the strict-vars
1017    /// compiler pass knows they exist.
1018    fn register_env_imports(layer: &mut ScopeLayer, imports: &[Expr]) {
1019        for e in imports {
1020            let mut names_owned: Vec<String> = Vec::new();
1021            match &e.kind {
1022                ExprKind::String(s) => names_owned.push(s.clone()),
1023                ExprKind::QW(ws) => names_owned.extend(ws.iter().cloned()),
1024                ExprKind::InterpolatedString(parts) => {
1025                    let mut s = String::new();
1026                    for p in parts {
1027                        match p {
1028                            StringPart::Literal(l) => s.push_str(l),
1029                            StringPart::ScalarVar(v) => {
1030                                s.push('$');
1031                                s.push_str(v);
1032                            }
1033                            StringPart::ArrayVar(v) => {
1034                                s.push('@');
1035                                s.push_str(v);
1036                            }
1037                            _ => continue,
1038                        }
1039                    }
1040                    names_owned.push(s);
1041                }
1042                _ => continue,
1043            };
1044            for raw in &names_owned {
1045                if let Some(arr) = raw.strip_prefix('@') {
1046                    layer.declared_arrays.insert(arr.to_string());
1047                } else if let Some(hash) = raw.strip_prefix('%') {
1048                    layer.declared_hashes.insert(hash.to_string());
1049                } else {
1050                    let scalar = raw.strip_prefix('$').unwrap_or(raw);
1051                    layer.declared_scalars.insert(scalar.to_string());
1052                }
1053            }
1054        }
1055    }
1056
1057    /// Emit an `Op::RuntimeErrorConst` that matches Perl's
1058    /// `Can't modify {array,hash} dereference in {pre,post}{increment,decrement} (++|--)` message.
1059    /// Used for `++@{…}`, `%{…}--`, `@$r++`, etc. — constructs that are invalid in Perl 5.
1060    /// Pushes `LoadUndef` afterwards so the rvalue position has a value on the stack for any
1061    /// surrounding `Pop` from statement-expression dispatch (the error op aborts the VM before
1062    /// the `LoadUndef` is reached, but it keeps the emitted sequence well-formed for stack tracking).
1063    fn emit_aggregate_symbolic_inc_dec_error(
1064        &mut self,
1065        kind: Sigil,
1066        is_pre: bool,
1067        is_inc: bool,
1068        line: usize,
1069        root: &Expr,
1070    ) -> Result<(), CompileError> {
1071        let agg = match kind {
1072            Sigil::Array => "array",
1073            Sigil::Hash => "hash",
1074            _ => {
1075                return Err(CompileError::Unsupported(
1076                    "internal: non-aggregate sigil passed to symbolic ++/-- error emitter".into(),
1077                ));
1078            }
1079        };
1080        let op_str = match (is_pre, is_inc) {
1081            (true, true) => "preincrement (++)",
1082            (true, false) => "predecrement (--)",
1083            (false, true) => "postincrement (++)",
1084            (false, false) => "postdecrement (--)",
1085        };
1086        let msg = format!("Can't modify {} dereference in {}", agg, op_str);
1087        let idx = self.chunk.add_constant(StrykeValue::string(msg));
1088        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
1089        // The op never returns; this LoadUndef is dead code but keeps any unreachable
1090        // `Pop` / rvalue consumer emitted by the enclosing dispatch well-formed.
1091        self.emit_op(Op::LoadUndef, line, Some(root));
1092        Ok(())
1093    }
1094
1095    /// `mysync @arr` / `mysync %h` — aggregate element updates use `atomic_*_mutate`, not yet lowered to bytecode.
1096    fn is_mysync_array(&self, array_name: &str) -> bool {
1097        let q = self.qualify_stash_array_name(array_name);
1098        self.scope_stack
1099            .iter()
1100            .rev()
1101            .any(|l| l.mysync_arrays.contains(&q))
1102    }
1103
1104    fn is_mysync_hash(&self, hash_name: &str) -> bool {
1105        self.scope_stack
1106            .iter()
1107            .rev()
1108            .any(|l| l.mysync_hashes.contains(hash_name))
1109    }
1110
1111    pub fn compile_program(mut self, program: &Program) -> Result<Chunk, CompileError> {
1112        // Extract BEGIN/END blocks before compiling.
1113        for stmt in &program.statements {
1114            match &stmt.kind {
1115                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
1116                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
1117                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
1118                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
1119                StmtKind::End(block) => self.end_blocks.push(block.clone()),
1120                _ => {}
1121            }
1122        }
1123
1124        // First pass: register sub names for forward calls (qualified stash keys, same as runtime).
1125        let mut pending_pkg = String::new();
1126        for stmt in &program.statements {
1127            match &stmt.kind {
1128                StmtKind::Package { name } => pending_pkg = name.clone(),
1129                StmtKind::SubDecl { name, .. } => {
1130                    let q = Self::qualify_sub_decl_pass1(name, &pending_pkg);
1131                    let name_idx = self.chunk.intern_name(&q);
1132                    self.chunk.sub_entries.push((name_idx, 0, false));
1133                }
1134                _ => {}
1135            }
1136        }
1137
1138        // Second pass: compile main body.
1139        // The last expression statement keeps its value on the stack so the
1140        // caller can read the program's return value (like Perl's implicit return).
1141        let main_stmts: Vec<&Statement> = program
1142            .statements
1143            .iter()
1144            .filter(|s| {
1145                !matches!(
1146                    s.kind,
1147                    StmtKind::SubDecl { .. }
1148                        | StmtKind::Begin(_)
1149                        | StmtKind::UnitCheck(_)
1150                        | StmtKind::Check(_)
1151                        | StmtKind::Init(_)
1152                        | StmtKind::End(_)
1153                )
1154            })
1155            .collect();
1156        let last_idx = main_stmts.len().saturating_sub(1);
1157        self.program_last_stmt_takes_value = main_stmts
1158            .last()
1159            .map(|s| matches!(s.kind, StmtKind::TryCatch { .. }))
1160            .unwrap_or(false);
1161        // BEGIN blocks run before main (same order as Perl phase blocks).
1162        if !self.begin_blocks.is_empty() {
1163            self.chunk.emit(Op::SetGlobalPhase(GP_START), 0);
1164        }
1165        for block in &self.begin_blocks.clone() {
1166            self.compile_block(block)?;
1167        }
1168        // Perl: `${^GLOBAL_PHASE}` stays **`START`** during UNITCHECK blocks.
1169        let unit_check_rev: Vec<Block> = self.unit_check_blocks.iter().rev().cloned().collect();
1170        for block in unit_check_rev {
1171            self.compile_block(&block)?;
1172        }
1173        if !self.check_blocks.is_empty() {
1174            self.chunk.emit(Op::SetGlobalPhase(GP_CHECK), 0);
1175        }
1176        let check_rev: Vec<Block> = self.check_blocks.iter().rev().cloned().collect();
1177        for block in check_rev {
1178            self.compile_block(&block)?;
1179        }
1180        if !self.init_blocks.is_empty() {
1181            self.chunk.emit(Op::SetGlobalPhase(GP_INIT), 0);
1182        }
1183        let inits = self.init_blocks.clone();
1184        for block in inits {
1185            self.compile_block(&block)?;
1186        }
1187        self.chunk.emit(Op::SetGlobalPhase(GP_RUN), 0);
1188        // Record where the main body starts — used by `-n`/`-p` to re-execute only the body per line.
1189        self.chunk.body_start_ip = self.chunk.ops.len();
1190
1191        // Top-level `goto LABEL` scope: labels defined on main-program statements are targetable
1192        // from `goto` statements in the same main program. Pushed before the main loop and
1193        // resolved after it (but before END blocks, which run in their own scope).
1194        self.enter_goto_scope();
1195
1196        let mut i = 0;
1197        while i < main_stmts.len() {
1198            let stmt = main_stmts[i];
1199            if i == last_idx {
1200                // The specialized `last statement leaves its value on the stack` path bypasses
1201                // `compile_statement` for Expression/If/Unless shapes, so we must record any
1202                // `LABEL:` on this statement manually before emitting its ops.
1203                if let Some(lbl) = &stmt.label {
1204                    self.record_stmt_label(lbl);
1205                }
1206                match &stmt.kind {
1207                    StmtKind::Expression(expr) => {
1208                        // Last statement of program: still not a regex *value* — bare `/pat/` matches `$_`.
1209                        if matches!(&expr.kind, ExprKind::Regex(..)) {
1210                            self.compile_boolean_rvalue_condition(expr)?;
1211                        } else {
1212                            self.compile_expr(expr)?;
1213                        }
1214                    }
1215                    StmtKind::If {
1216                        condition,
1217                        body,
1218                        elsifs,
1219                        else_block,
1220                    } => {
1221                        self.compile_boolean_rvalue_condition(condition)?;
1222                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
1223                        self.emit_block_value(body, stmt.line)?;
1224                        let mut ends = vec![self.chunk.emit(Op::Jump(0), stmt.line)];
1225                        self.chunk.patch_jump_here(j0);
1226                        for (c, blk) in elsifs {
1227                            self.compile_boolean_rvalue_condition(c)?;
1228                            let j = self.chunk.emit(Op::JumpIfFalse(0), c.line);
1229                            self.emit_block_value(blk, c.line)?;
1230                            ends.push(self.chunk.emit(Op::Jump(0), c.line));
1231                            self.chunk.patch_jump_here(j);
1232                        }
1233                        if let Some(eb) = else_block {
1234                            self.emit_block_value(eb, stmt.line)?;
1235                        } else {
1236                            self.chunk.emit(Op::LoadUndef, stmt.line);
1237                        }
1238                        for j in ends {
1239                            self.chunk.patch_jump_here(j);
1240                        }
1241                    }
1242                    StmtKind::Unless {
1243                        condition,
1244                        body,
1245                        else_block,
1246                    } => {
1247                        self.compile_boolean_rvalue_condition(condition)?;
1248                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
1249                        if let Some(eb) = else_block {
1250                            self.emit_block_value(eb, stmt.line)?;
1251                        } else {
1252                            self.chunk.emit(Op::LoadUndef, stmt.line);
1253                        }
1254                        let end = self.chunk.emit(Op::Jump(0), stmt.line);
1255                        self.chunk.patch_jump_here(j0);
1256                        self.emit_block_value(body, stmt.line)?;
1257                        self.chunk.patch_jump_here(end);
1258                    }
1259                    StmtKind::Block(block) => {
1260                        self.chunk.emit(Op::PushFrame, stmt.line);
1261                        self.emit_block_value(block, stmt.line)?;
1262                        self.chunk.emit(Op::PopFrame, stmt.line);
1263                    }
1264                    StmtKind::StmtGroup(block) => {
1265                        self.emit_block_value(block, stmt.line)?;
1266                    }
1267                    _ => self.compile_statement(stmt)?,
1268                }
1269            } else {
1270                self.compile_statement(stmt)?;
1271            }
1272            i += 1;
1273        }
1274        self.program_last_stmt_takes_value = false;
1275
1276        // Resolve all forward `goto LABEL` against labels recorded in the main scope.
1277        self.exit_goto_scope()?;
1278
1279        // END blocks run after main, before halt (same order as Perl phase blocks).
1280        if !self.end_blocks.is_empty() {
1281            self.chunk.emit(Op::SetGlobalPhase(GP_END), 0);
1282        }
1283        for block in &self.end_blocks.clone() {
1284            self.compile_block(block)?;
1285        }
1286
1287        self.chunk.emit(Op::Halt, 0);
1288
1289        // Third pass: compile sub bodies after Halt
1290        let mut entries: Vec<(String, Vec<Statement>, String, Vec<crate::ast::SubSigParam>)> =
1291            Vec::new();
1292        let mut pending_pkg = String::new();
1293        for stmt in &program.statements {
1294            match &stmt.kind {
1295                StmtKind::Package { name } => pending_pkg = name.clone(),
1296                StmtKind::SubDecl {
1297                    name, body, params, ..
1298                } => {
1299                    entries.push((
1300                        name.clone(),
1301                        body.clone(),
1302                        pending_pkg.clone(),
1303                        params.clone(),
1304                    ));
1305                }
1306                _ => {}
1307            }
1308        }
1309
1310        for (name, body, sub_pkg, params) in &entries {
1311            let saved_pkg = self.current_package.clone();
1312            self.current_package = sub_pkg.clone();
1313            self.push_scope_layer_with_slots();
1314            // Register signature parameters in the new scope layer so the
1315            // closure-write check (DESIGN-001) recognises writes to params
1316            // as local — not outer-scope `my`. Without this, mutating a
1317            // sub parameter inside a `for` block (or any nested
1318            // statement-block) would falsely trigger the closure-write
1319            // diagnostic.
1320            for p in params {
1321                self.register_sig_param(p);
1322            }
1323            let entry_ip = self.chunk.len();
1324            let q = self.qualify_sub_key(name);
1325            let name_idx = self.chunk.intern_name(&q);
1326            // Patch the entry point
1327            for e in &mut self.chunk.sub_entries {
1328                if e.0 == name_idx {
1329                    e.1 = entry_ip;
1330                }
1331            }
1332            // Each sub body gets its own `goto LABEL` scope: labels are not visible across
1333            // different subs or between a sub and the main program.
1334            self.enter_goto_scope();
1335            // Compile sub body (VM `Call` pushes a scope frame; mirror for frozen tracking).
1336            self.emit_subroutine_body_return(body)?;
1337            self.exit_goto_scope()?;
1338            self.pop_scope_layer();
1339
1340            // Peephole: convert leading `ShiftArray("_")` to `GetArg(n)` if @_ is
1341            // not referenced by any other op in this sub. This eliminates Vec
1342            // allocation + string-based @_ lookup on every call.
1343            let underscore_idx = self.chunk.intern_name("_");
1344            self.peephole_stack_args(name_idx, entry_ip, underscore_idx);
1345            self.current_package = saved_pkg;
1346        }
1347
1348        // Fourth pass: lower simple map/grep/sort block bodies to bytecode (after subs; same `ops` vec).
1349        // Each block was added via [`Self::add_deferred_block`], which captured a
1350        // snapshot of `scope_stack` at definition time. Swap that snapshot in
1351        // before compiling so references resolve against the LEXICAL scope the
1352        // block was written in, not the trailing top-level scope_stack that
1353        // exists once every sub body has been compiled and popped. Without
1354        // this, a sub-local `my $max` referenced inside a `map { … $max … }`
1355        // would silently resolve to a sibling top-level `my $max` slot.
1356        self.chunk.block_bytecode_ranges = vec![None; self.chunk.blocks.len()];
1357        for i in 0..self.chunk.blocks.len() {
1358            let b = self.chunk.blocks[i].clone();
1359            if Self::block_has_return(&b) {
1360                continue;
1361            }
1362            let saved_scope_stack = self
1363                .block_scope_snapshots
1364                .get(i)
1365                .cloned()
1366                .flatten()
1367                .map(|snap| std::mem::replace(&mut self.scope_stack, snap));
1368            // Sort/reduce blocks bind `$a`/`$b` via [`Scope::set_sort_pair`] (name-write).
1369            // Suppress slot resolution for those names while compiling the body so that
1370            // any outer `my $a`/`my $b` slot binding doesn't shadow the per-iter values.
1371            let is_sort_pair = self.sort_pair_block_indices.contains(&(i as u16));
1372            self.force_name_for_sort_pair = is_sort_pair;
1373            // Sub-body blocks (`sub { ... }` / `fn { ... }`): push a fresh
1374            // `is_sub_body: true` layer so the closure-write check
1375            // (DESIGN-001) can flag outer-scope `my` writes from inside the
1376            // body. Map/grep/sort blocks don't push this layer — they share
1377            // the enclosing scope normally.
1378            let pushed_sub_body = self.sub_body_block_indices.contains(&(i as u16));
1379            if pushed_sub_body {
1380                self.scope_stack.push(ScopeLayer {
1381                    use_slots: true,
1382                    is_sub_body: true,
1383                    ..Default::default()
1384                });
1385                // Register the closure's signature params in the new layer
1386                // so the closure-write check sees them as locally declared.
1387                if let Some(params) = self.code_ref_block_params.get(i).cloned() {
1388                    for p in &params {
1389                        self.register_sig_param(p);
1390                    }
1391                }
1392            }
1393            let result = self.try_compile_block_region(&b);
1394            self.force_name_for_sort_pair = false;
1395            if pushed_sub_body {
1396                self.scope_stack.pop();
1397            }
1398            match result {
1399                Ok(range) => {
1400                    self.chunk.block_bytecode_ranges[i] = Some(range);
1401                }
1402                Err(CompileError::Frozen { .. }) => {
1403                    // Real error (e.g. closure-write to outer-`my`,
1404                    // assignment to `frozen` binding). Restore scope and
1405                    // propagate.
1406                    if let Some(orig) = saved_scope_stack {
1407                        self.scope_stack = orig;
1408                    }
1409                    return Err(result.unwrap_err());
1410                }
1411                Err(CompileError::Unsupported(_)) => {
1412                    // Block lowering not yet supported — leave the
1413                    // bytecode_ranges entry as None; runtime will fall
1414                    // back to AST execution of the block body.
1415                }
1416            }
1417            if let Some(orig) = saved_scope_stack {
1418                self.scope_stack = orig;
1419            }
1420        }
1421
1422        // Fifth pass: `map EXPR, LIST` — list-context expression per `$_` (same `ops` vec as blocks).
1423        self.chunk.map_expr_bytecode_ranges = vec![None; self.chunk.map_expr_entries.len()];
1424        for i in 0..self.chunk.map_expr_entries.len() {
1425            let e = self.chunk.map_expr_entries[i].clone();
1426            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::List) {
1427                self.chunk.map_expr_bytecode_ranges[i] = Some(range);
1428            }
1429        }
1430
1431        // Fifth pass (a): `grep EXPR, LIST` — single-expression filter bodies (same `ops` vec as blocks).
1432        self.chunk.grep_expr_bytecode_ranges = vec![None; self.chunk.grep_expr_entries.len()];
1433        for i in 0..self.chunk.grep_expr_entries.len() {
1434            let e = self.chunk.grep_expr_entries[i].clone();
1435            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::Scalar) {
1436                self.chunk.grep_expr_bytecode_ranges[i] = Some(range);
1437            }
1438        }
1439
1440        // Fifth pass (b): regex flip-flop compound RHS — boolean context (same `ops` vec).
1441        self.chunk.regex_flip_flop_rhs_expr_bytecode_ranges =
1442            vec![None; self.chunk.regex_flip_flop_rhs_expr_entries.len()];
1443        for i in 0..self.chunk.regex_flip_flop_rhs_expr_entries.len() {
1444            let e = self.chunk.regex_flip_flop_rhs_expr_entries[i].clone();
1445            if let Ok(range) = self.try_compile_flip_flop_rhs_expr_region(&e) {
1446                self.chunk.regex_flip_flop_rhs_expr_bytecode_ranges[i] = Some(range);
1447            }
1448        }
1449
1450        // Sixth pass: `eval_timeout EXPR { ... }` — timeout expression only (body stays interpreter).
1451        self.chunk.eval_timeout_expr_bytecode_ranges =
1452            vec![None; self.chunk.eval_timeout_entries.len()];
1453        for i in 0..self.chunk.eval_timeout_entries.len() {
1454            let timeout_expr = self.chunk.eval_timeout_entries[i].0.clone();
1455            if let Ok(range) =
1456                self.try_compile_grep_expr_region(&timeout_expr, WantarrayCtx::Scalar)
1457            {
1458                self.chunk.eval_timeout_expr_bytecode_ranges[i] = Some(range);
1459            }
1460        }
1461
1462        // Seventh pass: `keys EXPR` / `values EXPR` — operand expression only.
1463        self.chunk.keys_expr_bytecode_ranges = vec![None; self.chunk.keys_expr_entries.len()];
1464        for i in 0..self.chunk.keys_expr_entries.len() {
1465            let e = self.chunk.keys_expr_entries[i].clone();
1466            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::List) {
1467                self.chunk.keys_expr_bytecode_ranges[i] = Some(range);
1468            }
1469        }
1470        self.chunk.values_expr_bytecode_ranges = vec![None; self.chunk.values_expr_entries.len()];
1471        for i in 0..self.chunk.values_expr_entries.len() {
1472            let e = self.chunk.values_expr_entries[i].clone();
1473            if let Ok(range) = self.try_compile_grep_expr_region(&e, WantarrayCtx::List) {
1474                self.chunk.values_expr_bytecode_ranges[i] = Some(range);
1475            }
1476        }
1477
1478        // Eighth pass: `given (TOPIC) { ... }` — topic expression only.
1479        self.chunk.given_topic_bytecode_ranges = vec![None; self.chunk.given_entries.len()];
1480        for i in 0..self.chunk.given_entries.len() {
1481            let topic = self.chunk.given_entries[i].0.clone();
1482            if let Ok(range) = self.try_compile_grep_expr_region(&topic, WantarrayCtx::Scalar) {
1483                self.chunk.given_topic_bytecode_ranges[i] = Some(range);
1484            }
1485        }
1486
1487        // Ninth pass: algebraic `match (SUBJECT) { ... }` — subject expression only.
1488        self.chunk.algebraic_match_subject_bytecode_ranges =
1489            vec![None; self.chunk.algebraic_match_entries.len()];
1490        for i in 0..self.chunk.algebraic_match_entries.len() {
1491            let subject = self.chunk.algebraic_match_entries[i].0.clone();
1492            let range: Option<(usize, usize)> = match &subject.kind {
1493                ExprKind::ArrayVar(name) => {
1494                    self.check_strict_array_access(name, subject.line)?;
1495                    let line = subject.line;
1496                    let start = self.chunk.len();
1497                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
1498                    self.chunk.emit(Op::MakeArrayBindingRef(idx), line);
1499                    self.chunk.emit(Op::BlockReturnValue, line);
1500                    Some((start, self.chunk.len()))
1501                }
1502                ExprKind::HashVar(name) => {
1503                    self.check_strict_hash_access(name, subject.line)?;
1504                    let line = subject.line;
1505                    let start = self.chunk.len();
1506                    let idx = self.chunk.intern_name(name);
1507                    self.chunk.emit(Op::MakeHashBindingRef(idx), line);
1508                    self.chunk.emit(Op::BlockReturnValue, line);
1509                    Some((start, self.chunk.len()))
1510                }
1511                _ => self
1512                    .try_compile_grep_expr_region(&subject, WantarrayCtx::Scalar)
1513                    .ok(),
1514            };
1515            self.chunk.algebraic_match_subject_bytecode_ranges[i] = range;
1516        }
1517
1518        Self::patch_static_sub_calls(&mut self.chunk);
1519        self.chunk.peephole_fuse();
1520
1521        Ok(self.chunk)
1522    }
1523
1524    /// Lower a block body to `ops` ending in [`Op::BlockReturnValue`] when possible.
1525    ///
1526    /// Matches `Interpreter::exec_block_no_scope` for blocks **without** `return`: last statement
1527    /// must be [`StmtKind::Expression`] (the value is that expression). Earlier statements use
1528    /// [`Self::compile_statement`] (void context). Any `CompileError` keeps AST fallback.
1529    fn try_compile_block_region(&mut self, block: &Block) -> Result<(usize, usize), CompileError> {
1530        let line0 = block.first().map(|s| s.line).unwrap_or(0);
1531        let start = self.chunk.len();
1532        if block.is_empty() {
1533            self.chunk.emit(Op::LoadUndef, line0);
1534            self.chunk.emit(Op::BlockReturnValue, line0);
1535            return Ok((start, self.chunk.len()));
1536        }
1537        let last = block.last().expect("non-empty block");
1538        let StmtKind::Expression(expr) = &last.kind else {
1539            return Err(CompileError::Unsupported(
1540                "block last statement must be an expression for bytecode lowering".into(),
1541            ));
1542        };
1543        for stmt in &block[..block.len() - 1] {
1544            self.compile_statement(stmt)?;
1545        }
1546        let line = last.line;
1547        self.compile_expr(expr)?;
1548        self.chunk.emit(Op::BlockReturnValue, line);
1549        Ok((start, self.chunk.len()))
1550    }
1551
1552    /// Lower a single expression to `ops` ending in [`Op::BlockReturnValue`].
1553    ///
1554    /// Used for `grep EXPR, LIST` (with `$_` set by the VM per item), `eval_timeout EXPR { ... }`,
1555    /// `keys EXPR` / `values EXPR` operands, `given (TOPIC) { ... }` topic, algebraic `match (SUBJECT)`
1556    /// subject, and similar one-shot regions matching [`VMHelper::eval_expr`].
1557    fn try_compile_grep_expr_region(
1558        &mut self,
1559        expr: &Expr,
1560        ctx: WantarrayCtx,
1561    ) -> Result<(usize, usize), CompileError> {
1562        let line = expr.line;
1563        let start = self.chunk.len();
1564        self.compile_expr_ctx(expr, ctx)?;
1565        self.chunk.emit(Op::BlockReturnValue, line);
1566        Ok((start, self.chunk.len()))
1567    }
1568
1569    /// Regex flip-flop right operand: boolean rvalue (bare `m//` is `$_ =~ m//`), like `if` / `grep EXPR`.
1570    fn try_compile_flip_flop_rhs_expr_region(
1571        &mut self,
1572        expr: &Expr,
1573    ) -> Result<(usize, usize), CompileError> {
1574        let line = expr.line;
1575        let start = self.chunk.len();
1576        self.compile_boolean_rvalue_condition(expr)?;
1577        self.chunk.emit(Op::BlockReturnValue, line);
1578        Ok((start, self.chunk.len()))
1579    }
1580
1581    /// Peephole optimization: if a compiled sub starts with `ShiftArray("_")`
1582    /// ops and `@_` is not referenced elsewhere, convert those shifts to
1583    /// `GetArg(n)` and mark the sub entry as `uses_stack_args = true`.
1584    /// This eliminates Vec allocation + string-based @_ lookup per call.
1585    fn peephole_stack_args(&mut self, sub_name_idx: u16, entry_ip: usize, underscore_idx: u16) {
1586        let ops = &self.chunk.ops;
1587        let end = ops.len();
1588
1589        // Count leading ShiftArray("_") ops
1590        let mut shift_count: u8 = 0;
1591        let mut ip = entry_ip;
1592        while ip < end {
1593            if ops[ip] == Op::ShiftArray(underscore_idx) {
1594                shift_count += 1;
1595                ip += 1;
1596            } else {
1597                break;
1598            }
1599        }
1600        if shift_count == 0 {
1601            return;
1602        }
1603
1604        // Check that @_ is not referenced by any other op in this sub
1605        let refs_underscore = |op: &Op| -> bool {
1606            match op {
1607                Op::GetArray(idx)
1608                | Op::SetArray(idx)
1609                | Op::DeclareArray(idx)
1610                | Op::DeclareArrayFrozen(idx)
1611                | Op::GetArrayElem(idx)
1612                | Op::SetArrayElem(idx)
1613                | Op::SetArrayElemKeep(idx)
1614                | Op::PushArray(idx)
1615                | Op::PopArray(idx)
1616                | Op::ShiftArray(idx)
1617                | Op::ArrayLen(idx) => *idx == underscore_idx,
1618                _ => false,
1619            }
1620        };
1621
1622        for op in ops.iter().take(end).skip(entry_ip + shift_count as usize) {
1623            if refs_underscore(op) {
1624                return; // @_ used elsewhere, can't optimize
1625            }
1626            if matches!(op, Op::Halt | Op::ReturnValue) {
1627                break; // end of this sub's bytecode
1628            }
1629        }
1630
1631        // Safe to convert: replace ShiftArray("_") with GetArg(n)
1632        for i in 0..shift_count {
1633            self.chunk.ops[entry_ip + i as usize] = Op::GetArg(i);
1634        }
1635
1636        // Mark sub entry as using stack args
1637        for e in &mut self.chunk.sub_entries {
1638            if e.0 == sub_name_idx {
1639                e.2 = true;
1640            }
1641        }
1642    }
1643
1644    fn emit_declare_scalar(&mut self, name_idx: u16, line: usize, frozen: bool) {
1645        let name = self.chunk.names[name_idx as usize].clone();
1646        self.register_declare(Sigil::Scalar, &name, frozen);
1647        if frozen {
1648            self.chunk.emit(Op::DeclareScalarFrozen(name_idx), line);
1649        } else if let Some(slot) = self.assign_scalar_slot(&name) {
1650            self.chunk.emit(Op::DeclareScalarSlot(slot, name_idx), line);
1651        } else {
1652            self.chunk.emit(Op::DeclareScalar(name_idx), line);
1653        }
1654    }
1655
1656    /// Emit the right `DeclareScalarTyped*` op for a `typed my $x : Ty`
1657    /// declaration. Handles primitive types (Int/Str/Float/…) via the
1658    /// 1-byte type encoding, and user-defined struct/class/enum types
1659    /// via the name-pool encoding (`DeclareScalarTypedUser`).
1660    fn emit_declare_scalar_typed(
1661        &mut self,
1662        name_idx: u16,
1663        ty: &crate::ast::PerlTypeName,
1664        line: usize,
1665        frozen: bool,
1666    ) {
1667        let name = self.chunk.names[name_idx as usize].clone();
1668        self.register_declare(Sigil::Scalar, &name, frozen);
1669        if let Some(ty_byte) = ty.as_byte() {
1670            if frozen {
1671                self.chunk
1672                    .emit(Op::DeclareScalarTypedFrozen(name_idx, ty_byte), line);
1673            } else {
1674                self.chunk
1675                    .emit(Op::DeclareScalarTyped(name_idx, ty_byte), line);
1676            }
1677            return;
1678        }
1679        // User-defined type — encode the name through the name pool.
1680        let (type_name, is_enum) = match ty {
1681            crate::ast::PerlTypeName::Struct(n) => (n.clone(), false),
1682            crate::ast::PerlTypeName::Enum(n) => (n.clone(), true),
1683            _ => unreachable!("non-byte non-user type slipped past as_byte()"),
1684        };
1685        let type_name_idx = self.chunk.intern_name(&type_name);
1686        let flag = (u8::from(frozen) << 1) | u8::from(is_enum);
1687        self.chunk.emit(
1688            Op::DeclareScalarTypedUser(name_idx, type_name_idx, flag),
1689            line,
1690        );
1691    }
1692
1693    fn emit_declare_array(&mut self, name_idx: u16, line: usize, frozen: bool) {
1694        let name = self.chunk.names[name_idx as usize].clone();
1695        self.register_declare(Sigil::Array, &name, frozen);
1696        if frozen {
1697            self.chunk.emit(Op::DeclareArrayFrozen(name_idx), line);
1698        } else {
1699            self.chunk.emit(Op::DeclareArray(name_idx), line);
1700        }
1701    }
1702
1703    fn emit_declare_hash(&mut self, name_idx: u16, line: usize, frozen: bool) {
1704        let name = self.chunk.names[name_idx as usize].clone();
1705        self.register_declare(Sigil::Hash, &name, frozen);
1706        if frozen {
1707            self.chunk.emit(Op::DeclareHashFrozen(name_idx), line);
1708        } else {
1709            self.chunk.emit(Op::DeclareHash(name_idx), line);
1710        }
1711    }
1712
1713    fn compile_var_declarations(
1714        &mut self,
1715        decls: &[VarDecl],
1716        line: usize,
1717        is_my: bool,
1718    ) -> Result<(), CompileError> {
1719        let allow_frozen = is_my;
1720        // List assignment: my ($a, $b) = (10, 20) — distribute elements
1721        if decls.len() > 1 && decls[0].initializer.is_some() {
1722            self.compile_expr_ctx(decls[0].initializer.as_ref().unwrap(), WantarrayCtx::List)?;
1723            let tmp_name = self.chunk.intern_name("__list_assign_tmp__");
1724            self.emit_declare_array(tmp_name, line, false);
1725            for (i, decl) in decls.iter().enumerate() {
1726                let frozen = allow_frozen && decl.frozen;
1727                match decl.sigil {
1728                    Sigil::Scalar => {
1729                        self.chunk.emit(Op::LoadInt(i as i64), line);
1730                        self.chunk.emit(Op::GetArrayElem(tmp_name), line);
1731                        if is_my {
1732                            let name_idx = self.chunk.intern_name(&decl.name);
1733                            if let Some(ref ty) = decl.type_annotation {
1734                                self.emit_declare_scalar_typed(name_idx, ty, line, frozen);
1735                            } else {
1736                                self.emit_declare_scalar(name_idx, line, frozen);
1737                            }
1738                        } else {
1739                            if decl.type_annotation.is_some() {
1740                                return Err(CompileError::Unsupported("typed our".into()));
1741                            }
1742                            self.emit_declare_our_scalar(&decl.name, line, frozen);
1743                        }
1744                    }
1745                    Sigil::Array => {
1746                        let name_idx = self
1747                            .chunk
1748                            .intern_name(&self.qualify_stash_array_name(&decl.name));
1749                        // Slurpy `@rest` at position `i` takes `tmp[i..]` (the
1750                        // tail), not the whole list. (BUG-090)
1751                        self.chunk
1752                            .emit(Op::GetArrayFromIndex(tmp_name, i as u16), line);
1753                        self.emit_declare_array(name_idx, line, frozen);
1754                    }
1755                    Sigil::Hash => {
1756                        let name_idx = self.chunk.intern_name(&decl.name);
1757                        // Slurpy `%rest` at position `i` takes `tmp[i..]`
1758                        // pairs, not the whole list. (BUG-090)
1759                        self.chunk
1760                            .emit(Op::GetArrayFromIndex(tmp_name, i as u16), line);
1761                        self.emit_declare_hash(name_idx, line, frozen);
1762                    }
1763                    Sigil::Typeglob => {
1764                        return Err(CompileError::Unsupported(
1765                            "list assignment to typeglob (my (*a, *b) = ...)".into(),
1766                        ));
1767                    }
1768                }
1769            }
1770        } else {
1771            for decl in decls {
1772                let frozen = allow_frozen && decl.frozen;
1773                match decl.sigil {
1774                    Sigil::Scalar => {
1775                        if let Some(init) = &decl.initializer {
1776                            if decl.list_context {
1777                                // my ($x) = @a → list context, extract first element
1778                                self.compile_expr_ctx(init, WantarrayCtx::List)?;
1779                                self.chunk.emit(Op::ListFirst, line);
1780                            } else {
1781                                self.compile_expr(init)?;
1782                            }
1783                        } else {
1784                            self.chunk.emit(Op::LoadUndef, line);
1785                        }
1786                        if is_my {
1787                            let name_idx = self.chunk.intern_name(&decl.name);
1788                            if let Some(ref ty) = decl.type_annotation {
1789                                self.emit_declare_scalar_typed(name_idx, ty, line, frozen);
1790                            } else {
1791                                self.emit_declare_scalar(name_idx, line, frozen);
1792                            }
1793                        } else {
1794                            if decl.type_annotation.is_some() {
1795                                return Err(CompileError::Unsupported("typed our".into()));
1796                            }
1797                            self.emit_declare_our_scalar(&decl.name, line, false);
1798                        }
1799                    }
1800                    Sigil::Array => {
1801                        let name_idx = self
1802                            .chunk
1803                            .intern_name(&self.qualify_stash_array_name(&decl.name));
1804                        if let Some(init) = &decl.initializer {
1805                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
1806                        } else {
1807                            self.chunk.emit(Op::LoadUndef, line);
1808                        }
1809                        self.emit_declare_array(name_idx, line, frozen);
1810                    }
1811                    Sigil::Hash => {
1812                        let name_idx = self.chunk.intern_name(&decl.name);
1813                        if let Some(init) = &decl.initializer {
1814                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
1815                        } else {
1816                            self.chunk.emit(Op::LoadUndef, line);
1817                        }
1818                        self.emit_declare_hash(name_idx, line, frozen);
1819                    }
1820                    Sigil::Typeglob => {
1821                        return Err(CompileError::Unsupported("my/our *GLOB".into()));
1822                    }
1823                }
1824            }
1825        }
1826        Ok(())
1827    }
1828
1829    fn compile_state_declarations(
1830        &mut self,
1831        decls: &[VarDecl],
1832        line: usize,
1833    ) -> Result<(), CompileError> {
1834        for decl in decls {
1835            match decl.sigil {
1836                Sigil::Scalar => {
1837                    if let Some(init) = &decl.initializer {
1838                        self.compile_expr(init)?;
1839                    } else {
1840                        self.chunk.emit(Op::LoadUndef, line);
1841                    }
1842                    let name_idx = self.chunk.intern_name(&decl.name);
1843                    let name = self.chunk.names[name_idx as usize].clone();
1844                    self.register_declare(Sigil::Scalar, &name, false);
1845                    self.chunk.emit(Op::DeclareStateScalar(name_idx), line);
1846                }
1847                Sigil::Array => {
1848                    let name_idx = self
1849                        .chunk
1850                        .intern_name(&self.qualify_stash_array_name(&decl.name));
1851                    if let Some(init) = &decl.initializer {
1852                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
1853                    } else {
1854                        self.chunk.emit(Op::LoadUndef, line);
1855                    }
1856                    self.chunk.emit(Op::DeclareStateArray(name_idx), line);
1857                }
1858                Sigil::Hash => {
1859                    let name_idx = self.chunk.intern_name(&decl.name);
1860                    if let Some(init) = &decl.initializer {
1861                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
1862                    } else {
1863                        self.chunk.emit(Op::LoadUndef, line);
1864                    }
1865                    self.chunk.emit(Op::DeclareStateHash(name_idx), line);
1866                }
1867                Sigil::Typeglob => {
1868                    return Err(CompileError::Unsupported("state *GLOB".into()));
1869                }
1870            }
1871        }
1872        Ok(())
1873    }
1874
1875    fn compile_local_declarations(
1876        &mut self,
1877        decls: &[VarDecl],
1878        line: usize,
1879    ) -> Result<(), CompileError> {
1880        if decls.iter().any(|d| d.type_annotation.is_some()) {
1881            return Err(CompileError::Unsupported("typed local".into()));
1882        }
1883        if decls.len() > 1 && decls[0].initializer.is_some() {
1884            self.compile_expr_ctx(decls[0].initializer.as_ref().unwrap(), WantarrayCtx::List)?;
1885            let tmp_name = self.chunk.intern_name("__list_assign_tmp__");
1886            self.emit_declare_array(tmp_name, line, false);
1887            for (i, decl) in decls.iter().enumerate() {
1888                match decl.sigil {
1889                    Sigil::Scalar => {
1890                        let name_idx = self.intern_scalar_for_local(&decl.name);
1891                        self.chunk.emit(Op::LoadInt(i as i64), line);
1892                        self.chunk.emit(Op::GetArrayElem(tmp_name), line);
1893                        self.chunk.emit(Op::LocalDeclareScalar(name_idx), line);
1894                    }
1895                    Sigil::Array => {
1896                        let q = self.qualify_stash_array_name(&decl.name);
1897                        let name_idx = self.chunk.intern_name(&q);
1898                        self.chunk.emit(Op::GetArray(tmp_name), line);
1899                        self.chunk.emit(Op::LocalDeclareArray(name_idx), line);
1900                    }
1901                    Sigil::Hash => {
1902                        let name_idx = self.chunk.intern_name(&decl.name);
1903                        self.chunk.emit(Op::GetArray(tmp_name), line);
1904                        self.chunk.emit(Op::LocalDeclareHash(name_idx), line);
1905                    }
1906                    Sigil::Typeglob => {
1907                        return Err(CompileError::Unsupported(
1908                            "local (*a,*b,...) with list initializer and typeglob".into(),
1909                        ));
1910                    }
1911                }
1912            }
1913        } else {
1914            for decl in decls {
1915                match decl.sigil {
1916                    Sigil::Scalar => {
1917                        let name_idx = self.intern_scalar_for_local(&decl.name);
1918                        if let Some(init) = &decl.initializer {
1919                            self.compile_expr(init)?;
1920                        } else {
1921                            self.chunk.emit(Op::LoadUndef, line);
1922                        }
1923                        self.chunk.emit(Op::LocalDeclareScalar(name_idx), line);
1924                    }
1925                    Sigil::Array => {
1926                        let q = self.qualify_stash_array_name(&decl.name);
1927                        let name_idx = self.chunk.intern_name(&q);
1928                        if let Some(init) = &decl.initializer {
1929                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
1930                        } else {
1931                            self.chunk.emit(Op::LoadUndef, line);
1932                        }
1933                        self.chunk.emit(Op::LocalDeclareArray(name_idx), line);
1934                    }
1935                    Sigil::Hash => {
1936                        let name_idx = self.chunk.intern_name(&decl.name);
1937                        if let Some(init) = &decl.initializer {
1938                            self.compile_expr_ctx(init, WantarrayCtx::List)?;
1939                        } else {
1940                            self.chunk.emit(Op::LoadUndef, line);
1941                        }
1942                        self.chunk.emit(Op::LocalDeclareHash(name_idx), line);
1943                    }
1944                    Sigil::Typeglob => {
1945                        let name_idx = self.chunk.intern_name(&decl.name);
1946                        if let Some(init) = &decl.initializer {
1947                            let ExprKind::Typeglob(rhs) = &init.kind else {
1948                                return Err(CompileError::Unsupported(
1949                                    "local *GLOB = non-typeglob".into(),
1950                                ));
1951                            };
1952                            let rhs_idx = self.chunk.intern_name(rhs);
1953                            self.chunk
1954                                .emit(Op::LocalDeclareTypeglob(name_idx, Some(rhs_idx)), line);
1955                        } else {
1956                            self.chunk
1957                                .emit(Op::LocalDeclareTypeglob(name_idx, None), line);
1958                        }
1959                    }
1960                }
1961            }
1962        }
1963        Ok(())
1964    }
1965
1966    /// `oursync $x` — package-global counterpart of `mysync`. Wraps the value in
1967    /// `Arc<Mutex<StrykeValue>>` (or `AtomicArray` / `AtomicHash`) like `mysync`, but
1968    /// keys the binding by the package-qualified stash name (`Pkg::x`) so all
1969    /// packages and parallel workers share one cell. Mirrors [`Self::emit_declare_our_scalar`]
1970    /// for name qualification + `register_declare_our_scalar` so later `$x` references
1971    /// rewrite to `Pkg::x` via [`Self::scalar_storage_name_for_ops`].
1972    fn compile_oursync_declarations(
1973        &mut self,
1974        decls: &[VarDecl],
1975        line: usize,
1976    ) -> Result<(), CompileError> {
1977        for decl in decls {
1978            if decl.type_annotation.is_some() {
1979                return Err(CompileError::Unsupported("typed oursync".into()));
1980            }
1981            match decl.sigil {
1982                Sigil::Typeglob => {
1983                    return Err(CompileError::Unsupported(
1984                        "`oursync` does not support typeglob variables".into(),
1985                    ));
1986                }
1987                Sigil::Scalar => {
1988                    if let Some(init) = &decl.initializer {
1989                        self.compile_expr(init)?;
1990                    } else {
1991                        self.chunk.emit(Op::LoadUndef, line);
1992                    }
1993                    let stash = self.qualify_stash_scalar_name(&decl.name);
1994                    let name_idx = self.chunk.intern_name(&stash);
1995                    self.register_declare_our_scalar(&decl.name);
1996                    if let Some(layer) = self.scope_stack.last_mut() {
1997                        layer.mysync_scalars.insert(stash);
1998                    }
1999                    self.chunk.emit(Op::DeclareOurSyncScalar(name_idx), line);
2000                }
2001                Sigil::Array => {
2002                    let stash = self.qualify_stash_array_name(&decl.name);
2003                    if let Some(init) = &decl.initializer {
2004                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2005                    } else {
2006                        self.chunk.emit(Op::LoadUndef, line);
2007                    }
2008                    let name_idx = self.chunk.intern_name(&stash);
2009                    self.register_declare(Sigil::Array, &stash, false);
2010                    if let Some(layer) = self.scope_stack.last_mut() {
2011                        layer.mysync_arrays.insert(stash);
2012                    }
2013                    self.chunk.emit(Op::DeclareOurSyncArray(name_idx), line);
2014                }
2015                Sigil::Hash => {
2016                    if let Some(init) = &decl.initializer {
2017                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2018                    } else {
2019                        self.chunk.emit(Op::LoadUndef, line);
2020                    }
2021                    // Hashes follow the existing `our %h` convention and use the
2022                    // bare name in bytecode (compiler.rs:1722). Cross-package
2023                    // hash access has separate quirks tracked elsewhere.
2024                    let name_idx = self.chunk.intern_name(&decl.name);
2025                    self.register_declare(Sigil::Hash, &decl.name, false);
2026                    if let Some(layer) = self.scope_stack.last_mut() {
2027                        layer.mysync_hashes.insert(decl.name.clone());
2028                    }
2029                    self.chunk.emit(Op::DeclareOurSyncHash(name_idx), line);
2030                }
2031            }
2032        }
2033        Ok(())
2034    }
2035
2036    fn compile_mysync_declarations(
2037        &mut self,
2038        decls: &[VarDecl],
2039        line: usize,
2040    ) -> Result<(), CompileError> {
2041        for decl in decls {
2042            if decl.type_annotation.is_some() {
2043                return Err(CompileError::Unsupported("typed mysync".into()));
2044            }
2045            match decl.sigil {
2046                Sigil::Typeglob => {
2047                    return Err(CompileError::Unsupported(
2048                        "`mysync` does not support typeglob variables".into(),
2049                    ));
2050                }
2051                Sigil::Scalar => {
2052                    if let Some(init) = &decl.initializer {
2053                        self.compile_expr(init)?;
2054                    } else {
2055                        self.chunk.emit(Op::LoadUndef, line);
2056                    }
2057                    let name_idx = self.chunk.intern_name(&decl.name);
2058                    self.register_declare(Sigil::Scalar, &decl.name, false);
2059                    self.chunk.emit(Op::DeclareMySyncScalar(name_idx), line);
2060                    if let Some(layer) = self.scope_stack.last_mut() {
2061                        layer.mysync_scalars.insert(decl.name.clone());
2062                    }
2063                }
2064                Sigil::Array => {
2065                    let stash = self.qualify_stash_array_name(&decl.name);
2066                    if let Some(init) = &decl.initializer {
2067                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2068                    } else {
2069                        self.chunk.emit(Op::LoadUndef, line);
2070                    }
2071                    let name_idx = self.chunk.intern_name(&stash);
2072                    self.register_declare(Sigil::Array, &stash, false);
2073                    self.chunk.emit(Op::DeclareMySyncArray(name_idx), line);
2074                    if let Some(layer) = self.scope_stack.last_mut() {
2075                        layer.mysync_arrays.insert(stash);
2076                    }
2077                }
2078                Sigil::Hash => {
2079                    if let Some(init) = &decl.initializer {
2080                        self.compile_expr_ctx(init, WantarrayCtx::List)?;
2081                    } else {
2082                        self.chunk.emit(Op::LoadUndef, line);
2083                    }
2084                    let name_idx = self.chunk.intern_name(&decl.name);
2085                    self.register_declare(Sigil::Hash, &decl.name, false);
2086                    self.chunk.emit(Op::DeclareMySyncHash(name_idx), line);
2087                    if let Some(layer) = self.scope_stack.last_mut() {
2088                        layer.mysync_hashes.insert(decl.name.clone());
2089                    }
2090                }
2091            }
2092        }
2093        Ok(())
2094    }
2095
2096    /// `local $h{k} = …` / `local $SIG{__WARN__}` — not plain [`StmtKind::Local`] declarations.
2097    fn compile_local_expr(
2098        &mut self,
2099        target: &Expr,
2100        initializer: Option<&Expr>,
2101        line: usize,
2102    ) -> Result<(), CompileError> {
2103        match &target.kind {
2104            ExprKind::HashElement { hash, key } => {
2105                self.check_strict_hash_access(hash, line)?;
2106                self.check_hash_mutable(hash, line)?;
2107                let hash_idx = self.chunk.intern_name(hash);
2108                if let Some(init) = initializer {
2109                    self.compile_expr(init)?;
2110                } else {
2111                    self.chunk.emit(Op::LoadUndef, line);
2112                }
2113                self.compile_expr(key)?;
2114                self.chunk.emit(Op::LocalDeclareHashElement(hash_idx), line);
2115                Ok(())
2116            }
2117            ExprKind::ArrayElement { array, index } => {
2118                self.check_strict_array_access(array, line)?;
2119                let q = self.qualify_stash_array_name(array);
2120                self.check_array_mutable(&q, line)?;
2121                let arr_idx = self.chunk.intern_name(&q);
2122                if let Some(init) = initializer {
2123                    self.compile_expr(init)?;
2124                } else {
2125                    self.chunk.emit(Op::LoadUndef, line);
2126                }
2127                self.compile_expr(index)?;
2128                self.chunk.emit(Op::LocalDeclareArrayElement(arr_idx), line);
2129                Ok(())
2130            }
2131            ExprKind::Typeglob(name) => {
2132                let lhs_idx = self.chunk.intern_name(name);
2133                if let Some(init) = initializer {
2134                    let ExprKind::Typeglob(rhs) = &init.kind else {
2135                        return Err(CompileError::Unsupported(
2136                            "local *GLOB = non-typeglob".into(),
2137                        ));
2138                    };
2139                    let rhs_idx = self.chunk.intern_name(rhs);
2140                    self.chunk
2141                        .emit(Op::LocalDeclareTypeglob(lhs_idx, Some(rhs_idx)), line);
2142                } else {
2143                    self.chunk
2144                        .emit(Op::LocalDeclareTypeglob(lhs_idx, None), line);
2145                }
2146                Ok(())
2147            }
2148            ExprKind::Deref {
2149                expr,
2150                kind: Sigil::Typeglob,
2151            } => {
2152                if let Some(init) = initializer {
2153                    let ExprKind::Typeglob(rhs) = &init.kind else {
2154                        return Err(CompileError::Unsupported(
2155                            "local *GLOB = non-typeglob".into(),
2156                        ));
2157                    };
2158                    let rhs_idx = self.chunk.intern_name(rhs);
2159                    self.compile_expr(expr)?;
2160                    self.chunk
2161                        .emit(Op::LocalDeclareTypeglobDynamic(Some(rhs_idx)), line);
2162                } else {
2163                    self.compile_expr(expr)?;
2164                    self.chunk.emit(Op::LocalDeclareTypeglobDynamic(None), line);
2165                }
2166                Ok(())
2167            }
2168            ExprKind::TypeglobExpr(expr) => {
2169                if let Some(init) = initializer {
2170                    let ExprKind::Typeglob(rhs) = &init.kind else {
2171                        return Err(CompileError::Unsupported(
2172                            "local *GLOB = non-typeglob".into(),
2173                        ));
2174                    };
2175                    let rhs_idx = self.chunk.intern_name(rhs);
2176                    self.compile_expr(expr)?;
2177                    self.chunk
2178                        .emit(Op::LocalDeclareTypeglobDynamic(Some(rhs_idx)), line);
2179                } else {
2180                    self.compile_expr(expr)?;
2181                    self.chunk.emit(Op::LocalDeclareTypeglobDynamic(None), line);
2182                }
2183                Ok(())
2184            }
2185            ExprKind::ScalarVar(name) => {
2186                let name_idx = self.intern_scalar_for_local(name);
2187                if let Some(init) = initializer {
2188                    self.compile_expr(init)?;
2189                } else {
2190                    self.chunk.emit(Op::LoadUndef, line);
2191                }
2192                self.chunk.emit(Op::LocalDeclareScalar(name_idx), line);
2193                Ok(())
2194            }
2195            ExprKind::ArrayVar(name) => {
2196                self.check_strict_array_access(name, line)?;
2197                let q = self.qualify_stash_array_name(name);
2198                let name_idx = self.chunk.intern_name(&q);
2199                if let Some(init) = initializer {
2200                    self.compile_expr_ctx(init, WantarrayCtx::List)?;
2201                } else {
2202                    self.chunk.emit(Op::LoadUndef, line);
2203                }
2204                self.chunk.emit(Op::LocalDeclareArray(name_idx), line);
2205                Ok(())
2206            }
2207            ExprKind::HashVar(name) => {
2208                let name_idx = self.chunk.intern_name(name);
2209                if let Some(init) = initializer {
2210                    self.compile_expr_ctx(init, WantarrayCtx::List)?;
2211                } else {
2212                    self.chunk.emit(Op::LoadUndef, line);
2213                }
2214                self.chunk.emit(Op::LocalDeclareHash(name_idx), line);
2215                Ok(())
2216            }
2217            _ => Err(CompileError::Unsupported("local on this lvalue".into())),
2218        }
2219    }
2220
2221    fn compile_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
2222        // A `LABEL:` on a statement binds the label to the IP of the first op emitted for that
2223        // statement, so that `goto LABEL` can jump to the effective start of execution.
2224        if let Some(lbl) = &stmt.label {
2225            self.record_stmt_label(lbl);
2226        }
2227        let line = stmt.line;
2228        match &stmt.kind {
2229            StmtKind::FormatDecl { name, lines } => {
2230                let idx = self.chunk.add_format_decl(name.clone(), lines.clone());
2231                self.chunk.emit(Op::FormatDecl(idx), line);
2232            }
2233            StmtKind::Expression(expr) => {
2234                self.compile_expr_ctx(expr, WantarrayCtx::Void)?;
2235                self.chunk.emit(Op::Pop, line);
2236            }
2237            StmtKind::Local(decls) => self.compile_local_declarations(decls, line)?,
2238            StmtKind::LocalExpr {
2239                target,
2240                initializer,
2241            } => {
2242                self.compile_local_expr(target, initializer.as_ref(), line)?;
2243            }
2244            StmtKind::MySync(decls) => self.compile_mysync_declarations(decls, line)?,
2245            StmtKind::OurSync(decls) => self.compile_oursync_declarations(decls, line)?,
2246            StmtKind::My(decls) => self.compile_var_declarations(decls, line, true)?,
2247            StmtKind::Our(decls) => self.compile_var_declarations(decls, line, false)?,
2248            StmtKind::State(decls) => self.compile_state_declarations(decls, line)?,
2249            StmtKind::If {
2250                condition,
2251                body,
2252                elsifs,
2253                else_block,
2254            } => {
2255                self.compile_boolean_rvalue_condition(condition)?;
2256                let jump_else = self.chunk.emit(Op::JumpIfFalse(0), line);
2257                self.compile_block(body)?;
2258                let mut end_jumps = vec![self.chunk.emit(Op::Jump(0), line)];
2259                self.chunk.patch_jump_here(jump_else);
2260
2261                for (cond, blk) in elsifs {
2262                    self.compile_boolean_rvalue_condition(cond)?;
2263                    let j = self.chunk.emit(Op::JumpIfFalse(0), cond.line);
2264                    self.compile_block(blk)?;
2265                    end_jumps.push(self.chunk.emit(Op::Jump(0), cond.line));
2266                    self.chunk.patch_jump_here(j);
2267                }
2268
2269                if let Some(eb) = else_block {
2270                    self.compile_block(eb)?;
2271                }
2272                for j in end_jumps {
2273                    self.chunk.patch_jump_here(j);
2274                }
2275            }
2276            StmtKind::Unless {
2277                condition,
2278                body,
2279                else_block,
2280            } => {
2281                self.compile_boolean_rvalue_condition(condition)?;
2282                let jump_else = self.chunk.emit(Op::JumpIfTrue(0), line);
2283                self.compile_block(body)?;
2284                if let Some(eb) = else_block {
2285                    let end_j = self.chunk.emit(Op::Jump(0), line);
2286                    self.chunk.patch_jump_here(jump_else);
2287                    self.compile_block(eb)?;
2288                    self.chunk.patch_jump_here(end_j);
2289                } else {
2290                    self.chunk.patch_jump_here(jump_else);
2291                }
2292            }
2293            StmtKind::While {
2294                condition,
2295                body,
2296                label,
2297                continue_block,
2298            } => {
2299                let loop_start = self.chunk.len();
2300                self.compile_boolean_rvalue_condition(condition)?;
2301                let exit_jump = self.chunk.emit(Op::JumpIfFalse(0), line);
2302                let body_start_ip = self.chunk.len();
2303
2304                self.loop_stack.push(LoopCtx {
2305                    label: label.clone(),
2306                    entry_frame_depth: self.frame_depth,
2307                    entry_try_depth: self.try_depth,
2308                    body_start_ip,
2309                    break_jumps: vec![],
2310                    continue_jumps: vec![],
2311                });
2312                self.compile_block_no_frame(body)?;
2313                // `continue { ... }` runs both on normal fall-through from the body and on
2314                // `next` (continue_jumps). `last` still bypasses it via break_jumps.
2315                let continue_entry = self.chunk.len();
2316                let cont_jumps =
2317                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2318                for j in cont_jumps {
2319                    self.chunk.patch_jump_to(j, continue_entry);
2320                }
2321                if let Some(cb) = continue_block {
2322                    self.compile_block_no_frame(cb)?;
2323                }
2324                self.chunk.emit(Op::Jump(loop_start), line);
2325                self.chunk.patch_jump_here(exit_jump);
2326                let ctx = self.loop_stack.pop().expect("loop");
2327                for j in ctx.break_jumps {
2328                    self.chunk.patch_jump_here(j);
2329                }
2330            }
2331            StmtKind::Until {
2332                condition,
2333                body,
2334                label,
2335                continue_block,
2336            } => {
2337                let loop_start = self.chunk.len();
2338                self.compile_boolean_rvalue_condition(condition)?;
2339                let exit_jump = self.chunk.emit(Op::JumpIfTrue(0), line);
2340                let body_start_ip = self.chunk.len();
2341
2342                self.loop_stack.push(LoopCtx {
2343                    label: label.clone(),
2344                    entry_frame_depth: self.frame_depth,
2345                    entry_try_depth: self.try_depth,
2346                    body_start_ip,
2347                    break_jumps: vec![],
2348                    continue_jumps: vec![],
2349                });
2350                self.compile_block_no_frame(body)?;
2351                let continue_entry = self.chunk.len();
2352                let cont_jumps =
2353                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2354                for j in cont_jumps {
2355                    self.chunk.patch_jump_to(j, continue_entry);
2356                }
2357                if let Some(cb) = continue_block {
2358                    self.compile_block_no_frame(cb)?;
2359                }
2360                self.chunk.emit(Op::Jump(loop_start), line);
2361                self.chunk.patch_jump_here(exit_jump);
2362                let ctx = self.loop_stack.pop().expect("loop");
2363                for j in ctx.break_jumps {
2364                    self.chunk.patch_jump_here(j);
2365                }
2366            }
2367            StmtKind::For {
2368                init,
2369                condition,
2370                step,
2371                body,
2372                label,
2373                continue_block,
2374            } => {
2375                // When the enclosing scope uses scalar slots, skip PushFrame/PopFrame for the
2376                // C-style `for` so loop variables (`$i`) and outer variables (`$sum`) share the
2377                // same runtime frame and are both accessible via O(1) slot ops.  The compiler's
2378                // scope layer still tracks `my` declarations for name resolution; only the runtime
2379                // frame push is elided.
2380                let outer_has_slots = self.scope_stack.last().is_some_and(|l| l.use_slots);
2381                if !outer_has_slots {
2382                    self.emit_push_frame(line);
2383                }
2384                if let Some(init) = init {
2385                    self.compile_statement(init)?;
2386                }
2387                let loop_start = self.chunk.len();
2388                let cond_exit = if let Some(cond) = condition {
2389                    self.compile_boolean_rvalue_condition(cond)?;
2390                    Some(self.chunk.emit(Op::JumpIfFalse(0), line))
2391                } else {
2392                    None
2393                };
2394                let body_start_ip = self.chunk.len();
2395
2396                self.loop_stack.push(LoopCtx {
2397                    label: label.clone(),
2398                    entry_frame_depth: self.frame_depth,
2399                    entry_try_depth: self.try_depth,
2400                    body_start_ip,
2401                    break_jumps: cond_exit.into_iter().collect(),
2402                    continue_jumps: vec![],
2403                });
2404                self.compile_block_no_frame(body)?;
2405
2406                let continue_entry = self.chunk.len();
2407                let cont_jumps =
2408                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2409                for j in cont_jumps {
2410                    self.chunk.patch_jump_to(j, continue_entry);
2411                }
2412                if let Some(cb) = continue_block {
2413                    self.compile_block_no_frame(cb)?;
2414                }
2415                if let Some(step) = step {
2416                    self.compile_expr(step)?;
2417                    self.chunk.emit(Op::Pop, line);
2418                }
2419                self.chunk.emit(Op::Jump(loop_start), line);
2420
2421                let ctx = self.loop_stack.pop().expect("loop");
2422                for j in ctx.break_jumps {
2423                    self.chunk.patch_jump_here(j);
2424                }
2425                if !outer_has_slots {
2426                    self.emit_pop_frame(line);
2427                }
2428            }
2429            StmtKind::Foreach {
2430                var,
2431                list,
2432                body,
2433                label,
2434                continue_block,
2435            } => {
2436                // Perl `for ARRAY` aliases the loop variable to each array
2437                // element — mutations through the loop var propagate back to
2438                // the array. We approximate by detecting a bare-`@arr` source
2439                // and emitting a write-back step at the end of each iteration
2440                // (before the counter increment, after the body and any
2441                // continue block). Complex sources (lists, ranges, `keys`)
2442                // keep copy semantics, matching Perl. (BUG-019)
2443                let alias_array_name_idx: Option<u16> = match &list.kind {
2444                    ExprKind::ArrayVar(name) => Some(self.chunk.intern_name(name)),
2445                    _ => None,
2446                };
2447                // PushFrame isolates __foreach_list__ / __foreach_i__ from outer/nested loops.
2448                self.emit_push_frame(line);
2449                self.compile_expr_ctx(list, WantarrayCtx::List)?;
2450                let list_name = self.chunk.intern_name("__foreach_list__");
2451                self.chunk.emit(Op::DeclareArray(list_name), line);
2452
2453                // Counter and loop variable go in slots so the hot per-iteration ops
2454                // (`GetScalarSlot` / `PreIncSlot`) skip the linear frame-scalar scan.
2455                // We cache the slot indices before compiling the body so that any
2456                // nested foreach / inner `my` that reallocates the same name in the
2457                // shared scope layer cannot poison our post-body increment op.
2458                let counter_name = self.chunk.intern_name("__foreach_i__");
2459                self.chunk.emit(Op::LoadInt(0), line);
2460                let counter_slot_opt = self.assign_scalar_slot("__foreach_i__");
2461                if let Some(slot) = counter_slot_opt {
2462                    self.chunk
2463                        .emit(Op::DeclareScalarSlot(slot, counter_name), line);
2464                } else {
2465                    self.chunk.emit(Op::DeclareScalar(counter_name), line);
2466                }
2467
2468                let var_name = self.chunk.intern_name(var);
2469                self.register_declare(Sigil::Scalar, var, false);
2470                self.chunk.emit(Op::LoadUndef, line);
2471                // `$_` is the global topic — keep it in the frame scalars so bareword calls
2472                // and `print`/`printf` arg-defaulting still see it via the usual special-var
2473                // path. Slotting it breaks callees that read `$_` across the call boundary.
2474                let var_slot_opt = if var == "_" {
2475                    None
2476                } else {
2477                    self.assign_scalar_slot(var)
2478                };
2479                if let Some(slot) = var_slot_opt {
2480                    self.chunk.emit(Op::DeclareScalarSlot(slot, var_name), line);
2481                } else {
2482                    self.chunk.emit(Op::DeclareScalar(var_name), line);
2483                }
2484
2485                let loop_start = self.chunk.len();
2486                // Check: $i < scalar @list
2487                if let Some(s) = counter_slot_opt {
2488                    self.chunk.emit(Op::GetScalarSlot(s), line);
2489                } else {
2490                    self.emit_get_scalar(counter_name, line, None);
2491                }
2492                self.chunk.emit(Op::ArrayLen(list_name), line);
2493                self.chunk.emit(Op::NumLt, line);
2494                let exit_jump = self.chunk.emit(Op::JumpIfFalse(0), line);
2495
2496                // $var = $list[$i]
2497                if let Some(s) = counter_slot_opt {
2498                    self.chunk.emit(Op::GetScalarSlot(s), line);
2499                } else {
2500                    self.emit_get_scalar(counter_name, line, None);
2501                }
2502                self.chunk.emit(Op::GetArrayElem(list_name), line);
2503                if let Some(s) = var_slot_opt {
2504                    self.chunk.emit(Op::SetScalarSlot(s), line);
2505                } else {
2506                    self.emit_set_scalar(var_name, line, None);
2507                }
2508                let body_start_ip = self.chunk.len();
2509
2510                self.loop_stack.push(LoopCtx {
2511                    label: label.clone(),
2512                    entry_frame_depth: self.frame_depth,
2513                    entry_try_depth: self.try_depth,
2514                    body_start_ip,
2515                    break_jumps: vec![],
2516                    continue_jumps: vec![],
2517                });
2518                self.compile_block_no_frame(body)?;
2519                // `continue { ... }` on foreach runs after each iteration body (and on `next`),
2520                // before the iterator increment.
2521                let step_ip = self.chunk.len();
2522                let cont_jumps =
2523                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2524                for j in cont_jumps {
2525                    self.chunk.patch_jump_to(j, step_ip);
2526                }
2527                // Alias write-back: $arr[$i] = $loopvar; (BUG-019). Runs at
2528                // the merged step_ip target so both normal-completion and
2529                // `next` paths write the loop var's current value back to the
2530                // source array element.
2531                if let Some(arr_idx) = alias_array_name_idx {
2532                    if let Some(s) = var_slot_opt {
2533                        self.chunk.emit(Op::GetScalarSlot(s), line);
2534                    } else {
2535                        self.emit_get_scalar(var_name, line, None);
2536                    }
2537                    if let Some(s) = counter_slot_opt {
2538                        self.chunk.emit(Op::GetScalarSlot(s), line);
2539                    } else {
2540                        self.emit_get_scalar(counter_name, line, None);
2541                    }
2542                    self.chunk.emit(Op::SetArrayElem(arr_idx), line);
2543                }
2544                if let Some(cb) = continue_block {
2545                    self.compile_block_no_frame(cb)?;
2546                }
2547
2548                // $i++ — use the cached slot directly. The scope layer's scalar_slots
2549                // map may now point `__foreach_i__` at a nested foreach's slot (if any),
2550                // so we must NOT re-resolve through `emit_pre_inc(counter_name)`.
2551                if let Some(s) = counter_slot_opt {
2552                    self.chunk.emit(Op::PreIncSlot(s), line);
2553                } else {
2554                    self.emit_pre_inc(counter_name, line, None);
2555                }
2556                self.chunk.emit(Op::Pop, line);
2557                self.chunk.emit(Op::Jump(loop_start), line);
2558
2559                self.chunk.patch_jump_here(exit_jump);
2560                let ctx = self.loop_stack.pop().expect("loop");
2561                for j in ctx.break_jumps {
2562                    self.chunk.patch_jump_here(j);
2563                }
2564                self.emit_pop_frame(line);
2565            }
2566            StmtKind::DoWhile { body, condition } => {
2567                let loop_start = self.chunk.len();
2568                self.loop_stack.push(LoopCtx {
2569                    label: None,
2570                    entry_frame_depth: self.frame_depth,
2571                    entry_try_depth: self.try_depth,
2572                    body_start_ip: loop_start,
2573                    break_jumps: vec![],
2574                    continue_jumps: vec![],
2575                });
2576                self.compile_block_no_frame(body)?;
2577                let cont_jumps =
2578                    std::mem::take(&mut self.loop_stack.last_mut().expect("loop").continue_jumps);
2579                for j in cont_jumps {
2580                    self.chunk.patch_jump_to(j, loop_start);
2581                }
2582                self.compile_boolean_rvalue_condition(condition)?;
2583                let exit_jump = self.chunk.emit(Op::JumpIfFalse(0), line);
2584                self.chunk.emit(Op::Jump(loop_start), line);
2585                self.chunk.patch_jump_here(exit_jump);
2586                let ctx = self.loop_stack.pop().expect("loop");
2587                for j in ctx.break_jumps {
2588                    self.chunk.patch_jump_here(j);
2589                }
2590            }
2591            StmtKind::Goto { target } => {
2592                // `goto LABEL` where LABEL is a compile-time-known bareword/string: emit a
2593                // forward `Jump(0)` and record it for patching when the current goto-scope
2594                // exits. `goto &sub` and `goto $expr` (dynamic target) stay Unsupported.
2595                if !self.try_emit_goto_label(target, line) {
2596                    return Err(CompileError::Unsupported(
2597                        "goto with dynamic or sub-ref target".into(),
2598                    ));
2599                }
2600            }
2601            StmtKind::Continue(block) => {
2602                // A bare `continue { ... }` statement (no attached loop) is a parser edge case:
2603                // Perl just runs the block (`Interpreter::exec_block_smart`).
2604                // Match that in the VM path.
2605                for stmt in block {
2606                    self.compile_statement(stmt)?;
2607                }
2608            }
2609            StmtKind::Return(val) => {
2610                if let Some(expr) = val {
2611                    // `return` propagates the caller's wantarray context, so the
2612                    // operand should be evaluated in list context here. Caller-
2613                    // side scalar coercion (Op::CallSub in scalar slot) takes the
2614                    // last element from the returned list — matching Perl's
2615                    // `return (1, 2, 3)` semantics. (BUG-010)
2616                    match &expr.kind {
2617                        ExprKind::Range { .. }
2618                        | ExprKind::SliceRange { .. }
2619                        | ExprKind::ArrayVar(_)
2620                        | ExprKind::List(_)
2621                        | ExprKind::HashVar(_)
2622                        | ExprKind::HashSlice { .. }
2623                        | ExprKind::HashKvSlice { .. }
2624                        | ExprKind::ArraySlice { .. } => {
2625                            self.compile_expr_ctx(expr, WantarrayCtx::List)?;
2626                        }
2627                        _ => {
2628                            self.compile_expr(expr)?;
2629                        }
2630                    }
2631                    self.chunk.emit(Op::ReturnValue, line);
2632                } else {
2633                    self.chunk.emit(Op::Return, line);
2634                }
2635            }
2636            StmtKind::Last(label) | StmtKind::Next(label) => {
2637                // Resolve the target loop via `self.loop_stack` — walk from the innermost loop
2638                // outward, picking the first one that matches the label (or the innermost if
2639                // `last`/`next` has no label). Emit `(frame_depth - entry_frame_depth)`
2640                // `PopFrame` ops first so any intervening block / if-body frames are torn down
2641                // before the jump. `try { }` crossings still bail to tree (see `entry_try_depth`).
2642                let is_last = matches!(&stmt.kind, StmtKind::Last(_));
2643                // Search the loop stack (innermost → outermost) for a matching label.
2644                let (target_idx, entry_frame_depth, entry_try_depth) = {
2645                    let mut found: Option<(usize, usize, usize)> = None;
2646                    for (i, lc) in self.loop_stack.iter().enumerate().rev() {
2647                        let matches = match (label.as_deref(), lc.label.as_deref()) {
2648                            (None, _) => true, // unlabeled `last`/`next` targets innermost loop
2649                            (Some(l), Some(lcl)) => l == lcl,
2650                            (Some(_), None) => false,
2651                        };
2652                        if matches {
2653                            found = Some((i, lc.entry_frame_depth, lc.entry_try_depth));
2654                            break;
2655                        }
2656                    }
2657                    found.ok_or_else(|| {
2658                        CompileError::Unsupported(if label.is_some() {
2659                            format!(
2660                                "last/next with label `{}` — no matching loop in compile scope",
2661                                label.as_deref().unwrap_or("")
2662                            )
2663                        } else {
2664                            "last/next outside any loop".into()
2665                        })
2666                    })?
2667                };
2668                // Cross-try-frame flow control is not modeled in bytecode.
2669                if self.try_depth != entry_try_depth {
2670                    return Err(CompileError::Unsupported(
2671                        "last/next across try { } frame".into(),
2672                    ));
2673                }
2674                // Tear down any scope frames pushed since the loop was entered.
2675                let frames_to_pop = self.frame_depth.saturating_sub(entry_frame_depth);
2676                for _ in 0..frames_to_pop {
2677                    // Emit the `PopFrame` op without decrementing `self.frame_depth` — the
2678                    // compiler is still emitting code for the enclosing block which will later
2679                    // emit its own `PopFrame`; we only need the runtime pop here for the
2680                    // `last`/`next` control path.
2681                    self.chunk.emit(Op::PopFrame, line);
2682                }
2683                let j = self.chunk.emit(Op::Jump(0), line);
2684                let slot = &mut self.loop_stack[target_idx];
2685                if is_last {
2686                    slot.break_jumps.push(j);
2687                } else {
2688                    slot.continue_jumps.push(j);
2689                }
2690            }
2691            StmtKind::Redo(label) => {
2692                let (target_idx, entry_frame_depth, entry_try_depth) = {
2693                    let mut found: Option<(usize, usize, usize)> = None;
2694                    for (i, lc) in self.loop_stack.iter().enumerate().rev() {
2695                        let matches = match (label.as_deref(), lc.label.as_deref()) {
2696                            (None, _) => true,
2697                            (Some(l), Some(lcl)) => l == lcl,
2698                            (Some(_), None) => false,
2699                        };
2700                        if matches {
2701                            found = Some((i, lc.entry_frame_depth, lc.entry_try_depth));
2702                            break;
2703                        }
2704                    }
2705                    found.ok_or_else(|| {
2706                        CompileError::Unsupported(if label.is_some() {
2707                            format!(
2708                                "redo with label `{}` — no matching loop in compile scope",
2709                                label.as_deref().unwrap_or("")
2710                            )
2711                        } else {
2712                            "redo outside any loop".into()
2713                        })
2714                    })?
2715                };
2716                if self.try_depth != entry_try_depth {
2717                    return Err(CompileError::Unsupported(
2718                        "redo across try { } frame".into(),
2719                    ));
2720                }
2721                let frames_to_pop = self.frame_depth.saturating_sub(entry_frame_depth);
2722                for _ in 0..frames_to_pop {
2723                    self.chunk.emit(Op::PopFrame, line);
2724                }
2725                let body_start = self.loop_stack[target_idx].body_start_ip;
2726                let j = self.chunk.emit(Op::Jump(0), line);
2727                self.chunk.patch_jump_to(j, body_start);
2728            }
2729            StmtKind::Block(block) => {
2730                self.chunk.emit(Op::PushFrame, line);
2731                self.compile_block_inner(block)?;
2732                self.chunk.emit(Op::PopFrame, line);
2733            }
2734            StmtKind::StmtGroup(block) => {
2735                self.compile_block_no_frame(block)?;
2736            }
2737            StmtKind::Package { name } => {
2738                self.current_package = name.clone();
2739                let val_idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
2740                let name_idx = self.chunk.intern_name("__PACKAGE__");
2741                self.chunk.emit(Op::LoadConst(val_idx), line);
2742                self.emit_set_scalar(name_idx, line, None);
2743            }
2744            StmtKind::SubDecl {
2745                name,
2746                params,
2747                body,
2748                prototype,
2749            } => {
2750                let idx = self.chunk.runtime_sub_decls.len();
2751                if idx > u16::MAX as usize {
2752                    return Err(CompileError::Unsupported(
2753                        "too many runtime sub declarations in one chunk".into(),
2754                    ));
2755                }
2756                self.chunk.runtime_sub_decls.push(RuntimeSubDecl {
2757                    name: name.clone(),
2758                    params: params.clone(),
2759                    body: body.clone(),
2760                    prototype: prototype.clone(),
2761                });
2762                self.chunk.emit(Op::RuntimeSubDecl(idx as u16), line);
2763            }
2764            StmtKind::AdviceDecl {
2765                kind,
2766                pattern,
2767                body,
2768            } => {
2769                let idx = self.chunk.runtime_advice_decls.len();
2770                if idx > u16::MAX as usize {
2771                    return Err(CompileError::Unsupported(
2772                        "too many AOP advice declarations in one chunk".into(),
2773                    ));
2774                }
2775                // Register the body as a chunk block so the fourth-pass lowering
2776                // (`Compiler::compile_program` / `block_bytecode_ranges`) emits its
2777                // bytecode and `run_block_region` can dispatch it. This keeps the
2778                // body on the VM bytecode path — the tree-walker (`exec_block`) is
2779                // banned for advice. See `tests/tree_walker_absent_aop.rs`.
2780                let body_block_idx = self.add_deferred_block(body.clone());
2781                self.chunk.runtime_advice_decls.push(RuntimeAdviceDecl {
2782                    kind: *kind,
2783                    pattern: pattern.clone(),
2784                    body: body.clone(),
2785                    body_block_idx,
2786                });
2787                self.chunk.emit(Op::RegisterAdvice(idx as u16), line);
2788            }
2789            StmtKind::StructDecl { def } => {
2790                if self.chunk.struct_defs.iter().any(|d| d.name == def.name) {
2791                    return Err(CompileError::Unsupported(format!(
2792                        "duplicate struct `{}`",
2793                        def.name
2794                    )));
2795                }
2796                self.chunk.struct_defs.push(def.clone());
2797            }
2798            StmtKind::EnumDecl { def } => {
2799                if self.chunk.enum_defs.iter().any(|d| d.name == def.name) {
2800                    return Err(CompileError::Unsupported(format!(
2801                        "duplicate enum `{}`",
2802                        def.name
2803                    )));
2804                }
2805                self.chunk.enum_defs.push(def.clone());
2806            }
2807            StmtKind::ClassDecl { def } => {
2808                if self.chunk.class_defs.iter().any(|d| d.name == def.name) {
2809                    return Err(CompileError::Unsupported(format!(
2810                        "duplicate class `{}`",
2811                        def.name
2812                    )));
2813                }
2814                self.chunk.class_defs.push(def.clone());
2815            }
2816            StmtKind::TraitDecl { def } => {
2817                if self.chunk.trait_defs.iter().any(|d| d.name == def.name) {
2818                    return Err(CompileError::Unsupported(format!(
2819                        "duplicate trait `{}`",
2820                        def.name
2821                    )));
2822                }
2823                self.chunk.trait_defs.push(def.clone());
2824            }
2825            StmtKind::TryCatch {
2826                try_block,
2827                catch_var,
2828                catch_block,
2829                finally_block,
2830            } => {
2831                let catch_var_idx = self.chunk.intern_name(catch_var);
2832                let try_push_idx = self.chunk.emit(
2833                    Op::TryPush {
2834                        catch_ip: 0,
2835                        finally_ip: None,
2836                        after_ip: 0,
2837                        catch_var_idx,
2838                    },
2839                    line,
2840                );
2841                self.chunk.emit(Op::PushFrame, line);
2842                if self.program_last_stmt_takes_value {
2843                    self.emit_block_value(try_block, line)?;
2844                } else {
2845                    self.compile_block_inner(try_block)?;
2846                }
2847                self.chunk.emit(Op::PopFrame, line);
2848                self.chunk.emit(Op::TryContinueNormal, line);
2849
2850                let catch_start = self.chunk.len();
2851                self.chunk.patch_try_push_catch(try_push_idx, catch_start);
2852
2853                self.chunk.emit(Op::CatchReceive(catch_var_idx), line);
2854                if self.program_last_stmt_takes_value {
2855                    self.emit_block_value(catch_block, line)?;
2856                } else {
2857                    self.compile_block_inner(catch_block)?;
2858                }
2859                self.chunk.emit(Op::PopFrame, line);
2860                self.chunk.emit(Op::TryContinueNormal, line);
2861
2862                if let Some(fin) = finally_block {
2863                    let finally_start = self.chunk.len();
2864                    self.chunk
2865                        .patch_try_push_finally(try_push_idx, Some(finally_start));
2866                    self.chunk.emit(Op::PushFrame, line);
2867                    self.compile_block_inner(fin)?;
2868                    self.chunk.emit(Op::PopFrame, line);
2869                    self.chunk.emit(Op::TryFinallyEnd, line);
2870                }
2871                let merge = self.chunk.len();
2872                self.chunk.patch_try_push_after(try_push_idx, merge);
2873            }
2874            StmtKind::EvalTimeout { timeout, body } => {
2875                let idx = self
2876                    .chunk
2877                    .add_eval_timeout_entry(timeout.clone(), body.clone());
2878                self.chunk.emit(Op::EvalTimeout(idx), line);
2879            }
2880            StmtKind::Given { topic, body } => {
2881                let idx = self.chunk.add_given_entry(topic.clone(), body.clone());
2882                self.chunk.emit(Op::Given(idx), line);
2883            }
2884            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => {
2885                return Err(CompileError::Unsupported(
2886                    "`when` / `default` only valid inside `given`".into(),
2887                ));
2888            }
2889            StmtKind::Tie {
2890                target,
2891                class,
2892                args,
2893            } => {
2894                self.compile_expr(class)?;
2895                for a in args {
2896                    self.compile_expr(a)?;
2897                }
2898                let (kind, name_idx) = match target {
2899                    TieTarget::Scalar(s) => (0u8, self.chunk.intern_name(s)),
2900                    TieTarget::Array(a) => (1u8, self.chunk.intern_name(a)),
2901                    TieTarget::Hash(h) => (2u8, self.chunk.intern_name(h)),
2902                };
2903                let argc = (1 + args.len()) as u8;
2904                self.chunk.emit(
2905                    Op::Tie {
2906                        target_kind: kind,
2907                        name_idx,
2908                        argc,
2909                    },
2910                    line,
2911                );
2912            }
2913            StmtKind::UseOverload { pairs } => {
2914                let idx = self.chunk.add_use_overload(pairs.clone());
2915                self.chunk.emit(Op::UseOverload(idx), line);
2916            }
2917            StmtKind::Use { module, imports } => {
2918                // `use Env '@PATH'` declares variables that must be visible to strict checking.
2919                if module == "Env" {
2920                    Self::register_env_imports(
2921                        self.scope_stack.last_mut().expect("scope"),
2922                        imports,
2923                    );
2924                }
2925            }
2926            StmtKind::UsePerlVersion { .. }
2927            | StmtKind::No { .. }
2928            | StmtKind::Begin(_)
2929            | StmtKind::UnitCheck(_)
2930            | StmtKind::Check(_)
2931            | StmtKind::Init(_)
2932            | StmtKind::End(_)
2933            | StmtKind::Empty => {
2934                // No-ops or handled elsewhere
2935            }
2936        }
2937        Ok(())
2938    }
2939
2940    /// Returns true if the block contains a Return statement (directly, not in nested subs).
2941    fn block_has_return(block: &Block) -> bool {
2942        for stmt in block {
2943            match &stmt.kind {
2944                StmtKind::Return(_) => return true,
2945                StmtKind::If {
2946                    body,
2947                    elsifs,
2948                    else_block,
2949                    ..
2950                } => {
2951                    if Self::block_has_return(body) {
2952                        return true;
2953                    }
2954                    for (_, blk) in elsifs {
2955                        if Self::block_has_return(blk) {
2956                            return true;
2957                        }
2958                    }
2959                    if let Some(eb) = else_block {
2960                        if Self::block_has_return(eb) {
2961                            return true;
2962                        }
2963                    }
2964                }
2965                StmtKind::Unless {
2966                    body, else_block, ..
2967                } => {
2968                    if Self::block_has_return(body) {
2969                        return true;
2970                    }
2971                    if let Some(eb) = else_block {
2972                        if Self::block_has_return(eb) {
2973                            return true;
2974                        }
2975                    }
2976                }
2977                StmtKind::While { body, .. }
2978                | StmtKind::Until { body, .. }
2979                | StmtKind::Foreach { body, .. }
2980                    if Self::block_has_return(body) =>
2981                {
2982                    return true;
2983                }
2984                StmtKind::For { body, .. } if Self::block_has_return(body) => {
2985                    return true;
2986                }
2987                StmtKind::Block(blk) if Self::block_has_return(blk) => {
2988                    return true;
2989                }
2990                StmtKind::DoWhile { body, .. } if Self::block_has_return(body) => {
2991                    return true;
2992                }
2993                _ => {}
2994            }
2995        }
2996        false
2997    }
2998
2999    /// Returns true if the block contains a local declaration.
3000    fn block_has_local(block: &Block) -> bool {
3001        block.iter().any(|s| match &s.kind {
3002            StmtKind::Local(_) | StmtKind::LocalExpr { .. } => true,
3003            StmtKind::StmtGroup(inner) => Self::block_has_local(inner),
3004            _ => false,
3005        })
3006    }
3007
3008    fn compile_block(&mut self, block: &Block) -> Result<(), CompileError> {
3009        if Self::block_has_return(block) {
3010            self.compile_block_inner(block)?;
3011        } else if self.scope_stack.last().is_some_and(|l| l.use_slots)
3012            && !Self::block_has_local(block)
3013        {
3014            // When scalar slots are active, skip PushFrame/PopFrame so slot indices keep
3015            // addressing the same runtime frame. New `my` decls still get fresh slot indices.
3016            self.compile_block_inner(block)?;
3017        } else {
3018            self.push_scope_layer();
3019            self.chunk.emit(Op::PushFrame, 0);
3020            self.compile_block_inner(block)?;
3021            self.chunk.emit(Op::PopFrame, 0);
3022            self.pop_scope_layer();
3023        }
3024        Ok(())
3025    }
3026
3027    fn compile_block_inner(&mut self, block: &Block) -> Result<(), CompileError> {
3028        for stmt in block {
3029            self.compile_statement(stmt)?;
3030        }
3031        Ok(())
3032    }
3033
3034    /// Compile a block that leaves its last expression's value on the stack.
3035    /// Used for if/unless as the last statement (implicit return).
3036    fn emit_block_value(&mut self, block: &Block, line: usize) -> Result<(), CompileError> {
3037        if block.is_empty() {
3038            self.chunk.emit(Op::LoadUndef, line);
3039            return Ok(());
3040        }
3041        let last_idx = block.len() - 1;
3042        for (i, stmt) in block.iter().enumerate() {
3043            if i == last_idx {
3044                match &stmt.kind {
3045                    StmtKind::Expression(expr) => {
3046                        self.compile_expr(expr)?;
3047                    }
3048                    StmtKind::Block(inner) => {
3049                        self.chunk.emit(Op::PushFrame, stmt.line);
3050                        self.emit_block_value(inner, stmt.line)?;
3051                        self.chunk.emit(Op::PopFrame, stmt.line);
3052                    }
3053                    StmtKind::StmtGroup(inner) => {
3054                        self.emit_block_value(inner, stmt.line)?;
3055                    }
3056                    StmtKind::If {
3057                        condition,
3058                        body,
3059                        elsifs,
3060                        else_block,
3061                    } => {
3062                        self.compile_boolean_rvalue_condition(condition)?;
3063                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
3064                        self.emit_block_value(body, stmt.line)?;
3065                        let mut ends = vec![self.chunk.emit(Op::Jump(0), stmt.line)];
3066                        self.chunk.patch_jump_here(j0);
3067                        for (c, blk) in elsifs {
3068                            self.compile_boolean_rvalue_condition(c)?;
3069                            let j = self.chunk.emit(Op::JumpIfFalse(0), c.line);
3070                            self.emit_block_value(blk, c.line)?;
3071                            ends.push(self.chunk.emit(Op::Jump(0), c.line));
3072                            self.chunk.patch_jump_here(j);
3073                        }
3074                        if let Some(eb) = else_block {
3075                            self.emit_block_value(eb, stmt.line)?;
3076                        } else {
3077                            self.chunk.emit(Op::LoadUndef, stmt.line);
3078                        }
3079                        for j in ends {
3080                            self.chunk.patch_jump_here(j);
3081                        }
3082                    }
3083                    StmtKind::Unless {
3084                        condition,
3085                        body,
3086                        else_block,
3087                    } => {
3088                        self.compile_boolean_rvalue_condition(condition)?;
3089                        let j0 = self.chunk.emit(Op::JumpIfFalse(0), stmt.line);
3090                        if let Some(eb) = else_block {
3091                            self.emit_block_value(eb, stmt.line)?;
3092                        } else {
3093                            self.chunk.emit(Op::LoadUndef, stmt.line);
3094                        }
3095                        let end = self.chunk.emit(Op::Jump(0), stmt.line);
3096                        self.chunk.patch_jump_here(j0);
3097                        self.emit_block_value(body, stmt.line)?;
3098                        self.chunk.patch_jump_here(end);
3099                    }
3100                    _ => self.compile_statement(stmt)?,
3101                }
3102            } else {
3103                self.compile_statement(stmt)?;
3104            }
3105        }
3106        Ok(())
3107    }
3108
3109    /// Compile a subroutine body so the return value matches Perl: the last statement's value is
3110    /// returned when it is an expression or a trailing `if`/`unless` (same shape as the main
3111    /// program's last-statement value rule). Otherwise falls through with `undef` after the last
3112    /// statement unless it already executed `return`.
3113    fn emit_subroutine_body_return(&mut self, body: &Block) -> Result<(), CompileError> {
3114        if body.is_empty() {
3115            self.chunk.emit(Op::LoadUndef, 0);
3116            self.chunk.emit(Op::ReturnValue, 0);
3117            return Ok(());
3118        }
3119        let last_idx = body.len() - 1;
3120        let last = &body[last_idx];
3121        match &last.kind {
3122            StmtKind::Return(_) => {
3123                for stmt in body {
3124                    self.compile_statement(stmt)?;
3125                }
3126            }
3127            StmtKind::Expression(expr) => {
3128                for stmt in &body[..last_idx] {
3129                    self.compile_statement(stmt)?;
3130                }
3131                // Compile tail expression in List context so @array returns
3132                // the array contents, not the count. The caller's ReturnValue
3133                // handler will adapt to the actual wantarray context.
3134                self.compile_expr_ctx(expr, WantarrayCtx::List)?;
3135                self.chunk.emit(Op::ReturnValue, last.line);
3136            }
3137            StmtKind::If {
3138                condition,
3139                body: if_body,
3140                elsifs,
3141                else_block,
3142            } => {
3143                for stmt in &body[..last_idx] {
3144                    self.compile_statement(stmt)?;
3145                }
3146                self.compile_boolean_rvalue_condition(condition)?;
3147                let j0 = self.chunk.emit(Op::JumpIfFalse(0), last.line);
3148                self.emit_block_value(if_body, last.line)?;
3149                let mut ends = vec![self.chunk.emit(Op::Jump(0), last.line)];
3150                self.chunk.patch_jump_here(j0);
3151                for (c, blk) in elsifs {
3152                    self.compile_boolean_rvalue_condition(c)?;
3153                    let j = self.chunk.emit(Op::JumpIfFalse(0), c.line);
3154                    self.emit_block_value(blk, c.line)?;
3155                    ends.push(self.chunk.emit(Op::Jump(0), c.line));
3156                    self.chunk.patch_jump_here(j);
3157                }
3158                if let Some(eb) = else_block {
3159                    self.emit_block_value(eb, last.line)?;
3160                } else {
3161                    self.chunk.emit(Op::LoadUndef, last.line);
3162                }
3163                for j in ends {
3164                    self.chunk.patch_jump_here(j);
3165                }
3166                self.chunk.emit(Op::ReturnValue, last.line);
3167            }
3168            StmtKind::Unless {
3169                condition,
3170                body: unless_body,
3171                else_block,
3172            } => {
3173                for stmt in &body[..last_idx] {
3174                    self.compile_statement(stmt)?;
3175                }
3176                self.compile_boolean_rvalue_condition(condition)?;
3177                let j0 = self.chunk.emit(Op::JumpIfFalse(0), last.line);
3178                if let Some(eb) = else_block {
3179                    self.emit_block_value(eb, last.line)?;
3180                } else {
3181                    self.chunk.emit(Op::LoadUndef, last.line);
3182                }
3183                let end = self.chunk.emit(Op::Jump(0), last.line);
3184                self.chunk.patch_jump_here(j0);
3185                self.emit_block_value(unless_body, last.line)?;
3186                self.chunk.patch_jump_here(end);
3187                self.chunk.emit(Op::ReturnValue, last.line);
3188            }
3189            _ => {
3190                for stmt in body {
3191                    self.compile_statement(stmt)?;
3192                }
3193                self.chunk.emit(Op::LoadUndef, 0);
3194                self.chunk.emit(Op::ReturnValue, 0);
3195            }
3196        }
3197        Ok(())
3198    }
3199
3200    /// Compile a loop body as a sequence of statements. `last`/`next` (including those nested
3201    /// inside `if`/`unless`/block statements) are handled by `compile_statement` via the
3202    /// [`Compiler::loop_stack`] — the innermost loop frame owns their break/continue patches.
3203    fn compile_block_no_frame(&mut self, block: &Block) -> Result<(), CompileError> {
3204        for stmt in block {
3205            self.compile_statement(stmt)?;
3206        }
3207        Ok(())
3208    }
3209
3210    fn compile_expr(&mut self, expr: &Expr) -> Result<(), CompileError> {
3211        self.compile_expr_ctx(expr, WantarrayCtx::Scalar)
3212    }
3213
3214    fn compile_expr_ctx(&mut self, root: &Expr, ctx: WantarrayCtx) -> Result<(), CompileError> {
3215        let line = root.line;
3216        match &root.kind {
3217            ExprKind::Integer(n) => {
3218                self.emit_op(Op::LoadInt(*n), line, Some(root));
3219            }
3220            ExprKind::Float(f) => {
3221                self.emit_op(Op::LoadFloat(*f), line, Some(root));
3222            }
3223            ExprKind::String(s) => {
3224                let processed = VMHelper::process_case_escapes(s);
3225                let idx = self.chunk.add_constant(StrykeValue::string(processed));
3226                self.emit_op(Op::LoadConst(idx), line, Some(root));
3227            }
3228            ExprKind::Bareword(name) => {
3229                // `BAREWORD` as an rvalue: run-time lookup via `Op::BarewordRvalue` — if a sub
3230                // with this name exists at run time, call it nullary; otherwise push the name
3231                // as a string. Mirrors Perl's `ExprKind::Bareword` eval path.
3232                let idx = self.chunk.intern_name(name);
3233                self.emit_op(Op::BarewordRvalue(idx), line, Some(root));
3234            }
3235            ExprKind::Undef => {
3236                self.emit_op(Op::LoadUndef, line, Some(root));
3237            }
3238            ExprKind::MagicConst(crate::ast::MagicConstKind::File) => {
3239                let idx = self
3240                    .chunk
3241                    .add_constant(StrykeValue::string(self.source_file.clone()));
3242                self.emit_op(Op::LoadConst(idx), line, Some(root));
3243            }
3244            ExprKind::MagicConst(crate::ast::MagicConstKind::Line) => {
3245                let idx = self
3246                    .chunk
3247                    .add_constant(StrykeValue::integer(root.line as i64));
3248                self.emit_op(Op::LoadConst(idx), line, Some(root));
3249            }
3250            ExprKind::MagicConst(crate::ast::MagicConstKind::Sub) => {
3251                self.emit_op(Op::LoadCurrentSub, line, Some(root));
3252            }
3253            ExprKind::ScalarVar(name) => {
3254                self.check_strict_scalar_access(name, line)?;
3255                let idx = self.intern_scalar_var_for_ops(name);
3256                self.emit_get_scalar(idx, line, Some(root));
3257            }
3258            ExprKind::ArrayVar(name) => {
3259                self.check_strict_array_access(name, line)?;
3260                let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
3261                if ctx == WantarrayCtx::List {
3262                    self.emit_op(Op::GetArray(idx), line, Some(root));
3263                } else {
3264                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
3265                }
3266            }
3267            ExprKind::HashVar(name) => {
3268                self.check_strict_hash_access(name, line)?;
3269                let idx = self.chunk.intern_name(name);
3270                self.emit_op(Op::GetHash(idx), line, Some(root));
3271                if ctx != WantarrayCtx::List {
3272                    self.emit_op(Op::ValueScalarContext, line, Some(root));
3273                }
3274            }
3275            ExprKind::Typeglob(name) => {
3276                let idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
3277                self.emit_op(Op::LoadConst(idx), line, Some(root));
3278            }
3279            ExprKind::TypeglobExpr(expr) => {
3280                self.compile_expr(expr)?;
3281                self.emit_op(Op::LoadDynamicTypeglob, line, Some(root));
3282            }
3283            ExprKind::ArrayElement { array, index } => {
3284                self.check_strict_array_access(array, line)?;
3285                let idx = self
3286                    .chunk
3287                    .intern_name(&self.qualify_stash_array_name(array));
3288                // Stryke string-slice sugar: `$s[from:to]` / `$s[from:to:step]`
3289                // returns a substring (or stepped pick) when the target is a
3290                // scalar string. Compile to ArraySliceRange — the VM handles
3291                // the scalar-string case when the array is empty.
3292                if let ExprKind::Range {
3293                    from,
3294                    to,
3295                    exclusive,
3296                    step,
3297                } = &index.kind
3298                {
3299                    self.compile_expr(from)?;
3300                    self.compile_expr(to)?;
3301                    self.compile_optional_or_undef(step.as_deref())?;
3302                    let _ = exclusive; // ArraySliceRange semantics treat to as inclusive
3303                    self.emit_op(Op::ArraySliceRange(idx), line, Some(root));
3304                    return Ok(());
3305                }
3306                // Open-ended slices like `$s[1:]`, `$s[:5]`, `$s[::2]`
3307                if let ExprKind::SliceRange { from, to, step } = &index.kind {
3308                    self.compile_optional_or_undef(from.as_deref())?;
3309                    self.compile_optional_or_undef(to.as_deref())?;
3310                    self.compile_optional_or_undef(step.as_deref())?;
3311                    self.emit_op(Op::ArraySliceRange(idx), line, Some(root));
3312                    return Ok(());
3313                }
3314                self.compile_expr(index)?;
3315                self.emit_op(Op::GetArrayElem(idx), line, Some(root));
3316            }
3317            ExprKind::HashElement { hash, key } => {
3318                self.check_strict_hash_access(hash, line)?;
3319                let idx = self.chunk.intern_name(hash);
3320                self.compile_expr(key)?;
3321                self.emit_op(Op::GetHashElem(idx), line, Some(root));
3322            }
3323            ExprKind::ArraySlice { array, indices } => {
3324                let arr_idx = self
3325                    .chunk
3326                    .intern_name(&self.qualify_stash_array_name(array));
3327                if indices.is_empty() {
3328                    self.emit_op(Op::MakeArray(0), line, Some(root));
3329                } else if indices.len() == 1 {
3330                    match &indices[0].kind {
3331                        ExprKind::SliceRange { from, to, step } => {
3332                            // Open-ended slice — push (from?, to?, step?); VM resolves
3333                            // defaults from array length. Integer-strict; aborts on
3334                            // non-integer endpoints.
3335                            self.compile_optional_or_undef(from.as_deref())?;
3336                            self.compile_optional_or_undef(to.as_deref())?;
3337                            self.compile_optional_or_undef(step.as_deref())?;
3338                            self.emit_op(Op::ArraySliceRange(arr_idx), line, Some(root));
3339                        }
3340                        ExprKind::Range { from, to, step, .. } => {
3341                            // Closed colon range like `1:3` or `1:3:2` — also strict
3342                            // integer (rejects `"a":"c"`, `1.5:5`, etc.).
3343                            self.compile_expr(from)?;
3344                            self.compile_expr(to)?;
3345                            self.compile_optional_or_undef(step.as_deref())?;
3346                            self.emit_op(Op::ArraySliceRange(arr_idx), line, Some(root));
3347                        }
3348                        _ => {
3349                            self.compile_array_slice_index_expr(&indices[0])?;
3350                            self.emit_op(Op::ArraySlicePart(arr_idx), line, Some(root));
3351                        }
3352                    }
3353                } else {
3354                    for (ix, index_expr) in indices.iter().enumerate() {
3355                        self.compile_array_slice_index_expr(index_expr)?;
3356                        self.emit_op(Op::ArraySlicePart(arr_idx), line, Some(root));
3357                        if ix > 0 {
3358                            self.emit_op(Op::ArrayConcatTwo, line, Some(root));
3359                        }
3360                    }
3361                }
3362            }
3363            ExprKind::HashSlice { hash, keys } => {
3364                let hash_idx = self.chunk.intern_name(hash);
3365                if keys.len() == 1 {
3366                    match &keys[0].kind {
3367                        ExprKind::SliceRange { from, to, step } => {
3368                            // Open-ended hash slice — VM aborts (no "all keys" in
3369                            // unordered hash).
3370                            self.compile_optional_or_undef(from.as_deref())?;
3371                            self.compile_optional_or_undef(to.as_deref())?;
3372                            self.compile_optional_or_undef(step.as_deref())?;
3373                            self.emit_op(Op::HashSliceRange(hash_idx), line, Some(root));
3374                            return Ok(());
3375                        }
3376                        ExprKind::Range { from, to, step, .. } => {
3377                            // Closed colon range like `a:c:1` or `1:3` — endpoints
3378                            // stringify to hash keys (auto-quoted barewords already
3379                            // resolved during parsing).
3380                            self.compile_expr(from)?;
3381                            self.compile_expr(to)?;
3382                            self.compile_optional_or_undef(step.as_deref())?;
3383                            self.emit_op(Op::HashSliceRange(hash_idx), line, Some(root));
3384                            return Ok(());
3385                        }
3386                        _ => {}
3387                    }
3388                }
3389                // If any key expression is a range, we need runtime flattening via GetHashSlice.
3390                let has_dynamic_keys = keys
3391                    .iter()
3392                    .any(|k| matches!(&k.kind, ExprKind::Range { .. }));
3393                if has_dynamic_keys {
3394                    for key_expr in keys {
3395                        self.compile_hash_slice_key_expr(key_expr)?;
3396                    }
3397                    self.emit_op(
3398                        Op::GetHashSlice(hash_idx, keys.len() as u16),
3399                        line,
3400                        Some(root),
3401                    );
3402                } else {
3403                    // Flatten multi-key subscripts (qw, lists) into individual GetHashElem ops
3404                    let mut total_keys = 0u16;
3405                    for key_expr in keys {
3406                        match &key_expr.kind {
3407                            ExprKind::QW(words) => {
3408                                for w in words {
3409                                    let cidx =
3410                                        self.chunk.add_constant(StrykeValue::string(w.clone()));
3411                                    self.emit_op(Op::LoadConst(cidx), line, Some(root));
3412                                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3413                                    total_keys += 1;
3414                                }
3415                            }
3416                            ExprKind::List(elems) => {
3417                                for e in elems {
3418                                    self.compile_expr(e)?;
3419                                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3420                                    total_keys += 1;
3421                                }
3422                            }
3423                            _ => {
3424                                self.compile_expr(key_expr)?;
3425                                self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3426                                total_keys += 1;
3427                            }
3428                        }
3429                    }
3430                    self.emit_op(Op::MakeArray(total_keys), line, Some(root));
3431                }
3432            }
3433            ExprKind::HashKvSlice { hash, keys } => {
3434                // `%h{KEYS}` — Perl 5.20+ kv-slice. Emit key-then-value
3435                // pairs, then MakeArray for a flat list. (BUG-008)
3436                let hash_idx = self.chunk.intern_name(hash);
3437                let mut total_pairs = 0u16;
3438                for key_expr in keys {
3439                    match &key_expr.kind {
3440                        ExprKind::QW(words) => {
3441                            for w in words {
3442                                let kidx = self.chunk.add_constant(StrykeValue::string(w.clone()));
3443                                self.emit_op(Op::LoadConst(kidx), line, Some(root));
3444                                let kidx2 = self.chunk.add_constant(StrykeValue::string(w.clone()));
3445                                self.emit_op(Op::LoadConst(kidx2), line, Some(root));
3446                                self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3447                                total_pairs += 1;
3448                            }
3449                        }
3450                        ExprKind::List(elems) => {
3451                            for e in elems {
3452                                self.compile_expr(e)?;
3453                                self.emit_op(Op::Dup, line, Some(root));
3454                                self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3455                                total_pairs += 1;
3456                            }
3457                        }
3458                        _ => {
3459                            self.compile_expr(key_expr)?;
3460                            self.emit_op(Op::Dup, line, Some(root));
3461                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3462                            total_pairs += 1;
3463                        }
3464                    }
3465                }
3466                self.emit_op(Op::MakeArray(total_pairs * 2), line, Some(root));
3467            }
3468            ExprKind::HashSliceDeref { container, keys } => {
3469                self.compile_expr(container)?;
3470                for key_expr in keys {
3471                    self.compile_hash_slice_key_expr(key_expr)?;
3472                }
3473                self.emit_op(Op::HashSliceDeref(keys.len() as u16), line, Some(root));
3474            }
3475            ExprKind::AnonymousListSlice { source, indices } => {
3476                if indices.is_empty() {
3477                    self.compile_expr_ctx(source, WantarrayCtx::List)?;
3478                    self.emit_op(Op::MakeArray(0), line, Some(root));
3479                } else {
3480                    self.compile_expr_ctx(source, WantarrayCtx::List)?;
3481                    for index_expr in indices {
3482                        self.compile_array_slice_index_expr(index_expr)?;
3483                    }
3484                    self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, Some(root));
3485                }
3486                if ctx != WantarrayCtx::List {
3487                    self.emit_op(Op::ListSliceToScalar, line, Some(root));
3488                }
3489            }
3490
3491            // ── Operators ──
3492            ExprKind::BinOp { left, op, right } => {
3493                // Short-circuit operators
3494                match op {
3495                    BinOp::LogAnd | BinOp::LogAndWord => {
3496                        if matches!(left.kind, ExprKind::Regex(..)) {
3497                            self.compile_boolean_rvalue_condition(left)?;
3498                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3499                        } else {
3500                            self.compile_expr(left)?;
3501                        }
3502                        let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
3503                        // JumpIfFalseKeep already pops on fall-through, so no explicit Pop needed
3504                        if matches!(right.kind, ExprKind::Regex(..)) {
3505                            self.compile_boolean_rvalue_condition(right)?;
3506                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3507                        } else {
3508                            self.compile_expr(right)?;
3509                        }
3510                        self.chunk.patch_jump_here(j);
3511                        return Ok(());
3512                    }
3513                    BinOp::LogOr | BinOp::LogOrWord => {
3514                        if matches!(left.kind, ExprKind::Regex(..)) {
3515                            self.compile_boolean_rvalue_condition(left)?;
3516                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3517                        } else {
3518                            self.compile_expr(left)?;
3519                        }
3520                        let j = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
3521                        // JumpIfTrueKeep already pops on fall-through, so no explicit Pop needed
3522                        if matches!(right.kind, ExprKind::Regex(..)) {
3523                            self.compile_boolean_rvalue_condition(right)?;
3524                            self.emit_op(Op::RegexBoolToScalar, line, Some(root));
3525                        } else {
3526                            self.compile_expr(right)?;
3527                        }
3528                        self.chunk.patch_jump_here(j);
3529                        return Ok(());
3530                    }
3531                    BinOp::DefinedOr => {
3532                        self.compile_expr(left)?;
3533                        let j = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
3534                        // JumpIfDefinedKeep already pops on fall-through, so no explicit Pop needed
3535                        self.compile_expr(right)?;
3536                        self.chunk.patch_jump_here(j);
3537                        return Ok(());
3538                    }
3539                    BinOp::BindMatch => {
3540                        self.compile_expr(left)?;
3541                        self.compile_expr(right)?;
3542                        self.emit_op(Op::RegexMatchDyn(false), line, Some(root));
3543                        return Ok(());
3544                    }
3545                    BinOp::BindNotMatch => {
3546                        self.compile_expr(left)?;
3547                        self.compile_expr(right)?;
3548                        self.emit_op(Op::RegexMatchDyn(true), line, Some(root));
3549                        return Ok(());
3550                    }
3551                    _ => {}
3552                }
3553
3554                self.compile_expr(left)?;
3555                self.compile_expr(right)?;
3556                let op_code = match op {
3557                    BinOp::Add => Op::Add,
3558                    BinOp::Sub => Op::Sub,
3559                    BinOp::Mul => Op::Mul,
3560                    BinOp::Div => Op::Div,
3561                    BinOp::Mod => Op::Mod,
3562                    BinOp::Pow => Op::Pow,
3563                    BinOp::Concat => Op::Concat,
3564                    BinOp::NumEq => Op::NumEq,
3565                    BinOp::NumNe => Op::NumNe,
3566                    BinOp::NumLt => Op::NumLt,
3567                    BinOp::NumGt => Op::NumGt,
3568                    BinOp::NumLe => Op::NumLe,
3569                    BinOp::NumGe => Op::NumGe,
3570                    BinOp::Spaceship => Op::Spaceship,
3571                    BinOp::StrEq => Op::StrEq,
3572                    BinOp::StrNe => Op::StrNe,
3573                    BinOp::StrLt => Op::StrLt,
3574                    BinOp::StrGt => Op::StrGt,
3575                    BinOp::StrLe => Op::StrLe,
3576                    BinOp::StrGe => Op::StrGe,
3577                    BinOp::StrCmp => Op::StrCmp,
3578                    BinOp::BitAnd => Op::BitAnd,
3579                    BinOp::BitOr => Op::BitOr,
3580                    BinOp::BitXor => Op::BitXor,
3581                    BinOp::ShiftLeft => Op::Shl,
3582                    BinOp::ShiftRight => Op::Shr,
3583                    // Short-circuit and regex bind handled above
3584                    BinOp::LogAnd
3585                    | BinOp::LogOr
3586                    | BinOp::DefinedOr
3587                    | BinOp::LogAndWord
3588                    | BinOp::LogOrWord
3589                    | BinOp::BindMatch
3590                    | BinOp::BindNotMatch => unreachable!(),
3591                };
3592                self.emit_op(op_code, line, Some(root));
3593            }
3594
3595            ExprKind::UnaryOp { op, expr } => match op {
3596                UnaryOp::PreIncrement => {
3597                    if let ExprKind::ScalarVar(name) = &expr.kind {
3598                        self.check_scalar_mutable(name, line)?;
3599                        self.check_closure_write_to_outer_my(name, line)?;
3600                        let idx = self.intern_scalar_var_for_ops(name);
3601                        self.emit_pre_inc(idx, line, Some(root));
3602                    } else if let ExprKind::ArrayElement { array, index } = &expr.kind {
3603                        if self.is_mysync_array(array) {
3604                            return Err(CompileError::Unsupported(
3605                                "mysync array element update".into(),
3606                            ));
3607                        }
3608                        let q = self.qualify_stash_array_name(array);
3609                        self.check_array_mutable(&q, line)?;
3610                        let arr_idx = self.chunk.intern_name(&q);
3611                        self.compile_expr(index)?;
3612                        self.emit_op(Op::Dup, line, Some(root));
3613                        self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
3614                        self.emit_op(Op::LoadInt(1), line, Some(root));
3615                        self.emit_op(Op::Add, line, Some(root));
3616                        self.emit_op(Op::Dup, line, Some(root));
3617                        self.emit_op(Op::Rot, line, Some(root));
3618                        self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
3619                    } else if let ExprKind::ArraySlice { array, indices } = &expr.kind {
3620                        if self.is_mysync_array(array) {
3621                            return Err(CompileError::Unsupported(
3622                                "mysync array element update".into(),
3623                            ));
3624                        }
3625                        self.check_strict_array_access(array, line)?;
3626                        let q = self.qualify_stash_array_name(array);
3627                        self.check_array_mutable(&q, line)?;
3628                        let arr_idx = self.chunk.intern_name(&q);
3629                        for ix in indices {
3630                            self.compile_array_slice_index_expr(ix)?;
3631                        }
3632                        self.emit_op(
3633                            Op::NamedArraySliceIncDec(0, arr_idx, indices.len() as u16),
3634                            line,
3635                            Some(root),
3636                        );
3637                    } else if let ExprKind::HashElement { hash, key } = &expr.kind {
3638                        if self.is_mysync_hash(hash) {
3639                            return Err(CompileError::Unsupported(
3640                                "mysync hash element update".into(),
3641                            ));
3642                        }
3643                        self.check_hash_mutable(hash, line)?;
3644                        let hash_idx = self.chunk.intern_name(hash);
3645                        self.compile_expr(key)?;
3646                        self.emit_op(Op::Dup, line, Some(root));
3647                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3648                        self.emit_op(Op::LoadInt(1), line, Some(root));
3649                        self.emit_op(Op::Add, line, Some(root));
3650                        self.emit_op(Op::Dup, line, Some(root));
3651                        self.emit_op(Op::Rot, line, Some(root));
3652                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
3653                    } else if let ExprKind::HashSlice { hash, keys } = &expr.kind {
3654                        if self.is_mysync_hash(hash) {
3655                            return Err(CompileError::Unsupported(
3656                                "mysync hash element update".into(),
3657                            ));
3658                        }
3659                        self.check_hash_mutable(hash, line)?;
3660                        let hash_idx = self.chunk.intern_name(hash);
3661                        if hash_slice_needs_slice_ops(keys) {
3662                            for hk in keys {
3663                                self.compile_expr(hk)?;
3664                            }
3665                            self.emit_op(
3666                                Op::NamedHashSliceIncDec(0, hash_idx, keys.len() as u16),
3667                                line,
3668                                Some(root),
3669                            );
3670                            return Ok(());
3671                        }
3672                        let hk = &keys[0];
3673                        self.compile_expr(hk)?;
3674                        self.emit_op(Op::Dup, line, Some(root));
3675                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3676                        self.emit_op(Op::LoadInt(1), line, Some(root));
3677                        self.emit_op(Op::Add, line, Some(root));
3678                        self.emit_op(Op::Dup, line, Some(root));
3679                        self.emit_op(Op::Rot, line, Some(root));
3680                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
3681                    } else if let ExprKind::ArrowDeref {
3682                        expr,
3683                        index,
3684                        kind: DerefKind::Array,
3685                    } = &expr.kind
3686                    {
3687                        if let ExprKind::List(indices) = &index.kind {
3688                            // Multi-index `++@$aref[i1,i2,...]` — delegates to VM slice inc-dec.
3689                            self.compile_arrow_array_base_expr(expr)?;
3690                            for ix in indices {
3691                                self.compile_array_slice_index_expr(ix)?;
3692                            }
3693                            self.emit_op(
3694                                Op::ArrowArraySliceIncDec(0, indices.len() as u16),
3695                                line,
3696                                Some(root),
3697                            );
3698                            return Ok(());
3699                        }
3700                        self.compile_arrow_array_base_expr(expr)?;
3701                        self.compile_array_slice_index_expr(index)?;
3702                        self.emit_op(Op::ArrowArraySliceIncDec(0, 1), line, Some(root));
3703                    } else if let ExprKind::AnonymousListSlice { source, indices } = &expr.kind {
3704                        if let ExprKind::Deref {
3705                            expr: inner,
3706                            kind: Sigil::Array,
3707                        } = &source.kind
3708                        {
3709                            self.compile_arrow_array_base_expr(inner)?;
3710                            for ix in indices {
3711                                self.compile_array_slice_index_expr(ix)?;
3712                            }
3713                            self.emit_op(
3714                                Op::ArrowArraySliceIncDec(0, indices.len() as u16),
3715                                line,
3716                                Some(root),
3717                            );
3718                            return Ok(());
3719                        }
3720                    } else if let ExprKind::ArrowDeref {
3721                        expr,
3722                        index,
3723                        kind: DerefKind::Hash,
3724                    } = &expr.kind
3725                    {
3726                        self.compile_arrow_hash_base_expr(expr)?;
3727                        self.compile_expr(index)?;
3728                        self.emit_op(Op::Dup2, line, Some(root));
3729                        self.emit_op(Op::ArrowHash, line, Some(root));
3730                        self.emit_op(Op::LoadInt(1), line, Some(root));
3731                        self.emit_op(Op::Add, line, Some(root));
3732                        self.emit_op(Op::Dup, line, Some(root));
3733                        self.emit_op(Op::Pop, line, Some(root));
3734                        self.emit_op(Op::Swap, line, Some(root));
3735                        self.emit_op(Op::Rot, line, Some(root));
3736                        self.emit_op(Op::Swap, line, Some(root));
3737                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
3738                    } else if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
3739                        if hash_slice_needs_slice_ops(keys) {
3740                            // Multi-key: matches generic PreIncrement fallback
3741                            // (list → int → ±1 → slice assign). Dedicated op in VM delegates to
3742                            // Interpreter::hash_slice_deref_inc_dec.
3743                            self.compile_expr(container)?;
3744                            for hk in keys {
3745                                self.compile_expr(hk)?;
3746                            }
3747                            self.emit_op(
3748                                Op::HashSliceDerefIncDec(0, keys.len() as u16),
3749                                line,
3750                                Some(root),
3751                            );
3752                            return Ok(());
3753                        }
3754                        let hk = &keys[0];
3755                        self.compile_expr(container)?;
3756                        self.compile_expr(hk)?;
3757                        self.emit_op(Op::Dup2, line, Some(root));
3758                        self.emit_op(Op::ArrowHash, line, Some(root));
3759                        self.emit_op(Op::LoadInt(1), line, Some(root));
3760                        self.emit_op(Op::Add, line, Some(root));
3761                        self.emit_op(Op::Dup, line, Some(root));
3762                        self.emit_op(Op::Pop, line, Some(root));
3763                        self.emit_op(Op::Swap, line, Some(root));
3764                        self.emit_op(Op::Rot, line, Some(root));
3765                        self.emit_op(Op::Swap, line, Some(root));
3766                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
3767                    } else if let ExprKind::Deref {
3768                        expr,
3769                        kind: Sigil::Scalar,
3770                    } = &expr.kind
3771                    {
3772                        self.compile_expr(expr)?;
3773                        self.emit_op(Op::Dup, line, Some(root));
3774                        self.emit_op(Op::SymbolicDeref(0), line, Some(root));
3775                        self.emit_op(Op::LoadInt(1), line, Some(root));
3776                        self.emit_op(Op::Add, line, Some(root));
3777                        self.emit_op(Op::Swap, line, Some(root));
3778                        self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
3779                    } else if let ExprKind::Deref { kind, .. } = &expr.kind {
3780                        // `++@{…}` / `++%{…}` (and `++@$r` / `++%$r`) are invalid in Perl 5.
3781                        // Emit a runtime error directly so the VM produces the same error
3782                        // Perl would.
3783                        self.emit_aggregate_symbolic_inc_dec_error(*kind, true, true, line, root)?;
3784                    } else {
3785                        return Err(CompileError::Unsupported("PreInc on non-scalar".into()));
3786                    }
3787                }
3788                UnaryOp::PreDecrement => {
3789                    if let ExprKind::ScalarVar(name) = &expr.kind {
3790                        self.check_scalar_mutable(name, line)?;
3791                        self.check_closure_write_to_outer_my(name, line)?;
3792                        let idx = self.intern_scalar_var_for_ops(name);
3793                        self.emit_pre_dec(idx, line, Some(root));
3794                    } else if let ExprKind::ArrayElement { array, index } = &expr.kind {
3795                        if self.is_mysync_array(array) {
3796                            return Err(CompileError::Unsupported(
3797                                "mysync array element update".into(),
3798                            ));
3799                        }
3800                        let q = self.qualify_stash_array_name(array);
3801                        self.check_array_mutable(&q, line)?;
3802                        let arr_idx = self.chunk.intern_name(&q);
3803                        self.compile_expr(index)?;
3804                        self.emit_op(Op::Dup, line, Some(root));
3805                        self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
3806                        self.emit_op(Op::LoadInt(1), line, Some(root));
3807                        self.emit_op(Op::Sub, line, Some(root));
3808                        self.emit_op(Op::Dup, line, Some(root));
3809                        self.emit_op(Op::Rot, line, Some(root));
3810                        self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
3811                    } else if let ExprKind::ArraySlice { array, indices } = &expr.kind {
3812                        if self.is_mysync_array(array) {
3813                            return Err(CompileError::Unsupported(
3814                                "mysync array element update".into(),
3815                            ));
3816                        }
3817                        self.check_strict_array_access(array, line)?;
3818                        let q = self.qualify_stash_array_name(array);
3819                        self.check_array_mutable(&q, line)?;
3820                        let arr_idx = self.chunk.intern_name(&q);
3821                        for ix in indices {
3822                            self.compile_array_slice_index_expr(ix)?;
3823                        }
3824                        self.emit_op(
3825                            Op::NamedArraySliceIncDec(1, arr_idx, indices.len() as u16),
3826                            line,
3827                            Some(root),
3828                        );
3829                    } else if let ExprKind::HashElement { hash, key } = &expr.kind {
3830                        if self.is_mysync_hash(hash) {
3831                            return Err(CompileError::Unsupported(
3832                                "mysync hash element update".into(),
3833                            ));
3834                        }
3835                        self.check_hash_mutable(hash, line)?;
3836                        let hash_idx = self.chunk.intern_name(hash);
3837                        self.compile_expr(key)?;
3838                        self.emit_op(Op::Dup, line, Some(root));
3839                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3840                        self.emit_op(Op::LoadInt(1), line, Some(root));
3841                        self.emit_op(Op::Sub, line, Some(root));
3842                        self.emit_op(Op::Dup, line, Some(root));
3843                        self.emit_op(Op::Rot, line, Some(root));
3844                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
3845                    } else if let ExprKind::HashSlice { hash, keys } = &expr.kind {
3846                        if self.is_mysync_hash(hash) {
3847                            return Err(CompileError::Unsupported(
3848                                "mysync hash element update".into(),
3849                            ));
3850                        }
3851                        self.check_hash_mutable(hash, line)?;
3852                        let hash_idx = self.chunk.intern_name(hash);
3853                        if hash_slice_needs_slice_ops(keys) {
3854                            for hk in keys {
3855                                self.compile_expr(hk)?;
3856                            }
3857                            self.emit_op(
3858                                Op::NamedHashSliceIncDec(1, hash_idx, keys.len() as u16),
3859                                line,
3860                                Some(root),
3861                            );
3862                            return Ok(());
3863                        }
3864                        let hk = &keys[0];
3865                        self.compile_expr(hk)?;
3866                        self.emit_op(Op::Dup, line, Some(root));
3867                        self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
3868                        self.emit_op(Op::LoadInt(1), line, Some(root));
3869                        self.emit_op(Op::Sub, line, Some(root));
3870                        self.emit_op(Op::Dup, line, Some(root));
3871                        self.emit_op(Op::Rot, line, Some(root));
3872                        self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
3873                    } else if let ExprKind::ArrowDeref {
3874                        expr,
3875                        index,
3876                        kind: DerefKind::Array,
3877                    } = &expr.kind
3878                    {
3879                        if let ExprKind::List(indices) = &index.kind {
3880                            self.compile_arrow_array_base_expr(expr)?;
3881                            for ix in indices {
3882                                self.compile_array_slice_index_expr(ix)?;
3883                            }
3884                            self.emit_op(
3885                                Op::ArrowArraySliceIncDec(1, indices.len() as u16),
3886                                line,
3887                                Some(root),
3888                            );
3889                            return Ok(());
3890                        }
3891                        self.compile_arrow_array_base_expr(expr)?;
3892                        self.compile_array_slice_index_expr(index)?;
3893                        self.emit_op(Op::ArrowArraySliceIncDec(1, 1), line, Some(root));
3894                    } else if let ExprKind::AnonymousListSlice { source, indices } = &expr.kind {
3895                        if let ExprKind::Deref {
3896                            expr: inner,
3897                            kind: Sigil::Array,
3898                        } = &source.kind
3899                        {
3900                            self.compile_arrow_array_base_expr(inner)?;
3901                            for ix in indices {
3902                                self.compile_array_slice_index_expr(ix)?;
3903                            }
3904                            self.emit_op(
3905                                Op::ArrowArraySliceIncDec(1, indices.len() as u16),
3906                                line,
3907                                Some(root),
3908                            );
3909                            return Ok(());
3910                        }
3911                    } else if let ExprKind::ArrowDeref {
3912                        expr,
3913                        index,
3914                        kind: DerefKind::Hash,
3915                    } = &expr.kind
3916                    {
3917                        self.compile_arrow_hash_base_expr(expr)?;
3918                        self.compile_expr(index)?;
3919                        self.emit_op(Op::Dup2, line, Some(root));
3920                        self.emit_op(Op::ArrowHash, line, Some(root));
3921                        self.emit_op(Op::LoadInt(1), line, Some(root));
3922                        self.emit_op(Op::Sub, line, Some(root));
3923                        self.emit_op(Op::Dup, line, Some(root));
3924                        self.emit_op(Op::Pop, line, Some(root));
3925                        self.emit_op(Op::Swap, line, Some(root));
3926                        self.emit_op(Op::Rot, line, Some(root));
3927                        self.emit_op(Op::Swap, line, Some(root));
3928                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
3929                    } else if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
3930                        if hash_slice_needs_slice_ops(keys) {
3931                            self.compile_expr(container)?;
3932                            for hk in keys {
3933                                self.compile_expr(hk)?;
3934                            }
3935                            self.emit_op(
3936                                Op::HashSliceDerefIncDec(1, keys.len() as u16),
3937                                line,
3938                                Some(root),
3939                            );
3940                            return Ok(());
3941                        }
3942                        let hk = &keys[0];
3943                        self.compile_expr(container)?;
3944                        self.compile_expr(hk)?;
3945                        self.emit_op(Op::Dup2, line, Some(root));
3946                        self.emit_op(Op::ArrowHash, line, Some(root));
3947                        self.emit_op(Op::LoadInt(1), line, Some(root));
3948                        self.emit_op(Op::Sub, line, Some(root));
3949                        self.emit_op(Op::Dup, line, Some(root));
3950                        self.emit_op(Op::Pop, line, Some(root));
3951                        self.emit_op(Op::Swap, line, Some(root));
3952                        self.emit_op(Op::Rot, line, Some(root));
3953                        self.emit_op(Op::Swap, line, Some(root));
3954                        self.emit_op(Op::SetArrowHashKeep, line, Some(root));
3955                    } else if let ExprKind::Deref {
3956                        expr,
3957                        kind: Sigil::Scalar,
3958                    } = &expr.kind
3959                    {
3960                        self.compile_expr(expr)?;
3961                        self.emit_op(Op::Dup, line, Some(root));
3962                        self.emit_op(Op::SymbolicDeref(0), line, Some(root));
3963                        self.emit_op(Op::LoadInt(1), line, Some(root));
3964                        self.emit_op(Op::Sub, line, Some(root));
3965                        self.emit_op(Op::Swap, line, Some(root));
3966                        self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
3967                    } else if let ExprKind::Deref { kind, .. } = &expr.kind {
3968                        self.emit_aggregate_symbolic_inc_dec_error(*kind, true, false, line, root)?;
3969                    } else {
3970                        return Err(CompileError::Unsupported("PreDec on non-scalar".into()));
3971                    }
3972                }
3973                UnaryOp::Ref => {
3974                    self.compile_expr(expr)?;
3975                    self.emit_op(Op::MakeScalarRef, line, Some(root));
3976                }
3977                _ => match op {
3978                    UnaryOp::LogNot | UnaryOp::LogNotWord => {
3979                        if matches!(expr.kind, ExprKind::Regex(..)) {
3980                            self.compile_boolean_rvalue_condition(expr)?;
3981                        } else {
3982                            self.compile_expr(expr)?;
3983                        }
3984                        self.emit_op(Op::LogNot, line, Some(root));
3985                    }
3986                    UnaryOp::Negate => {
3987                        self.compile_expr(expr)?;
3988                        self.emit_op(Op::Negate, line, Some(root));
3989                    }
3990                    UnaryOp::BitNot => {
3991                        self.compile_expr(expr)?;
3992                        self.emit_op(Op::BitNot, line, Some(root));
3993                    }
3994                    _ => unreachable!(),
3995                },
3996            },
3997            ExprKind::PostfixOp { expr, op } => {
3998                if let ExprKind::ScalarVar(name) = &expr.kind {
3999                    self.check_scalar_mutable(name, line)?;
4000                    self.check_closure_write_to_outer_my(name, line)?;
4001                    let idx = self.intern_scalar_var_for_ops(name);
4002                    match op {
4003                        PostfixOp::Increment => {
4004                            self.emit_post_inc(idx, line, Some(root));
4005                        }
4006                        PostfixOp::Decrement => {
4007                            self.emit_post_dec(idx, line, Some(root));
4008                        }
4009                    }
4010                } else if let ExprKind::ArrayElement { array, index } = &expr.kind {
4011                    if self.is_mysync_array(array) {
4012                        return Err(CompileError::Unsupported(
4013                            "mysync array element update".into(),
4014                        ));
4015                    }
4016                    let q = self.qualify_stash_array_name(array);
4017                    self.check_array_mutable(&q, line)?;
4018                    let arr_idx = self.chunk.intern_name(&q);
4019                    self.compile_expr(index)?;
4020                    self.emit_op(Op::Dup, line, Some(root));
4021                    self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4022                    self.emit_op(Op::Dup, line, Some(root));
4023                    self.emit_op(Op::LoadInt(1), line, Some(root));
4024                    match op {
4025                        PostfixOp::Increment => {
4026                            self.emit_op(Op::Add, line, Some(root));
4027                        }
4028                        PostfixOp::Decrement => {
4029                            self.emit_op(Op::Sub, line, Some(root));
4030                        }
4031                    }
4032                    self.emit_op(Op::Rot, line, Some(root));
4033                    self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
4034                } else if let ExprKind::ArraySlice { array, indices } = &expr.kind {
4035                    if self.is_mysync_array(array) {
4036                        return Err(CompileError::Unsupported(
4037                            "mysync array element update".into(),
4038                        ));
4039                    }
4040                    self.check_strict_array_access(array, line)?;
4041                    let q = self.qualify_stash_array_name(array);
4042                    self.check_array_mutable(&q, line)?;
4043                    let arr_idx = self.chunk.intern_name(&q);
4044                    let kind_byte: u8 = match op {
4045                        PostfixOp::Increment => 2,
4046                        PostfixOp::Decrement => 3,
4047                    };
4048                    for ix in indices {
4049                        self.compile_array_slice_index_expr(ix)?;
4050                    }
4051                    self.emit_op(
4052                        Op::NamedArraySliceIncDec(kind_byte, arr_idx, indices.len() as u16),
4053                        line,
4054                        Some(root),
4055                    );
4056                } else if let ExprKind::HashElement { hash, key } = &expr.kind {
4057                    if self.is_mysync_hash(hash) {
4058                        return Err(CompileError::Unsupported(
4059                            "mysync hash element update".into(),
4060                        ));
4061                    }
4062                    self.check_hash_mutable(hash, line)?;
4063                    let hash_idx = self.chunk.intern_name(hash);
4064                    self.compile_expr(key)?;
4065                    self.emit_op(Op::Dup, line, Some(root));
4066                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4067                    self.emit_op(Op::Dup, line, Some(root));
4068                    self.emit_op(Op::LoadInt(1), line, Some(root));
4069                    match op {
4070                        PostfixOp::Increment => {
4071                            self.emit_op(Op::Add, line, Some(root));
4072                        }
4073                        PostfixOp::Decrement => {
4074                            self.emit_op(Op::Sub, line, Some(root));
4075                        }
4076                    }
4077                    self.emit_op(Op::Rot, line, Some(root));
4078                    self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4079                } else if let ExprKind::HashSlice { hash, keys } = &expr.kind {
4080                    if self.is_mysync_hash(hash) {
4081                        return Err(CompileError::Unsupported(
4082                            "mysync hash element update".into(),
4083                        ));
4084                    }
4085                    self.check_hash_mutable(hash, line)?;
4086                    let hash_idx = self.chunk.intern_name(hash);
4087                    if hash_slice_needs_slice_ops(keys) {
4088                        let kind_byte: u8 = match op {
4089                            PostfixOp::Increment => 2,
4090                            PostfixOp::Decrement => 3,
4091                        };
4092                        for hk in keys {
4093                            self.compile_expr(hk)?;
4094                        }
4095                        self.emit_op(
4096                            Op::NamedHashSliceIncDec(kind_byte, hash_idx, keys.len() as u16),
4097                            line,
4098                            Some(root),
4099                        );
4100                        return Ok(());
4101                    }
4102                    let hk = &keys[0];
4103                    self.compile_expr(hk)?;
4104                    self.emit_op(Op::Dup, line, Some(root));
4105                    self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4106                    self.emit_op(Op::Dup, line, Some(root));
4107                    self.emit_op(Op::LoadInt(1), line, Some(root));
4108                    match op {
4109                        PostfixOp::Increment => {
4110                            self.emit_op(Op::Add, line, Some(root));
4111                        }
4112                        PostfixOp::Decrement => {
4113                            self.emit_op(Op::Sub, line, Some(root));
4114                        }
4115                    }
4116                    self.emit_op(Op::Rot, line, Some(root));
4117                    self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4118                } else if let ExprKind::ArrowDeref {
4119                    expr: inner,
4120                    index,
4121                    kind: DerefKind::Array,
4122                } = &expr.kind
4123                {
4124                    if let ExprKind::List(indices) = &index.kind {
4125                        let kind_byte: u8 = match op {
4126                            PostfixOp::Increment => 2,
4127                            PostfixOp::Decrement => 3,
4128                        };
4129                        self.compile_arrow_array_base_expr(inner)?;
4130                        for ix in indices {
4131                            self.compile_array_slice_index_expr(ix)?;
4132                        }
4133                        self.emit_op(
4134                            Op::ArrowArraySliceIncDec(kind_byte, indices.len() as u16),
4135                            line,
4136                            Some(root),
4137                        );
4138                        return Ok(());
4139                    }
4140                    self.compile_arrow_array_base_expr(inner)?;
4141                    self.compile_array_slice_index_expr(index)?;
4142                    let kind_byte: u8 = match op {
4143                        PostfixOp::Increment => 2,
4144                        PostfixOp::Decrement => 3,
4145                    };
4146                    self.emit_op(Op::ArrowArraySliceIncDec(kind_byte, 1), line, Some(root));
4147                } else if let ExprKind::AnonymousListSlice { source, indices } = &expr.kind {
4148                    let ExprKind::Deref {
4149                        expr: inner,
4150                        kind: Sigil::Array,
4151                    } = &source.kind
4152                    else {
4153                        return Err(CompileError::Unsupported(
4154                            "PostfixOp on list slice (non-array deref)".into(),
4155                        ));
4156                    };
4157                    if indices.is_empty() {
4158                        return Err(CompileError::Unsupported(
4159                            "postfix ++/-- on empty list slice (internal)".into(),
4160                        ));
4161                    }
4162                    let kind_byte: u8 = match op {
4163                        PostfixOp::Increment => 2,
4164                        PostfixOp::Decrement => 3,
4165                    };
4166                    self.compile_arrow_array_base_expr(inner)?;
4167                    if indices.len() > 1 {
4168                        for ix in indices {
4169                            self.compile_array_slice_index_expr(ix)?;
4170                        }
4171                        self.emit_op(
4172                            Op::ArrowArraySliceIncDec(kind_byte, indices.len() as u16),
4173                            line,
4174                            Some(root),
4175                        );
4176                    } else {
4177                        self.compile_array_slice_index_expr(&indices[0])?;
4178                        self.emit_op(Op::ArrowArraySliceIncDec(kind_byte, 1), line, Some(root));
4179                    }
4180                } else if let ExprKind::ArrowDeref {
4181                    expr: inner,
4182                    index,
4183                    kind: DerefKind::Hash,
4184                } = &expr.kind
4185                {
4186                    self.compile_arrow_hash_base_expr(inner)?;
4187                    self.compile_expr(index)?;
4188                    let b = match op {
4189                        PostfixOp::Increment => 0u8,
4190                        PostfixOp::Decrement => 1u8,
4191                    };
4192                    self.emit_op(Op::ArrowHashPostfix(b), line, Some(root));
4193                } else if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
4194                    if hash_slice_needs_slice_ops(keys) {
4195                        // Multi-key postfix ++/--: matches generic PostfixOp fallback
4196                        // (reads slice list, assigns scalar back, returns old list).
4197                        let kind_byte: u8 = match op {
4198                            PostfixOp::Increment => 2,
4199                            PostfixOp::Decrement => 3,
4200                        };
4201                        self.compile_expr(container)?;
4202                        for hk in keys {
4203                            self.compile_expr(hk)?;
4204                        }
4205                        self.emit_op(
4206                            Op::HashSliceDerefIncDec(kind_byte, keys.len() as u16),
4207                            line,
4208                            Some(root),
4209                        );
4210                        return Ok(());
4211                    }
4212                    let hk = &keys[0];
4213                    self.compile_expr(container)?;
4214                    self.compile_expr(hk)?;
4215                    let b = match op {
4216                        PostfixOp::Increment => 0u8,
4217                        PostfixOp::Decrement => 1u8,
4218                    };
4219                    self.emit_op(Op::ArrowHashPostfix(b), line, Some(root));
4220                } else if let ExprKind::Deref {
4221                    expr,
4222                    kind: Sigil::Scalar,
4223                } = &expr.kind
4224                {
4225                    self.compile_expr(expr)?;
4226                    let b = match op {
4227                        PostfixOp::Increment => 0u8,
4228                        PostfixOp::Decrement => 1u8,
4229                    };
4230                    self.emit_op(Op::SymbolicScalarRefPostfix(b), line, Some(root));
4231                } else if let ExprKind::Deref { kind, .. } = &expr.kind {
4232                    let is_inc = matches!(op, PostfixOp::Increment);
4233                    self.emit_aggregate_symbolic_inc_dec_error(*kind, false, is_inc, line, root)?;
4234                } else {
4235                    return Err(CompileError::Unsupported("PostfixOp on non-scalar".into()));
4236                }
4237            }
4238
4239            ExprKind::Assign { target, value } => {
4240                // `substr($s, $o, $l) = $rhs` is equivalent to the 4-arg
4241                // form `substr($s, $o, $l, $rhs)`. Rewrite + compile as
4242                // a function call so the dedicated lvalue path isn't
4243                // needed.
4244                if let ExprKind::Substr {
4245                    string,
4246                    offset,
4247                    length,
4248                    replacement: None,
4249                } = &target.kind
4250                {
4251                    let rewritten = Expr {
4252                        kind: ExprKind::Substr {
4253                            string: string.clone(),
4254                            offset: offset.clone(),
4255                            length: length.clone(),
4256                            replacement: Some(value.clone()),
4257                        },
4258                        line: target.line,
4259                    };
4260                    self.compile_expr(&rewritten)?;
4261                    return Ok(());
4262                }
4263                // `vec($s, $o, $b) = $rhs` — set the requested bit field
4264                // in $s and write back. Lower to:
4265                //     $s = vec_set_value($s, $o, $b, $rhs);
4266                // where `vec_set_value` is a 4-arg pure builtin that
4267                // returns the modified string. This lets every supported
4268                // lvalue shape for `$s` (plain scalar, array element,
4269                // hash element, etc.) reuse its existing assign path.
4270                if let ExprKind::FuncCall { name, args } = &target.kind {
4271                    if name == "vec" && args.len() == 3 {
4272                        let new_call = Expr {
4273                            kind: ExprKind::FuncCall {
4274                                name: "vec_set_value".to_string(),
4275                                args: vec![
4276                                    args[0].clone(),
4277                                    args[1].clone(),
4278                                    args[2].clone(),
4279                                    (**value).clone(),
4280                                ],
4281                            },
4282                            line: target.line,
4283                        };
4284                        let rewritten = Expr {
4285                            kind: ExprKind::Assign {
4286                                target: Box::new(args[0].clone()),
4287                                value: Box::new(new_call),
4288                            },
4289                            line,
4290                        };
4291                        self.compile_expr(&rewritten)?;
4292                        return Ok(());
4293                    }
4294                }
4295                if let (ExprKind::Typeglob(lhs), ExprKind::Typeglob(rhs)) =
4296                    (&target.kind, &value.kind)
4297                {
4298                    let lhs_idx = self.chunk.intern_name(lhs);
4299                    let rhs_idx = self.chunk.intern_name(rhs);
4300                    self.emit_op(Op::CopyTypeglobSlots(lhs_idx, rhs_idx), line, Some(root));
4301                    self.compile_expr(value)?;
4302                    return Ok(());
4303                }
4304                if let ExprKind::TypeglobExpr(expr) = &target.kind {
4305                    if let ExprKind::Typeglob(rhs) = &value.kind {
4306                        self.compile_expr(expr)?;
4307                        let rhs_idx = self.chunk.intern_name(rhs);
4308                        self.emit_op(Op::CopyTypeglobSlotsDynamicLhs(rhs_idx), line, Some(root));
4309                        self.compile_expr(value)?;
4310                        return Ok(());
4311                    }
4312                    self.compile_expr(expr)?;
4313                    self.compile_expr(value)?;
4314                    self.emit_op(Op::TypeglobAssignFromValueDynamic, line, Some(root));
4315                    return Ok(());
4316                }
4317                // Braced `*{EXPR}` parses as `Deref { kind: Typeglob }` (same VM lowering as `TypeglobExpr`).
4318                if let ExprKind::Deref {
4319                    expr,
4320                    kind: Sigil::Typeglob,
4321                } = &target.kind
4322                {
4323                    if let ExprKind::Typeglob(rhs) = &value.kind {
4324                        self.compile_expr(expr)?;
4325                        let rhs_idx = self.chunk.intern_name(rhs);
4326                        self.emit_op(Op::CopyTypeglobSlotsDynamicLhs(rhs_idx), line, Some(root));
4327                        self.compile_expr(value)?;
4328                        return Ok(());
4329                    }
4330                    self.compile_expr(expr)?;
4331                    self.compile_expr(value)?;
4332                    self.emit_op(Op::TypeglobAssignFromValueDynamic, line, Some(root));
4333                    return Ok(());
4334                }
4335                if let ExprKind::ArrowDeref {
4336                    expr,
4337                    index,
4338                    kind: DerefKind::Array,
4339                } = &target.kind
4340                {
4341                    if let ExprKind::List(indices) = &index.kind {
4342                        if let ExprKind::Deref {
4343                            expr: inner,
4344                            kind: Sigil::Array,
4345                        } = &expr.kind
4346                        {
4347                            if let ExprKind::List(vals) = &value.kind {
4348                                if !indices.is_empty() && indices.len() == vals.len() {
4349                                    for (idx_e, val_e) in indices.iter().zip(vals.iter()) {
4350                                        self.compile_expr(val_e)?;
4351                                        self.compile_expr(inner)?;
4352                                        self.compile_expr(idx_e)?;
4353                                        self.emit_op(Op::SetArrowArray, line, Some(root));
4354                                    }
4355                                    return Ok(());
4356                                }
4357                            }
4358                        }
4359                    }
4360                }
4361                // Fuse `$x = $x OP $y` / `$x = $x + 1` into slot ops when possible.
4362                if let ExprKind::ScalarVar(tgt_name) = &target.kind {
4363                    if let Some(dst_slot) = self.scalar_slot(tgt_name) {
4364                        if let ExprKind::BinOp { left, op, right } = &value.kind {
4365                            if let ExprKind::ScalarVar(lv) = &left.kind {
4366                                if lv == tgt_name {
4367                                    // $x = $x + SCALAR_VAR → AddAssignSlotSlot etc.
4368                                    if let ExprKind::ScalarVar(rv) = &right.kind {
4369                                        if let Some(src_slot) = self.scalar_slot(rv) {
4370                                            let fused = match op {
4371                                                BinOp::Add => {
4372                                                    Some(Op::AddAssignSlotSlot(dst_slot, src_slot))
4373                                                }
4374                                                BinOp::Sub => {
4375                                                    Some(Op::SubAssignSlotSlot(dst_slot, src_slot))
4376                                                }
4377                                                BinOp::Mul => {
4378                                                    Some(Op::MulAssignSlotSlot(dst_slot, src_slot))
4379                                                }
4380                                                _ => None,
4381                                            };
4382                                            if let Some(fop) = fused {
4383                                                self.emit_op(fop, line, Some(root));
4384                                                return Ok(());
4385                                            }
4386                                        }
4387                                    }
4388                                    // $x = $x + 1 → PreIncSlot, $x = $x - 1 → PreDecSlot
4389                                    if let ExprKind::Integer(1) = &right.kind {
4390                                        match op {
4391                                            BinOp::Add => {
4392                                                self.emit_op(
4393                                                    Op::PreIncSlot(dst_slot),
4394                                                    line,
4395                                                    Some(root),
4396                                                );
4397                                                return Ok(());
4398                                            }
4399                                            BinOp::Sub => {
4400                                                self.emit_op(
4401                                                    Op::PreDecSlot(dst_slot),
4402                                                    line,
4403                                                    Some(root),
4404                                                );
4405                                                return Ok(());
4406                                            }
4407                                            _ => {}
4408                                        }
4409                                    }
4410                                }
4411                            }
4412                        }
4413                    }
4414                }
4415                self.compile_expr_ctx(value, assign_rhs_wantarray(target))?;
4416                self.compile_assign(target, line, true, Some(root))?;
4417            }
4418            ExprKind::CompoundAssign { target, op, value } => {
4419                if let ExprKind::ScalarVar(name) = &target.kind {
4420                    self.check_scalar_mutable(name, line)?;
4421                    let idx = self.intern_scalar_var_for_ops(name);
4422                    // Fast path: `.=` on scalar → in-place append (no clone)
4423                    if *op == BinOp::Concat {
4424                        self.compile_expr(value)?;
4425                        if let Some(slot) = self.scalar_slot(name) {
4426                            self.emit_op(Op::ConcatAppendSlot(slot), line, Some(root));
4427                        } else {
4428                            self.emit_op(Op::ConcatAppend(idx), line, Some(root));
4429                        }
4430                        return Ok(());
4431                    }
4432                    // Fused slot+slot arithmetic: $slot_a += $slot_b (no stack traffic)
4433                    if let Some(dst_slot) = self.scalar_slot(name) {
4434                        if let ExprKind::ScalarVar(rhs_name) = &value.kind {
4435                            if let Some(src_slot) = self.scalar_slot(rhs_name) {
4436                                let fused = match op {
4437                                    BinOp::Add => Some(Op::AddAssignSlotSlot(dst_slot, src_slot)),
4438                                    BinOp::Sub => Some(Op::SubAssignSlotSlot(dst_slot, src_slot)),
4439                                    BinOp::Mul => Some(Op::MulAssignSlotSlot(dst_slot, src_slot)),
4440                                    _ => None,
4441                                };
4442                                if let Some(fop) = fused {
4443                                    self.emit_op(fop, line, Some(root));
4444                                    return Ok(());
4445                                }
4446                            }
4447                        }
4448                    }
4449                    if *op == BinOp::DefinedOr {
4450                        // `$x //=` — short-circuit when LHS is defined.
4451                        // Slot-aware: use GetScalarSlot/SetScalarSlot if available.
4452                        if let Some(slot) = self.scalar_slot(name) {
4453                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4454                            let j_def = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
4455                            self.compile_expr(value)?;
4456                            self.emit_op(Op::Dup, line, Some(root));
4457                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4458                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4459                            self.chunk.patch_jump_here(j_def);
4460                            self.chunk.patch_jump_here(j_end);
4461                        } else {
4462                            self.emit_get_scalar(idx, line, Some(root));
4463                            let j_def = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
4464                            self.compile_expr(value)?;
4465                            self.emit_set_scalar_keep(idx, line, Some(root));
4466                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4467                            self.chunk.patch_jump_here(j_def);
4468                            self.chunk.patch_jump_here(j_end);
4469                        }
4470                        return Ok(());
4471                    }
4472                    if *op == BinOp::LogOr {
4473                        // `$x ||=` — short-circuit when LHS is true.
4474                        if let Some(slot) = self.scalar_slot(name) {
4475                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4476                            let j_true = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
4477                            self.compile_expr(value)?;
4478                            self.emit_op(Op::Dup, line, Some(root));
4479                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4480                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4481                            self.chunk.patch_jump_here(j_true);
4482                            self.chunk.patch_jump_here(j_end);
4483                        } else {
4484                            self.emit_get_scalar(idx, line, Some(root));
4485                            let j_true = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
4486                            self.compile_expr(value)?;
4487                            self.emit_set_scalar_keep(idx, line, Some(root));
4488                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4489                            self.chunk.patch_jump_here(j_true);
4490                            self.chunk.patch_jump_here(j_end);
4491                        }
4492                        return Ok(());
4493                    }
4494                    if *op == BinOp::LogAnd {
4495                        // `$x &&=` — short-circuit when LHS is false.
4496                        if let Some(slot) = self.scalar_slot(name) {
4497                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4498                            let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
4499                            self.compile_expr(value)?;
4500                            self.emit_op(Op::Dup, line, Some(root));
4501                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4502                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4503                            self.chunk.patch_jump_here(j);
4504                            self.chunk.patch_jump_here(j_end);
4505                        } else {
4506                            self.emit_get_scalar(idx, line, Some(root));
4507                            let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
4508                            self.compile_expr(value)?;
4509                            self.emit_set_scalar_keep(idx, line, Some(root));
4510                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4511                            self.chunk.patch_jump_here(j);
4512                            self.chunk.patch_jump_here(j_end);
4513                        }
4514                        return Ok(());
4515                    }
4516                    if let Some(op_b) = scalar_compound_op_to_byte(*op) {
4517                        // Slot-aware path: `my $x` inside a sub body lives in a local slot.
4518                        if let Some(slot) = self.scalar_slot(name) {
4519                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4520                                CompileError::Unsupported("CompoundAssign op (slot)".into())
4521                            })?;
4522                            self.emit_op(Op::GetScalarSlot(slot), line, Some(root));
4523                            self.compile_expr(value)?;
4524                            self.emit_op(vm_op, line, Some(root));
4525                            self.emit_op(Op::Dup, line, Some(root));
4526                            self.emit_op(Op::SetScalarSlot(slot), line, Some(root));
4527                            return Ok(());
4528                        }
4529                        self.compile_expr(value)?;
4530                        self.emit_op(
4531                            Op::ScalarCompoundAssign {
4532                                name_idx: idx,
4533                                op: op_b,
4534                            },
4535                            line,
4536                            Some(root),
4537                        );
4538                    } else {
4539                        return Err(CompileError::Unsupported("CompoundAssign op".into()));
4540                    }
4541                } else if let ExprKind::ArrayElement { array, index } = &target.kind {
4542                    if self.is_mysync_array(array) {
4543                        return Err(CompileError::Unsupported(
4544                            "mysync array element update".into(),
4545                        ));
4546                    }
4547                    let q = self.qualify_stash_array_name(array);
4548                    self.check_array_mutable(&q, line)?;
4549                    let arr_idx = self.chunk.intern_name(&q);
4550                    match op {
4551                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4552                            self.compile_expr(index)?;
4553                            self.emit_op(Op::Dup, line, Some(root));
4554                            self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4555                            let j = match *op {
4556                                BinOp::DefinedOr => {
4557                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4558                                }
4559                                BinOp::LogOr => {
4560                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4561                                }
4562                                BinOp::LogAnd => {
4563                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4564                                }
4565                                _ => unreachable!(),
4566                            };
4567                            self.compile_expr(value)?;
4568                            self.emit_op(Op::Swap, line, Some(root));
4569                            self.emit_op(Op::SetArrayElemKeep(arr_idx), line, Some(root));
4570                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4571                            self.chunk.patch_jump_here(j);
4572                            self.emit_op(Op::Swap, line, Some(root));
4573                            self.emit_op(Op::Pop, line, Some(root));
4574                            self.chunk.patch_jump_here(j_end);
4575                        }
4576                        _ => {
4577                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4578                                CompileError::Unsupported("CompoundAssign op".into())
4579                            })?;
4580                            self.compile_expr(index)?;
4581                            self.emit_op(Op::Dup, line, Some(root));
4582                            self.emit_op(Op::GetArrayElem(arr_idx), line, Some(root));
4583                            self.compile_expr(value)?;
4584                            self.emit_op(vm_op, line, Some(root));
4585                            self.emit_op(Op::Dup, line, Some(root));
4586                            self.emit_op(Op::Rot, line, Some(root));
4587                            self.emit_op(Op::SetArrayElem(arr_idx), line, Some(root));
4588                        }
4589                    }
4590                } else if let ExprKind::HashElement { hash, key } = &target.kind {
4591                    if self.is_mysync_hash(hash) {
4592                        return Err(CompileError::Unsupported(
4593                            "mysync hash element update".into(),
4594                        ));
4595                    }
4596                    self.check_hash_mutable(hash, line)?;
4597                    let hash_idx = self.chunk.intern_name(hash);
4598                    match op {
4599                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4600                            self.compile_expr(key)?;
4601                            self.emit_op(Op::Dup, line, Some(root));
4602                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4603                            let j = match *op {
4604                                BinOp::DefinedOr => {
4605                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4606                                }
4607                                BinOp::LogOr => {
4608                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4609                                }
4610                                BinOp::LogAnd => {
4611                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4612                                }
4613                                _ => unreachable!(),
4614                            };
4615                            self.compile_expr(value)?;
4616                            self.emit_op(Op::Swap, line, Some(root));
4617                            self.emit_op(Op::SetHashElemKeep(hash_idx), line, Some(root));
4618                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4619                            self.chunk.patch_jump_here(j);
4620                            self.emit_op(Op::Swap, line, Some(root));
4621                            self.emit_op(Op::Pop, line, Some(root));
4622                            self.chunk.patch_jump_here(j_end);
4623                        }
4624                        _ => {
4625                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4626                                CompileError::Unsupported("CompoundAssign op".into())
4627                            })?;
4628                            self.compile_expr(key)?;
4629                            self.emit_op(Op::Dup, line, Some(root));
4630                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
4631                            self.compile_expr(value)?;
4632                            self.emit_op(vm_op, line, Some(root));
4633                            self.emit_op(Op::Dup, line, Some(root));
4634                            self.emit_op(Op::Rot, line, Some(root));
4635                            self.emit_op(Op::SetHashElem(hash_idx), line, Some(root));
4636                        }
4637                    }
4638                } else if let ExprKind::Deref {
4639                    expr,
4640                    kind: Sigil::Scalar,
4641                } = &target.kind
4642                {
4643                    match op {
4644                        BinOp::DefinedOr => {
4645                            // `$$r //=` — unlike binary `//`, no `Pop` after `JumpIfDefinedKeep`
4646                            // (the ref must stay under the deref); `Swap` before set (ref on TOS).
4647                            self.compile_expr(expr)?;
4648                            self.emit_op(Op::Dup, line, Some(root));
4649                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
4650                            let j_def = self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root));
4651                            self.compile_expr(value)?;
4652                            self.emit_op(Op::Swap, line, Some(root));
4653                            self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
4654                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4655                            self.chunk.patch_jump_here(j_def);
4656                            self.emit_op(Op::Swap, line, Some(root));
4657                            self.emit_op(Op::Pop, line, Some(root));
4658                            self.chunk.patch_jump_here(j_end);
4659                        }
4660                        BinOp::LogOr => {
4661                            // `$$r ||=` — same idea as `//=`: no `Pop` after `JumpIfTrueKeep`.
4662                            self.compile_expr(expr)?;
4663                            self.emit_op(Op::Dup, line, Some(root));
4664                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
4665                            let j_true = self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root));
4666                            self.compile_expr(value)?;
4667                            self.emit_op(Op::Swap, line, Some(root));
4668                            self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
4669                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4670                            self.chunk.patch_jump_here(j_true);
4671                            self.emit_op(Op::Swap, line, Some(root));
4672                            self.emit_op(Op::Pop, line, Some(root));
4673                            self.chunk.patch_jump_here(j_end);
4674                        }
4675                        BinOp::LogAnd => {
4676                            // `$$r &&=` — no `Pop` after `JumpIfFalseKeep` (ref under LHS).
4677                            self.compile_expr(expr)?;
4678                            self.emit_op(Op::Dup, line, Some(root));
4679                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
4680                            let j = self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root));
4681                            self.compile_expr(value)?;
4682                            self.emit_op(Op::Swap, line, Some(root));
4683                            self.emit_op(Op::SetSymbolicScalarRefKeep, line, Some(root));
4684                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4685                            self.chunk.patch_jump_here(j);
4686                            self.emit_op(Op::Swap, line, Some(root));
4687                            self.emit_op(Op::Pop, line, Some(root));
4688                            self.chunk.patch_jump_here(j_end);
4689                        }
4690                        _ => {
4691                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4692                                CompileError::Unsupported("CompoundAssign op".into())
4693                            })?;
4694                            self.compile_expr(expr)?;
4695                            self.emit_op(Op::Dup, line, Some(root));
4696                            self.emit_op(Op::SymbolicDeref(0), line, Some(root));
4697                            self.compile_expr(value)?;
4698                            self.emit_op(vm_op, line, Some(root));
4699                            self.emit_op(Op::Swap, line, Some(root));
4700                            self.emit_op(Op::SetSymbolicScalarRef, line, Some(root));
4701                        }
4702                    }
4703                } else if let ExprKind::ArrowDeref {
4704                    expr,
4705                    index,
4706                    kind: DerefKind::Hash,
4707                } = &target.kind
4708                {
4709                    match op {
4710                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4711                            self.compile_arrow_hash_base_expr(expr)?;
4712                            self.compile_expr(index)?;
4713                            self.emit_op(Op::Dup2, line, Some(root));
4714                            self.emit_op(Op::ArrowHash, line, Some(root));
4715                            let j = match *op {
4716                                BinOp::DefinedOr => {
4717                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4718                                }
4719                                BinOp::LogOr => {
4720                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4721                                }
4722                                BinOp::LogAnd => {
4723                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4724                                }
4725                                _ => unreachable!(),
4726                            };
4727                            self.compile_expr(value)?;
4728                            self.emit_op(Op::Swap, line, Some(root));
4729                            self.emit_op(Op::Rot, line, Some(root));
4730                            self.emit_op(Op::Swap, line, Some(root));
4731                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4732                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4733                            self.chunk.patch_jump_here(j);
4734                            // Stack: ref, key, cur — leave `cur` as the expression value.
4735                            self.emit_op(Op::Swap, line, Some(root));
4736                            self.emit_op(Op::Pop, line, Some(root));
4737                            self.emit_op(Op::Swap, line, Some(root));
4738                            self.emit_op(Op::Pop, line, Some(root));
4739                            self.chunk.patch_jump_here(j_end);
4740                        }
4741                        _ => {
4742                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4743                                CompileError::Unsupported("CompoundAssign op".into())
4744                            })?;
4745                            self.compile_arrow_hash_base_expr(expr)?;
4746                            self.compile_expr(index)?;
4747                            self.emit_op(Op::Dup2, line, Some(root));
4748                            self.emit_op(Op::ArrowHash, line, Some(root));
4749                            self.compile_expr(value)?;
4750                            self.emit_op(vm_op, line, Some(root));
4751                            self.emit_op(Op::Swap, line, Some(root));
4752                            self.emit_op(Op::Rot, line, Some(root));
4753                            self.emit_op(Op::Swap, line, Some(root));
4754                            // Use ...Keep so the new value remains on the stack
4755                            // as the expression's value — the statement-level
4756                            // `Pop` emitted by `StmtKind::Expression` will discard
4757                            // it. Previously emitted `SetArrowHash` (no-keep)
4758                            // left nothing, and that `Pop` then popped a slot
4759                            // from the CALLER's stack frame — silently corrupting
4760                            // multi-call expressions like `dec($n) + dec($n)`.
4761                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4762                        }
4763                    }
4764                } else if let ExprKind::ArrowDeref {
4765                    expr,
4766                    index,
4767                    kind: DerefKind::Array,
4768                } = &target.kind
4769                {
4770                    if let ExprKind::List(indices) = &index.kind {
4771                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
4772                            let k = indices.len() as u16;
4773                            self.compile_arrow_array_base_expr(expr)?;
4774                            for ix in indices {
4775                                self.compile_array_slice_index_expr(ix)?;
4776                            }
4777                            self.emit_op(Op::ArrowArraySlicePeekLast(k), line, Some(root));
4778                            let j = match *op {
4779                                BinOp::DefinedOr => {
4780                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4781                                }
4782                                BinOp::LogOr => {
4783                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4784                                }
4785                                BinOp::LogAnd => {
4786                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4787                                }
4788                                _ => unreachable!(),
4789                            };
4790                            self.compile_expr(value)?;
4791                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(k), line, Some(root));
4792                            self.emit_op(Op::SetArrowArraySliceLastKeep(k), line, Some(root));
4793                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4794                            self.chunk.patch_jump_here(j);
4795                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(k), line, Some(root));
4796                            self.chunk.patch_jump_here(j_end);
4797                            return Ok(());
4798                        }
4799                        // Multi-index `@$aref[i1,i2,...] OP= EXPR` — Perl applies the op only to the
4800                        // last index (see `Interpreter::compound_assign_arrow_array_slice`).
4801                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
4802                            CompileError::Unsupported(
4803                                "CompoundAssign op on multi-index array slice".into(),
4804                            )
4805                        })?;
4806                        self.compile_expr(value)?;
4807                        self.compile_arrow_array_base_expr(expr)?;
4808                        for ix in indices {
4809                            self.compile_array_slice_index_expr(ix)?;
4810                        }
4811                        self.emit_op(
4812                            Op::ArrowArraySliceCompound(op_byte, indices.len() as u16),
4813                            line,
4814                            Some(root),
4815                        );
4816                        return Ok(());
4817                    }
4818                    match op {
4819                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4820                            // Same last-slot short-circuit semantics as `@$r[i,j] //=` but with one
4821                            // subscript slot (`..` / list / `qw` flatten to multiple indices).
4822                            self.compile_arrow_array_base_expr(expr)?;
4823                            self.compile_array_slice_index_expr(index)?;
4824                            self.emit_op(Op::ArrowArraySlicePeekLast(1), line, Some(root));
4825                            let j = match *op {
4826                                BinOp::DefinedOr => {
4827                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4828                                }
4829                                BinOp::LogOr => {
4830                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4831                                }
4832                                BinOp::LogAnd => {
4833                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4834                                }
4835                                _ => unreachable!(),
4836                            };
4837                            self.compile_expr(value)?;
4838                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(1), line, Some(root));
4839                            self.emit_op(Op::SetArrowArraySliceLastKeep(1), line, Some(root));
4840                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4841                            self.chunk.patch_jump_here(j);
4842                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(1), line, Some(root));
4843                            self.chunk.patch_jump_here(j_end);
4844                        }
4845                        _ => {
4846                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
4847                                CompileError::Unsupported("CompoundAssign op".into())
4848                            })?;
4849                            self.compile_expr(value)?;
4850                            self.compile_arrow_array_base_expr(expr)?;
4851                            self.compile_array_slice_index_expr(index)?;
4852                            self.emit_op(Op::ArrowArraySliceCompound(op_byte, 1), line, Some(root));
4853                        }
4854                    }
4855                } else if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
4856                    // Single-key `@$href{"k"} OP= EXPR` matches `$href->{"k"} OP= EXPR` (ArrowHash).
4857                    // Multi-key `@$href{k1,k2} OP= EXPR` — Perl applies the op only to the last key.
4858                    if keys.is_empty() {
4859                        // Mirror `@h{} OP= EXPR`: evaluate invocant and RHS, then error (matches
4860                        // [`ExprKind::HashSlice`] empty `keys` compound path).
4861                        self.compile_expr(container)?;
4862                        self.emit_op(Op::Pop, line, Some(root));
4863                        self.compile_expr(value)?;
4864                        self.emit_op(Op::Pop, line, Some(root));
4865                        let idx = self
4866                            .chunk
4867                            .add_constant(StrykeValue::string("assign to empty hash slice".into()));
4868                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
4869                        self.emit_op(Op::LoadUndef, line, Some(root));
4870                        return Ok(());
4871                    }
4872                    if hash_slice_needs_slice_ops(keys) {
4873                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
4874                            let k = keys.len() as u16;
4875                            self.compile_expr(container)?;
4876                            for hk in keys {
4877                                self.compile_expr(hk)?;
4878                            }
4879                            self.emit_op(Op::HashSliceDerefPeekLast(k), line, Some(root));
4880                            let j = match *op {
4881                                BinOp::DefinedOr => {
4882                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4883                                }
4884                                BinOp::LogOr => {
4885                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4886                                }
4887                                BinOp::LogAnd => {
4888                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4889                                }
4890                                _ => unreachable!(),
4891                            };
4892                            self.compile_expr(value)?;
4893                            self.emit_op(Op::HashSliceDerefRollValUnderKeys(k), line, Some(root));
4894                            self.emit_op(Op::HashSliceDerefSetLastKeep(k), line, Some(root));
4895                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4896                            self.chunk.patch_jump_here(j);
4897                            self.emit_op(Op::HashSliceDerefDropKeysKeepCur(k), line, Some(root));
4898                            self.chunk.patch_jump_here(j_end);
4899                            return Ok(());
4900                        }
4901                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
4902                            CompileError::Unsupported(
4903                                "CompoundAssign op on multi-key hash slice".into(),
4904                            )
4905                        })?;
4906                        self.compile_expr(value)?;
4907                        self.compile_expr(container)?;
4908                        for hk in keys {
4909                            self.compile_expr(hk)?;
4910                        }
4911                        self.emit_op(
4912                            Op::HashSliceDerefCompound(op_byte, keys.len() as u16),
4913                            line,
4914                            Some(root),
4915                        );
4916                        return Ok(());
4917                    }
4918                    let hk = &keys[0];
4919                    match op {
4920                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4921                            self.compile_expr(container)?;
4922                            self.compile_expr(hk)?;
4923                            self.emit_op(Op::Dup2, line, Some(root));
4924                            self.emit_op(Op::ArrowHash, line, Some(root));
4925                            let j = match *op {
4926                                BinOp::DefinedOr => {
4927                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4928                                }
4929                                BinOp::LogOr => {
4930                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4931                                }
4932                                BinOp::LogAnd => {
4933                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4934                                }
4935                                _ => unreachable!(),
4936                            };
4937                            self.compile_expr(value)?;
4938                            self.emit_op(Op::Swap, line, Some(root));
4939                            self.emit_op(Op::Rot, line, Some(root));
4940                            self.emit_op(Op::Swap, line, Some(root));
4941                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4942                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4943                            self.chunk.patch_jump_here(j);
4944                            self.emit_op(Op::Swap, line, Some(root));
4945                            self.emit_op(Op::Pop, line, Some(root));
4946                            self.emit_op(Op::Swap, line, Some(root));
4947                            self.emit_op(Op::Pop, line, Some(root));
4948                            self.chunk.patch_jump_here(j_end);
4949                        }
4950                        _ => {
4951                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4952                                CompileError::Unsupported("CompoundAssign op".into())
4953                            })?;
4954                            self.compile_expr(container)?;
4955                            self.compile_expr(hk)?;
4956                            self.emit_op(Op::Dup2, line, Some(root));
4957                            self.emit_op(Op::ArrowHash, line, Some(root));
4958                            self.compile_expr(value)?;
4959                            self.emit_op(vm_op, line, Some(root));
4960                            self.emit_op(Op::Swap, line, Some(root));
4961                            self.emit_op(Op::Rot, line, Some(root));
4962                            self.emit_op(Op::Swap, line, Some(root));
4963                            self.emit_op(Op::SetArrowHash, line, Some(root));
4964                        }
4965                    }
4966                } else if let ExprKind::HashSlice { hash, keys } = &target.kind {
4967                    if keys.is_empty() {
4968                        if self.is_mysync_hash(hash) {
4969                            return Err(CompileError::Unsupported(
4970                                "mysync hash slice update".into(),
4971                            ));
4972                        }
4973                        self.check_strict_hash_access(hash, line)?;
4974                        self.check_hash_mutable(hash, line)?;
4975                        self.compile_expr(value)?;
4976                        self.emit_op(Op::Pop, line, Some(root));
4977                        let idx = self
4978                            .chunk
4979                            .add_constant(StrykeValue::string("assign to empty hash slice".into()));
4980                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
4981                        self.emit_op(Op::LoadUndef, line, Some(root));
4982                        return Ok(());
4983                    }
4984                    if self.is_mysync_hash(hash) {
4985                        return Err(CompileError::Unsupported("mysync hash slice update".into()));
4986                    }
4987                    self.check_strict_hash_access(hash, line)?;
4988                    self.check_hash_mutable(hash, line)?;
4989                    let hash_idx = self.chunk.intern_name(hash);
4990                    if hash_slice_needs_slice_ops(keys) {
4991                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
4992                            let k = keys.len() as u16;
4993                            for hk in keys {
4994                                self.compile_expr(hk)?;
4995                            }
4996                            self.emit_op(Op::NamedHashSlicePeekLast(hash_idx, k), line, Some(root));
4997                            let j = match *op {
4998                                BinOp::DefinedOr => {
4999                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5000                                }
5001                                BinOp::LogOr => {
5002                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5003                                }
5004                                BinOp::LogAnd => {
5005                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5006                                }
5007                                _ => unreachable!(),
5008                            };
5009                            self.compile_expr(value)?;
5010                            self.emit_op(Op::NamedArraySliceRollValUnderSpecs(k), line, Some(root));
5011                            self.emit_op(
5012                                Op::SetNamedHashSliceLastKeep(hash_idx, k),
5013                                line,
5014                                Some(root),
5015                            );
5016                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5017                            self.chunk.patch_jump_here(j);
5018                            self.emit_op(Op::NamedHashSliceDropKeysKeepCur(k), line, Some(root));
5019                            self.chunk.patch_jump_here(j_end);
5020                            return Ok(());
5021                        }
5022                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5023                            CompileError::Unsupported(
5024                                "CompoundAssign op on multi-key hash slice".into(),
5025                            )
5026                        })?;
5027                        self.compile_expr(value)?;
5028                        for hk in keys {
5029                            self.compile_expr(hk)?;
5030                        }
5031                        self.emit_op(
5032                            Op::NamedHashSliceCompound(op_byte, hash_idx, keys.len() as u16),
5033                            line,
5034                            Some(root),
5035                        );
5036                        return Ok(());
5037                    }
5038                    let hk = &keys[0];
5039                    match op {
5040                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5041                            self.compile_expr(hk)?;
5042                            self.emit_op(Op::Dup, line, Some(root));
5043                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
5044                            let j = match *op {
5045                                BinOp::DefinedOr => {
5046                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5047                                }
5048                                BinOp::LogOr => {
5049                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5050                                }
5051                                BinOp::LogAnd => {
5052                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5053                                }
5054                                _ => unreachable!(),
5055                            };
5056                            self.compile_expr(value)?;
5057                            self.emit_op(Op::Swap, line, Some(root));
5058                            self.emit_op(Op::SetHashElemKeep(hash_idx), line, Some(root));
5059                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5060                            self.chunk.patch_jump_here(j);
5061                            self.emit_op(Op::Swap, line, Some(root));
5062                            self.emit_op(Op::Pop, line, Some(root));
5063                            self.chunk.patch_jump_here(j_end);
5064                        }
5065                        _ => {
5066                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5067                                CompileError::Unsupported("CompoundAssign op".into())
5068                            })?;
5069                            self.compile_expr(value)?;
5070                            self.compile_expr(hk)?;
5071                            self.emit_op(
5072                                Op::NamedHashSliceCompound(op_byte, hash_idx, 1),
5073                                line,
5074                                Some(root),
5075                            );
5076                        }
5077                    }
5078                } else if let ExprKind::ArraySlice { array, indices } = &target.kind {
5079                    if indices.is_empty() {
5080                        if self.is_mysync_array(array) {
5081                            return Err(CompileError::Unsupported(
5082                                "mysync array slice update".into(),
5083                            ));
5084                        }
5085                        let q = self.qualify_stash_array_name(array);
5086                        self.check_array_mutable(&q, line)?;
5087                        let arr_idx = self.chunk.intern_name(&q);
5088                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5089                            self.compile_expr(value)?;
5090                            self.emit_op(Op::Pop, line, Some(root));
5091                            let idx = self.chunk.add_constant(StrykeValue::string(
5092                                "assign to empty array slice".into(),
5093                            ));
5094                            self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5095                            self.emit_op(Op::LoadUndef, line, Some(root));
5096                            return Ok(());
5097                        }
5098                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5099                            CompileError::Unsupported(
5100                                "CompoundAssign op on named array slice".into(),
5101                            )
5102                        })?;
5103                        self.compile_expr(value)?;
5104                        self.emit_op(
5105                            Op::NamedArraySliceCompound(op_byte, arr_idx, 0),
5106                            line,
5107                            Some(root),
5108                        );
5109                        return Ok(());
5110                    }
5111                    if self.is_mysync_array(array) {
5112                        return Err(CompileError::Unsupported(
5113                            "mysync array slice update".into(),
5114                        ));
5115                    }
5116                    let q = self.qualify_stash_array_name(array);
5117                    self.check_array_mutable(&q, line)?;
5118                    let arr_idx = self.chunk.intern_name(&q);
5119                    if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5120                        let k = indices.len() as u16;
5121                        for ix in indices {
5122                            self.compile_array_slice_index_expr(ix)?;
5123                        }
5124                        self.emit_op(Op::NamedArraySlicePeekLast(arr_idx, k), line, Some(root));
5125                        let j = match *op {
5126                            BinOp::DefinedOr => {
5127                                self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5128                            }
5129                            BinOp::LogOr => self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root)),
5130                            BinOp::LogAnd => self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root)),
5131                            _ => unreachable!(),
5132                        };
5133                        self.compile_expr(value)?;
5134                        self.emit_op(Op::NamedArraySliceRollValUnderSpecs(k), line, Some(root));
5135                        self.emit_op(Op::SetNamedArraySliceLastKeep(arr_idx, k), line, Some(root));
5136                        let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5137                        self.chunk.patch_jump_here(j);
5138                        self.emit_op(Op::NamedArraySliceDropKeysKeepCur(k), line, Some(root));
5139                        self.chunk.patch_jump_here(j_end);
5140                        return Ok(());
5141                    }
5142                    let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5143                        CompileError::Unsupported("CompoundAssign op on named array slice".into())
5144                    })?;
5145                    self.compile_expr(value)?;
5146                    for ix in indices {
5147                        self.compile_array_slice_index_expr(ix)?;
5148                    }
5149                    self.emit_op(
5150                        Op::NamedArraySliceCompound(op_byte, arr_idx, indices.len() as u16),
5151                        line,
5152                        Some(root),
5153                    );
5154                    return Ok(());
5155                } else if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
5156                    let ExprKind::Deref {
5157                        expr: inner,
5158                        kind: Sigil::Array,
5159                    } = &source.kind
5160                    else {
5161                        return Err(CompileError::Unsupported(
5162                            "CompoundAssign on AnonymousListSlice (non-array deref)".into(),
5163                        ));
5164                    };
5165                    if indices.is_empty() {
5166                        self.compile_arrow_array_base_expr(inner)?;
5167                        self.emit_op(Op::Pop, line, Some(root));
5168                        self.compile_expr(value)?;
5169                        self.emit_op(Op::Pop, line, Some(root));
5170                        let idx = self.chunk.add_constant(StrykeValue::string(
5171                            "assign to empty array slice".into(),
5172                        ));
5173                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5174                        self.emit_op(Op::LoadUndef, line, Some(root));
5175                        return Ok(());
5176                    }
5177                    if indices.len() > 1 {
5178                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5179                            let k = indices.len() as u16;
5180                            self.compile_arrow_array_base_expr(inner)?;
5181                            for ix in indices {
5182                                self.compile_array_slice_index_expr(ix)?;
5183                            }
5184                            self.emit_op(Op::ArrowArraySlicePeekLast(k), line, Some(root));
5185                            let j = match *op {
5186                                BinOp::DefinedOr => {
5187                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5188                                }
5189                                BinOp::LogOr => {
5190                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5191                                }
5192                                BinOp::LogAnd => {
5193                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5194                                }
5195                                _ => unreachable!(),
5196                            };
5197                            self.compile_expr(value)?;
5198                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(k), line, Some(root));
5199                            self.emit_op(Op::SetArrowArraySliceLastKeep(k), line, Some(root));
5200                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5201                            self.chunk.patch_jump_here(j);
5202                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(k), line, Some(root));
5203                            self.chunk.patch_jump_here(j_end);
5204                            return Ok(());
5205                        }
5206                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5207                            CompileError::Unsupported(
5208                                "CompoundAssign op on multi-index array slice".into(),
5209                            )
5210                        })?;
5211                        self.compile_expr(value)?;
5212                        self.compile_arrow_array_base_expr(inner)?;
5213                        for ix in indices {
5214                            self.compile_array_slice_index_expr(ix)?;
5215                        }
5216                        self.emit_op(
5217                            Op::ArrowArraySliceCompound(op_byte, indices.len() as u16),
5218                            line,
5219                            Some(root),
5220                        );
5221                        return Ok(());
5222                    }
5223                    let ix0 = &indices[0];
5224                    match op {
5225                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5226                            self.compile_arrow_array_base_expr(inner)?;
5227                            self.compile_array_slice_index_expr(ix0)?;
5228                            self.emit_op(Op::ArrowArraySlicePeekLast(1), line, Some(root));
5229                            let j = match *op {
5230                                BinOp::DefinedOr => {
5231                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5232                                }
5233                                BinOp::LogOr => {
5234                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5235                                }
5236                                BinOp::LogAnd => {
5237                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5238                                }
5239                                _ => unreachable!(),
5240                            };
5241                            self.compile_expr(value)?;
5242                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(1), line, Some(root));
5243                            self.emit_op(Op::SetArrowArraySliceLastKeep(1), line, Some(root));
5244                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5245                            self.chunk.patch_jump_here(j);
5246                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(1), line, Some(root));
5247                            self.chunk.patch_jump_here(j_end);
5248                        }
5249                        _ => {
5250                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5251                                CompileError::Unsupported("CompoundAssign op".into())
5252                            })?;
5253                            self.compile_expr(value)?;
5254                            self.compile_arrow_array_base_expr(inner)?;
5255                            self.compile_array_slice_index_expr(ix0)?;
5256                            self.emit_op(Op::ArrowArraySliceCompound(op_byte, 1), line, Some(root));
5257                        }
5258                    }
5259                } else {
5260                    return Err(CompileError::Unsupported(
5261                        "CompoundAssign on non-scalar".into(),
5262                    ));
5263                }
5264            }
5265
5266            ExprKind::Ternary {
5267                condition,
5268                then_expr,
5269                else_expr,
5270            } => {
5271                self.compile_boolean_rvalue_condition(condition)?;
5272                let jump_else = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
5273                self.compile_expr_ctx(then_expr, ctx)?;
5274                let jump_end = self.emit_op(Op::Jump(0), line, Some(root));
5275                self.chunk.patch_jump_here(jump_else);
5276                self.compile_expr_ctx(else_expr, ctx)?;
5277                self.chunk.patch_jump_here(jump_end);
5278            }
5279
5280            ExprKind::Range {
5281                from,
5282                to,
5283                exclusive,
5284                step,
5285            } => {
5286                if ctx == WantarrayCtx::List {
5287                    self.compile_expr_ctx(from, WantarrayCtx::Scalar)?;
5288                    self.compile_expr_ctx(to, WantarrayCtx::Scalar)?;
5289                    if let Some(s) = step {
5290                        self.compile_expr_ctx(s, WantarrayCtx::Scalar)?;
5291                        self.emit_op(Op::RangeStep, line, Some(root));
5292                    } else {
5293                        self.emit_op(Op::Range, line, Some(root));
5294                    }
5295                } else if let (ExprKind::Regex(lp, lf), ExprKind::Regex(rp, rf)) =
5296                    (&from.kind, &to.kind)
5297                {
5298                    let slot = self.chunk.alloc_flip_flop_slot();
5299                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5300                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5301                    let rp_idx = self.chunk.add_constant(StrykeValue::string(rp.clone()));
5302                    let rf_idx = self.chunk.add_constant(StrykeValue::string(rf.clone()));
5303                    self.emit_op(
5304                        Op::RegexFlipFlop(
5305                            slot,
5306                            u8::from(*exclusive),
5307                            lp_idx,
5308                            lf_idx,
5309                            rp_idx,
5310                            rf_idx,
5311                        ),
5312                        line,
5313                        Some(root),
5314                    );
5315                } else if let (ExprKind::Regex(lp, lf), ExprKind::Eof(None)) =
5316                    (&from.kind, &to.kind)
5317                {
5318                    let slot = self.chunk.alloc_flip_flop_slot();
5319                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5320                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5321                    self.emit_op(
5322                        Op::RegexEofFlipFlop(slot, u8::from(*exclusive), lp_idx, lf_idx),
5323                        line,
5324                        Some(root),
5325                    );
5326                } else if matches!(
5327                    (&from.kind, &to.kind),
5328                    (ExprKind::Regex(_, _), ExprKind::Eof(Some(_)))
5329                ) {
5330                    return Err(CompileError::Unsupported(
5331                        "regex flip-flop with eof(HANDLE) is not supported".into(),
5332                    ));
5333                } else if let ExprKind::Regex(lp, lf) = &from.kind {
5334                    let slot = self.chunk.alloc_flip_flop_slot();
5335                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5336                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5337                    if matches!(to.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
5338                        let line_target = match &to.kind {
5339                            ExprKind::Integer(n) => *n,
5340                            ExprKind::Float(f) => *f as i64,
5341                            _ => unreachable!(),
5342                        };
5343                        let line_cidx = self.chunk.add_constant(StrykeValue::integer(line_target));
5344                        self.emit_op(
5345                            Op::RegexFlipFlopDotLineRhs(
5346                                slot,
5347                                u8::from(*exclusive),
5348                                lp_idx,
5349                                lf_idx,
5350                                line_cidx,
5351                            ),
5352                            line,
5353                            Some(root),
5354                        );
5355                    } else {
5356                        let rhs_idx = self
5357                            .chunk
5358                            .add_regex_flip_flop_rhs_expr_entry((**to).clone());
5359                        self.emit_op(
5360                            Op::RegexFlipFlopExprRhs(
5361                                slot,
5362                                u8::from(*exclusive),
5363                                lp_idx,
5364                                lf_idx,
5365                                rhs_idx,
5366                            ),
5367                            line,
5368                            Some(root),
5369                        );
5370                    }
5371                } else {
5372                    self.compile_expr(from)?;
5373                    self.compile_expr(to)?;
5374                    let slot = self.chunk.alloc_flip_flop_slot();
5375                    self.emit_op(
5376                        Op::ScalarFlipFlop(slot, u8::from(*exclusive)),
5377                        line,
5378                        Some(root),
5379                    );
5380                }
5381            }
5382
5383            ExprKind::SliceRange { .. } => {
5384                // Open-ended slice ranges (`:N`, `N:`, `::-1`, `::`) only have meaning
5385                // inside slice subscripts (`@arr[...]`, `@h{...}`), where they are
5386                // intercepted by the slice arms above. Anywhere else is a hard error —
5387                // we have no container length context to resolve open ends.
5388                return Err(CompileError::Unsupported(
5389                    "open-ended slice range (`:N`/`N:`/`::-1`) is only valid inside `@arr[...]` or `@h{...}` subscripts"
5390                        .into(),
5391                ));
5392            }
5393
5394            ExprKind::Repeat {
5395                expr,
5396                count,
5397                list_repeat,
5398            } => {
5399                if *list_repeat {
5400                    // List context for the LHS so `(EXPR)` and `qw(...)` flatten
5401                    // into the array we'll replicate.
5402                    self.compile_expr_ctx(expr, WantarrayCtx::List)?;
5403                    self.compile_expr(count)?;
5404                    self.emit_op(Op::ListRepeat, line, Some(root));
5405                } else {
5406                    self.compile_expr(expr)?;
5407                    self.compile_expr(count)?;
5408                    self.emit_op(Op::StringRepeat, line, Some(root));
5409                }
5410            }
5411
5412            // ── Function calls ──
5413            ExprKind::FuncCall { name, args } => {
5414                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
5415                // bare-name fast path so the arms below stay flat.
5416                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
5417                match dispatch_name {
5418                    // read(FH, $buf, LEN) — emit ReadIntoVar with the buffer variable's name index
5419                    "read" => {
5420                        if args.len() < 3 {
5421                            return Err(CompileError::Unsupported(
5422                                "read() needs at least 3 args".into(),
5423                            ));
5424                        }
5425                        // Extract buffer variable name from 2nd arg
5426                        let buf_name =
5427                            match &args[1].kind {
5428                                ExprKind::ScalarVar(n) => n.clone(),
5429                                _ => return Err(CompileError::Unsupported(
5430                                    "read() buffer must be a simple scalar variable for bytecode"
5431                                        .into(),
5432                                )),
5433                            };
5434                        let buf_idx = self.chunk.intern_name(&buf_name);
5435                        // Stack: [filehandle, length]
5436                        self.compile_expr(&args[0])?; // filehandle
5437                        self.compile_expr(&args[2])?; // length
5438                        self.emit_op(Op::ReadIntoVar(buf_idx), line, Some(root));
5439                    }
5440                    // `defer { BLOCK }` — desugared by parser to `defer__internal(fn { BLOCK })`
5441                    "defer__internal" => {
5442                        if args.len() != 1 {
5443                            return Err(CompileError::Unsupported(
5444                                "defer__internal expects exactly one argument".into(),
5445                            ));
5446                        }
5447                        // Compile the coderef argument; afterwards, un-mark
5448                        // the defer block as a sub-body so the closure-write
5449                        // check (DESIGN-001) doesn't flag mutations of outer
5450                        // `my` vars from inside `defer { ... }`. defer runs
5451                        // synchronously at scope exit and is intentionally
5452                        // shared-state with the enclosing scope.
5453                        self.compile_expr(&args[0])?;
5454                        if let ExprKind::CodeRef { .. } = &args[0].kind {
5455                            // The most-recently-pushed CodeRef block index is
5456                            // the highest one in `sub_body_block_indices`.
5457                            if let Some(max_idx) = self.sub_body_block_indices.iter().copied().max()
5458                            {
5459                                self.sub_body_block_indices.remove(&max_idx);
5460                            }
5461                        }
5462                        self.emit_op(Op::DeferBlock, line, Some(root));
5463                    }
5464                    "deque" => {
5465                        if !args.is_empty() {
5466                            return Err(CompileError::Unsupported(
5467                                "deque() takes no arguments".into(),
5468                            ));
5469                        }
5470                        self.emit_op(
5471                            Op::CallBuiltin(BuiltinId::DequeNew as u16, 0),
5472                            line,
5473                            Some(root),
5474                        );
5475                    }
5476                    "inc" => {
5477                        let arg = args.first().cloned().unwrap_or_else(|| Expr {
5478                            kind: ExprKind::ScalarVar("_".into()),
5479                            line,
5480                        });
5481                        self.compile_expr(&arg)?;
5482                        self.emit_op(Op::Inc, line, Some(root));
5483                    }
5484                    "dec" => {
5485                        let arg = args.first().cloned().unwrap_or_else(|| Expr {
5486                            kind: ExprKind::ScalarVar("_".into()),
5487                            line,
5488                        });
5489                        self.compile_expr(&arg)?;
5490                        self.emit_op(Op::Dec, line, Some(root));
5491                    }
5492                    "heap" => {
5493                        if args.len() != 1 {
5494                            return Err(CompileError::Unsupported(
5495                                "heap() expects one comparator sub".into(),
5496                            ));
5497                        }
5498                        self.compile_expr(&args[0])?;
5499                        self.emit_op(
5500                            Op::CallBuiltin(BuiltinId::HeapNew as u16, 1),
5501                            line,
5502                            Some(root),
5503                        );
5504                    }
5505                    "pipeline" => {
5506                        for arg in args {
5507                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5508                        }
5509                        self.emit_op(
5510                            Op::CallBuiltin(BuiltinId::Pipeline as u16, args.len() as u8),
5511                            line,
5512                            Some(root),
5513                        );
5514                    }
5515                    "par_pipeline" => {
5516                        for arg in args {
5517                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5518                        }
5519                        self.emit_op(
5520                            Op::CallBuiltin(BuiltinId::ParPipeline as u16, args.len() as u8),
5521                            line,
5522                            Some(root),
5523                        );
5524                    }
5525                    "par_pipeline_stream" => {
5526                        for arg in args {
5527                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5528                        }
5529                        self.emit_op(
5530                            Op::CallBuiltin(BuiltinId::ParPipelineStream as u16, args.len() as u8),
5531                            line,
5532                            Some(root),
5533                        );
5534                    }
5535                    // `collect(EXPR)` — compile the argument in list context so nested
5536                    // `map { }` / `grep { }` keep a pipeline handle (scalar context adds
5537                    // `StackArrayLen`, which turns a pipeline into `1`). At runtime, a
5538                    // pipeline runs staged ops; any other value is materialized as an array
5539                    // (`|> … |> collect()`).
5540                    "collect" => {
5541                        for arg in args {
5542                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5543                        }
5544                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5545                        self.emit_op(
5546                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5547                            line,
5548                            Some(root),
5549                        );
5550                    }
5551                    "ppool" => {
5552                        if args.len() != 1 {
5553                            return Err(CompileError::Unsupported(
5554                                "ppool() expects one argument (worker count)".into(),
5555                            ));
5556                        }
5557                        self.compile_expr(&args[0])?;
5558                        self.emit_op(
5559                            Op::CallBuiltin(BuiltinId::Ppool as u16, 1),
5560                            line,
5561                            Some(root),
5562                        );
5563                    }
5564                    "barrier" => {
5565                        if args.len() != 1 {
5566                            return Err(CompileError::Unsupported(
5567                                "barrier() expects one argument (party count)".into(),
5568                            ));
5569                        }
5570                        self.compile_expr(&args[0])?;
5571                        self.emit_op(
5572                            Op::CallBuiltin(BuiltinId::BarrierNew as u16, 1),
5573                            line,
5574                            Some(root),
5575                        );
5576                    }
5577                    "cluster" => {
5578                        // Each arg pushed in list context so an `@hosts`
5579                        // operand flattens into individual slot specs and
5580                        // a single bareword `cluster("host1:4")` arrives
5581                        // as one string. `ClusterNew` mirrors the
5582                        // tree-walker arm in `vm_helper::call_named_sub`.
5583                        if args.is_empty() {
5584                            return Err(CompileError::Unsupported(
5585                                "cluster() expects at least one host/slot specifier".into(),
5586                            ));
5587                        }
5588                        for arg in args {
5589                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5590                        }
5591                        self.emit_op(
5592                            Op::CallBuiltin(BuiltinId::ClusterNew as u16, args.len() as u8),
5593                            line,
5594                            Some(root),
5595                        );
5596                    }
5597                    "pselect" => {
5598                        if args.is_empty() {
5599                            return Err(CompileError::Unsupported(
5600                                "pselect() expects at least one pchannel receiver".into(),
5601                            ));
5602                        }
5603                        for arg in args {
5604                            self.compile_expr(arg)?;
5605                        }
5606                        self.emit_op(
5607                            Op::CallBuiltin(BuiltinId::Pselect as u16, args.len() as u8),
5608                            line,
5609                            Some(root),
5610                        );
5611                    }
5612                    "ssh" => {
5613                        for arg in args {
5614                            self.compile_expr(arg)?;
5615                        }
5616                        self.emit_op(
5617                            Op::CallBuiltin(BuiltinId::Ssh as u16, args.len() as u8),
5618                            line,
5619                            Some(root),
5620                        );
5621                    }
5622                    "rmdir" => {
5623                        for arg in args {
5624                            self.compile_expr(arg)?;
5625                        }
5626                        self.emit_op(
5627                            Op::CallBuiltin(BuiltinId::Rmdir as u16, args.len() as u8),
5628                            line,
5629                            Some(root),
5630                        );
5631                    }
5632                    "utime" => {
5633                        for arg in args {
5634                            self.compile_expr(arg)?;
5635                        }
5636                        self.emit_op(
5637                            Op::CallBuiltin(BuiltinId::Utime as u16, args.len() as u8),
5638                            line,
5639                            Some(root),
5640                        );
5641                    }
5642                    "umask" => {
5643                        for arg in args {
5644                            self.compile_expr(arg)?;
5645                        }
5646                        self.emit_op(
5647                            Op::CallBuiltin(BuiltinId::Umask as u16, args.len() as u8),
5648                            line,
5649                            Some(root),
5650                        );
5651                    }
5652                    "getcwd" => {
5653                        for arg in args {
5654                            self.compile_expr(arg)?;
5655                        }
5656                        self.emit_op(
5657                            Op::CallBuiltin(BuiltinId::Getcwd as u16, args.len() as u8),
5658                            line,
5659                            Some(root),
5660                        );
5661                    }
5662                    "pipe" => {
5663                        if args.len() != 2 {
5664                            return Err(CompileError::Unsupported(
5665                                "pipe requires exactly two arguments".into(),
5666                            ));
5667                        }
5668                        for arg in args {
5669                            self.compile_expr(arg)?;
5670                        }
5671                        self.emit_op(Op::CallBuiltin(BuiltinId::Pipe as u16, 2), line, Some(root));
5672                    }
5673                    "uniq" | "distinct" | "flatten" | "set" | "with_index" | "list_count"
5674                    | "list_size" | "count" | "size" | "cnt" | "len" | "sum" | "sum0"
5675                    | "product" | "min" | "max" | "mean" | "median" | "mode" | "stddev"
5676                    | "variance" => {
5677                        // Fast path for `len @arr` / `cnt @arr` / `count @arr` and the deref
5678                        // variants `len @$ref` / `len @{$ref}`: emit the same direct length op
5679                        // (`ArrayLen` / `ArrayDerefLen`) that `scalar @arr` uses, so the
5680                        // idiomatic stryke spelling is no slower than the Perl-compat one.
5681                        if matches!(
5682                            name.as_str(),
5683                            "count" | "cnt" | "size" | "len" | "list_count" | "list_size"
5684                        ) && args.len() == 1
5685                        {
5686                            match &args[0].kind {
5687                                ExprKind::ArrayVar(arr_name) => {
5688                                    self.check_strict_array_access(arr_name, line)?;
5689                                    let idx = self
5690                                        .chunk
5691                                        .intern_name(&self.qualify_stash_array_name(arr_name));
5692                                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
5693                                    return Ok(());
5694                                }
5695                                ExprKind::Deref {
5696                                    expr,
5697                                    kind: Sigil::Array,
5698                                } => {
5699                                    self.compile_expr(expr)?;
5700                                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
5701                                    return Ok(());
5702                                }
5703                                _ => {}
5704                            }
5705                        }
5706                        for arg in args {
5707                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5708                        }
5709                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5710                        self.emit_op(
5711                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5712                            line,
5713                            Some(root),
5714                        );
5715                    }
5716                    "shuffle" => {
5717                        for arg in args {
5718                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5719                        }
5720                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5721                        self.emit_op(
5722                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5723                            line,
5724                            Some(root),
5725                        );
5726                    }
5727                    "chunked" | "windowed" => {
5728                        match args.len() {
5729                            0 => {
5730                                return Err(CompileError::Unsupported(
5731                                "chunked/windowed need (LIST, N) or unary N (e.g. `|> chunked(2)`)"
5732                                    .into(),
5733                            ));
5734                            }
5735                            1 => {
5736                                // chunked @l / windowed @l — compile in list context, default size
5737                                self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5738                            }
5739                            2 => {
5740                                self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5741                                self.compile_expr(&args[1])?;
5742                            }
5743                            _ => {
5744                                return Err(CompileError::Unsupported(
5745                                "chunked/windowed expect exactly two arguments (LIST, N); use a single list expression for the first operand".into(),
5746                            ));
5747                            }
5748                        }
5749                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5750                        self.emit_op(
5751                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5752                            line,
5753                            Some(root),
5754                        );
5755                    }
5756                    "take" | "head" | "tail" | "drop" => {
5757                        if args.is_empty() {
5758                            return Err(CompileError::Unsupported(
5759                                "take/head/tail/drop expect LIST..., N or unary N".into(),
5760                            ));
5761                        }
5762                        if args.len() == 1 {
5763                            // head @l == head @l, 1 — evaluate in list context
5764                            self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5765                        } else {
5766                            for a in &args[..args.len() - 1] {
5767                                self.compile_expr_ctx(a, WantarrayCtx::List)?;
5768                            }
5769                            self.compile_expr(&args[args.len() - 1])?;
5770                        }
5771                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5772                        self.emit_op(
5773                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5774                            line,
5775                            Some(root),
5776                        );
5777                    }
5778                    "any" | "all" | "none" | "first" | "take_while" | "drop_while" | "tap"
5779                    | "peek" => {
5780                        // Three shapes:
5781                        //   `any { BLOCK } @list`           — block form
5782                        //   `any(fn { ... }, 1, 2, 3)`      — slurpy `(&@)` form
5783                        //   `any($coderef, @list)`          — coderef-in-block-position
5784                        //   `any($f, @list)` (no parens)    — same, runtime dispatch
5785                        // Builtin runtime checks `as_code_ref()` and dispatches; if
5786                        // the first arg isn't a coderef the builtin uses its
5787                        // value-shape semantics. Compiler stays out of the way.
5788                        if args.is_empty() {
5789                            return Err(CompileError::Unsupported(
5790                            "any/all/none/first/take_while/drop_while/tap/peek expect BLOCK, LIST"
5791                                .into(),
5792                        ));
5793                        }
5794                        self.compile_expr(&args[0])?;
5795                        for arg in &args[1..] {
5796                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5797                        }
5798                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5799                        self.emit_op(
5800                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5801                            line,
5802                            Some(root),
5803                        );
5804                    }
5805                    "group_by" | "chunk_by" => {
5806                        if args.len() != 2 {
5807                            return Err(CompileError::Unsupported(
5808                                "group_by/chunk_by expect { BLOCK } or EXPR, LIST".into(),
5809                            ));
5810                        }
5811                        self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5812                        match &args[0].kind {
5813                            ExprKind::CodeRef { body, .. } => {
5814                                let block_idx = self.add_deferred_block(body.clone());
5815                                self.emit_op(Op::ChunkByWithBlock(block_idx), line, Some(root));
5816                            }
5817                            _ => {
5818                                let idx = self.chunk.add_map_expr_entry(args[0].clone());
5819                                self.emit_op(Op::ChunkByWithExpr(idx), line, Some(root));
5820                            }
5821                        }
5822                        if ctx != WantarrayCtx::List {
5823                            self.emit_op(Op::StackArrayLen, line, Some(root));
5824                        }
5825                    }
5826                    "zip" | "zip_longest" => {
5827                        for arg in args {
5828                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5829                        }
5830                        // Both forms are stryke bare-name builtins; the VM slow path strips the
5831                        // `main::` qualifier and routes through `try_builtin` → `dispatch_by_name`.
5832                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(dispatch_name));
5833                        self.emit_op(
5834                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5835                            line,
5836                            Some(root),
5837                        );
5838                    }
5839                    "puniq" => {
5840                        if args.is_empty() || args.len() > 2 {
5841                            return Err(CompileError::Unsupported(
5842                                "puniq expects LIST [, progress => EXPR]".into(),
5843                            ));
5844                        }
5845                        if args.len() == 2 {
5846                            self.compile_expr(&args[1])?;
5847                        } else {
5848                            self.emit_op(Op::LoadInt(0), line, Some(root));
5849                        }
5850                        self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5851                        self.emit_op(Op::Puniq, line, Some(root));
5852                        if ctx != WantarrayCtx::List {
5853                            self.emit_op(Op::StackArrayLen, line, Some(root));
5854                        }
5855                    }
5856                    "pfirst" | "pany" => {
5857                        if args.len() < 2 || args.len() > 3 {
5858                            return Err(CompileError::Unsupported(
5859                                "pfirst/pany expect BLOCK, LIST [, progress => EXPR]".into(),
5860                            ));
5861                        }
5862                        let body = match &args[0].kind {
5863                            ExprKind::CodeRef { body, .. } => body,
5864                            _ => {
5865                                return Err(CompileError::Unsupported(
5866                                    "pfirst/pany: first argument must be a { BLOCK }".into(),
5867                                ));
5868                            }
5869                        };
5870                        if args.len() == 3 {
5871                            self.compile_expr(&args[2])?;
5872                        } else {
5873                            self.emit_op(Op::LoadInt(0), line, Some(root));
5874                        }
5875                        self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5876                        let block_idx = self.add_deferred_block(body.clone());
5877                        let op = if name == "pfirst" {
5878                            Op::PFirstWithBlock(block_idx)
5879                        } else {
5880                            Op::PAnyWithBlock(block_idx)
5881                        };
5882                        self.emit_op(op, line, Some(root));
5883                    }
5884                    _ => {
5885                        // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
5886                        // `f(reverse LIST)` etc. flatten into `@_`. [`Self::pop_call_operands_flattened`]
5887                        // splats any array value at runtime, matching Perl's `@_` semantics.
5888                        for arg in args {
5889                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5890                        }
5891                        let q = self.qualify_sub_key(name);
5892                        let name_idx = self.chunk.intern_name(&q);
5893                        self.emit_op(
5894                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5895                            line,
5896                            Some(root),
5897                        );
5898                    }
5899                }
5900            }
5901
5902            // ── Method calls ──
5903            ExprKind::MethodCall {
5904                object,
5905                method,
5906                args,
5907                super_call,
5908            } => {
5909                self.compile_expr(object)?;
5910                for arg in args {
5911                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5912                }
5913                let name_idx = self.chunk.intern_name(method);
5914                if *super_call {
5915                    self.emit_op(
5916                        Op::MethodCallSuper(name_idx, args.len() as u8, ctx.as_byte()),
5917                        line,
5918                        Some(root),
5919                    );
5920                } else {
5921                    self.emit_op(
5922                        Op::MethodCall(name_idx, args.len() as u8, ctx.as_byte()),
5923                        line,
5924                        Some(root),
5925                    );
5926                }
5927            }
5928            ExprKind::IndirectCall {
5929                target,
5930                args,
5931                ampersand: _,
5932                pass_caller_arglist,
5933            } => {
5934                self.compile_expr(target)?;
5935                if !pass_caller_arglist {
5936                    for a in args {
5937                        self.compile_expr_ctx(a, WantarrayCtx::List)?;
5938                    }
5939                }
5940                let argc = if *pass_caller_arglist {
5941                    0
5942                } else {
5943                    args.len() as u8
5944                };
5945                self.emit_op(
5946                    Op::IndirectCall(
5947                        argc,
5948                        ctx.as_byte(),
5949                        if *pass_caller_arglist { 1 } else { 0 },
5950                    ),
5951                    line,
5952                    Some(root),
5953                );
5954            }
5955
5956            // ── Print / Say / Printf ──
5957            ExprKind::Print { handle, args } => {
5958                for arg in args {
5959                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5960                }
5961                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
5962                self.emit_op(Op::Print(h, args.len() as u8), line, Some(root));
5963            }
5964            ExprKind::Say { handle, args } => {
5965                for arg in args {
5966                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5967                }
5968                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
5969                self.emit_op(Op::Say(h, args.len() as u8), line, Some(root));
5970            }
5971            ExprKind::Printf { args, .. } => {
5972                // printf's format + arg list is Perl list context — ranges, arrays, and
5973                // `reverse`/`sort`/`grep` flatten into format argument positions.
5974                for arg in args {
5975                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5976                }
5977                self.emit_op(
5978                    Op::CallBuiltin(BuiltinId::Printf as u16, args.len() as u8),
5979                    line,
5980                    Some(root),
5981                );
5982            }
5983
5984            // ── Die / Warn ──
5985            ExprKind::Die(args) => {
5986                // die / warn take a list that gets stringified and concatenated — list context
5987                // so `die 1..5` matches Perl's "12345" stringification.
5988                for arg in args {
5989                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5990                }
5991                self.emit_op(
5992                    Op::CallBuiltin(BuiltinId::Die as u16, args.len() as u8),
5993                    line,
5994                    Some(root),
5995                );
5996            }
5997            ExprKind::Warn(args) => {
5998                for arg in args {
5999                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
6000                }
6001                self.emit_op(
6002                    Op::CallBuiltin(BuiltinId::Warn as u16, args.len() as u8),
6003                    line,
6004                    Some(root),
6005                );
6006            }
6007            ExprKind::Exit(code) => {
6008                if let Some(c) = code {
6009                    self.compile_expr(c)?;
6010                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
6011                } else {
6012                    self.emit_op(Op::LoadInt(0), line, Some(root));
6013                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
6014                }
6015            }
6016
6017            // ── Array ops ──
6018            ExprKind::Push { array, values } => {
6019                if let ExprKind::ArrayVar(name) = &array.kind {
6020                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6021                    for v in values {
6022                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
6023                        self.emit_op(Op::PushArray(idx), line, Some(root));
6024                    }
6025                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
6026                } else if let ExprKind::Deref {
6027                    expr: aref_expr,
6028                    kind: Sigil::Array,
6029                } = &array.kind
6030                {
6031                    // Autovivifiable inner shapes (`$x`, `$h{k}`, `$a[i]`) need lvalue
6032                    // resolution: when the slot is undef, `push @{...}` must create a new
6033                    // arrayref and store it back. Routed through PushExpr where
6034                    // `try_eval_array_deref_container` handles autoviv.
6035                    let needs_autoviv = matches!(
6036                        &aref_expr.kind,
6037                        ExprKind::ScalarVar(_)
6038                            | ExprKind::HashElement { .. }
6039                            | ExprKind::ArrayElement { .. }
6040                    );
6041                    if needs_autoviv {
6042                        let pool = self
6043                            .chunk
6044                            .add_push_expr_entry(array.as_ref().clone(), values.clone());
6045                        self.emit_op(Op::PushExpr(pool), line, Some(root));
6046                    } else {
6047                        self.compile_expr(aref_expr)?;
6048                        for v in values {
6049                            self.emit_op(Op::Dup, line, Some(root));
6050                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
6051                            self.emit_op(Op::PushArrayDeref, line, Some(root));
6052                        }
6053                        self.emit_op(Op::ArrayDerefLen, line, Some(root));
6054                    }
6055                } else {
6056                    let pool = self
6057                        .chunk
6058                        .add_push_expr_entry(array.as_ref().clone(), values.clone());
6059                    self.emit_op(Op::PushExpr(pool), line, Some(root));
6060                }
6061            }
6062            ExprKind::Pop(array) => {
6063                if let ExprKind::ArrayVar(name) = &array.kind {
6064                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6065                    self.emit_op(Op::PopArray(idx), line, Some(root));
6066                } else if let ExprKind::Deref {
6067                    expr: aref_expr,
6068                    kind: Sigil::Array,
6069                } = &array.kind
6070                {
6071                    let needs_autoviv = matches!(
6072                        &aref_expr.kind,
6073                        ExprKind::ScalarVar(_)
6074                            | ExprKind::HashElement { .. }
6075                            | ExprKind::ArrayElement { .. }
6076                    );
6077                    if needs_autoviv {
6078                        let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
6079                        self.emit_op(Op::PopExpr(pool), line, Some(root));
6080                    } else {
6081                        self.compile_expr(aref_expr)?;
6082                        self.emit_op(Op::PopArrayDeref, line, Some(root));
6083                    }
6084                } else {
6085                    let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
6086                    self.emit_op(Op::PopExpr(pool), line, Some(root));
6087                }
6088            }
6089            ExprKind::Shift(array) => {
6090                if let ExprKind::ArrayVar(name) = &array.kind {
6091                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6092                    self.emit_op(Op::ShiftArray(idx), line, Some(root));
6093                } else if let ExprKind::Deref {
6094                    expr: aref_expr,
6095                    kind: Sigil::Array,
6096                } = &array.kind
6097                {
6098                    let needs_autoviv = matches!(
6099                        &aref_expr.kind,
6100                        ExprKind::ScalarVar(_)
6101                            | ExprKind::HashElement { .. }
6102                            | ExprKind::ArrayElement { .. }
6103                    );
6104                    if needs_autoviv {
6105                        let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
6106                        self.emit_op(Op::ShiftExpr(pool), line, Some(root));
6107                    } else {
6108                        self.compile_expr(aref_expr)?;
6109                        self.emit_op(Op::ShiftArrayDeref, line, Some(root));
6110                    }
6111                } else {
6112                    let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
6113                    self.emit_op(Op::ShiftExpr(pool), line, Some(root));
6114                }
6115            }
6116            ExprKind::Unshift { array, values } => {
6117                if let ExprKind::ArrayVar(name) = &array.kind {
6118                    let q = self.qualify_stash_array_name(name);
6119                    let name_const = self.chunk.add_constant(StrykeValue::string(q));
6120                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
6121                    for v in values {
6122                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
6123                    }
6124                    let nargs = (1 + values.len()) as u8;
6125                    self.emit_op(
6126                        Op::CallBuiltin(BuiltinId::Unshift as u16, nargs),
6127                        line,
6128                        Some(root),
6129                    );
6130                } else if let ExprKind::Deref {
6131                    expr: aref_expr,
6132                    kind: Sigil::Array,
6133                } = &array.kind
6134                {
6135                    let needs_autoviv = matches!(
6136                        &aref_expr.kind,
6137                        ExprKind::ScalarVar(_)
6138                            | ExprKind::HashElement { .. }
6139                            | ExprKind::ArrayElement { .. }
6140                    );
6141                    if needs_autoviv || values.len() > u8::MAX as usize {
6142                        let pool = self
6143                            .chunk
6144                            .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
6145                        self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
6146                    } else {
6147                        self.compile_expr(aref_expr)?;
6148                        for v in values {
6149                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
6150                        }
6151                        self.emit_op(Op::UnshiftArrayDeref(values.len() as u8), line, Some(root));
6152                    }
6153                } else {
6154                    let pool = self
6155                        .chunk
6156                        .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
6157                    self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
6158                }
6159            }
6160            ExprKind::Splice {
6161                array,
6162                offset,
6163                length,
6164                replacement,
6165            } => {
6166                self.emit_op(Op::WantarrayPush(ctx.as_byte()), line, Some(root));
6167                if let ExprKind::ArrayVar(name) = &array.kind {
6168                    let q = self.qualify_stash_array_name(name);
6169                    let name_const = self.chunk.add_constant(StrykeValue::string(q));
6170                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
6171                    if let Some(o) = offset {
6172                        self.compile_expr(o)?;
6173                    } else {
6174                        self.emit_op(Op::LoadInt(0), line, Some(root));
6175                    }
6176                    if let Some(l) = length {
6177                        self.compile_expr(l)?;
6178                    } else {
6179                        self.emit_op(Op::LoadUndef, line, Some(root));
6180                    }
6181                    for r in replacement {
6182                        self.compile_expr(r)?;
6183                    }
6184                    let nargs = (3 + replacement.len()) as u8;
6185                    self.emit_op(
6186                        Op::CallBuiltin(BuiltinId::Splice as u16, nargs),
6187                        line,
6188                        Some(root),
6189                    );
6190                } else if let ExprKind::Deref {
6191                    expr: aref_expr,
6192                    kind: Sigil::Array,
6193                } = &array.kind
6194                {
6195                    if replacement.len() > u8::MAX as usize {
6196                        let pool = self.chunk.add_splice_expr_entry(
6197                            array.as_ref().clone(),
6198                            offset.as_deref().cloned(),
6199                            length.as_deref().cloned(),
6200                            replacement.clone(),
6201                        );
6202                        self.emit_op(Op::SpliceExpr(pool), line, Some(root));
6203                    } else {
6204                        self.compile_expr(aref_expr)?;
6205                        if let Some(o) = offset {
6206                            self.compile_expr(o)?;
6207                        } else {
6208                            self.emit_op(Op::LoadInt(0), line, Some(root));
6209                        }
6210                        if let Some(l) = length {
6211                            self.compile_expr(l)?;
6212                        } else {
6213                            self.emit_op(Op::LoadUndef, line, Some(root));
6214                        }
6215                        for r in replacement {
6216                            self.compile_expr(r)?;
6217                        }
6218                        self.emit_op(
6219                            Op::SpliceArrayDeref(replacement.len() as u8),
6220                            line,
6221                            Some(root),
6222                        );
6223                    }
6224                } else {
6225                    let pool = self.chunk.add_splice_expr_entry(
6226                        array.as_ref().clone(),
6227                        offset.as_deref().cloned(),
6228                        length.as_deref().cloned(),
6229                        replacement.clone(),
6230                    );
6231                    self.emit_op(Op::SpliceExpr(pool), line, Some(root));
6232                }
6233                self.emit_op(Op::WantarrayPop, line, Some(root));
6234            }
6235            ExprKind::ScalarContext(inner) => {
6236                // `scalar EXPR` forces scalar context on EXPR regardless of the outer context
6237                // (e.g. `print scalar grep { } @x` — grep's result is a count, not a list).
6238                self.compile_expr_ctx(inner, WantarrayCtx::Scalar)?;
6239                // Then apply aggregate scalar semantics (set size, pipeline source len, …) —
6240                // same as [`Op::ValueScalarContext`] / [`StrykeValue::scalar_context`].
6241                self.emit_op(Op::ValueScalarContext, line, Some(root));
6242            }
6243
6244            // ── Hash ops ──
6245            ExprKind::Delete(inner) => {
6246                if let ExprKind::HashElement { hash, key } = &inner.kind {
6247                    self.check_hash_mutable(hash, line)?;
6248                    let idx = self.chunk.intern_name(hash);
6249                    self.compile_expr(key)?;
6250                    self.emit_op(Op::DeleteHashElem(idx), line, Some(root));
6251                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
6252                    self.check_strict_array_access(array, line)?;
6253                    let q = self.qualify_stash_array_name(array);
6254                    self.check_array_mutable(&q, line)?;
6255                    let arr_idx = self.chunk.intern_name(&q);
6256                    self.compile_expr(index)?;
6257                    self.emit_op(Op::DeleteArrayElem(arr_idx), line, Some(root));
6258                } else if let ExprKind::ArrowDeref {
6259                    expr: container,
6260                    index,
6261                    kind: DerefKind::Hash,
6262                } = &inner.kind
6263                {
6264                    self.compile_arrow_hash_base_expr(container)?;
6265                    self.compile_expr(index)?;
6266                    self.emit_op(Op::DeleteArrowHashElem, line, Some(root));
6267                } else if let ExprKind::ArrowDeref {
6268                    expr: container,
6269                    index,
6270                    kind: DerefKind::Array,
6271                } = &inner.kind
6272                {
6273                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
6274                        self.compile_expr(container)?;
6275                        self.compile_expr(index)?;
6276                        self.emit_op(Op::DeleteArrowArrayElem, line, Some(root));
6277                    } else {
6278                        let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
6279                        self.emit_op(Op::DeleteExpr(pool), line, Some(root));
6280                    }
6281                } else {
6282                    let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
6283                    self.emit_op(Op::DeleteExpr(pool), line, Some(root));
6284                }
6285            }
6286            ExprKind::Exists(inner) => {
6287                if let ExprKind::HashElement { hash, key } = &inner.kind {
6288                    let idx = self.chunk.intern_name(hash);
6289                    self.compile_expr(key)?;
6290                    self.emit_op(Op::ExistsHashElem(idx), line, Some(root));
6291                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
6292                    self.check_strict_array_access(array, line)?;
6293                    let arr_idx = self
6294                        .chunk
6295                        .intern_name(&self.qualify_stash_array_name(array));
6296                    self.compile_expr(index)?;
6297                    self.emit_op(Op::ExistsArrayElem(arr_idx), line, Some(root));
6298                } else if let ExprKind::ArrowDeref {
6299                    expr: container,
6300                    index,
6301                    kind: DerefKind::Hash,
6302                } = &inner.kind
6303                {
6304                    // Multi-level chains (e.g. `exists $h{x}{y}{z}`) need
6305                    // undef-tolerant intermediate eval — route through
6306                    // `ExistsExpr` (which calls `eval_exists_operand` and
6307                    // soft-fails on undef intermediates). (BUG-009)
6308                    if matches!(container.kind, ExprKind::ArrowDeref { .. }) {
6309                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6310                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6311                    } else {
6312                        self.compile_arrow_hash_base_expr(container)?;
6313                        self.compile_expr(index)?;
6314                        self.emit_op(Op::ExistsArrowHashElem, line, Some(root));
6315                    }
6316                } else if let ExprKind::ArrowDeref {
6317                    expr: container,
6318                    index,
6319                    kind: DerefKind::Array,
6320                } = &inner.kind
6321                {
6322                    if !arrow_deref_arrow_subscript_is_plain_scalar_index(index)
6323                        || matches!(container.kind, ExprKind::ArrowDeref { .. })
6324                    {
6325                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6326                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6327                    } else {
6328                        self.compile_expr(container)?;
6329                        self.compile_expr(index)?;
6330                        self.emit_op(Op::ExistsArrowArrayElem, line, Some(root));
6331                    }
6332                } else {
6333                    let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6334                    self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6335                }
6336            }
6337            ExprKind::Keys(inner) => {
6338                if let ExprKind::HashVar(name) = &inner.kind {
6339                    let idx = self.chunk.intern_name(name);
6340                    if ctx == WantarrayCtx::List {
6341                        self.emit_op(Op::HashKeys(idx), line, Some(root));
6342                    } else {
6343                        self.emit_op(Op::HashKeysScalar(idx), line, Some(root));
6344                    }
6345                } else {
6346                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
6347                    if ctx == WantarrayCtx::List {
6348                        self.emit_op(Op::KeysFromValue, line, Some(root));
6349                    } else {
6350                        self.emit_op(Op::KeysFromValueScalar, line, Some(root));
6351                    }
6352                }
6353            }
6354            ExprKind::Values(inner) => {
6355                if let ExprKind::HashVar(name) = &inner.kind {
6356                    let idx = self.chunk.intern_name(name);
6357                    if ctx == WantarrayCtx::List {
6358                        self.emit_op(Op::HashValues(idx), line, Some(root));
6359                    } else {
6360                        self.emit_op(Op::HashValuesScalar(idx), line, Some(root));
6361                    }
6362                } else {
6363                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
6364                    if ctx == WantarrayCtx::List {
6365                        self.emit_op(Op::ValuesFromValue, line, Some(root));
6366                    } else {
6367                        self.emit_op(Op::ValuesFromValueScalar, line, Some(root));
6368                    }
6369                }
6370            }
6371            ExprKind::Each(e) => {
6372                self.compile_expr(e)?;
6373                self.emit_op(Op::CallBuiltin(BuiltinId::Each as u16, 1), line, Some(root));
6374            }
6375
6376            // ── Builtins that map to CallBuiltin ──
6377            ExprKind::Length(e) => {
6378                self.compile_expr(e)?;
6379                self.emit_op(
6380                    Op::CallBuiltin(BuiltinId::Length as u16, 1),
6381                    line,
6382                    Some(root),
6383                );
6384            }
6385            ExprKind::Chomp(e) => {
6386                self.compile_expr(e)?;
6387                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
6388                self.emit_op(Op::ChompInPlace(lv), line, Some(root));
6389            }
6390            ExprKind::Chop(e) => {
6391                self.compile_expr(e)?;
6392                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
6393                self.emit_op(Op::ChopInPlace(lv), line, Some(root));
6394            }
6395            ExprKind::Defined(e) => {
6396                self.compile_expr(e)?;
6397                self.emit_op(
6398                    Op::CallBuiltin(BuiltinId::Defined as u16, 1),
6399                    line,
6400                    Some(root),
6401                );
6402            }
6403            ExprKind::Abs(e) => {
6404                self.compile_expr(e)?;
6405                self.emit_op(Op::CallBuiltin(BuiltinId::Abs as u16, 1), line, Some(root));
6406            }
6407            ExprKind::Int(e) => {
6408                self.compile_expr(e)?;
6409                self.emit_op(Op::CallBuiltin(BuiltinId::Int as u16, 1), line, Some(root));
6410            }
6411            ExprKind::Sqrt(e) => {
6412                self.compile_expr(e)?;
6413                self.emit_op(Op::CallBuiltin(BuiltinId::Sqrt as u16, 1), line, Some(root));
6414            }
6415            ExprKind::Sin(e) => {
6416                self.compile_expr(e)?;
6417                self.emit_op(Op::CallBuiltin(BuiltinId::Sin as u16, 1), line, Some(root));
6418            }
6419            ExprKind::Cos(e) => {
6420                self.compile_expr(e)?;
6421                self.emit_op(Op::CallBuiltin(BuiltinId::Cos as u16, 1), line, Some(root));
6422            }
6423            ExprKind::Atan2 { y, x } => {
6424                self.compile_expr(y)?;
6425                self.compile_expr(x)?;
6426                self.emit_op(
6427                    Op::CallBuiltin(BuiltinId::Atan2 as u16, 2),
6428                    line,
6429                    Some(root),
6430                );
6431            }
6432            ExprKind::Exp(e) => {
6433                self.compile_expr(e)?;
6434                self.emit_op(Op::CallBuiltin(BuiltinId::Exp as u16, 1), line, Some(root));
6435            }
6436            ExprKind::Log(e) => {
6437                self.compile_expr(e)?;
6438                self.emit_op(Op::CallBuiltin(BuiltinId::Log as u16, 1), line, Some(root));
6439            }
6440            ExprKind::Rand(upper) => {
6441                if let Some(e) = upper {
6442                    self.compile_expr(e)?;
6443                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 1), line, Some(root));
6444                } else {
6445                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 0), line, Some(root));
6446                }
6447            }
6448            ExprKind::Srand(seed) => {
6449                if let Some(e) = seed {
6450                    self.compile_expr(e)?;
6451                    self.emit_op(
6452                        Op::CallBuiltin(BuiltinId::Srand as u16, 1),
6453                        line,
6454                        Some(root),
6455                    );
6456                } else {
6457                    self.emit_op(
6458                        Op::CallBuiltin(BuiltinId::Srand as u16, 0),
6459                        line,
6460                        Some(root),
6461                    );
6462                }
6463            }
6464            ExprKind::Chr(e) => {
6465                self.compile_expr(e)?;
6466                self.emit_op(Op::CallBuiltin(BuiltinId::Chr as u16, 1), line, Some(root));
6467            }
6468            ExprKind::Ord(e) => {
6469                self.compile_expr(e)?;
6470                self.emit_op(Op::CallBuiltin(BuiltinId::Ord as u16, 1), line, Some(root));
6471            }
6472            ExprKind::Hex(e) => {
6473                self.compile_expr(e)?;
6474                self.emit_op(Op::CallBuiltin(BuiltinId::Hex as u16, 1), line, Some(root));
6475            }
6476            ExprKind::Oct(e) => {
6477                self.compile_expr(e)?;
6478                self.emit_op(Op::CallBuiltin(BuiltinId::Oct as u16, 1), line, Some(root));
6479            }
6480            ExprKind::Uc(e) => {
6481                self.compile_expr(e)?;
6482                self.emit_op(Op::CallBuiltin(BuiltinId::Uc as u16, 1), line, Some(root));
6483            }
6484            ExprKind::Lc(e) => {
6485                self.compile_expr(e)?;
6486                self.emit_op(Op::CallBuiltin(BuiltinId::Lc as u16, 1), line, Some(root));
6487            }
6488            ExprKind::Ucfirst(e) => {
6489                self.compile_expr(e)?;
6490                self.emit_op(
6491                    Op::CallBuiltin(BuiltinId::Ucfirst as u16, 1),
6492                    line,
6493                    Some(root),
6494                );
6495            }
6496            ExprKind::Lcfirst(e) => {
6497                self.compile_expr(e)?;
6498                self.emit_op(
6499                    Op::CallBuiltin(BuiltinId::Lcfirst as u16, 1),
6500                    line,
6501                    Some(root),
6502                );
6503            }
6504            ExprKind::Fc(e) => {
6505                self.compile_expr(e)?;
6506                self.emit_op(Op::CallBuiltin(BuiltinId::Fc as u16, 1), line, Some(root));
6507            }
6508            ExprKind::Crypt { plaintext, salt } => {
6509                self.compile_expr(plaintext)?;
6510                self.compile_expr(salt)?;
6511                self.emit_op(
6512                    Op::CallBuiltin(BuiltinId::Crypt as u16, 2),
6513                    line,
6514                    Some(root),
6515                );
6516            }
6517            ExprKind::Pos(e) => match e {
6518                None => {
6519                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 0), line, Some(root));
6520                }
6521                Some(pos_arg) => {
6522                    if let ExprKind::ScalarVar(name) = &pos_arg.kind {
6523                        let stor = self.scalar_storage_name_for_ops(name);
6524                        let idx = self.chunk.add_constant(StrykeValue::string(stor));
6525                        self.emit_op(Op::LoadConst(idx), line, Some(root));
6526                    } else {
6527                        self.compile_expr(pos_arg)?;
6528                    }
6529                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 1), line, Some(root));
6530                }
6531            },
6532            ExprKind::Study(e) => {
6533                self.compile_expr(e)?;
6534                self.emit_op(
6535                    Op::CallBuiltin(BuiltinId::Study as u16, 1),
6536                    line,
6537                    Some(root),
6538                );
6539            }
6540            ExprKind::Ref(e) => {
6541                self.compile_expr(e)?;
6542                self.emit_op(Op::CallBuiltin(BuiltinId::Ref as u16, 1), line, Some(root));
6543            }
6544            ExprKind::Rev(e) => {
6545                // Compile in list context to get arrays/hashes as collections
6546                self.compile_expr_ctx(e, WantarrayCtx::List)?;
6547                if ctx == WantarrayCtx::List {
6548                    self.emit_op(Op::RevListOp, line, Some(root));
6549                } else {
6550                    self.emit_op(Op::RevScalarOp, line, Some(root));
6551                }
6552            }
6553            ExprKind::ReverseExpr(e) => {
6554                self.compile_expr_ctx(e, WantarrayCtx::List)?;
6555                if ctx == WantarrayCtx::List {
6556                    self.emit_op(Op::ReverseListOp, line, Some(root));
6557                } else {
6558                    self.emit_op(Op::ReverseScalarOp, line, Some(root));
6559                }
6560            }
6561            ExprKind::System(args) => {
6562                for a in args {
6563                    self.compile_expr(a)?;
6564                }
6565                self.emit_op(
6566                    Op::CallBuiltin(BuiltinId::System as u16, args.len() as u8),
6567                    line,
6568                    Some(root),
6569                );
6570            }
6571            ExprKind::Exec(args) => {
6572                for a in args {
6573                    self.compile_expr(a)?;
6574                }
6575                self.emit_op(
6576                    Op::CallBuiltin(BuiltinId::Exec as u16, args.len() as u8),
6577                    line,
6578                    Some(root),
6579                );
6580            }
6581
6582            // ── String builtins ──
6583            ExprKind::Substr {
6584                string,
6585                offset,
6586                length,
6587                replacement,
6588            } => {
6589                if let Some(rep) = replacement {
6590                    let idx = self.chunk.add_substr_four_arg_entry(
6591                        string.as_ref().clone(),
6592                        offset.as_ref().clone(),
6593                        length.as_ref().map(|b| b.as_ref().clone()),
6594                        rep.as_ref().clone(),
6595                    );
6596                    self.emit_op(Op::SubstrFourArg(idx), line, Some(root));
6597                } else {
6598                    self.compile_expr(string)?;
6599                    self.compile_expr(offset)?;
6600                    let mut argc: u8 = 2;
6601                    if let Some(len) = length {
6602                        self.compile_expr(len)?;
6603                        argc = 3;
6604                    }
6605                    self.emit_op(
6606                        Op::CallBuiltin(BuiltinId::Substr as u16, argc),
6607                        line,
6608                        Some(root),
6609                    );
6610                }
6611            }
6612            ExprKind::Index {
6613                string,
6614                substr,
6615                position,
6616            } => {
6617                self.compile_expr(string)?;
6618                self.compile_expr(substr)?;
6619                if let Some(pos) = position {
6620                    self.compile_expr(pos)?;
6621                    self.emit_op(
6622                        Op::CallBuiltin(BuiltinId::Index as u16, 3),
6623                        line,
6624                        Some(root),
6625                    );
6626                } else {
6627                    self.emit_op(
6628                        Op::CallBuiltin(BuiltinId::Index as u16, 2),
6629                        line,
6630                        Some(root),
6631                    );
6632                }
6633            }
6634            ExprKind::Rindex {
6635                string,
6636                substr,
6637                position,
6638            } => {
6639                self.compile_expr(string)?;
6640                self.compile_expr(substr)?;
6641                if let Some(pos) = position {
6642                    self.compile_expr(pos)?;
6643                    self.emit_op(
6644                        Op::CallBuiltin(BuiltinId::Rindex as u16, 3),
6645                        line,
6646                        Some(root),
6647                    );
6648                } else {
6649                    self.emit_op(
6650                        Op::CallBuiltin(BuiltinId::Rindex as u16, 2),
6651                        line,
6652                        Some(root),
6653                    );
6654                }
6655            }
6656
6657            ExprKind::JoinExpr { separator, list } => {
6658                self.compile_expr(separator)?;
6659                // Arguments after the separator are evaluated in list context (Perl 5).
6660                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6661                self.emit_op(Op::CallBuiltin(BuiltinId::Join as u16, 2), line, Some(root));
6662            }
6663            ExprKind::SplitExpr {
6664                pattern,
6665                string,
6666                limit,
6667            } => {
6668                self.compile_expr(pattern)?;
6669                self.compile_expr(string)?;
6670                if let Some(l) = limit {
6671                    self.compile_expr(l)?;
6672                    self.emit_op(
6673                        Op::CallBuiltin(BuiltinId::Split as u16, 3),
6674                        line,
6675                        Some(root),
6676                    );
6677                } else {
6678                    self.emit_op(
6679                        Op::CallBuiltin(BuiltinId::Split as u16, 2),
6680                        line,
6681                        Some(root),
6682                    );
6683                }
6684            }
6685            ExprKind::Sprintf { format, args } => {
6686                // sprintf's arg list after the format is Perl list context — ranges, arrays,
6687                // and `reverse`/`sort`/`grep` flatten into the format argument positions.
6688                self.compile_expr(format)?;
6689                for a in args {
6690                    self.compile_expr_ctx(a, WantarrayCtx::List)?;
6691                }
6692                self.emit_op(
6693                    Op::CallBuiltin(BuiltinId::Sprintf as u16, (1 + args.len()) as u8),
6694                    line,
6695                    Some(root),
6696                );
6697            }
6698
6699            // ── I/O ──
6700            ExprKind::Open { handle, mode, file } => {
6701                if let ExprKind::OpenMyHandle { name } = &handle.kind {
6702                    let name_idx = self.chunk.intern_name(name);
6703                    self.emit_op(Op::LoadUndef, line, Some(root));
6704                    self.emit_declare_scalar(name_idx, line, false);
6705                    let h_idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
6706                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
6707                    self.compile_expr(mode)?;
6708                    if let Some(f) = file {
6709                        self.compile_expr(f)?;
6710                        self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
6711                    } else {
6712                        self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
6713                    }
6714                    self.emit_op(Op::SetScalarKeepPlain(name_idx), line, Some(root));
6715                    return Ok(());
6716                }
6717                self.compile_expr(handle)?;
6718                self.compile_expr(mode)?;
6719                if let Some(f) = file {
6720                    self.compile_expr(f)?;
6721                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
6722                } else {
6723                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
6724                }
6725            }
6726            ExprKind::OpenMyHandle { .. } => {
6727                return Err(CompileError::Unsupported(
6728                    "open my $fh handle expression".into(),
6729                ));
6730            }
6731            ExprKind::Close(e) => {
6732                self.compile_expr(e)?;
6733                self.emit_op(
6734                    Op::CallBuiltin(BuiltinId::Close as u16, 1),
6735                    line,
6736                    Some(root),
6737                );
6738            }
6739            ExprKind::ReadLine(handle) => {
6740                let bid = if ctx == WantarrayCtx::List {
6741                    BuiltinId::ReadLineList
6742                } else {
6743                    BuiltinId::ReadLine
6744                };
6745                if let Some(h) = handle {
6746                    let idx = self.chunk.add_constant(StrykeValue::string(h.clone()));
6747                    self.emit_op(Op::LoadConst(idx), line, Some(root));
6748                    self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
6749                } else {
6750                    self.emit_op(Op::CallBuiltin(bid as u16, 0), line, Some(root));
6751                }
6752            }
6753            ExprKind::Eof(e) => {
6754                if let Some(inner) = e {
6755                    self.compile_expr(inner)?;
6756                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 1), line, Some(root));
6757                } else {
6758                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 0), line, Some(root));
6759                }
6760            }
6761            ExprKind::Opendir { handle, path } => {
6762                self.compile_expr(handle)?;
6763                self.compile_expr(path)?;
6764                self.emit_op(
6765                    Op::CallBuiltin(BuiltinId::Opendir as u16, 2),
6766                    line,
6767                    Some(root),
6768                );
6769            }
6770            ExprKind::Readdir(e) => {
6771                let bid = if ctx == WantarrayCtx::List {
6772                    BuiltinId::ReaddirList
6773                } else {
6774                    BuiltinId::Readdir
6775                };
6776                self.compile_expr(e)?;
6777                self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
6778            }
6779            ExprKind::Closedir(e) => {
6780                self.compile_expr(e)?;
6781                self.emit_op(
6782                    Op::CallBuiltin(BuiltinId::Closedir as u16, 1),
6783                    line,
6784                    Some(root),
6785                );
6786            }
6787            ExprKind::Rewinddir(e) => {
6788                self.compile_expr(e)?;
6789                self.emit_op(
6790                    Op::CallBuiltin(BuiltinId::Rewinddir as u16, 1),
6791                    line,
6792                    Some(root),
6793                );
6794            }
6795            ExprKind::Telldir(e) => {
6796                self.compile_expr(e)?;
6797                self.emit_op(
6798                    Op::CallBuiltin(BuiltinId::Telldir as u16, 1),
6799                    line,
6800                    Some(root),
6801                );
6802            }
6803            ExprKind::Seekdir { handle, position } => {
6804                self.compile_expr(handle)?;
6805                self.compile_expr(position)?;
6806                self.emit_op(
6807                    Op::CallBuiltin(BuiltinId::Seekdir as u16, 2),
6808                    line,
6809                    Some(root),
6810                );
6811            }
6812
6813            // ── File tests ──
6814            ExprKind::FileTest { op, expr } => {
6815                self.compile_expr(expr)?;
6816                self.emit_op(Op::FileTestOp(*op as u8), line, Some(root));
6817            }
6818
6819            // ── Eval / Do / Require ──
6820            ExprKind::Eval(e) => {
6821                self.compile_expr(e)?;
6822                // `eval { BLOCK }` runs synchronously and is intentionally
6823                // shared-state with the enclosing scope (it's used for error
6824                // catching, not stored as a closure value). Un-mark the
6825                // CodeRef's block so the closure-write check (DESIGN-001)
6826                // doesn't fire on writes to outer-scope `my` from inside
6827                // the eval body.
6828                if let ExprKind::CodeRef { .. } = &e.kind {
6829                    if let Some(max_idx) = self.sub_body_block_indices.iter().copied().max() {
6830                        self.sub_body_block_indices.remove(&max_idx);
6831                    }
6832                }
6833                self.emit_op(Op::CallBuiltin(BuiltinId::Eval as u16, 1), line, Some(root));
6834            }
6835            ExprKind::Do(e) => {
6836                // do { BLOCK } executes the block; do "file" loads a file
6837                if let ExprKind::CodeRef { body, .. } = &e.kind {
6838                    let block_idx = self.add_deferred_block(body.clone());
6839                    self.emit_op(Op::EvalBlock(block_idx, ctx.as_byte()), line, Some(root));
6840                } else {
6841                    self.compile_expr(e)?;
6842                    self.emit_op(Op::CallBuiltin(BuiltinId::Do as u16, 1), line, Some(root));
6843                }
6844            }
6845            ExprKind::Require(e) => {
6846                self.compile_expr(e)?;
6847                self.emit_op(
6848                    Op::CallBuiltin(BuiltinId::Require as u16, 1),
6849                    line,
6850                    Some(root),
6851                );
6852            }
6853
6854            // ── Filesystem ──
6855            ExprKind::Chdir(e) => {
6856                self.compile_expr(e)?;
6857                self.emit_op(
6858                    Op::CallBuiltin(BuiltinId::Chdir as u16, 1),
6859                    line,
6860                    Some(root),
6861                );
6862            }
6863            ExprKind::Mkdir { path, mode } => {
6864                self.compile_expr(path)?;
6865                if let Some(m) = mode {
6866                    self.compile_expr(m)?;
6867                    self.emit_op(
6868                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 2),
6869                        line,
6870                        Some(root),
6871                    );
6872                } else {
6873                    self.emit_op(
6874                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 1),
6875                        line,
6876                        Some(root),
6877                    );
6878                }
6879            }
6880            ExprKind::Unlink(args) => {
6881                for a in args {
6882                    self.compile_expr(a)?;
6883                }
6884                self.emit_op(
6885                    Op::CallBuiltin(BuiltinId::Unlink as u16, args.len() as u8),
6886                    line,
6887                    Some(root),
6888                );
6889            }
6890            ExprKind::Rename { old, new } => {
6891                self.compile_expr(old)?;
6892                self.compile_expr(new)?;
6893                self.emit_op(
6894                    Op::CallBuiltin(BuiltinId::Rename as u16, 2),
6895                    line,
6896                    Some(root),
6897                );
6898            }
6899            ExprKind::Chmod(args) => {
6900                for a in args {
6901                    self.compile_expr(a)?;
6902                }
6903                self.emit_op(
6904                    Op::CallBuiltin(BuiltinId::Chmod as u16, args.len() as u8),
6905                    line,
6906                    Some(root),
6907                );
6908            }
6909            ExprKind::Chown(args) => {
6910                for a in args {
6911                    self.compile_expr(a)?;
6912                }
6913                self.emit_op(
6914                    Op::CallBuiltin(BuiltinId::Chown as u16, args.len() as u8),
6915                    line,
6916                    Some(root),
6917                );
6918            }
6919            ExprKind::Stat(e) => {
6920                self.compile_expr(e)?;
6921                self.emit_op(Op::CallBuiltin(BuiltinId::Stat as u16, 1), line, Some(root));
6922            }
6923            ExprKind::Lstat(e) => {
6924                self.compile_expr(e)?;
6925                self.emit_op(
6926                    Op::CallBuiltin(BuiltinId::Lstat as u16, 1),
6927                    line,
6928                    Some(root),
6929                );
6930            }
6931            ExprKind::Link { old, new } => {
6932                self.compile_expr(old)?;
6933                self.compile_expr(new)?;
6934                self.emit_op(Op::CallBuiltin(BuiltinId::Link as u16, 2), line, Some(root));
6935            }
6936            ExprKind::Symlink { old, new } => {
6937                self.compile_expr(old)?;
6938                self.compile_expr(new)?;
6939                self.emit_op(
6940                    Op::CallBuiltin(BuiltinId::Symlink as u16, 2),
6941                    line,
6942                    Some(root),
6943                );
6944            }
6945            ExprKind::Readlink(e) => {
6946                self.compile_expr(e)?;
6947                self.emit_op(
6948                    Op::CallBuiltin(BuiltinId::Readlink as u16, 1),
6949                    line,
6950                    Some(root),
6951                );
6952            }
6953            ExprKind::Files(args) => {
6954                for a in args {
6955                    self.compile_expr(a)?;
6956                }
6957                self.emit_op(
6958                    Op::CallBuiltin(BuiltinId::Files as u16, args.len() as u8),
6959                    line,
6960                    Some(root),
6961                );
6962            }
6963            ExprKind::Filesf(args) => {
6964                for a in args {
6965                    self.compile_expr(a)?;
6966                }
6967                self.emit_op(
6968                    Op::CallBuiltin(BuiltinId::Filesf as u16, args.len() as u8),
6969                    line,
6970                    Some(root),
6971                );
6972            }
6973            ExprKind::FilesfRecursive(args) => {
6974                for a in args {
6975                    self.compile_expr(a)?;
6976                }
6977                self.emit_op(
6978                    Op::CallBuiltin(BuiltinId::FilesfRecursive as u16, args.len() as u8),
6979                    line,
6980                    Some(root),
6981                );
6982            }
6983            ExprKind::Dirs(args) => {
6984                for a in args {
6985                    self.compile_expr(a)?;
6986                }
6987                self.emit_op(
6988                    Op::CallBuiltin(BuiltinId::Dirs as u16, args.len() as u8),
6989                    line,
6990                    Some(root),
6991                );
6992            }
6993            ExprKind::DirsRecursive(args) => {
6994                for a in args {
6995                    self.compile_expr(a)?;
6996                }
6997                self.emit_op(
6998                    Op::CallBuiltin(BuiltinId::DirsRecursive as u16, args.len() as u8),
6999                    line,
7000                    Some(root),
7001                );
7002            }
7003            ExprKind::SymLinks(args) => {
7004                for a in args {
7005                    self.compile_expr(a)?;
7006                }
7007                self.emit_op(
7008                    Op::CallBuiltin(BuiltinId::SymLinks as u16, args.len() as u8),
7009                    line,
7010                    Some(root),
7011                );
7012            }
7013            ExprKind::Sockets(args) => {
7014                for a in args {
7015                    self.compile_expr(a)?;
7016                }
7017                self.emit_op(
7018                    Op::CallBuiltin(BuiltinId::Sockets as u16, args.len() as u8),
7019                    line,
7020                    Some(root),
7021                );
7022            }
7023            ExprKind::Pipes(args) => {
7024                for a in args {
7025                    self.compile_expr(a)?;
7026                }
7027                self.emit_op(
7028                    Op::CallBuiltin(BuiltinId::Pipes as u16, args.len() as u8),
7029                    line,
7030                    Some(root),
7031                );
7032            }
7033            ExprKind::BlockDevices(args) => {
7034                for a in args {
7035                    self.compile_expr(a)?;
7036                }
7037                self.emit_op(
7038                    Op::CallBuiltin(BuiltinId::BlockDevices as u16, args.len() as u8),
7039                    line,
7040                    Some(root),
7041                );
7042            }
7043            ExprKind::CharDevices(args) => {
7044                for a in args {
7045                    self.compile_expr(a)?;
7046                }
7047                self.emit_op(
7048                    Op::CallBuiltin(BuiltinId::CharDevices as u16, args.len() as u8),
7049                    line,
7050                    Some(root),
7051                );
7052            }
7053            ExprKind::Executables(args) => {
7054                for a in args {
7055                    self.compile_expr(a)?;
7056                }
7057                self.emit_op(
7058                    Op::CallBuiltin(BuiltinId::Executables as u16, args.len() as u8),
7059                    line,
7060                    Some(root),
7061                );
7062            }
7063            ExprKind::Glob(args) => {
7064                for a in args {
7065                    self.compile_expr(a)?;
7066                }
7067                self.emit_op(
7068                    Op::CallBuiltin(BuiltinId::Glob as u16, args.len() as u8),
7069                    line,
7070                    Some(root),
7071                );
7072            }
7073            ExprKind::GlobPar { args, progress } => {
7074                for a in args {
7075                    self.compile_expr(a)?;
7076                }
7077                match progress {
7078                    None => {
7079                        self.emit_op(
7080                            Op::CallBuiltin(BuiltinId::GlobPar as u16, args.len() as u8),
7081                            line,
7082                            Some(root),
7083                        );
7084                    }
7085                    Some(p) => {
7086                        self.compile_expr(p)?;
7087                        self.emit_op(
7088                            Op::CallBuiltin(
7089                                BuiltinId::GlobParProgress as u16,
7090                                (args.len() + 1) as u8,
7091                            ),
7092                            line,
7093                            Some(root),
7094                        );
7095                    }
7096                }
7097            }
7098            ExprKind::ParSed { args, progress } => {
7099                for a in args {
7100                    self.compile_expr(a)?;
7101                }
7102                match progress {
7103                    None => {
7104                        self.emit_op(
7105                            Op::CallBuiltin(BuiltinId::ParSed as u16, args.len() as u8),
7106                            line,
7107                            Some(root),
7108                        );
7109                    }
7110                    Some(p) => {
7111                        self.compile_expr(p)?;
7112                        self.emit_op(
7113                            Op::CallBuiltin(
7114                                BuiltinId::ParSedProgress as u16,
7115                                (args.len() + 1) as u8,
7116                            ),
7117                            line,
7118                            Some(root),
7119                        );
7120                    }
7121                }
7122            }
7123
7124            // ── OOP ──
7125            ExprKind::Bless { ref_expr, class } => {
7126                self.compile_expr(ref_expr)?;
7127                if let Some(c) = class {
7128                    self.compile_expr(c)?;
7129                    self.emit_op(
7130                        Op::CallBuiltin(BuiltinId::Bless as u16, 2),
7131                        line,
7132                        Some(root),
7133                    );
7134                } else {
7135                    self.emit_op(
7136                        Op::CallBuiltin(BuiltinId::Bless as u16, 1),
7137                        line,
7138                        Some(root),
7139                    );
7140                }
7141            }
7142            ExprKind::Caller(e) => {
7143                if let Some(inner) = e {
7144                    self.compile_expr(inner)?;
7145                    self.emit_op(
7146                        Op::CallBuiltin(BuiltinId::Caller as u16, 1),
7147                        line,
7148                        Some(root),
7149                    );
7150                } else {
7151                    self.emit_op(
7152                        Op::CallBuiltin(BuiltinId::Caller as u16, 0),
7153                        line,
7154                        Some(root),
7155                    );
7156                }
7157            }
7158            ExprKind::Wantarray => {
7159                self.emit_op(
7160                    Op::CallBuiltin(BuiltinId::Wantarray as u16, 0),
7161                    line,
7162                    Some(root),
7163                );
7164            }
7165
7166            // ── References ──
7167            ExprKind::ScalarRef(e) => match &e.kind {
7168                ExprKind::ScalarVar(name) => {
7169                    let idx = self.intern_scalar_var_for_ops(name);
7170                    self.emit_op(Op::MakeScalarBindingRef(idx), line, Some(root));
7171                }
7172                ExprKind::ArrayVar(name) => {
7173                    self.check_strict_array_access(name, line)?;
7174                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
7175                    self.emit_op(Op::MakeArrayBindingRef(idx), line, Some(root));
7176                }
7177                ExprKind::HashVar(name) => {
7178                    self.check_strict_hash_access(name, line)?;
7179                    let idx = self.chunk.intern_name(name);
7180                    self.emit_op(Op::MakeHashBindingRef(idx), line, Some(root));
7181                }
7182                ExprKind::Deref {
7183                    expr: inner,
7184                    kind: Sigil::Array,
7185                } => {
7186                    self.compile_expr(inner)?;
7187                    self.emit_op(Op::MakeArrayRefAlias, line, Some(root));
7188                }
7189                ExprKind::Deref {
7190                    expr: inner,
7191                    kind: Sigil::Hash,
7192                } => {
7193                    self.compile_expr(inner)?;
7194                    self.emit_op(Op::MakeHashRefAlias, line, Some(root));
7195                }
7196                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
7197                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7198                    self.emit_op(Op::MakeArrayRef, line, Some(root));
7199                }
7200                ExprKind::AnonymousListSlice { .. } | ExprKind::HashSliceDeref { .. } => {
7201                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7202                    self.emit_op(Op::MakeArrayRef, line, Some(root));
7203                }
7204                _ => {
7205                    self.compile_expr(e)?;
7206                    self.emit_op(Op::MakeScalarRef, line, Some(root));
7207                }
7208            },
7209            ExprKind::ArrayRef(elems) => {
7210                // `[ LIST ]` — each element is in list context so `1..5`, `reverse`, `grep`
7211                // and array variables flatten through [`Op::MakeArray`], which already splats
7212                // nested arrays.
7213                for e in elems {
7214                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7215                }
7216                self.emit_op(Op::MakeArray(elems.len() as u16), line, Some(root));
7217                self.emit_op(Op::MakeArrayRef, line, Some(root));
7218            }
7219            ExprKind::HashRef(pairs) => {
7220                // `{ K => V, ... }` — keys are scalar, values are list context so ranges and
7221                // slurpy constructs on the value side flatten into the built hash.
7222                // Special case: a single pair with the `__HASH_SPREAD__` sentinel key is
7223                // emitted by `parse_forced_hashref_body` (`+{ EXPR }`) and by the `{ %h }`
7224                // hash-spread short-form. Compile the value in list context and let
7225                // [`Op::MakeHashRef`] pair up the resulting list at runtime — the slow
7226                // path's [`Interpreter::eval_expr`] HashRef arm handles this too via the
7227                // same sentinel string.
7228                if pairs.len() == 1 {
7229                    if let ExprKind::String(ref k) = pairs[0].0.kind {
7230                        if k == "__HASH_SPREAD__" {
7231                            self.compile_expr_ctx(&pairs[0].1, WantarrayCtx::List)?;
7232                            self.emit_op(Op::MakeHashRef, line, Some(root));
7233                            return Ok(());
7234                        }
7235                    }
7236                }
7237                for (k, v) in pairs {
7238                    self.compile_expr(k)?;
7239                    self.compile_expr_ctx(v, WantarrayCtx::List)?;
7240                }
7241                self.emit_op(Op::MakeHash((pairs.len() * 2) as u16), line, Some(root));
7242                self.emit_op(Op::MakeHashRef, line, Some(root));
7243            }
7244            ExprKind::CodeRef { body, params } => {
7245                let block_idx = self.add_deferred_block(body.clone());
7246                self.sub_body_block_indices.insert(block_idx);
7247                // Stash params alongside the block index so the 4th-pass
7248                // compile can register them in the new sub-body layer (so
7249                // the closure-write check sees them as locally declared).
7250                while self.code_ref_block_params.len() <= block_idx as usize {
7251                    self.code_ref_block_params.push(Vec::new());
7252                }
7253                self.code_ref_block_params[block_idx as usize] = params.clone();
7254                let sig_idx = self.chunk.add_code_ref_sig(params.clone());
7255                self.emit_op(Op::MakeCodeRef(block_idx, sig_idx), line, Some(root));
7256            }
7257            ExprKind::SubroutineRef(name) => {
7258                // Unary `&name` — invoke subroutine with no explicit args (same as tree `call_named_sub`).
7259                let q = self.qualify_sub_key(name);
7260                let name_idx = self.chunk.intern_name(&q);
7261                self.emit_op(Op::Call(name_idx, 0, ctx.as_byte()), line, Some(root));
7262            }
7263            ExprKind::SubroutineCodeRef(name) => {
7264                // `\&name` — coderef (must exist at run time).
7265                let name_idx = self.chunk.intern_name(name);
7266                self.emit_op(Op::LoadNamedSubRef(name_idx), line, Some(root));
7267            }
7268            ExprKind::DynamicSubCodeRef(expr) => {
7269                self.compile_expr(expr)?;
7270                self.emit_op(Op::LoadDynamicSubRef, line, Some(root));
7271            }
7272
7273            // ── Derefs ──
7274            ExprKind::ArrowDeref { expr, index, kind } => match kind {
7275                DerefKind::Array => {
7276                    self.compile_arrow_array_base_expr(expr)?;
7277                    let mut used_arrow_slice = false;
7278                    // `$r->[$i]` with a single plain-scalar subscript is element
7279                    // access, not a slice — even when the parser wraps it in a
7280                    // 1-element `List`. Returning a 1-element list here breaks
7281                    // arithmetic (`$a + $b` numifies via length to `1+1=2`).
7282                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
7283                        let inner = match &index.kind {
7284                            ExprKind::List(el) if el.len() == 1 => &el[0],
7285                            _ => index.as_ref(),
7286                        };
7287                        self.compile_expr(inner)?;
7288                        self.emit_op(Op::ArrowArray, line, Some(root));
7289                    } else if let ExprKind::List(indices) = &index.kind {
7290                        for ix in indices {
7291                            self.compile_array_slice_index_expr(ix)?;
7292                        }
7293                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, Some(root));
7294                        used_arrow_slice = true;
7295                    } else {
7296                        // One subscript expr may expand to multiple indices (`$r->[0..1]`, `[(0,1)]`).
7297                        self.compile_array_slice_index_expr(index)?;
7298                        self.emit_op(Op::ArrowArraySlice(1), line, Some(root));
7299                        used_arrow_slice = true;
7300                    }
7301                    if used_arrow_slice && ctx != WantarrayCtx::List {
7302                        self.emit_op(Op::ListSliceToScalar, line, Some(root));
7303                    }
7304                }
7305                DerefKind::Hash => {
7306                    self.compile_arrow_hash_base_expr(expr)?;
7307                    self.compile_expr(index)?;
7308                    self.emit_op(Op::ArrowHash, line, Some(root));
7309                }
7310                DerefKind::Call => {
7311                    self.compile_expr(expr)?;
7312                    // Always compile args in list context to preserve all arguments
7313                    self.compile_expr_ctx(index, WantarrayCtx::List)?;
7314                    self.emit_op(Op::ArrowCall(ctx.as_byte()), line, Some(root));
7315                }
7316            },
7317            ExprKind::Deref { expr, kind } => {
7318                // Perl: `scalar @{EXPR}` / `scalar @$r` is the array length (not a copy of the list).
7319                // `scalar %{EXPR}` uses hash fill metrics like `%h` in scalar context.
7320                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
7321                    self.compile_expr(expr)?;
7322                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
7323                } else if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
7324                    self.compile_expr(expr)?;
7325                    self.emit_op(Op::SymbolicDeref(2), line, Some(root));
7326                    self.emit_op(Op::ValueScalarContext, line, Some(root));
7327                } else {
7328                    self.compile_expr(expr)?;
7329                    let b = match kind {
7330                        Sigil::Scalar => 0u8,
7331                        Sigil::Array => 1,
7332                        Sigil::Hash => 2,
7333                        Sigil::Typeglob => 3,
7334                    };
7335                    self.emit_op(Op::SymbolicDeref(b), line, Some(root));
7336                }
7337            }
7338
7339            // ── Interpolated strings ──
7340            ExprKind::InterpolatedString(parts) => {
7341                // Check if any literal part contains case-escape sequences.
7342                let has_case_escapes = parts.iter().any(|p| {
7343                    if let StringPart::Literal(s) = p {
7344                        s.contains('\\')
7345                            && (s.contains("\\U")
7346                                || s.contains("\\L")
7347                                || s.contains("\\u")
7348                                || s.contains("\\l")
7349                                || s.contains("\\Q")
7350                                || s.contains("\\E"))
7351                    } else {
7352                        false
7353                    }
7354                });
7355                if parts.is_empty() {
7356                    let idx = self.chunk.add_constant(StrykeValue::string(String::new()));
7357                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7358                } else {
7359                    // `"$x"` is a single [`StringPart`] — still string context; must go through
7360                    // [`Op::Concat`] so operands are stringified (`use overload '""'`, etc.).
7361                    if !matches!(&parts[0], StringPart::Literal(_)) {
7362                        let idx = self.chunk.add_constant(StrykeValue::string(String::new()));
7363                        self.emit_op(Op::LoadConst(idx), line, Some(root));
7364                    }
7365                    self.compile_string_part(&parts[0], line, Some(root))?;
7366                    for part in &parts[1..] {
7367                        self.compile_string_part(part, line, Some(root))?;
7368                        self.emit_op(Op::Concat, line, Some(root));
7369                    }
7370                    if !matches!(&parts[0], StringPart::Literal(_)) {
7371                        self.emit_op(Op::Concat, line, Some(root));
7372                    }
7373                }
7374                if has_case_escapes {
7375                    self.emit_op(Op::ProcessCaseEscapes, line, Some(root));
7376                }
7377            }
7378
7379            // ── List ──
7380            ExprKind::List(exprs) => {
7381                if ctx == WantarrayCtx::Scalar {
7382                    // Perl: comma-list in scalar context evaluates to the **last** element (`(1,2)` → 2).
7383                    if let Some(last) = exprs.last() {
7384                        self.compile_expr_ctx(last, WantarrayCtx::Scalar)?;
7385                    } else {
7386                        self.emit_op(Op::LoadUndef, line, Some(root));
7387                    }
7388                } else {
7389                    for e in exprs {
7390                        self.compile_expr_ctx(e, ctx)?;
7391                    }
7392                    if exprs.len() != 1 {
7393                        self.emit_op(Op::MakeArray(exprs.len() as u16), line, Some(root));
7394                    }
7395                }
7396            }
7397
7398            // ── QW ──
7399            ExprKind::QW(words) => {
7400                for w in words {
7401                    let idx = self.chunk.add_constant(StrykeValue::string(w.clone()));
7402                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7403                }
7404                self.emit_op(Op::MakeArray(words.len() as u16), line, Some(root));
7405            }
7406
7407            // ── Postfix if/unless ──
7408            ExprKind::PostfixIf { expr, condition } => {
7409                self.compile_boolean_rvalue_condition(condition)?;
7410                let j = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7411                self.compile_expr(expr)?;
7412                let end = self.emit_op(Op::Jump(0), line, Some(root));
7413                self.chunk.patch_jump_here(j);
7414                self.emit_op(Op::LoadUndef, line, Some(root));
7415                self.chunk.patch_jump_here(end);
7416            }
7417            ExprKind::PostfixUnless { expr, condition } => {
7418                self.compile_boolean_rvalue_condition(condition)?;
7419                let j = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
7420                self.compile_expr(expr)?;
7421                let end = self.emit_op(Op::Jump(0), line, Some(root));
7422                self.chunk.patch_jump_here(j);
7423                self.emit_op(Op::LoadUndef, line, Some(root));
7424                self.chunk.patch_jump_here(end);
7425            }
7426
7427            // ── Postfix while/until/foreach ──
7428            ExprKind::PostfixWhile { expr, condition } => {
7429                // Detect `do { BLOCK } while (COND)` pattern
7430                let is_do_block = matches!(
7431                    &expr.kind,
7432                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
7433                );
7434                if is_do_block {
7435                    // do-while: body executes before first condition check
7436                    let loop_start = self.chunk.len();
7437                    self.compile_expr(expr)?;
7438                    self.emit_op(Op::Pop, line, Some(root));
7439                    self.compile_boolean_rvalue_condition(condition)?;
7440                    self.emit_op(Op::JumpIfTrue(loop_start), line, Some(root));
7441                    self.emit_op(Op::LoadUndef, line, Some(root));
7442                } else {
7443                    // Regular postfix while: condition checked first
7444                    let loop_start = self.chunk.len();
7445                    self.compile_boolean_rvalue_condition(condition)?;
7446                    let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7447                    self.compile_expr(expr)?;
7448                    self.emit_op(Op::Pop, line, Some(root));
7449                    self.emit_op(Op::Jump(loop_start), line, Some(root));
7450                    self.chunk.patch_jump_here(exit_jump);
7451                    self.emit_op(Op::LoadUndef, line, Some(root));
7452                }
7453            }
7454            ExprKind::PostfixUntil { expr, condition } => {
7455                let is_do_block = matches!(
7456                    &expr.kind,
7457                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
7458                );
7459                if is_do_block {
7460                    let loop_start = self.chunk.len();
7461                    self.compile_expr(expr)?;
7462                    self.emit_op(Op::Pop, line, Some(root));
7463                    self.compile_boolean_rvalue_condition(condition)?;
7464                    self.emit_op(Op::JumpIfFalse(loop_start), line, Some(root));
7465                    self.emit_op(Op::LoadUndef, line, Some(root));
7466                } else {
7467                    let loop_start = self.chunk.len();
7468                    self.compile_boolean_rvalue_condition(condition)?;
7469                    let exit_jump = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
7470                    self.compile_expr(expr)?;
7471                    self.emit_op(Op::Pop, line, Some(root));
7472                    self.emit_op(Op::Jump(loop_start), line, Some(root));
7473                    self.chunk.patch_jump_here(exit_jump);
7474                    self.emit_op(Op::LoadUndef, line, Some(root));
7475                }
7476            }
7477            ExprKind::PostfixForeach { expr, list } => {
7478                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7479                let list_name = self.chunk.intern_name("__pf_foreach_list__");
7480                self.emit_op(Op::DeclareArray(list_name), line, Some(root));
7481                let counter = self.chunk.intern_name("__pf_foreach_i__");
7482                self.emit_op(Op::LoadInt(0), line, Some(root));
7483                self.emit_op(Op::DeclareScalar(counter), line, Some(root));
7484                let underscore = self.chunk.intern_name("_");
7485
7486                let loop_start = self.chunk.len();
7487                self.emit_get_scalar(counter, line, Some(root));
7488                self.emit_op(Op::ArrayLen(list_name), line, Some(root));
7489                self.emit_op(Op::NumLt, line, Some(root));
7490                let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7491
7492                self.emit_get_scalar(counter, line, Some(root));
7493                self.emit_op(Op::GetArrayElem(list_name), line, Some(root));
7494                self.emit_set_scalar(underscore, line, Some(root));
7495
7496                self.compile_expr(expr)?;
7497                self.emit_op(Op::Pop, line, Some(root));
7498
7499                self.emit_pre_inc(counter, line, Some(root));
7500                self.emit_op(Op::Pop, line, Some(root));
7501                self.emit_op(Op::Jump(loop_start), line, Some(root));
7502                self.chunk.patch_jump_here(exit_jump);
7503                self.emit_op(Op::LoadUndef, line, Some(root));
7504            }
7505
7506            ExprKind::AlgebraicMatch { subject, arms } => {
7507                let idx = self
7508                    .chunk
7509                    .add_algebraic_match_entry(subject.as_ref().clone(), arms.clone());
7510                self.emit_op(Op::AlgebraicMatch(idx), line, Some(root));
7511            }
7512
7513            // ── Match (regex) ──
7514            ExprKind::Match {
7515                expr,
7516                pattern,
7517                flags,
7518                scalar_g,
7519                delim: _,
7520            } => {
7521                self.compile_expr(expr)?;
7522                let pat_idx = self
7523                    .chunk
7524                    .add_constant(StrykeValue::string(pattern.clone()));
7525                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7526                let pos_key_idx = if *scalar_g && flags.contains('g') {
7527                    if let ExprKind::ScalarVar(n) = &expr.kind {
7528                        let stor = self.scalar_storage_name_for_ops(n);
7529                        self.chunk.add_constant(StrykeValue::string(stor))
7530                    } else {
7531                        u16::MAX
7532                    }
7533                } else {
7534                    u16::MAX
7535                };
7536                self.emit_op(
7537                    Op::RegexMatch(pat_idx, flags_idx, *scalar_g, pos_key_idx),
7538                    line,
7539                    Some(root),
7540                );
7541            }
7542
7543            ExprKind::Substitution {
7544                expr,
7545                pattern,
7546                replacement,
7547                flags,
7548                delim: _,
7549            } => {
7550                self.compile_expr(expr)?;
7551                let pat_idx = self
7552                    .chunk
7553                    .add_constant(StrykeValue::string(pattern.clone()));
7554                let repl_idx = self
7555                    .chunk
7556                    .add_constant(StrykeValue::string(replacement.clone()));
7557                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7558                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
7559                self.emit_op(
7560                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lv_idx),
7561                    line,
7562                    Some(root),
7563                );
7564            }
7565            ExprKind::Transliterate {
7566                expr,
7567                from,
7568                to,
7569                flags,
7570                delim: _,
7571            } => {
7572                self.compile_expr(expr)?;
7573                let from_idx = self.chunk.add_constant(StrykeValue::string(from.clone()));
7574                let to_idx = self.chunk.add_constant(StrykeValue::string(to.clone()));
7575                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7576                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
7577                self.emit_op(
7578                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lv_idx),
7579                    line,
7580                    Some(root),
7581                );
7582            }
7583
7584            // ── Regex literal ──
7585            ExprKind::Regex(pattern, flags) => {
7586                if ctx == WantarrayCtx::Void {
7587                    // Statement context: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a discarded regex object.
7588                    self.compile_boolean_rvalue_condition(root)?;
7589                } else {
7590                    let pat_idx = self
7591                        .chunk
7592                        .add_constant(StrykeValue::string(pattern.clone()));
7593                    let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7594                    self.emit_op(Op::LoadRegex(pat_idx, flags_idx), line, Some(root));
7595                }
7596            }
7597
7598            // ── Map/Grep/Sort with blocks ──
7599            ExprKind::MapExpr {
7600                block,
7601                list,
7602                flatten_array_refs,
7603                stream,
7604            } => {
7605                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7606                if *stream {
7607                    let block_idx = self.add_deferred_block(block.clone());
7608                    if *flatten_array_refs {
7609                        self.emit_op(Op::MapsFlatMapWithBlock(block_idx), line, Some(root));
7610                    } else {
7611                        self.emit_op(Op::MapsWithBlock(block_idx), line, Some(root));
7612                    }
7613                } else if let Some(k) = crate::map_grep_fast::detect_map_int_mul(block) {
7614                    self.emit_op(Op::MapIntMul(k), line, Some(root));
7615                } else {
7616                    let block_idx = self.add_deferred_block(block.clone());
7617                    if *flatten_array_refs {
7618                        self.emit_op(Op::FlatMapWithBlock(block_idx), line, Some(root));
7619                    } else {
7620                        self.emit_op(Op::MapWithBlock(block_idx), line, Some(root));
7621                    }
7622                }
7623                if ctx != WantarrayCtx::List {
7624                    self.emit_op(Op::StackArrayLen, line, Some(root));
7625                }
7626            }
7627            ExprKind::MapExprComma {
7628                expr,
7629                list,
7630                flatten_array_refs,
7631                stream,
7632            } => {
7633                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7634                let idx = self.chunk.add_map_expr_entry(*expr.clone());
7635                if *stream {
7636                    if *flatten_array_refs {
7637                        self.emit_op(Op::MapsFlatMapWithExpr(idx), line, Some(root));
7638                    } else {
7639                        self.emit_op(Op::MapsWithExpr(idx), line, Some(root));
7640                    }
7641                } else if *flatten_array_refs {
7642                    self.emit_op(Op::FlatMapWithExpr(idx), line, Some(root));
7643                } else {
7644                    self.emit_op(Op::MapWithExpr(idx), line, Some(root));
7645                }
7646                if ctx != WantarrayCtx::List {
7647                    self.emit_op(Op::StackArrayLen, line, Some(root));
7648                }
7649            }
7650            ExprKind::ForEachExpr { block, list } => {
7651                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7652                let block_idx = self.add_deferred_block(block.clone());
7653                self.emit_op(Op::ForEachWithBlock(block_idx), line, Some(root));
7654            }
7655            ExprKind::GrepExpr {
7656                block,
7657                list,
7658                keyword,
7659            } => {
7660                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7661                if keyword.is_stream() {
7662                    let block_idx = self.add_deferred_block(block.clone());
7663                    self.emit_op(Op::FilterWithBlock(block_idx), line, Some(root));
7664                } else if let Some((m, r)) = crate::map_grep_fast::detect_grep_int_mod_eq(block) {
7665                    self.emit_op(Op::GrepIntModEq(m, r), line, Some(root));
7666                } else {
7667                    let block_idx = self.add_deferred_block(block.clone());
7668                    self.emit_op(Op::GrepWithBlock(block_idx), line, Some(root));
7669                }
7670                if ctx != WantarrayCtx::List {
7671                    self.emit_op(Op::StackArrayLen, line, Some(root));
7672                }
7673            }
7674            ExprKind::GrepExprComma {
7675                expr,
7676                list,
7677                keyword,
7678            } => {
7679                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7680                let idx = self.chunk.add_grep_expr_entry(*expr.clone());
7681                if keyword.is_stream() {
7682                    self.emit_op(Op::FilterWithExpr(idx), line, Some(root));
7683                } else {
7684                    self.emit_op(Op::GrepWithExpr(idx), line, Some(root));
7685                }
7686                if ctx != WantarrayCtx::List {
7687                    self.emit_op(Op::StackArrayLen, line, Some(root));
7688                }
7689            }
7690            ExprKind::SortExpr { cmp, list } => {
7691                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7692                match cmp {
7693                    Some(crate::ast::SortComparator::Block(block)) => {
7694                        if let Some(mode) = detect_sort_block_fast(block) {
7695                            let tag = match mode {
7696                                crate::sort_fast::SortBlockFast::Numeric => 0u8,
7697                                crate::sort_fast::SortBlockFast::String => 1u8,
7698                                crate::sort_fast::SortBlockFast::NumericRev => 2u8,
7699                                crate::sort_fast::SortBlockFast::StringRev => 3u8,
7700                            };
7701                            self.emit_op(Op::SortWithBlockFast(tag), line, Some(root));
7702                        } else {
7703                            let block_idx = self.add_deferred_block(block.clone());
7704                            self.register_sort_pair_block(block_idx);
7705                            self.emit_op(Op::SortWithBlock(block_idx), line, Some(root));
7706                        }
7707                    }
7708                    Some(crate::ast::SortComparator::Code(code_expr)) => {
7709                        self.compile_expr(code_expr)?;
7710                        self.emit_op(Op::SortWithCodeComparator(ctx.as_byte()), line, Some(root));
7711                    }
7712                    None => {
7713                        self.emit_op(Op::SortNoBlock, line, Some(root));
7714                    }
7715                }
7716            }
7717
7718            // ── Parallel extensions ──
7719            ExprKind::ParExpr { .. }
7720            | ExprKind::ParReduceExpr { .. }
7721            | ExprKind::DistReduceExpr { .. } => {
7722                // `par { BLOCK }`, `par_reduce { extract } [{ merge }]`, and
7723                // `~d>` (distributed thread macro) — bytecode VM has no
7724                // dedicated opcodes; defer AST eval to the interpreter via
7725                // `Op::EvalAstExpr`.
7726                let idx = self.chunk.ast_eval_exprs.len() as u16;
7727                self.chunk.ast_eval_exprs.push(root.clone());
7728                self.emit_op(Op::EvalAstExpr(idx), line, Some(root));
7729            }
7730            ExprKind::PMapExpr {
7731                block,
7732                list,
7733                progress,
7734                flat_outputs,
7735                on_cluster,
7736                stream,
7737            } => {
7738                if *stream {
7739                    // Streaming: no progress flag needed, just list + block.
7740                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7741                    let block_idx = self.add_deferred_block(block.clone());
7742                    if *flat_outputs {
7743                        self.emit_op(Op::PFlatMapsWithBlock(block_idx), line, Some(root));
7744                    } else {
7745                        self.emit_op(Op::PMapsWithBlock(block_idx), line, Some(root));
7746                    }
7747                } else {
7748                    if let Some(p) = progress {
7749                        self.compile_expr(p)?;
7750                    } else {
7751                        self.emit_op(Op::LoadInt(0), line, Some(root));
7752                    }
7753                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7754                    if let Some(cluster_e) = on_cluster {
7755                        self.compile_expr(cluster_e)?;
7756                        let block_idx = self.add_deferred_block(block.clone());
7757                        self.emit_op(
7758                            Op::PMapRemote {
7759                                block_idx,
7760                                flat: u8::from(*flat_outputs),
7761                            },
7762                            line,
7763                            Some(root),
7764                        );
7765                    } else {
7766                        let block_idx = self.add_deferred_block(block.clone());
7767                        if *flat_outputs {
7768                            self.emit_op(Op::PFlatMapWithBlock(block_idx), line, Some(root));
7769                        } else {
7770                            self.emit_op(Op::PMapWithBlock(block_idx), line, Some(root));
7771                        }
7772                    }
7773                }
7774            }
7775            ExprKind::PMapChunkedExpr {
7776                chunk_size,
7777                block,
7778                list,
7779                progress,
7780            } => {
7781                if let Some(p) = progress {
7782                    self.compile_expr(p)?;
7783                } else {
7784                    self.emit_op(Op::LoadInt(0), line, Some(root));
7785                }
7786                self.compile_expr(chunk_size)?;
7787                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7788                let block_idx = self.add_deferred_block(block.clone());
7789                self.emit_op(Op::PMapChunkedWithBlock(block_idx), line, Some(root));
7790            }
7791            ExprKind::PGrepExpr {
7792                block,
7793                list,
7794                progress,
7795                stream,
7796            } => {
7797                if *stream {
7798                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7799                    let block_idx = self.add_deferred_block(block.clone());
7800                    self.emit_op(Op::PGrepsWithBlock(block_idx), line, Some(root));
7801                } else {
7802                    if let Some(p) = progress {
7803                        self.compile_expr(p)?;
7804                    } else {
7805                        self.emit_op(Op::LoadInt(0), line, Some(root));
7806                    }
7807                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7808                    let block_idx = self.add_deferred_block(block.clone());
7809                    self.emit_op(Op::PGrepWithBlock(block_idx), line, Some(root));
7810                }
7811            }
7812            ExprKind::PForExpr {
7813                block,
7814                list,
7815                progress,
7816            } => {
7817                if let Some(p) = progress {
7818                    self.compile_expr(p)?;
7819                } else {
7820                    self.emit_op(Op::LoadInt(0), line, Some(root));
7821                }
7822                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7823                let block_idx = self.add_deferred_block(block.clone());
7824                self.emit_op(Op::PForWithBlock(block_idx), line, Some(root));
7825            }
7826            ExprKind::ParLinesExpr {
7827                path,
7828                callback,
7829                progress,
7830            } => {
7831                let idx = self.chunk.add_par_lines_entry(
7832                    path.as_ref().clone(),
7833                    callback.as_ref().clone(),
7834                    progress.as_ref().map(|p| p.as_ref().clone()),
7835                );
7836                self.emit_op(Op::ParLines(idx), line, Some(root));
7837            }
7838            ExprKind::ParWalkExpr {
7839                path,
7840                callback,
7841                progress,
7842            } => {
7843                let idx = self.chunk.add_par_walk_entry(
7844                    path.as_ref().clone(),
7845                    callback.as_ref().clone(),
7846                    progress.as_ref().map(|p| p.as_ref().clone()),
7847                );
7848                self.emit_op(Op::ParWalk(idx), line, Some(root));
7849            }
7850            ExprKind::PwatchExpr { path, callback } => {
7851                let idx = self
7852                    .chunk
7853                    .add_pwatch_entry(path.as_ref().clone(), callback.as_ref().clone());
7854                self.emit_op(Op::Pwatch(idx), line, Some(root));
7855            }
7856            ExprKind::PSortExpr {
7857                cmp,
7858                list,
7859                progress,
7860            } => {
7861                if let Some(p) = progress {
7862                    self.compile_expr(p)?;
7863                } else {
7864                    self.emit_op(Op::LoadInt(0), line, Some(root));
7865                }
7866                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7867                if let Some(block) = cmp {
7868                    if let Some(mode) = detect_sort_block_fast(block) {
7869                        let tag = match mode {
7870                            crate::sort_fast::SortBlockFast::Numeric => 0u8,
7871                            crate::sort_fast::SortBlockFast::String => 1u8,
7872                            crate::sort_fast::SortBlockFast::NumericRev => 2u8,
7873                            crate::sort_fast::SortBlockFast::StringRev => 3u8,
7874                        };
7875                        self.emit_op(Op::PSortWithBlockFast(tag), line, Some(root));
7876                    } else {
7877                        let block_idx = self.add_deferred_block(block.clone());
7878                        self.register_sort_pair_block(block_idx);
7879                        self.emit_op(Op::PSortWithBlock(block_idx), line, Some(root));
7880                    }
7881                } else {
7882                    self.emit_op(Op::PSortNoBlockParallel, line, Some(root));
7883                }
7884            }
7885            ExprKind::ReduceExpr { block, list } => {
7886                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7887                let block_idx = self.add_deferred_block(block.clone());
7888                self.register_sort_pair_block(block_idx);
7889                self.emit_op(Op::ReduceWithBlock(block_idx), line, Some(root));
7890            }
7891            ExprKind::PReduceExpr {
7892                block,
7893                list,
7894                progress,
7895            } => {
7896                if let Some(p) = progress {
7897                    self.compile_expr(p)?;
7898                } else {
7899                    self.emit_op(Op::LoadInt(0), line, Some(root));
7900                }
7901                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7902                let block_idx = self.add_deferred_block(block.clone());
7903                self.register_sort_pair_block(block_idx);
7904                self.emit_op(Op::PReduceWithBlock(block_idx), line, Some(root));
7905            }
7906            ExprKind::PReduceInitExpr {
7907                init,
7908                block,
7909                list,
7910                progress,
7911            } => {
7912                if let Some(p) = progress {
7913                    self.compile_expr(p)?;
7914                } else {
7915                    self.emit_op(Op::LoadInt(0), line, Some(root));
7916                }
7917                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7918                self.compile_expr(init)?;
7919                let block_idx = self.add_deferred_block(block.clone());
7920                self.emit_op(Op::PReduceInitWithBlock(block_idx), line, Some(root));
7921            }
7922            ExprKind::PMapReduceExpr {
7923                map_block,
7924                reduce_block,
7925                list,
7926                progress,
7927            } => {
7928                if let Some(p) = progress {
7929                    self.compile_expr(p)?;
7930                } else {
7931                    self.emit_op(Op::LoadInt(0), line, Some(root));
7932                }
7933                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7934                let map_idx = self.add_deferred_block(map_block.clone());
7935                let reduce_idx = self.add_deferred_block(reduce_block.clone());
7936                self.emit_op(
7937                    Op::PMapReduceWithBlocks(map_idx, reduce_idx),
7938                    line,
7939                    Some(root),
7940                );
7941            }
7942            ExprKind::PcacheExpr {
7943                block,
7944                list,
7945                progress,
7946            } => {
7947                if let Some(p) = progress {
7948                    self.compile_expr(p)?;
7949                } else {
7950                    self.emit_op(Op::LoadInt(0), line, Some(root));
7951                }
7952                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7953                let block_idx = self.add_deferred_block(block.clone());
7954                self.emit_op(Op::PcacheWithBlock(block_idx), line, Some(root));
7955            }
7956            ExprKind::PselectExpr { receivers, timeout } => {
7957                let n = receivers.len();
7958                if n > u8::MAX as usize {
7959                    return Err(CompileError::Unsupported(
7960                        "pselect: too many receivers".into(),
7961                    ));
7962                }
7963                for r in receivers {
7964                    self.compile_expr(r)?;
7965                }
7966                let has_timeout = timeout.is_some();
7967                if let Some(t) = timeout {
7968                    self.compile_expr(t)?;
7969                }
7970                self.emit_op(
7971                    Op::Pselect {
7972                        n_rx: n as u8,
7973                        has_timeout,
7974                    },
7975                    line,
7976                    Some(root),
7977                );
7978            }
7979            ExprKind::FanExpr {
7980                count,
7981                block,
7982                progress,
7983                capture,
7984            } => {
7985                if let Some(p) = progress {
7986                    self.compile_expr(p)?;
7987                } else {
7988                    self.emit_op(Op::LoadInt(0), line, Some(root));
7989                }
7990                let block_idx = self.add_deferred_block(block.clone());
7991                match (count, capture) {
7992                    (Some(c), false) => {
7993                        self.compile_expr(c)?;
7994                        self.emit_op(Op::FanWithBlock(block_idx), line, Some(root));
7995                    }
7996                    (None, false) => {
7997                        self.emit_op(Op::FanWithBlockAuto(block_idx), line, Some(root));
7998                    }
7999                    (Some(c), true) => {
8000                        self.compile_expr(c)?;
8001                        self.emit_op(Op::FanCapWithBlock(block_idx), line, Some(root));
8002                    }
8003                    (None, true) => {
8004                        self.emit_op(Op::FanCapWithBlockAuto(block_idx), line, Some(root));
8005                    }
8006                }
8007            }
8008            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
8009                let block_idx = self.add_deferred_block(body.clone());
8010                self.emit_op(Op::AsyncBlock(block_idx), line, Some(root));
8011            }
8012            ExprKind::Trace { body } => {
8013                let block_idx = self.add_deferred_block(body.clone());
8014                self.emit_op(Op::TraceBlock(block_idx), line, Some(root));
8015            }
8016            ExprKind::Timer { body } => {
8017                let block_idx = self.add_deferred_block(body.clone());
8018                self.emit_op(Op::TimerBlock(block_idx), line, Some(root));
8019            }
8020            ExprKind::Bench { body, times } => {
8021                self.compile_expr(times)?;
8022                let block_idx = self.add_deferred_block(body.clone());
8023                self.emit_op(Op::BenchBlock(block_idx), line, Some(root));
8024            }
8025            ExprKind::Await(e) => {
8026                self.compile_expr(e)?;
8027                self.emit_op(Op::Await, line, Some(root));
8028            }
8029            ExprKind::Slurp(e) => {
8030                self.compile_expr(e)?;
8031                self.emit_op(
8032                    Op::CallBuiltin(BuiltinId::Slurp as u16, 1),
8033                    line,
8034                    Some(root),
8035                );
8036            }
8037            ExprKind::Capture(e) => {
8038                self.compile_expr(e)?;
8039                self.emit_op(
8040                    Op::CallBuiltin(BuiltinId::Capture as u16, 1),
8041                    line,
8042                    Some(root),
8043                );
8044            }
8045            ExprKind::Qx(e) => {
8046                self.compile_expr(e)?;
8047                self.emit_op(
8048                    Op::CallBuiltin(BuiltinId::Readpipe as u16, 1),
8049                    line,
8050                    Some(root),
8051                );
8052            }
8053            ExprKind::FetchUrl(e) => {
8054                self.compile_expr(e)?;
8055                self.emit_op(
8056                    Op::CallBuiltin(BuiltinId::FetchUrl as u16, 1),
8057                    line,
8058                    Some(root),
8059                );
8060            }
8061            ExprKind::Pchannel { capacity } => {
8062                if let Some(c) = capacity {
8063                    self.compile_expr(c)?;
8064                    self.emit_op(
8065                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 1),
8066                        line,
8067                        Some(root),
8068                    );
8069                } else {
8070                    self.emit_op(
8071                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 0),
8072                        line,
8073                        Some(root),
8074                    );
8075                }
8076            }
8077            ExprKind::RetryBlock { .. }
8078            | ExprKind::RateLimitBlock { .. }
8079            | ExprKind::EveryBlock { .. }
8080            | ExprKind::GenBlock { .. }
8081            | ExprKind::Yield(_)
8082            | ExprKind::Spinner { .. } => {
8083                let idx = self.chunk.ast_eval_exprs.len() as u16;
8084                self.chunk.ast_eval_exprs.push(root.clone());
8085                self.emit_op(Op::EvalAstExpr(idx), line, Some(root));
8086            }
8087            ExprKind::MyExpr { keyword, decls } => {
8088                // `my $x = EXPR` in expression context (e.g. `while (my $line = <$fh>)`)
8089                // Compile the declaration, then leave the value on the stack for the caller.
8090                if decls.len() == 1 && decls[0].sigil == Sigil::Scalar {
8091                    let decl = &decls[0];
8092                    if let Some(init) = &decl.initializer {
8093                        self.compile_expr(init)?;
8094                    } else {
8095                        self.chunk.emit(Op::LoadUndef, line);
8096                    }
8097                    // Dup so the value stays on stack after DeclareScalar consumes one copy
8098                    self.emit_op(Op::Dup, line, Some(root));
8099                    let name_idx = self.chunk.intern_name(&decl.name);
8100                    match keyword.as_str() {
8101                        "state" => {
8102                            let name = self.chunk.names[name_idx as usize].clone();
8103                            self.register_declare(Sigil::Scalar, &name, false);
8104                            self.chunk.emit(Op::DeclareStateScalar(name_idx), line);
8105                        }
8106                        _ => {
8107                            self.emit_declare_scalar(name_idx, line, false);
8108                        }
8109                    }
8110                } else {
8111                    return Err(CompileError::Unsupported(
8112                        "my/our/state/local in expression context with multiple or non-scalar decls".into(),
8113                    ));
8114                }
8115            }
8116        }
8117        Ok(())
8118    }
8119
8120    fn compile_string_part(
8121        &mut self,
8122        part: &StringPart,
8123        line: usize,
8124        parent: Option<&Expr>,
8125    ) -> Result<(), CompileError> {
8126        match part {
8127            StringPart::Literal(s) => {
8128                let idx = self.chunk.add_constant(StrykeValue::string(s.clone()));
8129                self.emit_op(Op::LoadConst(idx), line, parent);
8130            }
8131            StringPart::ScalarVar(name) => {
8132                let idx = self.intern_scalar_var_for_ops(name);
8133                self.emit_get_scalar(idx, line, parent);
8134            }
8135            StringPart::ArrayVar(name) => {
8136                let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
8137                self.emit_op(Op::GetArray(idx), line, parent);
8138                self.emit_op(Op::ArrayStringifyListSep, line, parent);
8139            }
8140            StringPart::Expr(e) => {
8141                // Interpolation uses list/array values (`$"`), not Perl scalar(@arr) length.
8142                if matches!(&e.kind, ExprKind::ArraySlice { .. })
8143                    || matches!(
8144                        &e.kind,
8145                        ExprKind::Deref {
8146                            kind: Sigil::Array,
8147                            ..
8148                        }
8149                    )
8150                {
8151                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
8152                    self.emit_op(Op::ArrayStringifyListSep, line, parent);
8153                } else {
8154                    self.compile_expr(e)?;
8155                }
8156            }
8157        }
8158        Ok(())
8159    }
8160
8161    fn compile_assign(
8162        &mut self,
8163        target: &Expr,
8164        line: usize,
8165        keep: bool,
8166        ast: Option<&Expr>,
8167    ) -> Result<(), CompileError> {
8168        match &target.kind {
8169            ExprKind::ScalarVar(name) => {
8170                self.check_strict_scalar_access(name, line)?;
8171                self.check_scalar_mutable(name, line)?;
8172                self.check_closure_write_to_outer_my(name, line)?;
8173                let idx = self.intern_scalar_var_for_ops(name);
8174                if keep {
8175                    self.emit_set_scalar_keep(idx, line, ast);
8176                } else {
8177                    self.emit_set_scalar(idx, line, ast);
8178                }
8179            }
8180            ExprKind::ArrayVar(name) => {
8181                self.check_strict_array_access(name, line)?;
8182                let q = self.qualify_stash_array_name(name);
8183                self.check_array_mutable(&q, line)?;
8184                let idx = self.chunk.intern_name(&q);
8185                self.emit_op(Op::SetArray(idx), line, ast);
8186                if keep {
8187                    self.emit_op(Op::GetArray(idx), line, ast);
8188                }
8189            }
8190            ExprKind::HashVar(name) => {
8191                self.check_strict_hash_access(name, line)?;
8192                self.check_hash_mutable(name, line)?;
8193                let idx = self.chunk.intern_name(name);
8194                self.emit_op(Op::SetHash(idx), line, ast);
8195                if keep {
8196                    self.emit_op(Op::GetHash(idx), line, ast);
8197                }
8198            }
8199            ExprKind::ArrayElement { array, index } => {
8200                self.check_strict_array_access(array, line)?;
8201                let q = self.qualify_stash_array_name(array);
8202                self.check_array_mutable(&q, line)?;
8203                let idx = self.chunk.intern_name(&q);
8204                self.compile_expr(index)?;
8205                self.emit_op(Op::SetArrayElem(idx), line, ast);
8206            }
8207            ExprKind::ArraySlice { array, indices } => {
8208                if indices.is_empty() {
8209                    if self.is_mysync_array(array) {
8210                        return Err(CompileError::Unsupported(
8211                            "mysync array slice assign".into(),
8212                        ));
8213                    }
8214                    self.check_strict_array_access(array, line)?;
8215                    let q = self.qualify_stash_array_name(array);
8216                    self.check_array_mutable(&q, line)?;
8217                    let arr_idx = self.chunk.intern_name(&q);
8218                    self.emit_op(Op::SetNamedArraySlice(arr_idx, 0), line, ast);
8219                    if keep {
8220                        self.emit_op(Op::MakeArray(0), line, ast);
8221                    }
8222                    return Ok(());
8223                }
8224                if self.is_mysync_array(array) {
8225                    return Err(CompileError::Unsupported(
8226                        "mysync array slice assign".into(),
8227                    ));
8228                }
8229                self.check_strict_array_access(array, line)?;
8230                let q = self.qualify_stash_array_name(array);
8231                self.check_array_mutable(&q, line)?;
8232                let arr_idx = self.chunk.intern_name(&q);
8233                for ix in indices {
8234                    self.compile_array_slice_index_expr(ix)?;
8235                }
8236                self.emit_op(
8237                    Op::SetNamedArraySlice(arr_idx, indices.len() as u16),
8238                    line,
8239                    ast,
8240                );
8241                if keep {
8242                    for (ix, index_expr) in indices.iter().enumerate() {
8243                        self.compile_array_slice_index_expr(index_expr)?;
8244                        self.emit_op(Op::ArraySlicePart(arr_idx), line, ast);
8245                        if ix > 0 {
8246                            self.emit_op(Op::ArrayConcatTwo, line, ast);
8247                        }
8248                    }
8249                }
8250                return Ok(());
8251            }
8252            ExprKind::HashElement { hash, key } => {
8253                self.check_strict_hash_access(hash, line)?;
8254                self.check_hash_mutable(hash, line)?;
8255                let idx = self.chunk.intern_name(hash);
8256                self.compile_expr(key)?;
8257                if keep {
8258                    self.emit_op(Op::SetHashElemKeep(idx), line, ast);
8259                } else {
8260                    self.emit_op(Op::SetHashElem(idx), line, ast);
8261                }
8262            }
8263            ExprKind::HashSlice { hash, keys } => {
8264                if keys.is_empty() {
8265                    if self.is_mysync_hash(hash) {
8266                        return Err(CompileError::Unsupported("mysync hash slice assign".into()));
8267                    }
8268                    self.check_strict_hash_access(hash, line)?;
8269                    self.check_hash_mutable(hash, line)?;
8270                    let hash_idx = self.chunk.intern_name(hash);
8271                    self.emit_op(Op::SetHashSlice(hash_idx, 0), line, ast);
8272                    if keep {
8273                        self.emit_op(Op::MakeArray(0), line, ast);
8274                    }
8275                    return Ok(());
8276                }
8277                if self.is_mysync_hash(hash) {
8278                    return Err(CompileError::Unsupported("mysync hash slice assign".into()));
8279                }
8280                self.check_strict_hash_access(hash, line)?;
8281                self.check_hash_mutable(hash, line)?;
8282                let hash_idx = self.chunk.intern_name(hash);
8283                // Multi-key entries (`'a'..'c'`, `qw/a b/`, list literals) push an array value;
8284                // [`Self::assign_named_hash_slice`] / [`crate::bytecode::Op::SetHashSlice`]
8285                // flattens it at runtime, so compile in list context (scalar context collapses
8286                // `..` to a flip-flop).
8287                for key_expr in keys {
8288                    self.compile_hash_slice_key_expr(key_expr)?;
8289                }
8290                self.emit_op(Op::SetHashSlice(hash_idx, keys.len() as u16), line, ast);
8291                if keep {
8292                    for key_expr in keys {
8293                        self.compile_expr(key_expr)?;
8294                        self.emit_op(Op::GetHashElem(hash_idx), line, ast);
8295                    }
8296                    self.emit_op(Op::MakeArray(keys.len() as u16), line, ast);
8297                }
8298                return Ok(());
8299            }
8300            ExprKind::Deref {
8301                expr,
8302                kind: Sigil::Scalar,
8303            } => {
8304                self.compile_expr(expr)?;
8305                if keep {
8306                    self.emit_op(Op::SetSymbolicScalarRefKeep, line, ast);
8307                } else {
8308                    self.emit_op(Op::SetSymbolicScalarRef, line, ast);
8309                }
8310            }
8311            ExprKind::Deref {
8312                expr,
8313                kind: Sigil::Array,
8314            } => {
8315                self.compile_expr(expr)?;
8316                self.emit_op(Op::SetSymbolicArrayRef, line, ast);
8317            }
8318            ExprKind::Deref {
8319                expr,
8320                kind: Sigil::Hash,
8321            } => {
8322                self.compile_expr(expr)?;
8323                self.emit_op(Op::SetSymbolicHashRef, line, ast);
8324            }
8325            ExprKind::Deref {
8326                expr,
8327                kind: Sigil::Typeglob,
8328            } => {
8329                self.compile_expr(expr)?;
8330                self.emit_op(Op::SetSymbolicTypeglobRef, line, ast);
8331            }
8332            ExprKind::Typeglob(name) => {
8333                let idx = self.chunk.intern_name(name);
8334                if keep {
8335                    self.emit_op(Op::TypeglobAssignFromValue(idx), line, ast);
8336                } else {
8337                    return Err(CompileError::Unsupported(
8338                        "typeglob assign without keep (internal)".into(),
8339                    ));
8340                }
8341            }
8342            ExprKind::AnonymousListSlice { source, indices } => {
8343                if let ExprKind::Deref {
8344                    expr: inner,
8345                    kind: Sigil::Array,
8346                } = &source.kind
8347                {
8348                    if indices.is_empty() {
8349                        return Err(CompileError::Unsupported(
8350                            "assign to empty list slice (internal)".into(),
8351                        ));
8352                    }
8353                    self.compile_arrow_array_base_expr(inner)?;
8354                    for ix in indices {
8355                        self.compile_array_slice_index_expr(ix)?;
8356                    }
8357                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
8358                    if keep {
8359                        self.compile_arrow_array_base_expr(inner)?;
8360                        for ix in indices {
8361                            self.compile_array_slice_index_expr(ix)?;
8362                        }
8363                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
8364                    }
8365                    return Ok(());
8366                }
8367                return Err(CompileError::Unsupported(
8368                    "assign to anonymous list slice (non-@array-deref base)".into(),
8369                ));
8370            }
8371            ExprKind::ArrowDeref {
8372                expr,
8373                index,
8374                kind: DerefKind::Hash,
8375            } => {
8376                self.compile_arrow_hash_base_expr(expr)?;
8377                self.compile_expr(index)?;
8378                if keep {
8379                    self.emit_op(Op::SetArrowHashKeep, line, ast);
8380                } else {
8381                    self.emit_op(Op::SetArrowHash, line, ast);
8382                }
8383            }
8384            ExprKind::ArrowDeref {
8385                expr,
8386                index,
8387                kind: DerefKind::Array,
8388            } => {
8389                if let ExprKind::List(indices) = &index.kind {
8390                    // Multi-index slice assignment: RHS value is already on the stack (pushed
8391                    // by the enclosing `compile_expr(value)` before `compile_assign` was called
8392                    // with keep = true). `SetArrowArraySlice` delegates to
8393                    // `Interpreter::assign_arrow_array_slice` for element-wise write.
8394                    self.compile_arrow_array_base_expr(expr)?;
8395                    for ix in indices {
8396                        self.compile_array_slice_index_expr(ix)?;
8397                    }
8398                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
8399                    if keep {
8400                        // The Set op pops the value; keep callers re-read via a fresh slice read.
8401                        self.compile_arrow_array_base_expr(expr)?;
8402                        for ix in indices {
8403                            self.compile_array_slice_index_expr(ix)?;
8404                        }
8405                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
8406                    }
8407                    return Ok(());
8408                }
8409                if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
8410                    self.compile_arrow_array_base_expr(expr)?;
8411                    self.compile_expr(index)?;
8412                    if keep {
8413                        self.emit_op(Op::SetArrowArrayKeep, line, ast);
8414                    } else {
8415                        self.emit_op(Op::SetArrowArray, line, ast);
8416                    }
8417                } else {
8418                    self.compile_arrow_array_base_expr(expr)?;
8419                    self.compile_array_slice_index_expr(index)?;
8420                    self.emit_op(Op::SetArrowArraySlice(1), line, ast);
8421                    if keep {
8422                        self.compile_arrow_array_base_expr(expr)?;
8423                        self.compile_array_slice_index_expr(index)?;
8424                        self.emit_op(Op::ArrowArraySlice(1), line, ast);
8425                    }
8426                }
8427            }
8428            ExprKind::ArrowDeref {
8429                kind: DerefKind::Call,
8430                ..
8431            } => {
8432                return Err(CompileError::Unsupported(
8433                    "Assign to arrow call deref".into(),
8434                ));
8435            }
8436            ExprKind::HashSliceDeref { container, keys } => {
8437                self.compile_expr(container)?;
8438                for key_expr in keys {
8439                    self.compile_hash_slice_key_expr(key_expr)?;
8440                }
8441                self.emit_op(Op::SetHashSliceDeref(keys.len() as u16), line, ast);
8442            }
8443            ExprKind::Pos(inner) => {
8444                let Some(inner_e) = inner.as_ref() else {
8445                    return Err(CompileError::Unsupported(
8446                        "assign to pos() without scalar".into(),
8447                    ));
8448                };
8449                if keep {
8450                    self.emit_op(Op::Dup, line, ast);
8451                }
8452                match &inner_e.kind {
8453                    ExprKind::ScalarVar(name) => {
8454                        let stor = self.scalar_storage_name_for_ops(name);
8455                        let idx = self.chunk.add_constant(StrykeValue::string(stor));
8456                        self.emit_op(Op::LoadConst(idx), line, ast);
8457                    }
8458                    _ => {
8459                        self.compile_expr(inner_e)?;
8460                    }
8461                }
8462                self.emit_op(Op::SetRegexPos, line, ast);
8463            }
8464            // List assignment: `($a, $b) = (val1, val2)` — RHS is on stack as array,
8465            // store into temp, then distribute elements to each target.
8466            ExprKind::List(targets) => {
8467                let tmp = self.chunk.intern_name("__list_assign_swap__");
8468                self.emit_op(Op::DeclareArray(tmp), line, ast);
8469                for (i, t) in targets.iter().enumerate() {
8470                    self.emit_op(Op::LoadInt(i as i64), line, ast);
8471                    self.emit_op(Op::GetArrayElem(tmp), line, ast);
8472                    self.compile_assign(t, line, false, ast)?;
8473                }
8474                if keep {
8475                    self.emit_op(Op::GetArray(tmp), line, ast);
8476                }
8477            }
8478            _ => {
8479                return Err(CompileError::Unsupported("Assign to complex lvalue".into()));
8480            }
8481        }
8482        Ok(())
8483    }
8484}
8485
8486/// Map a binary op to its stack opcode for compound assignment on aggregates (`$a[$i]`, `$h{$k}`).
8487pub(crate) fn binop_to_vm_op(op: BinOp) -> Option<Op> {
8488    Some(match op {
8489        BinOp::Add => Op::Add,
8490        BinOp::Sub => Op::Sub,
8491        BinOp::Mul => Op::Mul,
8492        BinOp::Div => Op::Div,
8493        BinOp::Mod => Op::Mod,
8494        BinOp::Pow => Op::Pow,
8495        BinOp::Concat => Op::Concat,
8496        BinOp::BitAnd => Op::BitAnd,
8497        BinOp::BitOr => Op::BitOr,
8498        BinOp::BitXor => Op::BitXor,
8499        BinOp::ShiftLeft => Op::Shl,
8500        BinOp::ShiftRight => Op::Shr,
8501        _ => return None,
8502    })
8503}
8504
8505/// Encode/decode scalar compound ops for [`Op::ScalarCompoundAssign`].
8506pub(crate) fn scalar_compound_op_to_byte(op: BinOp) -> Option<u8> {
8507    Some(match op {
8508        BinOp::Add => 0,
8509        BinOp::Sub => 1,
8510        BinOp::Mul => 2,
8511        BinOp::Div => 3,
8512        BinOp::Mod => 4,
8513        BinOp::Pow => 5,
8514        BinOp::Concat => 6,
8515        BinOp::BitAnd => 7,
8516        BinOp::BitOr => 8,
8517        BinOp::BitXor => 9,
8518        BinOp::ShiftLeft => 10,
8519        BinOp::ShiftRight => 11,
8520        _ => return None,
8521    })
8522}
8523
8524pub(crate) fn scalar_compound_op_from_byte(b: u8) -> Option<BinOp> {
8525    Some(match b {
8526        0 => BinOp::Add,
8527        1 => BinOp::Sub,
8528        2 => BinOp::Mul,
8529        3 => BinOp::Div,
8530        4 => BinOp::Mod,
8531        5 => BinOp::Pow,
8532        6 => BinOp::Concat,
8533        7 => BinOp::BitAnd,
8534        8 => BinOp::BitOr,
8535        9 => BinOp::BitXor,
8536        10 => BinOp::ShiftLeft,
8537        11 => BinOp::ShiftRight,
8538        _ => return None,
8539    })
8540}
8541
8542#[cfg(test)]
8543mod tests {
8544    use super::*;
8545    use crate::bytecode::{BuiltinId, Op, GP_RUN};
8546    use crate::parse;
8547
8548    fn compile_snippet(code: &str) -> Result<Chunk, CompileError> {
8549        let program = parse(code).expect("parse snippet");
8550        Compiler::new().compile_program(&program)
8551    }
8552
8553    fn assert_last_halt(chunk: &Chunk) {
8554        assert!(
8555            matches!(chunk.ops.last(), Some(Op::Halt)),
8556            "expected Halt last, got {:?}",
8557            chunk.ops.last()
8558        );
8559    }
8560
8561    #[test]
8562    fn compile_empty_program_emits_run_phase_then_halt() {
8563        let chunk = compile_snippet("").expect("compile");
8564        assert_eq!(chunk.ops.len(), 2);
8565        assert!(matches!(chunk.ops[0], Op::SetGlobalPhase(p) if p == GP_RUN));
8566        assert!(matches!(chunk.ops[1], Op::Halt));
8567    }
8568
8569    #[test]
8570    fn compile_struct_method_fn_eq_shorthand() {
8571        compile_snippet("struct CmpS { fn triple($n) = $n * 3 }").expect("compile");
8572    }
8573
8574    #[test]
8575    fn compile_integer_literal_statement() {
8576        let chunk = compile_snippet("42;").expect("compile");
8577        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadInt(42))));
8578        assert_last_halt(&chunk);
8579    }
8580
8581    #[test]
8582    fn compile_pos_assign_emits_set_regex_pos() {
8583        let chunk = compile_snippet(r#"$_ = ""; pos = 3;"#).expect("compile");
8584        assert!(
8585            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
8586            "expected SetRegexPos in {:?}",
8587            chunk.ops
8588        );
8589    }
8590
8591    #[test]
8592    fn compile_pos_deref_scalar_assign_emits_set_regex_pos() {
8593        let chunk = compile_snippet(
8594            r#"no strict 'vars';
8595            my $s;
8596            my $r = \$s;
8597            pos $$r = 0;"#,
8598        )
8599        .expect("compile");
8600        assert!(
8601            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
8602            r"expected SetRegexPos for pos $$r =, got {:?}",
8603            chunk.ops
8604        );
8605    }
8606
8607    #[test]
8608    fn compile_map_expr_comma_emits_map_with_expr() {
8609        let chunk = compile_snippet(
8610            r#"no strict 'vars';
8611            (map $_ + 1, (4, 5)) |> join ','"#,
8612        )
8613        .expect("compile");
8614        assert!(
8615            chunk.ops.iter().any(|o| matches!(o, Op::MapWithExpr(_))),
8616            "expected MapWithExpr, got {:?}",
8617            chunk.ops
8618        );
8619    }
8620
8621    #[test]
8622    fn compile_hash_slice_deref_assign_emits_set_op() {
8623        let code = r#"no strict 'vars';
8624        my $h = { "a" => 1, "b" => 2 };
8625        my $r = $h;
8626        @$r{"a", "b"} = (10, 20);
8627        $r->{"a"} . "," . $r->{"b"};"#;
8628        let chunk = compile_snippet(code).expect("compile");
8629        assert!(
8630            chunk
8631                .ops
8632                .iter()
8633                .any(|o| matches!(o, Op::SetHashSliceDeref(n) if *n == 2)),
8634            "expected SetHashSliceDeref(2), got {:?}",
8635            chunk.ops
8636        );
8637    }
8638
8639    #[test]
8640    fn compile_bare_array_assign_diamond_uses_readline_list() {
8641        let chunk = compile_snippet("@a = <>;").expect("compile");
8642        assert!(
8643            chunk.ops.iter().any(|o| matches!(
8644                o,
8645                Op::CallBuiltin(bid, 0) if *bid == BuiltinId::ReadLineList as u16
8646            )),
8647            "expected ReadLineList for bare @a = <>, got {:?}",
8648            chunk.ops
8649        );
8650    }
8651
8652    #[test]
8653    fn compile_float_literal() {
8654        let chunk = compile_snippet("3.25;").expect("compile");
8655        assert!(chunk
8656            .ops
8657            .iter()
8658            .any(|o| matches!(o, Op::LoadFloat(f) if (*f - 3.25).abs() < 1e-9)));
8659        assert_last_halt(&chunk);
8660    }
8661
8662    #[test]
8663    fn compile_addition() {
8664        let chunk = compile_snippet("1 + 2;").expect("compile");
8665        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Add)));
8666        assert_last_halt(&chunk);
8667    }
8668
8669    #[test]
8670    fn compile_sub_mul_div_mod_pow() {
8671        for (src, op) in [
8672            ("10 - 3;", "Sub"),
8673            ("6 * 7;", "Mul"),
8674            ("8 / 2;", "Div"),
8675            ("9 % 4;", "Mod"),
8676            ("2 ** 8;", "Pow"),
8677        ] {
8678            let chunk = compile_snippet(src).expect(src);
8679            assert!(
8680                chunk.ops.iter().any(|o| std::mem::discriminant(o) == {
8681                    let dummy = match op {
8682                        "Sub" => Op::Sub,
8683                        "Mul" => Op::Mul,
8684                        "Div" => Op::Div,
8685                        "Mod" => Op::Mod,
8686                        "Pow" => Op::Pow,
8687                        _ => unreachable!(),
8688                    };
8689                    std::mem::discriminant(&dummy)
8690                }),
8691                "{} missing {:?}",
8692                src,
8693                op
8694            );
8695            assert_last_halt(&chunk);
8696        }
8697    }
8698
8699    #[test]
8700    fn compile_string_literal_uses_constant_pool() {
8701        let chunk = compile_snippet(r#""hello";"#).expect("compile");
8702        assert!(chunk
8703            .constants
8704            .iter()
8705            .any(|c| c.as_str().as_deref() == Some("hello")));
8706        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadConst(_))));
8707        assert_last_halt(&chunk);
8708    }
8709
8710    #[test]
8711    fn compile_substitution_bind_emits_regex_subst() {
8712        let chunk = compile_snippet(r#"my $s = "aa"; $s =~ s/a/b/g;"#).expect("compile");
8713        assert!(
8714            chunk
8715                .ops
8716                .iter()
8717                .any(|o| matches!(o, Op::RegexSubst(_, _, _, _))),
8718            "expected RegexSubst in {:?}",
8719            chunk.ops
8720        );
8721        assert!(!chunk.lvalues.is_empty());
8722    }
8723
8724    #[test]
8725    fn compile_chomp_emits_chomp_in_place() {
8726        let chunk = compile_snippet(r#"my $s = "x\n"; chomp $s;"#).expect("compile");
8727        assert!(
8728            chunk.ops.iter().any(|o| matches!(o, Op::ChompInPlace(_))),
8729            "expected ChompInPlace, got {:?}",
8730            chunk.ops
8731        );
8732    }
8733
8734    #[test]
8735    fn compile_transliterate_bind_emits_regex_transliterate() {
8736        let chunk = compile_snippet(r#"my $u = "abc"; $u =~ tr/a-z/A-Z/;"#).expect("compile");
8737        assert!(
8738            chunk
8739                .ops
8740                .iter()
8741                .any(|o| matches!(o, Op::RegexTransliterate(_, _, _, _))),
8742            "expected RegexTransliterate in {:?}",
8743            chunk.ops
8744        );
8745        assert!(!chunk.lvalues.is_empty());
8746    }
8747
8748    #[test]
8749    fn compile_negation() {
8750        let chunk = compile_snippet("-7;").expect("compile");
8751        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Negate)));
8752        assert_last_halt(&chunk);
8753    }
8754
8755    #[test]
8756    fn compile_my_scalar_declares() {
8757        let chunk = compile_snippet("my $x = 1;").expect("compile");
8758        assert!(chunk
8759            .ops
8760            .iter()
8761            .any(|o| matches!(o, Op::DeclareScalar(_) | Op::DeclareScalarSlot(_, _))));
8762        assert_last_halt(&chunk);
8763    }
8764
8765    #[test]
8766    fn compile_scalar_fetch_and_assign() {
8767        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
8768        assert!(
8769            chunk
8770                .ops
8771                .iter()
8772                .filter(|o| matches!(
8773                    o,
8774                    Op::GetScalar(_) | Op::GetScalarPlain(_) | Op::GetScalarSlot(_)
8775                ))
8776                .count()
8777                >= 1
8778        );
8779        assert_last_halt(&chunk);
8780    }
8781
8782    #[test]
8783    fn compile_plain_scalar_read_emits_get_scalar_plain() {
8784        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
8785        assert!(
8786            chunk
8787                .ops
8788                .iter()
8789                .any(|o| matches!(o, Op::GetScalarPlain(_) | Op::GetScalarSlot(_))),
8790            "expected GetScalarPlain or GetScalarSlot for non-special $a, ops={:?}",
8791            chunk.ops
8792        );
8793    }
8794
8795    #[test]
8796    fn compile_sub_postfix_inc_emits_post_inc_slot() {
8797        let chunk = compile_snippet("fn foo { my $x = 0; $x++; return $x; }").expect("compile");
8798        assert!(
8799            chunk.ops.iter().any(|o| matches!(o, Op::PostIncSlot(_))),
8800            "expected PostIncSlot in compiled sub body, ops={:?}",
8801            chunk.ops
8802        );
8803    }
8804
8805    #[test]
8806    fn compile_comparison_ops_numeric() {
8807        for src in [
8808            "1 < 2;", "1 > 2;", "1 <= 2;", "1 >= 2;", "1 == 2;", "1 != 2;",
8809        ] {
8810            let chunk = compile_snippet(src).expect(src);
8811            assert!(
8812                chunk.ops.iter().any(|o| {
8813                    matches!(
8814                        o,
8815                        Op::NumLt | Op::NumGt | Op::NumLe | Op::NumGe | Op::NumEq | Op::NumNe
8816                    )
8817                }),
8818                "{}",
8819                src
8820            );
8821            assert_last_halt(&chunk);
8822        }
8823    }
8824
8825    #[test]
8826    fn compile_string_compare_ops() {
8827        for src in [
8828            r#"'a' lt 'b';"#,
8829            r#"'a' gt 'b';"#,
8830            r#"'a' le 'b';"#,
8831            r#"'a' ge 'b';"#,
8832        ] {
8833            let chunk = compile_snippet(src).expect(src);
8834            assert!(
8835                chunk
8836                    .ops
8837                    .iter()
8838                    .any(|o| matches!(o, Op::StrLt | Op::StrGt | Op::StrLe | Op::StrGe)),
8839                "{}",
8840                src
8841            );
8842            assert_last_halt(&chunk);
8843        }
8844    }
8845
8846    #[test]
8847    fn compile_concat() {
8848        let chunk = compile_snippet(r#"'a' . 'b';"#).expect("compile");
8849        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Concat)));
8850        assert_last_halt(&chunk);
8851    }
8852
8853    #[test]
8854    fn compile_bitwise_ops() {
8855        let chunk = compile_snippet("1 & 2 | 3 ^ 4;").expect("compile");
8856        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitAnd)));
8857        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitOr)));
8858        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitXor)));
8859        assert_last_halt(&chunk);
8860    }
8861
8862    #[test]
8863    fn compile_shift_right() {
8864        let chunk = compile_snippet("8 >> 1;").expect("compile");
8865        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shr)));
8866        assert_last_halt(&chunk);
8867    }
8868
8869    #[test]
8870    fn compile_shift_left() {
8871        let chunk = compile_snippet("1 << 4;").expect("compile");
8872        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shl)));
8873        assert_last_halt(&chunk);
8874    }
8875
8876    #[test]
8877    fn compile_log_not_and_bit_not() {
8878        let c1 = compile_snippet("!0;").expect("compile");
8879        assert!(c1.ops.iter().any(|o| matches!(o, Op::LogNot)));
8880        let c2 = compile_snippet("~0;").expect("compile");
8881        assert!(c2.ops.iter().any(|o| matches!(o, Op::BitNot)));
8882    }
8883
8884    #[test]
8885    fn compile_sub_registers_name_and_entry() {
8886        let chunk = compile_snippet("fn foo { return 1; }").expect("compile");
8887        assert!(chunk.names.iter().any(|n| n == "foo"));
8888        assert!(chunk
8889            .sub_entries
8890            .iter()
8891            .any(|&(idx, ip, _)| chunk.names[idx as usize] == "foo" && ip > 0));
8892        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Halt)));
8893        assert!(chunk.ops.iter().any(|o| matches!(o, Op::ReturnValue)));
8894    }
8895
8896    #[test]
8897    fn compile_postinc_scalar() {
8898        let chunk = compile_snippet("my $n = 1; $n++;").expect("compile");
8899        assert!(chunk
8900            .ops
8901            .iter()
8902            .any(|o| matches!(o, Op::PostInc(_) | Op::PostIncSlot(_))));
8903        assert_last_halt(&chunk);
8904    }
8905
8906    #[test]
8907    fn compile_preinc_scalar() {
8908        let chunk = compile_snippet("my $n = 1; ++$n;").expect("compile");
8909        assert!(chunk
8910            .ops
8911            .iter()
8912            .any(|o| matches!(o, Op::PreInc(_) | Op::PreIncSlot(_))));
8913        assert_last_halt(&chunk);
8914    }
8915
8916    #[test]
8917    fn compile_if_expression_value() {
8918        let chunk = compile_snippet("if (1) { 2 } else { 3 }").expect("compile");
8919        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8920        assert_last_halt(&chunk);
8921    }
8922
8923    #[test]
8924    fn compile_unless_expression_value() {
8925        let chunk = compile_snippet("unless (0) { 1 } else { 2 }").expect("compile");
8926        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8927        assert_last_halt(&chunk);
8928    }
8929
8930    #[test]
8931    fn compile_array_declare_and_push() {
8932        let chunk = compile_snippet("my @a; push @a, 1;").expect("compile");
8933        assert!(chunk.ops.iter().any(|o| matches!(o, Op::DeclareArray(_))));
8934        assert_last_halt(&chunk);
8935    }
8936
8937    #[test]
8938    fn compile_ternary() {
8939        let chunk = compile_snippet("1 ? 2 : 3;").expect("compile");
8940        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8941        assert_last_halt(&chunk);
8942    }
8943
8944    #[test]
8945    fn compile_repeat_operator() {
8946        let chunk = compile_snippet(r#"'ab' x 3;"#).expect("compile");
8947        assert!(chunk.ops.iter().any(|o| matches!(o, Op::StringRepeat)));
8948        assert_last_halt(&chunk);
8949    }
8950
8951    #[test]
8952    fn compile_range_to_array() {
8953        let chunk = compile_snippet("my @a = (1..3);").expect("compile");
8954        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Range)));
8955        assert_last_halt(&chunk);
8956    }
8957
8958    /// Scalar `..` / `...` in a boolean condition must be the flip-flop (`$.`), not a list range.
8959    #[test]
8960    fn compile_print_if_uses_scalar_flipflop_not_range_list() {
8961        let chunk = compile_snippet("print if 1..2;").expect("compile");
8962        assert!(
8963            chunk
8964                .ops
8965                .iter()
8966                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 0))),
8967            "expected ScalarFlipFlop in bytecode, got:\n{}",
8968            chunk.disassemble()
8969        );
8970        assert!(
8971            !chunk.ops.iter().any(|o| matches!(o, Op::Range)),
8972            "did not expect list Range op in scalar if-condition:\n{}",
8973            chunk.disassemble()
8974        );
8975    }
8976
8977    #[test]
8978    fn compile_print_if_three_dot_scalar_flipflop_sets_exclusive_flag() {
8979        let chunk = compile_snippet("print if 1...2;").expect("compile");
8980        assert!(
8981            chunk
8982                .ops
8983                .iter()
8984                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 1))),
8985            "expected ScalarFlipFlop(..., exclusive=1), got:\n{}",
8986            chunk.disassemble()
8987        );
8988    }
8989
8990    #[test]
8991    fn compile_regex_flipflop_two_dot_emits_regex_flipflop_op() {
8992        let chunk = compile_snippet(r#"print if /a/../b/;"#).expect("compile");
8993        assert!(
8994            chunk
8995                .ops
8996                .iter()
8997                .any(|o| matches!(o, Op::RegexFlipFlop(_, 0, _, _, _, _))),
8998            "expected RegexFlipFlop(.., exclusive=0), got:\n{}",
8999            chunk.disassemble()
9000        );
9001        assert!(
9002            !chunk
9003                .ops
9004                .iter()
9005                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9006            "regex flip-flop must not use ScalarFlipFlop:\n{}",
9007            chunk.disassemble()
9008        );
9009    }
9010
9011    #[test]
9012    fn compile_regex_flipflop_three_dot_sets_exclusive_flag() {
9013        let chunk = compile_snippet(r#"print if /a/.../b/;"#).expect("compile");
9014        assert!(
9015            chunk
9016                .ops
9017                .iter()
9018                .any(|o| matches!(o, Op::RegexFlipFlop(_, 1, _, _, _, _))),
9019            "expected RegexFlipFlop(..., exclusive=1), got:\n{}",
9020            chunk.disassemble()
9021        );
9022    }
9023
9024    #[test]
9025    fn compile_regex_eof_flipflop_emits_regex_eof_flipflop_op() {
9026        let chunk = compile_snippet(r#"print if /a/..eof;"#).expect("compile");
9027        assert!(
9028            chunk
9029                .ops
9030                .iter()
9031                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 0, _, _))),
9032            "expected RegexEofFlipFlop(.., exclusive=0), got:\n{}",
9033            chunk.disassemble()
9034        );
9035        assert!(
9036            !chunk
9037                .ops
9038                .iter()
9039                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9040            "regex/eof flip-flop must not use ScalarFlipFlop:\n{}",
9041            chunk.disassemble()
9042        );
9043    }
9044
9045    #[test]
9046    fn compile_regex_eof_flipflop_three_dot_sets_exclusive_flag() {
9047        let chunk = compile_snippet(r#"print if /a/...eof;"#).expect("compile");
9048        assert!(
9049            chunk
9050                .ops
9051                .iter()
9052                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 1, _, _))),
9053            "expected RegexEofFlipFlop(..., exclusive=1), got:\n{}",
9054            chunk.disassemble()
9055        );
9056    }
9057
9058    #[test]
9059    fn compile_regex_flipflop_compound_rhs_emits_regex_flip_flop_expr_rhs() {
9060        let chunk = compile_snippet(r#"print if /a/...(/b/ or /c/);"#).expect("compile");
9061        assert!(
9062            chunk
9063                .ops
9064                .iter()
9065                .any(|o| matches!(o, Op::RegexFlipFlopExprRhs(_, _, _, _, _))),
9066            "expected RegexFlipFlopExprRhs for compound RHS, got:\n{}",
9067            chunk.disassemble()
9068        );
9069        assert!(
9070            !chunk
9071                .ops
9072                .iter()
9073                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9074            "compound regex flip-flop must not use ScalarFlipFlop:\n{}",
9075            chunk.disassemble()
9076        );
9077    }
9078
9079    #[test]
9080    fn compile_print_statement() {
9081        let chunk = compile_snippet("print 1;").expect("compile");
9082        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Print(_, _))));
9083        assert_last_halt(&chunk);
9084    }
9085
9086    #[test]
9087    fn compile_defined_builtin() {
9088        let chunk = compile_snippet("defined 1;").expect("compile");
9089        assert!(chunk
9090            .ops
9091            .iter()
9092            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Defined as u16)));
9093        assert_last_halt(&chunk);
9094    }
9095
9096    #[test]
9097    fn compile_length_builtin() {
9098        let chunk = compile_snippet("length 'abc';").expect("compile");
9099        assert!(chunk
9100            .ops
9101            .iter()
9102            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Length as u16)));
9103        assert_last_halt(&chunk);
9104    }
9105
9106    #[test]
9107    fn compile_complex_expr_parentheses() {
9108        let chunk = compile_snippet("(1 + 2) * (3 + 4);").expect("compile");
9109        assert!(chunk.ops.iter().filter(|o| matches!(o, Op::Add)).count() >= 2);
9110        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Mul)));
9111        assert_last_halt(&chunk);
9112    }
9113
9114    #[test]
9115    fn compile_undef_literal() {
9116        let chunk = compile_snippet("undef;").expect("compile");
9117        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadUndef)));
9118        assert_last_halt(&chunk);
9119    }
9120
9121    #[test]
9122    fn compile_empty_statement_semicolons() {
9123        let chunk = compile_snippet(";;;").expect("compile");
9124        assert_last_halt(&chunk);
9125    }
9126
9127    #[test]
9128    fn compile_array_elem_preinc_uses_rot_and_set_elem() {
9129        let chunk = compile_snippet("my @a; $a[0] = 0; ++$a[0];").expect("compile");
9130        assert!(
9131            chunk.ops.iter().any(|o| matches!(o, Op::Rot)),
9132            "expected Rot in {:?}",
9133            chunk.ops
9134        );
9135        assert!(
9136            chunk.ops.iter().any(|o| matches!(o, Op::SetArrayElem(_))),
9137            "expected SetArrayElem in {:?}",
9138            chunk.ops
9139        );
9140        assert_last_halt(&chunk);
9141    }
9142
9143    #[test]
9144    fn compile_hash_elem_compound_assign_uses_rot() {
9145        let chunk = compile_snippet("my %h; $h{0} = 1; $h{0} += 2;").expect("compile");
9146        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
9147        assert!(
9148            chunk.ops.iter().any(|o| matches!(o, Op::SetHashElem(_))),
9149            "expected SetHashElem"
9150        );
9151        assert_last_halt(&chunk);
9152    }
9153
9154    #[test]
9155    fn compile_postfix_inc_array_elem_emits_rot() {
9156        let chunk = compile_snippet("my @a; $a[1] = 5; $a[1]++;").expect("compile");
9157        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
9158        assert_last_halt(&chunk);
9159    }
9160
9161    #[test]
9162    fn compile_tie_stmt_emits_op_tie() {
9163        let chunk = compile_snippet("tie %h, 'Pkg';").expect("compile");
9164        assert!(
9165            chunk.ops.iter().any(|o| matches!(o, Op::Tie { .. })),
9166            "expected Op::Tie in {:?}",
9167            chunk.ops
9168        );
9169        assert_last_halt(&chunk);
9170    }
9171
9172    #[test]
9173    fn compile_format_decl_emits_format_decl_op() {
9174        let chunk = compile_snippet(
9175            r#"
9176format FMT =
9177literal line
9178.
91791;
9180"#,
9181        )
9182        .expect("compile");
9183        assert!(
9184            chunk.ops.iter().any(|o| matches!(o, Op::FormatDecl(0))),
9185            "expected Op::FormatDecl(0), got {:?}",
9186            chunk.ops
9187        );
9188        assert_eq!(chunk.format_decls.len(), 1);
9189        assert_eq!(chunk.format_decls[0].0, "FMT");
9190        assert_eq!(chunk.format_decls[0].1, vec!["literal line".to_string()]);
9191        assert_last_halt(&chunk);
9192    }
9193
9194    #[test]
9195    fn compile_interpolated_string_scalar_only_emits_empty_prefix_and_concat() {
9196        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "$x";"#).expect("compile");
9197        let empty_idx = chunk
9198            .constants
9199            .iter()
9200            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9201            .expect("empty string in pool") as u16;
9202        assert!(
9203            chunk
9204                .ops
9205                .iter()
9206                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9207            "expected LoadConst(\"\"), ops={:?}",
9208            chunk.ops
9209        );
9210        assert!(
9211            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9212            "expected Op::Concat for qq with only a scalar part, ops={:?}",
9213            chunk.ops
9214        );
9215        assert_last_halt(&chunk);
9216    }
9217
9218    #[test]
9219    fn compile_interpolated_string_array_only_emits_stringify_and_concat() {
9220        let chunk = compile_snippet(r#"no strict 'vars'; my @a = (1, 2); "@a";"#).expect("compile");
9221        let empty_idx = chunk
9222            .constants
9223            .iter()
9224            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9225            .expect("empty string in pool") as u16;
9226        assert!(
9227            chunk
9228                .ops
9229                .iter()
9230                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9231            "expected LoadConst(\"\"), ops={:?}",
9232            chunk.ops
9233        );
9234        assert!(
9235            chunk
9236                .ops
9237                .iter()
9238                .any(|o| matches!(o, Op::ArrayStringifyListSep)),
9239            "expected ArrayStringifyListSep for array var in qq, ops={:?}",
9240            chunk.ops
9241        );
9242        assert!(
9243            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9244            "expected Op::Concat after array stringify, ops={:?}",
9245            chunk.ops
9246        );
9247        assert_last_halt(&chunk);
9248    }
9249
9250    #[test]
9251    fn compile_interpolated_string_hash_element_only_emits_empty_prefix_and_concat() {
9252        let chunk =
9253            compile_snippet(r#"no strict 'vars'; my %h = (k => 1); "$h{k}";"#).expect("compile");
9254        let empty_idx = chunk
9255            .constants
9256            .iter()
9257            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9258            .expect("empty string in pool") as u16;
9259        assert!(
9260            chunk
9261                .ops
9262                .iter()
9263                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9264            "expected LoadConst(\"\"), ops={:?}",
9265            chunk.ops
9266        );
9267        assert!(
9268            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9269            "expected Op::Concat for qq with only an expr part, ops={:?}",
9270            chunk.ops
9271        );
9272        assert_last_halt(&chunk);
9273    }
9274
9275    #[test]
9276    fn compile_interpolated_string_leading_literal_has_no_empty_string_prefix() {
9277        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "a$x";"#).expect("compile");
9278        assert!(
9279            !chunk
9280                .constants
9281                .iter()
9282                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
9283            "literal-first qq must not intern \"\" (only non-literal first parts need it), ops={:?}",
9284            chunk.ops
9285        );
9286        assert!(
9287            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9288            "expected Op::Concat after literal + scalar, ops={:?}",
9289            chunk.ops
9290        );
9291        assert_last_halt(&chunk);
9292    }
9293
9294    #[test]
9295    fn compile_interpolated_string_two_scalars_empty_prefix_and_two_concats() {
9296        let chunk =
9297            compile_snippet(r#"no strict 'vars'; my $a = 1; my $b = 2; "$a$b";"#).expect("compile");
9298        let empty_idx = chunk
9299            .constants
9300            .iter()
9301            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9302            .expect("empty string in pool") as u16;
9303        assert!(
9304            chunk
9305                .ops
9306                .iter()
9307                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9308            "expected LoadConst(\"\") before first scalar qq part, ops={:?}",
9309            chunk.ops
9310        );
9311        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9312        assert!(
9313            n_concat >= 2,
9314            "expected at least two Op::Concat for two scalar qq parts, got {} in {:?}",
9315            n_concat,
9316            chunk.ops
9317        );
9318        assert_last_halt(&chunk);
9319    }
9320
9321    #[test]
9322    fn compile_interpolated_string_literal_then_two_scalars_has_no_empty_prefix() {
9323        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 7; my $y = 8; "p$x$y";"#)
9324            .expect("compile");
9325        assert!(
9326            !chunk
9327                .constants
9328                .iter()
9329                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
9330            "literal-first qq must not intern empty string, ops={:?}",
9331            chunk.ops
9332        );
9333        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9334        assert!(
9335            n_concat >= 2,
9336            "expected two Concats for literal + two scalars, got {} in {:?}",
9337            n_concat,
9338            chunk.ops
9339        );
9340        assert_last_halt(&chunk);
9341    }
9342
9343    #[test]
9344    fn compile_interpolated_string_braced_scalar_trailing_literal_emits_concats() {
9345        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "a${u}z";"#).expect("compile");
9346        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9347        assert!(
9348            n_concat >= 2,
9349            "expected braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
9350            n_concat,
9351            chunk.ops
9352        );
9353        assert_last_halt(&chunk);
9354    }
9355
9356    #[test]
9357    fn compile_interpolated_string_braced_scalar_sandwiched_emits_concats() {
9358        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "L${u}R";"#).expect("compile");
9359        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9360        assert!(
9361            n_concat >= 2,
9362            "expected leading literal + braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
9363            n_concat,
9364            chunk.ops
9365        );
9366        assert_last_halt(&chunk);
9367    }
9368
9369    #[test]
9370    fn compile_interpolated_string_mixed_braced_and_plain_scalars_emits_concats() {
9371        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; my $y = 2; "a${x}b$y";"#)
9372            .expect("compile");
9373        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9374        assert!(
9375            n_concat >= 3,
9376            "expected literal/braced/plain qq mix to use at least three Concats, got {} in {:?}",
9377            n_concat,
9378            chunk.ops
9379        );
9380        assert_last_halt(&chunk);
9381    }
9382
9383    #[test]
9384    fn compile_use_overload_emits_use_overload_op() {
9385        let chunk = compile_snippet(r#"use overload '""' => 'as_string';"#).expect("compile");
9386        assert!(
9387            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9388            "expected Op::UseOverload(0), got {:?}",
9389            chunk.ops
9390        );
9391        assert_eq!(chunk.use_overload_entries.len(), 1);
9392        // Perl `'""'` is a single-quoted string whose contents are two `"` characters — the
9393        // overload table key for stringify (see [`Interpreter::overload_stringify_method`]).
9394        let stringify_key: String = ['"', '"'].iter().collect();
9395        assert_eq!(
9396            chunk.use_overload_entries[0],
9397            vec![(stringify_key, "as_string".to_string())]
9398        );
9399        assert_last_halt(&chunk);
9400    }
9401
9402    #[test]
9403    fn compile_use_overload_empty_list_emits_use_overload_with_no_pairs() {
9404        let chunk = compile_snippet(r#"use overload ();"#).expect("compile");
9405        assert!(
9406            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9407            "expected Op::UseOverload(0), got {:?}",
9408            chunk.ops
9409        );
9410        assert_eq!(chunk.use_overload_entries.len(), 1);
9411        assert!(chunk.use_overload_entries[0].is_empty());
9412        assert_last_halt(&chunk);
9413    }
9414
9415    #[test]
9416    fn compile_use_overload_multiple_pairs_single_op() {
9417        let chunk =
9418            compile_snippet(r#"use overload '+' => 'p_add', '-' => 'p_sub';"#).expect("compile");
9419        assert!(
9420            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9421            "expected Op::UseOverload(0), got {:?}",
9422            chunk.ops
9423        );
9424        assert_eq!(chunk.use_overload_entries.len(), 1);
9425        assert_eq!(
9426            chunk.use_overload_entries[0],
9427            vec![
9428                ("+".to_string(), "p_add".to_string()),
9429                ("-".to_string(), "p_sub".to_string()),
9430            ]
9431        );
9432        assert_last_halt(&chunk);
9433    }
9434
9435    #[test]
9436    fn compile_open_my_fh_emits_declare_open_set() {
9437        let chunk = compile_snippet(r#"open my $fh, "<", "/dev/null";"#).expect("compile");
9438        assert!(
9439            chunk.ops.iter().any(|o| matches!(
9440                o,
9441                Op::CallBuiltin(b, 3) if *b == BuiltinId::Open as u16
9442            )),
9443            "expected Open builtin 3-arg, got {:?}",
9444            chunk.ops
9445        );
9446        assert!(
9447            chunk
9448                .ops
9449                .iter()
9450                .any(|o| matches!(o, Op::SetScalarKeepPlain(_))),
9451            "expected SetScalarKeepPlain after open"
9452        );
9453        assert_last_halt(&chunk);
9454    }
9455
9456    #[test]
9457    fn compile_local_hash_element_emits_local_declare_hash_element() {
9458        let chunk = compile_snippet(r#"local $SIG{__WARN__} = 0;"#).expect("compile");
9459        assert!(
9460            chunk
9461                .ops
9462                .iter()
9463                .any(|o| matches!(o, Op::LocalDeclareHashElement(_))),
9464            "expected LocalDeclareHashElement in {:?}",
9465            chunk.ops
9466        );
9467        assert_last_halt(&chunk);
9468    }
9469
9470    #[test]
9471    fn compile_local_array_element_emits_local_declare_array_element() {
9472        let chunk = compile_snippet(r#"local $a[2] = 9;"#).expect("compile");
9473        assert!(
9474            chunk
9475                .ops
9476                .iter()
9477                .any(|o| matches!(o, Op::LocalDeclareArrayElement(_))),
9478            "expected LocalDeclareArrayElement in {:?}",
9479            chunk.ops
9480        );
9481        assert_last_halt(&chunk);
9482    }
9483
9484    #[test]
9485    fn compile_local_typeglob_emits_local_declare_typeglob() {
9486        let chunk = compile_snippet(r#"local *STDOUT;"#).expect("compile");
9487        assert!(
9488            chunk
9489                .ops
9490                .iter()
9491                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, None))),
9492            "expected LocalDeclareTypeglob(_, None) in {:?}",
9493            chunk.ops
9494        );
9495        assert_last_halt(&chunk);
9496    }
9497
9498    #[test]
9499    fn compile_local_typeglob_alias_emits_local_declare_typeglob_some_rhs() {
9500        let chunk = compile_snippet(r#"local *FOO = *STDOUT;"#).expect("compile");
9501        assert!(
9502            chunk
9503                .ops
9504                .iter()
9505                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, Some(_)))),
9506            "expected LocalDeclareTypeglob with rhs in {:?}",
9507            chunk.ops
9508        );
9509        assert_last_halt(&chunk);
9510    }
9511
9512    #[test]
9513    fn compile_local_braced_typeglob_emits_local_declare_typeglob_dynamic() {
9514        let chunk = compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *{ $g };"#)
9515            .expect("compile");
9516        assert!(
9517            chunk
9518                .ops
9519                .iter()
9520                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
9521            "expected LocalDeclareTypeglobDynamic(None) in {:?}",
9522            chunk.ops
9523        );
9524        assert_last_halt(&chunk);
9525    }
9526
9527    #[test]
9528    fn compile_local_star_deref_typeglob_emits_local_declare_typeglob_dynamic() {
9529        let chunk =
9530            compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *$g;"#).expect("compile");
9531        assert!(
9532            chunk
9533                .ops
9534                .iter()
9535                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
9536            "expected LocalDeclareTypeglobDynamic(None) for local *scalar glob in {:?}",
9537            chunk.ops
9538        );
9539        assert_last_halt(&chunk);
9540    }
9541
9542    #[test]
9543    fn compile_braced_glob_assign_to_named_glob_emits_copy_dynamic_lhs() {
9544        // `*{EXPR} = *FOO` — dynamic lhs name + static rhs glob → `CopyTypeglobSlotsDynamicLhs`.
9545        let chunk = compile_snippet(r#"no strict 'refs'; my $n = "x"; *{ $n } = *STDOUT;"#)
9546            .expect("compile");
9547        assert!(
9548            chunk
9549                .ops
9550                .iter()
9551                .any(|o| matches!(o, Op::CopyTypeglobSlotsDynamicLhs(_))),
9552            "expected CopyTypeglobSlotsDynamicLhs in {:?}",
9553            chunk.ops
9554        );
9555        assert_last_halt(&chunk);
9556    }
9557
9558    /// Regression for the class-field line-drift bug at the compiler level.
9559    /// A class body with a typed field used to push every subsequent
9560    /// statement's bytecode `lines[i]` off by +1 per field, so DAP
9561    /// breakpoints set on the post-class lines silently dropped. With the
9562    /// lexer's fat-arrow / qw / ... lookaheads correctly restoring
9563    /// `self.line`, the compiled chunk's `lines[]` matches the source.
9564    #[test]
9565    fn class_field_emits_correct_op_lines_for_subsequent_statements() {
9566        let chunk =
9567            compile_snippet("class Foo {\n    x: Int\n}\nmy $y = 1\np $y\n").expect("compile");
9568        // `my $y = 1` is at source line 4. Find any op with line 4 — it
9569        // proves the compiler emitted bytecode for that statement on the
9570        // correct source line. Before the fix, line 4 would not appear
9571        // (the ops would carry line 5 instead).
9572        let has_line_4 = chunk.lines.contains(&4);
9573        assert!(
9574            has_line_4,
9575            "expected at least one op tagged with source line 4 (my $y = 1), got lines {:?}",
9576            chunk.lines
9577        );
9578        // And `p $y` is at line 5; same regression catch.
9579        let has_line_5 = chunk.lines.contains(&5);
9580        assert!(
9581            has_line_5,
9582            "expected at least one op tagged with source line 5 (p $y), got lines {:?}",
9583            chunk.lines
9584        );
9585        // And the inverse — *no* op should be on line 6 (file only has
9586        // 5 logical lines plus a trailing newline). A line-6 op means the
9587        // counter drifted past the file.
9588        let has_line_6 = chunk.lines.contains(&6);
9589        assert!(
9590            !has_line_6,
9591            "no op should report line 6 in a 5-line file; lines {:?}",
9592            chunk.lines
9593        );
9594    }
9595
9596    /// Same check but for class with *two* typed fields (the original
9597    /// reproduction had 2 fields giving a +2 drift on subsequent ops).
9598    #[test]
9599    fn class_with_two_fields_does_not_drift_op_lines() {
9600        let chunk = compile_snippet(
9601            "class Foo {\n    x: Int\n    y: Int\n}\nmy $a = 1\nmy $b = 2\np $a\np $b\n",
9602        )
9603        .expect("compile");
9604        // `my $a = 1` is at source line 5; `my $b = 2` at line 6;
9605        // `p $a` at line 7; `p $b` at line 8.
9606        for expected in [5, 6, 7, 8] {
9607            assert!(
9608                chunk.lines.contains(&expected),
9609                "expected an op on line {expected}, got {:?}",
9610                chunk.lines
9611            );
9612        }
9613        // And no op past the end of the file.
9614        let max_line = chunk.lines.iter().copied().max().unwrap_or(0);
9615        assert!(
9616            max_line <= 8,
9617            "no op past source line 8: max was {max_line}"
9618        );
9619    }
9620}