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