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