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