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