Skip to main content

stryke/
compiler.rs

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