Skip to main content

stryke/
bytecode.rs

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