pub enum Opcode {
Show 155 variants
PushInt(i32),
PushFloat(f32),
PushBool(bool),
PushString(u16),
PushList(u16),
PushDivertTarget(DefinitionId),
PushNull,
Pop,
Duplicate,
Add,
Subtract,
Multiply,
Divide,
Modulo,
Negate,
Equal,
NotEqual,
Greater,
GreaterOrEqual,
Less,
LessOrEqual,
Not,
And,
Or,
GetGlobal(DefinitionId),
SetGlobal(DefinitionId),
DeclareTemp(u16),
GetTemp(u16),
SetTemp(u16),
GetTempRaw(u16),
PushVarPointer(DefinitionId),
PushTempPointer(u16),
Jump(i32),
JumpIfFalse(i32),
Goto(DefinitionId),
GotoIf(DefinitionId),
GotoVariable,
EnterContainer(DefinitionId),
ExitContainer,
Call(DefinitionId),
Return,
TunnelCall(DefinitionId),
TunnelReturn,
TunnelCallVariable,
CallVariable(u8),
ThreadCall(DefinitionId),
ThreadStart,
ThreadDone,
EmitLine(u16, u8),
EmitValue,
EmitNewline,
Spring,
Glue,
BeginTag,
EndTag,
EvalLine(u16, u8),
BeginFragment,
EndFragment,
AttachElement,
EndElementRun,
BeginChoice(ChoiceFlags, DefinitionId),
EndChoice,
Sequence(SequenceKind, u8),
SequenceBranch(i32),
VisitCount,
CurrentVisitCount,
TurnsSince,
TurnIndex,
ChoiceCount,
Random,
SeedRandom,
CastToInt,
CastToFloat,
Floor,
Ceiling,
Pow,
Min,
Max,
CallExternal(DefinitionId, u8),
ListContains,
ListNotContains,
ListIntersect,
ListAll,
ListInvert,
ListCount,
ListMin,
ListMax,
ListValue,
ListRange,
ListFromInt,
ListRandom,
ArrayNew(u32),
MapNew(u32),
IndexGet,
IndexSet,
CollectionLen,
MapGet,
MapInsert,
MapRemove,
MapContains,
CollectionKeys,
CollectionValues,
PushLiteral(u32),
TakeGlobal(DefinitionId),
TakeTemp(u16),
RecordNew(u32),
RecordGetDyn(u16),
RecordSetDyn(u16),
RecordGet(u16),
RecordSet(u16),
ConvertInt,
ConvertFloat,
ConvertString,
PushFnRef(DefinitionId),
MakeClosure {
target: DefinitionId,
bound_count: u8,
},
CallValue(u8),
BindValue(u8),
MakeProjection {
root: DefinitionId,
segment_count: u8,
},
ProjRead,
ProjWrite,
CharAt,
PushNone,
MakeSome,
StrFind,
SeqIndexOf,
SeqMin,
SeqMax,
SeqFirst,
SeqLast,
SeqPop,
MapGetOpt,
MapContainsValue,
MapClear,
CoalesceSome(i32),
OptionBind(u16),
SeqRemoveAt,
RandFloat,
RandChance,
RandPick,
RandShuffle,
RangeMakeExcl,
RangeMakeIncl,
RangeNonEmpty,
SeqSorted,
SeqSortedBy,
Tower(TowerOp),
Collect(CollectOp),
SeqVerb(SeqVerbOp),
Done,
Yield,
End,
Nop,
BeginStringEval,
EndStringEval,
SourceLocation(u32, u32),
}Expand description
A single VM instruction with its operands.
Variants§
PushInt(i32)
PushFloat(f32)
PushBool(bool)
PushString(u16)
PushList(u16)
PushDivertTarget(DefinitionId)
PushNull
Pop
Duplicate
Add
Subtract
Multiply
Divide
Modulo
Negate
Equal
NotEqual
Greater
GreaterOrEqual
Less
LessOrEqual
Not
And
Or
GetGlobal(DefinitionId)
SetGlobal(DefinitionId)
DeclareTemp(u16)
GetTemp(u16)
SetTemp(u16)
GetTempRaw(u16)
Get a temp’s raw value without auto-dereference (for passing a ref onward).
PushVarPointer(DefinitionId)
Push a pointer to a global variable onto the eval stack.
PushTempPointer(u16)
Push a pointer to a temp variable onto the eval stack.
Jump(i32)
JumpIfFalse(i32)
Goto(DefinitionId)
GotoIf(DefinitionId)
GotoVariable
EnterContainer(DefinitionId)
ExitContainer
Call(DefinitionId)
Return
TunnelCall(DefinitionId)
TunnelReturn
TunnelCallVariable
CallVariable(u8)
Call through a variable holding either a divert target (classic ink
function-via-variable) or a function value (T1c-2 direct-call form
f(args…)) — both share this dispatch site. argc is the exact
number of args codegen pushed before the callee at this call site
(never derived from the resolved target’s arity at runtime — issue
#721: doing so made a gradual-mode direct-call arity mismatch leave
a stray value on the stack instead of faulting). The divert-target
arm ignores argc (unchanged oracle-verified behavior); the
function-value arm pops exactly argc supplied args.
ThreadCall(DefinitionId)
ThreadStart
ThreadDone
EmitLine(u16, u8)
EmitValue
EmitNewline
Spring
Word break — renders as a single space between content parts.
Glue
BeginTag
EndTag
EvalLine(u16, u8)
BeginFragment
Begin capturing output into a fragment (structural preservation).
EndFragment
End fragment capture — store parts and push Value::FragmentRef.
AttachElement
An attach = StructName convention handler’s claimed line (issue
#2108) — see brink_ir::hir::Stmt::AttachElement’s doc. Pops the
call’s result off the value stack; when it is a Value::Record
matching a known StructShapes entry, merges its fields (converted
via the same stringify display path as string()/interpolation)
into the VM’s per-block attachment state — no output, no Step::Line.
EndElementRun
Closes the run an AttachElement opened — see
brink_ir::hir::Stmt::EndElementRun’s doc. Clears the VM’s
accumulated attachment data and starts a fresh block.
BeginChoice(ChoiceFlags, DefinitionId)
EndChoice
Sequence(SequenceKind, u8)
SequenceBranch(i32)
VisitCount
Pop a DivertTarget from the stack, push its visit count.
CurrentVisitCount
Push the visit count of the current container (no stack input).
TurnsSince
TurnIndex
ChoiceCount
Random
SeedRandom
CastToInt
CastToFloat
Floor
Ceiling
Pow
Min
Max
CallExternal(DefinitionId, u8)
ListContains
ListNotContains
ListIntersect
ListAll
ListInvert
ListCount
ListMin
ListMax
ListValue
ListRange
ListFromInt
ListRandom
ArrayNew(u32)
[elem_0, …, elem_{n-1}] → Array([elem_0, …, elem_{n-1}]).
MapNew(u32)
[k_0, v_0, …, k_{n-1}, v_{n-1}] → Map({k_0: v_0, …}) (insertion
order = argument order; a repeated key keeps its first position and
takes the last value, matching OrderedMap::insert).
IndexGet
[container, index] → element/value. Turn-terminating fault on
out-of-bounds array index or missing map key (value-model-spec §6).
IndexSet
[container, index, value] → updated container (take → make_mut →
write-back). Turn-terminating fault on out-of-bounds array index or
missing map key — no silent growth on write-past-end (spec §6).
CollectionLen
[container] → Int(len). Array or map.
MapGet
[map, key] → value. Turn-terminating fault on missing key.
MapInsert
[map, key, value] → updated map (insert-or-overwrite; unlike
IndexSet, a missing key is not a fault — this is the stdlib
insert() mutator’s primitive).
MapRemove
[map, key] → updated map with key removed (no-op if absent — the
stdlib remove() mutator’s primitive). Map-only as of issue #1484:
a non-map container is a turn-terminating fault (NotIndexable).
The array-index leg this op used to generalize over is
SeqRemoveAt.
MapContains
[map, key] → Bool.
CollectionKeys
[map] → Array of keys in insertion order.
CollectionValues
[map] → Array of values in insertion order.
PushLiteral(u32)
LiteralPool[idx] → cloned value (an Arc bump for collections).
TakeGlobal(DefinitionId)
Move a global’s current value out, leaving Value::Null behind —
the take-half of the take → make_mut → write-back RMW discipline
(value-model-spec §5). No stack input; pushes the taken value.
Unlike GetGlobal, never auto-dereferences (globals can’t hold
ref-param pointers — those live in temps).
TakeTemp(u16)
Move a temp’s current value out, leaving Value::Null behind —
mirrors TakeGlobal for temp slots. Auto-dereferences like
GetTemp: if the temp holds a VariablePointer/TempPointer, the
pointed-to location is taken (and left Null), not the pointer
value itself, which stays in this slot untouched.
RecordNew(u32)
[field_0, …, field_{n-1}] → Record (n = the shape’s declared
field count, looked up from StructShapes; fields popped/assigned in
shape declaration order). The u32 operand is the ShapeId.
RecordGetDyn(u16)
[record] → field value, looked up by name (NameId operand) in the
record’s own shape. Turn-terminating fault if the shape has no field
by that name (value-model-spec §11c).
RecordSetDyn(u16)
[record, value] → updated record (take → make_mut → write-back),
field selected by name (NameId operand). Turn-terminating fault if
the shape has no field by that name.
RecordGet(u16)
[record] → field value, looked up by flat offset into the record’s
own field vector (TM-4c, docs/typed-mode-spec.md §6 static-offset
payoff). Emitted only when the record’s shape is compile-time known
(types = strict); turn-terminating fault if the offset is out of
range for the popped record’s field count — no shape re-check.
RecordSet(u16)
[record, value] → updated record (take → make_mut → write-back),
field selected by flat offset (TM-4c). Turn-terminating fault if the
offset is out of range.
ConvertInt
[x] → Int. The int(x) pure conversion intrinsic: Int
(identity), Float (truncate toward zero, matching vanilla ink’s
INT()), Bool (true → 1, false → 0), String (parse).
Turn-terminating fault on a string that fails to parse, or on any
value outside this permissive numeric+bool domain (divert targets,
LIST values, arrays, maps, records) — value-model-spec §11c.
ConvertFloat
[x] → Float. The float(x) pure conversion intrinsic: Float
(identity), Int (widen), Bool (true → 1.0, false → 0.0),
String (parse). Same fault domain as ConvertInt.
ConvertString
[x] → String. The string(x) pure conversion intrinsic: display
form, identical to interpolation ({x}) — total over every Value,
never faults (typed-mode-spec §4: “display is universal, not a
coercion”).
PushFnRef(DefinitionId)
[] → FnRef. Push a zero-bound function value for the target
DefinitionId (#fn(name) where the target has no ref params).
MakeClosure
[bound_0, …, bound_{n-1}] → Closure. Pop the n = bound_count
bound args (in declared order) and pair each with its param name/mode
read from the target container’s own [ContainerDef::params] table
(the bound prefix params[0..n]) to build a Closure.
A ref bound arg is a VariablePointer (a captured durable cell); a
val bound arg is a snapshot. The names/modes are read from the
signature (not baked into the opcode) so there is one source of truth
the rehydration check compares against.
CallValue(u8)
[arg_0, …, arg_{argc-1}, callee] → return value. Pop the callee
function value then the argc supplied (val-only) args, splice the
closure’s bound prefix ahead of them, and enter the target. Faults
(turn-terminating, docs/t1c-spec.md §3): callee is not a function
value; bound + argc ≠ the target’s declared arity; a rehydrated env
entry’s name/mode no longer matches the current signature; the callee
ref-binds a #@local and is invoked from a non-creating flow.
BindValue(u8)
[arg_0, …, arg_{argc-1}, callee] → new function value. The
bind(f, args…) stdlib intrinsic (T1c-3, docs/t1c-spec.md §3):
pop the callee function value then the argc supplied (val-only)
args, append them to the callee’s bound-arg row (val-only currying,
consuming the head of the remaining param row), and push the new
function value. The appended entries take their param name/mode from
the target’s signature (always val). Faults (turn-terminating):
callee is not a function value; bound + argc exceeds the target’s
declared arity.
MakeProjection
[seg_0, …, seg_{n-1}] → Projection (n = segment_count, pushed
by codegen in source order; the VM’s LIFO pop-then-reverse restores
it). Each popped value is classified Int → ProjSegment::Index, else →
ProjSegment::Key and paired with the static root cell to build a
Value::Projection (docs/format-v4-rfc.md §1). Emitted at every
real path-projection ref-argument creation site (ref npc.inventory[3]) — the T1e-1 E099 lowering fence this replaces.
ProjRead
[projection] → value. Root-cell RMW read: take the root cell’s
current value, walk the segment chain, push the result. Faults
ProjectionInvalidated (turn-terminating) if the path no longer
resolves (spec §1(2)).
ProjWrite
[projection, value] → (assigns, pushes nothing). Root-cell RMW
write: take root → walk → make_mut spine → write the final segment
→ store back (spec §3). Faults ProjectionInvalidated on an
unresolved path, same domain as ProjRead.
CharAt
[s, i] → single-character String. The char_at(s, i) stdlib pure
function: i indexes Unicode scalar values (“chars”), not UTF-8
bytes. Turn-terminating fault (value-model-spec §11c) on a non-Int
i, a non-String s, or i outside [0, char_count).
PushNone
[] → none. Push the Option[T] absence value.
MakeSome
[x] → some(x). Wrap the top of stack — total over every value.
StrFind
[s, sub] → Option[int]: index of sub’s first occurrence in
s, counted in Unicode scalar values (chars, not bytes — the §3
indexing unit char_at already uses); absent → none.
Turn-terminating fault on non-string arguments.
SeqIndexOf
[a, x] → Option[int]: index of the first element structurally
equal to x; absent → none. Fault on a non-array container.
SeqMin
[a] → Option[T]: least element (empty → none). Orders
int/float (numeric promotion, NaN per the §4b pinned prod order),
bool, string; anything else faults (unorderable — wave A4 grows the
roster). Ties keep the first occurrence.
SeqMax
[a] → Option[T]: greatest element — see SeqMin.
SeqFirst
[a] → Option[T]: first element (empty → none).
SeqLast
[a] → Option[T]: last element (empty → none).
SeqPop
[a] → pushes Option[T] (the removed last element, or none on
empty), then the shrunk array on top of it. Codegen brackets this
TakeGlobal/TakeTemp … SetGlobal/SetTemp so the array writes
back to its root cell and the Option remains as the expression’s
value. Fault on a non-array.
MapGetOpt
[m, k] → Option[V]: the non-faulting map read (get(m, k),
§5 — martyr #3 redeemed). Missing key → none; a key outside the
int/string/bool key domain is a turn-terminating fault (malformed
question), as is a non-map container. The faulting m[k]
(MapGet) stays the “I expect it there” read.
MapContainsValue
[m, v] → Bool: content-equality scan over the map’s values
(§5 — honest O(n)). Fault on a non-map.
MapClear
[m] → empty map. The clear(m) statement-only mutator’s
primitive; in-place-ness comes from the RMW write-back, exactly
like MapInsert/MapRemove.
Fault on a non-map.
CoalesceSome(i32)
Pops lhs (must be an OptionVal). some(v) pushes the unwrapped
v and jumps rel bytes forward (the same relative-offset
convention as Jump/JumpIfFalse);
none pushes nothing and falls through to the next instruction,
which evaluates rhs. That fall-through is the short-circuit:
rhs’s bytecode is only ever reached when lhs is none — x or expensive() runs expensive() exactly once, and only on none
(RULED, issue #1471, flipping the eager evaluation PR #1469/#1460
landed and flagged as unruled). Native-surface only: reachable
exclusively through InfixOp::Coalesce, which the native lowering
path alone produces (InfixOp::Or, ink’s boolean ||, is untouched
and oracle-frozen).
The jump target is where the two branches join: the some(v) branch
has already unwrapped to v, and the none branch pushed rhs
as-is. Codegen emits a MakeSome right at that
target exactly when the step’s recorded typing says rhs is itself
Option[U] (the two-Option form, (Option[T],Option[T]) -> Option[U], preserving optionality for chaining), so both branches
agree on shape at the join; for the collapse form
((Option[T],T)->T) no MakeSome is emitted and v stands
unwrapped. The retired binary opcode decided that from rhs’s
runtime value; short-circuiting rules that out (rhs may never
run by the time the answer is needed), so the decision is made at
lowering time from the analyzer’s recorded types — see below.
§Where the collapse-vs-preserve answer comes from
RULED (maintainer, 2026-07-26, issue #1492 — docs/decision-log.md
“Lowering consumes analyzer types”): typing verdicts belong to
brink-analyzer, which records each or step’s operand/result
types for LIR lowering (brink_analyzer::coalesce_types, threaded
to lowering as brink_ir::lir::CoalesceLookup). Lowering consumes
that verdict; it never re-derives it from syntax. Under types = strict an ill-typed chain never reaches codegen at all — E066
rejects it during analysis — so this op only ever executes a chain
analysis either accepted or could not statically pin.
§The runtime check is the semantics for an unpinned lhs
That second case is the gradual-mode posture, and it is deliberate:
when the left-hand type is unknown (brink dialect, types = gradual — the un-overridden native default), the check this op
performs is the operator’s semantics, not a fallback for a missing
one. An OptionVal coalesces; a plain value raises the TypeError
fault (brink_runtime::value_ops::coalesce_unwrap_some) — the same
class as every other gradual runtime check. Strict/native never
reaches this path with an unpinned lhs, and the analyzer records
exactly this case as CoalesceShape::RuntimeCheck, on which codegen
emits no MakeSome: with rhs possibly never evaluated there is no
value to read a shape off, so the unwrapped collapse form is the one
shape that stays sound for the (Option[T],T)->T reading the check
admits.
OptionBind(u16)
[opt] → [bool] — the as binding’s fused test-and-bind
(if EXPR as name { … }, while EXPR as name { … },
{if EXPR as name: … else: …}). opt must be an OptionVal:
some(v) stores the unwrapped v in temp slot and pushes
true; none leaves slot untouched and pushes false. A
non-OptionVal operand faults
(RuntimeError::AsBindingNotOption — the
gradual-mode residual of the checker’s E147).
The slot is always freshly allocated by the binding itself, so —
unlike SetTemp — the write needs no
pointer/projection write-through: an as binding can never land on
a ref parameter’s cell. Native-surface only.
SeqRemoveAt
[a, i] → updated array with the element at i removed (shifts
later elements left) — the stdlib remove_at() mutator’s primitive,
the array-index leg MapRemove generalized over
before this PR. Array-only: a non-array a is a turn-terminating
fault (NotIndexable). i must be an Int in [0, len) — strictly
less than len, matching IndexGet/IndexSet (there is no element
to remove at len, unlike MapInsert’s append-friendly <=).
RandFloat
[] → Float uniform in [0,1). One draw. The value is built from
the draw’s top 24 bits (draw >> 7) divided by 2²⁴, so every result
is exactly representable in the f32 payload and 1.0 is unreachable —
part of the pinned-algorithm stability contract (see
brink-runtime::rand_ops).
RandChance
[p] → Bool: one uniform [0,1) draw u, result u < p with
p clamped to [0,1] and NaN → false (F3, ruled 2026-07-19:
interpretation, not fabrication — total over the numeric domain).
Always consumes exactly one draw, NaN included. Fault on a
non-numeric p (malformed question).
RandPick
[coll] → Option[T]: uniform draw of one element from an array
(→ some(elem)) or a flags subset (→ some(single-item list),
mirroring the frozen ListRandom selection). Empty → none
without consuming a draw. Fault on any other collection type.
RandShuffle
[a] → [a']: Fisher-Yates shuffle of an array, len-1 draws
(none for len < 2), each advancing the RNG cell. One op serves
both surfaces: shuffle(a) (statement-only, RMW write-back) and
shuffled(a) (functional). Fault on a non-array.
RangeMakeExcl
[start, end] → Range (NS-A5, F7): construct an exclusive
(start..end) range value from two int bounds. Fault on non-int
bounds (malformed question — the T1b stdlib doctrine; no numeric
coercion, range bounds are ints by ruling).
RangeMakeIncl
[start, end] → Range (NS-A5, F7): construct an inclusive
(start..=end) range value from two int bounds. Same fault
contract as RangeMakeExcl.
RangeNonEmpty
[r] → Option[Range] (NS-A5, the non_empty(r) validator —
S2 ruled 2026-07-19): some(r) when the range denotes at least one
element, none when it is empty. The Option tax sits once at the
boundary where dynamic bounds enter; the checker types the some
payload as the inhabited-range refinement. Pure — no draw, no
write. Fault on a non-range operand.
SeqSorted
[a] → [a']: the array sorted ascending by the §4b ordering
doctrine — int/float (numeric promotion), bool (false < true),
string (USV-lexicographic), arrays lexicographic element-wise
(recursively). Stable (equal elements keep their input order).
Float NaN is mode-dependent: DEV mode faults on any NaN comparand
(UnorderedComparand — the upstream bug surfaces at its first
ordering consumption); PROD mode places it by the pinned
non-fabricating total order (-0 == +0 ties, NaN greatest,
NaN-vs-NaN ties). One op serves both surfaces: sort(a)
(statement-only, RMW write-back) and sorted(a) (functional) —
the RandShuffle precedent. Fault on a non-array or unorderable
elements (structs/enums without a registered compare, maps,
flags subsets, divert targets — malformed question, all modes).
SeqSortedBy
[a, cmp] → [a']: the array sorted ascending by a user
comparator — cmp is a function value (FnRef/Closure) of
shape fn(T, T): int (negative = less, zero = tie, positive =
greater; F0 ruled 2026-07-19). Stable. The comparator runs under
the pure·silent contract (checker-enforced where provable); the
VM evaluates it re-entrantly with output isolated and faults if it
yields, presents choices, calls an external, or returns a
non-int. No NaN check here — F14: sort_by does not inherit
F:float; the comparator owns the element semantics. One op
serves sort_by(a, cmp) (statement-only, RMW write-back) and
sorted_by(a, cmp) (functional). Fault on a non-array or
non-function comparator.
Tower(TowerOp)
One opcode, thirteen operations: the TowerOp immediate selects
the constructor or verb (see its per-kind docs for stack shapes).
All pure; wrong-operand-type is a turn-terminating fault (a
malformed question, per the ruled fault-vs-absence doctrine). The
tower’s operator family (+/-/*, mat*vec, quat*quat,
quat*vec) rides the frozen arithmetic opcodes instead — see
value_ops::binary_op.
Collect(CollectOp)
One opcode, five operations: the CollectOp immediate selects
Weighted[T] construction, the rand::roll draw, or one of the
heap verbs (see its per-kind docs for stack shapes). RandRoll
writes the RNG cell; HeapPush carries the §4b dev/prod NaN
entry-check; everything else is pure over its operands.
SeqVerb(SeqVerbOp)
One opcode, one operation per SeqVerbOp kind: the pure trio
map/filter/fold. Every kind pops a callback function value
(FnRef/Closure) and evaluates it re-entrantly per element with
output isolated — the SeqSortedBy machinery, one callback contract.
See the per-kind docs for stack shapes.
Done
Yield
Pause for choice presentation. Like Done but does NOT set
did_safe_exit — if no choices are pending, the story ran
out of content rather than reaching an explicit -> DONE.