Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bytecode VM

The runtime is a stack-based bytecode VM.

Design properties

  • 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

Value types

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.

Opcode reference

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.

Stack and literals

OpcodeOperandsDescription
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
PushNullPush null
PopDiscard the top value
DuplicateDuplicate the top value

Arithmetic

OpcodeDescription
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

Comparison

OpcodeDescription
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

Logic

OpcodeDescription
NotPop one value, push its logical negation
AndPop two values, push logical AND
OrPop two values, push logical OR

Variables

OpcodeOperandsDescription
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

Control flow

OpcodeOperandsDescription
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
GotoVariablePop a DivertTarget from the stack and goto it

Container flow

OpcodeOperandsDescription
EnterContainerDefinitionIdPush a container onto the container stack (updates visit counts)
ExitContainerPop the current container from the container stack

Functions and tunnels

OpcodeOperandsDescription
CallDefinitionIdCall a function — pushes a new call frame with fresh temp storage
ReturnReturn from a function call
TunnelCallDefinitionIdTunnel into a knot — pushes a return address, shares the output stream
TunnelReturnReturn from a tunnel
TunnelCallVariablePop a DivertTarget and tunnel to it
CallVariablePop a DivertTarget and call it as a function

Threads

OpcodeOperandsDescription
ThreadCallDefinitionIdFork execution to explore a choice branch
ThreadStartMark the beginning of a forked thread’s code
ThreadDoneMark 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.

Output

OpcodeOperandsDescription
EmitLineu16 (index), u8 (slot count)Emit a line from the scope’s line table; slot count interpolation slots are popped from the stack
EmitValuePop a value and emit its string representation
EmitNewlineEmit a newline character
SpringWord break — renders as a single space between content parts
GlueSuppress the previous newline (joins lines)
BeginTagBegin capturing tag content
EndTagEnd tag capture and attach to current output
EvalLineu16 (index), u8 (slot count)Evaluate an interpolated line template with slot count popped slots
BeginFragmentBegin capturing output into a fragment
EndFragmentEnd fragment capture; store the parts and push a FragmentRef

Choices

OpcodeOperandsDescription
BeginChoiceflags: u8, DefinitionIdBegin a choice with flags and a target address
EndChoiceFinalize 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

Sequences

OpcodeOperandsDescription
Sequencekind: u8, count: u8Begin a sequence (kind: 0=cycle, 1=stopping, 2=once-only, 3=shuffle)
SequenceBranchi32 (offset)Jump offset for a sequence branch

Intrinsics

OpcodeDescription
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

Casts and math

OpcodeDescription
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

External functions

OpcodeOperandsDescription
CallExternalDefinitionId, u8 (arg count)Call an externally-bound function

List operations

OpcodeDescription
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

String evaluation

OpcodeDescription
BeginStringEvalBegin capturing output as a string value (for string interpolation)
EndStringEvalEnd string capture and push the result onto the stack

Lifecycle

OpcodeDescription
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

Debug

OpcodeOperandsDescription
SourceLocationu32 (line), u32 (col)Record source location for debugging

Execution model

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.