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