Skip to main content

stryke/
compiler.rs

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