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