Skip to main content

stryke/
compiler.rs

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