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