Skip to main content

stryke/
compiler.rs

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