pub enum Op {
Show 82 variants
PushConst(u32),
Pop,
Dup,
LoadLocal(u16),
StoreLocal(u16),
MakeRecord {
shape_idx: u32,
field_count: u16,
},
AllocStackRecord {
shape_idx: u32,
field_count: u16,
},
AllocArenaRecord {
shape_idx: u32,
field_count: u16,
},
MakeTuple(u16),
AllocStackTuple {
arity: u16,
},
AllocArenaTuple {
arity: u16,
},
MakeList(u32),
MakeVariant {
name_idx: u32,
arity: u16,
},
GetField {
name_idx: u32,
site_idx: u32,
},
GetElem(u16),
TestVariant(u32),
GetVariant(u32),
GetVariantArg(u16),
GetListLen,
GetListElem(u32),
ListAppend,
GetListElemDyn,
Jump(i32),
JumpIf(i32),
JumpIfNot(i32),
Call {
fn_id: u32,
arity: u16,
node_id_idx: u32,
},
TailCall {
fn_id: u32,
arity: u16,
node_id_idx: u32,
},
MakeClosure {
fn_id: u32,
capture_count: u16,
},
CallClosure {
arity: u16,
node_id_idx: u32,
},
SortByKey {
node_id_idx: u32,
},
ParallelMap {
node_id_idx: u32,
},
ListMap {
node_id_idx: u32,
},
ListFilter {
node_id_idx: u32,
},
ListFold {
node_id_idx: u32,
},
EffectCall {
kind_idx: u32,
op_idx: u32,
arity: u16,
node_id_idx: u32,
},
Return,
Panic(u32),
IntAdd,
IntSub,
IntMul,
IntDiv,
IntMod,
IntNeg,
IntEq,
IntLt,
IntLe,
FloatAdd,
FloatSub,
FloatMul,
FloatDiv,
FloatNeg,
FloatEq,
FloatLt,
FloatLe,
NumAdd,
NumSub,
NumMul,
NumDiv,
NumMod,
NumNeg,
NumEq,
NumLt,
NumLe,
BoolAnd,
BoolOr,
BoolNot,
StrConcat,
StrLen,
StrEq,
BytesLen,
BytesEq,
LoadLocalAddIntConst {
local_idx: u16,
imm_const_idx: u32,
},
LoadLocalAddIntConstStoreLocal {
src: u16,
imm_const_idx: u32,
dest: u16,
},
LoadLocalAddLocal {
lhs_idx: u16,
rhs_idx: u16,
},
LoadLocalSubLocal {
lhs_idx: u16,
rhs_idx: u16,
},
LoadLocalMulLocal {
lhs_idx: u16,
rhs_idx: u16,
},
LoadLocalEqIntConstJumpIfNot {
local_idx: u16,
imm_const_idx: u32,
jump_offset: i32,
},
LoadLocalStoreEqIntConstJumpIfNot {
src: u16,
dst: u16,
imm_const_idx: u32,
jump_offset: i32,
},
LoadLocalGetFieldAdd {
local_idx: u16,
name_idx: u32,
site_idx: u32,
},
LoadLocalGetFieldSub {
local_idx: u16,
name_idx: u32,
site_idx: u32,
},
LoadLocalGetFieldMul {
local_idx: u16,
name_idx: u32,
site_idx: u32,
},
LoadLocalGetField {
local_idx: u16,
name_idx: u32,
site_idx: u32,
},
}Variants§
PushConst(u32)
Pop
Dup
LoadLocal(u16)
StoreLocal(u16)
MakeRecord
Builds a record by interning its field-name shape in
Program.record_shapes (#461). shape_idx indexes that
side-table; field_count is shape.len() cached inline so the
stack-effect verifier can compute its delta without needing a
Program reference. The VM pops field_count values off the
stack and pairs them with Program.record_shapes[shape_idx].
Externalizing the field-name vec is what lets Op be Copy,
which is the precondition for direct-threaded dispatch
(code[pc] becomes a register-sized read instead of an
every-step Vec clone).
AllocStackRecord
Stack-allocated record (#464). Same shape semantics as
MakeRecord — pops field_count field values and pairs them
with Program.record_shapes[shape_idx] — but the field values
are stored in the current frame’s slab inside the VM’s
stack_record_arena, not in a heap-allocated IndexMap. The
stack pushes a Value::StackRecord whose slab_start indexes
into the arena.
Emitted by the compiler in place of MakeRecord at sites that
escape::build_escape_index proves do not escape the frame
(returned, captured, stored into another aggregate, or passed
to a call). Runtime fallback: when the frame’s
stack_record_budget_remaining is exhausted, the op silently
degrades to the heap path (identical observable effect to
MakeRecord), so a single function can mix stack and heap
records without compile-time partitioning.
body_hash stability (#222): canonical encoding decodes this
op back to the historical {"MakeRecord":{"field_name_indices": [...]}} form, so closure identity is invariant under the
step-2 lowering.
AllocArenaRecord
Request-scoped arena record (#463 slice 2a). Same shape semantics
as MakeRecord and AllocStackRecord — pops field_count field
values, pairs them with Program.record_shapes[shape_idx], and
pushes a record handle — but the fields live in the VM’s
request-scoped arena_slab, not the per-frame stack-record
arena, so the value can outlive the allocating frame as long as
the surrounding request scope (opened by
EffectHandler::enter_request_scope) is still active. The
resulting Value::ArenaRecord indexes the slab.
Emitted (slice 2b) at sites arena::build_arena_index proves do
not escape the request scope. Runtime fallback: when no scope
is active (e.g. a non-handler context calls a function that was
compiled with arena lowering), the op silently degrades to the
MakeRecord heap path — identical observable effect.
body_hash stability (#222): canonical encoding decodes back to
{"MakeRecord":{"field_name_indices":[...]}}, so closure
identity is invariant under the lowering, mirroring
AllocStackRecord.
MakeTuple(u16)
AllocStackTuple
Frame-local tuple (#464 tuple codegen). Stack-alloc analogue of
MakeTuple: pops arity values into the VM’s stack-record
arena and pushes a Value::StackTuple whose slab_start
indexes the arena. Emitted by the compiler in place of
MakeTuple at sites escape::build_escape_index proves do not
escape the frame. Runtime fallback to the heap Value::Tuple
path when the frame’s stack-record budget is exhausted —
identical observable effect, so stack and heap tuples can mix
within one function. body_hash stability (#222): canonical
encoding decodes this op back to MakeTuple(arity), so closure
identity is invariant under the lowering.
AllocArenaTuple
Request-scoped arena tuple (#463 slice 2a). Tuple analogue of
AllocArenaRecord: pops arity values into the VM’s
request-scoped arena_slab and pushes a Value::ArenaTuple
handle. Same fallback rule (no active scope → MakeTuple heap
path) and same body_hash invariance (decodes back to
MakeTuple(arity)).
MakeList(u32)
MakeVariant
GetField
Record field access. name_idx indexes a Const::FieldName
in the constant pool — the field to read. site_idx is a
stable per-function index assigned by the compiler at emit
time (#462 slice 1), keyed into the per-fn inline-cache table
in the VM. Replaces the pre-#462 (fn_id << 32 | pc) IC key
so the cache survives the future dispatch rewrite (#461) and
a JIT (#465). body_hash stability: the canonical encoding
drops site_idx and serializes as the historical GetField(u32)
tuple form, so closure identity (#222) is unchanged.
GetElem(u16)
TestVariant(u32)
GetVariant(u32)
GetVariantArg(u16)
GetListLen
GetListElem(u32)
ListAppend
Pop [list, value]; push list with value appended.
GetListElemDyn
Pop list; push it indexed by the integer on top. Stack: [list, idx] → [list[idx]]. (Like GetListElem(u32) but the index is dynamic.)
Jump(i32)
JumpIf(i32)
JumpIfNot(i32)
Call
TailCall
MakeClosure
Build a Value::Closure: pop capture_count values (in order) and
pair them with fn_id.
CallClosure
Call a closure: pop arity args + 1 closure (top of stack), invoke.
SortByKey
Stable sort-by-key (#338). Stack: [xs, f] (xs underneath).
Pops the key-fn f and the list xs, applies f to each
element to derive a sortable key, returns the list reordered
so keys ascend. Keys must be one of Int / Float / Str;
other key types pair-wise compare as equal (preserving
insertion order). node_id_idx is the originating NodeId.
ParallelMap
Parallel map (#305 slice 1). Stack: [xs, f] (xs underneath).
Pops the closure f and the list xs, applies f to each
element in parallel via OS threads, pushes the result list
in input order. node_id_idx is the originating NodeId for
trace keying. The pool size is capped by
LEX_PAR_MAX_CONCURRENCY (default = available CPU cores).
Slice 1 limitation: closures invoking effects fail at
runtime with VmError::Effect. The per-thread effect handler
split is queued as slice 2.
ListMap
Map a list (#464 list-builder fast path). Stack: [xs, f] (xs
underneath). Pops the closure f and list xs, applies f to
each element, pushes the result list. Native opcode (mirrors
SortByKey/ParallelMap) rather than an inlined bytecode loop:
the loop form re-LoadLocal’d (cloned) the whole input and
accumulator lists each iteration — O(n²) — whereas the VM here
owns xs and builds the output with one pre-sized allocation.
ListFilter
Filter a list (#464). Stack: [xs, pred]. Pops pred and xs,
keeps the elements for which pred(x) is true. Native, same
rationale as ListMap.
ListFold
Left-fold a list (#464). Stack: [xs, init, f] (xs deepest).
Pops f, init, xs; threads acc = f(acc, x) from init
over the elements; pushes the final acc. Native, same
rationale as ListMap.
EffectCall
EFFECT_CALL <effect_kind_const_idx> <op_name_const_idx> <arity>.
Pops arity args, dispatches to a host effect handler, pushes result.
node_id_idx points to a Const::NodeId for trace keying.
Return
Panic(u32)
IntAdd
IntSub
IntMul
IntDiv
IntMod
IntNeg
IntEq
IntLt
IntLe
FloatAdd
FloatSub
FloatMul
FloatDiv
FloatNeg
FloatEq
FloatLt
FloatLe
NumAdd
NumSub
NumMul
NumDiv
NumMod
NumNeg
NumEq
NumLt
NumLe
BoolAnd
BoolOr
BoolNot
StrConcat
StrLen
StrEq
BytesLen
BytesEq
LoadLocalAddIntConst
Fused LoadLocal(local_idx) + PushConst(imm_const_idx) + IntAdd. imm_const_idx must point to a Const::Int. The
dispatch arm reads the local, adds the constant, pushes the
result, and advances pc by 3 (past this op and the two
inert PushConst + IntAdd slots that follow). For
body_hash stability (#222) the canonical encoding decomposes
this op back to a standalone LoadLocal(local_idx) at hash
time; the unchanged PushConst / IntAdd at the next two
slots hash normally, so the total bytes match pre-fusion.
LoadLocalAddIntConstStoreLocal
Fused LoadLocal(src) + PushConst(imm_const_idx) + IntAdd + StoreLocal(dest) (#461 superinstruction slice 2). Bypasses
the value stack entirely: reads locals[src], adds the Int
constant, writes locals[dest]. Advances pc by 4. Stack
delta: 0.
The peephole pass that emits this op runs after slice 1,
looking for [LoadLocalAddIntConst, ., ., StoreLocal] where
the middle two slots are slice-1 tombstones (the original
PushConst + IntAdd). The verifier and the body-hash decoder
both treat the 3 following slots as tombstones owned by
this op.
LoadLocalAddLocal
Fused LoadLocal(lhs_idx) + LoadLocal(rhs_idx) + IntAdd
(#461 superinstruction slice 3). The binary-op-on-two-locals
idiom — fires on any a + b where both operands are
statically-typed Int locals (e.g. acc + n in tail-recursive
accumulator loops). Reads locals[lhs_idx] and locals[rhs_idx],
pushes the sum, advances pc by 3. Stack delta: +1.
body_hash stability (#222): canonical encoding decomposes
back to a standalone LoadLocal(lhs_idx). The unchanged
LoadLocal(rhs_idx) + IntAdd tombstones at pc+1 and pc+2
hash normally, so the total bytes match the pre-fusion form.
Verifier walks the tombstones as if live: their deltas
(+1 LoadLocal, -1 IntAdd) cancel, matching the unfused depth
at pc+3.
LoadLocalSubLocal
Fused LoadLocal(lhs_idx) + LoadLocal(rhs_idx) + IntSub
(#461 superinstruction slice 4). Sibling of LoadLocalAddLocal
for the typed Int subtraction binop — fires on any a - b
where both operands are Int locals. Reads locals[lhs_idx]
and locals[rhs_idx], pushes lhs - rhs, advances pc by 3.
Stack delta: +1. Tombstone + body-hash story matches
LoadLocalAddLocal exactly.
LoadLocalMulLocal
Fused LoadLocal(lhs_idx) + LoadLocal(rhs_idx) + IntMul
(#461 superinstruction slice 4). Sibling of LoadLocalAddLocal
for the typed Int multiplication binop. Same shape: reads
the two Int locals, pushes lhs * rhs, advances pc by 3.
Stack delta: +1. Tombstone + body-hash story matches
LoadLocalAddLocal exactly.
LoadLocalEqIntConstJumpIfNot
Fused LoadLocal(local_idx) + PushConst(imm_const_idx) + IntEq + JumpIfNot(offset) (#461 superinstruction slice 5,
pattern-match arm-test idiom). Fires on every numeric
pattern arm test — match n { 0 => acc; _ => recurse } and
the cascade of integer-literal arms in any pattern match —
after compile_pattern_test lowers the historical NumEq to
IntEq for Int-literal patterns. Reads the local, compares
against the Int constant; if equal, advances pc by 4 (past
the 3 tombstones, into the arm body); if not equal, jumps
to pc + 4 + jump_offset (the JumpIfNot’s original target —
the next arm test or the panic-no-match block). Stack
delta: 0 (original sequence had +1, +1, -1, -1).
Jump-aware peephole — slice 5 is the first fusion that
absorbs a control-flow op. The verifier walks the fused op
with both fall-through and branch successors, skipping past
the trailing three tombstones (mirroring slice 2’s 4-slot
pattern). body_hash decodes back to a standalone
LoadLocal(local_idx); the trailing primitive ops stay in
the code stream as tombstones and hash normally — so
closure identity (#222) stays invariant.
LoadLocalStoreEqIntConstJumpIfNot
Fused LoadLocal(src) + StoreLocal(dst) + LoadLocalEqIntConstJumpIfNot { local_idx: dst, ... } (#461
superinstruction slice 6). Absorbs the match-scrutinee dance
— the LoadLocal + StoreLocal the compiler emits to bind the
match expression to a fresh local before each arm’s pattern
test reads it back. Reads locals[src], mirrors the original
StoreLocal(dst) by writing the same value into locals[dst]
(so the SECOND and later arm tests in the same match still
see the scrutinee at the expected slot), then compares against
the constant. Equal → advance pc by 6 (skip past the 5
tombstones — original StoreLocal + slice-5 fused op + slice-5’s
3 trailing tombstones). Not equal → jump to
pc + 6 + jump_offset (the original JumpIfNot’s target;
jump_offset is copied unchanged from the slice-5 op).
Stack delta: 0.
body_hash decodes back to a standalone LoadLocal(src);
the trailing 5 ops (StoreLocal, the slice-5 fused op
decoded as LoadLocal(dst), PushConst, IntEq, JumpIfNot) stay
in the code stream as tombstones and hash normally.
LoadLocalGetFieldAdd
Fused LoadLocal(local_idx) + GetField{name_idx, site_idx} + IntAdd (#461 superinstruction slice 7). Fires on the
acc + r.field accumulator-with-field-read idiom — the
shape any expr + record.field lowers to when the LHS is
already on the stack and the RHS is a same-frame record
field. After #464 step 2 dropped the IndexMap allocation
from hot-path records, this fusion is the next dispatch-
overhead bottleneck on the response_build profile.
Dispatch: pops the prior stack top (an Int), reads
locals[local_idx], performs the polymorphic-IC GetField
lookup keyed by (fn_id, site_idx) against name_idx,
adds the field value to the popped Int, pushes the result,
advances pc by 3.
Stack delta: +1 (matches a bare LoadLocal). The trailing
GetField (delta 0) and IntAdd (delta -1) stay in the
code stream as inert tombstones; the verifier walks them as
live and their cancelling deltas leave depth at pc+3
matching the unfused form.
body_hash stability (#222): canonical encoding decomposes
to a standalone LoadLocal(local_idx); the unchanged
GetField and IntAdd at pc+1 and pc+2 hash normally, so
the total bytes match pre-fusion. The trailing GetField’s
own body-hash decoding (which strips site_idx) means the
hash is unchanged across recompiles where IC-site numbering
differs.
Safety: the trailing two slots must not be jump targets (standard tombstone rule). The first slot may be a target — the fused op there is live.
LoadLocalGetFieldSub
Slice 8 of #461: IntSub / IntMul siblings of slice 7’s
LoadLocalGetFieldAdd. Fuse LoadLocal + GetField + IntSub
and LoadLocal + GetField + IntMul respectively — the
acc - r.field and acc * r.field idioms. Same tombstone,
jump-safety, body-hash (decode to LoadLocal(local_idx)),
and verifier (+1 delta) story as slice 7.
IntSub is not commutative: the unfused sequence leaves the
field value on top, so IntSub’s deeper-minus-top semantics
give acc - field. The fused dispatch preserves that order.
LoadLocalGetFieldMul
LoadLocalGetField
Slice 9 of #461: fuse LoadLocal(local_idx) + GetField{name_idx, site_idx} — the bare record.field read, the single most
common field-access shape. Unlike slices 7/8 there’s no
arithmetic terminator; this is a 2-op window.
The win is allocation, not just dispatch: the unfused pair
LoadLocal clones the entire record onto the value stack
(a Box<IndexMap> for a heap record), GetField pops it,
reads one field, and drops the rest. The fused op reads the
field out of the local by reference (read_local_record_field)
and clones only that one value. On the response_build
profile the whole-record clone+drop of the returned Response
(r.total) was the dominant malloc source.
Stack delta: +1 (LoadLocal +1, GetField 0). The trailing
GetField stays as a single inert tombstone (delta 0); the
verifier walks it, leaving depth at pc+2 matching the unfused
[LoadLocal, GetField] pair. body_hash decodes to a
standalone LoadLocal(local_idx); the trailing GetField
hashes normally.
Safety: the trailing slot (the original GetField) must not
be a jump target. The first slot may be.