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