The runtime is a stack-based bytecode VM.
Stack-based: operands pushed/popped from a value stack
Jump offsets are container-relative
Cross-definition references use DefinitionId, resolved to compact indices at link time
Short-circuit and/or compiled to conditional jumps, not handled by the VM
enum Value {
Int(i32),
Float(f32),
Bool(bool),
String(Arc<str>), // Refcounted for cheap cloning
List(Arc<ListValue>), // Refcounted
DivertTarget(DefinitionId), // Target address for diverts
VariablePointer(DefinitionId), // Reference to a global variable
TempPointer { slot, frame_depth }, // Reference to a local variable
Null,
FragmentRef(u32), // Index into the output fragment store
}
String and List are Arc-wrapped so cloning is O(1), matching C# reference semantics and making call-frame forking cheap. (Atomic refcounts, so a Value can flow through Bevy’s parallel scheduler.) FragmentRef points at a captured run of structural output parts, kept intact so it can be re-rendered in another locale.
The VM’s full instruction set is listed below — around 70 opcodes. Each is encoded as a single discriminant byte followed by zero or more operand bytes.
Opcode Operands Description
PushInti32Push an integer constant
PushFloatf32Push a float constant
PushBoolu8Push a boolean (0 = false, 1 = true)
PushStringu16Push a string by line table index
PushListu16Push a list literal by index
PushDivertTargetDefinitionIdPush a divert target address
PushNull— Push null
Pop— Discard the top value
Duplicate— Duplicate the top value
Opcode Description
AddPop two values, push their sum (also concatenates strings)
SubtractPop two values, push their difference
MultiplyPop two values, push their product
DividePop two values, push their quotient
ModuloPop two values, push the remainder
NegatePop one value, push its negation
Opcode Description
EqualPop two values, push whether they are equal
NotEqualPop two values, push whether they differ
GreaterPop two values, push whether left > right
GreaterOrEqualPop two values, push whether left >= right
LessPop two values, push whether left < right
LessOrEqualPop two values, push whether left <= right
Opcode Description
NotPop one value, push its logical negation
AndPop two values, push logical AND
OrPop two values, push logical OR
Opcode Operands Description
GetGlobalDefinitionIdPush the value of a global variable
SetGlobalDefinitionIdPop a value and assign it to a global variable
DeclareTempu16 (slot)Declare a temp variable in the current frame
GetTempu16 (slot)Push the value of a temp (auto-dereferences pointers)
SetTempu16 (slot)Pop a value and assign it to a temp slot
GetTempRawu16 (slot)Push a temp’s raw value without auto-dereference
PushVarPointerDefinitionIdPush a pointer to a global variable
PushTempPointeru16 (slot)Push a pointer to a temp variable
Opcode Operands Description
Jumpi32 (offset)Unconditional relative jump within the current container
JumpIfFalsei32 (offset)Pop a value; jump if falsy
GotoDefinitionIdAbsolute jump to a named address
GotoIfDefinitionIdPop a value; goto the address if truthy
GotoVariable— Pop a DivertTarget from the stack and goto it
Opcode Operands Description
EnterContainerDefinitionIdPush a container onto the container stack (updates visit counts)
ExitContainer— Pop the current container from the container stack
Opcode Operands Description
CallDefinitionIdCall a function — pushes a new call frame with fresh temp storage
Return— Return from a function call
TunnelCallDefinitionIdTunnel into a knot — pushes a return address, shares the output stream
TunnelReturn— Return from a tunnel
TunnelCallVariable— Pop a DivertTarget and tunnel to it
CallVariable— Pop a DivertTarget and call it as a function
Opcode Operands Description
ThreadCallDefinitionIdFork execution to explore a choice branch
ThreadStart— Mark the beginning of a forked thread’s code
ThreadDone— Mark the end of a forked thread
Thread forking clones the current VM state (call stack, variable state) to explore choice branches in isolation. Each choice’s thread is evaluated independently to determine its display text and conditions.
Opcode Operands Description
EmitLineu16 (index), u8 (slot count)Emit a line from the scope’s line table; slot count interpolation slots are popped from the stack
EmitValue— Pop a value and emit its string representation
EmitNewline— Emit a newline character
Spring— Word break — renders as a single space between content parts
Glue— Suppress the previous newline (joins lines)
BeginTag— Begin capturing tag content
EndTag— End tag capture and attach to current output
EvalLineu16 (index), u8 (slot count)Evaluate an interpolated line template with slot count popped slots
BeginFragment— Begin capturing output into a fragment
EndFragment— End fragment capture; store the parts and push a FragmentRef
Opcode Operands Description
BeginChoiceflags: u8, DefinitionIdBegin a choice with flags and a target address
EndChoice— Finalize the current choice
BeginChoice flags (packed into a single byte):
Bit 0: has_condition — choice has a conditional guard
Bit 1: has_start_content — choice has text before [
Bit 2: has_choice_only_content — choice has text inside []
Bit 3: once_only — choice can only be selected once
Bit 4: is_invisible_default — fallback choice when no others are available
Opcode Operands Description
Sequencekind: u8, count: u8Begin a sequence (kind: 0=cycle, 1=stopping, 2=once-only, 3=shuffle)
SequenceBranchi32 (offset)Jump offset for a sequence branch
Opcode Description
VisitCountPop a DivertTarget, push its visit count
CurrentVisitCountPush the visit count of the current container
TurnsSincePop a DivertTarget, push turns since last visit (-1 if never)
TurnIndexPush the current turn index
ChoiceCountPush the number of currently available choices
RandomPop max and min, push a random integer in [min, max]
SeedRandomPop a seed value and set the RNG seed
Opcode Description
CastToIntPop a value, push it as an integer
CastToFloatPop a value, push it as a float
FloorPop a float, push its floor as an integer
CeilingPop a float, push its ceiling as an integer
PowPop exponent and base, push base^exponent
MinPop two values, push the smaller
MaxPop two values, push the larger
Opcode Operands Description
CallExternalDefinitionId, u8 (arg count)Call an externally-bound function
Opcode Description
ListContainsPop item and list, push whether the list contains the item
ListNotContainsPop item and list, push whether the list does not contain the item
ListIntersectPop two lists, push their intersection
ListAllPop a list, push all possible items from its origin lists
ListInvertPop a list, push the complement (all origin items not in the list)
ListCountPop a list, push its item count
ListMinPop a list, push its minimum item
ListMaxPop a list, push its maximum item
ListValuePop a list, push its integer value (ordinal of single item)
ListRangePop max, min, and list; push items within the ordinal range
ListFromIntPop an integer and list origin, push the item with that ordinal
ListRandomPop a list, push a random item from it
Opcode Description
BeginStringEvalBegin capturing output as a string value (for string interpolation)
EndStringEvalEnd string capture and push the result onto the stack
Opcode Description
DoneYield — the story pauses and can be resumed (marks a safe exit)
YieldPause for choice presentation — like Done but does not mark a safe exit
EndPermanent end — the story is finished
NopNo operation
Opcode Operands Description
SourceLocationu32 (line), u32 (col)Record source location for debugging
The step function executes opcodes in a loop until reaching a yield point: Done, End, or choice presentation. Each yield produces a Line (Text/Done/Choices/End) carrying the output text accumulated since the last yield — continue_single returns one, continue_maximally returns a Vec<Line> ending in a terminal variant.
Call stack : Function and tunnel calls push frames onto the call stack. Each frame has its own local variable storage (temp slots). Return and TunnelReturn pop frames.
Container stack : Each call frame tracks which containers are currently active. EnterContainer pushes, ExitContainer pops. This drives visit counting and turn tracking.
Thread forking : ThreadCall forks the current execution state (stacks, globals, output) to explore a choice branch. All threads run within the same step. At yield, threads are merged: each live thread contributes its choices to the final Line::Choices.