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    /// pmaps { BLOCK } LIST — streaming parallel map; stack: \[list\] → \[iterator\]
564    PMapsWithBlock(u16),
565    /// pflat_maps { BLOCK } LIST — streaming parallel flat map; stack: \[list\] → \[iterator\]
566    PFlatMapsWithBlock(u16),
567    /// `pmap_on` / `pflat_map_on` over SSH — stack: \[progress_flag, list, cluster\] → \[mapped\]; `flat` = 1 for flatten
568    PMapRemote {
569        block_idx: u16,
570        flat: u8,
571    },
572    /// puniq LIST — hash-partition parallel distinct (first occurrence order); stack: \[progress_flag, list\] → \[array\]
573    Puniq,
574    /// pfirst { BLOCK } LIST — short-circuit parallel; stack: \[progress_flag, list\] → value or undef
575    PFirstWithBlock(u16),
576    /// pany { BLOCK } LIST — short-circuit parallel; stack: \[progress_flag, list\] → 0/1
577    PAnyWithBlock(u16),
578    /// pmap_chunked N { BLOCK } @list — block_idx; stack: \[progress_flag, chunk_n, list\] → \[mapped\]
579    PMapChunkedWithBlock(u16),
580    /// pgrep { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[filtered\]
581    PGrepWithBlock(u16),
582    /// pgreps { BLOCK } LIST — streaming parallel grep; stack: \[list\] → \[iterator\]
583    PGrepsWithBlock(u16),
584    /// pfor { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[\]
585    PForWithBlock(u16),
586    /// psort { BLOCK } @list — block_idx; stack: \[progress_flag, list\] → \[sorted\]
587    PSortWithBlock(u16),
588    /// psort @list (no block) — stack: \[progress_flag, list\] → \[sorted\]
589    PSortNoBlockParallel,
590    /// `reduce { BLOCK } @list` — block_idx; stack: \[list\] → \[accumulator\]
591    ReduceWithBlock(u16),
592    /// `preduce { BLOCK } @list` — block_idx; stack: \[progress_flag, list\] → \[accumulator\]
593    PReduceWithBlock(u16),
594    /// `preduce_init EXPR, { BLOCK } @list` — block_idx; stack: \[progress_flag, list, init\] → \[accumulator\]
595    PReduceInitWithBlock(u16),
596    /// `pmap_reduce { MAP } { REDUCE } @list` — map and reduce block indices; stack: \[progress_flag, list\] → \[scalar\]
597    PMapReduceWithBlocks(u16, u16),
598    /// `pcache { BLOCK } @list` — block_idx; stack: \[progress_flag, list\] → \[array\]
599    PcacheWithBlock(u16),
600    /// `pselect($rx1, ... [, timeout => SECS])` — stack: \[rx0, …, rx_{n-1}\] with optional timeout on top
601    Pselect {
602        n_rx: u8,
603        has_timeout: bool,
604    },
605    /// `par_lines PATH, sub { } [, progress => EXPR]` — index into [`Chunk::par_lines_entries`]; stack: \[\] → `undef`
606    ParLines(u16),
607    /// `par_walk PATH, sub { } [, progress => EXPR]` — index into [`Chunk::par_walk_entries`]; stack: \[\] → `undef`
608    ParWalk(u16),
609    /// `pwatch GLOB, sub { }` — index into [`Chunk::pwatch_entries`]; stack: \[\] → result
610    Pwatch(u16),
611    /// fan N { BLOCK } — block_idx; stack: \[progress_flag, count\] (`progress_flag` is 0/1)
612    FanWithBlock(u16),
613    /// fan { BLOCK } — block_idx; stack: \[progress_flag\]; COUNT = rayon pool size (`stryke -j`)
614    FanWithBlockAuto(u16),
615    /// fan_cap N { BLOCK } — like fan; stack: \[progress_flag, count\] → array of block return values
616    FanCapWithBlock(u16),
617    /// fan_cap { BLOCK } — like fan; stack: \[progress_flag\] → array
618    FanCapWithBlockAuto(u16),
619    /// `do { BLOCK }` — block_idx + wantarray byte ([`crate::interpreter::WantarrayCtx::as_byte`]);
620    /// stack: \[\] → result
621    EvalBlock(u16, u8),
622    /// `trace { BLOCK }` — block_idx; stack: \[\] → block value (stderr tracing for mysync mutations)
623    TraceBlock(u16),
624    /// `timer { BLOCK }` — block_idx; stack: \[\] → elapsed ms as float
625    TimerBlock(u16),
626    /// `bench { BLOCK } N` — block_idx; stack: \[iterations\] → benchmark summary string
627    BenchBlock(u16),
628    /// `given (EXPR) { when ... default ... }` — [`Chunk::given_entries`] /
629    /// [`Chunk::given_topic_bytecode_ranges`]; stack: \[\] → topic result
630    Given(u16),
631    /// `eval_timeout SECS { ... }` — index into [`Chunk::eval_timeout_entries`] /
632    /// [`Chunk::eval_timeout_expr_bytecode_ranges`]; stack: \[\] → block value
633    EvalTimeout(u16),
634    /// Algebraic `match (SUBJECT) { ... }` — [`Chunk::algebraic_match_entries`] /
635    /// [`Chunk::algebraic_match_subject_bytecode_ranges`]; stack: \[\] → arm value
636    AlgebraicMatch(u16),
637    /// `async { BLOCK }` / `spawn { BLOCK }` — block_idx; stack: \[\] → AsyncTask
638    AsyncBlock(u16),
639    /// `await EXPR` — stack: \[value\] → result
640    Await,
641    /// `__SUB__` — push reference to currently executing sub (for anonymous recursion).
642    LoadCurrentSub,
643    /// `defer { BLOCK }` — register a block to run when the current scope exits.
644    /// Stack: `[coderef]` → `[]`. The coderef is pushed to the frame's defer list.
645    DeferBlock,
646    /// Make a scalar reference from TOS (copies value into a new `RwLock`).
647    MakeScalarRef,
648    /// `\$name` when `name` is a plain scalar variable — ref aliases the live binding (same as tree `scalar_binding_ref`).
649    MakeScalarBindingRef(u16),
650    /// `\@name` — ref aliases the live array in scope (name pool index, stash-qualified like [`Op::GetArray`]).
651    MakeArrayBindingRef(u16),
652    /// `\%name` — ref aliases the live hash in scope.
653    MakeHashBindingRef(u16),
654    /// `\@{ EXPR }` after `EXPR` is on the stack — ARRAY ref aliasing the same storage as Perl (ref to existing ref or package array).
655    MakeArrayRefAlias,
656    /// `\%{ EXPR }` — HASH ref alias (same semantics as [`Op::MakeArrayRefAlias`] for hashes).
657    MakeHashRefAlias,
658    /// Make an array reference from TOS (which should be an Array)
659    MakeArrayRef,
660    /// Make a hash reference from TOS (which should be a Hash)
661    MakeHashRef,
662    /// Make an anonymous sub from a block — block_idx; stack: \[\] → CodeRef
663    /// Anonymous `sub` / coderef: block pool index + [`Chunk::code_ref_sigs`] index (may be empty vec).
664    MakeCodeRef(u16, u16),
665    /// Push a code reference to a named sub (`\&foo`) — name pool index; resolves at run time.
666    LoadNamedSubRef(u16),
667    /// `\&{ EXPR }` — stack: \[sub name string\] → code ref (resolves at run time).
668    LoadDynamicSubRef,
669    /// `*{ EXPR }` — stack: \[stash / glob name string\] → resolved handle string (IO alias map + identity).
670    LoadDynamicTypeglob,
671    /// `*lhs = *rhs` — copy stash slots (sub, scalar, array, hash, IO alias); name pool indices for both sides.
672    CopyTypeglobSlots(u16, u16),
673    /// `*name = $coderef` — stack: pop value, install subroutine in typeglob, push value back (assignment result).
674    TypeglobAssignFromValue(u16),
675    /// `*{LHS} = $coderef` — stack: pop value, pop LHS glob name string, install sub, push value back.
676    TypeglobAssignFromValueDynamic,
677    /// `*{LHS} = *rhs` — stack: pop LHS glob name string; RHS name is pool index; copies stash like [`Op::CopyTypeglobSlots`].
678    CopyTypeglobSlotsDynamicLhs(u16),
679    /// Symbolic deref (`$$r`, `@{...}`, `%{...}`, `*{...}`): stack: \[ref or name value\] → result.
680    /// Byte: `0` = [`crate::ast::Sigil::Scalar`], `1` = Array, `2` = Hash, `3` = Typeglob.
681    SymbolicDeref(u8),
682    /// Dereference arrow: ->\[\] — stack: \[ref, index\] → value
683    ArrowArray,
684    /// Dereference arrow: ->{} — stack: \[ref, key\] → value
685    ArrowHash,
686    /// Assign to `->{}`: stack: \[value, ref, key\] (key on top) — consumes three values.
687    SetArrowHash,
688    /// Assign to `->[]`: stack: \[value, ref, index\] (index on top) — consumes three values.
689    SetArrowArray,
690    /// Like [`Op::SetArrowArray`] but leaves the assigned value on the stack (for `++$aref->[$i]` value).
691    SetArrowArrayKeep,
692    /// Like [`Op::SetArrowHash`] but leaves the assigned value on the stack (for `++$href->{k}` value).
693    SetArrowHashKeep,
694    /// Postfix `++` / `--` on `->[]`: stack \[ref, index\] (index on top) → old value; mutates slot.
695    /// Byte: `0` = increment, `1` = decrement.
696    ArrowArrayPostfix(u8),
697    /// Postfix `++` / `--` on `->{}`: stack \[ref, key\] (key on top) → old value; mutates slot.
698    /// Byte: `0` = increment, `1` = decrement.
699    ArrowHashPostfix(u8),
700    /// `$$r = $val` — stack: \[value, ref\] (ref on top).
701    SetSymbolicScalarRef,
702    /// Like [`Op::SetSymbolicScalarRef`] but leaves the assigned value on the stack.
703    SetSymbolicScalarRefKeep,
704    /// `@{ EXPR } = LIST` — stack: \[list value, ref-or-name\] (top = ref / package name); delegates to
705    /// [`Interpreter::assign_symbolic_array_ref_deref`](crate::interpreter::Interpreter::assign_symbolic_array_ref_deref).
706    SetSymbolicArrayRef,
707    /// `%{ EXPR } = LIST` — stack: \[list value, ref-or-name\]; pairs from list like `%h = (k => v, …)`.
708    SetSymbolicHashRef,
709    /// `*{ EXPR } = RHS` — stack: \[value, ref-or-name\] (top = symbolic glob name); coderef install or `*lhs = *rhs` copy.
710    SetSymbolicTypeglobRef,
711    /// Postfix `++` / `--` on symbolic scalar ref (`$$r`); stack \[ref\] → old value. Byte: `0` = increment, `1` = decrement.
712    SymbolicScalarRefPostfix(u8),
713    /// Dereference arrow: ->() — stack: \[ref, args_array\] → value
714    /// `$cr->(...)` — wantarray byte (see VM `WantarrayCtx` threading on `Call` / `MethodCall`).
715    ArrowCall(u8),
716    /// Indirect call `$coderef(ARG...)` / `&$coderef(ARG...)` — stack (bottom→top): `target`, then
717    /// `argc` argument values (first arg pushed first). Third byte: `1` = ignore stack args and use
718    /// caller `@_` (`argc` must be `0`).
719    IndirectCall(u8, u8, u8),
720    /// Method call: stack: \[object, args...\] → result; name_idx, argc, wantarray
721    MethodCall(u16, u8, u8),
722    /// Like [`Op::MethodCall`] but uses SUPER / C3 parent chain (see interpreter method resolution for `SUPER`).
723    MethodCallSuper(u16, u8, u8),
724    /// File test: -e, -f, -d, etc. — test char; stack: \[path\] → 0/1
725    FileTestOp(u8),
726
727    // ── try / catch / finally (VM exception handling; see [`VM::try_recover_from_exception`]) ──
728    /// Push a [`crate::vm::TryFrame`]; `catch_ip` / `after_ip` patched via [`Chunk::patch_try_push_catch`]
729    /// / [`Chunk::patch_try_push_after`]; `finally_ip` via [`Chunk::patch_try_push_finally`].
730    TryPush {
731        catch_ip: usize,
732        finally_ip: Option<usize>,
733        after_ip: usize,
734        catch_var_idx: u16,
735    },
736    /// Normal completion from try or catch body (jump to finally or merge).
737    TryContinueNormal,
738    /// End of `finally` block: pop try frame and jump to `after_ip`.
739    TryFinallyEnd,
740    /// Enter catch: consume [`crate::vm::VM::pending_catch_error`], pop try scope, push catch scope, bind `$var`.
741    CatchReceive(u16),
742
743    // ── `mysync` (thread-safe shared bindings; see [`StmtKind::MySync`]) ──
744    /// Stack: `[init]` → `[]`. Declares `${name}` as `PerlValue::atomic` (or deque/heap unwrapped).
745    DeclareMySyncScalar(u16),
746    /// Stack: `[init_list]` → `[]`. Declares `@name` as atomic array.
747    DeclareMySyncArray(u16),
748    /// Stack: `[init_list]` → `[]`. Declares `%name` as atomic hash.
749    DeclareMySyncHash(u16),
750    /// Register [`RuntimeSubDecl`] at index (nested `sub`, including inside `BEGIN`).
751    RuntimeSubDecl(u16),
752    /// `tie $x | @arr | %h, 'Class', ...` — stack bottom = class expr, then user args; `argc` = `1 + args.len()`.
753    /// `target_kind`: 0 = scalar (`TIESCALAR`), 1 = array (`TIEARRAY`), 2 = hash (`TIEHASH`). `name_idx` = bare name.
754    Tie {
755        target_kind: u8,
756        name_idx: u16,
757        argc: u8,
758    },
759    /// `format NAME =` … — index into [`Chunk::format_decls`]; installs into current package at run time.
760    FormatDecl(u16),
761    /// `use overload 'op' => 'method', …` — index into [`Chunk::use_overload_entries`].
762    UseOverload(u16),
763    /// Scalar `$x OP= $rhs` — uses [`Scope::atomic_mutate`] so `mysync` scalars are RMW-safe.
764    /// Stack: `[rhs]` → `[result]`. `op` byte is from [`crate::compiler::scalar_compound_op_to_byte`].
765    ScalarCompoundAssign {
766        name_idx: u16,
767        op: u8,
768    },
769
770    // ── Special ──
771    /// Set `${^GLOBAL_PHASE}` on the interpreter. See [`GP_START`] … [`GP_END`].
772    SetGlobalPhase(u8),
773    Halt,
774
775    // ── Streaming map (appended — do not reorder earlier op tags) ─────────────
776    /// `maps { BLOCK } LIST` — stack: \[list\] → lazy iterator (pull-based; stryke extension).
777    MapsWithBlock(u16),
778    /// `flat_maps { BLOCK } LIST` — like [`Op::MapsWithBlock`] with `flat_map`-style flattening.
779    MapsFlatMapWithBlock(u16),
780    /// `maps EXPR, LIST` — index into [`Chunk::map_expr_entries`]; stack: \[list\] → iterator.
781    MapsWithExpr(u16),
782    /// `flat_maps EXPR, LIST` — same pools as [`Op::MapsWithExpr`].
783    MapsFlatMapWithExpr(u16),
784    /// `filter { BLOCK } LIST` — stack: \[list\] → lazy iterator (stryke; `grep` remains eager).
785    FilterWithBlock(u16),
786    /// `filter EXPR, LIST` — index into [`Chunk::grep_expr_entries`]; stack: \[list\] → iterator.
787    FilterWithExpr(u16),
788}
789
790/// `${^GLOBAL_PHASE}` values emitted with [`Op::SetGlobalPhase`] (matches Perl’s phase strings).
791pub const GP_START: u8 = 0;
792/// Reserved; stock Perl 5 keeps `${^GLOBAL_PHASE}` as **`START`** during `UNITCHECK` blocks.
793pub const GP_UNITCHECK: u8 = 1;
794pub const GP_CHECK: u8 = 2;
795pub const GP_INIT: u8 = 3;
796pub const GP_RUN: u8 = 4;
797pub const GP_END: u8 = 5;
798
799/// Built-in function IDs for CallBuiltin dispatch.
800#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
801#[repr(u16)]
802pub enum BuiltinId {
803    // String
804    Length = 0,
805    Chomp,
806    Chop,
807    Substr,
808    Index,
809    Rindex,
810    Uc,
811    Lc,
812    Ucfirst,
813    Lcfirst,
814    Chr,
815    Ord,
816    Hex,
817    Oct,
818    Join,
819    Split,
820    Sprintf,
821
822    // Numeric
823    Abs,
824    Int,
825    Sqrt,
826
827    // Type
828    Defined,
829    Ref,
830    Scalar,
831
832    // Array
833    Splice,
834    Reverse,
835    Sort,
836    Unshift,
837
838    // Hash
839
840    // I/O
841    Open,
842    Close,
843    Eof,
844    ReadLine,
845    Printf,
846
847    // System
848    System,
849    Exec,
850    Exit,
851    Die,
852    Warn,
853    Chdir,
854    Mkdir,
855    Unlink,
856
857    // Control
858    Eval,
859    Do,
860    Require,
861
862    // OOP
863    Bless,
864    Caller,
865
866    // Parallel
867    PMap,
868    PGrep,
869    PFor,
870    PSort,
871    Fan,
872
873    // Map/Grep (block-based — need special handling)
874    MapBlock,
875    GrepBlock,
876    SortBlock,
877
878    // Math (appended — do not reorder earlier IDs)
879    Sin,
880    Cos,
881    Atan2,
882    Exp,
883    Log,
884    Rand,
885    Srand,
886
887    // String (appended)
888    Crypt,
889    Fc,
890    Pos,
891    Study,
892
893    Stat,
894    Lstat,
895    Link,
896    Symlink,
897    Readlink,
898    Glob,
899
900    Opendir,
901    Readdir,
902    Closedir,
903    Rewinddir,
904    Telldir,
905    Seekdir,
906    /// Read entire file as UTF-8 (`slurp $path`).
907    Slurp,
908    /// Blocking HTTP GET (`fetch_url $url`).
909    FetchUrl,
910    /// `pchannel()` — `(tx, rx)` as a two-element list.
911    Pchannel,
912    /// Parallel recursive glob (`glob_par`).
913    GlobPar,
914    /// `deque()` — empty deque.
915    DequeNew,
916    /// `heap(sub { })` — empty heap with comparator.
917    HeapNew,
918    /// `pipeline(...)` — lazy iterator (filter/map/take/collect).
919    Pipeline,
920    /// `capture("cmd")` — structured stdout/stderr/exit (via `sh -c`).
921    Capture,
922    /// `ppool(N)` — persistent thread pool (`submit` / `collect`).
923    Ppool,
924    /// Scalar/list context query (`wantarray`).
925    Wantarray,
926    /// `rename OLD, NEW`
927    Rename,
928    /// `chmod MODE, ...`
929    Chmod,
930    /// `chown UID, GID, ...`
931    Chown,
932    /// `pselect($rx1, $rx2, ...)` — multiplexed recv; returns `(value, index)`.
933    Pselect,
934    /// `barrier(N)` — thread barrier (`->wait`).
935    BarrierNew,
936    /// `par_pipeline(...)` — list form: same as `pipeline` but parallel `filter`/`map` on `collect()`.
937    ParPipeline,
938    /// `glob_par(..., progress => EXPR)` — last stack arg is truthy progress flag.
939    GlobParProgress,
940    /// `par_pipeline_stream(...)` — streaming pipeline with bounded channels between stages.
941    ParPipelineStream,
942    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file.
943    ParSed,
944    /// `par_sed(..., progress => EXPR)` — last stack arg is truthy progress flag.
945    ParSedProgress,
946    /// `each EXPR` — matches tree interpreter (returns empty list).
947    Each,
948    /// `` `cmd` `` / `qx{...}` — stdout string via `sh -c` (Perl readpipe); sets `$?`.
949    Readpipe,
950    /// `readline` / `<HANDLE>` in **list** context — all remaining lines until EOF (Perl `readline` list semantics).
951    ReadLineList,
952    /// `readdir` in **list** context — all names not yet returned (Perl drains the rest of the stream).
953    ReaddirList,
954    /// `ssh HOST, CMD, …` / `ssh(HOST, …)` — `execvp` style `ssh` only (no shell).
955    Ssh,
956    /// `rmdir LIST` — remove empty directories; returns count removed (appended ID).
957    Rmdir,
958    /// `utime ATIME, MTIME, LIST` — set access/mod times (Unix).
959    Utime,
960    /// `umask EXPR` / `umask()` — process file mode creation mask (Unix).
961    Umask,
962    /// `getcwd` / `Cwd::getcwd` / `CORE::getcwd`.
963    Getcwd,
964    /// `pipe READHANDLE, WRITEHANDLE` — OS pipe ends (Unix).
965    Pipe,
966    /// `files` / `files DIR` — list file names in a directory (default: `.`).
967    Files,
968    /// `filesf` / `filesf DIR` / `f` — list only regular file names in a directory (default: `.`).
969    Filesf,
970    /// `fr DIR` — list only regular file names recursively (default: `.`).
971    FilesfRecursive,
972    /// `dirs` / `dirs DIR` / `d` — list subdirectory names in a directory (default: `.`).
973    Dirs,
974    /// `dr DIR` — list subdirectory paths recursively (default: `.`).
975    DirsRecursive,
976    /// `sym_links` / `sym_links DIR` — list symlink names in a directory (default: `.`).
977    SymLinks,
978    /// `sockets` / `sockets DIR` — list Unix socket names in a directory (default: `.`).
979    Sockets,
980    /// `pipes` / `pipes DIR` — list named-pipe (FIFO) names in a directory (default: `.`).
981    Pipes,
982    /// `block_devices` / `block_devices DIR` — list block device names in a directory (default: `.`).
983    BlockDevices,
984    /// `char_devices` / `char_devices DIR` — list character device names in a directory (default: `.`).
985    CharDevices,
986}
987
988impl BuiltinId {
989    pub fn from_u16(v: u16) -> Option<Self> {
990        if v <= Self::CharDevices as u16 {
991            Some(unsafe { std::mem::transmute::<u16, BuiltinId>(v) })
992        } else {
993            None
994        }
995    }
996}
997
998/// A compiled chunk of bytecode with its constant pools.
999#[derive(Debug, Clone, Serialize, Deserialize)]
1000pub struct Chunk {
1001    pub ops: Vec<Op>,
1002    /// Constant pool: string literals, regex patterns, etc.
1003    #[serde(with = "crate::pec::constants_pool_codec")]
1004    pub constants: Vec<PerlValue>,
1005    /// Name pool: variable names, sub names (interned/deduped).
1006    pub names: Vec<String>,
1007    /// Source line for each op (parallel array for error reporting).
1008    pub lines: Vec<usize>,
1009    /// Optional link from each op to the originating [`Expr`] (pool index into [`Self::ast_expr_pool`]).
1010    /// Filled for ops emitted from [`crate::compiler::Compiler::compile_expr_ctx`]; other paths leave `None`.
1011    pub op_ast_expr: Vec<Option<u32>>,
1012    /// Interned [`Expr`] nodes referenced by [`Self::op_ast_expr`] (for debugging / tooling).
1013    pub ast_expr_pool: Vec<Expr>,
1014    /// Compiled subroutine entry points: (name_index, op_index, uses_stack_args).
1015    /// When `uses_stack_args` is true, the Call op leaves arguments on the value
1016    /// stack and the sub reads them via `GetArg(idx)` instead of `shift @_`.
1017    pub sub_entries: Vec<(u16, usize, bool)>,
1018    /// AST blocks for map/grep/sort/parallel operations.
1019    /// Referenced by block-based opcodes via u16 index.
1020    pub blocks: Vec<Block>,
1021    /// When `Some((start, end))`, `blocks[i]` is also lowered to `ops[start..end]` (exclusive `end`)
1022    /// with trailing [`Op::BlockReturnValue`]. VM uses opcodes; otherwise the AST in `blocks[i]`.
1023    pub block_bytecode_ranges: Vec<Option<(usize, usize)>>,
1024    /// Resolved [`Op::CallStaticSubId`] targets: subroutine entry IP, stack-args calling convention,
1025    /// and stash name pool index (qualified key matching [`Interpreter::subs`]).
1026    pub static_sub_calls: Vec<(usize, bool, u16)>,
1027    /// Assign targets for `s///` / `tr///` bytecode (LHS expressions).
1028    pub lvalues: Vec<Expr>,
1029    /// `struct Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1030    pub struct_defs: Vec<StructDef>,
1031    /// `enum Name { ... }` definitions in this chunk (registered on the interpreter at VM start).
1032    pub enum_defs: Vec<EnumDef>,
1033    /// `class Name extends ... impl ... { ... }` definitions.
1034    pub class_defs: Vec<ClassDef>,
1035    /// `trait Name { ... }` definitions.
1036    pub trait_defs: Vec<TraitDef>,
1037    /// `given (topic) { body }` — topic expression + body (when/default handled by interpreter).
1038    pub given_entries: Vec<(Expr, Block)>,
1039    /// When `Some((start, end))`, `given_entries[i].0` (topic) is lowered to `ops[start..end]` +
1040    /// [`Op::BlockReturnValue`].
1041    pub given_topic_bytecode_ranges: Vec<Option<(usize, usize)>>,
1042    /// `eval_timeout timeout_expr { body }` — evaluated at runtime.
1043    pub eval_timeout_entries: Vec<(Expr, Block)>,
1044    /// When `Some((start, end))`, `eval_timeout_entries[i].0` (timeout expr) is lowered to
1045    /// `ops[start..end]` with trailing [`Op::BlockReturnValue`].
1046    pub eval_timeout_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1047    /// Algebraic `match (subject) { arms }`.
1048    pub algebraic_match_entries: Vec<(Expr, Vec<MatchArm>)>,
1049    /// When `Some((start, end))`, `algebraic_match_entries[i].0` (subject) is lowered to
1050    /// `ops[start..end]` + [`Op::BlockReturnValue`].
1051    pub algebraic_match_subject_bytecode_ranges: Vec<Option<(usize, usize)>>,
1052    /// Nested / runtime `sub` declarations (see [`Op::RuntimeSubDecl`]).
1053    pub runtime_sub_decls: Vec<RuntimeSubDecl>,
1054    /// Stryke `sub ($a, …)` / hash-destruct params for [`Op::MakeCodeRef`] (second operand is pool index).
1055    pub code_ref_sigs: Vec<Vec<SubSigParam>>,
1056    /// `par_lines PATH, sub { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1057    pub par_lines_entries: Vec<(Expr, Expr, Option<Expr>)>,
1058    /// `par_walk PATH, sub { } [, progress => EXPR]` — evaluated by interpreter inside VM.
1059    pub par_walk_entries: Vec<(Expr, Expr, Option<Expr>)>,
1060    /// `pwatch GLOB, sub { }` — evaluated by interpreter inside VM.
1061    pub pwatch_entries: Vec<(Expr, Expr)>,
1062    /// `substr $var, OFF, LEN, REPL` — four-arg form (mutates `LHS`); evaluated by interpreter inside VM.
1063    pub substr_four_arg_entries: Vec<(Expr, Expr, Option<Expr>, Expr)>,
1064    /// `keys EXPR` when `EXPR` is not bare `%h`.
1065    pub keys_expr_entries: Vec<Expr>,
1066    /// When `Some((start, end))`, `keys_expr_entries[i]` is lowered to `ops[start..end]` +
1067    /// [`Op::BlockReturnValue`] (operand only; [`Op::KeysExpr`] still applies `keys` to the value).
1068    pub keys_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1069    /// `values EXPR` when not bare `%h`.
1070    pub values_expr_entries: Vec<Expr>,
1071    pub values_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1072    /// `delete EXPR` when not the fast `%h{k}` lowering.
1073    pub delete_expr_entries: Vec<Expr>,
1074    /// `exists EXPR` when not the fast `%h{k}` lowering.
1075    pub exists_expr_entries: Vec<Expr>,
1076    /// `push` when the array operand is not a bare `@name` (e.g. `push $aref, ...`).
1077    pub push_expr_entries: Vec<(Expr, Vec<Expr>)>,
1078    pub pop_expr_entries: Vec<Expr>,
1079    pub shift_expr_entries: Vec<Expr>,
1080    pub unshift_expr_entries: Vec<(Expr, Vec<Expr>)>,
1081    pub splice_expr_entries: Vec<SpliceExprEntry>,
1082    /// `map EXPR, LIST` — map expression (list context) with `$_` set to each element.
1083    pub map_expr_entries: Vec<Expr>,
1084    /// When `Some((start, end))`, `map_expr_entries[i]` is lowered like [`Self::grep_expr_bytecode_ranges`].
1085    pub map_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1086    /// `grep EXPR, LIST` — filter expression evaluated with `$_` set to each element.
1087    pub grep_expr_entries: Vec<Expr>,
1088    /// When `Some((start, end))`, `grep_expr_entries[i]` is also lowered to `ops[start..end]`
1089    /// (exclusive `end`) with trailing [`Op::BlockReturnValue`], like [`Self::block_bytecode_ranges`].
1090    pub grep_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1091    /// Right-hand expression for [`Op::RegexFlipFlopExprRhs`] — boolean context (bare `m//` is `$_ =~ m//`).
1092    pub regex_flip_flop_rhs_expr_entries: Vec<Expr>,
1093    /// When `Some((start, end))`, `regex_flip_flop_rhs_expr_entries[i]` is lowered to `ops[start..end]` +
1094    /// [`Op::BlockReturnValue`].
1095    pub regex_flip_flop_rhs_expr_bytecode_ranges: Vec<Option<(usize, usize)>>,
1096    /// Number of flip-flop slots ([`Op::ScalarFlipFlop`], [`Op::RegexFlipFlop`], [`Op::RegexEofFlipFlop`],
1097    /// [`Op::RegexFlipFlopExprRhs`], [`Op::RegexFlipFlopDotLineRhs`]); VM resets flip-flop vectors.
1098    pub flip_flop_slots: u16,
1099    /// `format NAME =` bodies: basename + lines between `=` and `.` (see lexer).
1100    pub format_decls: Vec<(String, Vec<String>)>,
1101    /// `use overload` pair lists (installed into current package at run time).
1102    pub use_overload_entries: Vec<Vec<(String, String)>>,
1103}
1104
1105impl Chunk {
1106    /// Look up a compiled subroutine entry by stash name pool index.
1107    pub fn find_sub_entry(&self, name_idx: u16) -> Option<(usize, bool)> {
1108        self.sub_entries
1109            .iter()
1110            .find(|(n, _, _)| *n == name_idx)
1111            .map(|(_, ip, stack_args)| (*ip, *stack_args))
1112    }
1113
1114    pub fn new() -> Self {
1115        Self {
1116            ops: Vec::with_capacity(256),
1117            constants: Vec::new(),
1118            names: Vec::new(),
1119            lines: Vec::new(),
1120            op_ast_expr: Vec::new(),
1121            ast_expr_pool: Vec::new(),
1122            sub_entries: Vec::new(),
1123            blocks: Vec::new(),
1124            block_bytecode_ranges: Vec::new(),
1125            static_sub_calls: Vec::new(),
1126            lvalues: Vec::new(),
1127            struct_defs: Vec::new(),
1128            enum_defs: Vec::new(),
1129            class_defs: Vec::new(),
1130            trait_defs: Vec::new(),
1131            given_entries: Vec::new(),
1132            given_topic_bytecode_ranges: Vec::new(),
1133            eval_timeout_entries: Vec::new(),
1134            eval_timeout_expr_bytecode_ranges: Vec::new(),
1135            algebraic_match_entries: Vec::new(),
1136            algebraic_match_subject_bytecode_ranges: Vec::new(),
1137            runtime_sub_decls: Vec::new(),
1138            code_ref_sigs: Vec::new(),
1139            par_lines_entries: Vec::new(),
1140            par_walk_entries: Vec::new(),
1141            pwatch_entries: Vec::new(),
1142            substr_four_arg_entries: Vec::new(),
1143            keys_expr_entries: Vec::new(),
1144            keys_expr_bytecode_ranges: Vec::new(),
1145            values_expr_entries: Vec::new(),
1146            values_expr_bytecode_ranges: Vec::new(),
1147            delete_expr_entries: Vec::new(),
1148            exists_expr_entries: Vec::new(),
1149            push_expr_entries: Vec::new(),
1150            pop_expr_entries: Vec::new(),
1151            shift_expr_entries: Vec::new(),
1152            unshift_expr_entries: Vec::new(),
1153            splice_expr_entries: Vec::new(),
1154            map_expr_entries: Vec::new(),
1155            map_expr_bytecode_ranges: Vec::new(),
1156            grep_expr_entries: Vec::new(),
1157            grep_expr_bytecode_ranges: Vec::new(),
1158            regex_flip_flop_rhs_expr_entries: Vec::new(),
1159            regex_flip_flop_rhs_expr_bytecode_ranges: Vec::new(),
1160            flip_flop_slots: 0,
1161            format_decls: Vec::new(),
1162            use_overload_entries: Vec::new(),
1163        }
1164    }
1165
1166    /// Pool index for [`Op::FormatDecl`].
1167    pub fn add_format_decl(&mut self, name: String, lines: Vec<String>) -> u16 {
1168        let idx = self.format_decls.len() as u16;
1169        self.format_decls.push((name, lines));
1170        idx
1171    }
1172
1173    /// Pool index for [`Op::UseOverload`].
1174    pub fn add_use_overload(&mut self, pairs: Vec<(String, String)>) -> u16 {
1175        let idx = self.use_overload_entries.len() as u16;
1176        self.use_overload_entries.push(pairs);
1177        idx
1178    }
1179
1180    /// Allocate a slot index for [`Op::ScalarFlipFlop`] / [`Op::RegexFlipFlop`] / [`Op::RegexEofFlipFlop`] /
1181    /// [`Op::RegexFlipFlopExprRhs`] / [`Op::RegexFlipFlopDotLineRhs`] flip-flop state.
1182    pub fn alloc_flip_flop_slot(&mut self) -> u16 {
1183        let id = self.flip_flop_slots;
1184        self.flip_flop_slots = self.flip_flop_slots.saturating_add(1);
1185        id
1186    }
1187
1188    /// `map EXPR, LIST` — pool index for [`Op::MapWithExpr`].
1189    pub fn add_map_expr_entry(&mut self, expr: Expr) -> u16 {
1190        let idx = self.map_expr_entries.len() as u16;
1191        self.map_expr_entries.push(expr);
1192        idx
1193    }
1194
1195    /// `grep EXPR, LIST` — pool index for [`Op::GrepWithExpr`].
1196    pub fn add_grep_expr_entry(&mut self, expr: Expr) -> u16 {
1197        let idx = self.grep_expr_entries.len() as u16;
1198        self.grep_expr_entries.push(expr);
1199        idx
1200    }
1201
1202    /// Regex flip-flop with compound RHS — pool index for [`Op::RegexFlipFlopExprRhs`].
1203    pub fn add_regex_flip_flop_rhs_expr_entry(&mut self, expr: Expr) -> u16 {
1204        let idx = self.regex_flip_flop_rhs_expr_entries.len() as u16;
1205        self.regex_flip_flop_rhs_expr_entries.push(expr);
1206        idx
1207    }
1208
1209    /// `keys EXPR` (dynamic) — pool index for [`Op::KeysExpr`].
1210    pub fn add_keys_expr_entry(&mut self, expr: Expr) -> u16 {
1211        let idx = self.keys_expr_entries.len() as u16;
1212        self.keys_expr_entries.push(expr);
1213        idx
1214    }
1215
1216    /// `values EXPR` (dynamic) — pool index for [`Op::ValuesExpr`].
1217    pub fn add_values_expr_entry(&mut self, expr: Expr) -> u16 {
1218        let idx = self.values_expr_entries.len() as u16;
1219        self.values_expr_entries.push(expr);
1220        idx
1221    }
1222
1223    /// `delete EXPR` (dynamic operand) — pool index for [`Op::DeleteExpr`].
1224    pub fn add_delete_expr_entry(&mut self, expr: Expr) -> u16 {
1225        let idx = self.delete_expr_entries.len() as u16;
1226        self.delete_expr_entries.push(expr);
1227        idx
1228    }
1229
1230    /// `exists EXPR` (dynamic operand) — pool index for [`Op::ExistsExpr`].
1231    pub fn add_exists_expr_entry(&mut self, expr: Expr) -> u16 {
1232        let idx = self.exists_expr_entries.len() as u16;
1233        self.exists_expr_entries.push(expr);
1234        idx
1235    }
1236
1237    pub fn add_push_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1238        let idx = self.push_expr_entries.len() as u16;
1239        self.push_expr_entries.push((array, values));
1240        idx
1241    }
1242
1243    pub fn add_pop_expr_entry(&mut self, array: Expr) -> u16 {
1244        let idx = self.pop_expr_entries.len() as u16;
1245        self.pop_expr_entries.push(array);
1246        idx
1247    }
1248
1249    pub fn add_shift_expr_entry(&mut self, array: Expr) -> u16 {
1250        let idx = self.shift_expr_entries.len() as u16;
1251        self.shift_expr_entries.push(array);
1252        idx
1253    }
1254
1255    pub fn add_unshift_expr_entry(&mut self, array: Expr, values: Vec<Expr>) -> u16 {
1256        let idx = self.unshift_expr_entries.len() as u16;
1257        self.unshift_expr_entries.push((array, values));
1258        idx
1259    }
1260
1261    pub fn add_splice_expr_entry(
1262        &mut self,
1263        array: Expr,
1264        offset: Option<Expr>,
1265        length: Option<Expr>,
1266        replacement: Vec<Expr>,
1267    ) -> u16 {
1268        let idx = self.splice_expr_entries.len() as u16;
1269        self.splice_expr_entries
1270            .push((array, offset, length, replacement));
1271        idx
1272    }
1273
1274    /// Four-arg `substr` — returns pool index for [`Op::SubstrFourArg`].
1275    pub fn add_substr_four_arg_entry(
1276        &mut self,
1277        string: Expr,
1278        offset: Expr,
1279        length: Option<Expr>,
1280        replacement: Expr,
1281    ) -> u16 {
1282        let idx = self.substr_four_arg_entries.len() as u16;
1283        self.substr_four_arg_entries
1284            .push((string, offset, length, replacement));
1285        idx
1286    }
1287
1288    /// `par_lines PATH, sub { } [, progress => EXPR]` — returns pool index for [`Op::ParLines`].
1289    pub fn add_par_lines_entry(
1290        &mut self,
1291        path: Expr,
1292        callback: Expr,
1293        progress: Option<Expr>,
1294    ) -> u16 {
1295        let idx = self.par_lines_entries.len() as u16;
1296        self.par_lines_entries.push((path, callback, progress));
1297        idx
1298    }
1299
1300    /// `par_walk PATH, sub { } [, progress => EXPR]` — returns pool index for [`Op::ParWalk`].
1301    pub fn add_par_walk_entry(
1302        &mut self,
1303        path: Expr,
1304        callback: Expr,
1305        progress: Option<Expr>,
1306    ) -> u16 {
1307        let idx = self.par_walk_entries.len() as u16;
1308        self.par_walk_entries.push((path, callback, progress));
1309        idx
1310    }
1311
1312    /// `pwatch GLOB, sub { }` — returns pool index for [`Op::Pwatch`].
1313    pub fn add_pwatch_entry(&mut self, path: Expr, callback: Expr) -> u16 {
1314        let idx = self.pwatch_entries.len() as u16;
1315        self.pwatch_entries.push((path, callback));
1316        idx
1317    }
1318
1319    /// `given (EXPR) { ... }` — returns pool index for [`Op::Given`].
1320    pub fn add_given_entry(&mut self, topic: Expr, body: Block) -> u16 {
1321        let idx = self.given_entries.len() as u16;
1322        self.given_entries.push((topic, body));
1323        idx
1324    }
1325
1326    /// `eval_timeout SECS { ... }` — returns pool index for [`Op::EvalTimeout`].
1327    pub fn add_eval_timeout_entry(&mut self, timeout: Expr, body: Block) -> u16 {
1328        let idx = self.eval_timeout_entries.len() as u16;
1329        self.eval_timeout_entries.push((timeout, body));
1330        idx
1331    }
1332
1333    /// Algebraic `match` — returns pool index for [`Op::AlgebraicMatch`].
1334    pub fn add_algebraic_match_entry(&mut self, subject: Expr, arms: Vec<MatchArm>) -> u16 {
1335        let idx = self.algebraic_match_entries.len() as u16;
1336        self.algebraic_match_entries.push((subject, arms));
1337        idx
1338    }
1339
1340    /// Store an AST block and return its index.
1341    pub fn add_block(&mut self, block: Block) -> u16 {
1342        let idx = self.blocks.len() as u16;
1343        self.blocks.push(block);
1344        idx
1345    }
1346
1347    /// Pool index for [`Op::MakeCodeRef`] signature (`stryke` extension); use empty vec for legacy `sub { }`.
1348    pub fn add_code_ref_sig(&mut self, params: Vec<SubSigParam>) -> u16 {
1349        let idx = self.code_ref_sigs.len();
1350        if idx > u16::MAX as usize {
1351            panic!("too many anonymous sub signatures in one chunk");
1352        }
1353        self.code_ref_sigs.push(params);
1354        idx as u16
1355    }
1356
1357    /// Store an assignable expression (LHS of `s///` / `tr///`) and return its index.
1358    pub fn add_lvalue_expr(&mut self, e: Expr) -> u16 {
1359        let idx = self.lvalues.len() as u16;
1360        self.lvalues.push(e);
1361        idx
1362    }
1363
1364    /// Intern a name, returning its pool index.
1365    pub fn intern_name(&mut self, name: &str) -> u16 {
1366        if let Some(idx) = self.names.iter().position(|n| n == name) {
1367            return idx as u16;
1368        }
1369        let idx = self.names.len() as u16;
1370        self.names.push(name.to_string());
1371        idx
1372    }
1373
1374    /// Add a constant to the pool, returning its index.
1375    pub fn add_constant(&mut self, val: PerlValue) -> u16 {
1376        // Dedup string constants
1377        if let Some(ref s) = val.as_str() {
1378            for (i, c) in self.constants.iter().enumerate() {
1379                if let Some(cs) = c.as_str() {
1380                    if cs == *s {
1381                        return i as u16;
1382                    }
1383                }
1384            }
1385        }
1386        let idx = self.constants.len() as u16;
1387        self.constants.push(val);
1388        idx
1389    }
1390
1391    /// Append an op with source line info.
1392    #[inline]
1393    pub fn emit(&mut self, op: Op, line: usize) -> usize {
1394        self.emit_with_ast_idx(op, line, None)
1395    }
1396
1397    /// Like [`Self::emit`] but attach an optional interned AST [`Expr`] pool index (see [`Self::op_ast_expr`]).
1398    #[inline]
1399    pub fn emit_with_ast_idx(&mut self, op: Op, line: usize, ast: Option<u32>) -> usize {
1400        let idx = self.ops.len();
1401        self.ops.push(op);
1402        self.lines.push(line);
1403        self.op_ast_expr.push(ast);
1404        idx
1405    }
1406
1407    /// Resolve the originating expression for an instruction pointer, if recorded.
1408    #[inline]
1409    pub fn ast_expr_at(&self, ip: usize) -> Option<&Expr> {
1410        let id = (*self.op_ast_expr.get(ip)?)?;
1411        self.ast_expr_pool.get(id as usize)
1412    }
1413
1414    /// Patch a jump instruction at `idx` to target the current position.
1415    pub fn patch_jump_here(&mut self, idx: usize) {
1416        let target = self.ops.len();
1417        self.patch_jump_to(idx, target);
1418    }
1419
1420    /// Patch a jump instruction at `idx` to target an explicit op address.
1421    pub fn patch_jump_to(&mut self, idx: usize, target: usize) {
1422        match &mut self.ops[idx] {
1423            Op::Jump(ref mut t)
1424            | Op::JumpIfTrue(ref mut t)
1425            | Op::JumpIfFalse(ref mut t)
1426            | Op::JumpIfFalseKeep(ref mut t)
1427            | Op::JumpIfTrueKeep(ref mut t)
1428            | Op::JumpIfDefinedKeep(ref mut t) => *t = target,
1429            _ => panic!("patch_jump_to on non-jump op at {}", idx),
1430        }
1431    }
1432
1433    pub fn patch_try_push_catch(&mut self, idx: usize, catch_ip: usize) {
1434        match &mut self.ops[idx] {
1435            Op::TryPush { catch_ip: c, .. } => *c = catch_ip,
1436            _ => panic!("patch_try_push_catch on non-TryPush op at {}", idx),
1437        }
1438    }
1439
1440    pub fn patch_try_push_finally(&mut self, idx: usize, finally_ip: Option<usize>) {
1441        match &mut self.ops[idx] {
1442            Op::TryPush { finally_ip: f, .. } => *f = finally_ip,
1443            _ => panic!("patch_try_push_finally on non-TryPush op at {}", idx),
1444        }
1445    }
1446
1447    pub fn patch_try_push_after(&mut self, idx: usize, after_ip: usize) {
1448        match &mut self.ops[idx] {
1449            Op::TryPush { after_ip: a, .. } => *a = after_ip,
1450            _ => panic!("patch_try_push_after on non-TryPush op at {}", idx),
1451        }
1452    }
1453
1454    /// Current op count (next emit position).
1455    #[inline]
1456    pub fn len(&self) -> usize {
1457        self.ops.len()
1458    }
1459
1460    #[inline]
1461    pub fn is_empty(&self) -> bool {
1462        self.ops.is_empty()
1463    }
1464
1465    /// Human-readable listing: subroutine entry points and each op with its source line (javap / `dis`-style).
1466    pub fn disassemble(&self) -> String {
1467        use std::fmt::Write;
1468        let mut out = String::new();
1469        for (i, n) in self.names.iter().enumerate() {
1470            let _ = writeln!(out, "; name[{}] = {}", i, n);
1471        }
1472        let _ = writeln!(out, "; sub_entries:");
1473        for (ni, ip, stack_args) in &self.sub_entries {
1474            let name = self
1475                .names
1476                .get(*ni as usize)
1477                .map(|s| s.as_str())
1478                .unwrap_or("?");
1479            let _ = writeln!(out, ";   {} @ {} stack_args={}", name, ip, stack_args);
1480        }
1481        for (i, op) in self.ops.iter().enumerate() {
1482            let line = self.lines.get(i).copied().unwrap_or(0);
1483            let ast = self
1484                .op_ast_expr
1485                .get(i)
1486                .copied()
1487                .flatten()
1488                .map(|id| id.to_string())
1489                .unwrap_or_else(|| "-".into());
1490            let _ = writeln!(out, "{:04} {:>5} {:>6}  {:?}", i, line, ast, op);
1491        }
1492        out
1493    }
1494
1495    /// Peephole pass: fuse common multi-op sequences into single superinstructions,
1496    /// then compact by removing Nop slots and remapping all jump targets.
1497    pub fn peephole_fuse(&mut self) {
1498        let len = self.ops.len();
1499        if len < 2 {
1500            return;
1501        }
1502        // Pass 1: fuse OP + Pop → OPVoid
1503        let mut i = 0;
1504        while i + 1 < len {
1505            if matches!(self.ops[i + 1], Op::Pop) {
1506                let replacement = match &self.ops[i] {
1507                    Op::AddAssignSlotSlot(d, s) => Some(Op::AddAssignSlotSlotVoid(*d, *s)),
1508                    Op::PreIncSlot(s) => Some(Op::PreIncSlotVoid(*s)),
1509                    Op::ConcatAppendSlot(s) => Some(Op::ConcatAppendSlotVoid(*s)),
1510                    _ => None,
1511                };
1512                if let Some(op) = replacement {
1513                    self.ops[i] = op;
1514                    self.ops[i + 1] = Op::Nop;
1515                    i += 2;
1516                    continue;
1517                }
1518            }
1519            i += 1;
1520        }
1521        // Pass 2: fuse multi-op patterns
1522        let len = self.ops.len();
1523        if len >= 4 {
1524            i = 0;
1525            while i + 3 < len {
1526                if let (
1527                    Op::GetScalarSlot(slot),
1528                    Op::LoadInt(n),
1529                    Op::NumLt,
1530                    Op::JumpIfFalse(target),
1531                ) = (
1532                    &self.ops[i],
1533                    &self.ops[i + 1],
1534                    &self.ops[i + 2],
1535                    &self.ops[i + 3],
1536                ) {
1537                    if let Ok(n32) = i32::try_from(*n) {
1538                        let slot = *slot;
1539                        let target = *target;
1540                        self.ops[i] = Op::SlotLtIntJumpIfFalse(slot, n32, target);
1541                        self.ops[i + 1] = Op::Nop;
1542                        self.ops[i + 2] = Op::Nop;
1543                        self.ops[i + 3] = Op::Nop;
1544                        i += 4;
1545                        continue;
1546                    }
1547                }
1548                i += 1;
1549            }
1550        }
1551        // Compact once so that pass 3 sees a Nop-free op stream and can match
1552        // adjacent `PreIncSlotVoid + Jump` backedges produced by passes 1/2.
1553        self.compact_nops();
1554        // Pass 3: fuse loop backedge
1555        //   PreIncSlotVoid(s)  + Jump(top)
1556        // where ops[top] is SlotLtIntJumpIfFalse(s, limit, exit)
1557        // becomes
1558        //   SlotIncLtIntJumpBack(s, limit, top + 1)   // body falls through
1559        //   Nop                                       // was Jump
1560        // The first-iteration check at `top` is still reached from before the loop
1561        // (the loop's initial entry goes through the top test), so leaving
1562        // SlotLtIntJumpIfFalse in place keeps the entry path correct. All
1563        // subsequent iterations now skip both the inc op and the jump.
1564        let len = self.ops.len();
1565        if len >= 2 {
1566            let mut i = 0;
1567            while i + 1 < len {
1568                if let (Op::PreIncSlotVoid(s), Op::Jump(top)) = (&self.ops[i], &self.ops[i + 1]) {
1569                    let slot = *s;
1570                    let top = *top;
1571                    // Only fuse backward branches — the C-style `for` shape where `top` is
1572                    // the loop's `SlotLtIntJumpIfFalse` test and the body falls through to
1573                    // this trailing increment. A forward `Jump` that happens to land on a
1574                    // similar test is not the same shape and must not be rewritten.
1575                    if top < i {
1576                        if let Op::SlotLtIntJumpIfFalse(tslot, limit, exit) = &self.ops[top] {
1577                            // Safety: the top test's exit target must equal the fused op's
1578                            // fall-through (i + 2). Otherwise exiting the loop via
1579                            // "condition false" would land somewhere the unfused shape never
1580                            // exited to.
1581                            if *tslot == slot && *exit == i + 2 {
1582                                let limit = *limit;
1583                                let body_target = top + 1;
1584                                self.ops[i] = Op::SlotIncLtIntJumpBack(slot, limit, body_target);
1585                                self.ops[i + 1] = Op::Nop;
1586                                i += 2;
1587                                continue;
1588                            }
1589                        }
1590                    }
1591                }
1592                i += 1;
1593            }
1594        }
1595        // Pass 4: compact again — remove the Nops introduced by pass 3.
1596        self.compact_nops();
1597        // Pass 5: fuse counted-loop bodies down to a single native superinstruction.
1598        //
1599        // After pass 3 + compact, a `for (my $i = ..; $i < N; $i = $i + 1) { $sum += $i }`
1600        // loop looks like:
1601        //
1602        //     [top]        SlotLtIntJumpIfFalse(i, N, exit)
1603        //     [body_start] AddAssignSlotSlotVoid(sum, i)       ← target of the backedge
1604        //                  SlotIncLtIntJumpBack(i, N, body_start)
1605        //     [exit]       ...
1606        //
1607        // When the body is exactly one op, we fuse the AddAssign + backedge into
1608        // `AccumSumLoop(sum, i, N)`, whose handler runs the whole remaining loop in a
1609        // tight Rust `while`. Same scheme for the counted `$s .= CONST` pattern, fused
1610        // into `ConcatConstSlotLoop`.
1611        //
1612        // Safety gate: only fire when no op jumps *into* the body (other than the backedge
1613        // itself and the top test's fall-through, which isn't a jump). That keeps loops with
1614        // interior labels / `last LABEL` / `next LABEL` from being silently skipped.
1615        let len = self.ops.len();
1616        if len >= 2 {
1617            let has_inbound_jump = |ops: &[Op], pos: usize, ignore: usize| -> bool {
1618                for (j, op) in ops.iter().enumerate() {
1619                    if j == ignore {
1620                        continue;
1621                    }
1622                    let t = match op {
1623                        Op::Jump(t)
1624                        | Op::JumpIfFalse(t)
1625                        | Op::JumpIfTrue(t)
1626                        | Op::JumpIfFalseKeep(t)
1627                        | Op::JumpIfTrueKeep(t)
1628                        | Op::JumpIfDefinedKeep(t) => Some(*t),
1629                        Op::SlotLtIntJumpIfFalse(_, _, t) => Some(*t),
1630                        Op::SlotIncLtIntJumpBack(_, _, t) => Some(*t),
1631                        _ => None,
1632                    };
1633                    if t == Some(pos) {
1634                        return true;
1635                    }
1636                }
1637                false
1638            };
1639            // 5a: AddAssignSlotSlotVoid + SlotIncLtIntJumpBack → AccumSumLoop
1640            let mut i = 0;
1641            while i + 1 < len {
1642                if let (
1643                    Op::AddAssignSlotSlotVoid(sum_slot, src_slot),
1644                    Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1645                ) = (&self.ops[i], &self.ops[i + 1])
1646                {
1647                    if *src_slot == *inc_slot
1648                        && *body_target == i
1649                        && !has_inbound_jump(&self.ops, i, i + 1)
1650                        && !has_inbound_jump(&self.ops, i + 1, i + 1)
1651                    {
1652                        let sum_slot = *sum_slot;
1653                        let src_slot = *src_slot;
1654                        let limit = *limit;
1655                        self.ops[i] = Op::AccumSumLoop(sum_slot, src_slot, limit);
1656                        self.ops[i + 1] = Op::Nop;
1657                        i += 2;
1658                        continue;
1659                    }
1660                }
1661                i += 1;
1662            }
1663            // 5b: LoadConst + ConcatAppendSlotVoid + SlotIncLtIntJumpBack → ConcatConstSlotLoop
1664            if len >= 3 {
1665                let mut i = 0;
1666                while i + 2 < len {
1667                    if let (
1668                        Op::LoadConst(const_idx),
1669                        Op::ConcatAppendSlotVoid(s_slot),
1670                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1671                    ) = (&self.ops[i], &self.ops[i + 1], &self.ops[i + 2])
1672                    {
1673                        if *body_target == i
1674                            && !has_inbound_jump(&self.ops, i, i + 2)
1675                            && !has_inbound_jump(&self.ops, i + 1, i + 2)
1676                            && !has_inbound_jump(&self.ops, i + 2, i + 2)
1677                        {
1678                            let const_idx = *const_idx;
1679                            let s_slot = *s_slot;
1680                            let inc_slot = *inc_slot;
1681                            let limit = *limit;
1682                            self.ops[i] =
1683                                Op::ConcatConstSlotLoop(const_idx, s_slot, inc_slot, limit);
1684                            self.ops[i + 1] = Op::Nop;
1685                            self.ops[i + 2] = Op::Nop;
1686                            i += 3;
1687                            continue;
1688                        }
1689                    }
1690                    i += 1;
1691                }
1692            }
1693            // 5e: `$sum += $h{$k}` body op inside `for my $k (keys %h) { ... }`
1694            //   GetScalarSlot(sum) + GetScalarPlain(k) + GetHashElem(h) + Add
1695            //     + SetScalarSlotKeep(sum) + Pop
1696            //   → AddHashElemPlainKeyToSlot(sum, k, h)
1697            // Safe because `SetScalarSlotKeep + Pop` leaves nothing on the stack net; the fused
1698            // op is a drop-in for that sequence. No inbound jumps permitted to interior ops.
1699            if len >= 6 {
1700                let mut i = 0;
1701                while i + 5 < len {
1702                    if let (
1703                        Op::GetScalarSlot(sum_slot),
1704                        Op::GetScalarPlain(k_idx),
1705                        Op::GetHashElem(h_idx),
1706                        Op::Add,
1707                        Op::SetScalarSlotKeep(sum_slot2),
1708                        Op::Pop,
1709                    ) = (
1710                        &self.ops[i],
1711                        &self.ops[i + 1],
1712                        &self.ops[i + 2],
1713                        &self.ops[i + 3],
1714                        &self.ops[i + 4],
1715                        &self.ops[i + 5],
1716                    ) {
1717                        if *sum_slot == *sum_slot2
1718                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1719                        {
1720                            let sum_slot = *sum_slot;
1721                            let k_idx = *k_idx;
1722                            let h_idx = *h_idx;
1723                            self.ops[i] = Op::AddHashElemPlainKeyToSlot(sum_slot, k_idx, h_idx);
1724                            for off in 1..=5 {
1725                                self.ops[i + off] = Op::Nop;
1726                            }
1727                            i += 6;
1728                            continue;
1729                        }
1730                    }
1731                    i += 1;
1732                }
1733            }
1734            // 5e-slot: slot-key variant of 5e, emitted when the compiler lowers `$k` (the foreach
1735            // loop variable) into a slot rather than a frame scalar.
1736            //   GetScalarSlot(sum) + GetScalarSlot(k) + GetHashElem(h) + Add
1737            //     + SetScalarSlotKeep(sum) + Pop
1738            //   → AddHashElemSlotKeyToSlot(sum, k, h)
1739            if len >= 6 {
1740                let mut i = 0;
1741                while i + 5 < len {
1742                    if let (
1743                        Op::GetScalarSlot(sum_slot),
1744                        Op::GetScalarSlot(k_slot),
1745                        Op::GetHashElem(h_idx),
1746                        Op::Add,
1747                        Op::SetScalarSlotKeep(sum_slot2),
1748                        Op::Pop,
1749                    ) = (
1750                        &self.ops[i],
1751                        &self.ops[i + 1],
1752                        &self.ops[i + 2],
1753                        &self.ops[i + 3],
1754                        &self.ops[i + 4],
1755                        &self.ops[i + 5],
1756                    ) {
1757                        if *sum_slot == *sum_slot2
1758                            && *sum_slot != *k_slot
1759                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, usize::MAX))
1760                        {
1761                            let sum_slot = *sum_slot;
1762                            let k_slot = *k_slot;
1763                            let h_idx = *h_idx;
1764                            self.ops[i] = Op::AddHashElemSlotKeyToSlot(sum_slot, k_slot, h_idx);
1765                            for off in 1..=5 {
1766                                self.ops[i + off] = Op::Nop;
1767                            }
1768                            i += 6;
1769                            continue;
1770                        }
1771                    }
1772                    i += 1;
1773                }
1774            }
1775            // 5d: counted hash-insert loop `$h{$i} = $i * K`
1776            //   GetScalarSlot(i) + LoadInt(k) + Mul + GetScalarSlot(i) + SetHashElem(h) + Pop
1777            //     + SlotIncLtIntJumpBack(i, limit, body_target)
1778            //   → SetHashIntTimesLoop(h, i, k, limit)
1779            if len >= 7 {
1780                let mut i = 0;
1781                while i + 6 < len {
1782                    if let (
1783                        Op::GetScalarSlot(gs1),
1784                        Op::LoadInt(k),
1785                        Op::Mul,
1786                        Op::GetScalarSlot(gs2),
1787                        Op::SetHashElem(h_idx),
1788                        Op::Pop,
1789                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1790                    ) = (
1791                        &self.ops[i],
1792                        &self.ops[i + 1],
1793                        &self.ops[i + 2],
1794                        &self.ops[i + 3],
1795                        &self.ops[i + 4],
1796                        &self.ops[i + 5],
1797                        &self.ops[i + 6],
1798                    ) {
1799                        if *gs1 == *inc_slot
1800                            && *gs2 == *inc_slot
1801                            && *body_target == i
1802                            && i32::try_from(*k).is_ok()
1803                            && (0..6).all(|off| !has_inbound_jump(&self.ops, i + off, i + 6))
1804                            && !has_inbound_jump(&self.ops, i + 6, i + 6)
1805                        {
1806                            let h_idx = *h_idx;
1807                            let inc_slot = *inc_slot;
1808                            let k32 = *k as i32;
1809                            let limit = *limit;
1810                            self.ops[i] = Op::SetHashIntTimesLoop(h_idx, inc_slot, k32, limit);
1811                            for off in 1..=6 {
1812                                self.ops[i + off] = Op::Nop;
1813                            }
1814                            i += 7;
1815                            continue;
1816                        }
1817                    }
1818                    i += 1;
1819                }
1820            }
1821            // 5c: GetScalarSlot + PushArray + ArrayLen + Pop + SlotIncLtIntJumpBack
1822            //      → PushIntRangeToArrayLoop
1823            // This is the compiler's `push @a, $i; $i++` shape in void context, where
1824            // the `push` expression's length return is pushed by `ArrayLen` and then `Pop`ped.
1825            if len >= 5 {
1826                let mut i = 0;
1827                while i + 4 < len {
1828                    if let (
1829                        Op::GetScalarSlot(get_slot),
1830                        Op::PushArray(push_idx),
1831                        Op::ArrayLen(len_idx),
1832                        Op::Pop,
1833                        Op::SlotIncLtIntJumpBack(inc_slot, limit, body_target),
1834                    ) = (
1835                        &self.ops[i],
1836                        &self.ops[i + 1],
1837                        &self.ops[i + 2],
1838                        &self.ops[i + 3],
1839                        &self.ops[i + 4],
1840                    ) {
1841                        if *get_slot == *inc_slot
1842                            && *push_idx == *len_idx
1843                            && *body_target == i
1844                            && !has_inbound_jump(&self.ops, i, i + 4)
1845                            && !has_inbound_jump(&self.ops, i + 1, i + 4)
1846                            && !has_inbound_jump(&self.ops, i + 2, i + 4)
1847                            && !has_inbound_jump(&self.ops, i + 3, i + 4)
1848                            && !has_inbound_jump(&self.ops, i + 4, i + 4)
1849                        {
1850                            let push_idx = *push_idx;
1851                            let inc_slot = *inc_slot;
1852                            let limit = *limit;
1853                            self.ops[i] = Op::PushIntRangeToArrayLoop(push_idx, inc_slot, limit);
1854                            self.ops[i + 1] = Op::Nop;
1855                            self.ops[i + 2] = Op::Nop;
1856                            self.ops[i + 3] = Op::Nop;
1857                            self.ops[i + 4] = Op::Nop;
1858                            i += 5;
1859                            continue;
1860                        }
1861                    }
1862                    i += 1;
1863                }
1864            }
1865        }
1866        // Pass 6: compact — remove the Nops pass 5 introduced.
1867        self.compact_nops();
1868        // Pass 7: fuse the entire `for my $k (keys %h) { $sum += $h{$k} }` loop into a single
1869        // `SumHashValuesToSlot` op that walks the hash's values in a tight native loop.
1870        //
1871        // After prior passes and compaction the shape is a 15-op block:
1872        //
1873        //     HashKeys(h)
1874        //     DeclareArray(list)
1875        //     LoadInt(0)
1876        //     DeclareScalarSlot(c, cname)
1877        //     LoadUndef
1878        //     DeclareScalarSlot(v, vname)
1879        //     [top]  GetScalarSlot(c)
1880        //            ArrayLen(list)
1881        //            NumLt
1882        //            JumpIfFalse(end)
1883        //            GetScalarSlot(c)
1884        //            GetArrayElem(list)
1885        //            SetScalarSlot(v)
1886        //            AddHashElemSlotKeyToSlot(sum, v, h)     ← fused body (pass 5e-slot)
1887        //            PreIncSlotVoid(c)
1888        //            Jump(top)
1889        //     [end]
1890        //
1891        // The counter (`__foreach_i__`), list (`__foreach_list__`), and loop var (`$k`) live
1892        // inside a `PushFrame`-isolated scope and are invisible after the loop — it is safe to
1893        // elide all of them. The fused op accumulates directly into `sum` without creating the
1894        // keys array at all.
1895        //
1896        // Safety gates:
1897        //   - `h` in HashKeys must match `h` in AddHashElemSlotKeyToSlot.
1898        //   - `list` in DeclareArray must match the loop `ArrayLen` / `GetArrayElem`.
1899        //   - `c` / `v` slots must be consistent throughout.
1900        //   - No inbound jump lands inside the 15-op window from the outside.
1901        //   - JumpIfFalse target must be i+15 (just past the Jump back-edge).
1902        //   - Jump back-edge target must be i+6 (the GetScalarSlot(c) at loop top).
1903        let len = self.ops.len();
1904        if len >= 15 {
1905            let has_inbound_jump =
1906                |ops: &[Op], pos: usize, ignore_from: usize, ignore_to: usize| -> bool {
1907                    for (j, op) in ops.iter().enumerate() {
1908                        if j >= ignore_from && j <= ignore_to {
1909                            continue;
1910                        }
1911                        let t = match op {
1912                            Op::Jump(t)
1913                            | Op::JumpIfFalse(t)
1914                            | Op::JumpIfTrue(t)
1915                            | Op::JumpIfFalseKeep(t)
1916                            | Op::JumpIfTrueKeep(t)
1917                            | Op::JumpIfDefinedKeep(t) => *t,
1918                            Op::SlotLtIntJumpIfFalse(_, _, t) => *t,
1919                            Op::SlotIncLtIntJumpBack(_, _, t) => *t,
1920                            _ => continue,
1921                        };
1922                        if t == pos {
1923                            return true;
1924                        }
1925                    }
1926                    false
1927                };
1928            let mut i = 0;
1929            while i + 15 < len {
1930                if let (
1931                    Op::HashKeys(h_idx),
1932                    Op::DeclareArray(list_idx),
1933                    Op::LoadInt(0),
1934                    Op::DeclareScalarSlot(c_slot, _c_name),
1935                    Op::LoadUndef,
1936                    Op::DeclareScalarSlot(v_slot, _v_name),
1937                    Op::GetScalarSlot(c_get1),
1938                    Op::ArrayLen(len_idx),
1939                    Op::NumLt,
1940                    Op::JumpIfFalse(end_tgt),
1941                    Op::GetScalarSlot(c_get2),
1942                    Op::GetArrayElem(elem_idx),
1943                    Op::SetScalarSlot(v_set),
1944                    Op::AddHashElemSlotKeyToSlot(sum_slot, v_in_body, h_in_body),
1945                    Op::PreIncSlotVoid(c_inc),
1946                    Op::Jump(top_tgt),
1947                ) = (
1948                    &self.ops[i],
1949                    &self.ops[i + 1],
1950                    &self.ops[i + 2],
1951                    &self.ops[i + 3],
1952                    &self.ops[i + 4],
1953                    &self.ops[i + 5],
1954                    &self.ops[i + 6],
1955                    &self.ops[i + 7],
1956                    &self.ops[i + 8],
1957                    &self.ops[i + 9],
1958                    &self.ops[i + 10],
1959                    &self.ops[i + 11],
1960                    &self.ops[i + 12],
1961                    &self.ops[i + 13],
1962                    &self.ops[i + 14],
1963                    &self.ops[i + 15],
1964                ) {
1965                    let full_end = i + 15;
1966                    if *list_idx == *len_idx
1967                        && *list_idx == *elem_idx
1968                        && *c_slot == *c_get1
1969                        && *c_slot == *c_get2
1970                        && *c_slot == *c_inc
1971                        && *v_slot == *v_set
1972                        && *v_slot == *v_in_body
1973                        && *h_idx == *h_in_body
1974                        && *top_tgt == i + 6
1975                        && *end_tgt == i + 16
1976                        && *sum_slot != *c_slot
1977                        && *sum_slot != *v_slot
1978                        && !(i..=full_end).any(|k| has_inbound_jump(&self.ops, k, i, full_end))
1979                    {
1980                        let sum_slot = *sum_slot;
1981                        let h_idx = *h_idx;
1982                        self.ops[i] = Op::SumHashValuesToSlot(sum_slot, h_idx);
1983                        for off in 1..=15 {
1984                            self.ops[i + off] = Op::Nop;
1985                        }
1986                        i += 16;
1987                        continue;
1988                    }
1989                }
1990                i += 1;
1991            }
1992        }
1993        // Pass 8: compact pass 7's Nops.
1994        self.compact_nops();
1995    }
1996
1997    /// Remove all `Nop` instructions and remap jump targets + metadata indices.
1998    fn compact_nops(&mut self) {
1999        let old_len = self.ops.len();
2000        // Build old→new index mapping.
2001        let mut remap = vec![0usize; old_len + 1];
2002        let mut new_idx = 0usize;
2003        for (old, slot) in remap[..old_len].iter_mut().enumerate() {
2004            *slot = new_idx;
2005            if !matches!(self.ops[old], Op::Nop) {
2006                new_idx += 1;
2007            }
2008        }
2009        remap[old_len] = new_idx;
2010        if new_idx == old_len {
2011            return; // nothing to compact
2012        }
2013        // Remap jump targets in all ops.
2014        for op in &mut self.ops {
2015            match op {
2016                Op::Jump(t) | Op::JumpIfFalse(t) | Op::JumpIfTrue(t) => *t = remap[*t],
2017                Op::JumpIfTrueKeep(t) | Op::JumpIfDefinedKeep(t) => *t = remap[*t],
2018                Op::SlotLtIntJumpIfFalse(_, _, t) => *t = remap[*t],
2019                Op::SlotIncLtIntJumpBack(_, _, t) => *t = remap[*t],
2020                _ => {}
2021            }
2022        }
2023        // Remap sub entry points.
2024        for e in &mut self.sub_entries {
2025            e.1 = remap[e.1];
2026        }
2027        // Remap `CallStaticSubId` resolved entry IPs — they were recorded by
2028        // `patch_static_sub_calls` before peephole fusion ran, so any Nop
2029        // removal in front of a sub body shifts its entry and must be
2030        // reflected here; otherwise `vm_dispatch_user_call` jumps one (or
2031        // more) ops past the real sub start and silently skips the first
2032        // instruction(s) of the body.
2033        for c in &mut self.static_sub_calls {
2034            c.0 = remap[c.0];
2035        }
2036        // Remap block/grep/sort/etc bytecode ranges.
2037        fn remap_ranges(ranges: &mut [Option<(usize, usize)>], remap: &[usize]) {
2038            for r in ranges.iter_mut().flatten() {
2039                r.0 = remap[r.0];
2040                r.1 = remap[r.1];
2041            }
2042        }
2043        remap_ranges(&mut self.block_bytecode_ranges, &remap);
2044        remap_ranges(&mut self.map_expr_bytecode_ranges, &remap);
2045        remap_ranges(&mut self.grep_expr_bytecode_ranges, &remap);
2046        remap_ranges(&mut self.keys_expr_bytecode_ranges, &remap);
2047        remap_ranges(&mut self.values_expr_bytecode_ranges, &remap);
2048        remap_ranges(&mut self.eval_timeout_expr_bytecode_ranges, &remap);
2049        remap_ranges(&mut self.given_topic_bytecode_ranges, &remap);
2050        remap_ranges(&mut self.algebraic_match_subject_bytecode_ranges, &remap);
2051        remap_ranges(&mut self.regex_flip_flop_rhs_expr_bytecode_ranges, &remap);
2052        // Compact ops, lines, op_ast_expr.
2053        let mut j = 0;
2054        for old in 0..old_len {
2055            if !matches!(self.ops[old], Op::Nop) {
2056                self.ops[j] = self.ops[old].clone();
2057                if old < self.lines.len() && j < self.lines.len() {
2058                    self.lines[j] = self.lines[old];
2059                }
2060                if old < self.op_ast_expr.len() && j < self.op_ast_expr.len() {
2061                    self.op_ast_expr[j] = self.op_ast_expr[old];
2062                }
2063                j += 1;
2064            }
2065        }
2066        self.ops.truncate(j);
2067        self.lines.truncate(j);
2068        self.op_ast_expr.truncate(j);
2069    }
2070}
2071
2072impl Default for Chunk {
2073    fn default() -> Self {
2074        Self::new()
2075    }
2076}
2077
2078#[cfg(test)]
2079mod tests {
2080    use super::*;
2081    use crate::ast;
2082
2083    #[test]
2084    fn chunk_new_and_default_match() {
2085        let a = Chunk::new();
2086        let b = Chunk::default();
2087        assert!(a.ops.is_empty() && a.names.is_empty() && a.constants.is_empty());
2088        assert!(b.ops.is_empty() && b.lines.is_empty());
2089    }
2090
2091    #[test]
2092    fn intern_name_deduplicates() {
2093        let mut c = Chunk::new();
2094        let i0 = c.intern_name("foo");
2095        let i1 = c.intern_name("foo");
2096        let i2 = c.intern_name("bar");
2097        assert_eq!(i0, i1);
2098        assert_ne!(i0, i2);
2099        assert_eq!(c.names.len(), 2);
2100    }
2101
2102    #[test]
2103    fn add_constant_dedups_identical_strings() {
2104        let mut c = Chunk::new();
2105        let a = c.add_constant(PerlValue::string("x".into()));
2106        let b = c.add_constant(PerlValue::string("x".into()));
2107        assert_eq!(a, b);
2108        assert_eq!(c.constants.len(), 1);
2109    }
2110
2111    #[test]
2112    fn add_constant_distinct_strings_different_indices() {
2113        let mut c = Chunk::new();
2114        let a = c.add_constant(PerlValue::string("a".into()));
2115        let b = c.add_constant(PerlValue::string("b".into()));
2116        assert_ne!(a, b);
2117        assert_eq!(c.constants.len(), 2);
2118    }
2119
2120    #[test]
2121    fn add_constant_non_string_no_dedup_scan() {
2122        let mut c = Chunk::new();
2123        let a = c.add_constant(PerlValue::integer(1));
2124        let b = c.add_constant(PerlValue::integer(1));
2125        assert_ne!(a, b);
2126        assert_eq!(c.constants.len(), 2);
2127    }
2128
2129    #[test]
2130    fn emit_records_parallel_ops_and_lines() {
2131        let mut c = Chunk::new();
2132        c.emit(Op::LoadInt(1), 10);
2133        c.emit(Op::Pop, 11);
2134        assert_eq!(c.len(), 2);
2135        assert_eq!(c.lines, vec![10, 11]);
2136        assert_eq!(c.op_ast_expr, vec![None, None]);
2137        assert!(!c.is_empty());
2138    }
2139
2140    #[test]
2141    fn len_is_empty_track_ops() {
2142        let mut c = Chunk::new();
2143        assert!(c.is_empty());
2144        assert_eq!(c.len(), 0);
2145        c.emit(Op::Halt, 0);
2146        assert!(!c.is_empty());
2147        assert_eq!(c.len(), 1);
2148    }
2149
2150    #[test]
2151    fn patch_jump_here_updates_jump_target() {
2152        let mut c = Chunk::new();
2153        let j = c.emit(Op::Jump(0), 1);
2154        c.emit(Op::LoadInt(99), 2);
2155        c.patch_jump_here(j);
2156        assert_eq!(c.ops.len(), 2);
2157        assert!(matches!(c.ops[j], Op::Jump(2)));
2158    }
2159
2160    #[test]
2161    fn patch_jump_here_jump_if_true() {
2162        let mut c = Chunk::new();
2163        let j = c.emit(Op::JumpIfTrue(0), 1);
2164        c.emit(Op::Halt, 2);
2165        c.patch_jump_here(j);
2166        assert!(matches!(c.ops[j], Op::JumpIfTrue(2)));
2167    }
2168
2169    #[test]
2170    fn patch_jump_here_jump_if_false_keep() {
2171        let mut c = Chunk::new();
2172        let j = c.emit(Op::JumpIfFalseKeep(0), 1);
2173        c.emit(Op::Pop, 2);
2174        c.patch_jump_here(j);
2175        assert!(matches!(c.ops[j], Op::JumpIfFalseKeep(2)));
2176    }
2177
2178    #[test]
2179    fn patch_jump_here_jump_if_true_keep() {
2180        let mut c = Chunk::new();
2181        let j = c.emit(Op::JumpIfTrueKeep(0), 1);
2182        c.emit(Op::Pop, 2);
2183        c.patch_jump_here(j);
2184        assert!(matches!(c.ops[j], Op::JumpIfTrueKeep(2)));
2185    }
2186
2187    #[test]
2188    fn patch_jump_here_jump_if_defined_keep() {
2189        let mut c = Chunk::new();
2190        let j = c.emit(Op::JumpIfDefinedKeep(0), 1);
2191        c.emit(Op::Halt, 2);
2192        c.patch_jump_here(j);
2193        assert!(matches!(c.ops[j], Op::JumpIfDefinedKeep(2)));
2194    }
2195
2196    #[test]
2197    #[should_panic(expected = "patch_jump_to on non-jump op")]
2198    fn patch_jump_here_panics_on_non_jump() {
2199        let mut c = Chunk::new();
2200        let idx = c.emit(Op::LoadInt(1), 1);
2201        c.patch_jump_here(idx);
2202    }
2203
2204    #[test]
2205    fn add_block_returns_sequential_indices() {
2206        let mut c = Chunk::new();
2207        let b0: ast::Block = vec![];
2208        let b1: ast::Block = vec![];
2209        assert_eq!(c.add_block(b0), 0);
2210        assert_eq!(c.add_block(b1), 1);
2211        assert_eq!(c.blocks.len(), 2);
2212    }
2213
2214    #[test]
2215    fn builtin_id_from_u16_first_and_last() {
2216        assert_eq!(BuiltinId::from_u16(0), Some(BuiltinId::Length));
2217        assert_eq!(
2218            BuiltinId::from_u16(BuiltinId::Pselect as u16),
2219            Some(BuiltinId::Pselect)
2220        );
2221        assert_eq!(
2222            BuiltinId::from_u16(BuiltinId::BarrierNew as u16),
2223            Some(BuiltinId::BarrierNew)
2224        );
2225        assert_eq!(
2226            BuiltinId::from_u16(BuiltinId::ParPipeline as u16),
2227            Some(BuiltinId::ParPipeline)
2228        );
2229        assert_eq!(
2230            BuiltinId::from_u16(BuiltinId::GlobParProgress as u16),
2231            Some(BuiltinId::GlobParProgress)
2232        );
2233        assert_eq!(
2234            BuiltinId::from_u16(BuiltinId::Readpipe as u16),
2235            Some(BuiltinId::Readpipe)
2236        );
2237        assert_eq!(
2238            BuiltinId::from_u16(BuiltinId::ReadLineList as u16),
2239            Some(BuiltinId::ReadLineList)
2240        );
2241        assert_eq!(
2242            BuiltinId::from_u16(BuiltinId::ReaddirList as u16),
2243            Some(BuiltinId::ReaddirList)
2244        );
2245        assert_eq!(
2246            BuiltinId::from_u16(BuiltinId::Ssh as u16),
2247            Some(BuiltinId::Ssh)
2248        );
2249        assert_eq!(
2250            BuiltinId::from_u16(BuiltinId::Pipe as u16),
2251            Some(BuiltinId::Pipe)
2252        );
2253        assert_eq!(
2254            BuiltinId::from_u16(BuiltinId::Files as u16),
2255            Some(BuiltinId::Files)
2256        );
2257        assert_eq!(
2258            BuiltinId::from_u16(BuiltinId::Filesf as u16),
2259            Some(BuiltinId::Filesf)
2260        );
2261        assert_eq!(
2262            BuiltinId::from_u16(BuiltinId::Dirs as u16),
2263            Some(BuiltinId::Dirs)
2264        );
2265        assert_eq!(
2266            BuiltinId::from_u16(BuiltinId::SymLinks as u16),
2267            Some(BuiltinId::SymLinks)
2268        );
2269        assert_eq!(
2270            BuiltinId::from_u16(BuiltinId::Sockets as u16),
2271            Some(BuiltinId::Sockets)
2272        );
2273        assert_eq!(
2274            BuiltinId::from_u16(BuiltinId::Pipes as u16),
2275            Some(BuiltinId::Pipes)
2276        );
2277        assert_eq!(
2278            BuiltinId::from_u16(BuiltinId::BlockDevices as u16),
2279            Some(BuiltinId::BlockDevices)
2280        );
2281        assert_eq!(
2282            BuiltinId::from_u16(BuiltinId::CharDevices as u16),
2283            Some(BuiltinId::CharDevices)
2284        );
2285    }
2286
2287    #[test]
2288    fn builtin_id_from_u16_out_of_range() {
2289        assert_eq!(BuiltinId::from_u16(BuiltinId::CharDevices as u16 + 1), None);
2290        assert_eq!(BuiltinId::from_u16(u16::MAX), None);
2291    }
2292
2293    #[test]
2294    fn op_enum_clone_roundtrip() {
2295        let o = Op::Call(42, 3, 0);
2296        assert!(matches!(o.clone(), Op::Call(42, 3, 0)));
2297    }
2298
2299    #[test]
2300    fn chunk_clone_independent_ops() {
2301        let mut c = Chunk::new();
2302        c.emit(Op::Negate, 1);
2303        let mut d = c.clone();
2304        d.emit(Op::Pop, 2);
2305        assert_eq!(c.len(), 1);
2306        assert_eq!(d.len(), 2);
2307    }
2308
2309    #[test]
2310    fn chunk_disassemble_includes_ops() {
2311        let mut c = Chunk::new();
2312        c.emit(Op::LoadInt(7), 1);
2313        let s = c.disassemble();
2314        assert!(s.contains("0000"));
2315        assert!(s.contains("LoadInt(7)"));
2316        assert!(s.contains("     -")); // no ast ref column
2317    }
2318
2319    #[test]
2320    fn ast_expr_at_roundtrips_pooled_expr() {
2321        let mut c = Chunk::new();
2322        let e = ast::Expr {
2323            kind: ast::ExprKind::Integer(99),
2324            line: 3,
2325        };
2326        c.ast_expr_pool.push(e);
2327        c.emit_with_ast_idx(Op::LoadInt(99), 3, Some(0));
2328        let got = c.ast_expr_at(0).expect("ast ref");
2329        assert!(matches!(&got.kind, ast::ExprKind::Integer(99)));
2330        assert_eq!(got.line, 3);
2331    }
2332}