Skip to main content

stryke/
compiler.rs

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