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                            self.emit_op(Op::SetArrowHash, line, Some(root));
4755                        }
4756                    }
4757                } else if let ExprKind::ArrowDeref {
4758                    expr,
4759                    index,
4760                    kind: DerefKind::Array,
4761                } = &target.kind
4762                {
4763                    if let ExprKind::List(indices) = &index.kind {
4764                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
4765                            let k = indices.len() as u16;
4766                            self.compile_arrow_array_base_expr(expr)?;
4767                            for ix in indices {
4768                                self.compile_array_slice_index_expr(ix)?;
4769                            }
4770                            self.emit_op(Op::ArrowArraySlicePeekLast(k), line, Some(root));
4771                            let j = match *op {
4772                                BinOp::DefinedOr => {
4773                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4774                                }
4775                                BinOp::LogOr => {
4776                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4777                                }
4778                                BinOp::LogAnd => {
4779                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4780                                }
4781                                _ => unreachable!(),
4782                            };
4783                            self.compile_expr(value)?;
4784                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(k), line, Some(root));
4785                            self.emit_op(Op::SetArrowArraySliceLastKeep(k), line, Some(root));
4786                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4787                            self.chunk.patch_jump_here(j);
4788                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(k), line, Some(root));
4789                            self.chunk.patch_jump_here(j_end);
4790                            return Ok(());
4791                        }
4792                        // Multi-index `@$aref[i1,i2,...] OP= EXPR` — Perl applies the op only to the
4793                        // last index (see `Interpreter::compound_assign_arrow_array_slice`).
4794                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
4795                            CompileError::Unsupported(
4796                                "CompoundAssign op on multi-index array slice".into(),
4797                            )
4798                        })?;
4799                        self.compile_expr(value)?;
4800                        self.compile_arrow_array_base_expr(expr)?;
4801                        for ix in indices {
4802                            self.compile_array_slice_index_expr(ix)?;
4803                        }
4804                        self.emit_op(
4805                            Op::ArrowArraySliceCompound(op_byte, indices.len() as u16),
4806                            line,
4807                            Some(root),
4808                        );
4809                        return Ok(());
4810                    }
4811                    match op {
4812                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4813                            // Same last-slot short-circuit semantics as `@$r[i,j] //=` but with one
4814                            // subscript slot (`..` / list / `qw` flatten to multiple indices).
4815                            self.compile_arrow_array_base_expr(expr)?;
4816                            self.compile_array_slice_index_expr(index)?;
4817                            self.emit_op(Op::ArrowArraySlicePeekLast(1), line, Some(root));
4818                            let j = match *op {
4819                                BinOp::DefinedOr => {
4820                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4821                                }
4822                                BinOp::LogOr => {
4823                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4824                                }
4825                                BinOp::LogAnd => {
4826                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4827                                }
4828                                _ => unreachable!(),
4829                            };
4830                            self.compile_expr(value)?;
4831                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(1), line, Some(root));
4832                            self.emit_op(Op::SetArrowArraySliceLastKeep(1), line, Some(root));
4833                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4834                            self.chunk.patch_jump_here(j);
4835                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(1), line, Some(root));
4836                            self.chunk.patch_jump_here(j_end);
4837                        }
4838                        _ => {
4839                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
4840                                CompileError::Unsupported("CompoundAssign op".into())
4841                            })?;
4842                            self.compile_expr(value)?;
4843                            self.compile_arrow_array_base_expr(expr)?;
4844                            self.compile_array_slice_index_expr(index)?;
4845                            self.emit_op(Op::ArrowArraySliceCompound(op_byte, 1), line, Some(root));
4846                        }
4847                    }
4848                } else if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
4849                    // Single-key `@$href{"k"} OP= EXPR` matches `$href->{"k"} OP= EXPR` (ArrowHash).
4850                    // Multi-key `@$href{k1,k2} OP= EXPR` — Perl applies the op only to the last key.
4851                    if keys.is_empty() {
4852                        // Mirror `@h{} OP= EXPR`: evaluate invocant and RHS, then error (matches
4853                        // [`ExprKind::HashSlice`] empty `keys` compound path).
4854                        self.compile_expr(container)?;
4855                        self.emit_op(Op::Pop, line, Some(root));
4856                        self.compile_expr(value)?;
4857                        self.emit_op(Op::Pop, line, Some(root));
4858                        let idx = self
4859                            .chunk
4860                            .add_constant(StrykeValue::string("assign to empty hash slice".into()));
4861                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
4862                        self.emit_op(Op::LoadUndef, line, Some(root));
4863                        return Ok(());
4864                    }
4865                    if hash_slice_needs_slice_ops(keys) {
4866                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
4867                            let k = keys.len() as u16;
4868                            self.compile_expr(container)?;
4869                            for hk in keys {
4870                                self.compile_expr(hk)?;
4871                            }
4872                            self.emit_op(Op::HashSliceDerefPeekLast(k), line, Some(root));
4873                            let j = match *op {
4874                                BinOp::DefinedOr => {
4875                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4876                                }
4877                                BinOp::LogOr => {
4878                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4879                                }
4880                                BinOp::LogAnd => {
4881                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4882                                }
4883                                _ => unreachable!(),
4884                            };
4885                            self.compile_expr(value)?;
4886                            self.emit_op(Op::HashSliceDerefRollValUnderKeys(k), line, Some(root));
4887                            self.emit_op(Op::HashSliceDerefSetLastKeep(k), line, Some(root));
4888                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4889                            self.chunk.patch_jump_here(j);
4890                            self.emit_op(Op::HashSliceDerefDropKeysKeepCur(k), line, Some(root));
4891                            self.chunk.patch_jump_here(j_end);
4892                            return Ok(());
4893                        }
4894                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
4895                            CompileError::Unsupported(
4896                                "CompoundAssign op on multi-key hash slice".into(),
4897                            )
4898                        })?;
4899                        self.compile_expr(value)?;
4900                        self.compile_expr(container)?;
4901                        for hk in keys {
4902                            self.compile_expr(hk)?;
4903                        }
4904                        self.emit_op(
4905                            Op::HashSliceDerefCompound(op_byte, keys.len() as u16),
4906                            line,
4907                            Some(root),
4908                        );
4909                        return Ok(());
4910                    }
4911                    let hk = &keys[0];
4912                    match op {
4913                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
4914                            self.compile_expr(container)?;
4915                            self.compile_expr(hk)?;
4916                            self.emit_op(Op::Dup2, line, Some(root));
4917                            self.emit_op(Op::ArrowHash, line, Some(root));
4918                            let j = match *op {
4919                                BinOp::DefinedOr => {
4920                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4921                                }
4922                                BinOp::LogOr => {
4923                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4924                                }
4925                                BinOp::LogAnd => {
4926                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4927                                }
4928                                _ => unreachable!(),
4929                            };
4930                            self.compile_expr(value)?;
4931                            self.emit_op(Op::Swap, line, Some(root));
4932                            self.emit_op(Op::Rot, line, Some(root));
4933                            self.emit_op(Op::Swap, line, Some(root));
4934                            self.emit_op(Op::SetArrowHashKeep, line, Some(root));
4935                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
4936                            self.chunk.patch_jump_here(j);
4937                            self.emit_op(Op::Swap, line, Some(root));
4938                            self.emit_op(Op::Pop, line, Some(root));
4939                            self.emit_op(Op::Swap, line, Some(root));
4940                            self.emit_op(Op::Pop, line, Some(root));
4941                            self.chunk.patch_jump_here(j_end);
4942                        }
4943                        _ => {
4944                            let vm_op = binop_to_vm_op(*op).ok_or_else(|| {
4945                                CompileError::Unsupported("CompoundAssign op".into())
4946                            })?;
4947                            self.compile_expr(container)?;
4948                            self.compile_expr(hk)?;
4949                            self.emit_op(Op::Dup2, line, Some(root));
4950                            self.emit_op(Op::ArrowHash, line, Some(root));
4951                            self.compile_expr(value)?;
4952                            self.emit_op(vm_op, line, Some(root));
4953                            self.emit_op(Op::Swap, line, Some(root));
4954                            self.emit_op(Op::Rot, line, Some(root));
4955                            self.emit_op(Op::Swap, line, Some(root));
4956                            self.emit_op(Op::SetArrowHash, line, Some(root));
4957                        }
4958                    }
4959                } else if let ExprKind::HashSlice { hash, keys } = &target.kind {
4960                    if keys.is_empty() {
4961                        if self.is_mysync_hash(hash) {
4962                            return Err(CompileError::Unsupported(
4963                                "mysync hash slice update".into(),
4964                            ));
4965                        }
4966                        self.check_strict_hash_access(hash, line)?;
4967                        self.check_hash_mutable(hash, line)?;
4968                        self.compile_expr(value)?;
4969                        self.emit_op(Op::Pop, line, Some(root));
4970                        let idx = self
4971                            .chunk
4972                            .add_constant(StrykeValue::string("assign to empty hash slice".into()));
4973                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
4974                        self.emit_op(Op::LoadUndef, line, Some(root));
4975                        return Ok(());
4976                    }
4977                    if self.is_mysync_hash(hash) {
4978                        return Err(CompileError::Unsupported("mysync hash slice update".into()));
4979                    }
4980                    self.check_strict_hash_access(hash, line)?;
4981                    self.check_hash_mutable(hash, line)?;
4982                    let hash_idx = self.chunk.intern_name(hash);
4983                    if hash_slice_needs_slice_ops(keys) {
4984                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
4985                            let k = keys.len() as u16;
4986                            for hk in keys {
4987                                self.compile_expr(hk)?;
4988                            }
4989                            self.emit_op(Op::NamedHashSlicePeekLast(hash_idx, k), line, Some(root));
4990                            let j = match *op {
4991                                BinOp::DefinedOr => {
4992                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
4993                                }
4994                                BinOp::LogOr => {
4995                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
4996                                }
4997                                BinOp::LogAnd => {
4998                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
4999                                }
5000                                _ => unreachable!(),
5001                            };
5002                            self.compile_expr(value)?;
5003                            self.emit_op(Op::NamedArraySliceRollValUnderSpecs(k), line, Some(root));
5004                            self.emit_op(
5005                                Op::SetNamedHashSliceLastKeep(hash_idx, k),
5006                                line,
5007                                Some(root),
5008                            );
5009                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5010                            self.chunk.patch_jump_here(j);
5011                            self.emit_op(Op::NamedHashSliceDropKeysKeepCur(k), line, Some(root));
5012                            self.chunk.patch_jump_here(j_end);
5013                            return Ok(());
5014                        }
5015                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5016                            CompileError::Unsupported(
5017                                "CompoundAssign op on multi-key hash slice".into(),
5018                            )
5019                        })?;
5020                        self.compile_expr(value)?;
5021                        for hk in keys {
5022                            self.compile_expr(hk)?;
5023                        }
5024                        self.emit_op(
5025                            Op::NamedHashSliceCompound(op_byte, hash_idx, keys.len() as u16),
5026                            line,
5027                            Some(root),
5028                        );
5029                        return Ok(());
5030                    }
5031                    let hk = &keys[0];
5032                    match op {
5033                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5034                            self.compile_expr(hk)?;
5035                            self.emit_op(Op::Dup, line, Some(root));
5036                            self.emit_op(Op::GetHashElem(hash_idx), line, Some(root));
5037                            let j = match *op {
5038                                BinOp::DefinedOr => {
5039                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5040                                }
5041                                BinOp::LogOr => {
5042                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5043                                }
5044                                BinOp::LogAnd => {
5045                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5046                                }
5047                                _ => unreachable!(),
5048                            };
5049                            self.compile_expr(value)?;
5050                            self.emit_op(Op::Swap, line, Some(root));
5051                            self.emit_op(Op::SetHashElemKeep(hash_idx), line, Some(root));
5052                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5053                            self.chunk.patch_jump_here(j);
5054                            self.emit_op(Op::Swap, line, Some(root));
5055                            self.emit_op(Op::Pop, line, Some(root));
5056                            self.chunk.patch_jump_here(j_end);
5057                        }
5058                        _ => {
5059                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5060                                CompileError::Unsupported("CompoundAssign op".into())
5061                            })?;
5062                            self.compile_expr(value)?;
5063                            self.compile_expr(hk)?;
5064                            self.emit_op(
5065                                Op::NamedHashSliceCompound(op_byte, hash_idx, 1),
5066                                line,
5067                                Some(root),
5068                            );
5069                        }
5070                    }
5071                } else if let ExprKind::ArraySlice { array, indices } = &target.kind {
5072                    if indices.is_empty() {
5073                        if self.is_mysync_array(array) {
5074                            return Err(CompileError::Unsupported(
5075                                "mysync array slice update".into(),
5076                            ));
5077                        }
5078                        let q = self.qualify_stash_array_name(array);
5079                        self.check_array_mutable(&q, line)?;
5080                        let arr_idx = self.chunk.intern_name(&q);
5081                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5082                            self.compile_expr(value)?;
5083                            self.emit_op(Op::Pop, line, Some(root));
5084                            let idx = self.chunk.add_constant(StrykeValue::string(
5085                                "assign to empty array slice".into(),
5086                            ));
5087                            self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5088                            self.emit_op(Op::LoadUndef, line, Some(root));
5089                            return Ok(());
5090                        }
5091                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5092                            CompileError::Unsupported(
5093                                "CompoundAssign op on named array slice".into(),
5094                            )
5095                        })?;
5096                        self.compile_expr(value)?;
5097                        self.emit_op(
5098                            Op::NamedArraySliceCompound(op_byte, arr_idx, 0),
5099                            line,
5100                            Some(root),
5101                        );
5102                        return Ok(());
5103                    }
5104                    if self.is_mysync_array(array) {
5105                        return Err(CompileError::Unsupported(
5106                            "mysync array slice update".into(),
5107                        ));
5108                    }
5109                    let q = self.qualify_stash_array_name(array);
5110                    self.check_array_mutable(&q, line)?;
5111                    let arr_idx = self.chunk.intern_name(&q);
5112                    if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5113                        let k = indices.len() as u16;
5114                        for ix in indices {
5115                            self.compile_array_slice_index_expr(ix)?;
5116                        }
5117                        self.emit_op(Op::NamedArraySlicePeekLast(arr_idx, k), line, Some(root));
5118                        let j = match *op {
5119                            BinOp::DefinedOr => {
5120                                self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5121                            }
5122                            BinOp::LogOr => self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root)),
5123                            BinOp::LogAnd => self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root)),
5124                            _ => unreachable!(),
5125                        };
5126                        self.compile_expr(value)?;
5127                        self.emit_op(Op::NamedArraySliceRollValUnderSpecs(k), line, Some(root));
5128                        self.emit_op(Op::SetNamedArraySliceLastKeep(arr_idx, k), line, Some(root));
5129                        let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5130                        self.chunk.patch_jump_here(j);
5131                        self.emit_op(Op::NamedArraySliceDropKeysKeepCur(k), line, Some(root));
5132                        self.chunk.patch_jump_here(j_end);
5133                        return Ok(());
5134                    }
5135                    let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5136                        CompileError::Unsupported("CompoundAssign op on named array slice".into())
5137                    })?;
5138                    self.compile_expr(value)?;
5139                    for ix in indices {
5140                        self.compile_array_slice_index_expr(ix)?;
5141                    }
5142                    self.emit_op(
5143                        Op::NamedArraySliceCompound(op_byte, arr_idx, indices.len() as u16),
5144                        line,
5145                        Some(root),
5146                    );
5147                    return Ok(());
5148                } else if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
5149                    let ExprKind::Deref {
5150                        expr: inner,
5151                        kind: Sigil::Array,
5152                    } = &source.kind
5153                    else {
5154                        return Err(CompileError::Unsupported(
5155                            "CompoundAssign on AnonymousListSlice (non-array deref)".into(),
5156                        ));
5157                    };
5158                    if indices.is_empty() {
5159                        self.compile_arrow_array_base_expr(inner)?;
5160                        self.emit_op(Op::Pop, line, Some(root));
5161                        self.compile_expr(value)?;
5162                        self.emit_op(Op::Pop, line, Some(root));
5163                        let idx = self.chunk.add_constant(StrykeValue::string(
5164                            "assign to empty array slice".into(),
5165                        ));
5166                        self.emit_op(Op::RuntimeErrorConst(idx), line, Some(root));
5167                        self.emit_op(Op::LoadUndef, line, Some(root));
5168                        return Ok(());
5169                    }
5170                    if indices.len() > 1 {
5171                        if matches!(op, BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd) {
5172                            let k = indices.len() as u16;
5173                            self.compile_arrow_array_base_expr(inner)?;
5174                            for ix in indices {
5175                                self.compile_array_slice_index_expr(ix)?;
5176                            }
5177                            self.emit_op(Op::ArrowArraySlicePeekLast(k), line, Some(root));
5178                            let j = match *op {
5179                                BinOp::DefinedOr => {
5180                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5181                                }
5182                                BinOp::LogOr => {
5183                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5184                                }
5185                                BinOp::LogAnd => {
5186                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5187                                }
5188                                _ => unreachable!(),
5189                            };
5190                            self.compile_expr(value)?;
5191                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(k), line, Some(root));
5192                            self.emit_op(Op::SetArrowArraySliceLastKeep(k), line, Some(root));
5193                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5194                            self.chunk.patch_jump_here(j);
5195                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(k), line, Some(root));
5196                            self.chunk.patch_jump_here(j_end);
5197                            return Ok(());
5198                        }
5199                        let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5200                            CompileError::Unsupported(
5201                                "CompoundAssign op on multi-index array slice".into(),
5202                            )
5203                        })?;
5204                        self.compile_expr(value)?;
5205                        self.compile_arrow_array_base_expr(inner)?;
5206                        for ix in indices {
5207                            self.compile_array_slice_index_expr(ix)?;
5208                        }
5209                        self.emit_op(
5210                            Op::ArrowArraySliceCompound(op_byte, indices.len() as u16),
5211                            line,
5212                            Some(root),
5213                        );
5214                        return Ok(());
5215                    }
5216                    let ix0 = &indices[0];
5217                    match op {
5218                        BinOp::DefinedOr | BinOp::LogOr | BinOp::LogAnd => {
5219                            self.compile_arrow_array_base_expr(inner)?;
5220                            self.compile_array_slice_index_expr(ix0)?;
5221                            self.emit_op(Op::ArrowArraySlicePeekLast(1), line, Some(root));
5222                            let j = match *op {
5223                                BinOp::DefinedOr => {
5224                                    self.emit_op(Op::JumpIfDefinedKeep(0), line, Some(root))
5225                                }
5226                                BinOp::LogOr => {
5227                                    self.emit_op(Op::JumpIfTrueKeep(0), line, Some(root))
5228                                }
5229                                BinOp::LogAnd => {
5230                                    self.emit_op(Op::JumpIfFalseKeep(0), line, Some(root))
5231                                }
5232                                _ => unreachable!(),
5233                            };
5234                            self.compile_expr(value)?;
5235                            self.emit_op(Op::ArrowArraySliceRollValUnderSpecs(1), line, Some(root));
5236                            self.emit_op(Op::SetArrowArraySliceLastKeep(1), line, Some(root));
5237                            let j_end = self.emit_op(Op::Jump(0), line, Some(root));
5238                            self.chunk.patch_jump_here(j);
5239                            self.emit_op(Op::ArrowArraySliceDropKeysKeepCur(1), line, Some(root));
5240                            self.chunk.patch_jump_here(j_end);
5241                        }
5242                        _ => {
5243                            let op_byte = scalar_compound_op_to_byte(*op).ok_or_else(|| {
5244                                CompileError::Unsupported("CompoundAssign op".into())
5245                            })?;
5246                            self.compile_expr(value)?;
5247                            self.compile_arrow_array_base_expr(inner)?;
5248                            self.compile_array_slice_index_expr(ix0)?;
5249                            self.emit_op(Op::ArrowArraySliceCompound(op_byte, 1), line, Some(root));
5250                        }
5251                    }
5252                } else {
5253                    return Err(CompileError::Unsupported(
5254                        "CompoundAssign on non-scalar".into(),
5255                    ));
5256                }
5257            }
5258
5259            ExprKind::Ternary {
5260                condition,
5261                then_expr,
5262                else_expr,
5263            } => {
5264                self.compile_boolean_rvalue_condition(condition)?;
5265                let jump_else = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
5266                self.compile_expr_ctx(then_expr, ctx)?;
5267                let jump_end = self.emit_op(Op::Jump(0), line, Some(root));
5268                self.chunk.patch_jump_here(jump_else);
5269                self.compile_expr_ctx(else_expr, ctx)?;
5270                self.chunk.patch_jump_here(jump_end);
5271            }
5272
5273            ExprKind::Range {
5274                from,
5275                to,
5276                exclusive,
5277                step,
5278            } => {
5279                if ctx == WantarrayCtx::List {
5280                    self.compile_expr_ctx(from, WantarrayCtx::Scalar)?;
5281                    self.compile_expr_ctx(to, WantarrayCtx::Scalar)?;
5282                    if let Some(s) = step {
5283                        self.compile_expr_ctx(s, WantarrayCtx::Scalar)?;
5284                        self.emit_op(Op::RangeStep, line, Some(root));
5285                    } else {
5286                        self.emit_op(Op::Range, line, Some(root));
5287                    }
5288                } else if let (ExprKind::Regex(lp, lf), ExprKind::Regex(rp, rf)) =
5289                    (&from.kind, &to.kind)
5290                {
5291                    let slot = self.chunk.alloc_flip_flop_slot();
5292                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5293                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5294                    let rp_idx = self.chunk.add_constant(StrykeValue::string(rp.clone()));
5295                    let rf_idx = self.chunk.add_constant(StrykeValue::string(rf.clone()));
5296                    self.emit_op(
5297                        Op::RegexFlipFlop(
5298                            slot,
5299                            u8::from(*exclusive),
5300                            lp_idx,
5301                            lf_idx,
5302                            rp_idx,
5303                            rf_idx,
5304                        ),
5305                        line,
5306                        Some(root),
5307                    );
5308                } else if let (ExprKind::Regex(lp, lf), ExprKind::Eof(None)) =
5309                    (&from.kind, &to.kind)
5310                {
5311                    let slot = self.chunk.alloc_flip_flop_slot();
5312                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5313                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5314                    self.emit_op(
5315                        Op::RegexEofFlipFlop(slot, u8::from(*exclusive), lp_idx, lf_idx),
5316                        line,
5317                        Some(root),
5318                    );
5319                } else if matches!(
5320                    (&from.kind, &to.kind),
5321                    (ExprKind::Regex(_, _), ExprKind::Eof(Some(_)))
5322                ) {
5323                    return Err(CompileError::Unsupported(
5324                        "regex flip-flop with eof(HANDLE) is not supported".into(),
5325                    ));
5326                } else if let ExprKind::Regex(lp, lf) = &from.kind {
5327                    let slot = self.chunk.alloc_flip_flop_slot();
5328                    let lp_idx = self.chunk.add_constant(StrykeValue::string(lp.clone()));
5329                    let lf_idx = self.chunk.add_constant(StrykeValue::string(lf.clone()));
5330                    if matches!(to.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
5331                        let line_target = match &to.kind {
5332                            ExprKind::Integer(n) => *n,
5333                            ExprKind::Float(f) => *f as i64,
5334                            _ => unreachable!(),
5335                        };
5336                        let line_cidx = self.chunk.add_constant(StrykeValue::integer(line_target));
5337                        self.emit_op(
5338                            Op::RegexFlipFlopDotLineRhs(
5339                                slot,
5340                                u8::from(*exclusive),
5341                                lp_idx,
5342                                lf_idx,
5343                                line_cidx,
5344                            ),
5345                            line,
5346                            Some(root),
5347                        );
5348                    } else {
5349                        let rhs_idx = self
5350                            .chunk
5351                            .add_regex_flip_flop_rhs_expr_entry((**to).clone());
5352                        self.emit_op(
5353                            Op::RegexFlipFlopExprRhs(
5354                                slot,
5355                                u8::from(*exclusive),
5356                                lp_idx,
5357                                lf_idx,
5358                                rhs_idx,
5359                            ),
5360                            line,
5361                            Some(root),
5362                        );
5363                    }
5364                } else {
5365                    self.compile_expr(from)?;
5366                    self.compile_expr(to)?;
5367                    let slot = self.chunk.alloc_flip_flop_slot();
5368                    self.emit_op(
5369                        Op::ScalarFlipFlop(slot, u8::from(*exclusive)),
5370                        line,
5371                        Some(root),
5372                    );
5373                }
5374            }
5375
5376            ExprKind::SliceRange { .. } => {
5377                // Open-ended slice ranges (`:N`, `N:`, `::-1`, `::`) only have meaning
5378                // inside slice subscripts (`@arr[...]`, `@h{...}`), where they are
5379                // intercepted by the slice arms above. Anywhere else is a hard error —
5380                // we have no container length context to resolve open ends.
5381                return Err(CompileError::Unsupported(
5382                    "open-ended slice range (`:N`/`N:`/`::-1`) is only valid inside `@arr[...]` or `@h{...}` subscripts"
5383                        .into(),
5384                ));
5385            }
5386
5387            ExprKind::Repeat {
5388                expr,
5389                count,
5390                list_repeat,
5391            } => {
5392                if *list_repeat {
5393                    // List context for the LHS so `(EXPR)` and `qw(...)` flatten
5394                    // into the array we'll replicate.
5395                    self.compile_expr_ctx(expr, WantarrayCtx::List)?;
5396                    self.compile_expr(count)?;
5397                    self.emit_op(Op::ListRepeat, line, Some(root));
5398                } else {
5399                    self.compile_expr(expr)?;
5400                    self.compile_expr(count)?;
5401                    self.emit_op(Op::StringRepeat, line, Some(root));
5402                }
5403            }
5404
5405            // ── Function calls ──
5406            ExprKind::FuncCall { name, args } => {
5407                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
5408                // bare-name fast path so the arms below stay flat.
5409                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
5410                match dispatch_name {
5411                    // read(FH, $buf, LEN) — emit ReadIntoVar with the buffer variable's name index
5412                    "read" => {
5413                        if args.len() < 3 {
5414                            return Err(CompileError::Unsupported(
5415                                "read() needs at least 3 args".into(),
5416                            ));
5417                        }
5418                        // Extract buffer variable name from 2nd arg
5419                        let buf_name =
5420                            match &args[1].kind {
5421                                ExprKind::ScalarVar(n) => n.clone(),
5422                                _ => return Err(CompileError::Unsupported(
5423                                    "read() buffer must be a simple scalar variable for bytecode"
5424                                        .into(),
5425                                )),
5426                            };
5427                        let buf_idx = self.chunk.intern_name(&buf_name);
5428                        // Stack: [filehandle, length]
5429                        self.compile_expr(&args[0])?; // filehandle
5430                        self.compile_expr(&args[2])?; // length
5431                        self.emit_op(Op::ReadIntoVar(buf_idx), line, Some(root));
5432                    }
5433                    // `defer { BLOCK }` — desugared by parser to `defer__internal(fn { BLOCK })`
5434                    "defer__internal" => {
5435                        if args.len() != 1 {
5436                            return Err(CompileError::Unsupported(
5437                                "defer__internal expects exactly one argument".into(),
5438                            ));
5439                        }
5440                        // Compile the coderef argument; afterwards, un-mark
5441                        // the defer block as a sub-body so the closure-write
5442                        // check (DESIGN-001) doesn't flag mutations of outer
5443                        // `my` vars from inside `defer { ... }`. defer runs
5444                        // synchronously at scope exit and is intentionally
5445                        // shared-state with the enclosing scope.
5446                        self.compile_expr(&args[0])?;
5447                        if let ExprKind::CodeRef { .. } = &args[0].kind {
5448                            // The most-recently-pushed CodeRef block index is
5449                            // the highest one in `sub_body_block_indices`.
5450                            if let Some(max_idx) = self.sub_body_block_indices.iter().copied().max()
5451                            {
5452                                self.sub_body_block_indices.remove(&max_idx);
5453                            }
5454                        }
5455                        self.emit_op(Op::DeferBlock, line, Some(root));
5456                    }
5457                    "deque" => {
5458                        if !args.is_empty() {
5459                            return Err(CompileError::Unsupported(
5460                                "deque() takes no arguments".into(),
5461                            ));
5462                        }
5463                        self.emit_op(
5464                            Op::CallBuiltin(BuiltinId::DequeNew as u16, 0),
5465                            line,
5466                            Some(root),
5467                        );
5468                    }
5469                    "inc" => {
5470                        let arg = args.first().cloned().unwrap_or_else(|| Expr {
5471                            kind: ExprKind::ScalarVar("_".into()),
5472                            line,
5473                        });
5474                        self.compile_expr(&arg)?;
5475                        self.emit_op(Op::Inc, line, Some(root));
5476                    }
5477                    "dec" => {
5478                        let arg = args.first().cloned().unwrap_or_else(|| Expr {
5479                            kind: ExprKind::ScalarVar("_".into()),
5480                            line,
5481                        });
5482                        self.compile_expr(&arg)?;
5483                        self.emit_op(Op::Dec, line, Some(root));
5484                    }
5485                    "heap" => {
5486                        if args.len() != 1 {
5487                            return Err(CompileError::Unsupported(
5488                                "heap() expects one comparator sub".into(),
5489                            ));
5490                        }
5491                        self.compile_expr(&args[0])?;
5492                        self.emit_op(
5493                            Op::CallBuiltin(BuiltinId::HeapNew as u16, 1),
5494                            line,
5495                            Some(root),
5496                        );
5497                    }
5498                    "pipeline" => {
5499                        for arg in args {
5500                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5501                        }
5502                        self.emit_op(
5503                            Op::CallBuiltin(BuiltinId::Pipeline as u16, args.len() as u8),
5504                            line,
5505                            Some(root),
5506                        );
5507                    }
5508                    "par_pipeline" => {
5509                        for arg in args {
5510                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5511                        }
5512                        self.emit_op(
5513                            Op::CallBuiltin(BuiltinId::ParPipeline as u16, args.len() as u8),
5514                            line,
5515                            Some(root),
5516                        );
5517                    }
5518                    "par_pipeline_stream" => {
5519                        for arg in args {
5520                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5521                        }
5522                        self.emit_op(
5523                            Op::CallBuiltin(BuiltinId::ParPipelineStream as u16, args.len() as u8),
5524                            line,
5525                            Some(root),
5526                        );
5527                    }
5528                    // `collect(EXPR)` — compile the argument in list context so nested
5529                    // `map { }` / `grep { }` keep a pipeline handle (scalar context adds
5530                    // `StackArrayLen`, which turns a pipeline into `1`). At runtime, a
5531                    // pipeline runs staged ops; any other value is materialized as an array
5532                    // (`|> … |> collect()`).
5533                    "collect" => {
5534                        for arg in args {
5535                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5536                        }
5537                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5538                        self.emit_op(
5539                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5540                            line,
5541                            Some(root),
5542                        );
5543                    }
5544                    "ppool" => {
5545                        if args.len() != 1 {
5546                            return Err(CompileError::Unsupported(
5547                                "ppool() expects one argument (worker count)".into(),
5548                            ));
5549                        }
5550                        self.compile_expr(&args[0])?;
5551                        self.emit_op(
5552                            Op::CallBuiltin(BuiltinId::Ppool as u16, 1),
5553                            line,
5554                            Some(root),
5555                        );
5556                    }
5557                    "barrier" => {
5558                        if args.len() != 1 {
5559                            return Err(CompileError::Unsupported(
5560                                "barrier() expects one argument (party count)".into(),
5561                            ));
5562                        }
5563                        self.compile_expr(&args[0])?;
5564                        self.emit_op(
5565                            Op::CallBuiltin(BuiltinId::BarrierNew as u16, 1),
5566                            line,
5567                            Some(root),
5568                        );
5569                    }
5570                    "cluster" => {
5571                        // Each arg pushed in list context so an `@hosts`
5572                        // operand flattens into individual slot specs and
5573                        // a single bareword `cluster("host1:4")` arrives
5574                        // as one string. `ClusterNew` mirrors the
5575                        // tree-walker arm in `vm_helper::call_named_sub`.
5576                        if args.is_empty() {
5577                            return Err(CompileError::Unsupported(
5578                                "cluster() expects at least one host/slot specifier".into(),
5579                            ));
5580                        }
5581                        for arg in args {
5582                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5583                        }
5584                        self.emit_op(
5585                            Op::CallBuiltin(BuiltinId::ClusterNew as u16, args.len() as u8),
5586                            line,
5587                            Some(root),
5588                        );
5589                    }
5590                    "pselect" => {
5591                        if args.is_empty() {
5592                            return Err(CompileError::Unsupported(
5593                                "pselect() expects at least one pchannel receiver".into(),
5594                            ));
5595                        }
5596                        for arg in args {
5597                            self.compile_expr(arg)?;
5598                        }
5599                        self.emit_op(
5600                            Op::CallBuiltin(BuiltinId::Pselect as u16, args.len() as u8),
5601                            line,
5602                            Some(root),
5603                        );
5604                    }
5605                    "ssh" => {
5606                        for arg in args {
5607                            self.compile_expr(arg)?;
5608                        }
5609                        self.emit_op(
5610                            Op::CallBuiltin(BuiltinId::Ssh as u16, args.len() as u8),
5611                            line,
5612                            Some(root),
5613                        );
5614                    }
5615                    "rmdir" => {
5616                        for arg in args {
5617                            self.compile_expr(arg)?;
5618                        }
5619                        self.emit_op(
5620                            Op::CallBuiltin(BuiltinId::Rmdir as u16, args.len() as u8),
5621                            line,
5622                            Some(root),
5623                        );
5624                    }
5625                    "utime" => {
5626                        for arg in args {
5627                            self.compile_expr(arg)?;
5628                        }
5629                        self.emit_op(
5630                            Op::CallBuiltin(BuiltinId::Utime as u16, args.len() as u8),
5631                            line,
5632                            Some(root),
5633                        );
5634                    }
5635                    "umask" => {
5636                        for arg in args {
5637                            self.compile_expr(arg)?;
5638                        }
5639                        self.emit_op(
5640                            Op::CallBuiltin(BuiltinId::Umask as u16, args.len() as u8),
5641                            line,
5642                            Some(root),
5643                        );
5644                    }
5645                    "getcwd" => {
5646                        for arg in args {
5647                            self.compile_expr(arg)?;
5648                        }
5649                        self.emit_op(
5650                            Op::CallBuiltin(BuiltinId::Getcwd as u16, args.len() as u8),
5651                            line,
5652                            Some(root),
5653                        );
5654                    }
5655                    "pipe" => {
5656                        if args.len() != 2 {
5657                            return Err(CompileError::Unsupported(
5658                                "pipe requires exactly two arguments".into(),
5659                            ));
5660                        }
5661                        for arg in args {
5662                            self.compile_expr(arg)?;
5663                        }
5664                        self.emit_op(Op::CallBuiltin(BuiltinId::Pipe as u16, 2), line, Some(root));
5665                    }
5666                    "uniq" | "distinct" | "flatten" | "set" | "with_index" | "list_count"
5667                    | "list_size" | "count" | "size" | "cnt" | "len" | "sum" | "sum0"
5668                    | "product" | "min" | "max" | "mean" | "median" | "mode" | "stddev"
5669                    | "variance" => {
5670                        // Fast path for `len @arr` / `cnt @arr` / `count @arr` and the deref
5671                        // variants `len @$ref` / `len @{$ref}`: emit the same direct length op
5672                        // (`ArrayLen` / `ArrayDerefLen`) that `scalar @arr` uses, so the
5673                        // idiomatic stryke spelling is no slower than the Perl-compat one.
5674                        if matches!(
5675                            name.as_str(),
5676                            "count" | "cnt" | "size" | "len" | "list_count" | "list_size"
5677                        ) && args.len() == 1
5678                        {
5679                            match &args[0].kind {
5680                                ExprKind::ArrayVar(arr_name) => {
5681                                    self.check_strict_array_access(arr_name, line)?;
5682                                    let idx = self
5683                                        .chunk
5684                                        .intern_name(&self.qualify_stash_array_name(arr_name));
5685                                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
5686                                    return Ok(());
5687                                }
5688                                ExprKind::Deref {
5689                                    expr,
5690                                    kind: Sigil::Array,
5691                                } => {
5692                                    self.compile_expr(expr)?;
5693                                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
5694                                    return Ok(());
5695                                }
5696                                _ => {}
5697                            }
5698                        }
5699                        for arg in args {
5700                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5701                        }
5702                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5703                        self.emit_op(
5704                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5705                            line,
5706                            Some(root),
5707                        );
5708                    }
5709                    "shuffle" => {
5710                        for arg in args {
5711                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5712                        }
5713                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5714                        self.emit_op(
5715                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5716                            line,
5717                            Some(root),
5718                        );
5719                    }
5720                    "chunked" | "windowed" => {
5721                        match args.len() {
5722                            0 => {
5723                                return Err(CompileError::Unsupported(
5724                                "chunked/windowed need (LIST, N) or unary N (e.g. `|> chunked(2)`)"
5725                                    .into(),
5726                            ));
5727                            }
5728                            1 => {
5729                                // chunked @l / windowed @l — compile in list context, default size
5730                                self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5731                            }
5732                            2 => {
5733                                self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5734                                self.compile_expr(&args[1])?;
5735                            }
5736                            _ => {
5737                                return Err(CompileError::Unsupported(
5738                                "chunked/windowed expect exactly two arguments (LIST, N); use a single list expression for the first operand".into(),
5739                            ));
5740                            }
5741                        }
5742                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5743                        self.emit_op(
5744                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5745                            line,
5746                            Some(root),
5747                        );
5748                    }
5749                    "take" | "head" | "tail" | "drop" => {
5750                        if args.is_empty() {
5751                            return Err(CompileError::Unsupported(
5752                                "take/head/tail/drop expect LIST..., N or unary N".into(),
5753                            ));
5754                        }
5755                        if args.len() == 1 {
5756                            // head @l == head @l, 1 — evaluate in list context
5757                            self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5758                        } else {
5759                            for a in &args[..args.len() - 1] {
5760                                self.compile_expr_ctx(a, WantarrayCtx::List)?;
5761                            }
5762                            self.compile_expr(&args[args.len() - 1])?;
5763                        }
5764                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5765                        self.emit_op(
5766                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5767                            line,
5768                            Some(root),
5769                        );
5770                    }
5771                    "any" | "all" | "none" | "first" | "take_while" | "drop_while" | "tap"
5772                    | "peek" => {
5773                        // Three shapes:
5774                        //   `any { BLOCK } @list`           — block form
5775                        //   `any(fn { ... }, 1, 2, 3)`      — slurpy `(&@)` form
5776                        //   `any($coderef, @list)`          — coderef-in-block-position
5777                        //   `any($f, @list)` (no parens)    — same, runtime dispatch
5778                        // Builtin runtime checks `as_code_ref()` and dispatches; if
5779                        // the first arg isn't a coderef the builtin uses its
5780                        // value-shape semantics. Compiler stays out of the way.
5781                        if args.is_empty() {
5782                            return Err(CompileError::Unsupported(
5783                            "any/all/none/first/take_while/drop_while/tap/peek expect BLOCK, LIST"
5784                                .into(),
5785                        ));
5786                        }
5787                        self.compile_expr(&args[0])?;
5788                        for arg in &args[1..] {
5789                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5790                        }
5791                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5792                        self.emit_op(
5793                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5794                            line,
5795                            Some(root),
5796                        );
5797                    }
5798                    "group_by" | "chunk_by" => {
5799                        if args.len() != 2 {
5800                            return Err(CompileError::Unsupported(
5801                                "group_by/chunk_by expect { BLOCK } or EXPR, LIST".into(),
5802                            ));
5803                        }
5804                        self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5805                        match &args[0].kind {
5806                            ExprKind::CodeRef { body, .. } => {
5807                                let block_idx = self.add_deferred_block(body.clone());
5808                                self.emit_op(Op::ChunkByWithBlock(block_idx), line, Some(root));
5809                            }
5810                            _ => {
5811                                let idx = self.chunk.add_map_expr_entry(args[0].clone());
5812                                self.emit_op(Op::ChunkByWithExpr(idx), line, Some(root));
5813                            }
5814                        }
5815                        if ctx != WantarrayCtx::List {
5816                            self.emit_op(Op::StackArrayLen, line, Some(root));
5817                        }
5818                    }
5819                    "zip" | "zip_longest" => {
5820                        for arg in args {
5821                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5822                        }
5823                        // Both forms are stryke bare-name builtins; the VM slow path strips the
5824                        // `main::` qualifier and routes through `try_builtin` → `dispatch_by_name`.
5825                        let name_idx = self.chunk.intern_name(&self.qualify_sub_key(dispatch_name));
5826                        self.emit_op(
5827                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5828                            line,
5829                            Some(root),
5830                        );
5831                    }
5832                    "puniq" => {
5833                        if args.is_empty() || args.len() > 2 {
5834                            return Err(CompileError::Unsupported(
5835                                "puniq expects LIST [, progress => EXPR]".into(),
5836                            ));
5837                        }
5838                        if args.len() == 2 {
5839                            self.compile_expr(&args[1])?;
5840                        } else {
5841                            self.emit_op(Op::LoadInt(0), line, Some(root));
5842                        }
5843                        self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5844                        self.emit_op(Op::Puniq, line, Some(root));
5845                        if ctx != WantarrayCtx::List {
5846                            self.emit_op(Op::StackArrayLen, line, Some(root));
5847                        }
5848                    }
5849                    "pfirst" | "pany" => {
5850                        if args.len() < 2 || args.len() > 3 {
5851                            return Err(CompileError::Unsupported(
5852                                "pfirst/pany expect BLOCK, LIST [, progress => EXPR]".into(),
5853                            ));
5854                        }
5855                        let body = match &args[0].kind {
5856                            ExprKind::CodeRef { body, .. } => body,
5857                            _ => {
5858                                return Err(CompileError::Unsupported(
5859                                    "pfirst/pany: first argument must be a { BLOCK }".into(),
5860                                ));
5861                            }
5862                        };
5863                        if args.len() == 3 {
5864                            self.compile_expr(&args[2])?;
5865                        } else {
5866                            self.emit_op(Op::LoadInt(0), line, Some(root));
5867                        }
5868                        self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5869                        let block_idx = self.add_deferred_block(body.clone());
5870                        let op = if name == "pfirst" {
5871                            Op::PFirstWithBlock(block_idx)
5872                        } else {
5873                            Op::PAnyWithBlock(block_idx)
5874                        };
5875                        self.emit_op(op, line, Some(root));
5876                    }
5877                    _ => {
5878                        // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
5879                        // `f(reverse LIST)` etc. flatten into `@_`. [`Self::pop_call_operands_flattened`]
5880                        // splats any array value at runtime, matching Perl's `@_` semantics.
5881                        for arg in args {
5882                            self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5883                        }
5884                        let q = self.qualify_sub_key(name);
5885                        let name_idx = self.chunk.intern_name(&q);
5886                        self.emit_op(
5887                            Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5888                            line,
5889                            Some(root),
5890                        );
5891                    }
5892                }
5893            }
5894
5895            // ── Method calls ──
5896            ExprKind::MethodCall {
5897                object,
5898                method,
5899                args,
5900                super_call,
5901            } => {
5902                self.compile_expr(object)?;
5903                for arg in args {
5904                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5905                }
5906                let name_idx = self.chunk.intern_name(method);
5907                if *super_call {
5908                    self.emit_op(
5909                        Op::MethodCallSuper(name_idx, args.len() as u8, ctx.as_byte()),
5910                        line,
5911                        Some(root),
5912                    );
5913                } else {
5914                    self.emit_op(
5915                        Op::MethodCall(name_idx, args.len() as u8, ctx.as_byte()),
5916                        line,
5917                        Some(root),
5918                    );
5919                }
5920            }
5921            ExprKind::IndirectCall {
5922                target,
5923                args,
5924                ampersand: _,
5925                pass_caller_arglist,
5926            } => {
5927                self.compile_expr(target)?;
5928                if !pass_caller_arglist {
5929                    for a in args {
5930                        self.compile_expr_ctx(a, WantarrayCtx::List)?;
5931                    }
5932                }
5933                let argc = if *pass_caller_arglist {
5934                    0
5935                } else {
5936                    args.len() as u8
5937                };
5938                self.emit_op(
5939                    Op::IndirectCall(
5940                        argc,
5941                        ctx.as_byte(),
5942                        if *pass_caller_arglist { 1 } else { 0 },
5943                    ),
5944                    line,
5945                    Some(root),
5946                );
5947            }
5948
5949            // ── Print / Say / Printf ──
5950            ExprKind::Print { handle, args } => {
5951                for arg in args {
5952                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5953                }
5954                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
5955                self.emit_op(Op::Print(h, args.len() as u8), line, Some(root));
5956            }
5957            ExprKind::Say { 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::Say(h, args.len() as u8), line, Some(root));
5963            }
5964            ExprKind::Printf { args, .. } => {
5965                // printf's format + arg list is Perl list context — ranges, arrays, and
5966                // `reverse`/`sort`/`grep` flatten into format argument positions.
5967                for arg in args {
5968                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5969                }
5970                self.emit_op(
5971                    Op::CallBuiltin(BuiltinId::Printf as u16, args.len() as u8),
5972                    line,
5973                    Some(root),
5974                );
5975            }
5976
5977            // ── Die / Warn ──
5978            ExprKind::Die(args) => {
5979                // die / warn take a list that gets stringified and concatenated — list context
5980                // so `die 1..5` matches Perl's "12345" stringification.
5981                for arg in args {
5982                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5983                }
5984                self.emit_op(
5985                    Op::CallBuiltin(BuiltinId::Die as u16, args.len() as u8),
5986                    line,
5987                    Some(root),
5988                );
5989            }
5990            ExprKind::Warn(args) => {
5991                for arg in args {
5992                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5993                }
5994                self.emit_op(
5995                    Op::CallBuiltin(BuiltinId::Warn as u16, args.len() as u8),
5996                    line,
5997                    Some(root),
5998                );
5999            }
6000            ExprKind::Exit(code) => {
6001                if let Some(c) = code {
6002                    self.compile_expr(c)?;
6003                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
6004                } else {
6005                    self.emit_op(Op::LoadInt(0), line, Some(root));
6006                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
6007                }
6008            }
6009
6010            // ── Array ops ──
6011            ExprKind::Push { array, values } => {
6012                if let ExprKind::ArrayVar(name) = &array.kind {
6013                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6014                    for v in values {
6015                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
6016                        self.emit_op(Op::PushArray(idx), line, Some(root));
6017                    }
6018                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
6019                } else if let ExprKind::Deref {
6020                    expr: aref_expr,
6021                    kind: Sigil::Array,
6022                } = &array.kind
6023                {
6024                    // Autovivifiable inner shapes (`$x`, `$h{k}`, `$a[i]`) need lvalue
6025                    // resolution: when the slot is undef, `push @{...}` must create a new
6026                    // arrayref and store it back. Routed through PushExpr where
6027                    // `try_eval_array_deref_container` handles autoviv.
6028                    let needs_autoviv = matches!(
6029                        &aref_expr.kind,
6030                        ExprKind::ScalarVar(_)
6031                            | ExprKind::HashElement { .. }
6032                            | ExprKind::ArrayElement { .. }
6033                    );
6034                    if needs_autoviv {
6035                        let pool = self
6036                            .chunk
6037                            .add_push_expr_entry(array.as_ref().clone(), values.clone());
6038                        self.emit_op(Op::PushExpr(pool), line, Some(root));
6039                    } else {
6040                        self.compile_expr(aref_expr)?;
6041                        for v in values {
6042                            self.emit_op(Op::Dup, line, Some(root));
6043                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
6044                            self.emit_op(Op::PushArrayDeref, line, Some(root));
6045                        }
6046                        self.emit_op(Op::ArrayDerefLen, line, Some(root));
6047                    }
6048                } else {
6049                    let pool = self
6050                        .chunk
6051                        .add_push_expr_entry(array.as_ref().clone(), values.clone());
6052                    self.emit_op(Op::PushExpr(pool), line, Some(root));
6053                }
6054            }
6055            ExprKind::Pop(array) => {
6056                if let ExprKind::ArrayVar(name) = &array.kind {
6057                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6058                    self.emit_op(Op::PopArray(idx), line, Some(root));
6059                } else if let ExprKind::Deref {
6060                    expr: aref_expr,
6061                    kind: Sigil::Array,
6062                } = &array.kind
6063                {
6064                    let needs_autoviv = matches!(
6065                        &aref_expr.kind,
6066                        ExprKind::ScalarVar(_)
6067                            | ExprKind::HashElement { .. }
6068                            | ExprKind::ArrayElement { .. }
6069                    );
6070                    if needs_autoviv {
6071                        let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
6072                        self.emit_op(Op::PopExpr(pool), line, Some(root));
6073                    } else {
6074                        self.compile_expr(aref_expr)?;
6075                        self.emit_op(Op::PopArrayDeref, line, Some(root));
6076                    }
6077                } else {
6078                    let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
6079                    self.emit_op(Op::PopExpr(pool), line, Some(root));
6080                }
6081            }
6082            ExprKind::Shift(array) => {
6083                if let ExprKind::ArrayVar(name) = &array.kind {
6084                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6085                    self.emit_op(Op::ShiftArray(idx), line, Some(root));
6086                } else if let ExprKind::Deref {
6087                    expr: aref_expr,
6088                    kind: Sigil::Array,
6089                } = &array.kind
6090                {
6091                    let needs_autoviv = matches!(
6092                        &aref_expr.kind,
6093                        ExprKind::ScalarVar(_)
6094                            | ExprKind::HashElement { .. }
6095                            | ExprKind::ArrayElement { .. }
6096                    );
6097                    if needs_autoviv {
6098                        let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
6099                        self.emit_op(Op::ShiftExpr(pool), line, Some(root));
6100                    } else {
6101                        self.compile_expr(aref_expr)?;
6102                        self.emit_op(Op::ShiftArrayDeref, line, Some(root));
6103                    }
6104                } else {
6105                    let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
6106                    self.emit_op(Op::ShiftExpr(pool), line, Some(root));
6107                }
6108            }
6109            ExprKind::Unshift { array, values } => {
6110                if let ExprKind::ArrayVar(name) = &array.kind {
6111                    let q = self.qualify_stash_array_name(name);
6112                    let name_const = self.chunk.add_constant(StrykeValue::string(q));
6113                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
6114                    for v in values {
6115                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
6116                    }
6117                    let nargs = (1 + values.len()) as u8;
6118                    self.emit_op(
6119                        Op::CallBuiltin(BuiltinId::Unshift as u16, nargs),
6120                        line,
6121                        Some(root),
6122                    );
6123                } else if let ExprKind::Deref {
6124                    expr: aref_expr,
6125                    kind: Sigil::Array,
6126                } = &array.kind
6127                {
6128                    let needs_autoviv = matches!(
6129                        &aref_expr.kind,
6130                        ExprKind::ScalarVar(_)
6131                            | ExprKind::HashElement { .. }
6132                            | ExprKind::ArrayElement { .. }
6133                    );
6134                    if needs_autoviv || values.len() > u8::MAX as usize {
6135                        let pool = self
6136                            .chunk
6137                            .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
6138                        self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
6139                    } else {
6140                        self.compile_expr(aref_expr)?;
6141                        for v in values {
6142                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
6143                        }
6144                        self.emit_op(Op::UnshiftArrayDeref(values.len() as u8), line, Some(root));
6145                    }
6146                } else {
6147                    let pool = self
6148                        .chunk
6149                        .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
6150                    self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
6151                }
6152            }
6153            ExprKind::Splice {
6154                array,
6155                offset,
6156                length,
6157                replacement,
6158            } => {
6159                self.emit_op(Op::WantarrayPush(ctx.as_byte()), line, Some(root));
6160                if let ExprKind::ArrayVar(name) = &array.kind {
6161                    let q = self.qualify_stash_array_name(name);
6162                    let name_const = self.chunk.add_constant(StrykeValue::string(q));
6163                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
6164                    if let Some(o) = offset {
6165                        self.compile_expr(o)?;
6166                    } else {
6167                        self.emit_op(Op::LoadInt(0), line, Some(root));
6168                    }
6169                    if let Some(l) = length {
6170                        self.compile_expr(l)?;
6171                    } else {
6172                        self.emit_op(Op::LoadUndef, line, Some(root));
6173                    }
6174                    for r in replacement {
6175                        self.compile_expr(r)?;
6176                    }
6177                    let nargs = (3 + replacement.len()) as u8;
6178                    self.emit_op(
6179                        Op::CallBuiltin(BuiltinId::Splice as u16, nargs),
6180                        line,
6181                        Some(root),
6182                    );
6183                } else if let ExprKind::Deref {
6184                    expr: aref_expr,
6185                    kind: Sigil::Array,
6186                } = &array.kind
6187                {
6188                    if replacement.len() > u8::MAX as usize {
6189                        let pool = self.chunk.add_splice_expr_entry(
6190                            array.as_ref().clone(),
6191                            offset.as_deref().cloned(),
6192                            length.as_deref().cloned(),
6193                            replacement.clone(),
6194                        );
6195                        self.emit_op(Op::SpliceExpr(pool), line, Some(root));
6196                    } else {
6197                        self.compile_expr(aref_expr)?;
6198                        if let Some(o) = offset {
6199                            self.compile_expr(o)?;
6200                        } else {
6201                            self.emit_op(Op::LoadInt(0), line, Some(root));
6202                        }
6203                        if let Some(l) = length {
6204                            self.compile_expr(l)?;
6205                        } else {
6206                            self.emit_op(Op::LoadUndef, line, Some(root));
6207                        }
6208                        for r in replacement {
6209                            self.compile_expr(r)?;
6210                        }
6211                        self.emit_op(
6212                            Op::SpliceArrayDeref(replacement.len() as u8),
6213                            line,
6214                            Some(root),
6215                        );
6216                    }
6217                } else {
6218                    let pool = self.chunk.add_splice_expr_entry(
6219                        array.as_ref().clone(),
6220                        offset.as_deref().cloned(),
6221                        length.as_deref().cloned(),
6222                        replacement.clone(),
6223                    );
6224                    self.emit_op(Op::SpliceExpr(pool), line, Some(root));
6225                }
6226                self.emit_op(Op::WantarrayPop, line, Some(root));
6227            }
6228            ExprKind::ScalarContext(inner) => {
6229                // `scalar EXPR` forces scalar context on EXPR regardless of the outer context
6230                // (e.g. `print scalar grep { } @x` — grep's result is a count, not a list).
6231                self.compile_expr_ctx(inner, WantarrayCtx::Scalar)?;
6232                // Then apply aggregate scalar semantics (set size, pipeline source len, …) —
6233                // same as [`Op::ValueScalarContext`] / [`StrykeValue::scalar_context`].
6234                self.emit_op(Op::ValueScalarContext, line, Some(root));
6235            }
6236
6237            // ── Hash ops ──
6238            ExprKind::Delete(inner) => {
6239                if let ExprKind::HashElement { hash, key } = &inner.kind {
6240                    self.check_hash_mutable(hash, line)?;
6241                    let idx = self.chunk.intern_name(hash);
6242                    self.compile_expr(key)?;
6243                    self.emit_op(Op::DeleteHashElem(idx), line, Some(root));
6244                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
6245                    self.check_strict_array_access(array, line)?;
6246                    let q = self.qualify_stash_array_name(array);
6247                    self.check_array_mutable(&q, line)?;
6248                    let arr_idx = self.chunk.intern_name(&q);
6249                    self.compile_expr(index)?;
6250                    self.emit_op(Op::DeleteArrayElem(arr_idx), line, Some(root));
6251                } else if let ExprKind::ArrowDeref {
6252                    expr: container,
6253                    index,
6254                    kind: DerefKind::Hash,
6255                } = &inner.kind
6256                {
6257                    self.compile_arrow_hash_base_expr(container)?;
6258                    self.compile_expr(index)?;
6259                    self.emit_op(Op::DeleteArrowHashElem, line, Some(root));
6260                } else if let ExprKind::ArrowDeref {
6261                    expr: container,
6262                    index,
6263                    kind: DerefKind::Array,
6264                } = &inner.kind
6265                {
6266                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
6267                        self.compile_expr(container)?;
6268                        self.compile_expr(index)?;
6269                        self.emit_op(Op::DeleteArrowArrayElem, line, Some(root));
6270                    } else {
6271                        let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
6272                        self.emit_op(Op::DeleteExpr(pool), line, Some(root));
6273                    }
6274                } else {
6275                    let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
6276                    self.emit_op(Op::DeleteExpr(pool), line, Some(root));
6277                }
6278            }
6279            ExprKind::Exists(inner) => {
6280                if let ExprKind::HashElement { hash, key } = &inner.kind {
6281                    let idx = self.chunk.intern_name(hash);
6282                    self.compile_expr(key)?;
6283                    self.emit_op(Op::ExistsHashElem(idx), line, Some(root));
6284                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
6285                    self.check_strict_array_access(array, line)?;
6286                    let arr_idx = self
6287                        .chunk
6288                        .intern_name(&self.qualify_stash_array_name(array));
6289                    self.compile_expr(index)?;
6290                    self.emit_op(Op::ExistsArrayElem(arr_idx), line, Some(root));
6291                } else if let ExprKind::ArrowDeref {
6292                    expr: container,
6293                    index,
6294                    kind: DerefKind::Hash,
6295                } = &inner.kind
6296                {
6297                    // Multi-level chains (e.g. `exists $h{x}{y}{z}`) need
6298                    // undef-tolerant intermediate eval — route through
6299                    // `ExistsExpr` (which calls `eval_exists_operand` and
6300                    // soft-fails on undef intermediates). (BUG-009)
6301                    if matches!(container.kind, ExprKind::ArrowDeref { .. }) {
6302                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6303                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6304                    } else {
6305                        self.compile_arrow_hash_base_expr(container)?;
6306                        self.compile_expr(index)?;
6307                        self.emit_op(Op::ExistsArrowHashElem, line, Some(root));
6308                    }
6309                } else if let ExprKind::ArrowDeref {
6310                    expr: container,
6311                    index,
6312                    kind: DerefKind::Array,
6313                } = &inner.kind
6314                {
6315                    if !arrow_deref_arrow_subscript_is_plain_scalar_index(index)
6316                        || matches!(container.kind, ExprKind::ArrowDeref { .. })
6317                    {
6318                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6319                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6320                    } else {
6321                        self.compile_expr(container)?;
6322                        self.compile_expr(index)?;
6323                        self.emit_op(Op::ExistsArrowArrayElem, line, Some(root));
6324                    }
6325                } else {
6326                    let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
6327                    self.emit_op(Op::ExistsExpr(pool), line, Some(root));
6328                }
6329            }
6330            ExprKind::Keys(inner) => {
6331                if let ExprKind::HashVar(name) = &inner.kind {
6332                    let idx = self.chunk.intern_name(name);
6333                    if ctx == WantarrayCtx::List {
6334                        self.emit_op(Op::HashKeys(idx), line, Some(root));
6335                    } else {
6336                        self.emit_op(Op::HashKeysScalar(idx), line, Some(root));
6337                    }
6338                } else {
6339                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
6340                    if ctx == WantarrayCtx::List {
6341                        self.emit_op(Op::KeysFromValue, line, Some(root));
6342                    } else {
6343                        self.emit_op(Op::KeysFromValueScalar, line, Some(root));
6344                    }
6345                }
6346            }
6347            ExprKind::Values(inner) => {
6348                if let ExprKind::HashVar(name) = &inner.kind {
6349                    let idx = self.chunk.intern_name(name);
6350                    if ctx == WantarrayCtx::List {
6351                        self.emit_op(Op::HashValues(idx), line, Some(root));
6352                    } else {
6353                        self.emit_op(Op::HashValuesScalar(idx), line, Some(root));
6354                    }
6355                } else {
6356                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
6357                    if ctx == WantarrayCtx::List {
6358                        self.emit_op(Op::ValuesFromValue, line, Some(root));
6359                    } else {
6360                        self.emit_op(Op::ValuesFromValueScalar, line, Some(root));
6361                    }
6362                }
6363            }
6364            ExprKind::Each(e) => {
6365                self.compile_expr(e)?;
6366                self.emit_op(Op::CallBuiltin(BuiltinId::Each as u16, 1), line, Some(root));
6367            }
6368
6369            // ── Builtins that map to CallBuiltin ──
6370            ExprKind::Length(e) => {
6371                self.compile_expr(e)?;
6372                self.emit_op(
6373                    Op::CallBuiltin(BuiltinId::Length as u16, 1),
6374                    line,
6375                    Some(root),
6376                );
6377            }
6378            ExprKind::Chomp(e) => {
6379                self.compile_expr(e)?;
6380                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
6381                self.emit_op(Op::ChompInPlace(lv), line, Some(root));
6382            }
6383            ExprKind::Chop(e) => {
6384                self.compile_expr(e)?;
6385                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
6386                self.emit_op(Op::ChopInPlace(lv), line, Some(root));
6387            }
6388            ExprKind::Defined(e) => {
6389                self.compile_expr(e)?;
6390                self.emit_op(
6391                    Op::CallBuiltin(BuiltinId::Defined as u16, 1),
6392                    line,
6393                    Some(root),
6394                );
6395            }
6396            ExprKind::Abs(e) => {
6397                self.compile_expr(e)?;
6398                self.emit_op(Op::CallBuiltin(BuiltinId::Abs as u16, 1), line, Some(root));
6399            }
6400            ExprKind::Int(e) => {
6401                self.compile_expr(e)?;
6402                self.emit_op(Op::CallBuiltin(BuiltinId::Int as u16, 1), line, Some(root));
6403            }
6404            ExprKind::Sqrt(e) => {
6405                self.compile_expr(e)?;
6406                self.emit_op(Op::CallBuiltin(BuiltinId::Sqrt as u16, 1), line, Some(root));
6407            }
6408            ExprKind::Sin(e) => {
6409                self.compile_expr(e)?;
6410                self.emit_op(Op::CallBuiltin(BuiltinId::Sin as u16, 1), line, Some(root));
6411            }
6412            ExprKind::Cos(e) => {
6413                self.compile_expr(e)?;
6414                self.emit_op(Op::CallBuiltin(BuiltinId::Cos as u16, 1), line, Some(root));
6415            }
6416            ExprKind::Atan2 { y, x } => {
6417                self.compile_expr(y)?;
6418                self.compile_expr(x)?;
6419                self.emit_op(
6420                    Op::CallBuiltin(BuiltinId::Atan2 as u16, 2),
6421                    line,
6422                    Some(root),
6423                );
6424            }
6425            ExprKind::Exp(e) => {
6426                self.compile_expr(e)?;
6427                self.emit_op(Op::CallBuiltin(BuiltinId::Exp as u16, 1), line, Some(root));
6428            }
6429            ExprKind::Log(e) => {
6430                self.compile_expr(e)?;
6431                self.emit_op(Op::CallBuiltin(BuiltinId::Log as u16, 1), line, Some(root));
6432            }
6433            ExprKind::Rand(upper) => {
6434                if let Some(e) = upper {
6435                    self.compile_expr(e)?;
6436                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 1), line, Some(root));
6437                } else {
6438                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 0), line, Some(root));
6439                }
6440            }
6441            ExprKind::Srand(seed) => {
6442                if let Some(e) = seed {
6443                    self.compile_expr(e)?;
6444                    self.emit_op(
6445                        Op::CallBuiltin(BuiltinId::Srand as u16, 1),
6446                        line,
6447                        Some(root),
6448                    );
6449                } else {
6450                    self.emit_op(
6451                        Op::CallBuiltin(BuiltinId::Srand as u16, 0),
6452                        line,
6453                        Some(root),
6454                    );
6455                }
6456            }
6457            ExprKind::Chr(e) => {
6458                self.compile_expr(e)?;
6459                self.emit_op(Op::CallBuiltin(BuiltinId::Chr as u16, 1), line, Some(root));
6460            }
6461            ExprKind::Ord(e) => {
6462                self.compile_expr(e)?;
6463                self.emit_op(Op::CallBuiltin(BuiltinId::Ord as u16, 1), line, Some(root));
6464            }
6465            ExprKind::Hex(e) => {
6466                self.compile_expr(e)?;
6467                self.emit_op(Op::CallBuiltin(BuiltinId::Hex as u16, 1), line, Some(root));
6468            }
6469            ExprKind::Oct(e) => {
6470                self.compile_expr(e)?;
6471                self.emit_op(Op::CallBuiltin(BuiltinId::Oct as u16, 1), line, Some(root));
6472            }
6473            ExprKind::Uc(e) => {
6474                self.compile_expr(e)?;
6475                self.emit_op(Op::CallBuiltin(BuiltinId::Uc as u16, 1), line, Some(root));
6476            }
6477            ExprKind::Lc(e) => {
6478                self.compile_expr(e)?;
6479                self.emit_op(Op::CallBuiltin(BuiltinId::Lc as u16, 1), line, Some(root));
6480            }
6481            ExprKind::Ucfirst(e) => {
6482                self.compile_expr(e)?;
6483                self.emit_op(
6484                    Op::CallBuiltin(BuiltinId::Ucfirst as u16, 1),
6485                    line,
6486                    Some(root),
6487                );
6488            }
6489            ExprKind::Lcfirst(e) => {
6490                self.compile_expr(e)?;
6491                self.emit_op(
6492                    Op::CallBuiltin(BuiltinId::Lcfirst as u16, 1),
6493                    line,
6494                    Some(root),
6495                );
6496            }
6497            ExprKind::Fc(e) => {
6498                self.compile_expr(e)?;
6499                self.emit_op(Op::CallBuiltin(BuiltinId::Fc as u16, 1), line, Some(root));
6500            }
6501            ExprKind::Crypt { plaintext, salt } => {
6502                self.compile_expr(plaintext)?;
6503                self.compile_expr(salt)?;
6504                self.emit_op(
6505                    Op::CallBuiltin(BuiltinId::Crypt as u16, 2),
6506                    line,
6507                    Some(root),
6508                );
6509            }
6510            ExprKind::Pos(e) => match e {
6511                None => {
6512                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 0), line, Some(root));
6513                }
6514                Some(pos_arg) => {
6515                    if let ExprKind::ScalarVar(name) = &pos_arg.kind {
6516                        let stor = self.scalar_storage_name_for_ops(name);
6517                        let idx = self.chunk.add_constant(StrykeValue::string(stor));
6518                        self.emit_op(Op::LoadConst(idx), line, Some(root));
6519                    } else {
6520                        self.compile_expr(pos_arg)?;
6521                    }
6522                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 1), line, Some(root));
6523                }
6524            },
6525            ExprKind::Study(e) => {
6526                self.compile_expr(e)?;
6527                self.emit_op(
6528                    Op::CallBuiltin(BuiltinId::Study as u16, 1),
6529                    line,
6530                    Some(root),
6531                );
6532            }
6533            ExprKind::Ref(e) => {
6534                self.compile_expr(e)?;
6535                self.emit_op(Op::CallBuiltin(BuiltinId::Ref as u16, 1), line, Some(root));
6536            }
6537            ExprKind::Rev(e) => {
6538                // Compile in list context to get arrays/hashes as collections
6539                self.compile_expr_ctx(e, WantarrayCtx::List)?;
6540                if ctx == WantarrayCtx::List {
6541                    self.emit_op(Op::RevListOp, line, Some(root));
6542                } else {
6543                    self.emit_op(Op::RevScalarOp, line, Some(root));
6544                }
6545            }
6546            ExprKind::ReverseExpr(e) => {
6547                self.compile_expr_ctx(e, WantarrayCtx::List)?;
6548                if ctx == WantarrayCtx::List {
6549                    self.emit_op(Op::ReverseListOp, line, Some(root));
6550                } else {
6551                    self.emit_op(Op::ReverseScalarOp, line, Some(root));
6552                }
6553            }
6554            ExprKind::System(args) => {
6555                for a in args {
6556                    self.compile_expr(a)?;
6557                }
6558                self.emit_op(
6559                    Op::CallBuiltin(BuiltinId::System as u16, args.len() as u8),
6560                    line,
6561                    Some(root),
6562                );
6563            }
6564            ExprKind::Exec(args) => {
6565                for a in args {
6566                    self.compile_expr(a)?;
6567                }
6568                self.emit_op(
6569                    Op::CallBuiltin(BuiltinId::Exec as u16, args.len() as u8),
6570                    line,
6571                    Some(root),
6572                );
6573            }
6574
6575            // ── String builtins ──
6576            ExprKind::Substr {
6577                string,
6578                offset,
6579                length,
6580                replacement,
6581            } => {
6582                if let Some(rep) = replacement {
6583                    let idx = self.chunk.add_substr_four_arg_entry(
6584                        string.as_ref().clone(),
6585                        offset.as_ref().clone(),
6586                        length.as_ref().map(|b| b.as_ref().clone()),
6587                        rep.as_ref().clone(),
6588                    );
6589                    self.emit_op(Op::SubstrFourArg(idx), line, Some(root));
6590                } else {
6591                    self.compile_expr(string)?;
6592                    self.compile_expr(offset)?;
6593                    let mut argc: u8 = 2;
6594                    if let Some(len) = length {
6595                        self.compile_expr(len)?;
6596                        argc = 3;
6597                    }
6598                    self.emit_op(
6599                        Op::CallBuiltin(BuiltinId::Substr as u16, argc),
6600                        line,
6601                        Some(root),
6602                    );
6603                }
6604            }
6605            ExprKind::Index {
6606                string,
6607                substr,
6608                position,
6609            } => {
6610                self.compile_expr(string)?;
6611                self.compile_expr(substr)?;
6612                if let Some(pos) = position {
6613                    self.compile_expr(pos)?;
6614                    self.emit_op(
6615                        Op::CallBuiltin(BuiltinId::Index as u16, 3),
6616                        line,
6617                        Some(root),
6618                    );
6619                } else {
6620                    self.emit_op(
6621                        Op::CallBuiltin(BuiltinId::Index as u16, 2),
6622                        line,
6623                        Some(root),
6624                    );
6625                }
6626            }
6627            ExprKind::Rindex {
6628                string,
6629                substr,
6630                position,
6631            } => {
6632                self.compile_expr(string)?;
6633                self.compile_expr(substr)?;
6634                if let Some(pos) = position {
6635                    self.compile_expr(pos)?;
6636                    self.emit_op(
6637                        Op::CallBuiltin(BuiltinId::Rindex as u16, 3),
6638                        line,
6639                        Some(root),
6640                    );
6641                } else {
6642                    self.emit_op(
6643                        Op::CallBuiltin(BuiltinId::Rindex as u16, 2),
6644                        line,
6645                        Some(root),
6646                    );
6647                }
6648            }
6649
6650            ExprKind::JoinExpr { separator, list } => {
6651                self.compile_expr(separator)?;
6652                // Arguments after the separator are evaluated in list context (Perl 5).
6653                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6654                self.emit_op(Op::CallBuiltin(BuiltinId::Join as u16, 2), line, Some(root));
6655            }
6656            ExprKind::SplitExpr {
6657                pattern,
6658                string,
6659                limit,
6660            } => {
6661                self.compile_expr(pattern)?;
6662                self.compile_expr(string)?;
6663                if let Some(l) = limit {
6664                    self.compile_expr(l)?;
6665                    self.emit_op(
6666                        Op::CallBuiltin(BuiltinId::Split as u16, 3),
6667                        line,
6668                        Some(root),
6669                    );
6670                } else {
6671                    self.emit_op(
6672                        Op::CallBuiltin(BuiltinId::Split as u16, 2),
6673                        line,
6674                        Some(root),
6675                    );
6676                }
6677            }
6678            ExprKind::Sprintf { format, args } => {
6679                // sprintf's arg list after the format is Perl list context — ranges, arrays,
6680                // and `reverse`/`sort`/`grep` flatten into the format argument positions.
6681                self.compile_expr(format)?;
6682                for a in args {
6683                    self.compile_expr_ctx(a, WantarrayCtx::List)?;
6684                }
6685                self.emit_op(
6686                    Op::CallBuiltin(BuiltinId::Sprintf as u16, (1 + args.len()) as u8),
6687                    line,
6688                    Some(root),
6689                );
6690            }
6691
6692            // ── I/O ──
6693            ExprKind::Open { handle, mode, file } => {
6694                if let ExprKind::OpenMyHandle { name } = &handle.kind {
6695                    let name_idx = self.chunk.intern_name(name);
6696                    self.emit_op(Op::LoadUndef, line, Some(root));
6697                    self.emit_declare_scalar(name_idx, line, false);
6698                    let h_idx = self.chunk.add_constant(StrykeValue::string(name.clone()));
6699                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
6700                    self.compile_expr(mode)?;
6701                    if let Some(f) = file {
6702                        self.compile_expr(f)?;
6703                        self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
6704                    } else {
6705                        self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
6706                    }
6707                    self.emit_op(Op::SetScalarKeepPlain(name_idx), line, Some(root));
6708                    return Ok(());
6709                }
6710                self.compile_expr(handle)?;
6711                self.compile_expr(mode)?;
6712                if let Some(f) = file {
6713                    self.compile_expr(f)?;
6714                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
6715                } else {
6716                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
6717                }
6718            }
6719            ExprKind::OpenMyHandle { .. } => {
6720                return Err(CompileError::Unsupported(
6721                    "open my $fh handle expression".into(),
6722                ));
6723            }
6724            ExprKind::Close(e) => {
6725                self.compile_expr(e)?;
6726                self.emit_op(
6727                    Op::CallBuiltin(BuiltinId::Close as u16, 1),
6728                    line,
6729                    Some(root),
6730                );
6731            }
6732            ExprKind::ReadLine(handle) => {
6733                let bid = if ctx == WantarrayCtx::List {
6734                    BuiltinId::ReadLineList
6735                } else {
6736                    BuiltinId::ReadLine
6737                };
6738                if let Some(h) = handle {
6739                    let idx = self.chunk.add_constant(StrykeValue::string(h.clone()));
6740                    self.emit_op(Op::LoadConst(idx), line, Some(root));
6741                    self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
6742                } else {
6743                    self.emit_op(Op::CallBuiltin(bid as u16, 0), line, Some(root));
6744                }
6745            }
6746            ExprKind::Eof(e) => {
6747                if let Some(inner) = e {
6748                    self.compile_expr(inner)?;
6749                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 1), line, Some(root));
6750                } else {
6751                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 0), line, Some(root));
6752                }
6753            }
6754            ExprKind::Opendir { handle, path } => {
6755                self.compile_expr(handle)?;
6756                self.compile_expr(path)?;
6757                self.emit_op(
6758                    Op::CallBuiltin(BuiltinId::Opendir as u16, 2),
6759                    line,
6760                    Some(root),
6761                );
6762            }
6763            ExprKind::Readdir(e) => {
6764                let bid = if ctx == WantarrayCtx::List {
6765                    BuiltinId::ReaddirList
6766                } else {
6767                    BuiltinId::Readdir
6768                };
6769                self.compile_expr(e)?;
6770                self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
6771            }
6772            ExprKind::Closedir(e) => {
6773                self.compile_expr(e)?;
6774                self.emit_op(
6775                    Op::CallBuiltin(BuiltinId::Closedir as u16, 1),
6776                    line,
6777                    Some(root),
6778                );
6779            }
6780            ExprKind::Rewinddir(e) => {
6781                self.compile_expr(e)?;
6782                self.emit_op(
6783                    Op::CallBuiltin(BuiltinId::Rewinddir as u16, 1),
6784                    line,
6785                    Some(root),
6786                );
6787            }
6788            ExprKind::Telldir(e) => {
6789                self.compile_expr(e)?;
6790                self.emit_op(
6791                    Op::CallBuiltin(BuiltinId::Telldir as u16, 1),
6792                    line,
6793                    Some(root),
6794                );
6795            }
6796            ExprKind::Seekdir { handle, position } => {
6797                self.compile_expr(handle)?;
6798                self.compile_expr(position)?;
6799                self.emit_op(
6800                    Op::CallBuiltin(BuiltinId::Seekdir as u16, 2),
6801                    line,
6802                    Some(root),
6803                );
6804            }
6805
6806            // ── File tests ──
6807            ExprKind::FileTest { op, expr } => {
6808                self.compile_expr(expr)?;
6809                self.emit_op(Op::FileTestOp(*op as u8), line, Some(root));
6810            }
6811
6812            // ── Eval / Do / Require ──
6813            ExprKind::Eval(e) => {
6814                self.compile_expr(e)?;
6815                // `eval { BLOCK }` runs synchronously and is intentionally
6816                // shared-state with the enclosing scope (it's used for error
6817                // catching, not stored as a closure value). Un-mark the
6818                // CodeRef's block so the closure-write check (DESIGN-001)
6819                // doesn't fire on writes to outer-scope `my` from inside
6820                // the eval body.
6821                if let ExprKind::CodeRef { .. } = &e.kind {
6822                    if let Some(max_idx) = self.sub_body_block_indices.iter().copied().max() {
6823                        self.sub_body_block_indices.remove(&max_idx);
6824                    }
6825                }
6826                self.emit_op(Op::CallBuiltin(BuiltinId::Eval as u16, 1), line, Some(root));
6827            }
6828            ExprKind::Do(e) => {
6829                // do { BLOCK } executes the block; do "file" loads a file
6830                if let ExprKind::CodeRef { body, .. } = &e.kind {
6831                    let block_idx = self.add_deferred_block(body.clone());
6832                    self.emit_op(Op::EvalBlock(block_idx, ctx.as_byte()), line, Some(root));
6833                } else {
6834                    self.compile_expr(e)?;
6835                    self.emit_op(Op::CallBuiltin(BuiltinId::Do as u16, 1), line, Some(root));
6836                }
6837            }
6838            ExprKind::Require(e) => {
6839                self.compile_expr(e)?;
6840                self.emit_op(
6841                    Op::CallBuiltin(BuiltinId::Require as u16, 1),
6842                    line,
6843                    Some(root),
6844                );
6845            }
6846
6847            // ── Filesystem ──
6848            ExprKind::Chdir(e) => {
6849                self.compile_expr(e)?;
6850                self.emit_op(
6851                    Op::CallBuiltin(BuiltinId::Chdir as u16, 1),
6852                    line,
6853                    Some(root),
6854                );
6855            }
6856            ExprKind::Mkdir { path, mode } => {
6857                self.compile_expr(path)?;
6858                if let Some(m) = mode {
6859                    self.compile_expr(m)?;
6860                    self.emit_op(
6861                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 2),
6862                        line,
6863                        Some(root),
6864                    );
6865                } else {
6866                    self.emit_op(
6867                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 1),
6868                        line,
6869                        Some(root),
6870                    );
6871                }
6872            }
6873            ExprKind::Unlink(args) => {
6874                for a in args {
6875                    self.compile_expr(a)?;
6876                }
6877                self.emit_op(
6878                    Op::CallBuiltin(BuiltinId::Unlink as u16, args.len() as u8),
6879                    line,
6880                    Some(root),
6881                );
6882            }
6883            ExprKind::Rename { old, new } => {
6884                self.compile_expr(old)?;
6885                self.compile_expr(new)?;
6886                self.emit_op(
6887                    Op::CallBuiltin(BuiltinId::Rename as u16, 2),
6888                    line,
6889                    Some(root),
6890                );
6891            }
6892            ExprKind::Chmod(args) => {
6893                for a in args {
6894                    self.compile_expr(a)?;
6895                }
6896                self.emit_op(
6897                    Op::CallBuiltin(BuiltinId::Chmod as u16, args.len() as u8),
6898                    line,
6899                    Some(root),
6900                );
6901            }
6902            ExprKind::Chown(args) => {
6903                for a in args {
6904                    self.compile_expr(a)?;
6905                }
6906                self.emit_op(
6907                    Op::CallBuiltin(BuiltinId::Chown as u16, args.len() as u8),
6908                    line,
6909                    Some(root),
6910                );
6911            }
6912            ExprKind::Stat(e) => {
6913                self.compile_expr(e)?;
6914                self.emit_op(Op::CallBuiltin(BuiltinId::Stat as u16, 1), line, Some(root));
6915            }
6916            ExprKind::Lstat(e) => {
6917                self.compile_expr(e)?;
6918                self.emit_op(
6919                    Op::CallBuiltin(BuiltinId::Lstat as u16, 1),
6920                    line,
6921                    Some(root),
6922                );
6923            }
6924            ExprKind::Link { old, new } => {
6925                self.compile_expr(old)?;
6926                self.compile_expr(new)?;
6927                self.emit_op(Op::CallBuiltin(BuiltinId::Link as u16, 2), line, Some(root));
6928            }
6929            ExprKind::Symlink { old, new } => {
6930                self.compile_expr(old)?;
6931                self.compile_expr(new)?;
6932                self.emit_op(
6933                    Op::CallBuiltin(BuiltinId::Symlink as u16, 2),
6934                    line,
6935                    Some(root),
6936                );
6937            }
6938            ExprKind::Readlink(e) => {
6939                self.compile_expr(e)?;
6940                self.emit_op(
6941                    Op::CallBuiltin(BuiltinId::Readlink as u16, 1),
6942                    line,
6943                    Some(root),
6944                );
6945            }
6946            ExprKind::Files(args) => {
6947                for a in args {
6948                    self.compile_expr(a)?;
6949                }
6950                self.emit_op(
6951                    Op::CallBuiltin(BuiltinId::Files as u16, args.len() as u8),
6952                    line,
6953                    Some(root),
6954                );
6955            }
6956            ExprKind::Filesf(args) => {
6957                for a in args {
6958                    self.compile_expr(a)?;
6959                }
6960                self.emit_op(
6961                    Op::CallBuiltin(BuiltinId::Filesf as u16, args.len() as u8),
6962                    line,
6963                    Some(root),
6964                );
6965            }
6966            ExprKind::FilesfRecursive(args) => {
6967                for a in args {
6968                    self.compile_expr(a)?;
6969                }
6970                self.emit_op(
6971                    Op::CallBuiltin(BuiltinId::FilesfRecursive as u16, args.len() as u8),
6972                    line,
6973                    Some(root),
6974                );
6975            }
6976            ExprKind::Dirs(args) => {
6977                for a in args {
6978                    self.compile_expr(a)?;
6979                }
6980                self.emit_op(
6981                    Op::CallBuiltin(BuiltinId::Dirs as u16, args.len() as u8),
6982                    line,
6983                    Some(root),
6984                );
6985            }
6986            ExprKind::DirsRecursive(args) => {
6987                for a in args {
6988                    self.compile_expr(a)?;
6989                }
6990                self.emit_op(
6991                    Op::CallBuiltin(BuiltinId::DirsRecursive as u16, args.len() as u8),
6992                    line,
6993                    Some(root),
6994                );
6995            }
6996            ExprKind::SymLinks(args) => {
6997                for a in args {
6998                    self.compile_expr(a)?;
6999                }
7000                self.emit_op(
7001                    Op::CallBuiltin(BuiltinId::SymLinks as u16, args.len() as u8),
7002                    line,
7003                    Some(root),
7004                );
7005            }
7006            ExprKind::Sockets(args) => {
7007                for a in args {
7008                    self.compile_expr(a)?;
7009                }
7010                self.emit_op(
7011                    Op::CallBuiltin(BuiltinId::Sockets as u16, args.len() as u8),
7012                    line,
7013                    Some(root),
7014                );
7015            }
7016            ExprKind::Pipes(args) => {
7017                for a in args {
7018                    self.compile_expr(a)?;
7019                }
7020                self.emit_op(
7021                    Op::CallBuiltin(BuiltinId::Pipes as u16, args.len() as u8),
7022                    line,
7023                    Some(root),
7024                );
7025            }
7026            ExprKind::BlockDevices(args) => {
7027                for a in args {
7028                    self.compile_expr(a)?;
7029                }
7030                self.emit_op(
7031                    Op::CallBuiltin(BuiltinId::BlockDevices as u16, args.len() as u8),
7032                    line,
7033                    Some(root),
7034                );
7035            }
7036            ExprKind::CharDevices(args) => {
7037                for a in args {
7038                    self.compile_expr(a)?;
7039                }
7040                self.emit_op(
7041                    Op::CallBuiltin(BuiltinId::CharDevices as u16, args.len() as u8),
7042                    line,
7043                    Some(root),
7044                );
7045            }
7046            ExprKind::Executables(args) => {
7047                for a in args {
7048                    self.compile_expr(a)?;
7049                }
7050                self.emit_op(
7051                    Op::CallBuiltin(BuiltinId::Executables as u16, args.len() as u8),
7052                    line,
7053                    Some(root),
7054                );
7055            }
7056            ExprKind::Glob(args) => {
7057                for a in args {
7058                    self.compile_expr(a)?;
7059                }
7060                self.emit_op(
7061                    Op::CallBuiltin(BuiltinId::Glob as u16, args.len() as u8),
7062                    line,
7063                    Some(root),
7064                );
7065            }
7066            ExprKind::GlobPar { args, progress } => {
7067                for a in args {
7068                    self.compile_expr(a)?;
7069                }
7070                match progress {
7071                    None => {
7072                        self.emit_op(
7073                            Op::CallBuiltin(BuiltinId::GlobPar as u16, args.len() as u8),
7074                            line,
7075                            Some(root),
7076                        );
7077                    }
7078                    Some(p) => {
7079                        self.compile_expr(p)?;
7080                        self.emit_op(
7081                            Op::CallBuiltin(
7082                                BuiltinId::GlobParProgress as u16,
7083                                (args.len() + 1) as u8,
7084                            ),
7085                            line,
7086                            Some(root),
7087                        );
7088                    }
7089                }
7090            }
7091            ExprKind::ParSed { args, progress } => {
7092                for a in args {
7093                    self.compile_expr(a)?;
7094                }
7095                match progress {
7096                    None => {
7097                        self.emit_op(
7098                            Op::CallBuiltin(BuiltinId::ParSed as u16, args.len() as u8),
7099                            line,
7100                            Some(root),
7101                        );
7102                    }
7103                    Some(p) => {
7104                        self.compile_expr(p)?;
7105                        self.emit_op(
7106                            Op::CallBuiltin(
7107                                BuiltinId::ParSedProgress as u16,
7108                                (args.len() + 1) as u8,
7109                            ),
7110                            line,
7111                            Some(root),
7112                        );
7113                    }
7114                }
7115            }
7116
7117            // ── OOP ──
7118            ExprKind::Bless { ref_expr, class } => {
7119                self.compile_expr(ref_expr)?;
7120                if let Some(c) = class {
7121                    self.compile_expr(c)?;
7122                    self.emit_op(
7123                        Op::CallBuiltin(BuiltinId::Bless as u16, 2),
7124                        line,
7125                        Some(root),
7126                    );
7127                } else {
7128                    self.emit_op(
7129                        Op::CallBuiltin(BuiltinId::Bless as u16, 1),
7130                        line,
7131                        Some(root),
7132                    );
7133                }
7134            }
7135            ExprKind::Caller(e) => {
7136                if let Some(inner) = e {
7137                    self.compile_expr(inner)?;
7138                    self.emit_op(
7139                        Op::CallBuiltin(BuiltinId::Caller as u16, 1),
7140                        line,
7141                        Some(root),
7142                    );
7143                } else {
7144                    self.emit_op(
7145                        Op::CallBuiltin(BuiltinId::Caller as u16, 0),
7146                        line,
7147                        Some(root),
7148                    );
7149                }
7150            }
7151            ExprKind::Wantarray => {
7152                self.emit_op(
7153                    Op::CallBuiltin(BuiltinId::Wantarray as u16, 0),
7154                    line,
7155                    Some(root),
7156                );
7157            }
7158
7159            // ── References ──
7160            ExprKind::ScalarRef(e) => match &e.kind {
7161                ExprKind::ScalarVar(name) => {
7162                    let idx = self.intern_scalar_var_for_ops(name);
7163                    self.emit_op(Op::MakeScalarBindingRef(idx), line, Some(root));
7164                }
7165                ExprKind::ArrayVar(name) => {
7166                    self.check_strict_array_access(name, line)?;
7167                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
7168                    self.emit_op(Op::MakeArrayBindingRef(idx), line, Some(root));
7169                }
7170                ExprKind::HashVar(name) => {
7171                    self.check_strict_hash_access(name, line)?;
7172                    let idx = self.chunk.intern_name(name);
7173                    self.emit_op(Op::MakeHashBindingRef(idx), line, Some(root));
7174                }
7175                ExprKind::Deref {
7176                    expr: inner,
7177                    kind: Sigil::Array,
7178                } => {
7179                    self.compile_expr(inner)?;
7180                    self.emit_op(Op::MakeArrayRefAlias, line, Some(root));
7181                }
7182                ExprKind::Deref {
7183                    expr: inner,
7184                    kind: Sigil::Hash,
7185                } => {
7186                    self.compile_expr(inner)?;
7187                    self.emit_op(Op::MakeHashRefAlias, line, Some(root));
7188                }
7189                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
7190                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7191                    self.emit_op(Op::MakeArrayRef, line, Some(root));
7192                }
7193                ExprKind::AnonymousListSlice { .. } | ExprKind::HashSliceDeref { .. } => {
7194                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7195                    self.emit_op(Op::MakeArrayRef, line, Some(root));
7196                }
7197                _ => {
7198                    self.compile_expr(e)?;
7199                    self.emit_op(Op::MakeScalarRef, line, Some(root));
7200                }
7201            },
7202            ExprKind::ArrayRef(elems) => {
7203                // `[ LIST ]` — each element is in list context so `1..5`, `reverse`, `grep`
7204                // and array variables flatten through [`Op::MakeArray`], which already splats
7205                // nested arrays.
7206                for e in elems {
7207                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7208                }
7209                self.emit_op(Op::MakeArray(elems.len() as u16), line, Some(root));
7210                self.emit_op(Op::MakeArrayRef, line, Some(root));
7211            }
7212            ExprKind::HashRef(pairs) => {
7213                // `{ K => V, ... }` — keys are scalar, values are list context so ranges and
7214                // slurpy constructs on the value side flatten into the built hash.
7215                // Special case: a single pair with the `__HASH_SPREAD__` sentinel key is
7216                // emitted by `parse_forced_hashref_body` (`+{ EXPR }`) and by the `{ %h }`
7217                // hash-spread short-form. Compile the value in list context and let
7218                // [`Op::MakeHashRef`] pair up the resulting list at runtime — the slow
7219                // path's [`Interpreter::eval_expr`] HashRef arm handles this too via the
7220                // same sentinel string.
7221                if pairs.len() == 1 {
7222                    if let ExprKind::String(ref k) = pairs[0].0.kind {
7223                        if k == "__HASH_SPREAD__" {
7224                            self.compile_expr_ctx(&pairs[0].1, WantarrayCtx::List)?;
7225                            self.emit_op(Op::MakeHashRef, line, Some(root));
7226                            return Ok(());
7227                        }
7228                    }
7229                }
7230                for (k, v) in pairs {
7231                    self.compile_expr(k)?;
7232                    self.compile_expr_ctx(v, WantarrayCtx::List)?;
7233                }
7234                self.emit_op(Op::MakeHash((pairs.len() * 2) as u16), line, Some(root));
7235                self.emit_op(Op::MakeHashRef, line, Some(root));
7236            }
7237            ExprKind::CodeRef { body, params } => {
7238                let block_idx = self.add_deferred_block(body.clone());
7239                self.sub_body_block_indices.insert(block_idx);
7240                // Stash params alongside the block index so the 4th-pass
7241                // compile can register them in the new sub-body layer (so
7242                // the closure-write check sees them as locally declared).
7243                while self.code_ref_block_params.len() <= block_idx as usize {
7244                    self.code_ref_block_params.push(Vec::new());
7245                }
7246                self.code_ref_block_params[block_idx as usize] = params.clone();
7247                let sig_idx = self.chunk.add_code_ref_sig(params.clone());
7248                self.emit_op(Op::MakeCodeRef(block_idx, sig_idx), line, Some(root));
7249            }
7250            ExprKind::SubroutineRef(name) => {
7251                // Unary `&name` — invoke subroutine with no explicit args (same as tree `call_named_sub`).
7252                let q = self.qualify_sub_key(name);
7253                let name_idx = self.chunk.intern_name(&q);
7254                self.emit_op(Op::Call(name_idx, 0, ctx.as_byte()), line, Some(root));
7255            }
7256            ExprKind::SubroutineCodeRef(name) => {
7257                // `\&name` — coderef (must exist at run time).
7258                let name_idx = self.chunk.intern_name(name);
7259                self.emit_op(Op::LoadNamedSubRef(name_idx), line, Some(root));
7260            }
7261            ExprKind::DynamicSubCodeRef(expr) => {
7262                self.compile_expr(expr)?;
7263                self.emit_op(Op::LoadDynamicSubRef, line, Some(root));
7264            }
7265
7266            // ── Derefs ──
7267            ExprKind::ArrowDeref { expr, index, kind } => match kind {
7268                DerefKind::Array => {
7269                    self.compile_arrow_array_base_expr(expr)?;
7270                    let mut used_arrow_slice = false;
7271                    // `$r->[$i]` with a single plain-scalar subscript is element
7272                    // access, not a slice — even when the parser wraps it in a
7273                    // 1-element `List`. Returning a 1-element list here breaks
7274                    // arithmetic (`$a + $b` numifies via length to `1+1=2`).
7275                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
7276                        let inner = match &index.kind {
7277                            ExprKind::List(el) if el.len() == 1 => &el[0],
7278                            _ => index.as_ref(),
7279                        };
7280                        self.compile_expr(inner)?;
7281                        self.emit_op(Op::ArrowArray, line, Some(root));
7282                    } else if let ExprKind::List(indices) = &index.kind {
7283                        for ix in indices {
7284                            self.compile_array_slice_index_expr(ix)?;
7285                        }
7286                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, Some(root));
7287                        used_arrow_slice = true;
7288                    } else {
7289                        // One subscript expr may expand to multiple indices (`$r->[0..1]`, `[(0,1)]`).
7290                        self.compile_array_slice_index_expr(index)?;
7291                        self.emit_op(Op::ArrowArraySlice(1), line, Some(root));
7292                        used_arrow_slice = true;
7293                    }
7294                    if used_arrow_slice && ctx != WantarrayCtx::List {
7295                        self.emit_op(Op::ListSliceToScalar, line, Some(root));
7296                    }
7297                }
7298                DerefKind::Hash => {
7299                    self.compile_arrow_hash_base_expr(expr)?;
7300                    self.compile_expr(index)?;
7301                    self.emit_op(Op::ArrowHash, line, Some(root));
7302                }
7303                DerefKind::Call => {
7304                    self.compile_expr(expr)?;
7305                    // Always compile args in list context to preserve all arguments
7306                    self.compile_expr_ctx(index, WantarrayCtx::List)?;
7307                    self.emit_op(Op::ArrowCall(ctx.as_byte()), line, Some(root));
7308                }
7309            },
7310            ExprKind::Deref { expr, kind } => {
7311                // Perl: `scalar @{EXPR}` / `scalar @$r` is the array length (not a copy of the list).
7312                // `scalar %{EXPR}` uses hash fill metrics like `%h` in scalar context.
7313                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
7314                    self.compile_expr(expr)?;
7315                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
7316                } else if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
7317                    self.compile_expr(expr)?;
7318                    self.emit_op(Op::SymbolicDeref(2), line, Some(root));
7319                    self.emit_op(Op::ValueScalarContext, line, Some(root));
7320                } else {
7321                    self.compile_expr(expr)?;
7322                    let b = match kind {
7323                        Sigil::Scalar => 0u8,
7324                        Sigil::Array => 1,
7325                        Sigil::Hash => 2,
7326                        Sigil::Typeglob => 3,
7327                    };
7328                    self.emit_op(Op::SymbolicDeref(b), line, Some(root));
7329                }
7330            }
7331
7332            // ── Interpolated strings ──
7333            ExprKind::InterpolatedString(parts) => {
7334                // Check if any literal part contains case-escape sequences.
7335                let has_case_escapes = parts.iter().any(|p| {
7336                    if let StringPart::Literal(s) = p {
7337                        s.contains('\\')
7338                            && (s.contains("\\U")
7339                                || s.contains("\\L")
7340                                || s.contains("\\u")
7341                                || s.contains("\\l")
7342                                || s.contains("\\Q")
7343                                || s.contains("\\E"))
7344                    } else {
7345                        false
7346                    }
7347                });
7348                if parts.is_empty() {
7349                    let idx = self.chunk.add_constant(StrykeValue::string(String::new()));
7350                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7351                } else {
7352                    // `"$x"` is a single [`StringPart`] — still string context; must go through
7353                    // [`Op::Concat`] so operands are stringified (`use overload '""'`, etc.).
7354                    if !matches!(&parts[0], StringPart::Literal(_)) {
7355                        let idx = self.chunk.add_constant(StrykeValue::string(String::new()));
7356                        self.emit_op(Op::LoadConst(idx), line, Some(root));
7357                    }
7358                    self.compile_string_part(&parts[0], line, Some(root))?;
7359                    for part in &parts[1..] {
7360                        self.compile_string_part(part, line, Some(root))?;
7361                        self.emit_op(Op::Concat, line, Some(root));
7362                    }
7363                    if !matches!(&parts[0], StringPart::Literal(_)) {
7364                        self.emit_op(Op::Concat, line, Some(root));
7365                    }
7366                }
7367                if has_case_escapes {
7368                    self.emit_op(Op::ProcessCaseEscapes, line, Some(root));
7369                }
7370            }
7371
7372            // ── List ──
7373            ExprKind::List(exprs) => {
7374                if ctx == WantarrayCtx::Scalar {
7375                    // Perl: comma-list in scalar context evaluates to the **last** element (`(1,2)` → 2).
7376                    if let Some(last) = exprs.last() {
7377                        self.compile_expr_ctx(last, WantarrayCtx::Scalar)?;
7378                    } else {
7379                        self.emit_op(Op::LoadUndef, line, Some(root));
7380                    }
7381                } else {
7382                    for e in exprs {
7383                        self.compile_expr_ctx(e, ctx)?;
7384                    }
7385                    if exprs.len() != 1 {
7386                        self.emit_op(Op::MakeArray(exprs.len() as u16), line, Some(root));
7387                    }
7388                }
7389            }
7390
7391            // ── QW ──
7392            ExprKind::QW(words) => {
7393                for w in words {
7394                    let idx = self.chunk.add_constant(StrykeValue::string(w.clone()));
7395                    self.emit_op(Op::LoadConst(idx), line, Some(root));
7396                }
7397                self.emit_op(Op::MakeArray(words.len() as u16), line, Some(root));
7398            }
7399
7400            // ── Postfix if/unless ──
7401            ExprKind::PostfixIf { expr, condition } => {
7402                self.compile_boolean_rvalue_condition(condition)?;
7403                let j = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7404                self.compile_expr(expr)?;
7405                let end = self.emit_op(Op::Jump(0), line, Some(root));
7406                self.chunk.patch_jump_here(j);
7407                self.emit_op(Op::LoadUndef, line, Some(root));
7408                self.chunk.patch_jump_here(end);
7409            }
7410            ExprKind::PostfixUnless { expr, condition } => {
7411                self.compile_boolean_rvalue_condition(condition)?;
7412                let j = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
7413                self.compile_expr(expr)?;
7414                let end = self.emit_op(Op::Jump(0), line, Some(root));
7415                self.chunk.patch_jump_here(j);
7416                self.emit_op(Op::LoadUndef, line, Some(root));
7417                self.chunk.patch_jump_here(end);
7418            }
7419
7420            // ── Postfix while/until/foreach ──
7421            ExprKind::PostfixWhile { expr, condition } => {
7422                // Detect `do { BLOCK } while (COND)` pattern
7423                let is_do_block = matches!(
7424                    &expr.kind,
7425                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
7426                );
7427                if is_do_block {
7428                    // do-while: body executes before first condition check
7429                    let loop_start = self.chunk.len();
7430                    self.compile_expr(expr)?;
7431                    self.emit_op(Op::Pop, line, Some(root));
7432                    self.compile_boolean_rvalue_condition(condition)?;
7433                    self.emit_op(Op::JumpIfTrue(loop_start), line, Some(root));
7434                    self.emit_op(Op::LoadUndef, line, Some(root));
7435                } else {
7436                    // Regular postfix while: condition checked first
7437                    let loop_start = self.chunk.len();
7438                    self.compile_boolean_rvalue_condition(condition)?;
7439                    let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7440                    self.compile_expr(expr)?;
7441                    self.emit_op(Op::Pop, line, Some(root));
7442                    self.emit_op(Op::Jump(loop_start), line, Some(root));
7443                    self.chunk.patch_jump_here(exit_jump);
7444                    self.emit_op(Op::LoadUndef, line, Some(root));
7445                }
7446            }
7447            ExprKind::PostfixUntil { expr, condition } => {
7448                let is_do_block = matches!(
7449                    &expr.kind,
7450                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
7451                );
7452                if is_do_block {
7453                    let loop_start = self.chunk.len();
7454                    self.compile_expr(expr)?;
7455                    self.emit_op(Op::Pop, line, Some(root));
7456                    self.compile_boolean_rvalue_condition(condition)?;
7457                    self.emit_op(Op::JumpIfFalse(loop_start), line, Some(root));
7458                    self.emit_op(Op::LoadUndef, line, Some(root));
7459                } else {
7460                    let loop_start = self.chunk.len();
7461                    self.compile_boolean_rvalue_condition(condition)?;
7462                    let exit_jump = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
7463                    self.compile_expr(expr)?;
7464                    self.emit_op(Op::Pop, line, Some(root));
7465                    self.emit_op(Op::Jump(loop_start), line, Some(root));
7466                    self.chunk.patch_jump_here(exit_jump);
7467                    self.emit_op(Op::LoadUndef, line, Some(root));
7468                }
7469            }
7470            ExprKind::PostfixForeach { expr, list } => {
7471                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7472                let list_name = self.chunk.intern_name("__pf_foreach_list__");
7473                self.emit_op(Op::DeclareArray(list_name), line, Some(root));
7474                let counter = self.chunk.intern_name("__pf_foreach_i__");
7475                self.emit_op(Op::LoadInt(0), line, Some(root));
7476                self.emit_op(Op::DeclareScalar(counter), line, Some(root));
7477                let underscore = self.chunk.intern_name("_");
7478
7479                let loop_start = self.chunk.len();
7480                self.emit_get_scalar(counter, line, Some(root));
7481                self.emit_op(Op::ArrayLen(list_name), line, Some(root));
7482                self.emit_op(Op::NumLt, line, Some(root));
7483                let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
7484
7485                self.emit_get_scalar(counter, line, Some(root));
7486                self.emit_op(Op::GetArrayElem(list_name), line, Some(root));
7487                self.emit_set_scalar(underscore, line, Some(root));
7488
7489                self.compile_expr(expr)?;
7490                self.emit_op(Op::Pop, line, Some(root));
7491
7492                self.emit_pre_inc(counter, line, Some(root));
7493                self.emit_op(Op::Pop, line, Some(root));
7494                self.emit_op(Op::Jump(loop_start), line, Some(root));
7495                self.chunk.patch_jump_here(exit_jump);
7496                self.emit_op(Op::LoadUndef, line, Some(root));
7497            }
7498
7499            ExprKind::AlgebraicMatch { subject, arms } => {
7500                let idx = self
7501                    .chunk
7502                    .add_algebraic_match_entry(subject.as_ref().clone(), arms.clone());
7503                self.emit_op(Op::AlgebraicMatch(idx), line, Some(root));
7504            }
7505
7506            // ── Match (regex) ──
7507            ExprKind::Match {
7508                expr,
7509                pattern,
7510                flags,
7511                scalar_g,
7512                delim: _,
7513            } => {
7514                self.compile_expr(expr)?;
7515                let pat_idx = self
7516                    .chunk
7517                    .add_constant(StrykeValue::string(pattern.clone()));
7518                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7519                let pos_key_idx = if *scalar_g && flags.contains('g') {
7520                    if let ExprKind::ScalarVar(n) = &expr.kind {
7521                        let stor = self.scalar_storage_name_for_ops(n);
7522                        self.chunk.add_constant(StrykeValue::string(stor))
7523                    } else {
7524                        u16::MAX
7525                    }
7526                } else {
7527                    u16::MAX
7528                };
7529                self.emit_op(
7530                    Op::RegexMatch(pat_idx, flags_idx, *scalar_g, pos_key_idx),
7531                    line,
7532                    Some(root),
7533                );
7534            }
7535
7536            ExprKind::Substitution {
7537                expr,
7538                pattern,
7539                replacement,
7540                flags,
7541                delim: _,
7542            } => {
7543                self.compile_expr(expr)?;
7544                let pat_idx = self
7545                    .chunk
7546                    .add_constant(StrykeValue::string(pattern.clone()));
7547                let repl_idx = self
7548                    .chunk
7549                    .add_constant(StrykeValue::string(replacement.clone()));
7550                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7551                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
7552                self.emit_op(
7553                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lv_idx),
7554                    line,
7555                    Some(root),
7556                );
7557            }
7558            ExprKind::Transliterate {
7559                expr,
7560                from,
7561                to,
7562                flags,
7563                delim: _,
7564            } => {
7565                self.compile_expr(expr)?;
7566                let from_idx = self.chunk.add_constant(StrykeValue::string(from.clone()));
7567                let to_idx = self.chunk.add_constant(StrykeValue::string(to.clone()));
7568                let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7569                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
7570                self.emit_op(
7571                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lv_idx),
7572                    line,
7573                    Some(root),
7574                );
7575            }
7576
7577            // ── Regex literal ──
7578            ExprKind::Regex(pattern, flags) => {
7579                if ctx == WantarrayCtx::Void {
7580                    // Statement context: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a discarded regex object.
7581                    self.compile_boolean_rvalue_condition(root)?;
7582                } else {
7583                    let pat_idx = self
7584                        .chunk
7585                        .add_constant(StrykeValue::string(pattern.clone()));
7586                    let flags_idx = self.chunk.add_constant(StrykeValue::string(flags.clone()));
7587                    self.emit_op(Op::LoadRegex(pat_idx, flags_idx), line, Some(root));
7588                }
7589            }
7590
7591            // ── Map/Grep/Sort with blocks ──
7592            ExprKind::MapExpr {
7593                block,
7594                list,
7595                flatten_array_refs,
7596                stream,
7597            } => {
7598                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7599                if *stream {
7600                    let block_idx = self.add_deferred_block(block.clone());
7601                    if *flatten_array_refs {
7602                        self.emit_op(Op::MapsFlatMapWithBlock(block_idx), line, Some(root));
7603                    } else {
7604                        self.emit_op(Op::MapsWithBlock(block_idx), line, Some(root));
7605                    }
7606                } else if let Some(k) = crate::map_grep_fast::detect_map_int_mul(block) {
7607                    self.emit_op(Op::MapIntMul(k), line, Some(root));
7608                } else {
7609                    let block_idx = self.add_deferred_block(block.clone());
7610                    if *flatten_array_refs {
7611                        self.emit_op(Op::FlatMapWithBlock(block_idx), line, Some(root));
7612                    } else {
7613                        self.emit_op(Op::MapWithBlock(block_idx), line, Some(root));
7614                    }
7615                }
7616                if ctx != WantarrayCtx::List {
7617                    self.emit_op(Op::StackArrayLen, line, Some(root));
7618                }
7619            }
7620            ExprKind::MapExprComma {
7621                expr,
7622                list,
7623                flatten_array_refs,
7624                stream,
7625            } => {
7626                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7627                let idx = self.chunk.add_map_expr_entry(*expr.clone());
7628                if *stream {
7629                    if *flatten_array_refs {
7630                        self.emit_op(Op::MapsFlatMapWithExpr(idx), line, Some(root));
7631                    } else {
7632                        self.emit_op(Op::MapsWithExpr(idx), line, Some(root));
7633                    }
7634                } else if *flatten_array_refs {
7635                    self.emit_op(Op::FlatMapWithExpr(idx), line, Some(root));
7636                } else {
7637                    self.emit_op(Op::MapWithExpr(idx), line, Some(root));
7638                }
7639                if ctx != WantarrayCtx::List {
7640                    self.emit_op(Op::StackArrayLen, line, Some(root));
7641                }
7642            }
7643            ExprKind::ForEachExpr { block, list } => {
7644                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7645                let block_idx = self.add_deferred_block(block.clone());
7646                self.emit_op(Op::ForEachWithBlock(block_idx), line, Some(root));
7647            }
7648            ExprKind::GrepExpr {
7649                block,
7650                list,
7651                keyword,
7652            } => {
7653                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7654                if keyword.is_stream() {
7655                    let block_idx = self.add_deferred_block(block.clone());
7656                    self.emit_op(Op::FilterWithBlock(block_idx), line, Some(root));
7657                } else if let Some((m, r)) = crate::map_grep_fast::detect_grep_int_mod_eq(block) {
7658                    self.emit_op(Op::GrepIntModEq(m, r), line, Some(root));
7659                } else {
7660                    let block_idx = self.add_deferred_block(block.clone());
7661                    self.emit_op(Op::GrepWithBlock(block_idx), line, Some(root));
7662                }
7663                if ctx != WantarrayCtx::List {
7664                    self.emit_op(Op::StackArrayLen, line, Some(root));
7665                }
7666            }
7667            ExprKind::GrepExprComma {
7668                expr,
7669                list,
7670                keyword,
7671            } => {
7672                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7673                let idx = self.chunk.add_grep_expr_entry(*expr.clone());
7674                if keyword.is_stream() {
7675                    self.emit_op(Op::FilterWithExpr(idx), line, Some(root));
7676                } else {
7677                    self.emit_op(Op::GrepWithExpr(idx), line, Some(root));
7678                }
7679                if ctx != WantarrayCtx::List {
7680                    self.emit_op(Op::StackArrayLen, line, Some(root));
7681                }
7682            }
7683            ExprKind::SortExpr { cmp, list } => {
7684                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7685                match cmp {
7686                    Some(crate::ast::SortComparator::Block(block)) => {
7687                        if let Some(mode) = detect_sort_block_fast(block) {
7688                            let tag = match mode {
7689                                crate::sort_fast::SortBlockFast::Numeric => 0u8,
7690                                crate::sort_fast::SortBlockFast::String => 1u8,
7691                                crate::sort_fast::SortBlockFast::NumericRev => 2u8,
7692                                crate::sort_fast::SortBlockFast::StringRev => 3u8,
7693                            };
7694                            self.emit_op(Op::SortWithBlockFast(tag), line, Some(root));
7695                        } else {
7696                            let block_idx = self.add_deferred_block(block.clone());
7697                            self.register_sort_pair_block(block_idx);
7698                            self.emit_op(Op::SortWithBlock(block_idx), line, Some(root));
7699                        }
7700                    }
7701                    Some(crate::ast::SortComparator::Code(code_expr)) => {
7702                        self.compile_expr(code_expr)?;
7703                        self.emit_op(Op::SortWithCodeComparator(ctx.as_byte()), line, Some(root));
7704                    }
7705                    None => {
7706                        self.emit_op(Op::SortNoBlock, line, Some(root));
7707                    }
7708                }
7709            }
7710
7711            // ── Parallel extensions ──
7712            ExprKind::ParExpr { .. }
7713            | ExprKind::ParReduceExpr { .. }
7714            | ExprKind::DistReduceExpr { .. } => {
7715                // `par { BLOCK }`, `par_reduce { extract } [{ merge }]`, and
7716                // `~d>` (distributed thread macro) — bytecode VM has no
7717                // dedicated opcodes; defer AST eval to the interpreter via
7718                // `Op::EvalAstExpr`.
7719                let idx = self.chunk.ast_eval_exprs.len() as u16;
7720                self.chunk.ast_eval_exprs.push(root.clone());
7721                self.emit_op(Op::EvalAstExpr(idx), line, Some(root));
7722            }
7723            ExprKind::PMapExpr {
7724                block,
7725                list,
7726                progress,
7727                flat_outputs,
7728                on_cluster,
7729                stream,
7730            } => {
7731                if *stream {
7732                    // Streaming: no progress flag needed, just list + block.
7733                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7734                    let block_idx = self.add_deferred_block(block.clone());
7735                    if *flat_outputs {
7736                        self.emit_op(Op::PFlatMapsWithBlock(block_idx), line, Some(root));
7737                    } else {
7738                        self.emit_op(Op::PMapsWithBlock(block_idx), line, Some(root));
7739                    }
7740                } else {
7741                    if let Some(p) = progress {
7742                        self.compile_expr(p)?;
7743                    } else {
7744                        self.emit_op(Op::LoadInt(0), line, Some(root));
7745                    }
7746                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7747                    if let Some(cluster_e) = on_cluster {
7748                        self.compile_expr(cluster_e)?;
7749                        let block_idx = self.add_deferred_block(block.clone());
7750                        self.emit_op(
7751                            Op::PMapRemote {
7752                                block_idx,
7753                                flat: u8::from(*flat_outputs),
7754                            },
7755                            line,
7756                            Some(root),
7757                        );
7758                    } else {
7759                        let block_idx = self.add_deferred_block(block.clone());
7760                        if *flat_outputs {
7761                            self.emit_op(Op::PFlatMapWithBlock(block_idx), line, Some(root));
7762                        } else {
7763                            self.emit_op(Op::PMapWithBlock(block_idx), line, Some(root));
7764                        }
7765                    }
7766                }
7767            }
7768            ExprKind::PMapChunkedExpr {
7769                chunk_size,
7770                block,
7771                list,
7772                progress,
7773            } => {
7774                if let Some(p) = progress {
7775                    self.compile_expr(p)?;
7776                } else {
7777                    self.emit_op(Op::LoadInt(0), line, Some(root));
7778                }
7779                self.compile_expr(chunk_size)?;
7780                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7781                let block_idx = self.add_deferred_block(block.clone());
7782                self.emit_op(Op::PMapChunkedWithBlock(block_idx), line, Some(root));
7783            }
7784            ExprKind::PGrepExpr {
7785                block,
7786                list,
7787                progress,
7788                stream,
7789            } => {
7790                if *stream {
7791                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7792                    let block_idx = self.add_deferred_block(block.clone());
7793                    self.emit_op(Op::PGrepsWithBlock(block_idx), line, Some(root));
7794                } else {
7795                    if let Some(p) = progress {
7796                        self.compile_expr(p)?;
7797                    } else {
7798                        self.emit_op(Op::LoadInt(0), line, Some(root));
7799                    }
7800                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
7801                    let block_idx = self.add_deferred_block(block.clone());
7802                    self.emit_op(Op::PGrepWithBlock(block_idx), line, Some(root));
7803                }
7804            }
7805            ExprKind::PForExpr {
7806                block,
7807                list,
7808                progress,
7809            } => {
7810                if let Some(p) = progress {
7811                    self.compile_expr(p)?;
7812                } else {
7813                    self.emit_op(Op::LoadInt(0), line, Some(root));
7814                }
7815                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7816                let block_idx = self.add_deferred_block(block.clone());
7817                self.emit_op(Op::PForWithBlock(block_idx), line, Some(root));
7818            }
7819            ExprKind::ParLinesExpr {
7820                path,
7821                callback,
7822                progress,
7823            } => {
7824                let idx = self.chunk.add_par_lines_entry(
7825                    path.as_ref().clone(),
7826                    callback.as_ref().clone(),
7827                    progress.as_ref().map(|p| p.as_ref().clone()),
7828                );
7829                self.emit_op(Op::ParLines(idx), line, Some(root));
7830            }
7831            ExprKind::ParWalkExpr {
7832                path,
7833                callback,
7834                progress,
7835            } => {
7836                let idx = self.chunk.add_par_walk_entry(
7837                    path.as_ref().clone(),
7838                    callback.as_ref().clone(),
7839                    progress.as_ref().map(|p| p.as_ref().clone()),
7840                );
7841                self.emit_op(Op::ParWalk(idx), line, Some(root));
7842            }
7843            ExprKind::PwatchExpr { path, callback } => {
7844                let idx = self
7845                    .chunk
7846                    .add_pwatch_entry(path.as_ref().clone(), callback.as_ref().clone());
7847                self.emit_op(Op::Pwatch(idx), line, Some(root));
7848            }
7849            ExprKind::PSortExpr {
7850                cmp,
7851                list,
7852                progress,
7853            } => {
7854                if let Some(p) = progress {
7855                    self.compile_expr(p)?;
7856                } else {
7857                    self.emit_op(Op::LoadInt(0), line, Some(root));
7858                }
7859                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7860                if let Some(block) = cmp {
7861                    if let Some(mode) = detect_sort_block_fast(block) {
7862                        let tag = match mode {
7863                            crate::sort_fast::SortBlockFast::Numeric => 0u8,
7864                            crate::sort_fast::SortBlockFast::String => 1u8,
7865                            crate::sort_fast::SortBlockFast::NumericRev => 2u8,
7866                            crate::sort_fast::SortBlockFast::StringRev => 3u8,
7867                        };
7868                        self.emit_op(Op::PSortWithBlockFast(tag), line, Some(root));
7869                    } else {
7870                        let block_idx = self.add_deferred_block(block.clone());
7871                        self.register_sort_pair_block(block_idx);
7872                        self.emit_op(Op::PSortWithBlock(block_idx), line, Some(root));
7873                    }
7874                } else {
7875                    self.emit_op(Op::PSortNoBlockParallel, line, Some(root));
7876                }
7877            }
7878            ExprKind::ReduceExpr { block, list } => {
7879                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7880                let block_idx = self.add_deferred_block(block.clone());
7881                self.register_sort_pair_block(block_idx);
7882                self.emit_op(Op::ReduceWithBlock(block_idx), line, Some(root));
7883            }
7884            ExprKind::PReduceExpr {
7885                block,
7886                list,
7887                progress,
7888            } => {
7889                if let Some(p) = progress {
7890                    self.compile_expr(p)?;
7891                } else {
7892                    self.emit_op(Op::LoadInt(0), line, Some(root));
7893                }
7894                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7895                let block_idx = self.add_deferred_block(block.clone());
7896                self.register_sort_pair_block(block_idx);
7897                self.emit_op(Op::PReduceWithBlock(block_idx), line, Some(root));
7898            }
7899            ExprKind::PReduceInitExpr {
7900                init,
7901                block,
7902                list,
7903                progress,
7904            } => {
7905                if let Some(p) = progress {
7906                    self.compile_expr(p)?;
7907                } else {
7908                    self.emit_op(Op::LoadInt(0), line, Some(root));
7909                }
7910                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7911                self.compile_expr(init)?;
7912                let block_idx = self.add_deferred_block(block.clone());
7913                self.emit_op(Op::PReduceInitWithBlock(block_idx), line, Some(root));
7914            }
7915            ExprKind::PMapReduceExpr {
7916                map_block,
7917                reduce_block,
7918                list,
7919                progress,
7920            } => {
7921                if let Some(p) = progress {
7922                    self.compile_expr(p)?;
7923                } else {
7924                    self.emit_op(Op::LoadInt(0), line, Some(root));
7925                }
7926                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7927                let map_idx = self.add_deferred_block(map_block.clone());
7928                let reduce_idx = self.add_deferred_block(reduce_block.clone());
7929                self.emit_op(
7930                    Op::PMapReduceWithBlocks(map_idx, reduce_idx),
7931                    line,
7932                    Some(root),
7933                );
7934            }
7935            ExprKind::PcacheExpr {
7936                block,
7937                list,
7938                progress,
7939            } => {
7940                if let Some(p) = progress {
7941                    self.compile_expr(p)?;
7942                } else {
7943                    self.emit_op(Op::LoadInt(0), line, Some(root));
7944                }
7945                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7946                let block_idx = self.add_deferred_block(block.clone());
7947                self.emit_op(Op::PcacheWithBlock(block_idx), line, Some(root));
7948            }
7949            ExprKind::PselectExpr { receivers, timeout } => {
7950                let n = receivers.len();
7951                if n > u8::MAX as usize {
7952                    return Err(CompileError::Unsupported(
7953                        "pselect: too many receivers".into(),
7954                    ));
7955                }
7956                for r in receivers {
7957                    self.compile_expr(r)?;
7958                }
7959                let has_timeout = timeout.is_some();
7960                if let Some(t) = timeout {
7961                    self.compile_expr(t)?;
7962                }
7963                self.emit_op(
7964                    Op::Pselect {
7965                        n_rx: n as u8,
7966                        has_timeout,
7967                    },
7968                    line,
7969                    Some(root),
7970                );
7971            }
7972            ExprKind::FanExpr {
7973                count,
7974                block,
7975                progress,
7976                capture,
7977            } => {
7978                if let Some(p) = progress {
7979                    self.compile_expr(p)?;
7980                } else {
7981                    self.emit_op(Op::LoadInt(0), line, Some(root));
7982                }
7983                let block_idx = self.add_deferred_block(block.clone());
7984                match (count, capture) {
7985                    (Some(c), false) => {
7986                        self.compile_expr(c)?;
7987                        self.emit_op(Op::FanWithBlock(block_idx), line, Some(root));
7988                    }
7989                    (None, false) => {
7990                        self.emit_op(Op::FanWithBlockAuto(block_idx), line, Some(root));
7991                    }
7992                    (Some(c), true) => {
7993                        self.compile_expr(c)?;
7994                        self.emit_op(Op::FanCapWithBlock(block_idx), line, Some(root));
7995                    }
7996                    (None, true) => {
7997                        self.emit_op(Op::FanCapWithBlockAuto(block_idx), line, Some(root));
7998                    }
7999                }
8000            }
8001            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
8002                let block_idx = self.add_deferred_block(body.clone());
8003                self.emit_op(Op::AsyncBlock(block_idx), line, Some(root));
8004            }
8005            ExprKind::Trace { body } => {
8006                let block_idx = self.add_deferred_block(body.clone());
8007                self.emit_op(Op::TraceBlock(block_idx), line, Some(root));
8008            }
8009            ExprKind::Timer { body } => {
8010                let block_idx = self.add_deferred_block(body.clone());
8011                self.emit_op(Op::TimerBlock(block_idx), line, Some(root));
8012            }
8013            ExprKind::Bench { body, times } => {
8014                self.compile_expr(times)?;
8015                let block_idx = self.add_deferred_block(body.clone());
8016                self.emit_op(Op::BenchBlock(block_idx), line, Some(root));
8017            }
8018            ExprKind::Await(e) => {
8019                self.compile_expr(e)?;
8020                self.emit_op(Op::Await, line, Some(root));
8021            }
8022            ExprKind::Slurp(e) => {
8023                self.compile_expr(e)?;
8024                self.emit_op(
8025                    Op::CallBuiltin(BuiltinId::Slurp as u16, 1),
8026                    line,
8027                    Some(root),
8028                );
8029            }
8030            ExprKind::Capture(e) => {
8031                self.compile_expr(e)?;
8032                self.emit_op(
8033                    Op::CallBuiltin(BuiltinId::Capture as u16, 1),
8034                    line,
8035                    Some(root),
8036                );
8037            }
8038            ExprKind::Qx(e) => {
8039                self.compile_expr(e)?;
8040                self.emit_op(
8041                    Op::CallBuiltin(BuiltinId::Readpipe as u16, 1),
8042                    line,
8043                    Some(root),
8044                );
8045            }
8046            ExprKind::FetchUrl(e) => {
8047                self.compile_expr(e)?;
8048                self.emit_op(
8049                    Op::CallBuiltin(BuiltinId::FetchUrl as u16, 1),
8050                    line,
8051                    Some(root),
8052                );
8053            }
8054            ExprKind::Pchannel { capacity } => {
8055                if let Some(c) = capacity {
8056                    self.compile_expr(c)?;
8057                    self.emit_op(
8058                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 1),
8059                        line,
8060                        Some(root),
8061                    );
8062                } else {
8063                    self.emit_op(
8064                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 0),
8065                        line,
8066                        Some(root),
8067                    );
8068                }
8069            }
8070            ExprKind::RetryBlock { .. }
8071            | ExprKind::RateLimitBlock { .. }
8072            | ExprKind::EveryBlock { .. }
8073            | ExprKind::GenBlock { .. }
8074            | ExprKind::Yield(_)
8075            | ExprKind::Spinner { .. } => {
8076                let idx = self.chunk.ast_eval_exprs.len() as u16;
8077                self.chunk.ast_eval_exprs.push(root.clone());
8078                self.emit_op(Op::EvalAstExpr(idx), line, Some(root));
8079            }
8080            ExprKind::MyExpr { keyword, decls } => {
8081                // `my $x = EXPR` in expression context (e.g. `while (my $line = <$fh>)`)
8082                // Compile the declaration, then leave the value on the stack for the caller.
8083                if decls.len() == 1 && decls[0].sigil == Sigil::Scalar {
8084                    let decl = &decls[0];
8085                    if let Some(init) = &decl.initializer {
8086                        self.compile_expr(init)?;
8087                    } else {
8088                        self.chunk.emit(Op::LoadUndef, line);
8089                    }
8090                    // Dup so the value stays on stack after DeclareScalar consumes one copy
8091                    self.emit_op(Op::Dup, line, Some(root));
8092                    let name_idx = self.chunk.intern_name(&decl.name);
8093                    match keyword.as_str() {
8094                        "state" => {
8095                            let name = self.chunk.names[name_idx as usize].clone();
8096                            self.register_declare(Sigil::Scalar, &name, false);
8097                            self.chunk.emit(Op::DeclareStateScalar(name_idx), line);
8098                        }
8099                        _ => {
8100                            self.emit_declare_scalar(name_idx, line, false);
8101                        }
8102                    }
8103                } else {
8104                    return Err(CompileError::Unsupported(
8105                        "my/our/state/local in expression context with multiple or non-scalar decls".into(),
8106                    ));
8107                }
8108            }
8109        }
8110        Ok(())
8111    }
8112
8113    fn compile_string_part(
8114        &mut self,
8115        part: &StringPart,
8116        line: usize,
8117        parent: Option<&Expr>,
8118    ) -> Result<(), CompileError> {
8119        match part {
8120            StringPart::Literal(s) => {
8121                let idx = self.chunk.add_constant(StrykeValue::string(s.clone()));
8122                self.emit_op(Op::LoadConst(idx), line, parent);
8123            }
8124            StringPart::ScalarVar(name) => {
8125                let idx = self.intern_scalar_var_for_ops(name);
8126                self.emit_get_scalar(idx, line, parent);
8127            }
8128            StringPart::ArrayVar(name) => {
8129                let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
8130                self.emit_op(Op::GetArray(idx), line, parent);
8131                self.emit_op(Op::ArrayStringifyListSep, line, parent);
8132            }
8133            StringPart::Expr(e) => {
8134                // Interpolation uses list/array values (`$"`), not Perl scalar(@arr) length.
8135                if matches!(&e.kind, ExprKind::ArraySlice { .. })
8136                    || matches!(
8137                        &e.kind,
8138                        ExprKind::Deref {
8139                            kind: Sigil::Array,
8140                            ..
8141                        }
8142                    )
8143                {
8144                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
8145                    self.emit_op(Op::ArrayStringifyListSep, line, parent);
8146                } else {
8147                    self.compile_expr(e)?;
8148                }
8149            }
8150        }
8151        Ok(())
8152    }
8153
8154    fn compile_assign(
8155        &mut self,
8156        target: &Expr,
8157        line: usize,
8158        keep: bool,
8159        ast: Option<&Expr>,
8160    ) -> Result<(), CompileError> {
8161        match &target.kind {
8162            ExprKind::ScalarVar(name) => {
8163                self.check_strict_scalar_access(name, line)?;
8164                self.check_scalar_mutable(name, line)?;
8165                self.check_closure_write_to_outer_my(name, line)?;
8166                let idx = self.intern_scalar_var_for_ops(name);
8167                if keep {
8168                    self.emit_set_scalar_keep(idx, line, ast);
8169                } else {
8170                    self.emit_set_scalar(idx, line, ast);
8171                }
8172            }
8173            ExprKind::ArrayVar(name) => {
8174                self.check_strict_array_access(name, line)?;
8175                let q = self.qualify_stash_array_name(name);
8176                self.check_array_mutable(&q, line)?;
8177                let idx = self.chunk.intern_name(&q);
8178                self.emit_op(Op::SetArray(idx), line, ast);
8179                if keep {
8180                    self.emit_op(Op::GetArray(idx), line, ast);
8181                }
8182            }
8183            ExprKind::HashVar(name) => {
8184                self.check_strict_hash_access(name, line)?;
8185                self.check_hash_mutable(name, line)?;
8186                let idx = self.chunk.intern_name(name);
8187                self.emit_op(Op::SetHash(idx), line, ast);
8188                if keep {
8189                    self.emit_op(Op::GetHash(idx), line, ast);
8190                }
8191            }
8192            ExprKind::ArrayElement { array, index } => {
8193                self.check_strict_array_access(array, line)?;
8194                let q = self.qualify_stash_array_name(array);
8195                self.check_array_mutable(&q, line)?;
8196                let idx = self.chunk.intern_name(&q);
8197                self.compile_expr(index)?;
8198                self.emit_op(Op::SetArrayElem(idx), line, ast);
8199            }
8200            ExprKind::ArraySlice { array, indices } => {
8201                if indices.is_empty() {
8202                    if self.is_mysync_array(array) {
8203                        return Err(CompileError::Unsupported(
8204                            "mysync array slice assign".into(),
8205                        ));
8206                    }
8207                    self.check_strict_array_access(array, line)?;
8208                    let q = self.qualify_stash_array_name(array);
8209                    self.check_array_mutable(&q, line)?;
8210                    let arr_idx = self.chunk.intern_name(&q);
8211                    self.emit_op(Op::SetNamedArraySlice(arr_idx, 0), line, ast);
8212                    if keep {
8213                        self.emit_op(Op::MakeArray(0), line, ast);
8214                    }
8215                    return Ok(());
8216                }
8217                if self.is_mysync_array(array) {
8218                    return Err(CompileError::Unsupported(
8219                        "mysync array slice assign".into(),
8220                    ));
8221                }
8222                self.check_strict_array_access(array, line)?;
8223                let q = self.qualify_stash_array_name(array);
8224                self.check_array_mutable(&q, line)?;
8225                let arr_idx = self.chunk.intern_name(&q);
8226                for ix in indices {
8227                    self.compile_array_slice_index_expr(ix)?;
8228                }
8229                self.emit_op(
8230                    Op::SetNamedArraySlice(arr_idx, indices.len() as u16),
8231                    line,
8232                    ast,
8233                );
8234                if keep {
8235                    for (ix, index_expr) in indices.iter().enumerate() {
8236                        self.compile_array_slice_index_expr(index_expr)?;
8237                        self.emit_op(Op::ArraySlicePart(arr_idx), line, ast);
8238                        if ix > 0 {
8239                            self.emit_op(Op::ArrayConcatTwo, line, ast);
8240                        }
8241                    }
8242                }
8243                return Ok(());
8244            }
8245            ExprKind::HashElement { hash, key } => {
8246                self.check_strict_hash_access(hash, line)?;
8247                self.check_hash_mutable(hash, line)?;
8248                let idx = self.chunk.intern_name(hash);
8249                self.compile_expr(key)?;
8250                if keep {
8251                    self.emit_op(Op::SetHashElemKeep(idx), line, ast);
8252                } else {
8253                    self.emit_op(Op::SetHashElem(idx), line, ast);
8254                }
8255            }
8256            ExprKind::HashSlice { hash, keys } => {
8257                if keys.is_empty() {
8258                    if self.is_mysync_hash(hash) {
8259                        return Err(CompileError::Unsupported("mysync hash slice assign".into()));
8260                    }
8261                    self.check_strict_hash_access(hash, line)?;
8262                    self.check_hash_mutable(hash, line)?;
8263                    let hash_idx = self.chunk.intern_name(hash);
8264                    self.emit_op(Op::SetHashSlice(hash_idx, 0), line, ast);
8265                    if keep {
8266                        self.emit_op(Op::MakeArray(0), line, ast);
8267                    }
8268                    return Ok(());
8269                }
8270                if self.is_mysync_hash(hash) {
8271                    return Err(CompileError::Unsupported("mysync hash slice assign".into()));
8272                }
8273                self.check_strict_hash_access(hash, line)?;
8274                self.check_hash_mutable(hash, line)?;
8275                let hash_idx = self.chunk.intern_name(hash);
8276                // Multi-key entries (`'a'..'c'`, `qw/a b/`, list literals) push an array value;
8277                // [`Self::assign_named_hash_slice`] / [`crate::bytecode::Op::SetHashSlice`]
8278                // flattens it at runtime, so compile in list context (scalar context collapses
8279                // `..` to a flip-flop).
8280                for key_expr in keys {
8281                    self.compile_hash_slice_key_expr(key_expr)?;
8282                }
8283                self.emit_op(Op::SetHashSlice(hash_idx, keys.len() as u16), line, ast);
8284                if keep {
8285                    for key_expr in keys {
8286                        self.compile_expr(key_expr)?;
8287                        self.emit_op(Op::GetHashElem(hash_idx), line, ast);
8288                    }
8289                    self.emit_op(Op::MakeArray(keys.len() as u16), line, ast);
8290                }
8291                return Ok(());
8292            }
8293            ExprKind::Deref {
8294                expr,
8295                kind: Sigil::Scalar,
8296            } => {
8297                self.compile_expr(expr)?;
8298                if keep {
8299                    self.emit_op(Op::SetSymbolicScalarRefKeep, line, ast);
8300                } else {
8301                    self.emit_op(Op::SetSymbolicScalarRef, line, ast);
8302                }
8303            }
8304            ExprKind::Deref {
8305                expr,
8306                kind: Sigil::Array,
8307            } => {
8308                self.compile_expr(expr)?;
8309                self.emit_op(Op::SetSymbolicArrayRef, line, ast);
8310            }
8311            ExprKind::Deref {
8312                expr,
8313                kind: Sigil::Hash,
8314            } => {
8315                self.compile_expr(expr)?;
8316                self.emit_op(Op::SetSymbolicHashRef, line, ast);
8317            }
8318            ExprKind::Deref {
8319                expr,
8320                kind: Sigil::Typeglob,
8321            } => {
8322                self.compile_expr(expr)?;
8323                self.emit_op(Op::SetSymbolicTypeglobRef, line, ast);
8324            }
8325            ExprKind::Typeglob(name) => {
8326                let idx = self.chunk.intern_name(name);
8327                if keep {
8328                    self.emit_op(Op::TypeglobAssignFromValue(idx), line, ast);
8329                } else {
8330                    return Err(CompileError::Unsupported(
8331                        "typeglob assign without keep (internal)".into(),
8332                    ));
8333                }
8334            }
8335            ExprKind::AnonymousListSlice { source, indices } => {
8336                if let ExprKind::Deref {
8337                    expr: inner,
8338                    kind: Sigil::Array,
8339                } = &source.kind
8340                {
8341                    if indices.is_empty() {
8342                        return Err(CompileError::Unsupported(
8343                            "assign to empty list slice (internal)".into(),
8344                        ));
8345                    }
8346                    self.compile_arrow_array_base_expr(inner)?;
8347                    for ix in indices {
8348                        self.compile_array_slice_index_expr(ix)?;
8349                    }
8350                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
8351                    if keep {
8352                        self.compile_arrow_array_base_expr(inner)?;
8353                        for ix in indices {
8354                            self.compile_array_slice_index_expr(ix)?;
8355                        }
8356                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
8357                    }
8358                    return Ok(());
8359                }
8360                return Err(CompileError::Unsupported(
8361                    "assign to anonymous list slice (non-@array-deref base)".into(),
8362                ));
8363            }
8364            ExprKind::ArrowDeref {
8365                expr,
8366                index,
8367                kind: DerefKind::Hash,
8368            } => {
8369                self.compile_arrow_hash_base_expr(expr)?;
8370                self.compile_expr(index)?;
8371                if keep {
8372                    self.emit_op(Op::SetArrowHashKeep, line, ast);
8373                } else {
8374                    self.emit_op(Op::SetArrowHash, line, ast);
8375                }
8376            }
8377            ExprKind::ArrowDeref {
8378                expr,
8379                index,
8380                kind: DerefKind::Array,
8381            } => {
8382                if let ExprKind::List(indices) = &index.kind {
8383                    // Multi-index slice assignment: RHS value is already on the stack (pushed
8384                    // by the enclosing `compile_expr(value)` before `compile_assign` was called
8385                    // with keep = true). `SetArrowArraySlice` delegates to
8386                    // `Interpreter::assign_arrow_array_slice` for element-wise write.
8387                    self.compile_arrow_array_base_expr(expr)?;
8388                    for ix in indices {
8389                        self.compile_array_slice_index_expr(ix)?;
8390                    }
8391                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
8392                    if keep {
8393                        // The Set op pops the value; keep callers re-read via a fresh slice read.
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::ArrowArraySlice(indices.len() as u16), line, ast);
8399                    }
8400                    return Ok(());
8401                }
8402                if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
8403                    self.compile_arrow_array_base_expr(expr)?;
8404                    self.compile_expr(index)?;
8405                    if keep {
8406                        self.emit_op(Op::SetArrowArrayKeep, line, ast);
8407                    } else {
8408                        self.emit_op(Op::SetArrowArray, line, ast);
8409                    }
8410                } else {
8411                    self.compile_arrow_array_base_expr(expr)?;
8412                    self.compile_array_slice_index_expr(index)?;
8413                    self.emit_op(Op::SetArrowArraySlice(1), line, ast);
8414                    if keep {
8415                        self.compile_arrow_array_base_expr(expr)?;
8416                        self.compile_array_slice_index_expr(index)?;
8417                        self.emit_op(Op::ArrowArraySlice(1), line, ast);
8418                    }
8419                }
8420            }
8421            ExprKind::ArrowDeref {
8422                kind: DerefKind::Call,
8423                ..
8424            } => {
8425                return Err(CompileError::Unsupported(
8426                    "Assign to arrow call deref".into(),
8427                ));
8428            }
8429            ExprKind::HashSliceDeref { container, keys } => {
8430                self.compile_expr(container)?;
8431                for key_expr in keys {
8432                    self.compile_hash_slice_key_expr(key_expr)?;
8433                }
8434                self.emit_op(Op::SetHashSliceDeref(keys.len() as u16), line, ast);
8435            }
8436            ExprKind::Pos(inner) => {
8437                let Some(inner_e) = inner.as_ref() else {
8438                    return Err(CompileError::Unsupported(
8439                        "assign to pos() without scalar".into(),
8440                    ));
8441                };
8442                if keep {
8443                    self.emit_op(Op::Dup, line, ast);
8444                }
8445                match &inner_e.kind {
8446                    ExprKind::ScalarVar(name) => {
8447                        let stor = self.scalar_storage_name_for_ops(name);
8448                        let idx = self.chunk.add_constant(StrykeValue::string(stor));
8449                        self.emit_op(Op::LoadConst(idx), line, ast);
8450                    }
8451                    _ => {
8452                        self.compile_expr(inner_e)?;
8453                    }
8454                }
8455                self.emit_op(Op::SetRegexPos, line, ast);
8456            }
8457            // List assignment: `($a, $b) = (val1, val2)` — RHS is on stack as array,
8458            // store into temp, then distribute elements to each target.
8459            ExprKind::List(targets) => {
8460                let tmp = self.chunk.intern_name("__list_assign_swap__");
8461                self.emit_op(Op::DeclareArray(tmp), line, ast);
8462                for (i, t) in targets.iter().enumerate() {
8463                    self.emit_op(Op::LoadInt(i as i64), line, ast);
8464                    self.emit_op(Op::GetArrayElem(tmp), line, ast);
8465                    self.compile_assign(t, line, false, ast)?;
8466                }
8467                if keep {
8468                    self.emit_op(Op::GetArray(tmp), line, ast);
8469                }
8470            }
8471            _ => {
8472                return Err(CompileError::Unsupported("Assign to complex lvalue".into()));
8473            }
8474        }
8475        Ok(())
8476    }
8477}
8478
8479/// Map a binary op to its stack opcode for compound assignment on aggregates (`$a[$i]`, `$h{$k}`).
8480pub(crate) fn binop_to_vm_op(op: BinOp) -> Option<Op> {
8481    Some(match op {
8482        BinOp::Add => Op::Add,
8483        BinOp::Sub => Op::Sub,
8484        BinOp::Mul => Op::Mul,
8485        BinOp::Div => Op::Div,
8486        BinOp::Mod => Op::Mod,
8487        BinOp::Pow => Op::Pow,
8488        BinOp::Concat => Op::Concat,
8489        BinOp::BitAnd => Op::BitAnd,
8490        BinOp::BitOr => Op::BitOr,
8491        BinOp::BitXor => Op::BitXor,
8492        BinOp::ShiftLeft => Op::Shl,
8493        BinOp::ShiftRight => Op::Shr,
8494        _ => return None,
8495    })
8496}
8497
8498/// Encode/decode scalar compound ops for [`Op::ScalarCompoundAssign`].
8499pub(crate) fn scalar_compound_op_to_byte(op: BinOp) -> Option<u8> {
8500    Some(match op {
8501        BinOp::Add => 0,
8502        BinOp::Sub => 1,
8503        BinOp::Mul => 2,
8504        BinOp::Div => 3,
8505        BinOp::Mod => 4,
8506        BinOp::Pow => 5,
8507        BinOp::Concat => 6,
8508        BinOp::BitAnd => 7,
8509        BinOp::BitOr => 8,
8510        BinOp::BitXor => 9,
8511        BinOp::ShiftLeft => 10,
8512        BinOp::ShiftRight => 11,
8513        _ => return None,
8514    })
8515}
8516
8517pub(crate) fn scalar_compound_op_from_byte(b: u8) -> Option<BinOp> {
8518    Some(match b {
8519        0 => BinOp::Add,
8520        1 => BinOp::Sub,
8521        2 => BinOp::Mul,
8522        3 => BinOp::Div,
8523        4 => BinOp::Mod,
8524        5 => BinOp::Pow,
8525        6 => BinOp::Concat,
8526        7 => BinOp::BitAnd,
8527        8 => BinOp::BitOr,
8528        9 => BinOp::BitXor,
8529        10 => BinOp::ShiftLeft,
8530        11 => BinOp::ShiftRight,
8531        _ => return None,
8532    })
8533}
8534
8535#[cfg(test)]
8536mod tests {
8537    use super::*;
8538    use crate::bytecode::{BuiltinId, Op, GP_RUN};
8539    use crate::parse;
8540
8541    fn compile_snippet(code: &str) -> Result<Chunk, CompileError> {
8542        let program = parse(code).expect("parse snippet");
8543        Compiler::new().compile_program(&program)
8544    }
8545
8546    fn assert_last_halt(chunk: &Chunk) {
8547        assert!(
8548            matches!(chunk.ops.last(), Some(Op::Halt)),
8549            "expected Halt last, got {:?}",
8550            chunk.ops.last()
8551        );
8552    }
8553
8554    #[test]
8555    fn compile_empty_program_emits_run_phase_then_halt() {
8556        let chunk = compile_snippet("").expect("compile");
8557        assert_eq!(chunk.ops.len(), 2);
8558        assert!(matches!(chunk.ops[0], Op::SetGlobalPhase(p) if p == GP_RUN));
8559        assert!(matches!(chunk.ops[1], Op::Halt));
8560    }
8561
8562    #[test]
8563    fn compile_struct_method_fn_eq_shorthand() {
8564        compile_snippet("struct CmpS { fn triple($n) = $n * 3 }").expect("compile");
8565    }
8566
8567    #[test]
8568    fn compile_integer_literal_statement() {
8569        let chunk = compile_snippet("42;").expect("compile");
8570        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadInt(42))));
8571        assert_last_halt(&chunk);
8572    }
8573
8574    #[test]
8575    fn compile_pos_assign_emits_set_regex_pos() {
8576        let chunk = compile_snippet(r#"$_ = ""; pos = 3;"#).expect("compile");
8577        assert!(
8578            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
8579            "expected SetRegexPos in {:?}",
8580            chunk.ops
8581        );
8582    }
8583
8584    #[test]
8585    fn compile_pos_deref_scalar_assign_emits_set_regex_pos() {
8586        let chunk = compile_snippet(
8587            r#"no strict 'vars';
8588            my $s;
8589            my $r = \$s;
8590            pos $$r = 0;"#,
8591        )
8592        .expect("compile");
8593        assert!(
8594            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
8595            r"expected SetRegexPos for pos $$r =, got {:?}",
8596            chunk.ops
8597        );
8598    }
8599
8600    #[test]
8601    fn compile_map_expr_comma_emits_map_with_expr() {
8602        let chunk = compile_snippet(
8603            r#"no strict 'vars';
8604            (map $_ + 1, (4, 5)) |> join ','"#,
8605        )
8606        .expect("compile");
8607        assert!(
8608            chunk.ops.iter().any(|o| matches!(o, Op::MapWithExpr(_))),
8609            "expected MapWithExpr, got {:?}",
8610            chunk.ops
8611        );
8612    }
8613
8614    #[test]
8615    fn compile_hash_slice_deref_assign_emits_set_op() {
8616        let code = r#"no strict 'vars';
8617        my $h = { "a" => 1, "b" => 2 };
8618        my $r = $h;
8619        @$r{"a", "b"} = (10, 20);
8620        $r->{"a"} . "," . $r->{"b"};"#;
8621        let chunk = compile_snippet(code).expect("compile");
8622        assert!(
8623            chunk
8624                .ops
8625                .iter()
8626                .any(|o| matches!(o, Op::SetHashSliceDeref(n) if *n == 2)),
8627            "expected SetHashSliceDeref(2), got {:?}",
8628            chunk.ops
8629        );
8630    }
8631
8632    #[test]
8633    fn compile_bare_array_assign_diamond_uses_readline_list() {
8634        let chunk = compile_snippet("@a = <>;").expect("compile");
8635        assert!(
8636            chunk.ops.iter().any(|o| matches!(
8637                o,
8638                Op::CallBuiltin(bid, 0) if *bid == BuiltinId::ReadLineList as u16
8639            )),
8640            "expected ReadLineList for bare @a = <>, got {:?}",
8641            chunk.ops
8642        );
8643    }
8644
8645    #[test]
8646    fn compile_float_literal() {
8647        let chunk = compile_snippet("3.25;").expect("compile");
8648        assert!(chunk
8649            .ops
8650            .iter()
8651            .any(|o| matches!(o, Op::LoadFloat(f) if (*f - 3.25).abs() < 1e-9)));
8652        assert_last_halt(&chunk);
8653    }
8654
8655    #[test]
8656    fn compile_addition() {
8657        let chunk = compile_snippet("1 + 2;").expect("compile");
8658        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Add)));
8659        assert_last_halt(&chunk);
8660    }
8661
8662    #[test]
8663    fn compile_sub_mul_div_mod_pow() {
8664        for (src, op) in [
8665            ("10 - 3;", "Sub"),
8666            ("6 * 7;", "Mul"),
8667            ("8 / 2;", "Div"),
8668            ("9 % 4;", "Mod"),
8669            ("2 ** 8;", "Pow"),
8670        ] {
8671            let chunk = compile_snippet(src).expect(src);
8672            assert!(
8673                chunk.ops.iter().any(|o| std::mem::discriminant(o) == {
8674                    let dummy = match op {
8675                        "Sub" => Op::Sub,
8676                        "Mul" => Op::Mul,
8677                        "Div" => Op::Div,
8678                        "Mod" => Op::Mod,
8679                        "Pow" => Op::Pow,
8680                        _ => unreachable!(),
8681                    };
8682                    std::mem::discriminant(&dummy)
8683                }),
8684                "{} missing {:?}",
8685                src,
8686                op
8687            );
8688            assert_last_halt(&chunk);
8689        }
8690    }
8691
8692    #[test]
8693    fn compile_string_literal_uses_constant_pool() {
8694        let chunk = compile_snippet(r#""hello";"#).expect("compile");
8695        assert!(chunk
8696            .constants
8697            .iter()
8698            .any(|c| c.as_str().as_deref() == Some("hello")));
8699        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadConst(_))));
8700        assert_last_halt(&chunk);
8701    }
8702
8703    #[test]
8704    fn compile_substitution_bind_emits_regex_subst() {
8705        let chunk = compile_snippet(r#"my $s = "aa"; $s =~ s/a/b/g;"#).expect("compile");
8706        assert!(
8707            chunk
8708                .ops
8709                .iter()
8710                .any(|o| matches!(o, Op::RegexSubst(_, _, _, _))),
8711            "expected RegexSubst in {:?}",
8712            chunk.ops
8713        );
8714        assert!(!chunk.lvalues.is_empty());
8715    }
8716
8717    #[test]
8718    fn compile_chomp_emits_chomp_in_place() {
8719        let chunk = compile_snippet(r#"my $s = "x\n"; chomp $s;"#).expect("compile");
8720        assert!(
8721            chunk.ops.iter().any(|o| matches!(o, Op::ChompInPlace(_))),
8722            "expected ChompInPlace, got {:?}",
8723            chunk.ops
8724        );
8725    }
8726
8727    #[test]
8728    fn compile_transliterate_bind_emits_regex_transliterate() {
8729        let chunk = compile_snippet(r#"my $u = "abc"; $u =~ tr/a-z/A-Z/;"#).expect("compile");
8730        assert!(
8731            chunk
8732                .ops
8733                .iter()
8734                .any(|o| matches!(o, Op::RegexTransliterate(_, _, _, _))),
8735            "expected RegexTransliterate in {:?}",
8736            chunk.ops
8737        );
8738        assert!(!chunk.lvalues.is_empty());
8739    }
8740
8741    #[test]
8742    fn compile_negation() {
8743        let chunk = compile_snippet("-7;").expect("compile");
8744        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Negate)));
8745        assert_last_halt(&chunk);
8746    }
8747
8748    #[test]
8749    fn compile_my_scalar_declares() {
8750        let chunk = compile_snippet("my $x = 1;").expect("compile");
8751        assert!(chunk
8752            .ops
8753            .iter()
8754            .any(|o| matches!(o, Op::DeclareScalar(_) | Op::DeclareScalarSlot(_, _))));
8755        assert_last_halt(&chunk);
8756    }
8757
8758    #[test]
8759    fn compile_scalar_fetch_and_assign() {
8760        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
8761        assert!(
8762            chunk
8763                .ops
8764                .iter()
8765                .filter(|o| matches!(
8766                    o,
8767                    Op::GetScalar(_) | Op::GetScalarPlain(_) | Op::GetScalarSlot(_)
8768                ))
8769                .count()
8770                >= 1
8771        );
8772        assert_last_halt(&chunk);
8773    }
8774
8775    #[test]
8776    fn compile_plain_scalar_read_emits_get_scalar_plain() {
8777        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
8778        assert!(
8779            chunk
8780                .ops
8781                .iter()
8782                .any(|o| matches!(o, Op::GetScalarPlain(_) | Op::GetScalarSlot(_))),
8783            "expected GetScalarPlain or GetScalarSlot for non-special $a, ops={:?}",
8784            chunk.ops
8785        );
8786    }
8787
8788    #[test]
8789    fn compile_sub_postfix_inc_emits_post_inc_slot() {
8790        let chunk = compile_snippet("fn foo { my $x = 0; $x++; return $x; }").expect("compile");
8791        assert!(
8792            chunk.ops.iter().any(|o| matches!(o, Op::PostIncSlot(_))),
8793            "expected PostIncSlot in compiled sub body, ops={:?}",
8794            chunk.ops
8795        );
8796    }
8797
8798    #[test]
8799    fn compile_comparison_ops_numeric() {
8800        for src in [
8801            "1 < 2;", "1 > 2;", "1 <= 2;", "1 >= 2;", "1 == 2;", "1 != 2;",
8802        ] {
8803            let chunk = compile_snippet(src).expect(src);
8804            assert!(
8805                chunk.ops.iter().any(|o| {
8806                    matches!(
8807                        o,
8808                        Op::NumLt | Op::NumGt | Op::NumLe | Op::NumGe | Op::NumEq | Op::NumNe
8809                    )
8810                }),
8811                "{}",
8812                src
8813            );
8814            assert_last_halt(&chunk);
8815        }
8816    }
8817
8818    #[test]
8819    fn compile_string_compare_ops() {
8820        for src in [
8821            r#"'a' lt 'b';"#,
8822            r#"'a' gt 'b';"#,
8823            r#"'a' le 'b';"#,
8824            r#"'a' ge 'b';"#,
8825        ] {
8826            let chunk = compile_snippet(src).expect(src);
8827            assert!(
8828                chunk
8829                    .ops
8830                    .iter()
8831                    .any(|o| matches!(o, Op::StrLt | Op::StrGt | Op::StrLe | Op::StrGe)),
8832                "{}",
8833                src
8834            );
8835            assert_last_halt(&chunk);
8836        }
8837    }
8838
8839    #[test]
8840    fn compile_concat() {
8841        let chunk = compile_snippet(r#"'a' . 'b';"#).expect("compile");
8842        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Concat)));
8843        assert_last_halt(&chunk);
8844    }
8845
8846    #[test]
8847    fn compile_bitwise_ops() {
8848        let chunk = compile_snippet("1 & 2 | 3 ^ 4;").expect("compile");
8849        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitAnd)));
8850        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitOr)));
8851        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitXor)));
8852        assert_last_halt(&chunk);
8853    }
8854
8855    #[test]
8856    fn compile_shift_right() {
8857        let chunk = compile_snippet("8 >> 1;").expect("compile");
8858        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shr)));
8859        assert_last_halt(&chunk);
8860    }
8861
8862    #[test]
8863    fn compile_shift_left() {
8864        let chunk = compile_snippet("1 << 4;").expect("compile");
8865        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shl)));
8866        assert_last_halt(&chunk);
8867    }
8868
8869    #[test]
8870    fn compile_log_not_and_bit_not() {
8871        let c1 = compile_snippet("!0;").expect("compile");
8872        assert!(c1.ops.iter().any(|o| matches!(o, Op::LogNot)));
8873        let c2 = compile_snippet("~0;").expect("compile");
8874        assert!(c2.ops.iter().any(|o| matches!(o, Op::BitNot)));
8875    }
8876
8877    #[test]
8878    fn compile_sub_registers_name_and_entry() {
8879        let chunk = compile_snippet("fn foo { return 1; }").expect("compile");
8880        assert!(chunk.names.iter().any(|n| n == "foo"));
8881        assert!(chunk
8882            .sub_entries
8883            .iter()
8884            .any(|&(idx, ip, _)| chunk.names[idx as usize] == "foo" && ip > 0));
8885        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Halt)));
8886        assert!(chunk.ops.iter().any(|o| matches!(o, Op::ReturnValue)));
8887    }
8888
8889    #[test]
8890    fn compile_postinc_scalar() {
8891        let chunk = compile_snippet("my $n = 1; $n++;").expect("compile");
8892        assert!(chunk
8893            .ops
8894            .iter()
8895            .any(|o| matches!(o, Op::PostInc(_) | Op::PostIncSlot(_))));
8896        assert_last_halt(&chunk);
8897    }
8898
8899    #[test]
8900    fn compile_preinc_scalar() {
8901        let chunk = compile_snippet("my $n = 1; ++$n;").expect("compile");
8902        assert!(chunk
8903            .ops
8904            .iter()
8905            .any(|o| matches!(o, Op::PreInc(_) | Op::PreIncSlot(_))));
8906        assert_last_halt(&chunk);
8907    }
8908
8909    #[test]
8910    fn compile_if_expression_value() {
8911        let chunk = compile_snippet("if (1) { 2 } else { 3 }").expect("compile");
8912        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8913        assert_last_halt(&chunk);
8914    }
8915
8916    #[test]
8917    fn compile_unless_expression_value() {
8918        let chunk = compile_snippet("unless (0) { 1 } else { 2 }").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_array_declare_and_push() {
8925        let chunk = compile_snippet("my @a; push @a, 1;").expect("compile");
8926        assert!(chunk.ops.iter().any(|o| matches!(o, Op::DeclareArray(_))));
8927        assert_last_halt(&chunk);
8928    }
8929
8930    #[test]
8931    fn compile_ternary() {
8932        let chunk = compile_snippet("1 ? 2 : 3;").expect("compile");
8933        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8934        assert_last_halt(&chunk);
8935    }
8936
8937    #[test]
8938    fn compile_repeat_operator() {
8939        let chunk = compile_snippet(r#"'ab' x 3;"#).expect("compile");
8940        assert!(chunk.ops.iter().any(|o| matches!(o, Op::StringRepeat)));
8941        assert_last_halt(&chunk);
8942    }
8943
8944    #[test]
8945    fn compile_range_to_array() {
8946        let chunk = compile_snippet("my @a = (1..3);").expect("compile");
8947        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Range)));
8948        assert_last_halt(&chunk);
8949    }
8950
8951    /// Scalar `..` / `...` in a boolean condition must be the flip-flop (`$.`), not a list range.
8952    #[test]
8953    fn compile_print_if_uses_scalar_flipflop_not_range_list() {
8954        let chunk = compile_snippet("print if 1..2;").expect("compile");
8955        assert!(
8956            chunk
8957                .ops
8958                .iter()
8959                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 0))),
8960            "expected ScalarFlipFlop in bytecode, got:\n{}",
8961            chunk.disassemble()
8962        );
8963        assert!(
8964            !chunk.ops.iter().any(|o| matches!(o, Op::Range)),
8965            "did not expect list Range op in scalar if-condition:\n{}",
8966            chunk.disassemble()
8967        );
8968    }
8969
8970    #[test]
8971    fn compile_print_if_three_dot_scalar_flipflop_sets_exclusive_flag() {
8972        let chunk = compile_snippet("print if 1...2;").expect("compile");
8973        assert!(
8974            chunk
8975                .ops
8976                .iter()
8977                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 1))),
8978            "expected ScalarFlipFlop(..., exclusive=1), got:\n{}",
8979            chunk.disassemble()
8980        );
8981    }
8982
8983    #[test]
8984    fn compile_regex_flipflop_two_dot_emits_regex_flipflop_op() {
8985        let chunk = compile_snippet(r#"print if /a/../b/;"#).expect("compile");
8986        assert!(
8987            chunk
8988                .ops
8989                .iter()
8990                .any(|o| matches!(o, Op::RegexFlipFlop(_, 0, _, _, _, _))),
8991            "expected RegexFlipFlop(.., exclusive=0), got:\n{}",
8992            chunk.disassemble()
8993        );
8994        assert!(
8995            !chunk
8996                .ops
8997                .iter()
8998                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
8999            "regex flip-flop must not use ScalarFlipFlop:\n{}",
9000            chunk.disassemble()
9001        );
9002    }
9003
9004    #[test]
9005    fn compile_regex_flipflop_three_dot_sets_exclusive_flag() {
9006        let chunk = compile_snippet(r#"print if /a/.../b/;"#).expect("compile");
9007        assert!(
9008            chunk
9009                .ops
9010                .iter()
9011                .any(|o| matches!(o, Op::RegexFlipFlop(_, 1, _, _, _, _))),
9012            "expected RegexFlipFlop(..., exclusive=1), got:\n{}",
9013            chunk.disassemble()
9014        );
9015    }
9016
9017    #[test]
9018    fn compile_regex_eof_flipflop_emits_regex_eof_flipflop_op() {
9019        let chunk = compile_snippet(r#"print if /a/..eof;"#).expect("compile");
9020        assert!(
9021            chunk
9022                .ops
9023                .iter()
9024                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 0, _, _))),
9025            "expected RegexEofFlipFlop(.., exclusive=0), got:\n{}",
9026            chunk.disassemble()
9027        );
9028        assert!(
9029            !chunk
9030                .ops
9031                .iter()
9032                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9033            "regex/eof flip-flop must not use ScalarFlipFlop:\n{}",
9034            chunk.disassemble()
9035        );
9036    }
9037
9038    #[test]
9039    fn compile_regex_eof_flipflop_three_dot_sets_exclusive_flag() {
9040        let chunk = compile_snippet(r#"print if /a/...eof;"#).expect("compile");
9041        assert!(
9042            chunk
9043                .ops
9044                .iter()
9045                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 1, _, _))),
9046            "expected RegexEofFlipFlop(..., exclusive=1), got:\n{}",
9047            chunk.disassemble()
9048        );
9049    }
9050
9051    #[test]
9052    fn compile_regex_flipflop_compound_rhs_emits_regex_flip_flop_expr_rhs() {
9053        let chunk = compile_snippet(r#"print if /a/...(/b/ or /c/);"#).expect("compile");
9054        assert!(
9055            chunk
9056                .ops
9057                .iter()
9058                .any(|o| matches!(o, Op::RegexFlipFlopExprRhs(_, _, _, _, _))),
9059            "expected RegexFlipFlopExprRhs for compound RHS, got:\n{}",
9060            chunk.disassemble()
9061        );
9062        assert!(
9063            !chunk
9064                .ops
9065                .iter()
9066                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
9067            "compound regex flip-flop must not use ScalarFlipFlop:\n{}",
9068            chunk.disassemble()
9069        );
9070    }
9071
9072    #[test]
9073    fn compile_print_statement() {
9074        let chunk = compile_snippet("print 1;").expect("compile");
9075        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Print(_, _))));
9076        assert_last_halt(&chunk);
9077    }
9078
9079    #[test]
9080    fn compile_defined_builtin() {
9081        let chunk = compile_snippet("defined 1;").expect("compile");
9082        assert!(chunk
9083            .ops
9084            .iter()
9085            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Defined as u16)));
9086        assert_last_halt(&chunk);
9087    }
9088
9089    #[test]
9090    fn compile_length_builtin() {
9091        let chunk = compile_snippet("length 'abc';").expect("compile");
9092        assert!(chunk
9093            .ops
9094            .iter()
9095            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Length as u16)));
9096        assert_last_halt(&chunk);
9097    }
9098
9099    #[test]
9100    fn compile_complex_expr_parentheses() {
9101        let chunk = compile_snippet("(1 + 2) * (3 + 4);").expect("compile");
9102        assert!(chunk.ops.iter().filter(|o| matches!(o, Op::Add)).count() >= 2);
9103        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Mul)));
9104        assert_last_halt(&chunk);
9105    }
9106
9107    #[test]
9108    fn compile_undef_literal() {
9109        let chunk = compile_snippet("undef;").expect("compile");
9110        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadUndef)));
9111        assert_last_halt(&chunk);
9112    }
9113
9114    #[test]
9115    fn compile_empty_statement_semicolons() {
9116        let chunk = compile_snippet(";;;").expect("compile");
9117        assert_last_halt(&chunk);
9118    }
9119
9120    #[test]
9121    fn compile_array_elem_preinc_uses_rot_and_set_elem() {
9122        let chunk = compile_snippet("my @a; $a[0] = 0; ++$a[0];").expect("compile");
9123        assert!(
9124            chunk.ops.iter().any(|o| matches!(o, Op::Rot)),
9125            "expected Rot in {:?}",
9126            chunk.ops
9127        );
9128        assert!(
9129            chunk.ops.iter().any(|o| matches!(o, Op::SetArrayElem(_))),
9130            "expected SetArrayElem in {:?}",
9131            chunk.ops
9132        );
9133        assert_last_halt(&chunk);
9134    }
9135
9136    #[test]
9137    fn compile_hash_elem_compound_assign_uses_rot() {
9138        let chunk = compile_snippet("my %h; $h{0} = 1; $h{0} += 2;").expect("compile");
9139        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
9140        assert!(
9141            chunk.ops.iter().any(|o| matches!(o, Op::SetHashElem(_))),
9142            "expected SetHashElem"
9143        );
9144        assert_last_halt(&chunk);
9145    }
9146
9147    #[test]
9148    fn compile_postfix_inc_array_elem_emits_rot() {
9149        let chunk = compile_snippet("my @a; $a[1] = 5; $a[1]++;").expect("compile");
9150        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
9151        assert_last_halt(&chunk);
9152    }
9153
9154    #[test]
9155    fn compile_tie_stmt_emits_op_tie() {
9156        let chunk = compile_snippet("tie %h, 'Pkg';").expect("compile");
9157        assert!(
9158            chunk.ops.iter().any(|o| matches!(o, Op::Tie { .. })),
9159            "expected Op::Tie in {:?}",
9160            chunk.ops
9161        );
9162        assert_last_halt(&chunk);
9163    }
9164
9165    #[test]
9166    fn compile_format_decl_emits_format_decl_op() {
9167        let chunk = compile_snippet(
9168            r#"
9169format FMT =
9170literal line
9171.
91721;
9173"#,
9174        )
9175        .expect("compile");
9176        assert!(
9177            chunk.ops.iter().any(|o| matches!(o, Op::FormatDecl(0))),
9178            "expected Op::FormatDecl(0), got {:?}",
9179            chunk.ops
9180        );
9181        assert_eq!(chunk.format_decls.len(), 1);
9182        assert_eq!(chunk.format_decls[0].0, "FMT");
9183        assert_eq!(chunk.format_decls[0].1, vec!["literal line".to_string()]);
9184        assert_last_halt(&chunk);
9185    }
9186
9187    #[test]
9188    fn compile_interpolated_string_scalar_only_emits_empty_prefix_and_concat() {
9189        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "$x";"#).expect("compile");
9190        let empty_idx = chunk
9191            .constants
9192            .iter()
9193            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9194            .expect("empty string in pool") as u16;
9195        assert!(
9196            chunk
9197                .ops
9198                .iter()
9199                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9200            "expected LoadConst(\"\"), ops={:?}",
9201            chunk.ops
9202        );
9203        assert!(
9204            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9205            "expected Op::Concat for qq with only a scalar part, ops={:?}",
9206            chunk.ops
9207        );
9208        assert_last_halt(&chunk);
9209    }
9210
9211    #[test]
9212    fn compile_interpolated_string_array_only_emits_stringify_and_concat() {
9213        let chunk = compile_snippet(r#"no strict 'vars'; my @a = (1, 2); "@a";"#).expect("compile");
9214        let empty_idx = chunk
9215            .constants
9216            .iter()
9217            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9218            .expect("empty string in pool") as u16;
9219        assert!(
9220            chunk
9221                .ops
9222                .iter()
9223                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9224            "expected LoadConst(\"\"), ops={:?}",
9225            chunk.ops
9226        );
9227        assert!(
9228            chunk
9229                .ops
9230                .iter()
9231                .any(|o| matches!(o, Op::ArrayStringifyListSep)),
9232            "expected ArrayStringifyListSep for array var in qq, ops={:?}",
9233            chunk.ops
9234        );
9235        assert!(
9236            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9237            "expected Op::Concat after array stringify, ops={:?}",
9238            chunk.ops
9239        );
9240        assert_last_halt(&chunk);
9241    }
9242
9243    #[test]
9244    fn compile_interpolated_string_hash_element_only_emits_empty_prefix_and_concat() {
9245        let chunk =
9246            compile_snippet(r#"no strict 'vars'; my %h = (k => 1); "$h{k}";"#).expect("compile");
9247        let empty_idx = chunk
9248            .constants
9249            .iter()
9250            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9251            .expect("empty string in pool") as u16;
9252        assert!(
9253            chunk
9254                .ops
9255                .iter()
9256                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9257            "expected LoadConst(\"\"), ops={:?}",
9258            chunk.ops
9259        );
9260        assert!(
9261            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9262            "expected Op::Concat for qq with only an expr part, ops={:?}",
9263            chunk.ops
9264        );
9265        assert_last_halt(&chunk);
9266    }
9267
9268    #[test]
9269    fn compile_interpolated_string_leading_literal_has_no_empty_string_prefix() {
9270        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "a$x";"#).expect("compile");
9271        assert!(
9272            !chunk
9273                .constants
9274                .iter()
9275                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
9276            "literal-first qq must not intern \"\" (only non-literal first parts need it), ops={:?}",
9277            chunk.ops
9278        );
9279        assert!(
9280            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
9281            "expected Op::Concat after literal + scalar, ops={:?}",
9282            chunk.ops
9283        );
9284        assert_last_halt(&chunk);
9285    }
9286
9287    #[test]
9288    fn compile_interpolated_string_two_scalars_empty_prefix_and_two_concats() {
9289        let chunk =
9290            compile_snippet(r#"no strict 'vars'; my $a = 1; my $b = 2; "$a$b";"#).expect("compile");
9291        let empty_idx = chunk
9292            .constants
9293            .iter()
9294            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
9295            .expect("empty string in pool") as u16;
9296        assert!(
9297            chunk
9298                .ops
9299                .iter()
9300                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
9301            "expected LoadConst(\"\") before first scalar qq part, ops={:?}",
9302            chunk.ops
9303        );
9304        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9305        assert!(
9306            n_concat >= 2,
9307            "expected at least two Op::Concat for two scalar qq parts, got {} in {:?}",
9308            n_concat,
9309            chunk.ops
9310        );
9311        assert_last_halt(&chunk);
9312    }
9313
9314    #[test]
9315    fn compile_interpolated_string_literal_then_two_scalars_has_no_empty_prefix() {
9316        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 7; my $y = 8; "p$x$y";"#)
9317            .expect("compile");
9318        assert!(
9319            !chunk
9320                .constants
9321                .iter()
9322                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
9323            "literal-first qq must not intern empty string, ops={:?}",
9324            chunk.ops
9325        );
9326        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9327        assert!(
9328            n_concat >= 2,
9329            "expected two Concats for literal + two scalars, got {} in {:?}",
9330            n_concat,
9331            chunk.ops
9332        );
9333        assert_last_halt(&chunk);
9334    }
9335
9336    #[test]
9337    fn compile_interpolated_string_braced_scalar_trailing_literal_emits_concats() {
9338        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "a${u}z";"#).expect("compile");
9339        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9340        assert!(
9341            n_concat >= 2,
9342            "expected braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
9343            n_concat,
9344            chunk.ops
9345        );
9346        assert_last_halt(&chunk);
9347    }
9348
9349    #[test]
9350    fn compile_interpolated_string_braced_scalar_sandwiched_emits_concats() {
9351        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "L${u}R";"#).expect("compile");
9352        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9353        assert!(
9354            n_concat >= 2,
9355            "expected leading literal + braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
9356            n_concat,
9357            chunk.ops
9358        );
9359        assert_last_halt(&chunk);
9360    }
9361
9362    #[test]
9363    fn compile_interpolated_string_mixed_braced_and_plain_scalars_emits_concats() {
9364        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; my $y = 2; "a${x}b$y";"#)
9365            .expect("compile");
9366        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
9367        assert!(
9368            n_concat >= 3,
9369            "expected literal/braced/plain qq mix to use at least three Concats, got {} in {:?}",
9370            n_concat,
9371            chunk.ops
9372        );
9373        assert_last_halt(&chunk);
9374    }
9375
9376    #[test]
9377    fn compile_use_overload_emits_use_overload_op() {
9378        let chunk = compile_snippet(r#"use overload '""' => 'as_string';"#).expect("compile");
9379        assert!(
9380            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9381            "expected Op::UseOverload(0), got {:?}",
9382            chunk.ops
9383        );
9384        assert_eq!(chunk.use_overload_entries.len(), 1);
9385        // Perl `'""'` is a single-quoted string whose contents are two `"` characters — the
9386        // overload table key for stringify (see [`Interpreter::overload_stringify_method`]).
9387        let stringify_key: String = ['"', '"'].iter().collect();
9388        assert_eq!(
9389            chunk.use_overload_entries[0],
9390            vec![(stringify_key, "as_string".to_string())]
9391        );
9392        assert_last_halt(&chunk);
9393    }
9394
9395    #[test]
9396    fn compile_use_overload_empty_list_emits_use_overload_with_no_pairs() {
9397        let chunk = compile_snippet(r#"use overload ();"#).expect("compile");
9398        assert!(
9399            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9400            "expected Op::UseOverload(0), got {:?}",
9401            chunk.ops
9402        );
9403        assert_eq!(chunk.use_overload_entries.len(), 1);
9404        assert!(chunk.use_overload_entries[0].is_empty());
9405        assert_last_halt(&chunk);
9406    }
9407
9408    #[test]
9409    fn compile_use_overload_multiple_pairs_single_op() {
9410        let chunk =
9411            compile_snippet(r#"use overload '+' => 'p_add', '-' => 'p_sub';"#).expect("compile");
9412        assert!(
9413            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
9414            "expected Op::UseOverload(0), got {:?}",
9415            chunk.ops
9416        );
9417        assert_eq!(chunk.use_overload_entries.len(), 1);
9418        assert_eq!(
9419            chunk.use_overload_entries[0],
9420            vec![
9421                ("+".to_string(), "p_add".to_string()),
9422                ("-".to_string(), "p_sub".to_string()),
9423            ]
9424        );
9425        assert_last_halt(&chunk);
9426    }
9427
9428    #[test]
9429    fn compile_open_my_fh_emits_declare_open_set() {
9430        let chunk = compile_snippet(r#"open my $fh, "<", "/dev/null";"#).expect("compile");
9431        assert!(
9432            chunk.ops.iter().any(|o| matches!(
9433                o,
9434                Op::CallBuiltin(b, 3) if *b == BuiltinId::Open as u16
9435            )),
9436            "expected Open builtin 3-arg, got {:?}",
9437            chunk.ops
9438        );
9439        assert!(
9440            chunk
9441                .ops
9442                .iter()
9443                .any(|o| matches!(o, Op::SetScalarKeepPlain(_))),
9444            "expected SetScalarKeepPlain after open"
9445        );
9446        assert_last_halt(&chunk);
9447    }
9448
9449    #[test]
9450    fn compile_local_hash_element_emits_local_declare_hash_element() {
9451        let chunk = compile_snippet(r#"local $SIG{__WARN__} = 0;"#).expect("compile");
9452        assert!(
9453            chunk
9454                .ops
9455                .iter()
9456                .any(|o| matches!(o, Op::LocalDeclareHashElement(_))),
9457            "expected LocalDeclareHashElement in {:?}",
9458            chunk.ops
9459        );
9460        assert_last_halt(&chunk);
9461    }
9462
9463    #[test]
9464    fn compile_local_array_element_emits_local_declare_array_element() {
9465        let chunk = compile_snippet(r#"local $a[2] = 9;"#).expect("compile");
9466        assert!(
9467            chunk
9468                .ops
9469                .iter()
9470                .any(|o| matches!(o, Op::LocalDeclareArrayElement(_))),
9471            "expected LocalDeclareArrayElement in {:?}",
9472            chunk.ops
9473        );
9474        assert_last_halt(&chunk);
9475    }
9476
9477    #[test]
9478    fn compile_local_typeglob_emits_local_declare_typeglob() {
9479        let chunk = compile_snippet(r#"local *STDOUT;"#).expect("compile");
9480        assert!(
9481            chunk
9482                .ops
9483                .iter()
9484                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, None))),
9485            "expected LocalDeclareTypeglob(_, None) in {:?}",
9486            chunk.ops
9487        );
9488        assert_last_halt(&chunk);
9489    }
9490
9491    #[test]
9492    fn compile_local_typeglob_alias_emits_local_declare_typeglob_some_rhs() {
9493        let chunk = compile_snippet(r#"local *FOO = *STDOUT;"#).expect("compile");
9494        assert!(
9495            chunk
9496                .ops
9497                .iter()
9498                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, Some(_)))),
9499            "expected LocalDeclareTypeglob with rhs in {:?}",
9500            chunk.ops
9501        );
9502        assert_last_halt(&chunk);
9503    }
9504
9505    #[test]
9506    fn compile_local_braced_typeglob_emits_local_declare_typeglob_dynamic() {
9507        let chunk = compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *{ $g };"#)
9508            .expect("compile");
9509        assert!(
9510            chunk
9511                .ops
9512                .iter()
9513                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
9514            "expected LocalDeclareTypeglobDynamic(None) in {:?}",
9515            chunk.ops
9516        );
9517        assert_last_halt(&chunk);
9518    }
9519
9520    #[test]
9521    fn compile_local_star_deref_typeglob_emits_local_declare_typeglob_dynamic() {
9522        let chunk =
9523            compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *$g;"#).expect("compile");
9524        assert!(
9525            chunk
9526                .ops
9527                .iter()
9528                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
9529            "expected LocalDeclareTypeglobDynamic(None) for local *scalar glob in {:?}",
9530            chunk.ops
9531        );
9532        assert_last_halt(&chunk);
9533    }
9534
9535    #[test]
9536    fn compile_braced_glob_assign_to_named_glob_emits_copy_dynamic_lhs() {
9537        // `*{EXPR} = *FOO` — dynamic lhs name + static rhs glob → `CopyTypeglobSlotsDynamicLhs`.
9538        let chunk = compile_snippet(r#"no strict 'refs'; my $n = "x"; *{ $n } = *STDOUT;"#)
9539            .expect("compile");
9540        assert!(
9541            chunk
9542                .ops
9543                .iter()
9544                .any(|o| matches!(o, Op::CopyTypeglobSlotsDynamicLhs(_))),
9545            "expected CopyTypeglobSlotsDynamicLhs in {:?}",
9546            chunk.ops
9547        );
9548        assert_last_halt(&chunk);
9549    }
9550
9551    /// Regression for the class-field line-drift bug at the compiler level.
9552    /// A class body with a typed field used to push every subsequent
9553    /// statement's bytecode `lines[i]` off by +1 per field, so DAP
9554    /// breakpoints set on the post-class lines silently dropped. With the
9555    /// lexer's fat-arrow / qw / ... lookaheads correctly restoring
9556    /// `self.line`, the compiled chunk's `lines[]` matches the source.
9557    #[test]
9558    fn class_field_emits_correct_op_lines_for_subsequent_statements() {
9559        let chunk = compile_snippet(
9560            "class Foo {\n    x: Int\n}\nmy $y = 1\np $y\n",
9561        )
9562        .expect("compile");
9563        // `my $y = 1` is at source line 4. Find any op with line 4 — it
9564        // proves the compiler emitted bytecode for that statement on the
9565        // correct source line. Before the fix, line 4 would not appear
9566        // (the ops would carry line 5 instead).
9567        let has_line_4 = chunk.lines.iter().any(|&l| l == 4);
9568        assert!(
9569            has_line_4,
9570            "expected at least one op tagged with source line 4 (my $y = 1), got lines {:?}",
9571            chunk.lines
9572        );
9573        // And `p $y` is at line 5; same regression catch.
9574        let has_line_5 = chunk.lines.iter().any(|&l| l == 5);
9575        assert!(
9576            has_line_5,
9577            "expected at least one op tagged with source line 5 (p $y), got lines {:?}",
9578            chunk.lines
9579        );
9580        // And the inverse — *no* op should be on line 6 (file only has
9581        // 5 logical lines plus a trailing newline). A line-6 op means the
9582        // counter drifted past the file.
9583        let has_line_6 = chunk.lines.iter().any(|&l| l == 6);
9584        assert!(
9585            !has_line_6,
9586            "no op should report line 6 in a 5-line file; lines {:?}",
9587            chunk.lines
9588        );
9589    }
9590
9591    /// Same check but for class with *two* typed fields (the original
9592    /// reproduction had 2 fields giving a +2 drift on subsequent ops).
9593    #[test]
9594    fn class_with_two_fields_does_not_drift_op_lines() {
9595        let chunk = compile_snippet(
9596            "class Foo {\n    x: Int\n    y: Int\n}\nmy $a = 1\nmy $b = 2\np $a\np $b\n",
9597        )
9598        .expect("compile");
9599        // `my $a = 1` is at source line 5; `my $b = 2` at line 6;
9600        // `p $a` at line 7; `p $b` at line 8.
9601        for expected in [5, 6, 7, 8] {
9602            assert!(
9603                chunk.lines.iter().any(|&l| l == expected),
9604                "expected an op on line {expected}, got {:?}",
9605                chunk.lines
9606            );
9607        }
9608        // And no op past the end of the file.
9609        let max_line = chunk.lines.iter().copied().max().unwrap_or(0);
9610        assert!(max_line <= 8, "no op past source line 8: max was {max_line}");
9611    }
9612}