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