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