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                step,
4596            } => {
4597                if ctx == WantarrayCtx::List {
4598                    self.compile_expr_ctx(from, WantarrayCtx::Scalar)?;
4599                    self.compile_expr_ctx(to, WantarrayCtx::Scalar)?;
4600                    if let Some(s) = step {
4601                        self.compile_expr_ctx(s, WantarrayCtx::Scalar)?;
4602                        self.emit_op(Op::RangeStep, line, Some(root));
4603                    } else {
4604                        self.emit_op(Op::Range, line, Some(root));
4605                    }
4606                } else if let (ExprKind::Regex(lp, lf), ExprKind::Regex(rp, rf)) =
4607                    (&from.kind, &to.kind)
4608                {
4609                    let slot = self.chunk.alloc_flip_flop_slot();
4610                    let lp_idx = self.chunk.add_constant(PerlValue::string(lp.clone()));
4611                    let lf_idx = self.chunk.add_constant(PerlValue::string(lf.clone()));
4612                    let rp_idx = self.chunk.add_constant(PerlValue::string(rp.clone()));
4613                    let rf_idx = self.chunk.add_constant(PerlValue::string(rf.clone()));
4614                    self.emit_op(
4615                        Op::RegexFlipFlop(
4616                            slot,
4617                            u8::from(*exclusive),
4618                            lp_idx,
4619                            lf_idx,
4620                            rp_idx,
4621                            rf_idx,
4622                        ),
4623                        line,
4624                        Some(root),
4625                    );
4626                } else if let (ExprKind::Regex(lp, lf), ExprKind::Eof(None)) =
4627                    (&from.kind, &to.kind)
4628                {
4629                    let slot = self.chunk.alloc_flip_flop_slot();
4630                    let lp_idx = self.chunk.add_constant(PerlValue::string(lp.clone()));
4631                    let lf_idx = self.chunk.add_constant(PerlValue::string(lf.clone()));
4632                    self.emit_op(
4633                        Op::RegexEofFlipFlop(slot, u8::from(*exclusive), lp_idx, lf_idx),
4634                        line,
4635                        Some(root),
4636                    );
4637                } else if matches!(
4638                    (&from.kind, &to.kind),
4639                    (ExprKind::Regex(_, _), ExprKind::Eof(Some(_)))
4640                ) {
4641                    return Err(CompileError::Unsupported(
4642                        "regex flip-flop with eof(HANDLE) is not supported".into(),
4643                    ));
4644                } else if let ExprKind::Regex(lp, lf) = &from.kind {
4645                    let slot = self.chunk.alloc_flip_flop_slot();
4646                    let lp_idx = self.chunk.add_constant(PerlValue::string(lp.clone()));
4647                    let lf_idx = self.chunk.add_constant(PerlValue::string(lf.clone()));
4648                    if matches!(to.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
4649                        let line_target = match &to.kind {
4650                            ExprKind::Integer(n) => *n,
4651                            ExprKind::Float(f) => *f as i64,
4652                            _ => unreachable!(),
4653                        };
4654                        let line_cidx = self.chunk.add_constant(PerlValue::integer(line_target));
4655                        self.emit_op(
4656                            Op::RegexFlipFlopDotLineRhs(
4657                                slot,
4658                                u8::from(*exclusive),
4659                                lp_idx,
4660                                lf_idx,
4661                                line_cidx,
4662                            ),
4663                            line,
4664                            Some(root),
4665                        );
4666                    } else {
4667                        let rhs_idx = self
4668                            .chunk
4669                            .add_regex_flip_flop_rhs_expr_entry((**to).clone());
4670                        self.emit_op(
4671                            Op::RegexFlipFlopExprRhs(
4672                                slot,
4673                                u8::from(*exclusive),
4674                                lp_idx,
4675                                lf_idx,
4676                                rhs_idx,
4677                            ),
4678                            line,
4679                            Some(root),
4680                        );
4681                    }
4682                } else {
4683                    self.compile_expr(from)?;
4684                    self.compile_expr(to)?;
4685                    let slot = self.chunk.alloc_flip_flop_slot();
4686                    self.emit_op(
4687                        Op::ScalarFlipFlop(slot, u8::from(*exclusive)),
4688                        line,
4689                        Some(root),
4690                    );
4691                }
4692            }
4693
4694            ExprKind::Repeat { expr, count } => {
4695                self.compile_expr(expr)?;
4696                self.compile_expr(count)?;
4697                self.emit_op(Op::StringRepeat, line, Some(root));
4698            }
4699
4700            // ── Function calls ──
4701            ExprKind::FuncCall { name, args } => match name.as_str() {
4702                // read() needs lvalue access to its 2nd arg; handled in tree-walker
4703                "read" | "CORE::read" => {
4704                    return Err(CompileError::Unsupported(
4705                        "read() needs tree-walker for lvalue buffer arg".into(),
4706                    ));
4707                }
4708                // `defer { BLOCK }` — desugared by parser to `defer__internal(fn { BLOCK })`
4709                "defer__internal" => {
4710                    if args.len() != 1 {
4711                        return Err(CompileError::Unsupported(
4712                            "defer__internal expects exactly one argument".into(),
4713                        ));
4714                    }
4715                    // Compile the coderef argument
4716                    self.compile_expr(&args[0])?;
4717                    // Register it for execution on scope exit
4718                    self.emit_op(Op::DeferBlock, line, Some(root));
4719                }
4720                "deque" => {
4721                    if !args.is_empty() {
4722                        return Err(CompileError::Unsupported(
4723                            "deque() takes no arguments".into(),
4724                        ));
4725                    }
4726                    self.emit_op(
4727                        Op::CallBuiltin(BuiltinId::DequeNew as u16, 0),
4728                        line,
4729                        Some(root),
4730                    );
4731                }
4732                "inc" => {
4733                    let arg = args.first().cloned().unwrap_or_else(|| Expr {
4734                        kind: ExprKind::ScalarVar("_".into()),
4735                        line,
4736                    });
4737                    self.compile_expr(&arg)?;
4738                    self.emit_op(Op::Inc, line, Some(root));
4739                }
4740                "dec" => {
4741                    let arg = args.first().cloned().unwrap_or_else(|| Expr {
4742                        kind: ExprKind::ScalarVar("_".into()),
4743                        line,
4744                    });
4745                    self.compile_expr(&arg)?;
4746                    self.emit_op(Op::Dec, line, Some(root));
4747                }
4748                "heap" => {
4749                    if args.len() != 1 {
4750                        return Err(CompileError::Unsupported(
4751                            "heap() expects one comparator sub".into(),
4752                        ));
4753                    }
4754                    self.compile_expr(&args[0])?;
4755                    self.emit_op(
4756                        Op::CallBuiltin(BuiltinId::HeapNew as u16, 1),
4757                        line,
4758                        Some(root),
4759                    );
4760                }
4761                "pipeline" => {
4762                    for arg in args {
4763                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
4764                    }
4765                    self.emit_op(
4766                        Op::CallBuiltin(BuiltinId::Pipeline as u16, args.len() as u8),
4767                        line,
4768                        Some(root),
4769                    );
4770                }
4771                "par_pipeline" => {
4772                    for arg in args {
4773                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
4774                    }
4775                    self.emit_op(
4776                        Op::CallBuiltin(BuiltinId::ParPipeline as u16, args.len() as u8),
4777                        line,
4778                        Some(root),
4779                    );
4780                }
4781                "par_pipeline_stream" => {
4782                    for arg in args {
4783                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
4784                    }
4785                    self.emit_op(
4786                        Op::CallBuiltin(BuiltinId::ParPipelineStream as u16, args.len() as u8),
4787                        line,
4788                        Some(root),
4789                    );
4790                }
4791                // `collect(EXPR)` — compile the argument in list context so nested
4792                // `map { }` / `grep { }` keep a pipeline handle (scalar context adds
4793                // `StackArrayLen`, which turns a pipeline into `1`). At runtime, a
4794                // pipeline runs staged ops; any other value is materialized as an array
4795                // (`|> … |> collect()`).
4796                "collect" => {
4797                    if args.len() != 1 {
4798                        return Err(CompileError::Unsupported(
4799                            "collect() expects exactly one argument".into(),
4800                        ));
4801                    }
4802                    self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
4803                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
4804                    self.emit_op(Op::Call(name_idx, 1, ctx.as_byte()), line, Some(root));
4805                }
4806                "ppool" => {
4807                    if args.len() != 1 {
4808                        return Err(CompileError::Unsupported(
4809                            "ppool() expects one argument (worker count)".into(),
4810                        ));
4811                    }
4812                    self.compile_expr(&args[0])?;
4813                    self.emit_op(
4814                        Op::CallBuiltin(BuiltinId::Ppool as u16, 1),
4815                        line,
4816                        Some(root),
4817                    );
4818                }
4819                "barrier" => {
4820                    if args.len() != 1 {
4821                        return Err(CompileError::Unsupported(
4822                            "barrier() expects one argument (party count)".into(),
4823                        ));
4824                    }
4825                    self.compile_expr(&args[0])?;
4826                    self.emit_op(
4827                        Op::CallBuiltin(BuiltinId::BarrierNew as u16, 1),
4828                        line,
4829                        Some(root),
4830                    );
4831                }
4832                "pselect" => {
4833                    if args.is_empty() {
4834                        return Err(CompileError::Unsupported(
4835                            "pselect() expects at least one pchannel receiver".into(),
4836                        ));
4837                    }
4838                    for arg in args {
4839                        self.compile_expr(arg)?;
4840                    }
4841                    self.emit_op(
4842                        Op::CallBuiltin(BuiltinId::Pselect as u16, args.len() as u8),
4843                        line,
4844                        Some(root),
4845                    );
4846                }
4847                "ssh" => {
4848                    for arg in args {
4849                        self.compile_expr(arg)?;
4850                    }
4851                    self.emit_op(
4852                        Op::CallBuiltin(BuiltinId::Ssh as u16, args.len() as u8),
4853                        line,
4854                        Some(root),
4855                    );
4856                }
4857                "rmdir" | "CORE::rmdir" => {
4858                    for arg in args {
4859                        self.compile_expr(arg)?;
4860                    }
4861                    self.emit_op(
4862                        Op::CallBuiltin(BuiltinId::Rmdir as u16, args.len() as u8),
4863                        line,
4864                        Some(root),
4865                    );
4866                }
4867                "utime" | "CORE::utime" => {
4868                    for arg in args {
4869                        self.compile_expr(arg)?;
4870                    }
4871                    self.emit_op(
4872                        Op::CallBuiltin(BuiltinId::Utime as u16, args.len() as u8),
4873                        line,
4874                        Some(root),
4875                    );
4876                }
4877                "umask" | "CORE::umask" => {
4878                    for arg in args {
4879                        self.compile_expr(arg)?;
4880                    }
4881                    self.emit_op(
4882                        Op::CallBuiltin(BuiltinId::Umask as u16, args.len() as u8),
4883                        line,
4884                        Some(root),
4885                    );
4886                }
4887                "getcwd" | "CORE::getcwd" | "Cwd::getcwd" => {
4888                    for arg in args {
4889                        self.compile_expr(arg)?;
4890                    }
4891                    self.emit_op(
4892                        Op::CallBuiltin(BuiltinId::Getcwd as u16, args.len() as u8),
4893                        line,
4894                        Some(root),
4895                    );
4896                }
4897                "pipe" | "CORE::pipe" => {
4898                    if args.len() != 2 {
4899                        return Err(CompileError::Unsupported(
4900                            "pipe requires exactly two arguments".into(),
4901                        ));
4902                    }
4903                    for arg in args {
4904                        self.compile_expr(arg)?;
4905                    }
4906                    self.emit_op(Op::CallBuiltin(BuiltinId::Pipe as u16, 2), line, Some(root));
4907                }
4908                "uniq"
4909                | "distinct"
4910                | "flatten"
4911                | "set"
4912                | "with_index"
4913                | "list_count"
4914                | "list_size"
4915                | "count"
4916                | "size"
4917                | "cnt"
4918                | "List::Util::uniq"
4919                | "sum"
4920                | "sum0"
4921                | "product"
4922                | "min"
4923                | "max"
4924                | "mean"
4925                | "median"
4926                | "mode"
4927                | "stddev"
4928                | "variance"
4929                | "List::Util::sum"
4930                | "List::Util::sum0"
4931                | "List::Util::product"
4932                | "List::Util::min"
4933                | "List::Util::max"
4934                | "List::Util::minstr"
4935                | "List::Util::maxstr"
4936                | "List::Util::mean"
4937                | "List::Util::median"
4938                | "List::Util::mode"
4939                | "List::Util::stddev"
4940                | "List::Util::variance" => {
4941                    for arg in args {
4942                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
4943                    }
4944                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
4945                    self.emit_op(
4946                        Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
4947                        line,
4948                        Some(root),
4949                    );
4950                }
4951                "shuffle" | "List::Util::shuffle" => {
4952                    for arg in args {
4953                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
4954                    }
4955                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
4956                    self.emit_op(
4957                        Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
4958                        line,
4959                        Some(root),
4960                    );
4961                }
4962                "chunked" | "List::Util::chunked" | "windowed" | "List::Util::windowed" => {
4963                    match args.len() {
4964                        0 => {
4965                            return Err(CompileError::Unsupported(
4966                                "chunked/windowed need (LIST, N) or unary N (e.g. `|> chunked(2)`)"
4967                                    .into(),
4968                            ));
4969                        }
4970                        1 => {
4971                            // chunked @l / windowed @l — compile in list context, default size
4972                            self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
4973                        }
4974                        2 => {
4975                            self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
4976                            self.compile_expr(&args[1])?;
4977                        }
4978                        _ => {
4979                            return Err(CompileError::Unsupported(
4980                                "chunked/windowed expect exactly two arguments (LIST, N); use a single list expression for the first operand".into(),
4981                            ));
4982                        }
4983                    }
4984                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
4985                    self.emit_op(
4986                        Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
4987                        line,
4988                        Some(root),
4989                    );
4990                }
4991                "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail" => {
4992                    if args.is_empty() {
4993                        return Err(CompileError::Unsupported(
4994                            "take/head/tail/drop/List::Util::head|tail expect LIST..., N or unary N"
4995                                .into(),
4996                        ));
4997                    }
4998                    if args.len() == 1 {
4999                        // head @l == head @l, 1 — evaluate in list context
5000                        self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5001                    } else {
5002                        for a in &args[..args.len() - 1] {
5003                            self.compile_expr_ctx(a, WantarrayCtx::List)?;
5004                        }
5005                        self.compile_expr(&args[args.len() - 1])?;
5006                    }
5007                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5008                    self.emit_op(
5009                        Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5010                        line,
5011                        Some(root),
5012                    );
5013                }
5014                "any" | "all" | "none" | "first" | "take_while" | "drop_while" | "tap" | "peek" => {
5015                    if args.len() != 2 {
5016                        return Err(CompileError::Unsupported(
5017                            "any/all/none/first/take_while/drop_while/tap/peek expect BLOCK, LIST"
5018                                .into(),
5019                        ));
5020                    }
5021                    if !matches!(&args[0].kind, ExprKind::CodeRef { .. }) {
5022                        return Err(CompileError::Unsupported(
5023                            "any/all/none/first/take_while/drop_while/tap/peek: first argument must be a { BLOCK }"
5024                                .into(),
5025                        ));
5026                    }
5027                    self.compile_expr(&args[0])?;
5028                    self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5029                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(name));
5030                    self.emit_op(Op::Call(name_idx, 2, ctx.as_byte()), line, Some(root));
5031                }
5032                "group_by" | "chunk_by" => {
5033                    if args.len() != 2 {
5034                        return Err(CompileError::Unsupported(
5035                            "group_by/chunk_by expect { BLOCK } or EXPR, LIST".into(),
5036                        ));
5037                    }
5038                    self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5039                    match &args[0].kind {
5040                        ExprKind::CodeRef { body, .. } => {
5041                            let block_idx = self.chunk.add_block(body.clone());
5042                            self.emit_op(Op::ChunkByWithBlock(block_idx), line, Some(root));
5043                        }
5044                        _ => {
5045                            let idx = self.chunk.add_map_expr_entry(args[0].clone());
5046                            self.emit_op(Op::ChunkByWithExpr(idx), line, Some(root));
5047                        }
5048                    }
5049                    if ctx != WantarrayCtx::List {
5050                        self.emit_op(Op::StackArrayLen, line, Some(root));
5051                    }
5052                }
5053                "zip" | "List::Util::zip" | "List::Util::zip_longest" => {
5054                    for arg in args {
5055                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5056                    }
5057                    let fq = match name.as_str() {
5058                        "List::Util::zip_longest" => "List::Util::zip_longest",
5059                        "List::Util::zip" => "List::Util::zip",
5060                        _ => "zip",
5061                    };
5062                    let name_idx = self.chunk.intern_name(&self.qualify_sub_key(fq));
5063                    self.emit_op(
5064                        Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5065                        line,
5066                        Some(root),
5067                    );
5068                }
5069                "puniq" => {
5070                    if args.is_empty() || args.len() > 2 {
5071                        return Err(CompileError::Unsupported(
5072                            "puniq expects LIST [, progress => EXPR]".into(),
5073                        ));
5074                    }
5075                    if args.len() == 2 {
5076                        self.compile_expr(&args[1])?;
5077                    } else {
5078                        self.emit_op(Op::LoadInt(0), line, Some(root));
5079                    }
5080                    self.compile_expr_ctx(&args[0], WantarrayCtx::List)?;
5081                    self.emit_op(Op::Puniq, line, Some(root));
5082                    if ctx != WantarrayCtx::List {
5083                        self.emit_op(Op::StackArrayLen, line, Some(root));
5084                    }
5085                }
5086                "pfirst" | "pany" => {
5087                    if args.len() < 2 || args.len() > 3 {
5088                        return Err(CompileError::Unsupported(
5089                            "pfirst/pany expect BLOCK, LIST [, progress => EXPR]".into(),
5090                        ));
5091                    }
5092                    let body = match &args[0].kind {
5093                        ExprKind::CodeRef { body, .. } => body,
5094                        _ => {
5095                            return Err(CompileError::Unsupported(
5096                                "pfirst/pany: first argument must be a { BLOCK }".into(),
5097                            ));
5098                        }
5099                    };
5100                    if args.len() == 3 {
5101                        self.compile_expr(&args[2])?;
5102                    } else {
5103                        self.emit_op(Op::LoadInt(0), line, Some(root));
5104                    }
5105                    self.compile_expr_ctx(&args[1], WantarrayCtx::List)?;
5106                    let block_idx = self.chunk.add_block(body.clone());
5107                    let op = if name == "pfirst" {
5108                        Op::PFirstWithBlock(block_idx)
5109                    } else {
5110                        Op::PAnyWithBlock(block_idx)
5111                    };
5112                    self.emit_op(op, line, Some(root));
5113                }
5114                _ => {
5115                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
5116                    // `f(reverse LIST)` etc. flatten into `@_`. [`Self::pop_call_operands_flattened`]
5117                    // splats any array value at runtime, matching Perl's `@_` semantics.
5118                    for arg in args {
5119                        self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5120                    }
5121                    let q = self.qualify_sub_key(name);
5122                    let name_idx = self.chunk.intern_name(&q);
5123                    self.emit_op(
5124                        Op::Call(name_idx, args.len() as u8, ctx.as_byte()),
5125                        line,
5126                        Some(root),
5127                    );
5128                }
5129            },
5130
5131            // ── Method calls ──
5132            ExprKind::MethodCall {
5133                object,
5134                method,
5135                args,
5136                super_call,
5137            } => {
5138                self.compile_expr(object)?;
5139                for arg in args {
5140                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5141                }
5142                let name_idx = self.chunk.intern_name(method);
5143                if *super_call {
5144                    self.emit_op(
5145                        Op::MethodCallSuper(name_idx, args.len() as u8, ctx.as_byte()),
5146                        line,
5147                        Some(root),
5148                    );
5149                } else {
5150                    self.emit_op(
5151                        Op::MethodCall(name_idx, args.len() as u8, ctx.as_byte()),
5152                        line,
5153                        Some(root),
5154                    );
5155                }
5156            }
5157            ExprKind::IndirectCall {
5158                target,
5159                args,
5160                ampersand: _,
5161                pass_caller_arglist,
5162            } => {
5163                self.compile_expr(target)?;
5164                if !pass_caller_arglist {
5165                    for a in args {
5166                        self.compile_expr_ctx(a, WantarrayCtx::List)?;
5167                    }
5168                }
5169                let argc = if *pass_caller_arglist {
5170                    0
5171                } else {
5172                    args.len() as u8
5173                };
5174                self.emit_op(
5175                    Op::IndirectCall(
5176                        argc,
5177                        ctx.as_byte(),
5178                        if *pass_caller_arglist { 1 } else { 0 },
5179                    ),
5180                    line,
5181                    Some(root),
5182                );
5183            }
5184
5185            // ── Print / Say / Printf ──
5186            ExprKind::Print { handle, args } => {
5187                for arg in args {
5188                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5189                }
5190                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
5191                self.emit_op(Op::Print(h, args.len() as u8), line, Some(root));
5192            }
5193            ExprKind::Say { handle, args } => {
5194                for arg in args {
5195                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5196                }
5197                let h = handle.as_ref().map(|s| self.chunk.intern_name(s));
5198                self.emit_op(Op::Say(h, args.len() as u8), line, Some(root));
5199            }
5200            ExprKind::Printf { args, .. } => {
5201                // printf's format + arg list is Perl list context — ranges, arrays, and
5202                // `reverse`/`sort`/`grep` flatten into format argument positions.
5203                for arg in args {
5204                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5205                }
5206                self.emit_op(
5207                    Op::CallBuiltin(BuiltinId::Printf as u16, args.len() as u8),
5208                    line,
5209                    Some(root),
5210                );
5211            }
5212
5213            // ── Die / Warn ──
5214            ExprKind::Die(args) => {
5215                // die / warn take a list that gets stringified and concatenated — list context
5216                // so `die 1..5` matches Perl's "12345" stringification.
5217                for arg in args {
5218                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5219                }
5220                self.emit_op(
5221                    Op::CallBuiltin(BuiltinId::Die as u16, args.len() as u8),
5222                    line,
5223                    Some(root),
5224                );
5225            }
5226            ExprKind::Warn(args) => {
5227                for arg in args {
5228                    self.compile_expr_ctx(arg, WantarrayCtx::List)?;
5229                }
5230                self.emit_op(
5231                    Op::CallBuiltin(BuiltinId::Warn as u16, args.len() as u8),
5232                    line,
5233                    Some(root),
5234                );
5235            }
5236            ExprKind::Exit(code) => {
5237                if let Some(c) = code {
5238                    self.compile_expr(c)?;
5239                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
5240                } else {
5241                    self.emit_op(Op::LoadInt(0), line, Some(root));
5242                    self.emit_op(Op::CallBuiltin(BuiltinId::Exit as u16, 1), line, Some(root));
5243                }
5244            }
5245
5246            // ── Array ops ──
5247            ExprKind::Push { array, values } => {
5248                if let ExprKind::ArrayVar(name) = &array.kind {
5249                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
5250                    for v in values {
5251                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
5252                        self.emit_op(Op::PushArray(idx), line, Some(root));
5253                    }
5254                    self.emit_op(Op::ArrayLen(idx), line, Some(root));
5255                } else if let ExprKind::Deref {
5256                    expr: aref_expr,
5257                    kind: Sigil::Array,
5258                } = &array.kind
5259                {
5260                    self.compile_expr(aref_expr)?;
5261                    for v in values {
5262                        self.emit_op(Op::Dup, line, Some(root));
5263                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
5264                        self.emit_op(Op::PushArrayDeref, line, Some(root));
5265                    }
5266                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
5267                } else {
5268                    let pool = self
5269                        .chunk
5270                        .add_push_expr_entry(array.as_ref().clone(), values.clone());
5271                    self.emit_op(Op::PushExpr(pool), line, Some(root));
5272                }
5273            }
5274            ExprKind::Pop(array) => {
5275                if let ExprKind::ArrayVar(name) = &array.kind {
5276                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
5277                    self.emit_op(Op::PopArray(idx), line, Some(root));
5278                } else if let ExprKind::Deref {
5279                    expr: aref_expr,
5280                    kind: Sigil::Array,
5281                } = &array.kind
5282                {
5283                    self.compile_expr(aref_expr)?;
5284                    self.emit_op(Op::PopArrayDeref, line, Some(root));
5285                } else {
5286                    let pool = self.chunk.add_pop_expr_entry(array.as_ref().clone());
5287                    self.emit_op(Op::PopExpr(pool), line, Some(root));
5288                }
5289            }
5290            ExprKind::Shift(array) => {
5291                if let ExprKind::ArrayVar(name) = &array.kind {
5292                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
5293                    self.emit_op(Op::ShiftArray(idx), line, Some(root));
5294                } else if let ExprKind::Deref {
5295                    expr: aref_expr,
5296                    kind: Sigil::Array,
5297                } = &array.kind
5298                {
5299                    self.compile_expr(aref_expr)?;
5300                    self.emit_op(Op::ShiftArrayDeref, line, Some(root));
5301                } else {
5302                    let pool = self.chunk.add_shift_expr_entry(array.as_ref().clone());
5303                    self.emit_op(Op::ShiftExpr(pool), line, Some(root));
5304                }
5305            }
5306            ExprKind::Unshift { array, values } => {
5307                if let ExprKind::ArrayVar(name) = &array.kind {
5308                    let q = self.qualify_stash_array_name(name);
5309                    let name_const = self.chunk.add_constant(PerlValue::string(q));
5310                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
5311                    for v in values {
5312                        self.compile_expr_ctx(v, WantarrayCtx::List)?;
5313                    }
5314                    let nargs = (1 + values.len()) as u8;
5315                    self.emit_op(
5316                        Op::CallBuiltin(BuiltinId::Unshift as u16, nargs),
5317                        line,
5318                        Some(root),
5319                    );
5320                } else if let ExprKind::Deref {
5321                    expr: aref_expr,
5322                    kind: Sigil::Array,
5323                } = &array.kind
5324                {
5325                    if values.len() > u8::MAX as usize {
5326                        let pool = self
5327                            .chunk
5328                            .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
5329                        self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
5330                    } else {
5331                        self.compile_expr(aref_expr)?;
5332                        for v in values {
5333                            self.compile_expr_ctx(v, WantarrayCtx::List)?;
5334                        }
5335                        self.emit_op(Op::UnshiftArrayDeref(values.len() as u8), line, Some(root));
5336                    }
5337                } else {
5338                    let pool = self
5339                        .chunk
5340                        .add_unshift_expr_entry(array.as_ref().clone(), values.clone());
5341                    self.emit_op(Op::UnshiftExpr(pool), line, Some(root));
5342                }
5343            }
5344            ExprKind::Splice {
5345                array,
5346                offset,
5347                length,
5348                replacement,
5349            } => {
5350                self.emit_op(Op::WantarrayPush(ctx.as_byte()), line, Some(root));
5351                if let ExprKind::ArrayVar(name) = &array.kind {
5352                    let q = self.qualify_stash_array_name(name);
5353                    let name_const = self.chunk.add_constant(PerlValue::string(q));
5354                    self.emit_op(Op::LoadConst(name_const), line, Some(root));
5355                    if let Some(o) = offset {
5356                        self.compile_expr(o)?;
5357                    } else {
5358                        self.emit_op(Op::LoadInt(0), line, Some(root));
5359                    }
5360                    if let Some(l) = length {
5361                        self.compile_expr(l)?;
5362                    } else {
5363                        self.emit_op(Op::LoadUndef, line, Some(root));
5364                    }
5365                    for r in replacement {
5366                        self.compile_expr(r)?;
5367                    }
5368                    let nargs = (3 + replacement.len()) as u8;
5369                    self.emit_op(
5370                        Op::CallBuiltin(BuiltinId::Splice as u16, nargs),
5371                        line,
5372                        Some(root),
5373                    );
5374                } else if let ExprKind::Deref {
5375                    expr: aref_expr,
5376                    kind: Sigil::Array,
5377                } = &array.kind
5378                {
5379                    if replacement.len() > u8::MAX as usize {
5380                        let pool = self.chunk.add_splice_expr_entry(
5381                            array.as_ref().clone(),
5382                            offset.as_deref().cloned(),
5383                            length.as_deref().cloned(),
5384                            replacement.clone(),
5385                        );
5386                        self.emit_op(Op::SpliceExpr(pool), line, Some(root));
5387                    } else {
5388                        self.compile_expr(aref_expr)?;
5389                        if let Some(o) = offset {
5390                            self.compile_expr(o)?;
5391                        } else {
5392                            self.emit_op(Op::LoadInt(0), line, Some(root));
5393                        }
5394                        if let Some(l) = length {
5395                            self.compile_expr(l)?;
5396                        } else {
5397                            self.emit_op(Op::LoadUndef, line, Some(root));
5398                        }
5399                        for r in replacement {
5400                            self.compile_expr(r)?;
5401                        }
5402                        self.emit_op(
5403                            Op::SpliceArrayDeref(replacement.len() as u8),
5404                            line,
5405                            Some(root),
5406                        );
5407                    }
5408                } else {
5409                    let pool = self.chunk.add_splice_expr_entry(
5410                        array.as_ref().clone(),
5411                        offset.as_deref().cloned(),
5412                        length.as_deref().cloned(),
5413                        replacement.clone(),
5414                    );
5415                    self.emit_op(Op::SpliceExpr(pool), line, Some(root));
5416                }
5417                self.emit_op(Op::WantarrayPop, line, Some(root));
5418            }
5419            ExprKind::ScalarContext(inner) => {
5420                // `scalar EXPR` forces scalar context on EXPR regardless of the outer context
5421                // (e.g. `print scalar grep { } @x` — grep's result is a count, not a list).
5422                self.compile_expr_ctx(inner, WantarrayCtx::Scalar)?;
5423                // Then apply aggregate scalar semantics (set size, pipeline source len, …) —
5424                // same as [`Op::ValueScalarContext`] / [`PerlValue::scalar_context`].
5425                self.emit_op(Op::ValueScalarContext, line, Some(root));
5426            }
5427
5428            // ── Hash ops ──
5429            ExprKind::Delete(inner) => {
5430                if let ExprKind::HashElement { hash, key } = &inner.kind {
5431                    self.check_hash_mutable(hash, line)?;
5432                    let idx = self.chunk.intern_name(hash);
5433                    self.compile_expr(key)?;
5434                    self.emit_op(Op::DeleteHashElem(idx), line, Some(root));
5435                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
5436                    self.check_strict_array_access(array, line)?;
5437                    let q = self.qualify_stash_array_name(array);
5438                    self.check_array_mutable(&q, line)?;
5439                    let arr_idx = self.chunk.intern_name(&q);
5440                    self.compile_expr(index)?;
5441                    self.emit_op(Op::DeleteArrayElem(arr_idx), line, Some(root));
5442                } else if let ExprKind::ArrowDeref {
5443                    expr: container,
5444                    index,
5445                    kind: DerefKind::Hash,
5446                } = &inner.kind
5447                {
5448                    self.compile_arrow_hash_base_expr(container)?;
5449                    self.compile_expr(index)?;
5450                    self.emit_op(Op::DeleteArrowHashElem, line, Some(root));
5451                } else if let ExprKind::ArrowDeref {
5452                    expr: container,
5453                    index,
5454                    kind: DerefKind::Array,
5455                } = &inner.kind
5456                {
5457                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
5458                        self.compile_expr(container)?;
5459                        self.compile_expr(index)?;
5460                        self.emit_op(Op::DeleteArrowArrayElem, line, Some(root));
5461                    } else {
5462                        let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
5463                        self.emit_op(Op::DeleteExpr(pool), line, Some(root));
5464                    }
5465                } else {
5466                    let pool = self.chunk.add_delete_expr_entry(inner.as_ref().clone());
5467                    self.emit_op(Op::DeleteExpr(pool), line, Some(root));
5468                }
5469            }
5470            ExprKind::Exists(inner) => {
5471                if let ExprKind::HashElement { hash, key } = &inner.kind {
5472                    let idx = self.chunk.intern_name(hash);
5473                    self.compile_expr(key)?;
5474                    self.emit_op(Op::ExistsHashElem(idx), line, Some(root));
5475                } else if let ExprKind::ArrayElement { array, index } = &inner.kind {
5476                    self.check_strict_array_access(array, line)?;
5477                    let arr_idx = self
5478                        .chunk
5479                        .intern_name(&self.qualify_stash_array_name(array));
5480                    self.compile_expr(index)?;
5481                    self.emit_op(Op::ExistsArrayElem(arr_idx), line, Some(root));
5482                } else if let ExprKind::ArrowDeref {
5483                    expr: container,
5484                    index,
5485                    kind: DerefKind::Hash,
5486                } = &inner.kind
5487                {
5488                    self.compile_arrow_hash_base_expr(container)?;
5489                    self.compile_expr(index)?;
5490                    self.emit_op(Op::ExistsArrowHashElem, line, Some(root));
5491                } else if let ExprKind::ArrowDeref {
5492                    expr: container,
5493                    index,
5494                    kind: DerefKind::Array,
5495                } = &inner.kind
5496                {
5497                    if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
5498                        self.compile_expr(container)?;
5499                        self.compile_expr(index)?;
5500                        self.emit_op(Op::ExistsArrowArrayElem, line, Some(root));
5501                    } else {
5502                        let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
5503                        self.emit_op(Op::ExistsExpr(pool), line, Some(root));
5504                    }
5505                } else {
5506                    let pool = self.chunk.add_exists_expr_entry(inner.as_ref().clone());
5507                    self.emit_op(Op::ExistsExpr(pool), line, Some(root));
5508                }
5509            }
5510            ExprKind::Keys(inner) => {
5511                if let ExprKind::HashVar(name) = &inner.kind {
5512                    let idx = self.chunk.intern_name(name);
5513                    if ctx == WantarrayCtx::List {
5514                        self.emit_op(Op::HashKeys(idx), line, Some(root));
5515                    } else {
5516                        self.emit_op(Op::HashKeysScalar(idx), line, Some(root));
5517                    }
5518                } else {
5519                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
5520                    if ctx == WantarrayCtx::List {
5521                        self.emit_op(Op::KeysFromValue, line, Some(root));
5522                    } else {
5523                        self.emit_op(Op::KeysFromValueScalar, line, Some(root));
5524                    }
5525                }
5526            }
5527            ExprKind::Values(inner) => {
5528                if let ExprKind::HashVar(name) = &inner.kind {
5529                    let idx = self.chunk.intern_name(name);
5530                    if ctx == WantarrayCtx::List {
5531                        self.emit_op(Op::HashValues(idx), line, Some(root));
5532                    } else {
5533                        self.emit_op(Op::HashValuesScalar(idx), line, Some(root));
5534                    }
5535                } else {
5536                    self.compile_expr_ctx(inner, WantarrayCtx::List)?;
5537                    if ctx == WantarrayCtx::List {
5538                        self.emit_op(Op::ValuesFromValue, line, Some(root));
5539                    } else {
5540                        self.emit_op(Op::ValuesFromValueScalar, line, Some(root));
5541                    }
5542                }
5543            }
5544            ExprKind::Each(e) => {
5545                self.compile_expr(e)?;
5546                self.emit_op(Op::CallBuiltin(BuiltinId::Each as u16, 1), line, Some(root));
5547            }
5548
5549            // ── Builtins that map to CallBuiltin ──
5550            ExprKind::Length(e) => {
5551                self.compile_expr(e)?;
5552                self.emit_op(
5553                    Op::CallBuiltin(BuiltinId::Length as u16, 1),
5554                    line,
5555                    Some(root),
5556                );
5557            }
5558            ExprKind::Chomp(e) => {
5559                self.compile_expr(e)?;
5560                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
5561                self.emit_op(Op::ChompInPlace(lv), line, Some(root));
5562            }
5563            ExprKind::Chop(e) => {
5564                self.compile_expr(e)?;
5565                let lv = self.chunk.add_lvalue_expr(e.as_ref().clone());
5566                self.emit_op(Op::ChopInPlace(lv), line, Some(root));
5567            }
5568            ExprKind::Defined(e) => {
5569                self.compile_expr(e)?;
5570                self.emit_op(
5571                    Op::CallBuiltin(BuiltinId::Defined as u16, 1),
5572                    line,
5573                    Some(root),
5574                );
5575            }
5576            ExprKind::Abs(e) => {
5577                self.compile_expr(e)?;
5578                self.emit_op(Op::CallBuiltin(BuiltinId::Abs as u16, 1), line, Some(root));
5579            }
5580            ExprKind::Int(e) => {
5581                self.compile_expr(e)?;
5582                self.emit_op(Op::CallBuiltin(BuiltinId::Int as u16, 1), line, Some(root));
5583            }
5584            ExprKind::Sqrt(e) => {
5585                self.compile_expr(e)?;
5586                self.emit_op(Op::CallBuiltin(BuiltinId::Sqrt as u16, 1), line, Some(root));
5587            }
5588            ExprKind::Sin(e) => {
5589                self.compile_expr(e)?;
5590                self.emit_op(Op::CallBuiltin(BuiltinId::Sin as u16, 1), line, Some(root));
5591            }
5592            ExprKind::Cos(e) => {
5593                self.compile_expr(e)?;
5594                self.emit_op(Op::CallBuiltin(BuiltinId::Cos as u16, 1), line, Some(root));
5595            }
5596            ExprKind::Atan2 { y, x } => {
5597                self.compile_expr(y)?;
5598                self.compile_expr(x)?;
5599                self.emit_op(
5600                    Op::CallBuiltin(BuiltinId::Atan2 as u16, 2),
5601                    line,
5602                    Some(root),
5603                );
5604            }
5605            ExprKind::Exp(e) => {
5606                self.compile_expr(e)?;
5607                self.emit_op(Op::CallBuiltin(BuiltinId::Exp as u16, 1), line, Some(root));
5608            }
5609            ExprKind::Log(e) => {
5610                self.compile_expr(e)?;
5611                self.emit_op(Op::CallBuiltin(BuiltinId::Log as u16, 1), line, Some(root));
5612            }
5613            ExprKind::Rand(upper) => {
5614                if let Some(e) = upper {
5615                    self.compile_expr(e)?;
5616                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 1), line, Some(root));
5617                } else {
5618                    self.emit_op(Op::CallBuiltin(BuiltinId::Rand as u16, 0), line, Some(root));
5619                }
5620            }
5621            ExprKind::Srand(seed) => {
5622                if let Some(e) = seed {
5623                    self.compile_expr(e)?;
5624                    self.emit_op(
5625                        Op::CallBuiltin(BuiltinId::Srand as u16, 1),
5626                        line,
5627                        Some(root),
5628                    );
5629                } else {
5630                    self.emit_op(
5631                        Op::CallBuiltin(BuiltinId::Srand as u16, 0),
5632                        line,
5633                        Some(root),
5634                    );
5635                }
5636            }
5637            ExprKind::Chr(e) => {
5638                self.compile_expr(e)?;
5639                self.emit_op(Op::CallBuiltin(BuiltinId::Chr as u16, 1), line, Some(root));
5640            }
5641            ExprKind::Ord(e) => {
5642                self.compile_expr(e)?;
5643                self.emit_op(Op::CallBuiltin(BuiltinId::Ord as u16, 1), line, Some(root));
5644            }
5645            ExprKind::Hex(e) => {
5646                self.compile_expr(e)?;
5647                self.emit_op(Op::CallBuiltin(BuiltinId::Hex as u16, 1), line, Some(root));
5648            }
5649            ExprKind::Oct(e) => {
5650                self.compile_expr(e)?;
5651                self.emit_op(Op::CallBuiltin(BuiltinId::Oct as u16, 1), line, Some(root));
5652            }
5653            ExprKind::Uc(e) => {
5654                self.compile_expr(e)?;
5655                self.emit_op(Op::CallBuiltin(BuiltinId::Uc as u16, 1), line, Some(root));
5656            }
5657            ExprKind::Lc(e) => {
5658                self.compile_expr(e)?;
5659                self.emit_op(Op::CallBuiltin(BuiltinId::Lc as u16, 1), line, Some(root));
5660            }
5661            ExprKind::Ucfirst(e) => {
5662                self.compile_expr(e)?;
5663                self.emit_op(
5664                    Op::CallBuiltin(BuiltinId::Ucfirst as u16, 1),
5665                    line,
5666                    Some(root),
5667                );
5668            }
5669            ExprKind::Lcfirst(e) => {
5670                self.compile_expr(e)?;
5671                self.emit_op(
5672                    Op::CallBuiltin(BuiltinId::Lcfirst as u16, 1),
5673                    line,
5674                    Some(root),
5675                );
5676            }
5677            ExprKind::Fc(e) => {
5678                self.compile_expr(e)?;
5679                self.emit_op(Op::CallBuiltin(BuiltinId::Fc as u16, 1), line, Some(root));
5680            }
5681            ExprKind::Crypt { plaintext, salt } => {
5682                self.compile_expr(plaintext)?;
5683                self.compile_expr(salt)?;
5684                self.emit_op(
5685                    Op::CallBuiltin(BuiltinId::Crypt as u16, 2),
5686                    line,
5687                    Some(root),
5688                );
5689            }
5690            ExprKind::Pos(e) => match e {
5691                None => {
5692                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 0), line, Some(root));
5693                }
5694                Some(pos_arg) => {
5695                    if let ExprKind::ScalarVar(name) = &pos_arg.kind {
5696                        let stor = self.scalar_storage_name_for_ops(name);
5697                        let idx = self.chunk.add_constant(PerlValue::string(stor));
5698                        self.emit_op(Op::LoadConst(idx), line, Some(root));
5699                    } else {
5700                        self.compile_expr(pos_arg)?;
5701                    }
5702                    self.emit_op(Op::CallBuiltin(BuiltinId::Pos as u16, 1), line, Some(root));
5703                }
5704            },
5705            ExprKind::Study(e) => {
5706                self.compile_expr(e)?;
5707                self.emit_op(
5708                    Op::CallBuiltin(BuiltinId::Study as u16, 1),
5709                    line,
5710                    Some(root),
5711                );
5712            }
5713            ExprKind::Ref(e) => {
5714                self.compile_expr(e)?;
5715                self.emit_op(Op::CallBuiltin(BuiltinId::Ref as u16, 1), line, Some(root));
5716            }
5717            ExprKind::Rev(e) => {
5718                // Compile in list context to get arrays/hashes as collections
5719                self.compile_expr_ctx(e, WantarrayCtx::List)?;
5720                if ctx == WantarrayCtx::List {
5721                    self.emit_op(Op::RevListOp, line, Some(root));
5722                } else {
5723                    self.emit_op(Op::RevScalarOp, line, Some(root));
5724                }
5725            }
5726            ExprKind::ReverseExpr(e) => {
5727                self.compile_expr_ctx(e, WantarrayCtx::List)?;
5728                if ctx == WantarrayCtx::List {
5729                    self.emit_op(Op::ReverseListOp, line, Some(root));
5730                } else {
5731                    self.emit_op(Op::ReverseScalarOp, line, Some(root));
5732                }
5733            }
5734            ExprKind::System(args) => {
5735                for a in args {
5736                    self.compile_expr(a)?;
5737                }
5738                self.emit_op(
5739                    Op::CallBuiltin(BuiltinId::System as u16, args.len() as u8),
5740                    line,
5741                    Some(root),
5742                );
5743            }
5744            ExprKind::Exec(args) => {
5745                for a in args {
5746                    self.compile_expr(a)?;
5747                }
5748                self.emit_op(
5749                    Op::CallBuiltin(BuiltinId::Exec as u16, args.len() as u8),
5750                    line,
5751                    Some(root),
5752                );
5753            }
5754
5755            // ── String builtins ──
5756            ExprKind::Substr {
5757                string,
5758                offset,
5759                length,
5760                replacement,
5761            } => {
5762                if let Some(rep) = replacement {
5763                    let idx = self.chunk.add_substr_four_arg_entry(
5764                        string.as_ref().clone(),
5765                        offset.as_ref().clone(),
5766                        length.as_ref().map(|b| b.as_ref().clone()),
5767                        rep.as_ref().clone(),
5768                    );
5769                    self.emit_op(Op::SubstrFourArg(idx), line, Some(root));
5770                } else {
5771                    self.compile_expr(string)?;
5772                    self.compile_expr(offset)?;
5773                    let mut argc: u8 = 2;
5774                    if let Some(len) = length {
5775                        self.compile_expr(len)?;
5776                        argc = 3;
5777                    }
5778                    self.emit_op(
5779                        Op::CallBuiltin(BuiltinId::Substr as u16, argc),
5780                        line,
5781                        Some(root),
5782                    );
5783                }
5784            }
5785            ExprKind::Index {
5786                string,
5787                substr,
5788                position,
5789            } => {
5790                self.compile_expr(string)?;
5791                self.compile_expr(substr)?;
5792                if let Some(pos) = position {
5793                    self.compile_expr(pos)?;
5794                    self.emit_op(
5795                        Op::CallBuiltin(BuiltinId::Index as u16, 3),
5796                        line,
5797                        Some(root),
5798                    );
5799                } else {
5800                    self.emit_op(
5801                        Op::CallBuiltin(BuiltinId::Index as u16, 2),
5802                        line,
5803                        Some(root),
5804                    );
5805                }
5806            }
5807            ExprKind::Rindex {
5808                string,
5809                substr,
5810                position,
5811            } => {
5812                self.compile_expr(string)?;
5813                self.compile_expr(substr)?;
5814                if let Some(pos) = position {
5815                    self.compile_expr(pos)?;
5816                    self.emit_op(
5817                        Op::CallBuiltin(BuiltinId::Rindex as u16, 3),
5818                        line,
5819                        Some(root),
5820                    );
5821                } else {
5822                    self.emit_op(
5823                        Op::CallBuiltin(BuiltinId::Rindex as u16, 2),
5824                        line,
5825                        Some(root),
5826                    );
5827                }
5828            }
5829
5830            ExprKind::JoinExpr { separator, list } => {
5831                self.compile_expr(separator)?;
5832                // Arguments after the separator are evaluated in list context (Perl 5).
5833                self.compile_expr_ctx(list, WantarrayCtx::List)?;
5834                self.emit_op(Op::CallBuiltin(BuiltinId::Join as u16, 2), line, Some(root));
5835            }
5836            ExprKind::SplitExpr {
5837                pattern,
5838                string,
5839                limit,
5840            } => {
5841                self.compile_expr(pattern)?;
5842                self.compile_expr(string)?;
5843                if let Some(l) = limit {
5844                    self.compile_expr(l)?;
5845                    self.emit_op(
5846                        Op::CallBuiltin(BuiltinId::Split as u16, 3),
5847                        line,
5848                        Some(root),
5849                    );
5850                } else {
5851                    self.emit_op(
5852                        Op::CallBuiltin(BuiltinId::Split as u16, 2),
5853                        line,
5854                        Some(root),
5855                    );
5856                }
5857            }
5858            ExprKind::Sprintf { format, args } => {
5859                // sprintf's arg list after the format is Perl list context — ranges, arrays,
5860                // and `reverse`/`sort`/`grep` flatten into the format argument positions.
5861                self.compile_expr(format)?;
5862                for a in args {
5863                    self.compile_expr_ctx(a, WantarrayCtx::List)?;
5864                }
5865                self.emit_op(
5866                    Op::CallBuiltin(BuiltinId::Sprintf as u16, (1 + args.len()) as u8),
5867                    line,
5868                    Some(root),
5869                );
5870            }
5871
5872            // ── I/O ──
5873            ExprKind::Open { handle, mode, file } => {
5874                if let ExprKind::OpenMyHandle { name } = &handle.kind {
5875                    let name_idx = self.chunk.intern_name(name);
5876                    self.emit_op(Op::LoadUndef, line, Some(root));
5877                    self.emit_declare_scalar(name_idx, line, false);
5878                    let h_idx = self.chunk.add_constant(PerlValue::string(name.clone()));
5879                    self.emit_op(Op::LoadConst(h_idx), line, Some(root));
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                    self.emit_op(Op::SetScalarKeepPlain(name_idx), line, Some(root));
5888                    return Ok(());
5889                }
5890                self.compile_expr(handle)?;
5891                self.compile_expr(mode)?;
5892                if let Some(f) = file {
5893                    self.compile_expr(f)?;
5894                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 3), line, Some(root));
5895                } else {
5896                    self.emit_op(Op::CallBuiltin(BuiltinId::Open as u16, 2), line, Some(root));
5897                }
5898            }
5899            ExprKind::OpenMyHandle { .. } => {
5900                return Err(CompileError::Unsupported(
5901                    "open my $fh handle expression".into(),
5902                ));
5903            }
5904            ExprKind::Close(e) => {
5905                self.compile_expr(e)?;
5906                self.emit_op(
5907                    Op::CallBuiltin(BuiltinId::Close as u16, 1),
5908                    line,
5909                    Some(root),
5910                );
5911            }
5912            ExprKind::ReadLine(handle) => {
5913                let bid = if ctx == WantarrayCtx::List {
5914                    BuiltinId::ReadLineList
5915                } else {
5916                    BuiltinId::ReadLine
5917                };
5918                if let Some(h) = handle {
5919                    let idx = self.chunk.add_constant(PerlValue::string(h.clone()));
5920                    self.emit_op(Op::LoadConst(idx), line, Some(root));
5921                    self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
5922                } else {
5923                    self.emit_op(Op::CallBuiltin(bid as u16, 0), line, Some(root));
5924                }
5925            }
5926            ExprKind::Eof(e) => {
5927                if let Some(inner) = e {
5928                    self.compile_expr(inner)?;
5929                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 1), line, Some(root));
5930                } else {
5931                    self.emit_op(Op::CallBuiltin(BuiltinId::Eof as u16, 0), line, Some(root));
5932                }
5933            }
5934            ExprKind::Opendir { handle, path } => {
5935                self.compile_expr(handle)?;
5936                self.compile_expr(path)?;
5937                self.emit_op(
5938                    Op::CallBuiltin(BuiltinId::Opendir as u16, 2),
5939                    line,
5940                    Some(root),
5941                );
5942            }
5943            ExprKind::Readdir(e) => {
5944                let bid = if ctx == WantarrayCtx::List {
5945                    BuiltinId::ReaddirList
5946                } else {
5947                    BuiltinId::Readdir
5948                };
5949                self.compile_expr(e)?;
5950                self.emit_op(Op::CallBuiltin(bid as u16, 1), line, Some(root));
5951            }
5952            ExprKind::Closedir(e) => {
5953                self.compile_expr(e)?;
5954                self.emit_op(
5955                    Op::CallBuiltin(BuiltinId::Closedir as u16, 1),
5956                    line,
5957                    Some(root),
5958                );
5959            }
5960            ExprKind::Rewinddir(e) => {
5961                self.compile_expr(e)?;
5962                self.emit_op(
5963                    Op::CallBuiltin(BuiltinId::Rewinddir as u16, 1),
5964                    line,
5965                    Some(root),
5966                );
5967            }
5968            ExprKind::Telldir(e) => {
5969                self.compile_expr(e)?;
5970                self.emit_op(
5971                    Op::CallBuiltin(BuiltinId::Telldir as u16, 1),
5972                    line,
5973                    Some(root),
5974                );
5975            }
5976            ExprKind::Seekdir { handle, position } => {
5977                self.compile_expr(handle)?;
5978                self.compile_expr(position)?;
5979                self.emit_op(
5980                    Op::CallBuiltin(BuiltinId::Seekdir as u16, 2),
5981                    line,
5982                    Some(root),
5983                );
5984            }
5985
5986            // ── File tests ──
5987            ExprKind::FileTest { op, expr } => {
5988                self.compile_expr(expr)?;
5989                self.emit_op(Op::FileTestOp(*op as u8), line, Some(root));
5990            }
5991
5992            // ── Eval / Do / Require ──
5993            ExprKind::Eval(e) => {
5994                self.compile_expr(e)?;
5995                self.emit_op(Op::CallBuiltin(BuiltinId::Eval as u16, 1), line, Some(root));
5996            }
5997            ExprKind::Do(e) => {
5998                // do { BLOCK } executes the block; do "file" loads a file
5999                if let ExprKind::CodeRef { body, .. } = &e.kind {
6000                    let block_idx = self.chunk.add_block(body.clone());
6001                    self.emit_op(Op::EvalBlock(block_idx, ctx.as_byte()), line, Some(root));
6002                } else {
6003                    self.compile_expr(e)?;
6004                    self.emit_op(Op::CallBuiltin(BuiltinId::Do as u16, 1), line, Some(root));
6005                }
6006            }
6007            ExprKind::Require(e) => {
6008                self.compile_expr(e)?;
6009                self.emit_op(
6010                    Op::CallBuiltin(BuiltinId::Require as u16, 1),
6011                    line,
6012                    Some(root),
6013                );
6014            }
6015
6016            // ── Filesystem ──
6017            ExprKind::Chdir(e) => {
6018                self.compile_expr(e)?;
6019                self.emit_op(
6020                    Op::CallBuiltin(BuiltinId::Chdir as u16, 1),
6021                    line,
6022                    Some(root),
6023                );
6024            }
6025            ExprKind::Mkdir { path, mode } => {
6026                self.compile_expr(path)?;
6027                if let Some(m) = mode {
6028                    self.compile_expr(m)?;
6029                    self.emit_op(
6030                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 2),
6031                        line,
6032                        Some(root),
6033                    );
6034                } else {
6035                    self.emit_op(
6036                        Op::CallBuiltin(BuiltinId::Mkdir as u16, 1),
6037                        line,
6038                        Some(root),
6039                    );
6040                }
6041            }
6042            ExprKind::Unlink(args) => {
6043                for a in args {
6044                    self.compile_expr(a)?;
6045                }
6046                self.emit_op(
6047                    Op::CallBuiltin(BuiltinId::Unlink as u16, args.len() as u8),
6048                    line,
6049                    Some(root),
6050                );
6051            }
6052            ExprKind::Rename { old, new } => {
6053                self.compile_expr(old)?;
6054                self.compile_expr(new)?;
6055                self.emit_op(
6056                    Op::CallBuiltin(BuiltinId::Rename as u16, 2),
6057                    line,
6058                    Some(root),
6059                );
6060            }
6061            ExprKind::Chmod(args) => {
6062                for a in args {
6063                    self.compile_expr(a)?;
6064                }
6065                self.emit_op(
6066                    Op::CallBuiltin(BuiltinId::Chmod as u16, args.len() as u8),
6067                    line,
6068                    Some(root),
6069                );
6070            }
6071            ExprKind::Chown(args) => {
6072                for a in args {
6073                    self.compile_expr(a)?;
6074                }
6075                self.emit_op(
6076                    Op::CallBuiltin(BuiltinId::Chown as u16, args.len() as u8),
6077                    line,
6078                    Some(root),
6079                );
6080            }
6081            ExprKind::Stat(e) => {
6082                self.compile_expr(e)?;
6083                self.emit_op(Op::CallBuiltin(BuiltinId::Stat as u16, 1), line, Some(root));
6084            }
6085            ExprKind::Lstat(e) => {
6086                self.compile_expr(e)?;
6087                self.emit_op(
6088                    Op::CallBuiltin(BuiltinId::Lstat as u16, 1),
6089                    line,
6090                    Some(root),
6091                );
6092            }
6093            ExprKind::Link { old, new } => {
6094                self.compile_expr(old)?;
6095                self.compile_expr(new)?;
6096                self.emit_op(Op::CallBuiltin(BuiltinId::Link as u16, 2), line, Some(root));
6097            }
6098            ExprKind::Symlink { old, new } => {
6099                self.compile_expr(old)?;
6100                self.compile_expr(new)?;
6101                self.emit_op(
6102                    Op::CallBuiltin(BuiltinId::Symlink as u16, 2),
6103                    line,
6104                    Some(root),
6105                );
6106            }
6107            ExprKind::Readlink(e) => {
6108                self.compile_expr(e)?;
6109                self.emit_op(
6110                    Op::CallBuiltin(BuiltinId::Readlink as u16, 1),
6111                    line,
6112                    Some(root),
6113                );
6114            }
6115            ExprKind::Files(args) => {
6116                for a in args {
6117                    self.compile_expr(a)?;
6118                }
6119                self.emit_op(
6120                    Op::CallBuiltin(BuiltinId::Files as u16, args.len() as u8),
6121                    line,
6122                    Some(root),
6123                );
6124            }
6125            ExprKind::Filesf(args) => {
6126                for a in args {
6127                    self.compile_expr(a)?;
6128                }
6129                self.emit_op(
6130                    Op::CallBuiltin(BuiltinId::Filesf as u16, args.len() as u8),
6131                    line,
6132                    Some(root),
6133                );
6134            }
6135            ExprKind::FilesfRecursive(args) => {
6136                for a in args {
6137                    self.compile_expr(a)?;
6138                }
6139                self.emit_op(
6140                    Op::CallBuiltin(BuiltinId::FilesfRecursive as u16, args.len() as u8),
6141                    line,
6142                    Some(root),
6143                );
6144            }
6145            ExprKind::Dirs(args) => {
6146                for a in args {
6147                    self.compile_expr(a)?;
6148                }
6149                self.emit_op(
6150                    Op::CallBuiltin(BuiltinId::Dirs as u16, args.len() as u8),
6151                    line,
6152                    Some(root),
6153                );
6154            }
6155            ExprKind::DirsRecursive(args) => {
6156                for a in args {
6157                    self.compile_expr(a)?;
6158                }
6159                self.emit_op(
6160                    Op::CallBuiltin(BuiltinId::DirsRecursive as u16, args.len() as u8),
6161                    line,
6162                    Some(root),
6163                );
6164            }
6165            ExprKind::SymLinks(args) => {
6166                for a in args {
6167                    self.compile_expr(a)?;
6168                }
6169                self.emit_op(
6170                    Op::CallBuiltin(BuiltinId::SymLinks as u16, args.len() as u8),
6171                    line,
6172                    Some(root),
6173                );
6174            }
6175            ExprKind::Sockets(args) => {
6176                for a in args {
6177                    self.compile_expr(a)?;
6178                }
6179                self.emit_op(
6180                    Op::CallBuiltin(BuiltinId::Sockets as u16, args.len() as u8),
6181                    line,
6182                    Some(root),
6183                );
6184            }
6185            ExprKind::Pipes(args) => {
6186                for a in args {
6187                    self.compile_expr(a)?;
6188                }
6189                self.emit_op(
6190                    Op::CallBuiltin(BuiltinId::Pipes as u16, args.len() as u8),
6191                    line,
6192                    Some(root),
6193                );
6194            }
6195            ExprKind::BlockDevices(args) => {
6196                for a in args {
6197                    self.compile_expr(a)?;
6198                }
6199                self.emit_op(
6200                    Op::CallBuiltin(BuiltinId::BlockDevices as u16, args.len() as u8),
6201                    line,
6202                    Some(root),
6203                );
6204            }
6205            ExprKind::CharDevices(args) => {
6206                for a in args {
6207                    self.compile_expr(a)?;
6208                }
6209                self.emit_op(
6210                    Op::CallBuiltin(BuiltinId::CharDevices as u16, args.len() as u8),
6211                    line,
6212                    Some(root),
6213                );
6214            }
6215            ExprKind::Executables(args) => {
6216                for a in args {
6217                    self.compile_expr(a)?;
6218                }
6219                self.emit_op(
6220                    Op::CallBuiltin(BuiltinId::Executables as u16, args.len() as u8),
6221                    line,
6222                    Some(root),
6223                );
6224            }
6225            ExprKind::Glob(args) => {
6226                for a in args {
6227                    self.compile_expr(a)?;
6228                }
6229                self.emit_op(
6230                    Op::CallBuiltin(BuiltinId::Glob as u16, args.len() as u8),
6231                    line,
6232                    Some(root),
6233                );
6234            }
6235            ExprKind::GlobPar { args, progress } => {
6236                for a in args {
6237                    self.compile_expr(a)?;
6238                }
6239                match progress {
6240                    None => {
6241                        self.emit_op(
6242                            Op::CallBuiltin(BuiltinId::GlobPar as u16, args.len() as u8),
6243                            line,
6244                            Some(root),
6245                        );
6246                    }
6247                    Some(p) => {
6248                        self.compile_expr(p)?;
6249                        self.emit_op(
6250                            Op::CallBuiltin(
6251                                BuiltinId::GlobParProgress as u16,
6252                                (args.len() + 1) as u8,
6253                            ),
6254                            line,
6255                            Some(root),
6256                        );
6257                    }
6258                }
6259            }
6260            ExprKind::ParSed { args, progress } => {
6261                for a in args {
6262                    self.compile_expr(a)?;
6263                }
6264                match progress {
6265                    None => {
6266                        self.emit_op(
6267                            Op::CallBuiltin(BuiltinId::ParSed as u16, args.len() as u8),
6268                            line,
6269                            Some(root),
6270                        );
6271                    }
6272                    Some(p) => {
6273                        self.compile_expr(p)?;
6274                        self.emit_op(
6275                            Op::CallBuiltin(
6276                                BuiltinId::ParSedProgress as u16,
6277                                (args.len() + 1) as u8,
6278                            ),
6279                            line,
6280                            Some(root),
6281                        );
6282                    }
6283                }
6284            }
6285
6286            // ── OOP ──
6287            ExprKind::Bless { ref_expr, class } => {
6288                self.compile_expr(ref_expr)?;
6289                if let Some(c) = class {
6290                    self.compile_expr(c)?;
6291                    self.emit_op(
6292                        Op::CallBuiltin(BuiltinId::Bless as u16, 2),
6293                        line,
6294                        Some(root),
6295                    );
6296                } else {
6297                    self.emit_op(
6298                        Op::CallBuiltin(BuiltinId::Bless as u16, 1),
6299                        line,
6300                        Some(root),
6301                    );
6302                }
6303            }
6304            ExprKind::Caller(e) => {
6305                if let Some(inner) = e {
6306                    self.compile_expr(inner)?;
6307                    self.emit_op(
6308                        Op::CallBuiltin(BuiltinId::Caller as u16, 1),
6309                        line,
6310                        Some(root),
6311                    );
6312                } else {
6313                    self.emit_op(
6314                        Op::CallBuiltin(BuiltinId::Caller as u16, 0),
6315                        line,
6316                        Some(root),
6317                    );
6318                }
6319            }
6320            ExprKind::Wantarray => {
6321                self.emit_op(
6322                    Op::CallBuiltin(BuiltinId::Wantarray as u16, 0),
6323                    line,
6324                    Some(root),
6325                );
6326            }
6327
6328            // ── References ──
6329            ExprKind::ScalarRef(e) => match &e.kind {
6330                ExprKind::ScalarVar(name) => {
6331                    let idx = self.intern_scalar_var_for_ops(name);
6332                    self.emit_op(Op::MakeScalarBindingRef(idx), line, Some(root));
6333                }
6334                ExprKind::ArrayVar(name) => {
6335                    self.check_strict_array_access(name, line)?;
6336                    let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
6337                    self.emit_op(Op::MakeArrayBindingRef(idx), line, Some(root));
6338                }
6339                ExprKind::HashVar(name) => {
6340                    self.check_strict_hash_access(name, line)?;
6341                    let idx = self.chunk.intern_name(name);
6342                    self.emit_op(Op::MakeHashBindingRef(idx), line, Some(root));
6343                }
6344                ExprKind::Deref {
6345                    expr: inner,
6346                    kind: Sigil::Array,
6347                } => {
6348                    self.compile_expr(inner)?;
6349                    self.emit_op(Op::MakeArrayRefAlias, line, Some(root));
6350                }
6351                ExprKind::Deref {
6352                    expr: inner,
6353                    kind: Sigil::Hash,
6354                } => {
6355                    self.compile_expr(inner)?;
6356                    self.emit_op(Op::MakeHashRefAlias, line, Some(root));
6357                }
6358                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
6359                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
6360                    self.emit_op(Op::MakeArrayRef, line, Some(root));
6361                }
6362                ExprKind::AnonymousListSlice { .. } | ExprKind::HashSliceDeref { .. } => {
6363                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
6364                    self.emit_op(Op::MakeArrayRef, line, Some(root));
6365                }
6366                _ => {
6367                    self.compile_expr(e)?;
6368                    self.emit_op(Op::MakeScalarRef, line, Some(root));
6369                }
6370            },
6371            ExprKind::ArrayRef(elems) => {
6372                // `[ LIST ]` — each element is in list context so `1..5`, `reverse`, `grep`
6373                // and array variables flatten through [`Op::MakeArray`], which already splats
6374                // nested arrays.
6375                for e in elems {
6376                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
6377                }
6378                self.emit_op(Op::MakeArray(elems.len() as u16), line, Some(root));
6379                self.emit_op(Op::MakeArrayRef, line, Some(root));
6380            }
6381            ExprKind::HashRef(pairs) => {
6382                // `{ K => V, ... }` — keys are scalar, values are list context so ranges and
6383                // slurpy constructs on the value side flatten into the built hash.
6384                for (k, v) in pairs {
6385                    self.compile_expr(k)?;
6386                    self.compile_expr_ctx(v, WantarrayCtx::List)?;
6387                }
6388                self.emit_op(Op::MakeHash((pairs.len() * 2) as u16), line, Some(root));
6389                self.emit_op(Op::MakeHashRef, line, Some(root));
6390            }
6391            ExprKind::CodeRef { body, params } => {
6392                let block_idx = self.chunk.add_block(body.clone());
6393                let sig_idx = self.chunk.add_code_ref_sig(params.clone());
6394                self.emit_op(Op::MakeCodeRef(block_idx, sig_idx), line, Some(root));
6395            }
6396            ExprKind::SubroutineRef(name) => {
6397                // Unary `&name` — invoke subroutine with no explicit args (same as tree `call_named_sub`).
6398                let q = self.qualify_sub_key(name);
6399                let name_idx = self.chunk.intern_name(&q);
6400                self.emit_op(Op::Call(name_idx, 0, ctx.as_byte()), line, Some(root));
6401            }
6402            ExprKind::SubroutineCodeRef(name) => {
6403                // `\&name` — coderef (must exist at run time).
6404                let name_idx = self.chunk.intern_name(name);
6405                self.emit_op(Op::LoadNamedSubRef(name_idx), line, Some(root));
6406            }
6407            ExprKind::DynamicSubCodeRef(expr) => {
6408                self.compile_expr(expr)?;
6409                self.emit_op(Op::LoadDynamicSubRef, line, Some(root));
6410            }
6411
6412            // ── Derefs ──
6413            ExprKind::ArrowDeref { expr, index, kind } => match kind {
6414                DerefKind::Array => {
6415                    self.compile_arrow_array_base_expr(expr)?;
6416                    let mut used_arrow_slice = false;
6417                    if let ExprKind::List(indices) = &index.kind {
6418                        for ix in indices {
6419                            self.compile_array_slice_index_expr(ix)?;
6420                        }
6421                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, Some(root));
6422                        used_arrow_slice = true;
6423                    } else if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
6424                        self.compile_expr(index)?;
6425                        self.emit_op(Op::ArrowArray, line, Some(root));
6426                    } else {
6427                        // One subscript expr may expand to multiple indices (`$r->[0..1]`, `[(0,1)]`).
6428                        self.compile_array_slice_index_expr(index)?;
6429                        self.emit_op(Op::ArrowArraySlice(1), line, Some(root));
6430                        used_arrow_slice = true;
6431                    }
6432                    if used_arrow_slice && ctx != WantarrayCtx::List {
6433                        self.emit_op(Op::ListSliceToScalar, line, Some(root));
6434                    }
6435                }
6436                DerefKind::Hash => {
6437                    self.compile_arrow_hash_base_expr(expr)?;
6438                    self.compile_expr(index)?;
6439                    self.emit_op(Op::ArrowHash, line, Some(root));
6440                }
6441                DerefKind::Call => {
6442                    self.compile_expr(expr)?;
6443                    // Always compile args in list context to preserve all arguments
6444                    self.compile_expr_ctx(index, WantarrayCtx::List)?;
6445                    self.emit_op(Op::ArrowCall(ctx.as_byte()), line, Some(root));
6446                }
6447            },
6448            ExprKind::Deref { expr, kind } => {
6449                // Perl: `scalar @{EXPR}` / `scalar @$r` is the array length (not a copy of the list).
6450                // `scalar %{EXPR}` uses hash fill metrics like `%h` in scalar context.
6451                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
6452                    self.compile_expr(expr)?;
6453                    self.emit_op(Op::ArrayDerefLen, line, Some(root));
6454                } else if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
6455                    self.compile_expr(expr)?;
6456                    self.emit_op(Op::SymbolicDeref(2), line, Some(root));
6457                    self.emit_op(Op::ValueScalarContext, line, Some(root));
6458                } else {
6459                    self.compile_expr(expr)?;
6460                    let b = match kind {
6461                        Sigil::Scalar => 0u8,
6462                        Sigil::Array => 1,
6463                        Sigil::Hash => 2,
6464                        Sigil::Typeglob => 3,
6465                    };
6466                    self.emit_op(Op::SymbolicDeref(b), line, Some(root));
6467                }
6468            }
6469
6470            // ── Interpolated strings ──
6471            ExprKind::InterpolatedString(parts) => {
6472                // Check if any literal part contains case-escape sequences.
6473                let has_case_escapes = parts.iter().any(|p| {
6474                    if let StringPart::Literal(s) = p {
6475                        s.contains('\\')
6476                            && (s.contains("\\U")
6477                                || s.contains("\\L")
6478                                || s.contains("\\u")
6479                                || s.contains("\\l")
6480                                || s.contains("\\Q")
6481                                || s.contains("\\E"))
6482                    } else {
6483                        false
6484                    }
6485                });
6486                if parts.is_empty() {
6487                    let idx = self.chunk.add_constant(PerlValue::string(String::new()));
6488                    self.emit_op(Op::LoadConst(idx), line, Some(root));
6489                } else {
6490                    // `"$x"` is a single [`StringPart`] — still string context; must go through
6491                    // [`Op::Concat`] so operands are stringified (`use overload '""'`, etc.).
6492                    if !matches!(&parts[0], StringPart::Literal(_)) {
6493                        let idx = self.chunk.add_constant(PerlValue::string(String::new()));
6494                        self.emit_op(Op::LoadConst(idx), line, Some(root));
6495                    }
6496                    self.compile_string_part(&parts[0], line, Some(root))?;
6497                    for part in &parts[1..] {
6498                        self.compile_string_part(part, line, Some(root))?;
6499                        self.emit_op(Op::Concat, line, Some(root));
6500                    }
6501                    if !matches!(&parts[0], StringPart::Literal(_)) {
6502                        self.emit_op(Op::Concat, line, Some(root));
6503                    }
6504                }
6505                if has_case_escapes {
6506                    self.emit_op(Op::ProcessCaseEscapes, line, Some(root));
6507                }
6508            }
6509
6510            // ── List ──
6511            ExprKind::List(exprs) => {
6512                if ctx == WantarrayCtx::Scalar {
6513                    // Perl: comma-list in scalar context evaluates to the **last** element (`(1,2)` → 2).
6514                    if let Some(last) = exprs.last() {
6515                        self.compile_expr_ctx(last, WantarrayCtx::Scalar)?;
6516                    } else {
6517                        self.emit_op(Op::LoadUndef, line, Some(root));
6518                    }
6519                } else {
6520                    for e in exprs {
6521                        self.compile_expr_ctx(e, ctx)?;
6522                    }
6523                    if exprs.len() != 1 {
6524                        self.emit_op(Op::MakeArray(exprs.len() as u16), line, Some(root));
6525                    }
6526                }
6527            }
6528
6529            // ── QW ──
6530            ExprKind::QW(words) => {
6531                for w in words {
6532                    let idx = self.chunk.add_constant(PerlValue::string(w.clone()));
6533                    self.emit_op(Op::LoadConst(idx), line, Some(root));
6534                }
6535                self.emit_op(Op::MakeArray(words.len() as u16), line, Some(root));
6536            }
6537
6538            // ── Postfix if/unless ──
6539            ExprKind::PostfixIf { expr, condition } => {
6540                self.compile_boolean_rvalue_condition(condition)?;
6541                let j = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
6542                self.compile_expr(expr)?;
6543                let end = self.emit_op(Op::Jump(0), line, Some(root));
6544                self.chunk.patch_jump_here(j);
6545                self.emit_op(Op::LoadUndef, line, Some(root));
6546                self.chunk.patch_jump_here(end);
6547            }
6548            ExprKind::PostfixUnless { expr, condition } => {
6549                self.compile_boolean_rvalue_condition(condition)?;
6550                let j = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
6551                self.compile_expr(expr)?;
6552                let end = self.emit_op(Op::Jump(0), line, Some(root));
6553                self.chunk.patch_jump_here(j);
6554                self.emit_op(Op::LoadUndef, line, Some(root));
6555                self.chunk.patch_jump_here(end);
6556            }
6557
6558            // ── Postfix while/until/foreach ──
6559            ExprKind::PostfixWhile { expr, condition } => {
6560                // Detect `do { BLOCK } while (COND)` pattern
6561                let is_do_block = matches!(
6562                    &expr.kind,
6563                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
6564                );
6565                if is_do_block {
6566                    // do-while: body executes before first condition check
6567                    let loop_start = self.chunk.len();
6568                    self.compile_expr(expr)?;
6569                    self.emit_op(Op::Pop, line, Some(root));
6570                    self.compile_boolean_rvalue_condition(condition)?;
6571                    self.emit_op(Op::JumpIfTrue(loop_start), line, Some(root));
6572                    self.emit_op(Op::LoadUndef, line, Some(root));
6573                } else {
6574                    // Regular postfix while: condition checked first
6575                    let loop_start = self.chunk.len();
6576                    self.compile_boolean_rvalue_condition(condition)?;
6577                    let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
6578                    self.compile_expr(expr)?;
6579                    self.emit_op(Op::Pop, line, Some(root));
6580                    self.emit_op(Op::Jump(loop_start), line, Some(root));
6581                    self.chunk.patch_jump_here(exit_jump);
6582                    self.emit_op(Op::LoadUndef, line, Some(root));
6583                }
6584            }
6585            ExprKind::PostfixUntil { expr, condition } => {
6586                let is_do_block = matches!(
6587                    &expr.kind,
6588                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
6589                );
6590                if is_do_block {
6591                    let loop_start = self.chunk.len();
6592                    self.compile_expr(expr)?;
6593                    self.emit_op(Op::Pop, line, Some(root));
6594                    self.compile_boolean_rvalue_condition(condition)?;
6595                    self.emit_op(Op::JumpIfFalse(loop_start), line, Some(root));
6596                    self.emit_op(Op::LoadUndef, line, Some(root));
6597                } else {
6598                    let loop_start = self.chunk.len();
6599                    self.compile_boolean_rvalue_condition(condition)?;
6600                    let exit_jump = self.emit_op(Op::JumpIfTrue(0), line, Some(root));
6601                    self.compile_expr(expr)?;
6602                    self.emit_op(Op::Pop, line, Some(root));
6603                    self.emit_op(Op::Jump(loop_start), line, Some(root));
6604                    self.chunk.patch_jump_here(exit_jump);
6605                    self.emit_op(Op::LoadUndef, line, Some(root));
6606                }
6607            }
6608            ExprKind::PostfixForeach { expr, list } => {
6609                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6610                let list_name = self.chunk.intern_name("__pf_foreach_list__");
6611                self.emit_op(Op::DeclareArray(list_name), line, Some(root));
6612                let counter = self.chunk.intern_name("__pf_foreach_i__");
6613                self.emit_op(Op::LoadInt(0), line, Some(root));
6614                self.emit_op(Op::DeclareScalar(counter), line, Some(root));
6615                let underscore = self.chunk.intern_name("_");
6616
6617                let loop_start = self.chunk.len();
6618                self.emit_get_scalar(counter, line, Some(root));
6619                self.emit_op(Op::ArrayLen(list_name), line, Some(root));
6620                self.emit_op(Op::NumLt, line, Some(root));
6621                let exit_jump = self.emit_op(Op::JumpIfFalse(0), line, Some(root));
6622
6623                self.emit_get_scalar(counter, line, Some(root));
6624                self.emit_op(Op::GetArrayElem(list_name), line, Some(root));
6625                self.emit_set_scalar(underscore, line, Some(root));
6626
6627                self.compile_expr(expr)?;
6628                self.emit_op(Op::Pop, line, Some(root));
6629
6630                self.emit_pre_inc(counter, line, Some(root));
6631                self.emit_op(Op::Pop, line, Some(root));
6632                self.emit_op(Op::Jump(loop_start), line, Some(root));
6633                self.chunk.patch_jump_here(exit_jump);
6634                self.emit_op(Op::LoadUndef, line, Some(root));
6635            }
6636
6637            ExprKind::AlgebraicMatch { subject, arms } => {
6638                let idx = self
6639                    .chunk
6640                    .add_algebraic_match_entry(subject.as_ref().clone(), arms.clone());
6641                self.emit_op(Op::AlgebraicMatch(idx), line, Some(root));
6642            }
6643
6644            // ── Match (regex) ──
6645            ExprKind::Match {
6646                expr,
6647                pattern,
6648                flags,
6649                scalar_g,
6650                delim: _,
6651            } => {
6652                self.compile_expr(expr)?;
6653                let pat_idx = self.chunk.add_constant(PerlValue::string(pattern.clone()));
6654                let flags_idx = self.chunk.add_constant(PerlValue::string(flags.clone()));
6655                let pos_key_idx = if *scalar_g && flags.contains('g') {
6656                    if let ExprKind::ScalarVar(n) = &expr.kind {
6657                        let stor = self.scalar_storage_name_for_ops(n);
6658                        self.chunk.add_constant(PerlValue::string(stor))
6659                    } else {
6660                        u16::MAX
6661                    }
6662                } else {
6663                    u16::MAX
6664                };
6665                self.emit_op(
6666                    Op::RegexMatch(pat_idx, flags_idx, *scalar_g, pos_key_idx),
6667                    line,
6668                    Some(root),
6669                );
6670            }
6671
6672            ExprKind::Substitution {
6673                expr,
6674                pattern,
6675                replacement,
6676                flags,
6677                delim: _,
6678            } => {
6679                self.compile_expr(expr)?;
6680                let pat_idx = self.chunk.add_constant(PerlValue::string(pattern.clone()));
6681                let repl_idx = self
6682                    .chunk
6683                    .add_constant(PerlValue::string(replacement.clone()));
6684                let flags_idx = self.chunk.add_constant(PerlValue::string(flags.clone()));
6685                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
6686                self.emit_op(
6687                    Op::RegexSubst(pat_idx, repl_idx, flags_idx, lv_idx),
6688                    line,
6689                    Some(root),
6690                );
6691            }
6692            ExprKind::Transliterate {
6693                expr,
6694                from,
6695                to,
6696                flags,
6697                delim: _,
6698            } => {
6699                self.compile_expr(expr)?;
6700                let from_idx = self.chunk.add_constant(PerlValue::string(from.clone()));
6701                let to_idx = self.chunk.add_constant(PerlValue::string(to.clone()));
6702                let flags_idx = self.chunk.add_constant(PerlValue::string(flags.clone()));
6703                let lv_idx = self.chunk.add_lvalue_expr(expr.as_ref().clone());
6704                self.emit_op(
6705                    Op::RegexTransliterate(from_idx, to_idx, flags_idx, lv_idx),
6706                    line,
6707                    Some(root),
6708                );
6709            }
6710
6711            // ── Regex literal ──
6712            ExprKind::Regex(pattern, flags) => {
6713                if ctx == WantarrayCtx::Void {
6714                    // Statement context: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a discarded regex object.
6715                    self.compile_boolean_rvalue_condition(root)?;
6716                } else {
6717                    let pat_idx = self.chunk.add_constant(PerlValue::string(pattern.clone()));
6718                    let flags_idx = self.chunk.add_constant(PerlValue::string(flags.clone()));
6719                    self.emit_op(Op::LoadRegex(pat_idx, flags_idx), line, Some(root));
6720                }
6721            }
6722
6723            // ── Map/Grep/Sort with blocks ──
6724            ExprKind::MapExpr {
6725                block,
6726                list,
6727                flatten_array_refs,
6728                stream,
6729            } => {
6730                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6731                if *stream {
6732                    let block_idx = self.chunk.add_block(block.clone());
6733                    if *flatten_array_refs {
6734                        self.emit_op(Op::MapsFlatMapWithBlock(block_idx), line, Some(root));
6735                    } else {
6736                        self.emit_op(Op::MapsWithBlock(block_idx), line, Some(root));
6737                    }
6738                } else if let Some(k) = crate::map_grep_fast::detect_map_int_mul(block) {
6739                    self.emit_op(Op::MapIntMul(k), line, Some(root));
6740                } else {
6741                    let block_idx = self.chunk.add_block(block.clone());
6742                    if *flatten_array_refs {
6743                        self.emit_op(Op::FlatMapWithBlock(block_idx), line, Some(root));
6744                    } else {
6745                        self.emit_op(Op::MapWithBlock(block_idx), line, Some(root));
6746                    }
6747                }
6748                if ctx != WantarrayCtx::List {
6749                    self.emit_op(Op::StackArrayLen, line, Some(root));
6750                }
6751            }
6752            ExprKind::MapExprComma {
6753                expr,
6754                list,
6755                flatten_array_refs,
6756                stream,
6757            } => {
6758                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6759                let idx = self.chunk.add_map_expr_entry(*expr.clone());
6760                if *stream {
6761                    if *flatten_array_refs {
6762                        self.emit_op(Op::MapsFlatMapWithExpr(idx), line, Some(root));
6763                    } else {
6764                        self.emit_op(Op::MapsWithExpr(idx), line, Some(root));
6765                    }
6766                } else if *flatten_array_refs {
6767                    self.emit_op(Op::FlatMapWithExpr(idx), line, Some(root));
6768                } else {
6769                    self.emit_op(Op::MapWithExpr(idx), line, Some(root));
6770                }
6771                if ctx != WantarrayCtx::List {
6772                    self.emit_op(Op::StackArrayLen, line, Some(root));
6773                }
6774            }
6775            ExprKind::ForEachExpr { block, list } => {
6776                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6777                let block_idx = self.chunk.add_block(block.clone());
6778                self.emit_op(Op::ForEachWithBlock(block_idx), line, Some(root));
6779            }
6780            ExprKind::GrepExpr {
6781                block,
6782                list,
6783                keyword,
6784            } => {
6785                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6786                if keyword.is_stream() {
6787                    let block_idx = self.chunk.add_block(block.clone());
6788                    self.emit_op(Op::FilterWithBlock(block_idx), line, Some(root));
6789                } else if let Some((m, r)) = crate::map_grep_fast::detect_grep_int_mod_eq(block) {
6790                    self.emit_op(Op::GrepIntModEq(m, r), line, Some(root));
6791                } else {
6792                    let block_idx = self.chunk.add_block(block.clone());
6793                    self.emit_op(Op::GrepWithBlock(block_idx), line, Some(root));
6794                }
6795                if ctx != WantarrayCtx::List {
6796                    self.emit_op(Op::StackArrayLen, line, Some(root));
6797                }
6798            }
6799            ExprKind::GrepExprComma {
6800                expr,
6801                list,
6802                keyword,
6803            } => {
6804                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6805                let idx = self.chunk.add_grep_expr_entry(*expr.clone());
6806                if keyword.is_stream() {
6807                    self.emit_op(Op::FilterWithExpr(idx), line, Some(root));
6808                } else {
6809                    self.emit_op(Op::GrepWithExpr(idx), line, Some(root));
6810                }
6811                if ctx != WantarrayCtx::List {
6812                    self.emit_op(Op::StackArrayLen, line, Some(root));
6813                }
6814            }
6815            ExprKind::SortExpr { cmp, list } => {
6816                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6817                match cmp {
6818                    Some(crate::ast::SortComparator::Block(block)) => {
6819                        if let Some(mode) = detect_sort_block_fast(block) {
6820                            let tag = match mode {
6821                                crate::sort_fast::SortBlockFast::Numeric => 0u8,
6822                                crate::sort_fast::SortBlockFast::String => 1u8,
6823                                crate::sort_fast::SortBlockFast::NumericRev => 2u8,
6824                                crate::sort_fast::SortBlockFast::StringRev => 3u8,
6825                            };
6826                            self.emit_op(Op::SortWithBlockFast(tag), line, Some(root));
6827                        } else {
6828                            let block_idx = self.chunk.add_block(block.clone());
6829                            self.emit_op(Op::SortWithBlock(block_idx), line, Some(root));
6830                        }
6831                    }
6832                    Some(crate::ast::SortComparator::Code(code_expr)) => {
6833                        self.compile_expr(code_expr)?;
6834                        self.emit_op(Op::SortWithCodeComparator(ctx.as_byte()), line, Some(root));
6835                    }
6836                    None => {
6837                        self.emit_op(Op::SortNoBlock, line, Some(root));
6838                    }
6839                }
6840            }
6841
6842            // ── Parallel extensions ──
6843            ExprKind::PMapExpr {
6844                block,
6845                list,
6846                progress,
6847                flat_outputs,
6848                on_cluster,
6849                stream,
6850            } => {
6851                if *stream {
6852                    // Streaming: no progress flag needed, just list + block.
6853                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
6854                    let block_idx = self.chunk.add_block(block.clone());
6855                    if *flat_outputs {
6856                        self.emit_op(Op::PFlatMapsWithBlock(block_idx), line, Some(root));
6857                    } else {
6858                        self.emit_op(Op::PMapsWithBlock(block_idx), line, Some(root));
6859                    }
6860                } else {
6861                    if let Some(p) = progress {
6862                        self.compile_expr(p)?;
6863                    } else {
6864                        self.emit_op(Op::LoadInt(0), line, Some(root));
6865                    }
6866                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
6867                    if let Some(cluster_e) = on_cluster {
6868                        self.compile_expr(cluster_e)?;
6869                        let block_idx = self.chunk.add_block(block.clone());
6870                        self.emit_op(
6871                            Op::PMapRemote {
6872                                block_idx,
6873                                flat: u8::from(*flat_outputs),
6874                            },
6875                            line,
6876                            Some(root),
6877                        );
6878                    } else {
6879                        let block_idx = self.chunk.add_block(block.clone());
6880                        if *flat_outputs {
6881                            self.emit_op(Op::PFlatMapWithBlock(block_idx), line, Some(root));
6882                        } else {
6883                            self.emit_op(Op::PMapWithBlock(block_idx), line, Some(root));
6884                        }
6885                    }
6886                }
6887            }
6888            ExprKind::PMapChunkedExpr {
6889                chunk_size,
6890                block,
6891                list,
6892                progress,
6893            } => {
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(chunk_size)?;
6900                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6901                let block_idx = self.chunk.add_block(block.clone());
6902                self.emit_op(Op::PMapChunkedWithBlock(block_idx), line, Some(root));
6903            }
6904            ExprKind::PGrepExpr {
6905                block,
6906                list,
6907                progress,
6908                stream,
6909            } => {
6910                if *stream {
6911                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
6912                    let block_idx = self.chunk.add_block(block.clone());
6913                    self.emit_op(Op::PGrepsWithBlock(block_idx), line, Some(root));
6914                } else {
6915                    if let Some(p) = progress {
6916                        self.compile_expr(p)?;
6917                    } else {
6918                        self.emit_op(Op::LoadInt(0), line, Some(root));
6919                    }
6920                    self.compile_expr_ctx(list, WantarrayCtx::List)?;
6921                    let block_idx = self.chunk.add_block(block.clone());
6922                    self.emit_op(Op::PGrepWithBlock(block_idx), line, Some(root));
6923                }
6924            }
6925            ExprKind::PForExpr {
6926                block,
6927                list,
6928                progress,
6929            } => {
6930                if let Some(p) = progress {
6931                    self.compile_expr(p)?;
6932                } else {
6933                    self.emit_op(Op::LoadInt(0), line, Some(root));
6934                }
6935                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6936                let block_idx = self.chunk.add_block(block.clone());
6937                self.emit_op(Op::PForWithBlock(block_idx), line, Some(root));
6938            }
6939            ExprKind::ParLinesExpr {
6940                path,
6941                callback,
6942                progress,
6943            } => {
6944                let idx = self.chunk.add_par_lines_entry(
6945                    path.as_ref().clone(),
6946                    callback.as_ref().clone(),
6947                    progress.as_ref().map(|p| p.as_ref().clone()),
6948                );
6949                self.emit_op(Op::ParLines(idx), line, Some(root));
6950            }
6951            ExprKind::ParWalkExpr {
6952                path,
6953                callback,
6954                progress,
6955            } => {
6956                let idx = self.chunk.add_par_walk_entry(
6957                    path.as_ref().clone(),
6958                    callback.as_ref().clone(),
6959                    progress.as_ref().map(|p| p.as_ref().clone()),
6960                );
6961                self.emit_op(Op::ParWalk(idx), line, Some(root));
6962            }
6963            ExprKind::PwatchExpr { path, callback } => {
6964                let idx = self
6965                    .chunk
6966                    .add_pwatch_entry(path.as_ref().clone(), callback.as_ref().clone());
6967                self.emit_op(Op::Pwatch(idx), line, Some(root));
6968            }
6969            ExprKind::PSortExpr {
6970                cmp,
6971                list,
6972                progress,
6973            } => {
6974                if let Some(p) = progress {
6975                    self.compile_expr(p)?;
6976                } else {
6977                    self.emit_op(Op::LoadInt(0), line, Some(root));
6978                }
6979                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6980                if let Some(block) = cmp {
6981                    if let Some(mode) = detect_sort_block_fast(block) {
6982                        let tag = match mode {
6983                            crate::sort_fast::SortBlockFast::Numeric => 0u8,
6984                            crate::sort_fast::SortBlockFast::String => 1u8,
6985                            crate::sort_fast::SortBlockFast::NumericRev => 2u8,
6986                            crate::sort_fast::SortBlockFast::StringRev => 3u8,
6987                        };
6988                        self.emit_op(Op::PSortWithBlockFast(tag), line, Some(root));
6989                    } else {
6990                        let block_idx = self.chunk.add_block(block.clone());
6991                        self.emit_op(Op::PSortWithBlock(block_idx), line, Some(root));
6992                    }
6993                } else {
6994                    self.emit_op(Op::PSortNoBlockParallel, line, Some(root));
6995                }
6996            }
6997            ExprKind::ReduceExpr { block, list } => {
6998                self.compile_expr_ctx(list, WantarrayCtx::List)?;
6999                let block_idx = self.chunk.add_block(block.clone());
7000                self.emit_op(Op::ReduceWithBlock(block_idx), line, Some(root));
7001            }
7002            ExprKind::PReduceExpr {
7003                block,
7004                list,
7005                progress,
7006            } => {
7007                if let Some(p) = progress {
7008                    self.compile_expr(p)?;
7009                } else {
7010                    self.emit_op(Op::LoadInt(0), line, Some(root));
7011                }
7012                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7013                let block_idx = self.chunk.add_block(block.clone());
7014                self.emit_op(Op::PReduceWithBlock(block_idx), line, Some(root));
7015            }
7016            ExprKind::PReduceInitExpr {
7017                init,
7018                block,
7019                list,
7020                progress,
7021            } => {
7022                if let Some(p) = progress {
7023                    self.compile_expr(p)?;
7024                } else {
7025                    self.emit_op(Op::LoadInt(0), line, Some(root));
7026                }
7027                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7028                self.compile_expr(init)?;
7029                let block_idx = self.chunk.add_block(block.clone());
7030                self.emit_op(Op::PReduceInitWithBlock(block_idx), line, Some(root));
7031            }
7032            ExprKind::PMapReduceExpr {
7033                map_block,
7034                reduce_block,
7035                list,
7036                progress,
7037            } => {
7038                if let Some(p) = progress {
7039                    self.compile_expr(p)?;
7040                } else {
7041                    self.emit_op(Op::LoadInt(0), line, Some(root));
7042                }
7043                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7044                let map_idx = self.chunk.add_block(map_block.clone());
7045                let reduce_idx = self.chunk.add_block(reduce_block.clone());
7046                self.emit_op(
7047                    Op::PMapReduceWithBlocks(map_idx, reduce_idx),
7048                    line,
7049                    Some(root),
7050                );
7051            }
7052            ExprKind::PcacheExpr {
7053                block,
7054                list,
7055                progress,
7056            } => {
7057                if let Some(p) = progress {
7058                    self.compile_expr(p)?;
7059                } else {
7060                    self.emit_op(Op::LoadInt(0), line, Some(root));
7061                }
7062                self.compile_expr_ctx(list, WantarrayCtx::List)?;
7063                let block_idx = self.chunk.add_block(block.clone());
7064                self.emit_op(Op::PcacheWithBlock(block_idx), line, Some(root));
7065            }
7066            ExprKind::PselectExpr { receivers, timeout } => {
7067                let n = receivers.len();
7068                if n > u8::MAX as usize {
7069                    return Err(CompileError::Unsupported(
7070                        "pselect: too many receivers".into(),
7071                    ));
7072                }
7073                for r in receivers {
7074                    self.compile_expr(r)?;
7075                }
7076                let has_timeout = timeout.is_some();
7077                if let Some(t) = timeout {
7078                    self.compile_expr(t)?;
7079                }
7080                self.emit_op(
7081                    Op::Pselect {
7082                        n_rx: n as u8,
7083                        has_timeout,
7084                    },
7085                    line,
7086                    Some(root),
7087                );
7088            }
7089            ExprKind::FanExpr {
7090                count,
7091                block,
7092                progress,
7093                capture,
7094            } => {
7095                if let Some(p) = progress {
7096                    self.compile_expr(p)?;
7097                } else {
7098                    self.emit_op(Op::LoadInt(0), line, Some(root));
7099                }
7100                let block_idx = self.chunk.add_block(block.clone());
7101                match (count, capture) {
7102                    (Some(c), false) => {
7103                        self.compile_expr(c)?;
7104                        self.emit_op(Op::FanWithBlock(block_idx), line, Some(root));
7105                    }
7106                    (None, false) => {
7107                        self.emit_op(Op::FanWithBlockAuto(block_idx), line, Some(root));
7108                    }
7109                    (Some(c), true) => {
7110                        self.compile_expr(c)?;
7111                        self.emit_op(Op::FanCapWithBlock(block_idx), line, Some(root));
7112                    }
7113                    (None, true) => {
7114                        self.emit_op(Op::FanCapWithBlockAuto(block_idx), line, Some(root));
7115                    }
7116                }
7117            }
7118            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
7119                let block_idx = self.chunk.add_block(body.clone());
7120                self.emit_op(Op::AsyncBlock(block_idx), line, Some(root));
7121            }
7122            ExprKind::Trace { body } => {
7123                let block_idx = self.chunk.add_block(body.clone());
7124                self.emit_op(Op::TraceBlock(block_idx), line, Some(root));
7125            }
7126            ExprKind::Timer { body } => {
7127                let block_idx = self.chunk.add_block(body.clone());
7128                self.emit_op(Op::TimerBlock(block_idx), line, Some(root));
7129            }
7130            ExprKind::Bench { body, times } => {
7131                self.compile_expr(times)?;
7132                let block_idx = self.chunk.add_block(body.clone());
7133                self.emit_op(Op::BenchBlock(block_idx), line, Some(root));
7134            }
7135            ExprKind::Await(e) => {
7136                self.compile_expr(e)?;
7137                self.emit_op(Op::Await, line, Some(root));
7138            }
7139            ExprKind::Slurp(e) => {
7140                self.compile_expr(e)?;
7141                self.emit_op(
7142                    Op::CallBuiltin(BuiltinId::Slurp as u16, 1),
7143                    line,
7144                    Some(root),
7145                );
7146            }
7147            ExprKind::Capture(e) => {
7148                self.compile_expr(e)?;
7149                self.emit_op(
7150                    Op::CallBuiltin(BuiltinId::Capture as u16, 1),
7151                    line,
7152                    Some(root),
7153                );
7154            }
7155            ExprKind::Qx(e) => {
7156                self.compile_expr(e)?;
7157                self.emit_op(
7158                    Op::CallBuiltin(BuiltinId::Readpipe as u16, 1),
7159                    line,
7160                    Some(root),
7161                );
7162            }
7163            ExprKind::FetchUrl(e) => {
7164                self.compile_expr(e)?;
7165                self.emit_op(
7166                    Op::CallBuiltin(BuiltinId::FetchUrl as u16, 1),
7167                    line,
7168                    Some(root),
7169                );
7170            }
7171            ExprKind::Pchannel { capacity } => {
7172                if let Some(c) = capacity {
7173                    self.compile_expr(c)?;
7174                    self.emit_op(
7175                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 1),
7176                        line,
7177                        Some(root),
7178                    );
7179                } else {
7180                    self.emit_op(
7181                        Op::CallBuiltin(BuiltinId::Pchannel as u16, 0),
7182                        line,
7183                        Some(root),
7184                    );
7185                }
7186            }
7187            ExprKind::RetryBlock { .. }
7188            | ExprKind::RateLimitBlock { .. }
7189            | ExprKind::EveryBlock { .. }
7190            | ExprKind::GenBlock { .. }
7191            | ExprKind::Yield(_)
7192            | ExprKind::Spinner { .. } => {
7193                return Err(CompileError::Unsupported(
7194                    "retry/rate_limit/every/gen/yield (tree interpreter only)".into(),
7195                ));
7196            }
7197            ExprKind::MyExpr { .. } => {
7198                // `my $x = …` used as an expression (e.g. `if (my $x = …)`).
7199                // Tree interpreter handles via `Interpreter::exec_statement`.
7200                return Err(CompileError::Unsupported(
7201                    "my/our/state/local in expression context (tree interpreter only)".into(),
7202                ));
7203            }
7204        }
7205        Ok(())
7206    }
7207
7208    fn compile_string_part(
7209        &mut self,
7210        part: &StringPart,
7211        line: usize,
7212        parent: Option<&Expr>,
7213    ) -> Result<(), CompileError> {
7214        match part {
7215            StringPart::Literal(s) => {
7216                let idx = self.chunk.add_constant(PerlValue::string(s.clone()));
7217                self.emit_op(Op::LoadConst(idx), line, parent);
7218            }
7219            StringPart::ScalarVar(name) => {
7220                let idx = self.intern_scalar_var_for_ops(name);
7221                self.emit_get_scalar(idx, line, parent);
7222            }
7223            StringPart::ArrayVar(name) => {
7224                let idx = self.chunk.intern_name(&self.qualify_stash_array_name(name));
7225                self.emit_op(Op::GetArray(idx), line, parent);
7226                self.emit_op(Op::ArrayStringifyListSep, line, parent);
7227            }
7228            StringPart::Expr(e) => {
7229                // Interpolation uses list/array values (`$"`), not Perl scalar(@arr) length.
7230                if matches!(&e.kind, ExprKind::ArraySlice { .. })
7231                    || matches!(
7232                        &e.kind,
7233                        ExprKind::Deref {
7234                            kind: Sigil::Array,
7235                            ..
7236                        }
7237                    )
7238                {
7239                    self.compile_expr_ctx(e, WantarrayCtx::List)?;
7240                    self.emit_op(Op::ArrayStringifyListSep, line, parent);
7241                } else {
7242                    self.compile_expr(e)?;
7243                }
7244            }
7245        }
7246        Ok(())
7247    }
7248
7249    fn compile_assign(
7250        &mut self,
7251        target: &Expr,
7252        line: usize,
7253        keep: bool,
7254        ast: Option<&Expr>,
7255    ) -> Result<(), CompileError> {
7256        match &target.kind {
7257            ExprKind::ScalarVar(name) => {
7258                self.check_strict_scalar_access(name, line)?;
7259                self.check_scalar_mutable(name, line)?;
7260                let idx = self.intern_scalar_var_for_ops(name);
7261                if keep {
7262                    self.emit_set_scalar_keep(idx, line, ast);
7263                } else {
7264                    self.emit_set_scalar(idx, line, ast);
7265                }
7266            }
7267            ExprKind::ArrayVar(name) => {
7268                self.check_strict_array_access(name, line)?;
7269                let q = self.qualify_stash_array_name(name);
7270                self.check_array_mutable(&q, line)?;
7271                let idx = self.chunk.intern_name(&q);
7272                self.emit_op(Op::SetArray(idx), line, ast);
7273                if keep {
7274                    self.emit_op(Op::GetArray(idx), line, ast);
7275                }
7276            }
7277            ExprKind::HashVar(name) => {
7278                self.check_strict_hash_access(name, line)?;
7279                self.check_hash_mutable(name, line)?;
7280                let idx = self.chunk.intern_name(name);
7281                self.emit_op(Op::SetHash(idx), line, ast);
7282                if keep {
7283                    self.emit_op(Op::GetHash(idx), line, ast);
7284                }
7285            }
7286            ExprKind::ArrayElement { array, index } => {
7287                self.check_strict_array_access(array, line)?;
7288                let q = self.qualify_stash_array_name(array);
7289                self.check_array_mutable(&q, line)?;
7290                let idx = self.chunk.intern_name(&q);
7291                self.compile_expr(index)?;
7292                self.emit_op(Op::SetArrayElem(idx), line, ast);
7293            }
7294            ExprKind::ArraySlice { array, indices } => {
7295                if indices.is_empty() {
7296                    if self.is_mysync_array(array) {
7297                        return Err(CompileError::Unsupported(
7298                            "mysync array slice assign (tree interpreter)".into(),
7299                        ));
7300                    }
7301                    self.check_strict_array_access(array, line)?;
7302                    let q = self.qualify_stash_array_name(array);
7303                    self.check_array_mutable(&q, line)?;
7304                    let arr_idx = self.chunk.intern_name(&q);
7305                    self.emit_op(Op::SetNamedArraySlice(arr_idx, 0), line, ast);
7306                    if keep {
7307                        self.emit_op(Op::MakeArray(0), line, ast);
7308                    }
7309                    return Ok(());
7310                }
7311                if self.is_mysync_array(array) {
7312                    return Err(CompileError::Unsupported(
7313                        "mysync array slice assign (tree interpreter)".into(),
7314                    ));
7315                }
7316                self.check_strict_array_access(array, line)?;
7317                let q = self.qualify_stash_array_name(array);
7318                self.check_array_mutable(&q, line)?;
7319                let arr_idx = self.chunk.intern_name(&q);
7320                for ix in indices {
7321                    self.compile_array_slice_index_expr(ix)?;
7322                }
7323                self.emit_op(
7324                    Op::SetNamedArraySlice(arr_idx, indices.len() as u16),
7325                    line,
7326                    ast,
7327                );
7328                if keep {
7329                    for (ix, index_expr) in indices.iter().enumerate() {
7330                        self.compile_array_slice_index_expr(index_expr)?;
7331                        self.emit_op(Op::ArraySlicePart(arr_idx), line, ast);
7332                        if ix > 0 {
7333                            self.emit_op(Op::ArrayConcatTwo, line, ast);
7334                        }
7335                    }
7336                }
7337                return Ok(());
7338            }
7339            ExprKind::HashElement { hash, key } => {
7340                self.check_strict_hash_access(hash, line)?;
7341                self.check_hash_mutable(hash, line)?;
7342                let idx = self.chunk.intern_name(hash);
7343                self.compile_expr(key)?;
7344                if keep {
7345                    self.emit_op(Op::SetHashElemKeep(idx), line, ast);
7346                } else {
7347                    self.emit_op(Op::SetHashElem(idx), line, ast);
7348                }
7349            }
7350            ExprKind::HashSlice { hash, keys } => {
7351                if keys.is_empty() {
7352                    if self.is_mysync_hash(hash) {
7353                        return Err(CompileError::Unsupported(
7354                            "mysync hash slice assign (tree interpreter)".into(),
7355                        ));
7356                    }
7357                    self.check_strict_hash_access(hash, line)?;
7358                    self.check_hash_mutable(hash, line)?;
7359                    let hash_idx = self.chunk.intern_name(hash);
7360                    self.emit_op(Op::SetHashSlice(hash_idx, 0), line, ast);
7361                    if keep {
7362                        self.emit_op(Op::MakeArray(0), line, ast);
7363                    }
7364                    return Ok(());
7365                }
7366                if self.is_mysync_hash(hash) {
7367                    return Err(CompileError::Unsupported(
7368                        "mysync hash slice assign (tree interpreter)".into(),
7369                    ));
7370                }
7371                self.check_strict_hash_access(hash, line)?;
7372                self.check_hash_mutable(hash, line)?;
7373                let hash_idx = self.chunk.intern_name(hash);
7374                // Multi-key entries (`'a'..'c'`, `qw/a b/`, list literals) push an array value;
7375                // [`Self::assign_named_hash_slice`] / [`crate::bytecode::Op::SetHashSlice`]
7376                // flattens it at runtime, so compile in list context (scalar context collapses
7377                // `..` to a flip-flop).
7378                for key_expr in keys {
7379                    self.compile_hash_slice_key_expr(key_expr)?;
7380                }
7381                self.emit_op(Op::SetHashSlice(hash_idx, keys.len() as u16), line, ast);
7382                if keep {
7383                    for key_expr in keys {
7384                        self.compile_expr(key_expr)?;
7385                        self.emit_op(Op::GetHashElem(hash_idx), line, ast);
7386                    }
7387                    self.emit_op(Op::MakeArray(keys.len() as u16), line, ast);
7388                }
7389                return Ok(());
7390            }
7391            ExprKind::Deref {
7392                expr,
7393                kind: Sigil::Scalar,
7394            } => {
7395                self.compile_expr(expr)?;
7396                if keep {
7397                    self.emit_op(Op::SetSymbolicScalarRefKeep, line, ast);
7398                } else {
7399                    self.emit_op(Op::SetSymbolicScalarRef, line, ast);
7400                }
7401            }
7402            ExprKind::Deref {
7403                expr,
7404                kind: Sigil::Array,
7405            } => {
7406                self.compile_expr(expr)?;
7407                self.emit_op(Op::SetSymbolicArrayRef, line, ast);
7408            }
7409            ExprKind::Deref {
7410                expr,
7411                kind: Sigil::Hash,
7412            } => {
7413                self.compile_expr(expr)?;
7414                self.emit_op(Op::SetSymbolicHashRef, line, ast);
7415            }
7416            ExprKind::Deref {
7417                expr,
7418                kind: Sigil::Typeglob,
7419            } => {
7420                self.compile_expr(expr)?;
7421                self.emit_op(Op::SetSymbolicTypeglobRef, line, ast);
7422            }
7423            ExprKind::Typeglob(name) => {
7424                let idx = self.chunk.intern_name(name);
7425                if keep {
7426                    self.emit_op(Op::TypeglobAssignFromValue(idx), line, ast);
7427                } else {
7428                    return Err(CompileError::Unsupported(
7429                        "typeglob assign without keep (internal)".into(),
7430                    ));
7431                }
7432            }
7433            ExprKind::AnonymousListSlice { source, indices } => {
7434                if let ExprKind::Deref {
7435                    expr: inner,
7436                    kind: Sigil::Array,
7437                } = &source.kind
7438                {
7439                    if indices.is_empty() {
7440                        return Err(CompileError::Unsupported(
7441                            "assign to empty list slice (internal)".into(),
7442                        ));
7443                    }
7444                    self.compile_arrow_array_base_expr(inner)?;
7445                    for ix in indices {
7446                        self.compile_array_slice_index_expr(ix)?;
7447                    }
7448                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
7449                    if keep {
7450                        self.compile_arrow_array_base_expr(inner)?;
7451                        for ix in indices {
7452                            self.compile_array_slice_index_expr(ix)?;
7453                        }
7454                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
7455                    }
7456                    return Ok(());
7457                }
7458                return Err(CompileError::Unsupported(
7459                    "assign to anonymous list slice (non-@array-deref base)".into(),
7460                ));
7461            }
7462            ExprKind::ArrowDeref {
7463                expr,
7464                index,
7465                kind: DerefKind::Hash,
7466            } => {
7467                self.compile_arrow_hash_base_expr(expr)?;
7468                self.compile_expr(index)?;
7469                if keep {
7470                    self.emit_op(Op::SetArrowHashKeep, line, ast);
7471                } else {
7472                    self.emit_op(Op::SetArrowHash, line, ast);
7473                }
7474            }
7475            ExprKind::ArrowDeref {
7476                expr,
7477                index,
7478                kind: DerefKind::Array,
7479            } => {
7480                if let ExprKind::List(indices) = &index.kind {
7481                    // Multi-index slice assignment: RHS value is already on the stack (pushed
7482                    // by the enclosing `compile_expr(value)` before `compile_assign` was called
7483                    // with keep = true). `SetArrowArraySlice` delegates to
7484                    // `Interpreter::assign_arrow_array_slice` for element-wise write.
7485                    self.compile_arrow_array_base_expr(expr)?;
7486                    for ix in indices {
7487                        self.compile_array_slice_index_expr(ix)?;
7488                    }
7489                    self.emit_op(Op::SetArrowArraySlice(indices.len() as u16), line, ast);
7490                    if keep {
7491                        // The Set op pops the value; keep callers re-read via a fresh slice read.
7492                        self.compile_arrow_array_base_expr(expr)?;
7493                        for ix in indices {
7494                            self.compile_array_slice_index_expr(ix)?;
7495                        }
7496                        self.emit_op(Op::ArrowArraySlice(indices.len() as u16), line, ast);
7497                    }
7498                    return Ok(());
7499                }
7500                if arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
7501                    self.compile_arrow_array_base_expr(expr)?;
7502                    self.compile_expr(index)?;
7503                    if keep {
7504                        self.emit_op(Op::SetArrowArrayKeep, line, ast);
7505                    } else {
7506                        self.emit_op(Op::SetArrowArray, line, ast);
7507                    }
7508                } else {
7509                    self.compile_arrow_array_base_expr(expr)?;
7510                    self.compile_array_slice_index_expr(index)?;
7511                    self.emit_op(Op::SetArrowArraySlice(1), line, ast);
7512                    if keep {
7513                        self.compile_arrow_array_base_expr(expr)?;
7514                        self.compile_array_slice_index_expr(index)?;
7515                        self.emit_op(Op::ArrowArraySlice(1), line, ast);
7516                    }
7517                }
7518            }
7519            ExprKind::ArrowDeref {
7520                kind: DerefKind::Call,
7521                ..
7522            } => {
7523                return Err(CompileError::Unsupported(
7524                    "Assign to arrow call deref (tree interpreter)".into(),
7525                ));
7526            }
7527            ExprKind::HashSliceDeref { container, keys } => {
7528                self.compile_expr(container)?;
7529                for key_expr in keys {
7530                    self.compile_hash_slice_key_expr(key_expr)?;
7531                }
7532                self.emit_op(Op::SetHashSliceDeref(keys.len() as u16), line, ast);
7533            }
7534            ExprKind::Pos(inner) => {
7535                let Some(inner_e) = inner.as_ref() else {
7536                    return Err(CompileError::Unsupported(
7537                        "assign to pos() without scalar".into(),
7538                    ));
7539                };
7540                if keep {
7541                    self.emit_op(Op::Dup, line, ast);
7542                }
7543                match &inner_e.kind {
7544                    ExprKind::ScalarVar(name) => {
7545                        let stor = self.scalar_storage_name_for_ops(name);
7546                        let idx = self.chunk.add_constant(PerlValue::string(stor));
7547                        self.emit_op(Op::LoadConst(idx), line, ast);
7548                    }
7549                    _ => {
7550                        self.compile_expr(inner_e)?;
7551                    }
7552                }
7553                self.emit_op(Op::SetRegexPos, line, ast);
7554            }
7555            // List assignment: `($a, $b) = (val1, val2)` — RHS is on stack as array,
7556            // store into temp, then distribute elements to each target.
7557            ExprKind::List(targets) => {
7558                let tmp = self.chunk.intern_name("__list_assign_swap__");
7559                self.emit_op(Op::DeclareArray(tmp), line, ast);
7560                for (i, t) in targets.iter().enumerate() {
7561                    self.emit_op(Op::LoadInt(i as i64), line, ast);
7562                    self.emit_op(Op::GetArrayElem(tmp), line, ast);
7563                    self.compile_assign(t, line, false, ast)?;
7564                }
7565                if keep {
7566                    self.emit_op(Op::GetArray(tmp), line, ast);
7567                }
7568            }
7569            _ => {
7570                return Err(CompileError::Unsupported("Assign to complex lvalue".into()));
7571            }
7572        }
7573        Ok(())
7574    }
7575}
7576
7577/// Map a binary op to its stack opcode for compound assignment on aggregates (`$a[$i]`, `$h{$k}`).
7578pub(crate) fn binop_to_vm_op(op: BinOp) -> Option<Op> {
7579    Some(match op {
7580        BinOp::Add => Op::Add,
7581        BinOp::Sub => Op::Sub,
7582        BinOp::Mul => Op::Mul,
7583        BinOp::Div => Op::Div,
7584        BinOp::Mod => Op::Mod,
7585        BinOp::Pow => Op::Pow,
7586        BinOp::Concat => Op::Concat,
7587        BinOp::BitAnd => Op::BitAnd,
7588        BinOp::BitOr => Op::BitOr,
7589        BinOp::BitXor => Op::BitXor,
7590        BinOp::ShiftLeft => Op::Shl,
7591        BinOp::ShiftRight => Op::Shr,
7592        _ => return None,
7593    })
7594}
7595
7596/// Encode/decode scalar compound ops for [`Op::ScalarCompoundAssign`].
7597pub(crate) fn scalar_compound_op_to_byte(op: BinOp) -> Option<u8> {
7598    Some(match op {
7599        BinOp::Add => 0,
7600        BinOp::Sub => 1,
7601        BinOp::Mul => 2,
7602        BinOp::Div => 3,
7603        BinOp::Mod => 4,
7604        BinOp::Pow => 5,
7605        BinOp::Concat => 6,
7606        BinOp::BitAnd => 7,
7607        BinOp::BitOr => 8,
7608        BinOp::BitXor => 9,
7609        BinOp::ShiftLeft => 10,
7610        BinOp::ShiftRight => 11,
7611        _ => return None,
7612    })
7613}
7614
7615pub(crate) fn scalar_compound_op_from_byte(b: u8) -> Option<BinOp> {
7616    Some(match b {
7617        0 => BinOp::Add,
7618        1 => BinOp::Sub,
7619        2 => BinOp::Mul,
7620        3 => BinOp::Div,
7621        4 => BinOp::Mod,
7622        5 => BinOp::Pow,
7623        6 => BinOp::Concat,
7624        7 => BinOp::BitAnd,
7625        8 => BinOp::BitOr,
7626        9 => BinOp::BitXor,
7627        10 => BinOp::ShiftLeft,
7628        11 => BinOp::ShiftRight,
7629        _ => return None,
7630    })
7631}
7632
7633#[cfg(test)]
7634mod tests {
7635    use super::*;
7636    use crate::bytecode::{BuiltinId, Op, GP_RUN};
7637    use crate::parse;
7638
7639    fn compile_snippet(code: &str) -> Result<Chunk, CompileError> {
7640        let program = parse(code).expect("parse snippet");
7641        Compiler::new().compile_program(&program)
7642    }
7643
7644    fn assert_last_halt(chunk: &Chunk) {
7645        assert!(
7646            matches!(chunk.ops.last(), Some(Op::Halt)),
7647            "expected Halt last, got {:?}",
7648            chunk.ops.last()
7649        );
7650    }
7651
7652    #[test]
7653    fn compile_empty_program_emits_run_phase_then_halt() {
7654        let chunk = compile_snippet("").expect("compile");
7655        assert_eq!(chunk.ops.len(), 2);
7656        assert!(matches!(chunk.ops[0], Op::SetGlobalPhase(p) if p == GP_RUN));
7657        assert!(matches!(chunk.ops[1], Op::Halt));
7658    }
7659
7660    #[test]
7661    fn compile_integer_literal_statement() {
7662        let chunk = compile_snippet("42;").expect("compile");
7663        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadInt(42))));
7664        assert_last_halt(&chunk);
7665    }
7666
7667    #[test]
7668    fn compile_pos_assign_emits_set_regex_pos() {
7669        let chunk = compile_snippet(r#"$_ = ""; pos = 3;"#).expect("compile");
7670        assert!(
7671            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
7672            "expected SetRegexPos in {:?}",
7673            chunk.ops
7674        );
7675    }
7676
7677    #[test]
7678    fn compile_pos_deref_scalar_assign_emits_set_regex_pos() {
7679        let chunk = compile_snippet(
7680            r#"no strict 'vars';
7681            my $s;
7682            my $r = \$s;
7683            pos $$r = 0;"#,
7684        )
7685        .expect("compile");
7686        assert!(
7687            chunk.ops.iter().any(|o| matches!(o, Op::SetRegexPos)),
7688            r"expected SetRegexPos for pos $$r =, got {:?}",
7689            chunk.ops
7690        );
7691    }
7692
7693    #[test]
7694    fn compile_map_expr_comma_emits_map_with_expr() {
7695        let chunk = compile_snippet(
7696            r#"no strict 'vars';
7697            (map $_ + 1, (4, 5)) |> join ','"#,
7698        )
7699        .expect("compile");
7700        assert!(
7701            chunk.ops.iter().any(|o| matches!(o, Op::MapWithExpr(_))),
7702            "expected MapWithExpr, got {:?}",
7703            chunk.ops
7704        );
7705    }
7706
7707    #[test]
7708    fn compile_hash_slice_deref_assign_emits_set_op() {
7709        let code = r#"no strict 'vars';
7710        my $h = { "a" => 1, "b" => 2 };
7711        my $r = $h;
7712        @$r{"a", "b"} = (10, 20);
7713        $r->{"a"} . "," . $r->{"b"};"#;
7714        let chunk = compile_snippet(code).expect("compile");
7715        assert!(
7716            chunk
7717                .ops
7718                .iter()
7719                .any(|o| matches!(o, Op::SetHashSliceDeref(n) if *n == 2)),
7720            "expected SetHashSliceDeref(2), got {:?}",
7721            chunk.ops
7722        );
7723    }
7724
7725    #[test]
7726    fn compile_bare_array_assign_diamond_uses_readline_list() {
7727        let chunk = compile_snippet("@a = <>;").expect("compile");
7728        assert!(
7729            chunk.ops.iter().any(|o| matches!(
7730                o,
7731                Op::CallBuiltin(bid, 0) if *bid == BuiltinId::ReadLineList as u16
7732            )),
7733            "expected ReadLineList for bare @a = <>, got {:?}",
7734            chunk.ops
7735        );
7736    }
7737
7738    #[test]
7739    fn compile_float_literal() {
7740        let chunk = compile_snippet("3.25;").expect("compile");
7741        assert!(chunk
7742            .ops
7743            .iter()
7744            .any(|o| matches!(o, Op::LoadFloat(f) if (*f - 3.25).abs() < 1e-9)));
7745        assert_last_halt(&chunk);
7746    }
7747
7748    #[test]
7749    fn compile_addition() {
7750        let chunk = compile_snippet("1 + 2;").expect("compile");
7751        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Add)));
7752        assert_last_halt(&chunk);
7753    }
7754
7755    #[test]
7756    fn compile_sub_mul_div_mod_pow() {
7757        for (src, op) in [
7758            ("10 - 3;", "Sub"),
7759            ("6 * 7;", "Mul"),
7760            ("8 / 2;", "Div"),
7761            ("9 % 4;", "Mod"),
7762            ("2 ** 8;", "Pow"),
7763        ] {
7764            let chunk = compile_snippet(src).expect(src);
7765            assert!(
7766                chunk.ops.iter().any(|o| std::mem::discriminant(o) == {
7767                    let dummy = match op {
7768                        "Sub" => Op::Sub,
7769                        "Mul" => Op::Mul,
7770                        "Div" => Op::Div,
7771                        "Mod" => Op::Mod,
7772                        "Pow" => Op::Pow,
7773                        _ => unreachable!(),
7774                    };
7775                    std::mem::discriminant(&dummy)
7776                }),
7777                "{} missing {:?}",
7778                src,
7779                op
7780            );
7781            assert_last_halt(&chunk);
7782        }
7783    }
7784
7785    #[test]
7786    fn compile_string_literal_uses_constant_pool() {
7787        let chunk = compile_snippet(r#""hello";"#).expect("compile");
7788        assert!(chunk
7789            .constants
7790            .iter()
7791            .any(|c| c.as_str().as_deref() == Some("hello")));
7792        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadConst(_))));
7793        assert_last_halt(&chunk);
7794    }
7795
7796    #[test]
7797    fn compile_substitution_bind_emits_regex_subst() {
7798        let chunk = compile_snippet(r#"my $s = "aa"; $s =~ s/a/b/g;"#).expect("compile");
7799        assert!(
7800            chunk
7801                .ops
7802                .iter()
7803                .any(|o| matches!(o, Op::RegexSubst(_, _, _, _))),
7804            "expected RegexSubst in {:?}",
7805            chunk.ops
7806        );
7807        assert!(!chunk.lvalues.is_empty());
7808    }
7809
7810    #[test]
7811    fn compile_chomp_emits_chomp_in_place() {
7812        let chunk = compile_snippet(r#"my $s = "x\n"; chomp $s;"#).expect("compile");
7813        assert!(
7814            chunk.ops.iter().any(|o| matches!(o, Op::ChompInPlace(_))),
7815            "expected ChompInPlace, got {:?}",
7816            chunk.ops
7817        );
7818    }
7819
7820    #[test]
7821    fn compile_transliterate_bind_emits_regex_transliterate() {
7822        let chunk = compile_snippet(r#"my $u = "abc"; $u =~ tr/a-z/A-Z/;"#).expect("compile");
7823        assert!(
7824            chunk
7825                .ops
7826                .iter()
7827                .any(|o| matches!(o, Op::RegexTransliterate(_, _, _, _))),
7828            "expected RegexTransliterate in {:?}",
7829            chunk.ops
7830        );
7831        assert!(!chunk.lvalues.is_empty());
7832    }
7833
7834    #[test]
7835    fn compile_negation() {
7836        let chunk = compile_snippet("-7;").expect("compile");
7837        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Negate)));
7838        assert_last_halt(&chunk);
7839    }
7840
7841    #[test]
7842    fn compile_my_scalar_declares() {
7843        let chunk = compile_snippet("my $x = 1;").expect("compile");
7844        assert!(chunk
7845            .ops
7846            .iter()
7847            .any(|o| matches!(o, Op::DeclareScalar(_) | Op::DeclareScalarSlot(_, _))));
7848        assert_last_halt(&chunk);
7849    }
7850
7851    #[test]
7852    fn compile_scalar_fetch_and_assign() {
7853        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
7854        assert!(
7855            chunk
7856                .ops
7857                .iter()
7858                .filter(|o| matches!(
7859                    o,
7860                    Op::GetScalar(_) | Op::GetScalarPlain(_) | Op::GetScalarSlot(_)
7861                ))
7862                .count()
7863                >= 1
7864        );
7865        assert_last_halt(&chunk);
7866    }
7867
7868    #[test]
7869    fn compile_plain_scalar_read_emits_get_scalar_plain() {
7870        let chunk = compile_snippet("my $a = 1; $a + 0;").expect("compile");
7871        assert!(
7872            chunk
7873                .ops
7874                .iter()
7875                .any(|o| matches!(o, Op::GetScalarPlain(_) | Op::GetScalarSlot(_))),
7876            "expected GetScalarPlain or GetScalarSlot for non-special $a, ops={:?}",
7877            chunk.ops
7878        );
7879    }
7880
7881    #[test]
7882    fn compile_sub_postfix_inc_emits_post_inc_slot() {
7883        let chunk = compile_snippet("fn foo { my $x = 0; $x++; return $x; }").expect("compile");
7884        assert!(
7885            chunk.ops.iter().any(|o| matches!(o, Op::PostIncSlot(_))),
7886            "expected PostIncSlot in compiled sub body, ops={:?}",
7887            chunk.ops
7888        );
7889    }
7890
7891    #[test]
7892    fn compile_comparison_ops_numeric() {
7893        for src in [
7894            "1 < 2;", "1 > 2;", "1 <= 2;", "1 >= 2;", "1 == 2;", "1 != 2;",
7895        ] {
7896            let chunk = compile_snippet(src).expect(src);
7897            assert!(
7898                chunk.ops.iter().any(|o| {
7899                    matches!(
7900                        o,
7901                        Op::NumLt | Op::NumGt | Op::NumLe | Op::NumGe | Op::NumEq | Op::NumNe
7902                    )
7903                }),
7904                "{}",
7905                src
7906            );
7907            assert_last_halt(&chunk);
7908        }
7909    }
7910
7911    #[test]
7912    fn compile_string_compare_ops() {
7913        for src in [
7914            r#"'a' lt 'b';"#,
7915            r#"'a' gt 'b';"#,
7916            r#"'a' le 'b';"#,
7917            r#"'a' ge 'b';"#,
7918        ] {
7919            let chunk = compile_snippet(src).expect(src);
7920            assert!(
7921                chunk
7922                    .ops
7923                    .iter()
7924                    .any(|o| matches!(o, Op::StrLt | Op::StrGt | Op::StrLe | Op::StrGe)),
7925                "{}",
7926                src
7927            );
7928            assert_last_halt(&chunk);
7929        }
7930    }
7931
7932    #[test]
7933    fn compile_concat() {
7934        let chunk = compile_snippet(r#"'a' . 'b';"#).expect("compile");
7935        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Concat)));
7936        assert_last_halt(&chunk);
7937    }
7938
7939    #[test]
7940    fn compile_bitwise_ops() {
7941        let chunk = compile_snippet("1 & 2 | 3 ^ 4;").expect("compile");
7942        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitAnd)));
7943        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitOr)));
7944        assert!(chunk.ops.iter().any(|o| matches!(o, Op::BitXor)));
7945        assert_last_halt(&chunk);
7946    }
7947
7948    #[test]
7949    fn compile_shift_right() {
7950        let chunk = compile_snippet("8 >> 1;").expect("compile");
7951        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shr)));
7952        assert_last_halt(&chunk);
7953    }
7954
7955    #[test]
7956    fn compile_shift_left() {
7957        let chunk = compile_snippet("1 << 4;").expect("compile");
7958        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Shl)));
7959        assert_last_halt(&chunk);
7960    }
7961
7962    #[test]
7963    fn compile_log_not_and_bit_not() {
7964        let c1 = compile_snippet("!0;").expect("compile");
7965        assert!(c1.ops.iter().any(|o| matches!(o, Op::LogNot)));
7966        let c2 = compile_snippet("~0;").expect("compile");
7967        assert!(c2.ops.iter().any(|o| matches!(o, Op::BitNot)));
7968    }
7969
7970    #[test]
7971    fn compile_sub_registers_name_and_entry() {
7972        let chunk = compile_snippet("fn foo { return 1; }").expect("compile");
7973        assert!(chunk.names.iter().any(|n| n == "foo"));
7974        assert!(chunk
7975            .sub_entries
7976            .iter()
7977            .any(|&(idx, ip, _)| chunk.names[idx as usize] == "foo" && ip > 0));
7978        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Halt)));
7979        assert!(chunk.ops.iter().any(|o| matches!(o, Op::ReturnValue)));
7980    }
7981
7982    #[test]
7983    fn compile_postinc_scalar() {
7984        let chunk = compile_snippet("my $n = 1; $n++;").expect("compile");
7985        assert!(chunk
7986            .ops
7987            .iter()
7988            .any(|o| matches!(o, Op::PostInc(_) | Op::PostIncSlot(_))));
7989        assert_last_halt(&chunk);
7990    }
7991
7992    #[test]
7993    fn compile_preinc_scalar() {
7994        let chunk = compile_snippet("my $n = 1; ++$n;").expect("compile");
7995        assert!(chunk
7996            .ops
7997            .iter()
7998            .any(|o| matches!(o, Op::PreInc(_) | Op::PreIncSlot(_))));
7999        assert_last_halt(&chunk);
8000    }
8001
8002    #[test]
8003    fn compile_if_expression_value() {
8004        let chunk = compile_snippet("if (1) { 2 } else { 3 }").expect("compile");
8005        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8006        assert_last_halt(&chunk);
8007    }
8008
8009    #[test]
8010    fn compile_unless_expression_value() {
8011        let chunk = compile_snippet("unless (0) { 1 } else { 2 }").expect("compile");
8012        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8013        assert_last_halt(&chunk);
8014    }
8015
8016    #[test]
8017    fn compile_array_declare_and_push() {
8018        let chunk = compile_snippet("my @a; push @a, 1;").expect("compile");
8019        assert!(chunk.ops.iter().any(|o| matches!(o, Op::DeclareArray(_))));
8020        assert_last_halt(&chunk);
8021    }
8022
8023    #[test]
8024    fn compile_ternary() {
8025        let chunk = compile_snippet("1 ? 2 : 3;").expect("compile");
8026        assert!(chunk.ops.iter().any(|o| matches!(o, Op::JumpIfFalse(_))));
8027        assert_last_halt(&chunk);
8028    }
8029
8030    #[test]
8031    fn compile_repeat_operator() {
8032        let chunk = compile_snippet(r#"'ab' x 3;"#).expect("compile");
8033        assert!(chunk.ops.iter().any(|o| matches!(o, Op::StringRepeat)));
8034        assert_last_halt(&chunk);
8035    }
8036
8037    #[test]
8038    fn compile_range_to_array() {
8039        let chunk = compile_snippet("my @a = (1..3);").expect("compile");
8040        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Range)));
8041        assert_last_halt(&chunk);
8042    }
8043
8044    /// Scalar `..` / `...` in a boolean condition must be the flip-flop (`$.`), not a list range.
8045    #[test]
8046    fn compile_print_if_uses_scalar_flipflop_not_range_list() {
8047        let chunk = compile_snippet("print if 1..2;").expect("compile");
8048        assert!(
8049            chunk
8050                .ops
8051                .iter()
8052                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 0))),
8053            "expected ScalarFlipFlop in bytecode, got:\n{}",
8054            chunk.disassemble()
8055        );
8056        assert!(
8057            !chunk.ops.iter().any(|o| matches!(o, Op::Range)),
8058            "did not expect list Range op in scalar if-condition:\n{}",
8059            chunk.disassemble()
8060        );
8061    }
8062
8063    #[test]
8064    fn compile_print_if_three_dot_scalar_flipflop_sets_exclusive_flag() {
8065        let chunk = compile_snippet("print if 1...2;").expect("compile");
8066        assert!(
8067            chunk
8068                .ops
8069                .iter()
8070                .any(|o| matches!(o, Op::ScalarFlipFlop(_, 1))),
8071            "expected ScalarFlipFlop(..., exclusive=1), got:\n{}",
8072            chunk.disassemble()
8073        );
8074    }
8075
8076    #[test]
8077    fn compile_regex_flipflop_two_dot_emits_regex_flipflop_op() {
8078        let chunk = compile_snippet(r#"print if /a/../b/;"#).expect("compile");
8079        assert!(
8080            chunk
8081                .ops
8082                .iter()
8083                .any(|o| matches!(o, Op::RegexFlipFlop(_, 0, _, _, _, _))),
8084            "expected RegexFlipFlop(.., exclusive=0), got:\n{}",
8085            chunk.disassemble()
8086        );
8087        assert!(
8088            !chunk
8089                .ops
8090                .iter()
8091                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
8092            "regex flip-flop must not use ScalarFlipFlop:\n{}",
8093            chunk.disassemble()
8094        );
8095    }
8096
8097    #[test]
8098    fn compile_regex_flipflop_three_dot_sets_exclusive_flag() {
8099        let chunk = compile_snippet(r#"print if /a/.../b/;"#).expect("compile");
8100        assert!(
8101            chunk
8102                .ops
8103                .iter()
8104                .any(|o| matches!(o, Op::RegexFlipFlop(_, 1, _, _, _, _))),
8105            "expected RegexFlipFlop(..., exclusive=1), got:\n{}",
8106            chunk.disassemble()
8107        );
8108    }
8109
8110    #[test]
8111    fn compile_regex_eof_flipflop_emits_regex_eof_flipflop_op() {
8112        let chunk = compile_snippet(r#"print if /a/..eof;"#).expect("compile");
8113        assert!(
8114            chunk
8115                .ops
8116                .iter()
8117                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 0, _, _))),
8118            "expected RegexEofFlipFlop(.., exclusive=0), got:\n{}",
8119            chunk.disassemble()
8120        );
8121        assert!(
8122            !chunk
8123                .ops
8124                .iter()
8125                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
8126            "regex/eof flip-flop must not use ScalarFlipFlop:\n{}",
8127            chunk.disassemble()
8128        );
8129    }
8130
8131    #[test]
8132    fn compile_regex_eof_flipflop_three_dot_sets_exclusive_flag() {
8133        let chunk = compile_snippet(r#"print if /a/...eof;"#).expect("compile");
8134        assert!(
8135            chunk
8136                .ops
8137                .iter()
8138                .any(|o| matches!(o, Op::RegexEofFlipFlop(_, 1, _, _))),
8139            "expected RegexEofFlipFlop(..., exclusive=1), got:\n{}",
8140            chunk.disassemble()
8141        );
8142    }
8143
8144    #[test]
8145    fn compile_regex_flipflop_compound_rhs_emits_regex_flip_flop_expr_rhs() {
8146        let chunk = compile_snippet(r#"print if /a/...(/b/ or /c/);"#).expect("compile");
8147        assert!(
8148            chunk
8149                .ops
8150                .iter()
8151                .any(|o| matches!(o, Op::RegexFlipFlopExprRhs(_, _, _, _, _))),
8152            "expected RegexFlipFlopExprRhs for compound RHS, got:\n{}",
8153            chunk.disassemble()
8154        );
8155        assert!(
8156            !chunk
8157                .ops
8158                .iter()
8159                .any(|o| matches!(o, Op::ScalarFlipFlop(_, _))),
8160            "compound regex flip-flop must not use ScalarFlipFlop:\n{}",
8161            chunk.disassemble()
8162        );
8163    }
8164
8165    #[test]
8166    fn compile_print_statement() {
8167        let chunk = compile_snippet("print 1;").expect("compile");
8168        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Print(_, _))));
8169        assert_last_halt(&chunk);
8170    }
8171
8172    #[test]
8173    fn compile_defined_builtin() {
8174        let chunk = compile_snippet("defined 1;").expect("compile");
8175        assert!(chunk
8176            .ops
8177            .iter()
8178            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Defined as u16)));
8179        assert_last_halt(&chunk);
8180    }
8181
8182    #[test]
8183    fn compile_length_builtin() {
8184        let chunk = compile_snippet("length 'abc';").expect("compile");
8185        assert!(chunk
8186            .ops
8187            .iter()
8188            .any(|o| matches!(o, Op::CallBuiltin(id, _) if *id == BuiltinId::Length as u16)));
8189        assert_last_halt(&chunk);
8190    }
8191
8192    #[test]
8193    fn compile_complex_expr_parentheses() {
8194        let chunk = compile_snippet("(1 + 2) * (3 + 4);").expect("compile");
8195        assert!(chunk.ops.iter().filter(|o| matches!(o, Op::Add)).count() >= 2);
8196        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Mul)));
8197        assert_last_halt(&chunk);
8198    }
8199
8200    #[test]
8201    fn compile_undef_literal() {
8202        let chunk = compile_snippet("undef;").expect("compile");
8203        assert!(chunk.ops.iter().any(|o| matches!(o, Op::LoadUndef)));
8204        assert_last_halt(&chunk);
8205    }
8206
8207    #[test]
8208    fn compile_empty_statement_semicolons() {
8209        let chunk = compile_snippet(";;;").expect("compile");
8210        assert_last_halt(&chunk);
8211    }
8212
8213    #[test]
8214    fn compile_array_elem_preinc_uses_rot_and_set_elem() {
8215        let chunk = compile_snippet("my @a; $a[0] = 0; ++$a[0];").expect("compile");
8216        assert!(
8217            chunk.ops.iter().any(|o| matches!(o, Op::Rot)),
8218            "expected Rot in {:?}",
8219            chunk.ops
8220        );
8221        assert!(
8222            chunk.ops.iter().any(|o| matches!(o, Op::SetArrayElem(_))),
8223            "expected SetArrayElem in {:?}",
8224            chunk.ops
8225        );
8226        assert_last_halt(&chunk);
8227    }
8228
8229    #[test]
8230    fn compile_hash_elem_compound_assign_uses_rot() {
8231        let chunk = compile_snippet("my %h; $h{0} = 1; $h{0} += 2;").expect("compile");
8232        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
8233        assert!(
8234            chunk.ops.iter().any(|o| matches!(o, Op::SetHashElem(_))),
8235            "expected SetHashElem"
8236        );
8237        assert_last_halt(&chunk);
8238    }
8239
8240    #[test]
8241    fn compile_postfix_inc_array_elem_emits_rot() {
8242        let chunk = compile_snippet("my @a; $a[1] = 5; $a[1]++;").expect("compile");
8243        assert!(chunk.ops.iter().any(|o| matches!(o, Op::Rot)));
8244        assert_last_halt(&chunk);
8245    }
8246
8247    #[test]
8248    fn compile_tie_stmt_emits_op_tie() {
8249        let chunk = compile_snippet("tie %h, 'Pkg';").expect("compile");
8250        assert!(
8251            chunk.ops.iter().any(|o| matches!(o, Op::Tie { .. })),
8252            "expected Op::Tie in {:?}",
8253            chunk.ops
8254        );
8255        assert_last_halt(&chunk);
8256    }
8257
8258    #[test]
8259    fn compile_format_decl_emits_format_decl_op() {
8260        let chunk = compile_snippet(
8261            r#"
8262format FMT =
8263literal line
8264.
82651;
8266"#,
8267        )
8268        .expect("compile");
8269        assert!(
8270            chunk.ops.iter().any(|o| matches!(o, Op::FormatDecl(0))),
8271            "expected Op::FormatDecl(0), got {:?}",
8272            chunk.ops
8273        );
8274        assert_eq!(chunk.format_decls.len(), 1);
8275        assert_eq!(chunk.format_decls[0].0, "FMT");
8276        assert_eq!(chunk.format_decls[0].1, vec!["literal line".to_string()]);
8277        assert_last_halt(&chunk);
8278    }
8279
8280    #[test]
8281    fn compile_interpolated_string_scalar_only_emits_empty_prefix_and_concat() {
8282        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "$x";"#).expect("compile");
8283        let empty_idx = chunk
8284            .constants
8285            .iter()
8286            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
8287            .expect("empty string in pool") as u16;
8288        assert!(
8289            chunk
8290                .ops
8291                .iter()
8292                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
8293            "expected LoadConst(\"\"), ops={:?}",
8294            chunk.ops
8295        );
8296        assert!(
8297            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
8298            "expected Op::Concat for qq with only a scalar part, ops={:?}",
8299            chunk.ops
8300        );
8301        assert_last_halt(&chunk);
8302    }
8303
8304    #[test]
8305    fn compile_interpolated_string_array_only_emits_stringify_and_concat() {
8306        let chunk = compile_snippet(r#"no strict 'vars'; my @a = (1, 2); "@a";"#).expect("compile");
8307        let empty_idx = chunk
8308            .constants
8309            .iter()
8310            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
8311            .expect("empty string in pool") as u16;
8312        assert!(
8313            chunk
8314                .ops
8315                .iter()
8316                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
8317            "expected LoadConst(\"\"), ops={:?}",
8318            chunk.ops
8319        );
8320        assert!(
8321            chunk
8322                .ops
8323                .iter()
8324                .any(|o| matches!(o, Op::ArrayStringifyListSep)),
8325            "expected ArrayStringifyListSep for array var in qq, ops={:?}",
8326            chunk.ops
8327        );
8328        assert!(
8329            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
8330            "expected Op::Concat after array stringify, ops={:?}",
8331            chunk.ops
8332        );
8333        assert_last_halt(&chunk);
8334    }
8335
8336    #[test]
8337    fn compile_interpolated_string_hash_element_only_emits_empty_prefix_and_concat() {
8338        let chunk =
8339            compile_snippet(r#"no strict 'vars'; my %h = (k => 1); "$h{k}";"#).expect("compile");
8340        let empty_idx = chunk
8341            .constants
8342            .iter()
8343            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
8344            .expect("empty string in pool") as u16;
8345        assert!(
8346            chunk
8347                .ops
8348                .iter()
8349                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
8350            "expected LoadConst(\"\"), ops={:?}",
8351            chunk.ops
8352        );
8353        assert!(
8354            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
8355            "expected Op::Concat for qq with only an expr part, ops={:?}",
8356            chunk.ops
8357        );
8358        assert_last_halt(&chunk);
8359    }
8360
8361    #[test]
8362    fn compile_interpolated_string_leading_literal_has_no_empty_string_prefix() {
8363        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; "a$x";"#).expect("compile");
8364        assert!(
8365            !chunk
8366                .constants
8367                .iter()
8368                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
8369            "literal-first qq must not intern \"\" (only non-literal first parts need it), ops={:?}",
8370            chunk.ops
8371        );
8372        assert!(
8373            chunk.ops.iter().any(|o| matches!(o, Op::Concat)),
8374            "expected Op::Concat after literal + scalar, ops={:?}",
8375            chunk.ops
8376        );
8377        assert_last_halt(&chunk);
8378    }
8379
8380    #[test]
8381    fn compile_interpolated_string_two_scalars_empty_prefix_and_two_concats() {
8382        let chunk =
8383            compile_snippet(r#"no strict 'vars'; my $a = 1; my $b = 2; "$a$b";"#).expect("compile");
8384        let empty_idx = chunk
8385            .constants
8386            .iter()
8387            .position(|c| c.as_str().is_some_and(|s| s.is_empty()))
8388            .expect("empty string in pool") as u16;
8389        assert!(
8390            chunk
8391                .ops
8392                .iter()
8393                .any(|o| matches!(o, Op::LoadConst(i) if *i == empty_idx)),
8394            "expected LoadConst(\"\") before first scalar qq part, ops={:?}",
8395            chunk.ops
8396        );
8397        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
8398        assert!(
8399            n_concat >= 2,
8400            "expected at least two Op::Concat for two scalar qq parts, got {} in {:?}",
8401            n_concat,
8402            chunk.ops
8403        );
8404        assert_last_halt(&chunk);
8405    }
8406
8407    #[test]
8408    fn compile_interpolated_string_literal_then_two_scalars_has_no_empty_prefix() {
8409        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 7; my $y = 8; "p$x$y";"#)
8410            .expect("compile");
8411        assert!(
8412            !chunk
8413                .constants
8414                .iter()
8415                .any(|c| c.as_str().is_some_and(|s| s.is_empty())),
8416            "literal-first qq must not intern empty string, ops={:?}",
8417            chunk.ops
8418        );
8419        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
8420        assert!(
8421            n_concat >= 2,
8422            "expected two Concats for literal + two scalars, got {} in {:?}",
8423            n_concat,
8424            chunk.ops
8425        );
8426        assert_last_halt(&chunk);
8427    }
8428
8429    #[test]
8430    fn compile_interpolated_string_braced_scalar_trailing_literal_emits_concats() {
8431        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "a${u}z";"#).expect("compile");
8432        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
8433        assert!(
8434            n_concat >= 2,
8435            "expected braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
8436            n_concat,
8437            chunk.ops
8438        );
8439        assert_last_halt(&chunk);
8440    }
8441
8442    #[test]
8443    fn compile_interpolated_string_braced_scalar_sandwiched_emits_concats() {
8444        let chunk = compile_snippet(r#"no strict 'vars'; my $u = 1; "L${u}R";"#).expect("compile");
8445        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
8446        assert!(
8447            n_concat >= 2,
8448            "expected leading literal + braced scalar + trailing literal to use multiple Concats, got {} in {:?}",
8449            n_concat,
8450            chunk.ops
8451        );
8452        assert_last_halt(&chunk);
8453    }
8454
8455    #[test]
8456    fn compile_interpolated_string_mixed_braced_and_plain_scalars_emits_concats() {
8457        let chunk = compile_snippet(r#"no strict 'vars'; my $x = 1; my $y = 2; "a${x}b$y";"#)
8458            .expect("compile");
8459        let n_concat = chunk.ops.iter().filter(|o| matches!(o, Op::Concat)).count();
8460        assert!(
8461            n_concat >= 3,
8462            "expected literal/braced/plain qq mix to use at least three Concats, got {} in {:?}",
8463            n_concat,
8464            chunk.ops
8465        );
8466        assert_last_halt(&chunk);
8467    }
8468
8469    #[test]
8470    fn compile_use_overload_emits_use_overload_op() {
8471        let chunk = compile_snippet(r#"use overload '""' => 'as_string';"#).expect("compile");
8472        assert!(
8473            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
8474            "expected Op::UseOverload(0), got {:?}",
8475            chunk.ops
8476        );
8477        assert_eq!(chunk.use_overload_entries.len(), 1);
8478        // Perl `'""'` is a single-quoted string whose contents are two `"` characters — the
8479        // overload table key for stringify (see [`Interpreter::overload_stringify_method`]).
8480        let stringify_key: String = ['"', '"'].iter().collect();
8481        assert_eq!(
8482            chunk.use_overload_entries[0],
8483            vec![(stringify_key, "as_string".to_string())]
8484        );
8485        assert_last_halt(&chunk);
8486    }
8487
8488    #[test]
8489    fn compile_use_overload_empty_list_emits_use_overload_with_no_pairs() {
8490        let chunk = compile_snippet(r#"use overload ();"#).expect("compile");
8491        assert!(
8492            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
8493            "expected Op::UseOverload(0), got {:?}",
8494            chunk.ops
8495        );
8496        assert_eq!(chunk.use_overload_entries.len(), 1);
8497        assert!(chunk.use_overload_entries[0].is_empty());
8498        assert_last_halt(&chunk);
8499    }
8500
8501    #[test]
8502    fn compile_use_overload_multiple_pairs_single_op() {
8503        let chunk =
8504            compile_snippet(r#"use overload '+' => 'p_add', '-' => 'p_sub';"#).expect("compile");
8505        assert!(
8506            chunk.ops.iter().any(|o| matches!(o, Op::UseOverload(0))),
8507            "expected Op::UseOverload(0), got {:?}",
8508            chunk.ops
8509        );
8510        assert_eq!(chunk.use_overload_entries.len(), 1);
8511        assert_eq!(
8512            chunk.use_overload_entries[0],
8513            vec![
8514                ("+".to_string(), "p_add".to_string()),
8515                ("-".to_string(), "p_sub".to_string()),
8516            ]
8517        );
8518        assert_last_halt(&chunk);
8519    }
8520
8521    #[test]
8522    fn compile_open_my_fh_emits_declare_open_set() {
8523        let chunk = compile_snippet(r#"open my $fh, "<", "/dev/null";"#).expect("compile");
8524        assert!(
8525            chunk.ops.iter().any(|o| matches!(
8526                o,
8527                Op::CallBuiltin(b, 3) if *b == BuiltinId::Open as u16
8528            )),
8529            "expected Open builtin 3-arg, got {:?}",
8530            chunk.ops
8531        );
8532        assert!(
8533            chunk
8534                .ops
8535                .iter()
8536                .any(|o| matches!(o, Op::SetScalarKeepPlain(_))),
8537            "expected SetScalarKeepPlain after open"
8538        );
8539        assert_last_halt(&chunk);
8540    }
8541
8542    #[test]
8543    fn compile_local_hash_element_emits_local_declare_hash_element() {
8544        let chunk = compile_snippet(r#"local $SIG{__WARN__} = 0;"#).expect("compile");
8545        assert!(
8546            chunk
8547                .ops
8548                .iter()
8549                .any(|o| matches!(o, Op::LocalDeclareHashElement(_))),
8550            "expected LocalDeclareHashElement in {:?}",
8551            chunk.ops
8552        );
8553        assert_last_halt(&chunk);
8554    }
8555
8556    #[test]
8557    fn compile_local_array_element_emits_local_declare_array_element() {
8558        let chunk = compile_snippet(r#"local $a[2] = 9;"#).expect("compile");
8559        assert!(
8560            chunk
8561                .ops
8562                .iter()
8563                .any(|o| matches!(o, Op::LocalDeclareArrayElement(_))),
8564            "expected LocalDeclareArrayElement in {:?}",
8565            chunk.ops
8566        );
8567        assert_last_halt(&chunk);
8568    }
8569
8570    #[test]
8571    fn compile_local_typeglob_emits_local_declare_typeglob() {
8572        let chunk = compile_snippet(r#"local *STDOUT;"#).expect("compile");
8573        assert!(
8574            chunk
8575                .ops
8576                .iter()
8577                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, None))),
8578            "expected LocalDeclareTypeglob(_, None) in {:?}",
8579            chunk.ops
8580        );
8581        assert_last_halt(&chunk);
8582    }
8583
8584    #[test]
8585    fn compile_local_typeglob_alias_emits_local_declare_typeglob_some_rhs() {
8586        let chunk = compile_snippet(r#"local *FOO = *STDOUT;"#).expect("compile");
8587        assert!(
8588            chunk
8589                .ops
8590                .iter()
8591                .any(|o| matches!(o, Op::LocalDeclareTypeglob(_, Some(_)))),
8592            "expected LocalDeclareTypeglob with rhs in {:?}",
8593            chunk.ops
8594        );
8595        assert_last_halt(&chunk);
8596    }
8597
8598    #[test]
8599    fn compile_local_braced_typeglob_emits_local_declare_typeglob_dynamic() {
8600        let chunk = compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *{ $g };"#)
8601            .expect("compile");
8602        assert!(
8603            chunk
8604                .ops
8605                .iter()
8606                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
8607            "expected LocalDeclareTypeglobDynamic(None) in {:?}",
8608            chunk.ops
8609        );
8610        assert_last_halt(&chunk);
8611    }
8612
8613    #[test]
8614    fn compile_local_star_deref_typeglob_emits_local_declare_typeglob_dynamic() {
8615        let chunk =
8616            compile_snippet(r#"no strict 'refs'; my $g = "STDOUT"; local *$g;"#).expect("compile");
8617        assert!(
8618            chunk
8619                .ops
8620                .iter()
8621                .any(|o| matches!(o, Op::LocalDeclareTypeglobDynamic(None))),
8622            "expected LocalDeclareTypeglobDynamic(None) for local *scalar glob in {:?}",
8623            chunk.ops
8624        );
8625        assert_last_halt(&chunk);
8626    }
8627
8628    #[test]
8629    fn compile_braced_glob_assign_to_named_glob_emits_copy_dynamic_lhs() {
8630        // `*{EXPR} = *FOO` — dynamic lhs name + static rhs glob → `CopyTypeglobSlotsDynamicLhs`.
8631        let chunk = compile_snippet(r#"no strict 'refs'; my $n = "x"; *{ $n } = *STDOUT;"#)
8632            .expect("compile");
8633        assert!(
8634            chunk
8635                .ops
8636                .iter()
8637                .any(|o| matches!(o, Op::CopyTypeglobSlotsDynamicLhs(_))),
8638            "expected CopyTypeglobSlotsDynamicLhs in {:?}",
8639            chunk.ops
8640        );
8641        assert_last_halt(&chunk);
8642    }
8643}