Skip to main content

stryke/
bytecode.rs

1use serde::{Deserialize, Serialize};
2
3use crate::ast::{Block, ClassDef, EnumDef, Expr, MatchArm, StructDef, SubSigParam, TraitDef};
4use crate::value::PerlValue;
5
6/// `splice` operand tuple: array expr, offset, length, replacement list (see [`Chunk::splice_expr_entries`]).
7pub(crate) type SpliceExprEntry = (Expr, Option<Expr>, Option<Expr>, Vec<Expr>);
8
9/// `sub` body registered at run time (e.g. `BEGIN { sub f { ... } }`), mirrored from
10/// [`crate::interpreter::Interpreter::exec_statement`] `StmtKind::SubDecl`.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RuntimeSubDecl {
13    pub name: String,
14    pub params: Vec<SubSigParam>,
15    pub body: Block,
16    pub prototype: Option<String>,
17}
18
19/// Stack-based bytecode instruction set for the stryke VM.
20/// Operands use u16 for pool indices (64k names/constants) and i32 for jumps.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub enum Op {
23    Nop,
24    // ── Constants ──
25    LoadInt(i64),
26    LoadFloat(f64),
27    LoadConst(u16), // index into constant pool
28    LoadUndef,
29
30    // ── Stack ──
31    Pop,
32    Dup,
33    /// Duplicate the top two stack values: \[a, b\] (b on top) → \[a, b, a, b\].
34    Dup2,
35    /// Swap the top two stack values (PerlValue).
36    Swap,
37    /// Rotate the top three values upward (FORTH `rot`): `[a, b, c]` (c on top) → `[b, c, a]`.
38    Rot,
39    /// Pop one value; push [`PerlValue::scalar_context`] of that value (Perl aggregate rules).
40    ValueScalarContext,
41
42    // ── Scalars (u16 = name pool index) ──
43    GetScalar(u16),
44    /// Like `GetScalar` but reads `scope.get_scalar` only (no Perl special-variable dispatch).
45    GetScalarPlain(u16),
46    SetScalar(u16),
47    /// Like `SetScalar` but calls `scope.set_scalar` only (no special-variable dispatch).
48    SetScalarPlain(u16),
49    DeclareScalar(u16),
50    /// Like `DeclareScalar` but the binding is immutable after initialization.
51    DeclareScalarFrozen(u16),
52    /// `typed my $x : Type` — u8 encodes [`crate::ast::PerlTypeName`] (0=Int,1=Str,2=Float).
53    DeclareScalarTyped(u16, u8),
54    /// `frozen typed my $x : Type` — immutable after initialization + type-checked.
55    DeclareScalarTypedFrozen(u16, u8),
56
57    // ── Arrays ──
58    GetArray(u16),
59    SetArray(u16),
60    DeclareArray(u16),
61    DeclareArrayFrozen(u16),
62    GetArrayElem(u16), // stack: [index] → value
63    SetArrayElem(u16), // stack: [value, index]
64    /// Like [`Op::SetArrayElem`] but leaves the assigned value on the stack (e.g. `$a[$i] //=`).
65    SetArrayElemKeep(u16),
66    PushArray(u16),  // stack: [value] → push to named array
67    PopArray(u16),   // → popped value
68    ShiftArray(u16), // → shifted value
69    ArrayLen(u16),   // → integer length
70    /// Pop index spec (scalar or array from [`Op::Range`]); push one `PerlValue::array` of elements
71    /// read from the named array. Used for `@name[...]` slice rvalues.
72    ArraySlicePart(u16),
73    /// Pop `b`, pop `a` (arrays); push concatenation `a` followed by `b` (Perl slice / list glue).
74    ArrayConcatTwo,
75    /// `exists $a[$i]` — stack: `[index]` → 0/1 (stash-qualified array name pool index).
76    ExistsArrayElem(u16),
77    /// `delete $a[$i]` — stack: `[index]` → deleted value (or undef).
78    DeleteArrayElem(u16),
79
80    // ── Hashes ──
81    GetHash(u16),
82    SetHash(u16),
83    DeclareHash(u16),
84    DeclareHashFrozen(u16),
85    /// Dynamic `local $x` — save previous binding, assign TOS (same stack shape as DeclareScalar).
86    LocalDeclareScalar(u16),
87    LocalDeclareArray(u16),
88    LocalDeclareHash(u16),
89    /// `local $h{key} = val` — stack: `[value, key]` (key on top), same as [`Op::SetHashElem`].
90    LocalDeclareHashElement(u16),
91    /// `local $a[i] = val` — stack: `[value, index]` (index on top), same as [`Op::SetArrayElem`].
92    LocalDeclareArrayElement(u16),
93    /// `local *name` or `local *name = *other` — second pool index is `Some(rhs)` when aliasing.
94    LocalDeclareTypeglob(u16, Option<u16>),
95    /// `local *{EXPR}` / `local *$x` — LHS glob name string on stack (TOS); optional static `*rhs` pool index.
96    LocalDeclareTypeglobDynamic(Option<u16>),
97    GetHashElem(u16), // stack: [key] → value
98    SetHashElem(u16), // stack: [value, key]
99    /// Like [`Op::SetHashElem`] but leaves the assigned value on the stack (e.g. `$h{k} //=`).
100    SetHashElemKeep(u16),
101    DeleteHashElem(u16), // stack: [key] → deleted value
102    ExistsHashElem(u16), // stack: [key] → 0/1
103    /// `delete $href->{key}` — stack: `[container, key]` (key on top) → deleted value.
104    DeleteArrowHashElem,
105    /// `exists $href->{key}` — stack: `[container, key]` → 0/1.
106    ExistsArrowHashElem,
107    /// `exists $aref->[$i]` — stack: `[container, index]` (index on top, int-coerced).
108    ExistsArrowArrayElem,
109    /// `delete $aref->[$i]` — stack: `[container, index]` → deleted value (or undef).
110    DeleteArrowArrayElem,
111    HashKeys(u16),   // → array of keys
112    HashValues(u16), // → array of values
113    /// Scalar `keys %h` — push integer key count.
114    HashKeysScalar(u16),
115    /// Scalar `values %h` — push integer value count.
116    HashValuesScalar(u16),
117    /// `keys EXPR` after operand evaluated in list context — stack: `[value]` → key list array.
118    KeysFromValue,
119    /// Scalar `keys EXPR` after operand — stack: `[value]` → key count.
120    KeysFromValueScalar,
121    /// `values EXPR` after operand evaluated in list context — stack: `[value]` → values array.
122    ValuesFromValue,
123    /// Scalar `values EXPR` after operand — stack: `[value]` → value count.
124    ValuesFromValueScalar,
125
126    /// `push @$aref, ITEM` — stack: `[aref, item]` (item on top); mutates; pushes `aref` back.
127    PushArrayDeref,
128    /// After `push @$aref, …` — stack: `[aref]` → `[len]` (consumes aref).
129    ArrayDerefLen,
130    /// `pop @$aref` — stack: `[aref]` → popped value.
131    PopArrayDeref,
132    /// `shift @$aref` — stack: `[aref]` → shifted value.
133    ShiftArrayDeref,
134    /// `unshift @$aref, LIST` — stack `[aref, v1, …, vn]` (vn on top); `n` extra values.
135    UnshiftArrayDeref(u8),
136    /// `splice @$aref, off, len, LIST` — stack top: replacements, then `len`, `off`, `aref` (`len` may be undef).
137    SpliceArrayDeref(u8),
138
139    // ── Arithmetic ──
140    Add,
141    Sub,
142    Mul,
143    Div,
144    Mod,
145    Pow,
146    Negate,
147    /// `inc EXPR` — pop value, push value + 1 (integer if input is integer, else float).
148    Inc,
149    /// `dec EXPR` — pop value, push value - 1.
150    Dec,
151
152    // ── String ──
153    Concat,
154    /// Pop array (or value coerced with [`PerlValue::to_list`]), join element strings with
155    /// [`Interpreter::list_separator`] (`$"`), push one string. Used for `@a` in `"` / `qq`.
156    ArrayStringifyListSep,
157    StringRepeat,
158    /// Pop string, apply `\U` / `\L` / `\u` / `\l` / `\Q` / `\E` case escapes, push result.
159    ProcessCaseEscapes,
160
161    // ── Comparison (numeric) ──
162    NumEq,
163    NumNe,
164    NumLt,
165    NumGt,
166    NumLe,
167    NumGe,
168    Spaceship,
169
170    // ── Comparison (string) ──
171    StrEq,
172    StrNe,
173    StrLt,
174    StrGt,
175    StrLe,
176    StrGe,
177    StrCmp,
178
179    // ── Logical / Bitwise ──
180    LogNot,
181    BitAnd,
182    BitOr,
183    BitXor,
184    BitNot,
185    Shl,
186    Shr,
187
188    // ── Control flow (absolute target addresses) ──
189    Jump(usize),
190    JumpIfTrue(usize),
191    JumpIfFalse(usize),
192    /// Jump if TOS is falsy WITHOUT popping (for short-circuit &&)
193    JumpIfFalseKeep(usize),
194    /// Jump if TOS is truthy WITHOUT popping (for short-circuit ||)
195    JumpIfTrueKeep(usize),
196    /// Jump if TOS is defined WITHOUT popping (for //)
197    JumpIfDefinedKeep(usize),
198
199    // ── Increment / Decrement ──
200    PreInc(u16),
201    PreDec(u16),
202    PostInc(u16),
203    PostDec(u16),
204    /// Pre-increment on a frame slot entry (compiled `my $x` fast path).
205    PreIncSlot(u8),
206    PreDecSlot(u8),
207    PostIncSlot(u8),
208    PostDecSlot(u8),
209
210    // ── Functions ──
211    /// Call subroutine: name index, arg count, `WantarrayCtx` discriminant as `u8`
212    Call(u16, u8, u8),
213    /// Like [`Op::Call`] but with a compile-time-resolved entry: `sid` indexes [`Chunk::static_sub_calls`]
214    /// (entry IP + stack-args); `name_idx` duplicates the stash pool index for closure restore / JIT
215    /// (same as in the table; kept in the opcode so JIT does not need the side table).
216    CallStaticSubId(u16, u16, u8, u8),
217    Return,
218    ReturnValue,
219    /// End of a compiled `map` / `grep` / `sort` block body (empty block or last statement an expression).
220    /// Pops the synthetic call frame from [`crate::vm::VM::run_block_region`] and unwinds the
221    /// block-local scope (`scope_push_hook` per iteration, like [`crate::interpreter::Interpreter::exec_block`]);
222    /// not subroutine `return` and not a closure capture.
223    BlockReturnValue,
224    /// At runtime statement position: capture current lexicals into [`crate::value::PerlSub::closure_env`]
225    /// for a sub already registered in [`Interpreter::subs`] (see `prepare_program_top_level`).
226    BindSubClosure(u16),
227
228    // ── Scope ──
229    PushFrame,
230    PopFrame,
231
232    // ── I/O ──
233    /// `print [HANDLE] LIST` — `None` uses [`crate::interpreter::Interpreter::default_print_handle`].
234    Print(Option<u16>, u8),
235    Say(Option<u16>, u8),
236
237    // ── Built-in function calls ──
238    /// Calls a registered built-in: (builtin_id, arg_count)
239    CallBuiltin(u16, u8),
240    /// Save [`crate::interpreter::Interpreter::wantarray_kind`] and set from `u8`
241    /// ([`crate::interpreter::WantarrayCtx::as_byte`]). Used for `splice` / similar where the
242    /// dynamic context must match the expression's compile-time [`WantarrayCtx`] (e.g. `print splice…`).
243    WantarrayPush(u8),
244    /// Restore after [`Op::WantarrayPush`].
245    WantarrayPop,
246
247    // ── List / Range ──
248    MakeArray(u16), // pop N values, push as Array
249    /// `@$href{k1,k2}` — stack: `[container, key1, …, keyN]` (TOS = last key); pops `N+1` values; pushes array of slot values.
250    HashSliceDeref(u16),
251    /// `@$aref[i1,i2,...]` — stack: `[array_ref, spec1, …, specN]` (TOS = last spec); each spec is a
252    /// scalar index or array of indices (list-context `..` / `qw`/list). Pops `N+1`; pushes elements.
253    ArrowArraySlice(u16),
254    /// `@$href{k1,k2} = VALUE` — stack: `[value, container, key1, …, keyN]` (TOS = last key); pops `N+2` values.
255    SetHashSliceDeref(u16),
256    /// `%name{k1,k2} = VALUE` — stack: `[value, key1, …, keyN]` (TOS = last key); pops `N+1`. Pool: hash name, key count.
257    SetHashSlice(u16, u16),
258    /// `@$href{k1,k2} OP= VALUE` — stack: `[rhs, container, key1, …, keyN]` (TOS = last key); pops `N+2`, pushes the new value.
259    /// `u8` = [`crate::compiler::scalar_compound_op_to_byte`] encoding of the binop.
260    /// Perl 5 applies the op only to the **last** key’s element.
261    HashSliceDerefCompound(u8, u16),
262    /// `++@$href{k1,k2}` / `--...` / `@$href{k1,k2}++` / `...--` — stack: `[container, key1, …, keyN]`;
263    /// pops `N+1`. Pre-forms push the new last-element value; post-forms push the **old** last value.
264    /// `u8` encodes kind: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec. Only the last key is updated.
265    HashSliceDerefIncDec(u8, u16),
266    /// `@name{k1,k2} OP= rhs` — stack: `[rhs, key1, …, keyN]` (TOS = last key); pops `N+1`, pushes the new value.
267    /// Pool: compound-op byte ([`crate::compiler::scalar_compound_op_to_byte`]), stash hash name, key-slot count.
268    /// Only the **last** flattened key is updated (same as [`Op::HashSliceDerefCompound`]).
269    NamedHashSliceCompound(u8, u16, u16),
270    /// `++@name{k1,k2}` / `--…` / `@name{k1,k2}++` / `…--` — stack: `[key1, …, keyN]`; pops `N`.
271    /// `u8` kind matches [`Op::HashSliceDerefIncDec`]. Only the last key is updated.
272    NamedHashSliceIncDec(u8, u16, u16),
273    /// Multi-key `@h{k1,k2} //=` / `||=` / `&&=` — stack `[key1, …, keyN]` unchanged; pushes the **last**
274    /// flattened slot (Perl only tests that slot). Pool: hash name, key-slot count.
275    NamedHashSlicePeekLast(u16, u16),
276    /// Stack `[key1, …, keyN, cur]` — pop `N` key slots, keep `cur` (short-circuit path).
277    NamedHashSliceDropKeysKeepCur(u16),
278    /// Assign list RHS’s last element to the **last** flattened key; stack `[val, key1, …, keyN]` (TOS = last key). Pushes `val`.
279    SetNamedHashSliceLastKeep(u16, u16),
280    /// Multi-key `@$href{k1,k2} //=` — stack `[container, key1, …, keyN]`; pushes last slice element (see [`Op::ArrowArraySlicePeekLast`]).
281    HashSliceDerefPeekLast(u16),
282    /// `[container, key1, …, keyN, val]` → `[val, container, key1, …, keyN]` for [`Op::HashSliceDerefSetLastKeep`].
283    HashSliceDerefRollValUnderKeys(u16),
284    /// Assign to last flattened key only; stack `[val, container, key1, …, keyN]`. Pushes `val`.
285    HashSliceDerefSetLastKeep(u16),
286    /// Stack `[container, key1, …, keyN, cur]` — drop container and keys; keep `cur`.
287    HashSliceDerefDropKeysKeepCur(u16),
288    /// `@$aref[i1,i2,...] = LIST` — stack: `[value, aref, spec1, …, specN]` (TOS = last spec);
289    /// pops `N+2`. Delegates to [`crate::interpreter::Interpreter::assign_arrow_array_slice`].
290    SetArrowArraySlice(u16),
291    /// `@$aref[i1,i2,...] OP= rhs` — stack: `[rhs, aref, spec1, …, specN]`; pops `N+2`, pushes new value.
292    /// `u8` = [`crate::compiler::scalar_compound_op_to_byte`] encoding of the binop.
293    /// Perl 5 applies the op only to the **last** index. Delegates to [`crate::interpreter::Interpreter::compound_assign_arrow_array_slice`].
294    ArrowArraySliceCompound(u8, u16),
295    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — stack: `[aref, spec1, …, specN]`;
296    /// pops `N+1`. Pre-forms push the new last-element value; post-forms push the old last value.
297    /// `u8` kind matches [`Op::HashSliceDerefIncDec`]. Only the last index is updated. Delegates to
298    /// [`crate::interpreter::Interpreter::arrow_array_slice_inc_dec`].
299    ArrowArraySliceIncDec(u8, u16),
300    /// Read the element at the **last** flattened index of `@$aref[spec1,…]` without popping `aref`
301    /// or specs. Stack: `[aref, spec1, …, specN]` (TOS = last spec) → same plus pushed scalar.
302    /// Used for `@$r[i,j] //=` / `||=` / `&&=` short-circuit tests (Perl only tests the last slot).
303    ArrowArraySlicePeekLast(u16),
304    /// Stack: `[aref, spec1, …, specN, cur]` — pop slice keys and container, keep `cur` (short-circuit
305    /// result). `u16` = number of spec slots (same as [`Op::ArrowArraySlice`]).
306    ArrowArraySliceDropKeysKeepCur(u16),
307    /// Reorder `[aref, spec1, …, specN, val]` → `[val, aref, spec1, …, specN]` for
308    /// [`Op::SetArrowArraySliceLastKeep`].
309    ArrowArraySliceRollValUnderSpecs(u16),
310    /// Assign `val` to the **last** flattened index only; stack `[val, aref, spec1, …, specN]`
311    /// (TOS = last spec). Pushes `val` (like [`Op::SetArrowArrayKeep`]).
312    SetArrowArraySliceLastKeep(u16),
313    /// Like [`Op::ArrowArraySliceIncDec`] but for a **named** stash array (`@a[i1,i2,...]`).
314    /// Stack: `[spec1, …, specN]` (TOS = last spec). `u16` = name pool index (stash-qualified).
315    /// Delegates to [`crate::interpreter::Interpreter::named_array_slice_inc_dec`].
316    NamedArraySliceIncDec(u8, u16, u16),
317    /// `@name[spec1,…] OP= rhs` — stack `[rhs, spec1, …, specN]` (TOS = last spec); pops `N+1`.
318    /// Only the **last** flattened index is updated (same as [`Op::ArrowArraySliceCompound`]).
319    NamedArraySliceCompound(u8, u16, u16),
320    /// Read the **last** flattened slot of `@name[spec1,…]` without popping specs. Stack:
321    /// `[spec1, …, specN]` → same plus pushed scalar. `u16` pairs: name pool index, spec count.
322    NamedArraySlicePeekLast(u16, u16),
323    /// Stack: `[spec1, …, specN, cur]` — pop specs, keep `cur` (short-circuit). `u16` = spec count.
324    NamedArraySliceDropKeysKeepCur(u16),
325    /// `[spec1, …, specN, val]` → `[val, spec1, …, specN]` for [`Op::SetNamedArraySliceLastKeep`].
326    NamedArraySliceRollValUnderSpecs(u16),
327    /// Assign to the **last** index only; stack `[val, spec1, …, specN]`. Pushes `val`.
328    SetNamedArraySliceLastKeep(u16, u16),
329    /// `@name[spec1,…] = LIST` — stack `[value, spec1, …, specN]` (TOS = last spec); pops `N+1`.
330    /// Element-wise like [`Op::SetArrowArraySlice`]. Pool indices: stash-qualified array name, spec count.
331    SetNamedArraySlice(u16, u16),
332    /// `BAREWORD` as an rvalue — at run time, look up a subroutine with this name; if found,
333    /// call it with no args (nullary), otherwise push the name as a string (Perl's bareword-as-
334    /// stringifies behavior). `u16` is a name-pool index. Delegates to
335    /// [`crate::interpreter::Interpreter::resolve_bareword_rvalue`].
336    BarewordRvalue(u16),
337    /// Throw `PerlError::runtime` with the message at constant pool index `u16`. Used by the compiler
338    /// to hard-reject constructs whose only valid response is the same runtime error that the
339    /// tree-walker produces (e.g. `++@$r`, `%{...}--`) without falling back to the tree path.
340    RuntimeErrorConst(u16),
341    MakeHash(u16), // pop N key-value pairs, push as Hash
342    Range,         // stack: [from, to] → Array
343    /// Scalar `..` / `...` flip-flop (numeric bounds vs `$.` — [`Interpreter::scalar_flipflop_dot_line`]).
344    /// Stack: `[from, to]` (ints); pushes `1` or `0`. `u16` indexes flip-flop slots; `u8` is `1` for `...`
345    /// (exclusive: right bound only after `$.` is strictly past the line where the left bound matched).
346    ScalarFlipFlop(u16, u8),
347    /// Regex `..` / `...` flip-flop: both bounds are pattern literals; tests use `$_` and `$.` like Perl
348    /// (`Interpreter::regex_flip_flop_eval`). Operand order: `slot`, `exclusive`, left pattern, left flags,
349    /// right pattern, right flags (constant pool indices). No stack operands; pushes `0`/`1`.
350    RegexFlipFlop(u16, u8, u16, u16, u16, u16),
351    /// Regex `..` / `...` flip-flop with `eof` as the right operand (no arguments). Left bound matches `$_`;
352    /// right bound is [`Interpreter::eof_without_arg_is_true`] (Perl `eof` in `-n`/`-p`). Operand order:
353    /// `slot`, `exclusive`, left pattern, left flags.
354    RegexEofFlipFlop(u16, u8, u16, u16),
355    /// Regex `..` / `...` with a non-literal right operand (e.g. `m/a/ ... (m/b/ or m/c/)`). Left bound is
356    /// pattern + flags; right is evaluated in boolean context each line (pool index into
357    /// [`Chunk::regex_flip_flop_rhs_expr_entries`] / bytecode ranges). Operand order: `slot`, `exclusive`,
358    /// left pattern, left flags, rhs expr index.
359    RegexFlipFlopExprRhs(u16, u8, u16, u16, u16),
360    /// Regex `..` / `...` with a numeric right operand (Perl: right bound is [`Interpreter::scalar_flipflop_dot_line`]
361    /// vs literal line). Constant pool index holds the RHS line as [`PerlValue::integer`]. Operand order:
362    /// `slot`, `exclusive`, left pattern, left flags, rhs line constant index.
363    RegexFlipFlopDotLineRhs(u16, u8, u16, u16, u16),
364
365    // ── Regex ──
366    /// Match: pattern_const_idx, flags_const_idx, scalar_g, pos_key_name_idx (`u16::MAX` = `$_`);
367    /// stack: string operand → result
368    RegexMatch(u16, u16, bool, u16),
369    /// Substitution `s///`: pattern, replacement, flags constant indices; lvalue index into chunk.
370    /// stack: string (subject from LHS expr) → replacement count
371    RegexSubst(u16, u16, u16, u16),
372    /// Transliterate `tr///`: from, to, flags constant indices; lvalue index into chunk.
373    /// stack: string → transliteration count
374    RegexTransliterate(u16, u16, u16, u16),
375    /// Dynamic `=~` / `!~`: pattern from RHS, subject from LHS; empty flags.
376    /// stack: `[subject, pattern]` (pattern on top) → 0/1; `true` = negate (`!~`).
377    RegexMatchDyn(bool),
378    /// Regex literal as a value (`qr/PAT/FLAGS`) — pattern and flags string pool indices.
379    LoadRegex(u16, u16),
380    /// After [`RegexMatchDyn`] for bare `m//` in `&&` / `||`: pop 0/1; push `""` or `1` (Perl scalar).
381    RegexBoolToScalar,
382    /// `pos $var = EXPR` / `pos = EXPR` (implicit `$_`). Stack: `[value, key]` (key string on top).
383    SetRegexPos,
384
385    // ── Assign helpers ──
386    /// SetScalar that also leaves the value on the stack (for chained assignment)
387    SetScalarKeep(u16),
388    /// `SetScalarKeep` for non-special scalars (see `SetScalarPlain`).
389    SetScalarKeepPlain(u16),
390
391    // ── Block-based operations (u16 = index into chunk.blocks) ──
392    /// map { BLOCK } @list — block_idx; stack: \[list\] → \[mapped\]
393    MapWithBlock(u16),
394    /// flat_map { BLOCK } @list — like [`Op::MapWithBlock`] but peels one ARRAY ref per iteration ([`PerlValue::map_flatten_outputs`])
395    FlatMapWithBlock(u16),
396    /// grep { BLOCK } @list — block_idx; stack: \[list\] → \[filtered\]
397    GrepWithBlock(u16),
398    /// each { BLOCK } @list — block_idx; stack: \[list\] → \[count\]
399    ForEachWithBlock(u16),
400    /// map EXPR, LIST — index into [`Chunk::map_expr_entries`] / [`Chunk::map_expr_bytecode_ranges`];
401    /// stack: \[list\] → \[mapped\]
402    MapWithExpr(u16),
403    /// flat_map EXPR, LIST — same pools as [`Op::MapWithExpr`]; stack: \[list\] → \[mapped\]
404    FlatMapWithExpr(u16),
405    /// grep EXPR, LIST — index into [`Chunk::grep_expr_entries`] / [`Chunk::grep_expr_bytecode_ranges`];
406    /// stack: \[list\] → \[filtered\]
407    GrepWithExpr(u16),
408    /// `group_by { BLOCK } LIST` / `chunk_by { BLOCK } LIST` — consecutive runs where the block’s
409    /// return value stringifies the same as the previous (`str_eq`); stack: \[list\] → \[arrayrefs\]
410    ChunkByWithBlock(u16),
411    /// `group_by EXPR, LIST` / `chunk_by EXPR, LIST` — same as [`Op::ChunkByWithBlock`] but key from
412    /// `EXPR` with `$_` set each iteration; uses [`Chunk::map_expr_entries`].
413    ChunkByWithExpr(u16),
414    /// sort { BLOCK } @list — block_idx; stack: \[list\] → \[sorted\]
415    SortWithBlock(u16),
416    /// sort @list (no block) — stack: \[list\] → \[sorted\]
417    SortNoBlock,
418    /// sort $coderef LIST — stack: \[list, coderef\] (coderef on top); `u8` = wantarray for comparator calls.
419    SortWithCodeComparator(u8),
420    /// `{ $a <=> $b }` (0), `{ $a cmp $b }` (1), `{ $b <=> $a }` (2), `{ $b cmp $a }` (3)
421    SortWithBlockFast(u8),
422    /// `map { $_ * k }` with integer `k` — stack: \[list\] → \[mapped\]
423    MapIntMul(i64),
424    /// `grep { $_ % m == r }` with integer `m` (non-zero), `r` — stack: \[list\] → \[filtered\]
425    GrepIntModEq(i64, i64),
426    /// Parallel sort, same fast modes as [`Op::SortWithBlockFast`].
427    PSortWithBlockFast(u8),
428    /// `chomp` on assignable expr: stack has value → chomped count; uses `chunk.lvalues[idx]`.
429    ChompInPlace(u16),
430    /// `chop` on assignable expr: stack has value → chopped char; uses `chunk.lvalues[idx]`.
431    ChopInPlace(u16),
432    /// Four-arg `substr LHS, OFF, LEN, REPL` — index into [`Chunk::substr_four_arg_entries`]; stack: \[\] → extracted slice string
433    SubstrFourArg(u16),
434    /// `keys EXPR` when `EXPR` is not a bare `%h` — [`Chunk::keys_expr_entries`] /
435    /// [`Chunk::keys_expr_bytecode_ranges`]
436    KeysExpr(u16),
437    /// `values EXPR` when not a bare `%h` — [`Chunk::values_expr_entries`] /
438    /// [`Chunk::values_expr_bytecode_ranges`]
439    ValuesExpr(u16),
440    /// Scalar `keys EXPR` (dynamic) — same pools as [`Op::KeysExpr`].
441    KeysExprScalar(u16),
442    /// Scalar `values EXPR` — same pools as [`Op::ValuesExpr`].
443    ValuesExprScalar(u16),
444    /// `delete EXPR` when not a fast `%h{...}` — index into [`Chunk::delete_expr_entries`]
445    DeleteExpr(u16),
446    /// `exists EXPR` when not a fast `%h{...}` — index into [`Chunk::exists_expr_entries`]
447    ExistsExpr(u16),
448    /// `push EXPR, ...` when not a bare `@name` — [`Chunk::push_expr_entries`]
449    PushExpr(u16),
450    /// `pop EXPR` when not a bare `@name` — [`Chunk::pop_expr_entries`]
451    PopExpr(u16),
452    /// `shift EXPR` when not a bare `@name` — [`Chunk::shift_expr_entries`]
453    ShiftExpr(u16),
454    /// `unshift EXPR, ...` when not a bare `@name` — [`Chunk::unshift_expr_entries`]
455    UnshiftExpr(u16),
456    /// `splice EXPR, ...` when not a bare `@name` — [`Chunk::splice_expr_entries`]
457    SpliceExpr(u16),
458    /// `$var .= expr` — append to scalar string in-place without cloning.
459    /// Stack: \[value_to_append\] → \[resulting_string\]. u16 = name pool index of target scalar.
460    ConcatAppend(u16),
461    /// Slot-indexed `$var .= expr` — avoids frame walking and string comparison.
462    /// Stack: \[value_to_append\] → \[resulting_string\]. u8 = slot index.
463    ConcatAppendSlot(u8),
464    /// Fused `$slot_a += $slot_b` — no stack traffic. Pushes result.
465    AddAssignSlotSlot(u8, u8),
466    /// Fused `$slot_a -= $slot_b` — no stack traffic. Pushes result.
467    SubAssignSlotSlot(u8, u8),
468    /// Fused `$slot_a *= $slot_b` — no stack traffic. Pushes result.
469    MulAssignSlotSlot(u8, u8),
470    /// Fused `if ($slot < INT) goto target` — replaces GetScalarSlot + LoadInt + NumLt + JumpIfFalse.
471    /// (slot, i32_limit, jump_target)
472    SlotLtIntJumpIfFalse(u8, i32, usize),
473    /// Void-context `$slot_a += $slot_b` — no stack push. Replaces AddAssignSlotSlot + Pop.
474    AddAssignSlotSlotVoid(u8, u8),
475    /// Void-context `++$slot` — no stack push. Replaces PreIncSlot + Pop.
476    PreIncSlotVoid(u8),
477    /// Void-context `$slot .= expr` — no stack push. Replaces ConcatAppendSlot + Pop.
478    ConcatAppendSlotVoid(u8),
479    /// Fused loop backedge: `$slot += 1; if $slot < limit jump body_target; else fall through`.
480    ///
481    /// Replaces the trailing `PreIncSlotVoid(s) + Jump(top)` of a C-style `for (my $i=0; $i<N; $i=$i+1)`
482    /// loop whose top op is a `SlotLtIntJumpIfFalse(s, limit, exit)`. The initial iteration still
483    /// goes through the top check; this op handles all subsequent iterations in a single dispatch,
484    /// halving the number of ops per loop trip for the `bench_loop`/`bench_string`/`bench_array` shape.
485    /// (slot, i32_limit, body_target)
486    SlotIncLtIntJumpBack(u8, i32, usize),
487    /// Fused accumulator loop: `while $i < limit { $sum += $i; $i += 1 }` — runs the entire
488    /// remaining counted-sum loop in native Rust, eliminating op dispatch per iteration.
489    ///
490    /// Fused when a `for (my $i = a; $i < N; $i = $i + 1) { $sum += $i }` body compiles down to
491    /// exactly `AddAssignSlotSlotVoid(sum, i) + SlotIncLtIntJumpBack(i, limit, body_target)` with
492    /// `body_target` pointing at the AddAssign — i.e. the body is 1 Perl statement. Both slots are
493    /// left as integers on exit (same coercion as `AddAssignSlotSlotVoid` + `PreIncSlotVoid`).
494    /// (sum_slot, i_slot, i32_limit)
495    AccumSumLoop(u8, u8, i32),
496    /// Fused string-append counted loop: `while $i < limit { $s .= CONST; $i += 1 }` — extends
497    /// the `String` buffer in place once and pushes the literal `(limit - i)` times in a tight
498    /// Rust loop, with `Arc::get_mut` → `reserve` → `push_str`. Falls back to the regular op
499    /// sequence if the slot is not a uniquely-owned heap `String`.
500    ///
501    /// Fused when the loop body is exactly `LoadConst(c) + ConcatAppendSlotVoid(s) +
502    /// SlotIncLtIntJumpBack(i, limit, body_target)` with `body_target` pointing at the `LoadConst`.
503    /// (const_idx, s_slot, i_slot, i32_limit)
504    ConcatConstSlotLoop(u16, u8, u8, i32),
505    /// Fused array-push counted loop: `while $i < limit { push @a, $i; $i += 1 }` — reserves the
506    /// target `Vec` once and pushes `PerlValue::integer(i)` in a tight Rust loop. Emitted when
507    /// the loop body is exactly `GetScalarSlot(i) + PushArray(arr) + ArrayLen(arr) + Pop +
508    /// SlotIncLtIntJumpBack(i, limit, body_target)` with `body_target` pointing at the
509    /// `GetScalarSlot` (i.e. the body is one `push` statement whose return is discarded).
510    /// (arr_name_idx, i_slot, i32_limit)
511    PushIntRangeToArrayLoop(u16, u8, i32),
512    /// Fused hash-insert counted loop: `while $i < limit { $h{$i} = $i * k; $i += 1 }` — runs the
513    /// entire insert loop natively, reserving hash capacity once and writing `(stringified i, i*k)`
514    /// pairs in tight Rust. Emitted when the body is exactly
515    /// `GetScalarSlot(i) + LoadInt(k) + Mul + GetScalarSlot(i) + SetHashElem(h) + Pop +
516    /// SlotIncLtIntJumpBack(i, limit, body_target)` with `body_target` at the first `GetScalarSlot`.
517    /// (hash_name_idx, i_slot, i32_multiplier, i32_limit)
518    SetHashIntTimesLoop(u16, u8, i32, i32),
519    /// Fused `$sum += $h{$k}` body op for the inner loop of `for my $k (keys %h) { $sum += $h{$k} }`.
520    ///
521    /// Replaces the 6-op sequence `GetScalarSlot(sum) + GetScalarPlain(k) + GetHashElem(h) + Add +
522    /// SetScalarSlotKeep(sum) + Pop` with a single dispatch that reads the hash element directly
523    /// into the slot without going through the VM stack. (sum_slot, k_name_idx, h_name_idx)
524    AddHashElemPlainKeyToSlot(u8, u16, u16),
525    /// Like [`Op::AddHashElemPlainKeyToSlot`] but the key variable lives in a slot (`for my $k`
526    /// in slot-mode foreach). Pure slot read + hash lookup + slot write with zero VM stack traffic.
527    /// (sum_slot, k_slot, h_name_idx)
528    AddHashElemSlotKeyToSlot(u8, u8, u16),
529    /// Fused `for my $k (keys %h) { $sum += $h{$k} }` — walks `hash.values()` in a tight native
530    /// loop, accumulating integer or float sums directly into `sum_slot`. Emitted by the
531    /// bytecode-level peephole when the foreach shape + `AddHashElemSlotKeyToSlot` body + slot
532    /// counter/var declarations are detected. `h_name_idx` is the source hash's name pool index.
533    /// (sum_slot, h_name_idx)
534    SumHashValuesToSlot(u8, u16),
535
536    // ── Frame-local scalar slots (O(1) access, no string lookup) ──
537    /// Read scalar from current frame's slot array. u8 = slot index.
538    GetScalarSlot(u8),
539    /// Write scalar to current frame's slot array (pop, discard). u8 = slot index.
540    SetScalarSlot(u8),
541    /// Write scalar to current frame's slot array (pop, keep on stack). u8 = slot index.
542    SetScalarSlotKeep(u8),
543    /// Declare + initialize scalar in current frame's slot array. u8 = slot index; u16 = name pool
544    /// index (bare name) for closure capture.
545    DeclareScalarSlot(u8, u16),
546    /// Read argument from caller's stack region: push stack\[call_frame.stack_base + idx\].
547    /// Avoids @_ allocation + string-based shift for compiled sub argument passing.
548    GetArg(u8),
549    /// `reverse` in list context — stack: \[list\] → \[reversed list\]
550    ReverseListOp,
551    /// `scalar reverse` — stack: \[list\] → concatenated string with chars reversed (Perl).
552    ReverseScalarOp,
553    /// `rev` — smart reverse: single value → char-reverse; multiple values → list-reverse.
554    RevOp,
555    /// Pop TOS (array/list), push `to_list().len()` as integer (Perl `scalar` on map/grep result).
556    StackArrayLen,
557    /// Pop list-slice result array; push last element (Perl `scalar (LIST)[i,...]`).
558    ListSliceToScalar,
559    /// pmap { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[mapped\] (`progress_flag` is 0/1)
560    PMapWithBlock(u16),
561    /// pflat_map { BLOCK } @list — flatten array results; output in **input order**; stack same as [`Op::PMapWithBlock`]
562    PFlatMapWithBlock(u16),
563    /// `pmap_on` / `pflat_map_on` over SSH — stack: \[progress_flag, list, cluster\] → \[mapped\]; `flat` = 1 for flatten
564    PMapRemote {
565        block_idx: u16,
566        flat: u8,
567    },
568    /// puniq LIST — hash-partition parallel distinct (first occurrence order); stack: \[progress_flag, list\] → \[array\]
569    Puniq,
570    /// pfirst { BLOCK } LIST — short-circuit parallel; stack: \[progress_flag, list\] → value or undef
571    PFirstWithBlock(u16),
572    /// pany { BLOCK } LIST — short-circuit parallel; stack: \[progress_flag, list\] → 0/1
573    PAnyWithBlock(u16),
574    /// pmap_chunked N { BLOCK } @list — block_idx; stack: \[progress_flag, chunk_n, list\] → \[mapped\]
575    PMapChunkedWithBlock(u16),
576    /// pgrep { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[filtered\]
577    PGrepWithBlock(u16),
578    /// pfor { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[\]
579    PForWithBlock(u16),
580    /// psort { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[sorted\]
581    PSortWithBlock(u16),
582    /// psort @list (no block) — stack: \[progress_flag, list\] → \[sorted\]
583    PSortNoBlockParallel,
584    /// `reduce { BLOCK } @list` — block_idx; stack: \[list\] → \[accumulator\]
585    ReduceWithBlock(u16),
586    /// `preduce { BLOCK } @list` — block_idx; stack: \[progress_flag, list\] → \[accumulator\]
587    PReduceWithBlock(u16),
588    /// `preduce_init EXPR, { BLOCK } @list` — block_idx; stack: \[progress_flag, list, init\] → \[accumulator\]
589    PReduceInitWithBlock(u16),
590    /// `pmap_reduce { MAP } { REDUCE } @list` — map and reduce block indices; stack: \[progress_flag, list\] → \[scalar\]
591    PMapReduceWithBlocks(u16, u16),
592    /// `pcache { BLOCK } @list` — block_idx; stack: \[progress_flag, list\] → \[array\]
593    PcacheWithBlock(u16),
594    /// `pselect($rx1, ... [, timeout => SECS])` — stack: \[rx0, …, rx_{n-1}\] with optional timeout on top
595    Pselect {
596        n_rx: u8,
597        has_timeout: bool,
598    },
599    /// `par_lines PATH, sub { } [, progress => EXPR]` — index into [`Chunk::par_lines_entries`]; stack: \[\] → `undef`
600    ParLines(u16),
601    /// `par_walk PATH, sub { } [, progress => EXPR]` — index into [`Chunk::par_walk_entries`]; stack: \[\] → `undef`
602    ParWalk(u16),
603    /// `pwatch GLOB, sub { }` — index into [`Chunk::pwatch_entries`]; stack: \[\] → result
604    Pwatch(u16),
605    /// fan N { BLOCK } — block_idx; stack: \[progress_flag, count\] (`progress_flag` is 0/1)
606    FanWithBlock(u16),
607    /// fan { BLOCK } — block_idx; stack: \[progress_flag\]; COUNT = rayon pool size (`stryke -j`)
608    FanWithBlockAuto(u16),
609    /// fan_cap N { BLOCK } — like fan; stack: \[progress_flag, count\] → array of block return values
610    FanCapWithBlock(u16),
611    /// fan_cap { BLOCK } — like fan; stack: \[progress_flag\] → array
612    FanCapWithBlockAuto(u16),
613    /// `do { BLOCK }` — block_idx + wantarray byte ([`crate::interpreter::WantarrayCtx::as_byte`]);
614    /// stack: \[\] → result
615    EvalBlock(u16, u8),
616    /// `trace { BLOCK }` — block_idx; stack: \[\] → block value (stderr tracing for mysync mutations)
617    TraceBlock(u16),
618    /// `timer { BLOCK }` — block_idx; stack: \[\] → elapsed ms as float
619    TimerBlock(u16),
620    /// `bench { BLOCK } N` — block_idx; stack: \[iterations\] → benchmark summary string
621    BenchBlock(u16),
622    /// `given (EXPR) { when ... default ... }` — [`Chunk::given_entries`] /
623    /// [`Chunk::given_topic_bytecode_ranges`]; stack: \[\] → topic result
624    Given(u16),
625    /// `eval_timeout SECS { ... }` — index into [`Chunk::eval_timeout_entries`] /
626    /// [`Chunk::eval_timeout_expr_bytecode_ranges`]; stack: \[\] → block value
627    EvalTimeout(u16),
628    /// Algebraic `match (SUBJECT) { ... }` — [`Chunk::algebraic_match_entries`] /
629    /// [`Chunk::algebraic_match_subject_bytecode_ranges`]; stack: \[\] → arm value
630    AlgebraicMatch(u16),
631    /// `async { BLOCK }` / `spawn { BLOCK }` — block_idx; stack: \[\] → AsyncTask
632    AsyncBlock(u16),
633    /// `await EXPR` — stack: \[value\] → result
634    Await,
635    /// `__SUB__` — push reference to currently executing sub (for anonymous recursion).
636    LoadCurrentSub,
637    /// `defer { BLOCK }` — register a block to run when the current scope exits.
638    /// Stack: `[coderef]` → `[]`. The coderef is pushed to the frame's defer list.
639    DeferBlock,
640    /// Make a scalar reference from TOS (copies value into a new `RwLock`).
641    MakeScalarRef,
642    /// `\$name` when `name` is a plain scalar variable — ref aliases the live binding (same as tree `scalar_binding_ref`).
643    MakeScalarBindingRef(u16),
644    /// `\@name` — ref aliases the live array in scope (name pool index, stash-qualified like [`Op::GetArray`]).
645    MakeArrayBindingRef(u16),
646    /// `\%name` — ref aliases the live hash in scope.
647    MakeHashBindingRef(u16),
648    /// `\@{ EXPR }` after `EXPR` is on the stack — ARRAY ref aliasing the same storage as Perl (ref to existing ref or package array).
649    MakeArrayRefAlias,
650    /// `\%{ EXPR }` — HASH ref alias (same semantics as [`Op::MakeArrayRefAlias`] for hashes).
651    MakeHashRefAlias,
652    /// Make an array reference from TOS (which should be an Array)
653    MakeArrayRef,
654    /// Make a hash reference from TOS (which should be a Hash)
655    MakeHashRef,
656    /// Make an anonymous sub from a block — block_idx; stack: \[\] → CodeRef
657    /// Anonymous `sub` / coderef: block pool index + [`Chunk::code_ref_sigs`] index (may be empty vec).
658    MakeCodeRef(u16, u16),
659    /// Push a code reference to a named sub (`\&foo`) — name pool index; resolves at run time.
660    LoadNamedSubRef(u16),
661    /// `\&{ EXPR }` — stack: \[sub name string\] → code ref (resolves at run time).
662    LoadDynamicSubRef,
663    /// `*{ EXPR }` — stack: \[stash / glob name string\] → resolved handle string (IO alias map + identity).
664    LoadDynamicTypeglob,
665    /// `*lhs = *rhs` — copy stash slots (sub, scalar, array, hash, IO alias); name pool indices for both sides.
666    CopyTypeglobSlots(u16, u16),
667    /// `*name = $coderef` — stack: pop value, install subroutine in typeglob, push value back (assignment result).
668    TypeglobAssignFromValue(u16),
669    /// `*{LHS} = $coderef` — stack: pop value, pop LHS glob name string, install sub, push value back.
670    TypeglobAssignFromValueDynamic,
671    /// `*{LHS} = *rhs` — stack: pop LHS glob name string; RHS name is pool index; copies stash like [`Op::CopyTypeglobSlots`].
672    CopyTypeglobSlotsDynamicLhs(u16),
673    /// Symbolic deref (`$$r`, `@{...}`, `%{...}`, `*{...}`): stack: \[ref or name value\] → result.
674    /// Byte: `0` = [`crate::ast::Sigil::Scalar`], `1` = Array, `2` = Hash, `3` = Typeglob.
675    SymbolicDeref(u8),
676    /// Dereference arrow: ->\[\] — stack: \[ref, index\] → value
677    ArrowArray,
678    /// Dereference arrow: ->{} — stack: \[ref, key\] → value
679    ArrowHash,
680    /// Assign to `->{}`: stack: \[value, ref, key\] (key on top) — consumes three values.
681    SetArrowHash,
682    /// Assign to `->[]`: stack: \[value, ref, index\] (index on top) — consumes three values.
683    SetArrowArray,
684    /// Like [`Op::SetArrowArray`] but leaves the assigned value on the stack (for `++$aref->[$i]` value).
685    SetArrowArrayKeep,
686    /// Like [`Op::SetArrowHash`] but leaves the assigned value on the stack (for `++$href->{k}` value).
687    SetArrowHashKeep,
688    /// Postfix `++` / `--` on `->[]`: stack \[ref, index\] (index on top) → old value; mutates slot.
689    /// Byte: `0` = increment, `1` = decrement.
690    ArrowArrayPostfix(u8),
691    /// Postfix `++` / `--` on `->{}`: stack \[ref, key\] (key on top) → old value; mutates slot.
692    /// Byte: `0` = increment, `1` = decrement.
693    ArrowHashPostfix(u8),
694    /// `$$r = $val` — stack: \[value, ref\] (ref on top).
695    SetSymbolicScalarRef,
696    /// Like [`Op::SetSymbolicScalarRef`] but leaves the assigned value on the stack.
697    SetSymbolicScalarRefKeep,
698    /// `@{ EXPR } = LIST` — stack: \[list value, ref-or-name\] (top = ref / package name); delegates to
699    /// [`Interpreter::assign_symbolic_array_ref_deref`](crate::interpreter::Interpreter::assign_symbolic_array_ref_deref).
700    SetSymbolicArrayRef,
701    /// `%{ EXPR } = LIST` — stack: \[list value, ref-or-name\]; pairs from list like `%h = (k => v, …)`.
702    SetSymbolicHashRef,
703    /// `*{ EXPR } = RHS` — stack: \[value, ref-or-name\] (top = symbolic glob name); coderef install or `*lhs = *rhs` copy.
704    SetSymbolicTypeglobRef,
705    /// Postfix `++` / `--` on symbolic scalar ref (`$$r`); stack \[ref\] → old value. Byte: `0` = increment, `1` = decrement.
706    SymbolicScalarRefPostfix(u8),
707    /// Dereference arrow: ->() — stack: \[ref, args_array\] → value
708    /// `$cr->(...)` — wantarray byte (see VM `WantarrayCtx` threading on `Call` / `MethodCall`).
709    ArrowCall(u8),
710    /// Indirect call `$coderef(ARG...)` / `&$coderef(ARG...)` — stack (bottom→top): `target`, then
711    /// `argc` argument values (first arg pushed first). Third byte: `1` = ignore stack args and use
712    /// caller `@_` (`argc` must be `0`).
713    IndirectCall(u8, u8, u8),
714    /// Method call: stack: \[object, args...\] → result; name_idx, argc, wantarray
715    MethodCall(u16, u8, u8),
716    /// Like [`Op::MethodCall`] but uses SUPER / C3 parent chain (see interpreter method resolution for `SUPER`).
717    MethodCallSuper(u16, u8, u8),
718    /// File test: -e, -f, -d, etc. — test char; stack: \[path\] → 0/1
719    FileTestOp(u8),
720
721    // ── try / catch / finally (VM exception handling; see [`VM::try_recover_from_exception`]) ──
722    /// Push a [`crate::vm::TryFrame`]; `catch_ip` / `after_ip` patched via [`Chunk::patch_try_push_catch`]
723    /// / [`Chunk::patch_try_push_after`]; `finally_ip` via [`Chunk::patch_try_push_finally`].
724    TryPush {
725        catch_ip: usize,
726        finally_ip: Option<usize>,
727        after_ip: usize,
728        catch_var_idx: u16,
729    },
730    /// Normal completion from try or catch body (jump to finally or merge).
731    TryContinueNormal,
732    /// End of `finally` block: pop try frame and jump to `after_ip`.
733    TryFinallyEnd,
734    /// Enter catch: consume [`crate::vm::VM::pending_catch_error`], pop try scope, push catch scope, bind `$var`.
735    CatchReceive(u16),
736
737    // ── `mysync` (thread-safe shared bindings; see [`StmtKind::MySync`]) ──
738    /// Stack: `[init]` → `[]`. Declares `${name}` as `PerlValue::atomic` (or deque/heap unwrapped).
739    DeclareMySyncScalar(u16),
740    /// Stack: `[init_list]` → `[]`. Declares `@name` as atomic array.
741    DeclareMySyncArray(u16),
742    /// Stack: `[init_list]` → `[]`. Declares `%name` as atomic hash.
743    DeclareMySyncHash(u16),
744    /// Register [`RuntimeSubDecl`] at index (nested `sub`, including inside `BEGIN`).
745    RuntimeSubDecl(u16),
746    /// `tie $x | @arr | %h, 'Class', ...` — stack bottom = class expr, then user args; `argc` = `1 + args.len()`.
747    /// `target_kind`: 0 = scalar (`TIESCALAR`), 1 = array (`TIEARRAY`), 2 = hash (`TIEHASH`). `name_idx` = bare name.
748    Tie {
749        target_kind: u8,
750        name_idx: u16,
751        argc: u8,
752    },
753    /// `format NAME =` … — index into [`Chunk::format_decls`]; installs into current package at run time.
754    FormatDecl(u16),
755    /// `use overload 'op' => 'method', …` — index into [`Chunk::use_overload_entries`].
756    UseOverload(u16),
757    /// Scalar `$x OP= $rhs` — uses [`Scope::atomic_mutate`] so `mysync` scalars are RMW-safe.
758    /// Stack: `[rhs]` → `[result]`. `op` byte is from [`crate::compiler::scalar_compound_op_to_byte`].
759    ScalarCompoundAssign {
760        name_idx: u16,
761        op: u8,
762    },
763
764    // ── Special ──
765    /// Set `${^GLOBAL_PHASE}` on the interpreter. See [`GP_START`] … [`GP_END`].
766    SetGlobalPhase(u8),
767    Halt,
768
769    // ── Streaming map (appended — do not reorder earlier op tags) ─────────────
770    /// `maps { BLOCK } LIST` — stack: \[list\] → lazy iterator (pull-based; stryke extension).
771    MapsWithBlock(u16),
772    /// `flat_maps { BLOCK } LIST` — like [`Op::MapsWithBlock`] with `flat_map`-style flattening.
773    MapsFlatMapWithBlock(u16),
774    /// `maps EXPR, LIST` — index into [`Chunk::map_expr_entries`]; stack: \[list\] → iterator.
775    MapsWithExpr(u16),
776    /// `flat_maps EXPR, LIST` — same pools as [`Op::MapsWithExpr`].
777    MapsFlatMapWithExpr(u16),
778    /// `filter { BLOCK } LIST` — stack: \[list\] → lazy iterator (stryke; `grep` remains eager).
779    FilterWithBlock(u16),
780    /// `filter EXPR, LIST` — index into [`Chunk::grep_expr_entries`]; stack: \[list\] → iterator.
781    FilterWithExpr(u16),
782}
783
784/// `${^GLOBAL_PHASE}` values emitted with [`Op::SetGlobalPhase`] (matches Perl’s phase strings).
785pub const GP_START: u8 = 0;
786/// Reserved; stock Perl 5 keeps `${^GLOBAL_PHASE}` as **`START`** during `UNITCHECK` blocks.
787pub const GP_UNITCHECK: u8 = 1;
788pub const GP_CHECK: u8 = 2;
789pub const GP_INIT: u8 = 3;
790pub const GP_RUN: u8 = 4;
791pub const GP_END: u8 = 5;
792
793/// Built-in function IDs for CallBuiltin dispatch.
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
795#[repr(u16)]
796pub enum BuiltinId {
797    // String
798    Length = 0,
799    Chomp,
800    Chop,
801    Substr,
802    Index,
803    Rindex,
804    Uc,
805    Lc,
806    Ucfirst,
807    Lcfirst,
808    Chr,
809    Ord,
810    Hex,
811    Oct,
812    Join,
813    Split,
814    Sprintf,
815
816    // Numeric
817    Abs,
818    Int,
819    Sqrt,
820
821    // Type
822    Defined,
823    Ref,
824    Scalar,
825
826    // Array
827    Splice,
828    Reverse,
829    Sort,
830    Unshift,
831
832    // Hash
833
834    // I/O
835    Open,
836    Close,
837    Eof,
838    ReadLine,
839    Printf,
840
841    // System
842    System,
843    Exec,
844    Exit,
845    Die,
846    Warn,
847    Chdir,
848    Mkdir,
849    Unlink,
850
851    // Control
852    Eval,
853    Do,
854    Require,
855
856    // OOP
857    Bless,
858    Caller,
859
860    // Parallel
861    PMap,
862    PGrep,
863    PFor,
864    PSort,
865    Fan,
866
867    // Map/Grep (block-based — need special handling)
868    MapBlock,
869    GrepBlock,
870    SortBlock,
871
872    // Math (appended — do not reorder earlier IDs)
873    Sin,
874    Cos,
875    Atan2,
876    Exp,
877    Log,
878    Rand,
879    Srand,
880
881    // String (appended)
882    Crypt,
883    Fc,
884    Pos,
885    Study,
886
887    Stat,
888    Lstat,
889    Link,
890    Symlink,
891    Readlink,
892    Glob,
893
894    Opendir,
895    Readdir,
896    Closedir,
897    Rewinddir,
898    Telldir,
899    Seekdir,
900    /// Read entire file as UTF-8 (`slurp $path`).
901    Slurp,
902    /// Blocking HTTP GET (`fetch_url $url`).
903    FetchUrl,
904    /// `pchannel()` — `(tx, rx)` as a two-element list.
905    Pchannel,
906    /// Parallel recursive glob (`glob_par`).
907    GlobPar,
908    /// `deque()` — empty deque.
909    DequeNew,
910    /// `heap(sub { })` — empty heap with comparator.
911    HeapNew,
912    /// `pipeline(...)` — lazy iterator (filter/map/take/collect).
913    Pipeline,
914    /// `capture("cmd")` — structured stdout/stderr/exit (via `sh -c`).
915    Capture,
916    /// `ppool(N)` — persistent thread pool (`submit` / `collect`).
917    Ppool,
918    /// Scalar/list context query (`wantarray`).
919    Wantarray,
920    /// `rename OLD, NEW`
921    Rename,
922    /// `chmod MODE, ...`
923    Chmod,
924    /// `chown UID, GID, ...`
925    Chown,
926    /// `pselect($rx1, $rx2, ...)` — multiplexed recv; returns `(value, index)`.
927    Pselect,
928    /// `barrier(N)` — thread barrier (`->wait`).
929    BarrierNew,
930    /// `par_pipeline(...)` — list form: same as `pipeline` but parallel `filter`/`map` on `collect()`.
931    ParPipeline,
932    /// `glob_par(..., progress => EXPR)` — last stack arg is truthy progress flag.
933    GlobParProgress,
934    /// `par_pipeline_stream(...)` — streaming pipeline with bounded channels between stages.
935    ParPipelineStream,
936    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file.
937    ParSed,
938    /// `par_sed(..., progress => EXPR)` — last stack arg is truthy progress flag.
939    ParSedProgress,
940    /// `each EXPR` — matches tree interpreter (returns empty list).
941    Each,
942    /// `` `cmd` `` / `qx{...}` — stdout string via `sh -c` (Perl readpipe); sets `$?`.
943    Readpipe,
944    /// `readline` / `<HANDLE>` in **list** context — all remaining lines until EOF (Perl `readline` list semantics).
945    ReadLineList,
946    /// `readdir` in **list** context — all names not yet returned (Perl drains the rest of the stream).
947    ReaddirList,
948    /// `ssh HOST, CMD, …` / `ssh(HOST, …)` — `execvp` style `ssh` only (no shell).
949    Ssh,
950    /// `rmdir LIST` — remove empty directories; returns count removed (appended ID).
951    Rmdir,
952    /// `utime ATIME, MTIME, LIST` — set access/mod times (Unix).
953    Utime,
954    /// `umask EXPR` / `umask()` — process file mode creation mask (Unix).
955    Umask,
956    /// `getcwd` / `Cwd::getcwd` / `CORE::getcwd`.
957    Getcwd,
958    /// `pipe READHANDLE, WRITEHANDLE` — OS pipe ends (Unix).
959    Pipe,
960    /// `files` / `files DIR` — list file names in a directory (default: `.`).
961    Files,
962    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
963    Filesf,
964    /// `fr DIR` — list only regular file names recursively (default: `.`).
965    FilesfRecursive,
966    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
967    Dirs,
968    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
969    DirsRecursive,
970    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
971    SymLinks,
972    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
973    Sockets,
974    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
975    Pipes,
976    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
977    BlockDevices,
978    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
979    CharDevices,
980}
981
982impl BuiltinId {
983    pub fn from_u16(v: u16) -> Option<Self> {
984        if v <= Self::CharDevices as u16 {
985            Some(unsafe { std::mem::transmute::<u16, BuiltinId>(v) })
986        } else {
987            None
988        }
989    }
990}
991
992/// A compiled chunk of bytecode with its constant pools.
993#[derive(Debug, Clone, Serialize, Deserialize)]
994pub struct Chunk {
995    pub ops: Vec<Op>,
996    /// Constant pool: string literals, regex patterns, etc.
997    #[serde(with = "crate::pec::constants_pool_codec")]
998    pub constants: Vec<PerlValue>,
999    /// Name pool: variable names, sub names (interned/deduped).
1000    pub names: Vec<String>,
1001    /// Source line for each op (parallel array for error reporting).
1002    pub lines: Vec<usize>,
1003    /// Optional link from each op to the originating [`Expr`] (pool index into [`Self::ast_expr_pool`]).
1004    /// Filled for ops emitted from [`crate::compiler::Compiler::compile_expr_ctx`]; other paths leave `None`.
1005    pub op_ast_expr: Vec<Option<u32>>,
1006    /// Interned [`Expr`] nodes referenced by [`Self::op_ast_expr`] (for debugging / tooling).
1007    pub ast_expr_pool: Vec<Expr>,
1008    /// Compiled subroutine entry points: (name_index, op_index, uses_stack_args).
1009    /// When `uses_stack_args` is true, the Call op leaves arguments on the value
1010    /// stack and the sub reads them via `GetArg(idx)` instead of `shift @_`.
1011    pub sub_entries: Vec<(u16, usize, bool)>,
1012    /// AST blocks for map/grep/sort/parallel operations.
1013    /// Referenced by block-based opcodes via u16 index.
1014    pub blocks: Vec<Block>,
1015    /// When `Some((start, end))`, `blocks[i]` is also lowered to `ops[start..end]` (exclusive `end`)
1016    /// with trailing [`Op::BlockReturnValue`]. VM uses opcodes; otherwise the AST in `blocks[i]`.
1017    pub block_bytecode_ranges: Vec<Option<(usize, usize)>>,
1018    /// Resolved [`Op::CallStaticSubId`] targets: subroutine entry IP, stack-args calling convention,
1019    /// and stash name pool index (qualified key matching [`Interpreter::subs`]).
1020    pub static_sub_calls: Vec<(usize, bool, u16)>,
1021    /// Assign targets for `s///` / `tr///` bytecode (LHS expressions).
1022    pub lvalues: Vec<Expr>,
1023    /// `struct Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1024    pub struct_defs: Vec<StructDef>,
1025    /// `enum Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1026    pub enum_defs: Vec<EnumDef>,
1027    /// `class Name extends ... impl ... { ... }` definitions.
1028    pub class_defs: Vec<ClassDef>,
1029    /// `trait Name { ... }` definitions.
1030    pub trait_defs: Vec<TraitDef>,
1031    /// `given (topic) { body }` — topic expression + body (when/default handled by interpreter).
1032    pub given_entries: Vec<(Expr, Block)>,
1033    /// When `Some((start, end))`, `given_entries[i].0` (topic) is lowered to `ops[start..end]` +
1034    /// [`Op::BlockReturnValue`].
1035    pub given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
1036    /// `eval_timeout timeout_expr { body }` — evaluated at runtime.
1037    pub eval_timeout_entries: Vec<(Expr, Block)>,
1038    /// When `Some((start, end))`, `eval_timeout_entries[i].0` (timeout expr) is lowered to
1039    /// `ops[start..end]` with trailing [`Op::BlockReturnValue`].
1040    pub eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1041    /// Algebraic `match (subject) { arms }`.
1042    pub algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
1043    /// When `Some((start, end))`, `algebraic_match_entries[i].0` (subject) is lowered to
1044    /// `ops[start..end]` + [`Op::BlockReturnValue`].
1045    pub algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
1046    /// Nested / runtime `sub` declarations (see [`Op::RuntimeSubDecl`]).
1047    pub runtime_sub_decls: Vec<RuntimeSubDecl>,
1048    /// Stryke `sub ($a, …)` / hash-destruct params for [`Op::MakeCodeRef`] (second operand is pool index).
1049    pub code_ref_sigs: Vec<Vec<SubSigParam>>,
1050    /// `par_lines PATH, sub { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1051    pub par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
1052    /// `par_walk PATH, sub { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1053    pub par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
1054    /// `pwatch GLOB, sub { }` — evaluated by interpreter inside VM.
1055    pub pwatch_entries: Vec<(Expr, Expr)>,
1056    /// `substr $var, OFF, LEN, REPL` — four-arg form (mutates `LHS`); evaluated by interpreter inside VM.
1057    pub substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
1058    /// `keys EXPR` when `EXPR` is not bare `%h`.
1059    pub keys_expr_entries: Vec<Expr>,
1060    /// When `Some((start, end))`, `keys_expr_entries[i]` is lowered to `ops[start..end]` +
1061    /// [`Op::BlockReturnValue`] (operand only; [`Op::KeysExpr`] still applies `keys` to the value).
1062    pub keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1063    /// `values EXPR` when not bare `%h`.
1064    pub values_expr_entries: Vec<Expr>,
1065    pub values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1066    /// `delete EXPR` when not the fast `%h{k}` lowering.
1067    pub delete_expr_entries: Vec<Expr>,
1068    /// `exists EXPR` when not the fast `%h{k}` lowering.
1069    pub exists_expr_entries: Vec<Expr>,
1070    /// `push` when the array operand is not a bare `@name` (e.g. `push $aref, ...`).
1071    pub push_expr_entries: Vec<(Expr, Vec<Expr>)>,
1072    pub pop_expr_entries: Vec<Expr>,
1073    pub shift_expr_entries: Vec<Expr>,
1074    pub unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
1075    pub splice_expr_entries: Vec<SpliceExprEntry>,
1076    /// `map EXPR, LIST` — map expression (list context) with `$_` set to each element.
1077    pub map_expr_entries: Vec<Expr>,
1078    /// When `Some((start, end))`, `map_expr_entries[i]` is lowered like [`Self::grep_expr_bytecode_ranges`].
1079    pub map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1080    /// `grep EXPR, LIST` — filter expression evaluated with `$_` set to each element.
1081    pub grep_expr_entries: Vec<Expr>,
1082    /// When `Some((start, end))`, `grep_expr_entries[i]` is also lowered to `ops[start..end]`
1083    /// (exclusive `end`) with trailing [`Op::BlockReturnValue`], like [`Self::block_bytecode_ranges`].
1084    pub grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1085    /// Right-hand expression for [`Op::RegexFlipFlopExprRhs`] — boolean context (bare `m//` is `$_ =~ m//`).
1086    pub regex_flip_flop_rhs_expr_entries: Vec<Expr>,
1087    /// When `Some((start, end))`, `regex_flip_flop_rhs_expr_entries[i]` is lowered to `ops[start..end]` +
1088    /// [`Op::BlockReturnValue`].
1089    pub regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1090    /// Number of flip-flop slots ([`Op::ScalarFlipFlop`], [`Op::RegexFlipFlop`], [`Op::RegexEofFlipFlop`],
1091    /// [`Op::RegexFlipFlopExprRhs`], [`Op::RegexFlipFlopDotLineRhs`]); VM resets flip-flop vectors.
1092    pub flip_flop_slots: u16,
1093    /// `format NAME =` bodies: basename + lines between `=` and `.` (see lexer).
1094    pub format_decls: Vec<(String, Vec<String>)>,
1095    /// `use overload` pair lists (installed into current package at run time).
1096    pub use_overload_entries: Vec<Vec<(String, String)>>,
1097}
1098
1099impl Chunk {
1100    /// Look up a compiled subroutine entry by stash name pool index.
1101    pub fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
1102        self.sub_entries
1103            .iter()
1104            .find(|(n, _, _)| *n == name_idx)
1105            .map(|(_, ip, stack_args)| (*ip, *stack_args))
1106    }
1107
1108    pub fn new() -> Self {
1109        Self {
1110            ops: Vec::with_capacity(256),
1111            constants: Vec::new(),
1112            names: Vec::new(),
1113            lines: Vec::new(),
1114            op_ast_expr: Vec::new(),
1115            ast_expr_pool: Vec::new(),
1116            sub_entries: Vec::new(),
1117            blocks: Vec::new(),
1118            block_bytecode_ranges: Vec::new(),
1119            static_sub_calls: Vec::new(),
1120            lvalues: Vec::new(),
1121            struct_defs: Vec::new(),
1122            enum_defs: Vec::new(),
1123            class_defs: Vec::new(),
1124            trait_defs: Vec::new(),
1125            given_entries: Vec::new(),
1126            given_topic_bytecode_ranges: Vec::new(),
1127            eval_timeout_entries: Vec::new(),
1128            eval_timeout_expr_bytecode_ranges: Vec::new(),
1129            algebraic_match_entries: Vec::new(),
1130            algebraic_match_subject_bytecode_ranges: Vec::new(),
1131            runtime_sub_decls: Vec::new(),
1132            code_ref_sigs: Vec::new(),
1133            par_lines_entries: Vec::new(),
1134            par_walk_entries: Vec::new(),
1135            pwatch_entries: Vec::new(),
1136            substr_four_arg_entries: Vec::new(),
1137            keys_expr_entries: Vec::new(),
1138            keys_expr_bytecode_ranges: Vec::new(),
1139            values_expr_entries: Vec::new(),
1140            values_expr_bytecode_ranges: Vec::new(),
1141            delete_expr_entries: Vec::new(),
1142            exists_expr_entries: Vec::new(),
1143            push_expr_entries: Vec::new(),
1144            pop_expr_entries: Vec::new(),
1145            shift_expr_entries: Vec::new(),
1146            unshift_expr_entries: Vec::new(),
1147            splice_expr_entries: Vec::new(),
1148            map_expr_entries: Vec::new(),
1149            map_expr_bytecode_ranges: Vec::new(),
1150            grep_expr_entries: Vec::new(),
1151            grep_expr_bytecode_ranges: Vec::new(),
1152            regex_flip_flop_rhs_expr_entries: Vec::new(),
1153            regex_flip_flop_rhs_expr_bytecode_ranges: Vec::new(),
1154            flip_flop_slots: 0,
1155            format_decls: Vec::new(),
1156            use_overload_entries: Vec::new(),
1157        }
1158    }
1159
1160    /// Pool index for [`Op::FormatDecl`].
1161    pub fn add_format_decl(&mut self, name: String, lines: Vec<String>) -> u16 {
1162        let idx = self.format_decls.len() as u16;
1163        self.format_decls.push((name, lines));
1164        idx
1165    }
1166
1167    /// Pool index for [`Op::UseOverload`].
1168    pub fn add_use_overload(&mut self, pairs: Vec<(String, String)>) -> u16 {
1169        let idx = self.use_overload_entries.len() as u16;
1170        self.use_overload_entries.push(pairs);
1171        idx
1172    }
1173
1174    /// Allocate a slot index for [`Op::ScalarFlipFlop`] / [`Op::RegexFlipFlop`] / [`Op::RegexEofFlipFlop`] /
1175    /// [`Op::RegexFlipFlopExprRhs`] / [`Op::RegexFlipFlopDotLineRhs`] flip-flop state.
1176    pub fn alloc_flip_flop_slot(&mut self) -> u16 {
1177        let id = self.flip_flop_slots;
1178        self.flip_flop_slots = self.flip_flop_slots.saturating_add(1);
1179        id
1180    }
1181
1182    /// `map EXPR, LIST` — pool index for [`Op::MapWithExpr`].
1183    pub fn add_map_expr_entry(&mut self, expr: Expr) -> u16 {
1184        let idx = self.map_expr_entries.len() as u16;
1185        self.map_expr_entries.push(expr);
1186        idx
1187    }
1188
1189    /// `grep EXPR, LIST` — pool index for [`Op::GrepWithExpr`].
1190    pub fn add_grep_expr_entry(&mut self, expr: Expr) -> u16 {
1191        let idx = self.grep_expr_entries.len() as u16;
1192        self.grep_expr_entries.push(expr);
1193        idx
1194    }
1195
1196    /// Regex flip-flop with compound RHS — pool index for [`Op::RegexFlipFlopExprRhs`].
1197    pub fn add_regex_flip_flop_rhs_expr_entry(&mut self, expr: Expr) -> u16 {
1198        let idx = self.regex_flip_flop_rhs_expr_entries.len() as u16;
1199        self.regex_flip_flop_rhs_expr_entries.push(expr);
1200        idx
1201    }
1202
1203    /// `keys EXPR` (dynamic) — pool index for [`Op::KeysExpr`].
1204    pub fn add_keys_expr_entry(&mut self, expr: Expr) -> u16 {
1205        let idx = self.keys_expr_entries.len() as u16;
1206        self.keys_expr_entries.push(expr);
1207        idx
1208    }
1209
1210    /// `values EXPR` (dynamic) — pool index for [`Op::ValuesExpr`].
1211    pub fn add_values_expr_entry(&mut self, expr: Expr) -> u16 {
1212        let idx = self.values_expr_entries.len() as u16;
1213        self.values_expr_entries.push(expr);
1214        idx
1215    }
1216
1217    /// `delete EXPR` (dynamic operand) — pool index for [`Op::DeleteExpr`].
1218    pub fn add_delete_expr_entry(&mut self, expr: Expr) -> u16 {
1219        let idx = self.delete_expr_entries.len() as u16;
1220        self.delete_expr_entries.push(expr);
1221        idx
1222    }
1223
1224    /// `exists EXPR` (dynamic operand) — pool index for [`Op::ExistsExpr`].
1225    pub fn add_exists_expr_entry(&mut self, expr: Expr) -> u16 {
1226        let idx = self.exists_expr_entries.len() as u16;
1227        self.exists_expr_entries.push(expr);
1228        idx
1229    }
1230
1231    pub fn add_push_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1232        let idx = self.push_expr_entries.len() as u16;
1233        self.push_expr_entries.push((array, values));
1234        idx
1235    }
1236
1237    pub fn add_pop_expr_entry(&mut self, array: Expr) -> u16 {
1238        let idx = self.pop_expr_entries.len() as u16;
1239        self.pop_expr_entries.push(array);
1240        idx
1241    }
1242
1243    pub fn add_shift_expr_entry(&mut self, array: Expr) -> u16 {
1244        let idx = self.shift_expr_entries.len() as u16;
1245        self.shift_expr_entries.push(array);
1246        idx
1247    }
1248
1249    pub fn add_unshift_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1250        let idx = self.unshift_expr_entries.len() as u16;
1251        self.unshift_expr_entries.push((array, values));
1252        idx
1253    }
1254
1255    pub fn add_splice_expr_entry(
1256        &mut self,
1257        array: Expr,
1258        offset: Option<Expr>,
1259        length: Option<Expr>,
1260        replacement: Vec<Expr>,
1261    ) -> u16 {
1262        let idx = self.splice_expr_entries.len() as u16;
1263        self.splice_expr_entries
1264            .push((array, offset, length, replacement));
1265        idx
1266    }
1267
1268    /// Four-arg `substr` — returns pool index for [`Op::SubstrFourArg`].
1269    pub fn add_substr_four_arg_entry(
1270        &mut self,
1271        string: Expr,
1272        offset: Expr,
1273        length: Option<Expr>,
1274        replacement: Expr,
1275    ) -> u16 {
1276        let idx = self.substr_four_arg_entries.len() as u16;
1277        self.substr_four_arg_entries
1278            .push((string, offset, length, replacement));
1279        idx
1280    }
1281
1282    /// `par_lines PATH, sub { } [, progress => EXPR]` — returns pool index for [`Op::ParLines`].
1283    pub fn add_par_lines_entry(
1284        &mut self,
1285        path: Expr,
1286        callback: Expr,
1287        progress: Option<Expr>,
1288    ) -> u16 {
1289        let idx = self.par_lines_entries.len() as u16;
1290        self.par_lines_entries.push((path, callback, progress));
1291        idx
1292    }
1293
1294    /// `par_walk PATH, sub { } [, progress => EXPR]` — returns pool index for [`Op::ParWalk`].
1295    pub fn add_par_walk_entry(
1296        &mut self,
1297        path: Expr,
1298        callback: Expr,
1299        progress: Option<Expr>,
1300    ) -> u16 {
1301        let idx = self.par_walk_entries.len() as u16;
1302        self.par_walk_entries.push((path, callback, progress));
1303        idx
1304    }
1305
1306    /// `pwatch GLOB, sub { }` — returns pool index for [`Op::Pwatch`].
1307    pub fn add_pwatch_entry(&mut self, path: Expr, callback: Expr) -> u16 {
1308        let idx = self.pwatch_entries.len() as u16;
1309        self.pwatch_entries.push((path, callback));
1310        idx
1311    }
1312
1313    /// `given (EXPR) { ... }` — returns pool index for [`Op::Given`].
1314    pub fn add_given_entry(&mut self, topic: Expr, body: Block) -> u16 {
1315        let idx = self.given_entries.len() as u16;
1316        self.given_entries.push((topic, body));
1317        idx
1318    }
1319
1320    /// `eval_timeout SECS { ... }` — returns pool index for [`Op::EvalTimeout`].
1321    pub fn add_eval_timeout_entry(&mut self, timeout: Expr, body: Block) -> u16 {
1322        let idx = self.eval_timeout_entries.len() as u16;
1323        self.eval_timeout_entries.push((timeout, body));
1324        idx
1325    }
1326
1327    /// Algebraic `match` — returns pool index for [`Op::AlgebraicMatch`].
1328    pub fn add_algebraic_match_entry(&mut self, subject: Expr, arms: Vec<MatchArm>) -> u16 {
1329        let idx = self.algebraic_match_entries.len() as u16;
1330        self.algebraic_match_entries.push((subject, arms));
1331        idx
1332    }
1333
1334    /// Store an AST block and return its index.
1335    pub fn add_block(&mut self, block: Block) -> u16 {
1336        let idx = self.blocks.len() as u16;
1337        self.blocks.push(block);
1338        idx
1339    }
1340
1341    /// Pool index for [`Op::MakeCodeRef`] signature (`stryke` extension); use empty vec for legacy `sub { }`.
1342    pub fn add_code_ref_sig(&mut self, params: Vec<SubSigParam>) -> u16 {
1343        let idx = self.code_ref_sigs.len();
1344        if idx > u16::MAX as usize {
1345            panic!("too many anonymous sub signatures in one chunk");
1346        }
1347        self.code_ref_sigs.push(params);
1348        idx as u16
1349    }
1350
1351    /// Store an assignable expression (LHS of `s///` / `tr///`) and return its index.
1352    pub fn add_lvalue_expr(&mut self, e: Expr) -> u16 {
1353        let idx = self.lvalues.len() as u16;
1354        self.lvalues.push(e);
1355        idx
1356    }
1357
1358    /// Intern a name, returning its pool index.
1359    pub fn intern_name(&mut self, name: &str) -> u16 {
1360        if let Some(idx) = self.names.iter().position(|n| n == name) {
1361            return idx as u16;
1362        }
1363        let idx = self.names.len() as u16;
1364        self.names.push(name.to_string());
1365        idx
1366    }
1367
1368    /// Add a constant to the pool, returning its index.
1369    pub fn add_constant(&mut self, val: PerlValue) -> u16 {
1370        // Dedup string constants
1371        if let Some(ref s) = val.as_str() {
1372            for (i, c) in self.constants.iter().enumerate() {
1373                if let Some(cs) = c.as_str() {
1374                    if cs == *s {
1375                        return i as u16;
1376                    }
1377                }
1378            }
1379        }
1380        let idx = self.constants.len() as u16;
1381        self.constants.push(val);
1382        idx
1383    }
1384
1385    /// Append an op with source line info.
1386    #[inline]
1387    pub fn emit(&mut self, op: Op, line: usize) -> usize {
1388        self.emit_with_ast_idx(op, line, None)
1389    }
1390
1391    /// Like [`Self::emit`] but attach an optional interned AST [`Expr`] pool index (see [`Self::op_ast_expr`]).
1392    #[inline]
1393    pub fn emit_with_ast_idx(&mut self, op: Op, line: usize, ast: Option<u32>) -> usize {
1394        let idx = self.ops.len();
1395        self.ops.push(op);
1396        self.lines.push(line);
1397        self.op_ast_expr.push(ast);
1398        idx
1399    }
1400
1401    /// Resolve the originating expression for an instruction pointer, if recorded.
1402    #[inline]
1403    pub fn ast_expr_at(&self, ip: usize) -> Option<&Expr> {
1404        let id = (*self.op_ast_expr.get(ip)?)?;
1405        self.ast_expr_pool.get(id as usize)
1406    }
1407
1408    /// Patch a jump instruction at `idx` to target the current position.
1409    pub fn patch_jump_here(&mut self, idx: usize) {
1410        let target = self.ops.len();
1411        self.patch_jump_to(idx, target);
1412    }
1413
1414    /// Patch a jump instruction at `idx` to target an explicit op address.
1415    pub fn patch_jump_to(&mut self, idx: usize, target: usize) {
1416        match &mut self.ops[idx] {
1417            Op::Jump(ref mut t)
1418            | Op::JumpIfTrue(ref mut t)
1419            | Op::JumpIfFalse(ref mut t)
1420            | Op::JumpIfFalseKeep(ref mut t)
1421            | Op::JumpIfTrueKeep(ref mut t)
1422            | Op::JumpIfDefinedKeep(ref mut t) => *t = target,
1423            _ => panic!("patch_jump_to on non-jump op at {}", idx),
1424        }
1425    }
1426
1427    pub fn patch_try_push_catch(&mut self, idx: usize, catch_ip: usize) {
1428        match &mut self.ops[idx] {
1429            Op::TryPush { catch_ip: c, .. } => *c = catch_ip,
1430            _ => panic!("patch_try_push_catch on non-TryPush op at {}", idx),
1431        }
1432    }
1433
1434    pub fn patch_try_push_finally(&mut self, idx: usize, finally_ip: Option<usize>) {
1435        match &mut self.ops[idx] {
1436            Op::TryPush { finally_ip: f, .. } => *f = finally_ip,
1437            _ => panic!("patch_try_push_finally on non-TryPush op at {}", idx),
1438        }
1439    }
1440
1441    pub fn patch_try_push_after(&mut self, idx: usize, after_ip: usize) {
1442        match &mut self.ops[idx] {
1443            Op::TryPush { after_ip: a, .. } => *a = after_ip,
1444            _ => panic!("patch_try_push_after on non-TryPush op at {}", idx),
1445        }
1446    }
1447
1448    /// Current op count (next emit position).
1449    #[inline]
1450    pub fn len(&self) -> usize {
1451        self.ops.len()
1452    }
1453
1454    #[inline]
1455    pub fn is_empty(&self) -> bool {
1456        self.ops.is_empty()
1457    }
1458
1459    /// Human-readable listing: subroutine entry points and each op with its source line (javap / `dis`-style).
1460    pub fn disassemble(&self) -> String {
1461        use std::fmt::Write;
1462        let mut out = String::new();
1463        for (i, n) in self.names.iter().enumerate() {
1464            let _ = writeln!(out, "; name[{}] = {}", i, n);
1465        }
1466        let _ = writeln!(out, "; sub_entries:");
1467        for (ni, ip, stack_args) in &self.sub_entries {
1468            let name = self
1469                .names
1470                .get(*ni as usize)
1471                .map(|s| s.as_str())
1472                .unwrap_or("?");
1473            let _ = writeln!(out, ";   {} @ {} stack_args={}", name, ip, stack_args);
1474        }
1475        for (i, op) in self.ops.iter().enumerate() {
1476            let line = self.lines.get(i).copied().unwrap_or(0);
1477            let ast = self
1478                .op_ast_expr
1479                .get(i)
1480                .copied()
1481                .flatten()
1482                .map(|id| id.to_string())
1483                .unwrap_or_else(|| "-".into());
1484            let _ = writeln!(out, "{:04} {:>5} {:>6}  {:?}", i, line, ast, op);
1485        }
1486        out
1487    }
1488
1489    /// Peephole pass: fuse common multi-op sequences into single superinstructions,
1490    /// then compact by removing Nop slots and remapping all jump targets.
1491    pub fn peephole_fuse(&mut self) {
1492        let len = self.ops.len();
1493        if len < 2 {
1494            return;
1495        }
1496        // Pass 1: fuse OP + Pop → OPVoid
1497        let mut i = 0;
1498        while i + 1 < len {
1499            if matches!(self.ops[i + 1], Op::Pop) {
1500                let replacement = match &self.ops[i] {
1501                    Op::AddAssignSlotSlot(d, s) => Some(Op::AddAssignSlotSlotVoid(*d, *s)),
1502                    Op::PreIncSlot(s) => Some(Op::PreIncSlotVoid(*s)),
1503                    Op::ConcatAppendSlot(s) => Some(Op::ConcatAppendSlotVoid(*s)),
1504                    _ => None,
1505                };
1506                if let Some(op) = replacement {
1507                    self.ops[i] = op;
1508                    self.ops[i + 1] = Op::Nop;
1509                    i += 2;
1510                    continue;
1511                }
1512            }
1513            i += 1;
1514        }
1515        // Pass 2: fuse multi-op patterns
1516        let len = self.ops.len();
1517        if len >= 4 {
1518            i = 0;
1519            while i + 3 < len {
1520                if let (
1521                    Op::GetScalarSlot(slot),
1522                    Op::LoadInt(n),
1523                    Op::NumLt,
1524                    Op::JumpIfFalse(target),
1525                ) = (
1526                    &self.ops[i],
1527                    &self.ops[i + 1],
1528                    &self.ops[i + 2],
1529                    &self.ops[i + 3],
1530                ) {
1531                    if let Ok(n32) = i32::try_from(*n) {
1532                        let slot = *slot;
1533                        let target = *target;
1534                        self.ops[i] = Op::SlotLtIntJumpIfFalse(slot, n32, target);
1535                        self.ops[i + 1] = Op::Nop;
1536                        self.ops[i + 2] = Op::Nop;
1537                        self.ops[i + 3] = Op::Nop;
1538                        i += 4;
1539                        continue;
1540                    }
1541                }
1542                i += 1;
1543            }
1544        }
1545        // Compact once so that pass 3 sees a Nop-free op stream and can match
1546        // adjacent `PreIncSlotVoid + Jump` backedges produced by passes 1/2.
1547        self.compact_nops();
1548        // Pass 3: fuse loop backedge
1549        //   PreIncSlotVoid(s)  + Jump(top)
1550        // where ops[top] is SlotLtIntJumpIfFalse(s, limit, exit)
1551        // becomes
1552        //   SlotIncLtIntJumpBack(s, limit, top + 1)   // body falls through
1553        //   Nop                                       // was Jump
1554        // The first-iteration check at `top` is still reached from before the loop
1555        // (the loop's initial entry goes through the top test), so leaving
1556        // SlotLtIntJumpIfFalse in place keeps the entry path correct. All
1557        // subsequent iterations now skip both the inc op and the jump.
1558        let len = self.ops.len();
1559        if len >= 2 {
1560            let mut i = 0;
1561            while i + 1 < len {
1562                if let (Op::PreIncSlotVoid(s), Op::Jump(top)) = (&self.ops[i], &self.ops[i + 1]) {
1563                    let slot = *s;
1564                    let top = *top;
1565                    // Only fuse backward branches — the C-style `for` shape where `top` is
1566                    // the loop's `SlotLtIntJumpIfFalse` test and the body falls through to
1567                    // this trailing increment. A forward `Jump` that happens to land on a
1568                    // similar test is not the same shape and must not be rewritten.
1569                    if top < i {
1570                        if let Op::SlotLtIntJumpIfFalse(tslot, limit, exit) = &self.ops[top] {
1571                            // Safety: the top test's exit target must equal the fused op's
1572                            // fall-through (i + 2). Otherwise exiting the loop via
1573                            // "condition false" would land somewhere the unfused shape never
1574                            // exited to.
1575                            if *tslot == slot && *exit == i + 2 {
1576                                let limit = *limit;
1577                                let body_target = top + 1;
1578                                self.ops[i] = Op::SlotIncLtIntJumpBack(slot, limit, body_target);
1579                                self.ops[i + 1] = Op::Nop;
1580                                i += 2;
1581                                continue;
1582                            }
1583                        }
1584                    }
1585                }
1586                i += 1;
1587            }
1588        }
1589        // Pass 4: compact again — remove the Nops introduced by pass 3.
1590        self.compact_nops();
1591        // Pass 5: fuse counted-loop bodies down to a single native superinstruction.
1592        //
1593        // After pass 3 + compact, a `for (my $i = ..; $i < N; $i = $i + 1) { $sum += $i }`
1594        // loop looks like:
1595        //
1596        //     [top]        SlotLtIntJumpIfFalse(i, N, exit)
1597        //     [body_start] AddAssignSlotSlotVoid(sum, i)       ← target of the backedge
1598        //                  SlotIncLtIntJumpBack(i, N, body_start)
1599        //     [exit]       ...
1600        //
1601        // When the body is exactly one op, we fuse the AddAssign + backedge into
1602        // `AccumSumLoop(sum, i, N)`, whose handler runs the whole remaining loop in a
1603        // tight Rust `while`. Same scheme for the counted `$s .= CONST` pattern, fused
1604        // into `ConcatConstSlotLoop`.
1605        //
1606        // Safety gate: only fire when no op jumps *into* the body (other than the backedge
1607        // itself and the top test's fall-through, which isn't a jump). That keeps loops with
1608        // interior labels / `last LABEL` / `next LABEL` from being silently skipped.
1609        let len = self.ops.len();
1610        if len >= 2 {
1611            let has_inbound_jump = |ops: &[Op], pos: usize, ignore: usize| -> bool {
1612                for (j, op) in ops.iter().enumerate() {
1613                    if j == ignore {
1614                        continue;
1615                    }
1616                    let t = match op {
1617                        Op::Jump(t)
1618                        | Op::JumpIfFalse(t)
1619                        | Op::JumpIfTrue(t)
1620                        | Op::JumpIfFalseKeep(t)
1621                        | Op::JumpIfTrueKeep(t)
1622                        | Op::JumpIfDefinedKeep(t) => Some(*t),
1623                        Op::SlotLtIntJumpIfFalse(_, _, t) => Some(*t),
1624                        Op::SlotIncLtIntJumpBack(_, _, t) => Some(*t),
1625                        _ => None,
1626                    };
1627                    if t == Some(pos) {
1628                        return true;
1629                    }
1630                }
1631                false
1632            };
1633            // 5a: AddAssignSlotSlotVoid + SlotIncLtIntJumpBack → AccumSumLoop
1634            let mut i = 0;
1635            while i + 1 < len {
1636                if let (
1637                    Op::AddAssignSlotSlotVoid(sum_slot, src_slot),
1638                    Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1639                ) = (&self.ops[i], &self.ops[i + 1])
1640                {
1641                    if *src_slot == *inc_slot
1642                        && *body_target == i
1643                        && !has_inbound_jump(&self.ops, i, i + 1)
1644                        && !has_inbound_jump(&self.ops, i + 1, i + 1)
1645                    {
1646                        let sum_slot = *sum_slot;
1647                        let src_slot = *src_slot;
1648                        let limit = *limit;
1649                        self.ops[i] = Op::AccumSumLoop(sum_slot, src_slot, limit);
1650                        self.ops[i + 1] = Op::Nop;
1651                        i += 2;
1652                        continue;
1653                    }
1654                }
1655                i += 1;
1656            }
1657            // 5b: LoadConst + ConcatAppendSlotVoid + SlotIncLtIntJumpBack → ConcatConstSlotLoop
1658            if len >= 3 {
1659                let mut i = 0;
1660                while i + 2 < len {
1661                    if let (
1662                        Op::LoadConst(const_idx),
1663                        Op::ConcatAppendSlotVoid(s_slot),
1664                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1665                    ) = (&self.ops[i], &self.ops[i + 1], &self.ops[i + 2])
1666                    {
1667                        if *body_target == i
1668                            && !has_inbound_jump(&self.ops, i, i + 2)
1669                            && !has_inbound_jump(&self.ops, i + 1, i + 2)
1670                            && !has_inbound_jump(&self.ops, i + 2, i + 2)
1671                        {
1672                            let const_idx = *const_idx;
1673                            let s_slot = *s_slot;
1674                            let inc_slot = *inc_slot;
1675                            let limit = *limit;
1676                            self.ops[i] =
1677                                Op::ConcatConstSlotLoop(const_idx, s_slot, inc_slot, limit);
1678                            self.ops[i + 1] = Op::Nop;
1679                            self.ops[i + 2] = Op::Nop;
1680                            i += 3;
1681                            continue;
1682                        }
1683                    }
1684                    i += 1;
1685                }
1686            }
1687            // 5e: `$sum += $h{$k}` body op inside `for my $k (keys %h) { ... }`
1688            //   GetScalarSlot(sum) + GetScalarPlain(k) + GetHashElem(h) + Add
1689            //     + SetScalarSlotKeep(sum) + Pop
1690            //   → AddHashElemPlainKeyToSlot(sum, k, h)
1691            // Safe because `SetScalarSlotKeep + Pop` leaves nothing on the stack net; the fused
1692            // op is a drop-in for that sequence. No inbound jumps permitted to interior ops.
1693            if len >= 6 {
1694                let mut i = 0;
1695                while i + 5 < len {
1696                    if let (
1697                        Op::GetScalarSlot(sum_slot),
1698                        Op::GetScalarPlain(k_idx),
1699                        Op::GetHashElem(h_idx),
1700                        Op::Add,
1701                        Op::SetScalarSlotKeep(sum_slot2),
1702                        Op::Pop,
1703                    ) = (
1704                        &self.ops[i],
1705                        &self.ops[i + 1],
1706                        &self.ops[i + 2],
1707                        &self.ops[i + 3],
1708                        &self.ops[i + 4],
1709                        &self.ops[i + 5],
1710                    ) {
1711                        if *sum_slot == *sum_slot2
1712                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1713                        {
1714                            let sum_slot = *sum_slot;
1715                            let k_idx = *k_idx;
1716                            let h_idx = *h_idx;
1717                            self.ops[i] = Op::AddHashElemPlainKeyToSlot(sum_slot, k_idx, h_idx);
1718                            for off in 1..=5 {
1719                                self.ops[i + off] = Op::Nop;
1720                            }
1721                            i += 6;
1722                            continue;
1723                        }
1724                    }
1725                    i += 1;
1726                }
1727            }
1728            // 5e-slot: slot-key variant of 5e, emitted when the compiler lowers `$k` (the foreach
1729            // loop variable) into a slot rather than a frame scalar.
1730            //   GetScalarSlot(sum) + GetScalarSlot(k) + GetHashElem(h) + Add
1731            //     + SetScalarSlotKeep(sum) + Pop
1732            //   → AddHashElemSlotKeyToSlot(sum, k, h)
1733            if len >= 6 {
1734                let mut i = 0;
1735                while i + 5 < len {
1736                    if let (
1737                        Op::GetScalarSlot(sum_slot),
1738                        Op::GetScalarSlot(k_slot),
1739                        Op::GetHashElem(h_idx),
1740                        Op::Add,
1741                        Op::SetScalarSlotKeep(sum_slot2),
1742                        Op::Pop,
1743                    ) = (
1744                        &self.ops[i],
1745                        &self.ops[i + 1],
1746                        &self.ops[i + 2],
1747                        &self.ops[i + 3],
1748                        &self.ops[i + 4],
1749                        &self.ops[i + 5],
1750                    ) {
1751                        if *sum_slot == *sum_slot2
1752                            && *sum_slot != *k_slot
1753                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1754                        {
1755                            let sum_slot = *sum_slot;
1756                            let k_slot = *k_slot;
1757                            let h_idx = *h_idx;
1758                            self.ops[i] = Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_idx);
1759                            for off in 1..=5 {
1760                                self.ops[i + off] = Op::Nop;
1761                            }
1762                            i += 6;
1763                            continue;
1764                        }
1765                    }
1766                    i += 1;
1767                }
1768            }
1769            // 5d: counted hash-insert loop `$h{$i} = $i * K`
1770            //   GetScalarSlot(i) + LoadInt(k) + Mul + GetScalarSlot(i) + SetHashElem(h) + Pop
1771            //     + SlotIncLtIntJumpBack(i, limit, body_target)
1772            //   → SetHashIntTimesLoop(h, i, k, limit)
1773            if len >= 7 {
1774                let mut i = 0;
1775                while i + 6 < len {
1776                    if let (
1777                        Op::GetScalarSlot(gs1),
1778                        Op::LoadInt(k),
1779                        Op::Mul,
1780                        Op::GetScalarSlot(gs2),
1781                        Op::SetHashElem(h_idx),
1782                        Op::Pop,
1783                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1784                    ) = (
1785                        &self.ops[i],
1786                        &self.ops[i + 1],
1787                        &self.ops[i + 2],
1788                        &self.ops[i + 3],
1789                        &self.ops[i + 4],
1790                        &self.ops[i + 5],
1791                        &self.ops[i + 6],
1792                    ) {
1793                        if *gs1 == *inc_slot
1794                            && *gs2 == *inc_slot
1795                            && *body_target == i
1796                            && i32::try_from(*k).is_ok()
1797                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, i + 6))
1798                            && !has_inbound_jump(&self.ops, i + 6, i + 6)
1799                        {
1800                            let h_idx = *h_idx;
1801                            let inc_slot = *inc_slot;
1802                            let k32 = *k as i32;
1803                            let limit = *limit;
1804                            self.ops[i] = Op::SetHashIntTimesLoop(h_idx, inc_slot, k32, limit);
1805                            for off in 1..=6 {
1806                                self.ops[i + off] = Op::Nop;
1807                            }
1808                            i += 7;
1809                            continue;
1810                        }
1811                    }
1812                    i += 1;
1813                }
1814            }
1815            // 5c: GetScalarSlot + PushArray + ArrayLen + Pop + SlotIncLtIntJumpBack
1816            //      → PushIntRangeToArrayLoop
1817            // This is the compiler's `push @a, $i; $i++` shape in void context, where
1818            // the `push` expression's length return is pushed by `ArrayLen` and then `Pop`ped.
1819            if len >= 5 {
1820                let mut i = 0;
1821                while i + 4 < len {
1822                    if let (
1823                        Op::GetScalarSlot(get_slot),
1824                        Op::PushArray(push_idx),
1825                        Op::ArrayLen(len_idx),
1826                        Op::Pop,
1827                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1828                    ) = (
1829                        &self.ops[i],
1830                        &self.ops[i + 1],
1831                        &self.ops[i + 2],
1832                        &self.ops[i + 3],
1833                        &self.ops[i + 4],
1834                    ) {
1835                        if *get_slot == *inc_slot
1836                            && *push_idx == *len_idx
1837                            && *body_target == i
1838                            && !has_inbound_jump(&self.ops, i, i + 4)
1839                            && !has_inbound_jump(&self.ops, i + 1, i + 4)
1840                            && !has_inbound_jump(&self.ops, i + 2, i + 4)
1841                            && !has_inbound_jump(&self.ops, i + 3, i + 4)
1842                            && !has_inbound_jump(&self.ops, i + 4, i + 4)
1843                        {
1844                            let push_idx = *push_idx;
1845                            let inc_slot = *inc_slot;
1846                            let limit = *limit;
1847                            self.ops[i] = Op::PushIntRangeToArrayLoop(push_idx, inc_slot, limit);
1848                            self.ops[i + 1] = Op::Nop;
1849                            self.ops[i + 2] = Op::Nop;
1850                            self.ops[i + 3] = Op::Nop;
1851                            self.ops[i + 4] = Op::Nop;
1852                            i += 5;
1853                            continue;
1854                        }
1855                    }
1856                    i += 1;
1857                }
1858            }
1859        }
1860        // Pass 6: compact — remove the Nops pass 5 introduced.
1861        self.compact_nops();
1862        // Pass 7: fuse the entire `for my $k (keys %h) { $sum += $h{$k} }` loop into a single
1863        // `SumHashValuesToSlot` op that walks the hash's values in a tight native loop.
1864        //
1865        // After prior passes and compaction the shape is a 15-op block:
1866        //
1867        //     HashKeys(h)
1868        //     DeclareArray(list)
1869        //     LoadInt(0)
1870        //     DeclareScalarSlot(c, cname)
1871        //     LoadUndef
1872        //     DeclareScalarSlot(v, vname)
1873        //     [top]  GetScalarSlot(c)
1874        //            ArrayLen(list)
1875        //            NumLt
1876        //            JumpIfFalse(end)
1877        //            GetScalarSlot(c)
1878        //            GetArrayElem(list)
1879        //            SetScalarSlot(v)
1880        //            AddHashElemSlotKeyToSlot(sum, v, h)     ← fused body (pass 5e-slot)
1881        //            PreIncSlotVoid(c)
1882        //            Jump(top)
1883        //     [end]
1884        //
1885        // The counter (`__foreach_i__`), list (`__foreach_list__`), and loop var (`$k`) live
1886        // inside a `PushFrame`-isolated scope and are invisible after the loop — it is safe to
1887        // elide all of them. The fused op accumulates directly into `sum` without creating the
1888        // keys array at all.
1889        //
1890        // Safety gates:
1891        //   - `h` in HashKeys must match `h` in AddHashElemSlotKeyToSlot.
1892        //   - `list` in DeclareArray must match the loop `ArrayLen` / `GetArrayElem`.
1893        //   - `c` / `v` slots must be consistent throughout.
1894        //   - No inbound jump lands inside the 15-op window from the outside.
1895        //   - JumpIfFalse target must be i+15 (just past the Jump back-edge).
1896        //   - Jump back-edge target must be i+6 (the GetScalarSlot(c) at loop top).
1897        let len = self.ops.len();
1898        if len >= 15 {
1899            let has_inbound_jump =
1900                |ops: &[Op], pos: usize, ignore_from: usize, ignore_to: usize| -> bool {
1901                    for (j, op) in ops.iter().enumerate() {
1902                        if j >= ignore_from && j <= ignore_to {
1903                            continue;
1904                        }
1905                        let t = match op {
1906                            Op::Jump(t)
1907                            | Op::JumpIfFalse(t)
1908                            | Op::JumpIfTrue(t)
1909                            | Op::JumpIfFalseKeep(t)
1910                            | Op::JumpIfTrueKeep(t)
1911                            | Op::JumpIfDefinedKeep(t) => *t,
1912                            Op::SlotLtIntJumpIfFalse(_, _, t) => *t,
1913                            Op::SlotIncLtIntJumpBack(_, _, t) => *t,
1914                            _ => continue,
1915                        };
1916                        if t == pos {
1917                            return true;
1918                        }
1919                    }
1920                    false
1921                };
1922            let mut i = 0;
1923            while i + 15 < len {
1924                if let (
1925                    Op::HashKeys(h_idx),
1926                    Op::DeclareArray(list_idx),
1927                    Op::LoadInt(0),
1928                    Op::DeclareScalarSlot(c_slot, _c_name),
1929                    Op::LoadUndef,
1930                    Op::DeclareScalarSlot(v_slot, _v_name),
1931                    Op::GetScalarSlot(c_get1),
1932                    Op::ArrayLen(len_idx),
1933                    Op::NumLt,
1934                    Op::JumpIfFalse(end_tgt),
1935                    Op::GetScalarSlot(c_get2),
1936                    Op::GetArrayElem(elem_idx),
1937                    Op::SetScalarSlot(v_set),
1938                    Op::AddHashElemSlotKeyToSlot(sum_slot, v_in_body, h_in_body),
1939                    Op::PreIncSlotVoid(c_inc),
1940                    Op::Jump(top_tgt),
1941                ) = (
1942                    &self.ops[i],
1943                    &self.ops[i + 1],
1944                    &self.ops[i + 2],
1945                    &self.ops[i + 3],
1946                    &self.ops[i + 4],
1947                    &self.ops[i + 5],
1948                    &self.ops[i + 6],
1949                    &self.ops[i + 7],
1950                    &self.ops[i + 8],
1951                    &self.ops[i + 9],
1952                    &self.ops[i + 10],
1953                    &self.ops[i + 11],
1954                    &self.ops[i + 12],
1955                    &self.ops[i + 13],
1956                    &self.ops[i + 14],
1957                    &self.ops[i + 15],
1958                ) {
1959                    let full_end = i + 15;
1960                    if *list_idx == *len_idx
1961                        && *list_idx == *elem_idx
1962                        && *c_slot == *c_get1
1963                        && *c_slot == *c_get2
1964                        && *c_slot == *c_inc
1965                        && *v_slot == *v_set
1966                        && *v_slot == *v_in_body
1967                        && *h_idx == *h_in_body
1968                        && *top_tgt == i + 6
1969                        && *end_tgt == i + 16
1970                        && *sum_slot != *c_slot
1971                        && *sum_slot != *v_slot
1972                        && !(i..=full_end).any(|k| has_inbound_jump(&self.ops, k, i, full_end))
1973                    {
1974                        let sum_slot = *sum_slot;
1975                        let h_idx = *h_idx;
1976                        self.ops[i] = Op::SumHashValuesToSlot(sum_slot, h_idx);
1977                        for off in 1..=15 {
1978                            self.ops[i + off] = Op::Nop;
1979                        }
1980                        i += 16;
1981                        continue;
1982                    }
1983                }
1984                i += 1;
1985            }
1986        }
1987        // Pass 8: compact pass 7's Nops.
1988        self.compact_nops();
1989    }
1990
1991    /// Remove all `Nop` instructions and remap jump targets + metadata indices.
1992    fn compact_nops(&mut self) {
1993        let old_len = self.ops.len();
1994        // Build old→new index mapping.
1995        let mut remap = vec![0usize; old_len + 1];
1996        let mut new_idx = 0usize;
1997        for (old, slot) in remap[..old_len].iter_mut().enumerate() {
1998            *slot = new_idx;
1999            if !matches!(self.ops[old], Op::Nop) {
2000                new_idx += 1;
2001            }
2002        }
2003        remap[old_len] = new_idx;
2004        if new_idx == old_len {
2005            return; // nothing to compact
2006        }
2007        // Remap jump targets in all ops.
2008        for op in &mut self.ops {
2009            match op {
2010                Op::Jump(t) | Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => *t = remap[*t],
2011                Op::JumpIfTrueKeep(t) | Op::JumpIfDefinedKeep(t) => *t = remap[*t],
2012                Op::SlotLtIntJumpIfFalse(_, _, t) => *t = remap[*t],
2013                Op::SlotIncLtIntJumpBack(_, _, t) => *t = remap[*t],
2014                _ => {}
2015            }
2016        }
2017        // Remap sub entry points.
2018        for e in &mut self.sub_entries {
2019            e.1 = remap[e.1];
2020        }
2021        // Remap `CallStaticSubId` resolved entry IPs — they were recorded by
2022        // `patch_static_sub_calls` before peephole fusion ran, so any Nop
2023        // removal in front of a sub body shifts its entry and must be
2024        // reflected here; otherwise `vm_dispatch_user_call` jumps one (or
2025        // more) ops past the real sub start and silently skips the first
2026        // instruction(s) of the body.
2027        for c in &mut self.static_sub_calls {
2028            c.0 = remap[c.0];
2029        }
2030        // Remap block/grep/sort/etc bytecode ranges.
2031        fn remap_ranges(ranges: &mut [Option<(usize, usize)>], remap: &[usize]) {
2032            for r in ranges.iter_mut().flatten() {
2033                r.0 = remap[r.0];
2034                r.1 = remap[r.1];
2035            }
2036        }
2037        remap_ranges(&mut self.block_bytecode_ranges, &remap);
2038        remap_ranges(&mut self.map_expr_bytecode_ranges, &remap);
2039        remap_ranges(&mut self.grep_expr_bytecode_ranges, &remap);
2040        remap_ranges(&mut self.keys_expr_bytecode_ranges, &remap);
2041        remap_ranges(&mut self.values_expr_bytecode_ranges, &remap);
2042        remap_ranges(&mut self.eval_timeout_expr_bytecode_ranges, &remap);
2043        remap_ranges(&mut self.given_topic_bytecode_ranges, &remap);
2044        remap_ranges(&mut self.algebraic_match_subject_bytecode_ranges, &remap);
2045        remap_ranges(&mut self.regex_flip_flop_rhs_expr_bytecode_ranges, &remap);
2046        // Compact ops, lines, op_ast_expr.
2047        let mut j = 0;
2048        for old in 0..old_len {
2049            if !matches!(self.ops[old], Op::Nop) {
2050                self.ops[j] = self.ops[old].clone();
2051                if old < self.lines.len() && j < self.lines.len() {
2052                    self.lines[j] = self.lines[old];
2053                }
2054                if old < self.op_ast_expr.len() && j < self.op_ast_expr.len() {
2055                    self.op_ast_expr[j] = self.op_ast_expr[old];
2056                }
2057                j += 1;
2058            }
2059        }
2060        self.ops.truncate(j);
2061        self.lines.truncate(j);
2062        self.op_ast_expr.truncate(j);
2063    }
2064}
2065
2066impl Default for Chunk {
2067    fn default() -> Self {
2068        Self::new()
2069    }
2070}
2071
2072#[cfg(test)]
2073mod tests {
2074    use super::*;
2075    use crate::ast;
2076
2077    #[test]
2078    fn chunk_new_and_default_match() {
2079        let a = Chunk::new();
2080        let b = Chunk::default();
2081        assert!(a.ops.is_empty() && a.names.is_empty() && a.constants.is_empty());
2082        assert!(b.ops.is_empty() && b.lines.is_empty());
2083    }
2084
2085    #[test]
2086    fn intern_name_deduplicates() {
2087        let mut c = Chunk::new();
2088        let i0 = c.intern_name("foo");
2089        let i1 = c.intern_name("foo");
2090        let i2 = c.intern_name("bar");
2091        assert_eq!(i0, i1);
2092        assert_ne!(i0, i2);
2093        assert_eq!(c.names.len(), 2);
2094    }
2095
2096    #[test]
2097    fn add_constant_dedups_identical_strings() {
2098        let mut c = Chunk::new();
2099        let a = c.add_constant(PerlValue::string("x".into()));
2100        let b = c.add_constant(PerlValue::string("x".into()));
2101        assert_eq!(a, b);
2102        assert_eq!(c.constants.len(), 1);
2103    }
2104
2105    #[test]
2106    fn add_constant_distinct_strings_different_indices() {
2107        let mut c = Chunk::new();
2108        let a = c.add_constant(PerlValue::string("a".into()));
2109        let b = c.add_constant(PerlValue::string("b".into()));
2110        assert_ne!(a, b);
2111        assert_eq!(c.constants.len(), 2);
2112    }
2113
2114    #[test]
2115    fn add_constant_non_string_no_dedup_scan() {
2116        let mut c = Chunk::new();
2117        let a = c.add_constant(PerlValue::integer(1));
2118        let b = c.add_constant(PerlValue::integer(1));
2119        assert_ne!(a, b);
2120        assert_eq!(c.constants.len(), 2);
2121    }
2122
2123    #[test]
2124    fn emit_records_parallel_ops_and_lines() {
2125        let mut c = Chunk::new();
2126        c.emit(Op::LoadInt(1), 10);
2127        c.emit(Op::Pop, 11);
2128        assert_eq!(c.len(), 2);
2129        assert_eq!(c.lines, vec![10, 11]);
2130        assert_eq!(c.op_ast_expr, vec![None, None]);
2131        assert!(!c.is_empty());
2132    }
2133
2134    #[test]
2135    fn len_is_empty_track_ops() {
2136        let mut c = Chunk::new();
2137        assert!(c.is_empty());
2138        assert_eq!(c.len(), 0);
2139        c.emit(Op::Halt, 0);
2140        assert!(!c.is_empty());
2141        assert_eq!(c.len(), 1);
2142    }
2143
2144    #[test]
2145    fn patch_jump_here_updates_jump_target() {
2146        let mut c = Chunk::new();
2147        let j = c.emit(Op::Jump(0), 1);
2148        c.emit(Op::LoadInt(99), 2);
2149        c.patch_jump_here(j);
2150        assert_eq!(c.ops.len(), 2);
2151        assert!(matches!(c.ops[j], Op::Jump(2)));
2152    }
2153
2154    #[test]
2155    fn patch_jump_here_jump_if_true() {
2156        let mut c = Chunk::new();
2157        let j = c.emit(Op::JumpIfTrue(0), 1);
2158        c.emit(Op::Halt, 2);
2159        c.patch_jump_here(j);
2160        assert!(matches!(c.ops[j], Op::JumpIfTrue(2)));
2161    }
2162
2163    #[test]
2164    fn patch_jump_here_jump_if_false_keep() {
2165        let mut c = Chunk::new();
2166        let j = c.emit(Op::JumpIfFalseKeep(0), 1);
2167        c.emit(Op::Pop, 2);
2168        c.patch_jump_here(j);
2169        assert!(matches!(c.ops[j], Op::JumpIfFalseKeep(2)));
2170    }
2171
2172    #[test]
2173    fn patch_jump_here_jump_if_true_keep() {
2174        let mut c = Chunk::new();
2175        let j = c.emit(Op::JumpIfTrueKeep(0), 1);
2176        c.emit(Op::Pop, 2);
2177        c.patch_jump_here(j);
2178        assert!(matches!(c.ops[j], Op::JumpIfTrueKeep(2)));
2179    }
2180
2181    #[test]
2182    fn patch_jump_here_jump_if_defined_keep() {
2183        let mut c = Chunk::new();
2184        let j = c.emit(Op::JumpIfDefinedKeep(0), 1);
2185        c.emit(Op::Halt, 2);
2186        c.patch_jump_here(j);
2187        assert!(matches!(c.ops[j], Op::JumpIfDefinedKeep(2)));
2188    }
2189
2190    #[test]
2191    #[should_panic(expected = "patch_jump_to on non-jump op")]
2192    fn patch_jump_here_panics_on_non_jump() {
2193        let mut c = Chunk::new();
2194        let idx = c.emit(Op::LoadInt(1), 1);
2195        c.patch_jump_here(idx);
2196    }
2197
2198    #[test]
2199    fn add_block_returns_sequential_indices() {
2200        let mut c = Chunk::new();
2201        let b0: ast::Block = vec![];
2202        let b1: ast::Block = vec![];
2203        assert_eq!(c.add_block(b0), 0);
2204        assert_eq!(c.add_block(b1), 1);
2205        assert_eq!(c.blocks.len(), 2);
2206    }
2207
2208    #[test]
2209    fn builtin_id_from_u16_first_and_last() {
2210        assert_eq!(BuiltinId::from_u16(0), Some(BuiltinId::Length));
2211        assert_eq!(
2212            BuiltinId::from_u16(BuiltinId::Pselect as u16),
2213            Some(BuiltinId::Pselect)
2214        );
2215        assert_eq!(
2216            BuiltinId::from_u16(BuiltinId::BarrierNew as u16),
2217            Some(BuiltinId::BarrierNew)
2218        );
2219        assert_eq!(
2220            BuiltinId::from_u16(BuiltinId::ParPipeline as u16),
2221            Some(BuiltinId::ParPipeline)
2222        );
2223        assert_eq!(
2224            BuiltinId::from_u16(BuiltinId::GlobParProgress as u16),
2225            Some(BuiltinId::GlobParProgress)
2226        );
2227        assert_eq!(
2228            BuiltinId::from_u16(BuiltinId::Readpipe as u16),
2229            Some(BuiltinId::Readpipe)
2230        );
2231        assert_eq!(
2232            BuiltinId::from_u16(BuiltinId::ReadLineList as u16),
2233            Some(BuiltinId::ReadLineList)
2234        );
2235        assert_eq!(
2236            BuiltinId::from_u16(BuiltinId::ReaddirList as u16),
2237            Some(BuiltinId::ReaddirList)
2238        );
2239        assert_eq!(
2240            BuiltinId::from_u16(BuiltinId::Ssh as u16),
2241            Some(BuiltinId::Ssh)
2242        );
2243        assert_eq!(
2244            BuiltinId::from_u16(BuiltinId::Pipe as u16),
2245            Some(BuiltinId::Pipe)
2246        );
2247        assert_eq!(
2248            BuiltinId::from_u16(BuiltinId::Files as u16),
2249            Some(BuiltinId::Files)
2250        );
2251        assert_eq!(
2252            BuiltinId::from_u16(BuiltinId::Filesf as u16),
2253            Some(BuiltinId::Filesf)
2254        );
2255        assert_eq!(
2256            BuiltinId::from_u16(BuiltinId::Dirs as u16),
2257            Some(BuiltinId::Dirs)
2258        );
2259        assert_eq!(
2260            BuiltinId::from_u16(BuiltinId::SymLinks as u16),
2261            Some(BuiltinId::SymLinks)
2262        );
2263        assert_eq!(
2264            BuiltinId::from_u16(BuiltinId::Sockets as u16),
2265            Some(BuiltinId::Sockets)
2266        );
2267        assert_eq!(
2268            BuiltinId::from_u16(BuiltinId::Pipes as u16),
2269            Some(BuiltinId::Pipes)
2270        );
2271        assert_eq!(
2272            BuiltinId::from_u16(BuiltinId::BlockDevices as u16),
2273            Some(BuiltinId::BlockDevices)
2274        );
2275        assert_eq!(
2276            BuiltinId::from_u16(BuiltinId::CharDevices as u16),
2277            Some(BuiltinId::CharDevices)
2278        );
2279    }
2280
2281    #[test]
2282    fn builtin_id_from_u16_out_of_range() {
2283        assert_eq!(BuiltinId::from_u16(BuiltinId::CharDevices as u16 + 1), None);
2284        assert_eq!(BuiltinId::from_u16(u16::MAX), None);
2285    }
2286
2287    #[test]
2288    fn op_enum_clone_roundtrip() {
2289        let o = Op::Call(42, 3, 0);
2290        assert!(matches!(o.clone(), Op::Call(42, 3, 0)));
2291    }
2292
2293    #[test]
2294    fn chunk_clone_independent_ops() {
2295        let mut c = Chunk::new();
2296        c.emit(Op::Negate, 1);
2297        let mut d = c.clone();
2298        d.emit(Op::Pop, 2);
2299        assert_eq!(c.len(), 1);
2300        assert_eq!(d.len(), 2);
2301    }
2302
2303    #[test]
2304    fn chunk_disassemble_includes_ops() {
2305        let mut c = Chunk::new();
2306        c.emit(Op::LoadInt(7), 1);
2307        let s = c.disassemble();
2308        assert!(s.contains("0000"));
2309        assert!(s.contains("LoadInt(7)"));
2310        assert!(s.contains("     -")); // no ast ref column
2311    }
2312
2313    #[test]
2314    fn ast_expr_at_roundtrips_pooled_expr() {
2315        let mut c = Chunk::new();
2316        let e = ast::Expr {
2317            kind: ast::ExprKind::Integer(99),
2318            line: 3,
2319        };
2320        c.ast_expr_pool.push(e);
2321        c.emit_with_ast_idx(Op::LoadInt(99), 3, Some(0));
2322        let got = c.ast_expr_at(0).expect("ast ref");
2323        assert!(matches!(&got.kind, ast::ExprKind::Integer(99)));
2324        assert_eq!(got.line, 3);
2325    }
2326}