| Field | Value |
|---|---|
| RFC | 0002 |
| Title | Tollana Intermediate Representation Version 0 |
| Category | Standards Track |
| Status | Proposed Standard |
| Created | 2026-08-19 |
| Spec version | Tollana IR v0 |
| formatVersion | 1 |
| Updates | — |
| Obsoletes | — |
| Relates | RFC 0001 — Architecture |
This document specifies Tollana IR version 0: the instruction set, value types, validation, text and binary module encodings, the host.invoke suspend/resume protocol, the IR-level snapshot surface, and conformance programs.
Tollana IR is the native format of the Tollana stack-machine interpreter defined by RFC 0001. It is not WebAssembly and does not depend on a WebAssembly runtime. A breaking change to this ISA MUST increment formatVersion or be published as a new RFC.
Implementation sequencing and pull-request order are not requirements of this document.
This is a standards-track specification. Implementations MUST follow the requirements herein. Sections marked non-normative are informational.
- Introduction
- Background
- Conventions
- Canonical Naming
- Scope
- Abstract Machine
- Value Types and Sensitivity Labels
- Traps versus Suspend
- Validation
- Instruction Reference
- host.invoke Protocol
- Module Format
- IR-Level Snapshot Surface
- Instantiation and Invocation Interface
- Conformance Programs
- Appendix A — Alternatives Considered
- Appendix B — Reserved Extensions
- Security and Privacy Considerations
- Observability
- IANA Considerations
- References
RFC 0001 requires that a guest program can call a host function, be snapshotted mid-await, restored on a fresh machine, and continue correctly. This RFC defines the IR that makes that contract executable. The core is an explicit stack-machine interpreter. Its native format is Tollana IR — not WebAssembly, not a WASM engine.
This specification defines:
- Tagged runtime values (
i32,i64,unit,Capability) with a two-bitLabel. - A closed v0 instruction set using WASM-text names (
i32.add,local.get,host.invoke). The ISA is still Tollana IR, not WebAssembly. - Text (
.tir) and binary (.tirb) module encodings whose source of truth is the decodedInstructionstream. host.invokeas the only host-call opcode: it suspends theContinuationand yieldsHostCall; the host runs asynchronously; resume pushes aValueor traps.- Fuel checked before each instruction; exhaustion is resumable
OutOfFuel, not a trap. - An IR-level snapshot record that MUST round-trip core machine state (the on-disk AEAD container remains architecture-owned).
Implementers MUST be able to write MachineState, the interpreter loop, and the conformance programs without inventing opcode meanings.
RFC 0001 locks an interpreter-first core so the runtime owns stacks, frames, continuations, linear memory, capabilities, and fuel. External WASM runtimes are not part of the abstract machine. Guest languages will later lower to this IR or embed a snapshot-friendly runtime inside MachineState.
Pain points this v0 slice addresses:
- There is no executable contract for “suspend on host call / snapshot / restore / resume.”
- Plugin dispatch is numeric (
pluginId+methodId) but had no instruction or module import shape. - Capability unforgeability and sensitivity labels MUST live in the value representation from v0, even if full attenuation lands later.
- Tests need a hand-writable text format and a compact binary with fixed-width immediates.
Version 0 is complete for the durability contract: integer arithmetic, bounds-checked memory, structured control, host-driven sibling continuations, and in-flight host calls keyed by continuationIdentifier. It is not a language platform.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.
Normative sections: abstract machine, values, validation, every instruction, host-call protocol, encodings, snapshot field set, instantiation/invocation API, conformance programs, and the Limits tables (hard vs instance-config).
Non-normative sections: introduction, background, alternatives, reserved extensions, examples marked “illustrative.”
Opcodes follow WASM text: type.op with a dot (i32.add, i64.div_s, local.get, memory.size, host.invoke). Control names match WASM text (block, loop, if, else, end, br, br_if, call, return, drop, nop, unreachable).
This is a naming convention. The binary is Tollana IR (TIR\0), not WASM. There is no WASM opcode map.
Types use the same spellings as Rust / WASM: i32, i64, u8, u16, u32, u64, plus Tollana’s unit and Capability.
Machine objects stay full words (not UNIX one-letter types): Value, ValueType, Instruction, MachineState, Continuation, CallFrame, HostCall, CapHandle, Label. MUST NOT: Instr, Val, ValType, pc as a type or field.
| Kind | Canonical | MUST NOT use |
|---|---|---|
| Integers | i32, i64 |
Integer32, int32 |
| Unsigned widths | u8, u16, u32, u64 |
UnsignedInteger32 |
| Other values | unit, Capability |
void, (), cap as a type |
| Ops | i32.add, i32.const, i32.div_s, local.get, local.tee, host.invoke |
addI32, host_invoke, hostInvoke |
| Plugin ids | pluginId, methodId (values are u32) |
pid, mid |
Field labels: pluginId, methodId, functionIndex, instructionIndex, tableIndex, generation, immediateOffset, localIndex, globalIndex, hostImportIndex, labelDepth, typeIndex.
Rust mapping: strip dots and use PascalCase (I32Add for i32.add, I32DivS for i32.div_s, LocalGet for local.get, HostInvoke for host.invoke, BrIf for br_if). Fields MAY be snake_case (plugin_id, function_index). Types Instruction, MachineState, HostCall keep those names.
- Specify a closed v0 ISA sufficient for RFC 0001’s durability success criterion.
- Define
MachineState,Continuation,Value, fuel, trap vs suspend so an interpreter can be written from this document alone. - Define text and binary module encodings and a stack-typing validator specified in this document’s names.
- Define
host.invokesuspend/resume and the IR-level snapshot round-trip. - Provide eight conformance programs with expected traces.
- Keep opcode names in WASM text (
i32.add,host.invoke).
- Implementing the interpreter, crates, or tests (this RFC specifies behavior only).
- Plugin content-hash assignment (RFC 0001 §6).
- On-disk AEAD snapshot/journal container (RFC 0001 §11, §13).
- Goal trees,
code.runchild machines, AI gateway, language frontends, JIT. Float32/Float64, SIMD, GC heap objects,Stringas aValueType.MemoryGrow, multiple memories, indirect call, tables.- Binary compatibility with WebAssembly.
- A full information-flow type system (labels are carried; policy is host-side).
The interpreter executes one Module instance. MachineState.continuations is a list of live fibers. Sequential guests use one continuation; true concurrency (Promise.all, goal.all) is extra Continuation instances created by the host. The core does not create fibers from guest opcodes.
MachineState
├── moduleBytes raw .tirb (identity of the code)
├── entryExportName UTF-8; default entry is "main"
├── linearMemory untyped bytes; length = pageCount × 65536
├── globals[] Value
├── remainingFuel u64
├── quotas[] QuotaSlot (instance-level; absent dimension = unlimited)
├── capabilityTable[] CapabilityTableEntry
├── pendingHostCalls[] HostCall, at most one per continuationIdentifier
├── activeContinuationIdentifier optional u32
└── continuations[] unique continuationIdentifier values
Continuation
├── continuationIdentifier u32 ; unique in the instance; 0 is the first Invoke fiber, not reserved
├── valueStack[] Value (top = last)
└── callFrames[] CallFrame (top = current)
CallFrame
├── functionIndex u32
├── instructionIndex u32 // next Instruction to execute
├── locals[] Value // parameters then declared locals
├── controlStack[] ControlLabel
└── returnProgramCounter optional ProgramCounter // None = entry frame
ProgramCounter
├── functionIndex u32
├── instructionIndex u32
ControlLabel
├── labelKind block | loop | if
├── parameterCount u32
├── resultCount u32
├── stackHeight u32 // value-stack height beneath block params
└── branchInstructionIndex u32
CapabilityTableEntry
├── tableIndex u32
├── generation u32
├── live Boolean
└── hostIdentityOpaque host-owned bytes; IR MUST NOT interpret
HostCall
├── pluginId pluginId (u32)
├── methodId methodId (u32)
├── arguments[] Value, left-to-right declaration order
├── capabilities[] CapHandle subsequence of arguments
└── continuationIdentifier u32
QuotaSlot
├── dimension u8 ; RFC 0001 §12 codes; not 0
└── remaining u64
SuspendReason
├── host.invoke
├── OutOfFuel
└── QuotaExhausted dimension (u8)
ProgramCounter of the current frame is the pair (functionIndex, instructionIndex) stored in that CallFrame. Prose MAY say “program counter.”
flowchart TB
subgraph HostBoundary[Host process — not IR bytes]
PluginMap["plugin map: pluginId → content hash + impl"]
Quotas["quota policy at Instantiate"]
Scheduler["cooperative scheduler"]
end
subgraph MS[MachineState]
MB[moduleBytes]
MEM["linearMemory — untyped bytes"]
GLO[globals]
FUEL[remainingFuel]
QSLOTS[quotas[]]
CAP[capabilityTable]
PHC["pendingHostCalls — at most one per continuation"]
subgraph C[Continuation — N live]
VS[valueStack of Value]
CF[callFrames]
CS[controlStack per CallFrame]
end
end
Scheduler -->|"Continue / Resume / AddFuel / AddQuota"| MS
PluginMap -.->|"resolves host.invoke"| PHC
C -->|"yield"| PHC
PHC -->|"Resume Value or Trap"| C
Continue (see Instantiation and Invocation Interface) first applies the entry checks below, then repeatedly performs a step until Completed, Suspended, or Trapped.
A continuation is ready when it is in continuations[], has a non-empty callFrames, and has no pendingHostCalls entry with its continuationIdentifier.
Continue entry checks (MUST run before any step; none of these is a guest TrapKind):
- If the last guest outcome is
Trapped,ContinueMUST return that outcome again and MUST NOT execute. - Select the ready continuation with the lowest
continuationIdentifier. SetactiveContinuationIdentifierto that id. If several are ready, MUST NOT prefer the previously active fiber over a lower id. - If none is ready:
- If
pendingHostCallsis non-empty, MUST return host API errorHostCallPending. The host MUSTResumeorTrapPendingbycontinuationIdentifier. - Else if the last guest outcome is
Completed, MUST return that outcome again (idempotent replay) and MUST NOT execute. - Else MUST return host API error
InstanceIdle.
- If
On each step, the implementation MUST:
- Let
framebe the topCallFrame. IfinstructionIndexis out of range of that function’s decoded stream, MUST trapInvalidProgramCounter(malformed execution; validated modules never reach this ifbr/returnfollow this spec). - Fuel check (before the instruction): if
remainingFuel == 0, MUST suspend withSuspendReason.OutOfFuelwithout executing and without advancinginstructionIndex. MUST NOT append apendingHostCallsentry. AfterAddFuel, the host MUST callContinue(notResume). ThatContinueMUST re-attempt the same instruction, including a new fuel check. - Quota check (before the instruction, after the fuel check): if the instruction is
host.invokeand aHostCallCountslot is present withremaining == 0, MUST suspend withSuspendReason.QuotaExhaustedfor that dimension without executing, without charging fuel, and without advancinginstructionIndex. MUST NOT append apendingHostCallsentry. AfterAddQuota, the host MUST callContinue(notResume). - Subtract
1fromremainingFuel(decrement only whenremainingFuel >= 1). - Execute
instructionper Instruction Reference. A successfulhost.invokethat appends apendingHostCallsentry MUST then decrementHostCallCount.remainingby 1 when that slot is present.
Fuel cost: every executed Instruction, including nop, end, else, and host.invoke, costs 1. Resume, TrapPending, AddFuel, AddQuota, ConsumeQuota, SnapshotCore, RestoreCore, SpawnContinuation, and CancelContinuation MUST NOT decrement remainingFuel. Host-side plugin work is not IR fuel (RFC 0001 §12). Quota slots are independent of fuel. Sibling continuations share the instance remainingFuel.
Traps and instructionIndex: a trap MUST leave instructionIndex at the instruction that was executing (the one that just consumed fuel, if any). Traps MUST NOT advance instructionIndex past that instruction. Operands already popped by that instruction are not pushed back.
Two layers. Changing a hard cap is an IR/module-format break. Changing an instance-config default is not, provided the cap stays finite and does not exceed a hard cap.
Hard module-format limits (decode/validation MUST reject):
| Resource | MUST reject above |
|---|---|
| Module file size | 16 777 216 bytes (16 MiB) |
| Sum of Custom-section payloads | 1 048 576 bytes (1 MiB) |
functionCount / function types |
4096 |
| Locals per function (parameters + locals) | 4096 |
pageCount |
65536 (4 GiB). memory.size is i32; addresses are 32-bit. Implementations MUST reject pageCount > 65536. |
sectionByteLength |
MUST NOT exceed remaining bytes after the length field |
Instance-config limits (MUST be finite; recommended defaults below; host MAY raise at instantiate but MUST NOT exceed the hard cap where one exists):
| Resource | Recommended default | Runtime trap if exceeded |
|---|---|---|
| Value stack depth | 65536 Values |
ValueStackOverflow |
| Call frame depth | 1024 | CallStackOverflow |
| Control-stack depth per frame | 1024 | ControlStackOverflow |
| Live continuations | 1024 | reject SpawnContinuation (host API error, not a guest trap) |
| Host max pages | 16 (1 MiB) | reject instantiate if module pageCount > host max |
AddFuel MUST saturate at u64 maximum (MUST NOT wrap). A zero amount is allowed and is a no-op.
A step burst (run-until-yield) ends in exactly one of:
| Outcome | Meaning |
|---|---|
Completed { results[] } |
The active continuation returned with an empty call stack and is removed from continuations[]. Sibling continuations, if any, remain. The step burst ends; the host MUST Continue to run another ready fiber. |
Suspended { reason: host.invoke } |
a pendingHostCalls entry is set for the active continuation |
Suspended { reason: OutOfFuel } |
the active continuation is captured at the unpaid instruction; siblings remain |
Trapped { trapKind, programCounter } |
fatal for the instance; MUST NOT execute further except discard/inspect |
Code (u8) |
Name | Payload |
|---|---|---|
0x01 |
i32 |
32-bit two’s-complement bits |
0x02 |
i64 |
64-bit two’s-complement bits |
0x03 |
unit |
empty |
0x04 |
Capability |
CapHandle |
All other type codes MUST fail decode/validation. There is no String, Float32, Float64, or heap object type in v0.
Value {
valueType: ValueType
payload: integer bits | CapHandle | (empty)
sensitivityLabel: Label
}
Every Value on the stack, in locals, in globals, in HostCall.arguments, and in snapshots MUST carry a Label.
Code (low 2 bits of an u8) |
Name |
|---|---|
0 |
Public |
1 |
Internal |
2 |
Confidential |
3 |
Secret |
Order (non-normative drawing; join is least upper bound):
flowchart BT
Public --> Internal --> Confidential --> Secret
join(a, b) = max(a, b)on this total order.- Constants produced by
i32.const/i64.constMUST bePublic. - Binary integer operations: result label MUST be
join(lhs, rhs). - Unary
i32.eqz/i64.eqz: result label MUST equal the operand’s label. - Loads from linear memory MUST produce
Publicvalues of the load type. Memory-region labels are a later extension. - Copies (
local.get,global.get,local.tee, passing) MUST preserve the source label. - The IR MUST NOT drop a label. Host policy MAY refuse a
host.invokebased on argument labels; that refusal is a trap or host-denied resume, not silent downgrade.
CapHandle {
tableIndex: u32
generation: u32
}
Null handle: { tableIndex: 0, generation: 0 }. The table MUST NOT allocate this pair as a live entry. generation == 0 is reserved for null; live entries MUST use generation >= 1.
Unforgeability (normative):
- Guests MUST NOT construct a
Capabilitywithi32.const,i64.const, arithmetic, comparison, or load/store. Those opcodes’ result types are integers (ori32booleans). Validation MUST reject any attempt to treat those results asCapability. - A
CapabilityValueappears only by: host-injected entry parameters, host-injected globals, host resume values of typeCapability, or copying an existingCapabilityvia locals/globals/stack (local.get,local.set,local.tee,global.get,global.set,dropdoes not create one). - Using a handle that is null, not live, or whose
generationdoes not match the table entry MUST trapInvalidCapability. - Stale generation after slot reuse MUST trap
InvalidCapability. - When bumping
generationon reuse, if the next value would be0, the implementation MUST skip0.
Integer locals initialize to 0 with label Public. unit locals initialize to unit / Public. Capability locals and non-injected Capability globals initialize to the null handle with label Public. Moving null is allowed; using it in host.invoke MUST trap.
Linear memory is untyped bytes. Guest string payloads in memory have no IR type. The convention for text is UTF-8; the IR does not validate memory contents as UTF-8. There is no String value type in v0.
Names in the module (exports, optional function/import names stored in the binary) MUST be UTF-8; see Module Format.
| Class | Resumable? | Causes |
|---|---|---|
| Suspend | Yes | host.invoke, OutOfFuel |
| Trap | No | out-of-bounds memory; divide/remainder by zero; signed divide overflow; invalid/stale/null Capability; unreachable; value/call/control overflow; type/tag mismatch; missing plugin; host import type mismatch at runtime |
TrapKind (canonical names):
| Name | When |
|---|---|
UnreachableInstruction |
unreachable executed |
OutOfBoundsMemory |
load/store effective range not inside memory |
IntegerDivideByZero |
signed divide or remainder with divisor 0 |
IntegerOverflow |
i32.div_s / i64.div_s of MIN / -1 |
InvalidCapability |
null, stale, or unknown handle used as a capability |
ValueStackOverflow |
push beyond cap |
ValueStackUnderflow |
pop on empty (should be unreachable if validation is correct; still MUST trap) |
CallStackOverflow |
call / Invoke beyond call-depth cap |
ControlStackOverflow |
pushing a ControlLabel beyond the per-frame cap |
TypeMismatch |
popped or local/global Value.valueType is not the type required by the instruction (except host.invoke arguments — those are HostTypeMismatch) |
InvalidProgramCounter |
instructionIndex out of range (should be unreachable) |
HostNotFound |
pluginId / methodId not in the instance plugin map |
HostTypeMismatch |
host.invoke argument tag, or Resume result tag, does not match the HostImport type |
A trapped instance MUST remain inspectable (snapshot of pre-trap or at-trap state is host-defined) but MUST NOT execute further guest instructions.
Invalid modules MUST NOT execute. Decode errors and validation errors are reject-at-load (or reject-at-instantiate before any CallFrame exists), never runtime traps of a running guest.
This section is the algorithm. It is not a citation of the WebAssembly Core Specification. WASM is prior art only (Alternatives Considered).
ValidationControlFrame {
labelKind: Function | block | loop | if
parameterTypes[]: ValueType
resultTypes[]: ValueType
height: u32 // type-stack height under this frame’s parameters
unreachable: Boolean // set when this frame’s body becomes stack-polymorphic
elseSeen: Boolean // If only
}
Every function starts with one Function frame pushed (parameterTypes / resultTypes from the function type, height = 0, unreachable = false). Parameters are not on the type stack; they live in the local-type vector. The body MUST end by validating an end that pops this frame.
pushType(t): MUST appendteven if the current frame isunreachable. Dead code stays typed aboveheight. Implementations MUST NOT skip the push.popType(expected): ifunreachableandtypeStack.length == current.height, succeed without popping (polymorphic only at the truncated base). Else if stack length ≤current.height, fail. Else popactual; ifactual ≠ expected, fail.popTypes(ts): poptsfrom right to left (last type intspopped first).pushTypes(ts): push left to right.markUnreachable(): setcurrent.unreachable = true; truncatetypeStacktocurrent.height.
Underflow of the type stack in reachable code MUST fail.
| Encoding | parameterTypes |
resultTypes |
|---|---|---|
Empty |
[] |
[] |
SingleResult t |
[] |
[t] |
TypeIndex i |
type section entry i parameters |
that entry’s results |
Unknown typeIndex MUST fail.
Let L be the frame at labelDepth (0 = innermost, including the function frame).
- If
L.labelKind == Loop: branch signature isL.parameterTypes(continue). - Else (
Function,block,if): branch signature isL.resultTypes(exit). Branching to theFunctionframe is valid and is areturnat runtime.
labelDepth MUST be < controlStack.length.
| Instruction | Type-stack action |
|---|---|
nop |
none |
unreachable |
markUnreachable() |
drop |
pop one type (any ValueType; fail if reachable-empty) |
i32.const |
pushType(i32) |
i64.const |
pushType(i64) |
| i32 arithmetic / tests | popTypes then pushType per Instruction Reference |
| i64 arithmetic / tests | same; compare / i64.eqz push i32 |
local.get |
localIndex in range; pushType(localTypes[localIndex]) |
local.set |
popType(localTypes[localIndex]) |
local.tee |
popType(t) then pushType(t) for that local |
global.get / global.set |
index in range; global.set requires Mutability = Mutable |
Memory ops / memory.size |
module MUST declare exactly one memory; stack effects as in the instruction reference |
block |
popTypes(params); push frame (block, height = typeStack.length, unreachable = false); pushTypes(params) |
loop |
same with labelKind = loop |
if |
popType(i32) then same as block with labelKind = if, elseSeen = false |
else |
current MUST be if with elseSeen = false. popTypes(results); type stack length MUST equal height unless unreachable; set elseSeen = true, unreachable = false; pushTypes(params) of the if |
end |
popTypes(results); if not unreachable, type stack length MUST equal height; then MUST truncate typeStack to height (discard types pushed in dead code); pop frame; pushTypes(results) onto the parent. If the popped frame is if with non-empty resultTypes and elseSeen = false, fail. Empty-result if MAY omit else. Function-body end MUST leave the control stack empty and the type stack equal to the function resultTypes. |
br |
popTypes(branch signature of L); markUnreachable() |
br_if |
popType(i32) then popTypes(branch signature of L) then pushTypes those types (optional branch) |
call |
functionIndex in range; popTypes(callee params); pushTypes(callee results) |
return |
popTypes(function resultTypes); markUnreachable() |
host.invoke |
hostImportIndex in range; same as call against the import type |
Integer operators, loads, and memory.size MUST NOT produce Capability.
- Every
typeIndex,functionIndex,hostImportIndex,localIndex,globalIndexthat appears MUST be in range. - Constant expressions for globals: only
i32.constori64.constthenend, matching the global’sValueType.Capabilityglobals MUST useGlobalInitKindHostInjected. - Duplicate export names MUST be rejected. Duplicate
HostImportnames MUST be rejected (texthost.invokebinds by name). Duplicate(pluginId, methodId)pairs MUST be rejected. - Export
"main"MAY be absent; thenInvoke("main")MUST fail. It is the default entry when present. - Memory instructions require a memory; modules with
memoryCount = 0MUST NOT contain them.
HostImport.pluginId / methodId are instance-local IDs written by codegen or test fixtures, not allocated by the interpreter. Architecture §4 sort-by-hash assignment is a host step that MUST run before instantiate; the map passed to Instantiate MUST use those same numbers.
Instantiate MUST fail if:
- Any import pair is missing from the plugin map, or
- The host’s type/arity for that pair disagrees with the import’s function type, or
- The map would assign a different
pluginIdthan the module encodes (explicit-map v0: the host MUST NOT re-number at instantiate).
Runtime HostNotFound exists only if the map is mutated after instantiate (hosts MUST NOT). RestoreCore MUST re-bind the same local identifiers and content hashes.
- Opcode is
u8. Unknown opcodes MUST fail decode. - Immediates are little-endian fixed-width (not LEB128).
- Stack effect is written with the stack growing rightward. Execution pops the rightmost operand first.
- Fuel: 1, after the pre-instruction fuel check.
- Tags: if a popped
Value.valueTypeis not the required type, MUST trapTypeMismatch, excepthost.invokearguments, which MUST trapHostTypeMismatch(same carve-out asResumeresults). instructionIndex: eachInstructionin the decoded stream counts as 1 (not bytes).block,loop,if,else,endeach count as one. After a non-control sequential instruction that succeeds,instructionIndexincreases by 1. Control transfers set it as specified. A trap MUST leaveinstructionIndexon the trapping instruction.
| Opcode | Instruction | Immediate(s) |
|---|---|---|
0x00 |
nop |
— |
0x01 |
unreachable |
— |
0x02 |
drop |
— |
0x10 |
i32.const |
value: i32 (4 bytes) |
0x11 |
i64.const |
value: i64 (8 bytes) |
0x20 |
i32.add |
— |
0x21 |
i32.sub |
— |
0x22 |
i32.mul |
— |
0x23 |
i32.div_s |
— |
0x24 |
i32.rem_s |
— |
0x25 |
i32.eqz |
— |
0x26 |
i32.eq |
— |
0x27 |
i32.ne |
— |
0x28 |
i32.lt_s |
— |
0x29 |
i32.gt_s |
— |
0x2A |
i32.le_s |
— |
0x2B |
i32.ge_s |
— |
0x30 |
i64.add |
— |
0x31 |
i64.sub |
— |
0x32 |
i64.mul |
— |
0x33 |
i64.div_s |
— |
0x34 |
i64.rem_s |
— |
0x35 |
i64.eqz |
— |
0x36 |
i64.eq |
— |
0x37 |
i64.ne |
— |
0x38 |
i64.lt_s |
— |
0x39 |
i64.gt_s |
— |
0x3A |
i64.le_s |
— |
0x3B |
i64.ge_s |
— |
0x40 |
local.get |
localIndex: u32 |
0x41 |
local.set |
localIndex: u32 |
0x42 |
local.tee |
localIndex: u32 |
0x43 |
global.get |
globalIndex: u32 |
0x44 |
global.set |
globalIndex: u32 |
0x50 |
i32.load |
immediateOffset: u32 |
0x51 |
i32.store |
immediateOffset: u32 |
0x52 |
i64.load |
immediateOffset: u32 |
0x53 |
i64.store |
immediateOffset: u32 |
0x54 |
memory.size |
— |
0x60 |
block |
blockType: BlockType |
0x61 |
loop |
blockType: BlockType |
0x62 |
if |
blockType: BlockType |
0x63 |
else |
— |
0x64 |
end |
— |
0x65 |
br |
labelDepth: u32 |
0x66 |
br_if |
labelDepth: u32 |
0x67 |
call |
functionIndex: u32 |
0x68 |
return |
— |
0x70 |
host.invoke |
hostImportIndex: u32 |
All other opcode bytes are reserved and MUST fail decode.
| First byte | Meaning | Following bytes |
|---|---|---|
0x00 |
Empty (0 params, 0 results) |
— |
0x01 |
SingleResult |
one ValueType byte |
0x02 |
TypeIndex |
typeIndex: u32 (params and results from the type section) |
Two’s complement. Add/subtract/multiply wrap modulo 2^32 or 2^64. Comparisons and i32.eqz / i64.eqz produce i32 1 (true) or 0 (false). There are no unsigned divide or compare opcodes and no i32/i64 convert opcodes in v0.
Signed divide truncates toward zero. Signed remainder follows the dividend sign. Divisor 0 MUST trap IntegerDivideByZero.
i32 min is -2147483648. i64 min is -9223372036854775808.
i32.div_sofmin / -1MUST trapIntegerOverflow.i64.div_sofmin / -1MUST trapIntegerOverflow.i32.rem_sofmin % -1MUST NOT trap; result is0.i64.rem_sofmin % -1MUST NOT trap; result is0.
- Immediates:
value: i32. - Stack:
[] -> [i32]. - Result label:
Public. - Traps:
ValueStackOverflow.
- Immediates:
value: i64. - Stack:
[] -> [i64]. - Result label:
Public. - Traps:
ValueStackOverflow.
Pop rhs then lhs. Result label = join(lhs, rhs).
| Instruction | Opcode | Stack | Traps |
|---|---|---|---|
i32.add |
0x20 |
[i32, i32] -> [i32] |
TypeMismatch, ValueStackUnderflow |
i32.sub |
0x21 |
same (lhs - rhs) |
same |
i32.mul |
0x22 |
same | same |
i32.div_s |
0x23 |
same | plus IntegerDivideByZero, IntegerOverflow (min / -1) |
i32.rem_s |
0x24 |
same | plus IntegerDivideByZero only (min % -1 yields 0) |
| Instruction | Opcode | Stack | Traps |
|---|---|---|---|
i32.eqz |
0x25 |
[i32] -> [i32] |
TypeMismatch, ValueStackUnderflow |
i32.eq |
0x26 |
[i32, i32] -> [i32] |
same |
i32.ne |
0x27 |
same | same |
i32.lt_s |
0x28 |
same | same |
i32.gt_s |
0x29 |
same | same |
i32.le_s |
0x2A |
same | same |
i32.ge_s |
0x2B |
same | same |
Same wrapping and join rules with i64 operands. Comparison and i64.eqz results are i32.
| Instruction | Opcode | Stack | Traps |
|---|---|---|---|
i64.add |
0x30 |
[i64, i64] -> [i64] |
TypeMismatch, ValueStackUnderflow |
i64.sub |
0x31 |
same (lhs - rhs) |
same |
i64.mul |
0x32 |
same | same |
i64.div_s |
0x33 |
same | plus IntegerDivideByZero, IntegerOverflow (min / -1) |
i64.rem_s |
0x34 |
same | plus IntegerDivideByZero only (min % -1 yields 0) |
i64.eqz |
0x35 |
[i64] -> [i32] |
TypeMismatch, ValueStackUnderflow |
i64.eq |
0x36 |
[i64, i64] -> [i32] |
same |
i64.ne |
0x37 |
same | same |
i64.lt_s |
0x38 |
same | same |
i64.gt_s |
0x39 |
same | same |
i64.le_s |
0x3A |
same | same |
i64.ge_s |
0x3B |
same | same |
Parameters occupy localIndex 0 .. parameterCount-1. Declared locals follow in declaration order.
- Immediate:
localIndex. - Stack:
[] -> [Value]copy oflocals[localIndex](label and tag preserved, includingCapability). - Traps:
ValueStackOverflow. Out-of-rangelocalIndexMUST have failed validation.
- Immediate:
localIndex. - Stack:
[t] -> []wheretmatches the local’sValueType. - Stores the popped value (label preserved).
- Traps:
TypeMismatch,ValueStackUnderflow.
- Immediate:
localIndex. - Stack:
[t] -> [t]. - Writes
locals[localIndex]without removing the stack copy (observably: copy then set, or set then push the sameValue). - Traps:
TypeMismatch,ValueStackUnderflow.
- Immediate:
globalIndex. global.get:[] -> [Value]. Traps:ValueStackOverflow.global.set:[t] -> []; the global MUST beMutable; type MUST match. Traps:TypeMismatch,ValueStackUnderflow.- Setting an
Immutableglobal MUST fail validation (not a runtime trap in valid modules).
- Stack:
[t] -> []for anyValueTypeincludingCapability. - Traps:
ValueStackUnderflow. - Does not create or forge handles; dropping a capability does not revoke the table entry in v0 (revocation is host-side / later).
- Stack:
[] -> []. - Traps: none.
- No effect other than fuel.
- Stack: polymorphic (validation treats subsequent code as unreachable).
- Traps:
UnreachableInstruction(after consuming fuel). MUST NOT advanceinstructionIndex.
Page size MUST be 65536 bytes. The module declares pageCount. In v0 maximum = minimum (fixed). No MemoryGrow. At most one memory.
Encoding: little-endian. Unaligned load/store MUST be allowed.
Address operand is i32. Let addressBits be its 32 bits interpreted as u32.
effectiveAddress = u64(addressBits) + u64(immediateOffset)
If effectiveAddress + accessSize > memoryLength (comparison in u64), MUST trap OutOfBoundsMemory. Addition of address and offset MUST NOT wrap into low memory; the 64-bit sum is authoritative.
Loads produce Public values. Stores write raw bits; they MUST NOT write a Capability (type mismatch).
- Immediate:
immediateOffset. - Stack:
[i32 address] -> [i32]. accessSize = 4. Little-endian two’s-complement.- Traps:
TypeMismatch,ValueStackUnderflow,OutOfBoundsMemory,ValueStackOverflow.
- Immediate:
immediateOffset. - Stack:
[i32 address, i32 value] -> [](pop value, then address). - Traps:
TypeMismatch,ValueStackUnderflow,OutOfBoundsMemory.
Same with accessSize = 8 and i64 data. Same trap set as the i32 pair.
- Stack:
[] -> [i32]page count, labelPublic. - Traps:
ValueStackOverflow. - Requires a memory in the module.
Structured control only. There are no unstructured jumps to arbitrary instructionIndex values.
Implementations MAY precompute matching else / end indices at decode time as derived fields. Observable behavior MUST match the operational semantics below.
EnterFrame (used by Invoke and call):
- Pop callee arguments into
locals[0 .. parameterCount)(already done by the caller of EnterFrame). - Zero/null-initialize remaining locals.
- Push
CallFramewithinstructionIndex = 0,controlStackempty, then immediately push the implicit functionControlLabel:labelKind = block(same encoding as an innerblock; not a discriminator)parameterCount/resultCountfrom the function typestackHeight= value-stack height after arguments have been moved to localsbranchInstructionIndex=instructionIndexof the function’s closingend(unused when the label is targeted; seebr)
- If that push would exceed the control-stack cap, MUST trap
ControlStackOverflowand MUST NOT leave a partialCallFrame(pop it if already pushed).
The implicit function label is always controlStack[0] of that frame (deepest / first pushed). Inner block labels have labelKind = block too; they are not at index 0 unless the body has no other labels. Validation’s labelKind = function is validator-only and MUST NOT appear in snapshots.
- Immediate:
BlockType. Fuel: 1. Traps:ControlStackOverflow. - Resolve params/results from
BlockType. Params are already on the stack (validation). - Push
ControlLabel:labelKind = block;parameterCount/resultCountfrom the type;stackHeight= current value-stack height minusparameterCount;branchInstructionIndex= index of the matchingend. - Set
instructionIndexto the next instruction (body).
- Immediate:
BlockType. Fuel: 1. Traps:ControlStackOverflow. - Push
ControlLabel:labelKind = loop;parameterCount/resultCountfrom the type;stackHeight= current height minusparameterCount;branchInstructionIndex= index of the instruction after thisloop(body start). - Set
instructionIndexto the next instruction.
- Immediate:
BlockType. Fuel: 1. Traps:TypeMismatch,ValueStackUnderflow,ControlStackOverflow. - Pop
i32condition. Block parameters remain on the stack. - Push
ControlLabel:labelKind = if;parameterCount/resultCountfrom the type;stackHeight= current height minusparameterCount;branchInstructionIndex= matchingend. - If condition ≠ 0: set
instructionIndexto the next instruction (then-arm). - If condition = 0 and the
ifhaselse: setinstructionIndexto the instruction after thatelse(do not executeelse). Keep theiflabel. - If condition = 0 and there is no
else: setinstructionIndexto the matchingend(thatendwill execute and pop the label).
TypeIndex parameters: after popping the condition they remain under the then/else bodies exactly as for block.
- Fuel: 1. Traps: none in a valid module (a reachable
elseis only executed from a true then-arm). - MUST pop the current
ifControlLabel. - MUST set
instructionIndexto the instruction after the matchingend(skip the else-arm and do not execute thatend). - Result values already on the stack stay; height MUST equal
stackHeight + resultCount(validation).
- Fuel: 1. Traps: none in a valid module besides those of
returnwhen it closes a function. - Pop the current
ControlLabel. - If
controlStackis now empty (the popped label wascontrolStack[0], the implicit function label): performreturn(do not charge a second fuel). - Else: set
instructionIndexto the next instruction. Results stay on the stack (resultCountvalues abovestackHeight).
- Immediate:
labelDepth(0= innermost). Fuel: 1. Traps:TypeMismatch,ValueStackUnderflow. - Let
Lbe the label at that depth (0= innermost = top ofcontrolStack). - If
Lis the implicit function label — defined ascontrolStack[0]of the current frame, equivalentlylabelDepth == controlStack.length - 1— MUST performreturnwith the function’sresultCount(same pops, frame discard,Completedor caller push). MUST NOT setinstructionIndexpast the function’s lastend. MUST NOT decide this bylabelKind == Block(inner blocks share that kind). - Otherwise:
- Let
n=L.parameterCountifL.labelKind == Loop, elseL.resultCount. - Pop
nvalues, unwind the value stack toL.stackHeight, push thosenvalues. - Pop every
ControlLabelstrictly insideL. IfLisloop, keepL. IfLisblockorif, popLas well. - Charge fuel for
bronly. Do not execute a skippedend. - Landing:
loop→instructionIndex = L.branchInstructionIndex(body start).block/if→instructionIndex= the instruction after the matchingend.
- Let
- Immediate:
labelDepth. Fuel: 1. Traps:TypeMismatch,ValueStackUnderflow. - Pop
i32condition (label discarded). - If condition ≠ 0: same as
br(including function-labelreturn). If 0: setinstructionIndexto the next instruction.
- Immediate:
functionIndex. Fuel: 1. Traps:TypeMismatch,ValueStackUnderflow,CallStackOverflow,ControlStackOverflow. - Pop arguments rightmost first. On later traps, those operands stay consumed.
- Let
returnSite = (caller.functionIndex, caller.instructionIndex + 1). MUST NOT writecaller.instructionIndexyet. - If
callFrames.length + 1would exceed the call-depth cap, MUST trapCallStackOverflowwith the callerinstructionIndexstill on thiscall. EnterFramefor the callee withreturnProgramCounter = returnSite(implicit functionControlLabel, not an emptycontrolStack). IfEnterFrametrapsControlStackOverflow, MUST NOT mutate the callerinstructionIndexand MUST NOT leave the new frame on the stack.- Only after
EnterFramesucceeds: set the caller frame’sinstructionIndextoreturnSite.instructionIndex. Snapshots of nested calls MUST then show the caller at that return site, not oncall. - Execute next in the callee at
instructionIndex = 0.
- Fuel: 1 (already consumed by
endwhenenddelegates here; do not charge twice). Traps:TypeMismatch,ValueStackUnderflow. - Pop
resultCountvalues of the current function type. - Discard every remaining
ControlLabelon this frame (nestedblock/loop/if). - Pop the
CallFrame. - If no frames remain: remove this continuation from
continuations[]; clearactiveContinuationIdentifierif it named this fiber; this continuation completes with those results. Sibling continuations MUST remain. - Else: push results onto the caller’s value stack; the caller’s
instructionIndexis alreadyreturnProgramCounter. The nextContinuestep executes that instruction.
Function-closing end MUST behave as return after popping the function label.
- Immediate:
hostImportIndexinto the module’sHostImportvector. - Look up
pluginId,methodId, and the import function type. - Stack effect: same as calling that type (pop params rightmost first; results are not pushed now).
- Fuel: 1 for this opcode; host work is not IR fuel.
- Traps (before suspend):
HostTypeMismatchon argument tags (notTypeMismatch),ValueStackUnderflow,InvalidCapability,HostNotFound. On any of these: operands already popped are not restored;instructionIndexstays on this instruction; apendingHostCallsentry MUST NOT be appended. - Then follow host.invoke Protocol. MUST NOT run the host inside the interpreter.
If any parameter is Capability, the handle MUST be validated live before suspend; invalid MUST trap InvalidCapability and MUST NOT yield HostCall.
Missing plugin at runtime MUST trap HostNotFound.
sequenceDiagram
participant Guest as Interpreter
participant Cont as Continuation
participant State as MachineState
participant Host as Host scheduler / plugin
Guest->>Guest: Fuel check; consume 1 for host.invoke
Guest->>Guest: Pop parameters (rightmost first)
Guest->>Guest: Validate Capability handles
Guest->>Cont: Advance instructionIndex past host.invoke
Guest->>Cont: Suspend (do not push results)
Guest->>State: append pendingHostCalls HostCall
Guest->>Host: yield Suspended host.invoke
Note over Host: Plugin runs asynchronously (not IR fuel)
alt Success
Host->>Guest: Resume(continuationIdentifier, results)
Guest->>State: remove that pendingHostCalls entry
Guest->>Cont: Push result Value(s); Continue (lowest-ready-id)
else Plugin or policy failure
Host->>Guest: TrapPending(continuationIdentifier, trapKind)
Guest->>State: remove that pendingHostCalls entry; instance trapped
end
Normative steps when executing host.invoke:
- Pop parameters (rightmost first) into a list
argumentsrestored to declaration order (left-to-right). - Build
capabilitiesas everyCapHandleamongarguments, same order. - Validate each capability against
capabilityTable. On failure, trap; do not suspend. - Resolve
(pluginId, methodId)in the instance plugin map. On failure, trapHostNotFound. - Set the current frame
instructionIndexto the instruction afterhost.invoke. - Suspend the current
Continuation(it remains inMachineState.continuations). - Append to
pendingHostCalls(MUST NOT already contain thiscontinuationIdentifier):
HostCall {
pluginId,
methodId,
arguments, // full Values including labels
capabilities, // handles only
continuationIdentifier // the active continuation
}
- Return
Suspended { host.invoke }to the host. The interpreter MUST NOT call the plugin.
The host MUST eventually:
- Resume with a result list matching the import’s result types (v0 tests use one
i32or oneCapability), or - Trap the pending call.
On resume:
Resume(continuationIdentifier, results): apendingHostCallsentry with that identifier MUST be present; elseResumeMUST return a host API error (not a guest trap).ResumeMUST NOT be used in place ofContinueafterOutOfFuelorQuotaExhausted.- Remove that pending entry. MUST NOT decrement
remainingFuel. MUST NOT remove other pending calls. - For each result
Value: tag MUST match the import type (HostTypeMismatchif not).Capabilityresults MUST be installed incapabilityTable(host may pass an existing live handle or a newly allocated(tableIndex, generation)). - Set
activeContinuationIdentifierto that identifier and push results onto that continuation’s value stack (left-to-right, last result on top). - Call
Continue(lowest-ready-id rule; MAY run a different ready fiber than the one just resumed).
Zero-result imports: push nothing. A result type of unit requires pushing a unit value.
TrapPending(continuationIdentifier, trapKind) MUST require a pending call with that identifier, clear that entry, MUST NOT decrement fuel, and MUST mark the instance Trapped.
A snapshot taken after step 7 MUST include every live Continuation and every pendingHostCalls entry. Restore on a fresh machine MUST restore both. The host MUST also restore plugin identity hashes and implementations (not IR-specified bytes). After restore, resume is identical to a no-migrate resume.
Text:
(module
(host.import Echo
(pluginId 0)
(methodId 0)
(param i32)
(result i32))
(func (export "main") (result i32)
(host.invoke Echo (i32.const 41))))
Decoded body of function 0:
| instructionIndex | Instruction |
|---|---|
| 0 | i32.const 41 |
| 1 | host.invoke hostImportIndex 0 |
| 2 | end |
After the invoke suspends:
remainingFueldecreased by 2 from the run start (push + invoke), if started with enough fuel.valueStackempty.CallFrame:functionIndex = 0,instructionIndex = 2, locals empty, control stack = implicit function label only.pendingHostCalls(one entry):
pluginId: 0
methodId: 0
arguments: [ Value { valueType: i32, payload: 41, sensitivityLabel: Public } ]
capabilities: []
continuationIdentifier: 0
Resume i32 42 Public pushes 42; end returns 42.
Binary of host.invoke at index 1: opcode 70, immediate 00 00 00 00.
Implicit function ControlLabel in this snapshot:
labelKind: 1 ; block (implicit function)
parameterCount: 0
resultCount: 1
stackHeight: 0
branchInstructionIndex: 2 ; function End (unused: Branch to this label is Return)
A complete TIRS encoding of this suspended instance (fuel started at 1000) is in Complete TIRS hex — Echo suspend. Field order matches the TIRS layout there; this is not a subset and MUST NOT be compared out of order.
Three layers; the decoded Instruction sequence is the source of truth.
| Layer | Suggested extension | Role |
|---|---|---|
| Text | .tir |
Required for hand-written tests |
| Binary | .tirb |
Compact distribution / snapshots of code |
| In-memory | — | Instruction stream the interpreter runs |
- All names stored in the module (export names,
HostImportnames, optional function names) MUST be UTF-8. - Binary string:
byteLength: u32, then exactly that many bytes. MUST NOT include a NUL terminator in the length (no extra00counted as terminator). Invalid UTF-8 MUST fail validation. Embedded U+0000 MUST be rejected (export lookup MUST NOT be C-string truncated). - Text-format identifiers that are stored MUST be UTF-8 and MUST NOT contain U+0000.
S-expressions. ; comments run to end of line. Numbers: decimal or 0x hex (i32 / i64 / unsigned immediates).
Lexicon
Identifier: first character ASCIIA–Z/a–z/_, then those plus0–9. Additional UTF-8 letters are allowed. MUST NOT contain U+0000.String:"…"with escapes\\,\",\n,\t,\u{hex}(Unicode scalar). MUST NOT encode U+0000.Name:IdentifierorString. Import/export names stored in the module are the string value.
Folding. Folded forms MUST desugar left-to-right, operator last (lhs pushed before rhs):
(i32.add (i32.const 1) (i32.const 2))
is
i32.const 1
i32.const 2
i32.add
Folded (host.invoke Name Operand*): Operand count MUST equal that import’s parameter count. Extra or missing operands MUST fail assemble.
Folded structured control (MUST desugar a matching end per construct; this is in addition to the function-level implicit end):
(block bt body*) → block bt, desugar(body*), end
(loop bt body*) → loop bt, desugar(body*), end
(if bt then*) → if bt, desugar(then*), end
(if bt then* else else*) → if bt, desugar(then*), else, desugar(else*), end
else inside a folded if is the keyword else, not a nested folded instruction. Nested block/loop/if each emit their own end. Example: (func (export "main") (result i32) (block (result i32) (i32.const 1))) desugars to block, i32.const 1, end (block), then the function-level end.
Function-level implicit end. A function body’s decoded stream MUST end with end. If the text body’s last instruction (after folded-control desugar) is not a function-closing end, the assembler MUST append one. Folded function bodies MUST NOT write that trailing end; the assembler always appends it.
Grammar (normative):
Module ::= (module ModuleField*)
ModuleField ::= MemoryField | HostImport | Function | GlobalField
MemoryField ::= (memory (pages u32))
HostImport ::= (host.import Name
(pluginId u32)
(methodId u32)
ParameterDecl* ResultDecl*)
ParameterDecl ::= (param ValueType)
ResultDecl ::= (result ValueType)
Function ::= (func FunctionHeader* InstructionForm*)
FunctionHeader ::= (export String)
| ParameterDecl | ResultDecl
| (local ValueType)
; order MUST be: export? then param* then result* then local*
GlobalField ::= (global Mutability? ValueType GlobalInit)
Mutability ::= (mutable) ; absent => Immutable
GlobalInit ::= (host.injected) | InstructionForm* ; constant expr; assembler appends end
ValueType ::= i32 | i64 | unit | Capability
InstructionForm ::= Unfolded | Folded
Unfolded ::= InstructionName ImmediateAtom* ; next to `end`/`else`/`block`… at column 0 of a body
Folded ::= (InstructionName ImmediateAtom* InstructionForm*)
ImmediateAtom ::= Integer ; positional, see table
| (offset u32)
| (localIndex u32)
| (globalIndex u32)
| (labelDepth u32)
| (functionIndex u32)
| (hostImportIndex u32) ; binary only; text host.invoke uses Name
| (typeIndex u32)
| BlockTypeAtom
| Name ; host.invoke import name
BlockTypeAtom ::= empty | (result ValueType) | (typeIndex u32)
Positional immediates (when the labeled form is omitted):
| Instruction | Text immediates | Default if omitted |
|---|---|---|
i32.const / i64.const |
the constant | none (required) |
local.get / local.set / local.tee |
localIndex |
none |
global.get / global.set |
globalIndex |
none |
i32.load / i32.store / i64.load / i64.store |
(offset n) then address operand |
immediateOffset = 0 |
br / br_if |
labelDepth |
none |
call |
functionIndex |
none |
block / loop / if |
BlockTypeAtom |
Empty |
host.invoke |
import Name |
none |
Examples:
(i32.load (i32.const 0)) ; immediateOffset 0
(i32.load (offset 4) (i32.const 0))
(br 0)
(block (result i32) ... )
host.invoke in text uses the import name, not hostImportIndex. The assembler MUST resolve it. Duplicate import names MUST fail.
Deterministic type-section assignment (.tir → .tirb MUST be unique):
- Start with an empty type list.
- For each
HostImportin source order, letTbe(ParameterDecl* → ResultDecl*). If an identicalTalready exists, reuse itstypeIndex; else appendT. - For each
Functionin source order, same reuse/append using that function’s parameters and results.
Therefore Echo: import (i32)→(i32) is type 0; main ()→(i32) is type 1.
Functions in the Function/Code sections appear in source order. Host imports do not occupy functionIndex.
Example (canonical, from the binding):
(module
(memory (pages 1))
(host.import Echo
(pluginId 0)
(methodId 0)
(param i32)
(result i32))
(func (export "main") (result i32)
(host.invoke Echo (i32.const 41))))
Little-endian throughout. Fixed-width immediates.
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 4 | magic | 54 49 52 00 (“TIR” + NUL) |
| 4 | 2 | formatVersion |
u16 1 (01 00) |
| 6 | 2 | reserved |
u16 0 |
Other formatVersion MUST be rejected. Nonzero reserved MUST be rejected in v0.
sectionId: u8
sectionByteLength: u32 ; payload size, not including these 5 bytes
payload: sectionByteLength bytes
sectionId |
Name | Max count | Required order among non-custom |
|---|---|---|---|
0x00 |
Custom | unbounded | MAY appear anywhere |
0x01 |
Type | 1 | 1 |
0x02 |
HostImport | 1 | 2 |
0x03 |
Function | 1 | 3 |
0x04 |
Memory | 1 | 4 |
0x05 |
Global | 1 | 5 |
0x06 |
Export | 1 | 6 |
0x07 |
Code | 1 | 7 |
Empty sections MAY be omitted. If present, non-custom sections MUST appear in this order. Unknown sectionId MUST be rejected. sectionByteLength past end-of-file MUST be rejected.
Custom (0x00): name (length-prefixed UTF-8) then opaque bytes. Interpreters MUST ignore unknown custom sections.
Type (0x01):
functionTypeCount: u32
for each:
parameterCount: u32
parameterTypes: ValueType × parameterCount
resultCount: u32
resultTypes: ValueType × resultCount
HostImport (0x02):
importCount: u32
for each:
name: length-prefixed UTF-8
pluginId: u32
methodId: u32
typeIndex: u32
Function (0x03):
functionCount: u32
for each: typeIndex: u32
Memory (0x04):
memoryCount: u32 ; MUST be 0 or 1
for each: pageCount: u32
pageCount == 0 is allowed (empty memory). v0 has no separate maximum field; maximum = pageCount.
Global (0x05):
globalCount: u32
for each:
valueType: ValueType
mutability: u8 ; 0 Immutable, 1 Mutable (field name: Mutability)
globalInitKind: u8 ; 0x01 ConstantExpression, 0x02 HostInjected
if ConstantExpression: instructions ending with End
if HostInjected: no bytes; host MUST supply the Value at instantiate
Export (0x06):
exportCount: u32
for each:
name: length-prefixed UTF-8
kind: u8 ; 0x01 Function, 0x02 Memory, 0x03 Global
index: u32
Code (0x07):
functionBodyCount: u32 ; MUST equal Function.functionCount
for each:
bodyByteLength: u32 ; bytes after this field
localGroupCount: u32
for each group:
count: u32
valueType: ValueType
instructions: bytes ending with End (0x64)
bodyByteLength MUST equal the local-groups encoding plus instruction bytes.
Text:
(module
(func (export "main") (result i32)
(i32.add (i32.const 1) (i32.const 2))))
Hex (90 bytes):
54 49 52 00 01 00 00 00
01 0D 00 00 00 01 00 00 00 00 00 00 00 01 00 00 00 01
03 08 00 00 00 01 00 00 00 00 00 00 00
06 11 00 00 00 01 00 00 00 04 00 00 00 6D 61 69 6E 01 00 00 00 00
07 18 00 00 00 01 00 00 00 10 00 00 00 00 00 00 00
10 01 00 00 00 10 02 00 00 00 20 64
Text: Echo module above (no Memory field).
Type section payload is 23 bytes: functionTypeCount = 2 (02 00 00 00); type 0 is (i32) -> (i32); type 1 is () -> (i32). Payload length = 4 + (4+1+4+1) + (4+0+4+1) = 23 (0x17).
54 49 52 00 01 00 00 00
01 17 00 00 00
02 00 00 00
01 00 00 00 01 01 00 00 00 01 ; type 0: param i32, result i32
00 00 00 00 01 00 00 00 01 ; type 1: no params, result i32
; HostImport id=02 length=24
02 18 00 00 00
01 00 00 00
04 00 00 00 45 63 68 6F ; "Echo"
00 00 00 00 ; pluginId 0
00 00 00 00 ; methodId 0
00 00 00 00 ; typeIndex 0
; Function id=03 length=8
03 08 00 00 00
01 00 00 00 01 00 00 00 ; one function, typeIndex 1
; Export id=06 length=17
06 11 00 00 00
01 00 00 00 04 00 00 00 6D 61 69 6E 01 00 00 00 00
; Code id=07 length=23
07 17 00 00 00
01 00 00 00 ; one body
0F 00 00 00 ; bodyByteLength 15
00 00 00 00 ; localGroupCount 0
10 29 00 00 00 ; i32.const 41
70 00 00 00 00 ; host.invoke hostImportIndex 0
64 ; End
RFC 0001 owns the on-disk AEAD/checksum container (RFC 0001 §11, v0 snapshot container). TIRS is the payload inside that container, not the product file. This section defines what MUST round-trip at the IR layer so tests can assert exact core-state equality.
moduleBytes(raw.tirb) andentryExportName(or equivalentfunctionIndexof the entry plus the name).activeContinuationIdentifier(absent if no live continuation).- Plugin identity map: for each registered plugin,
{ pluginId, identityHash, name, version }(RFC 0001 §6, §11).identityHashis 32 opaque bytes at the IR layer; assignment is RFC 0001 §6. Unknown TIRSformatVersionMUST fail closed. - Every live
Continuation:continuationIdentifier,valueStack,callFrames. - Every
CallFrame:functionIndex,instructionIndex,locals,controlStack,returnProgramCounter. - Every
ControlLabelfield listed in the abstract machine. - Linear memory bytes (full dump in v0; dirty-page later).
globals[].remainingFuel.quotas[]: zero or more{dimension, remaining}slots, unique dimension codes, strictly increasingdimensionin the canonical encoding (RFC 0001 §12).capabilityTable(tableIndex,generation,live; host-side identity opaque to IR but MUST be carried as opaque bytes so the host can rebind).- Every
pendingHostCallsentry (HostCallfields). At most one percontinuationIdentifier. Each identifier MUST name a live continuation. Labelon everyValue.
Opaque plugin blobs and scheduler queues. Quota slots are in TIRS (above). The identity map is in TIRS. RestoreCore MUST reject if the host-supplied implementations’ content hashes or local pluginIds disagree with the snapshot map.
RestoreCore MUST reject (fail-fast, not load-and-later-trap) if any restored Capability Value (stack, locals, globals, HostCall.arguments) names a (tableIndex, generation) that is missing, not live, or disagrees with the restored table. Corrupting a generation on purpose is a rejected restore, not program-7 “use traps.”
Not the production snapshot file. Magic 54 49 52 53 (“TIRS”). TIRS formatVersion is the snapshot encoding version and is distinct from the module .tirb formatVersion. Little-endian. Purpose: bitwise comparison in conformance program 3.
magic: 54 49 52 53
formatVersion: u16 = 1
reserved: u16 = 0
moduleByteLength: u32
moduleBytes: that many bytes
entryName: length-prefixed UTF-8
pluginIdentityCount: u32
for each:
pluginId: u32
identityHash: 32 bytes
name: length-prefixed UTF-8
version: length-prefixed UTF-8
remainingFuel: u64
quotaCount: u32
for each:
dimension: u8
remaining: u64
memoryByteLength: u32
memoryBytes: that many bytes
globalCount: u32
globals: Value*
capabilityEntryCount: u32
for each entry:
tableIndex: u32
generation: u32
live: u8 (0/1)
opaqueLength: u32
opaqueBytes: that many bytes
activeContinuationPresent: u8 ; 0 or 1
if 1: activeContinuationIdentifier: u32
continuationCount: u32
for each continuation:
continuationIdentifier: u32
valueStackCount: u32
valueStack: Value*
callFrameCount: u32
for each frame:
functionIndex, instructionIndex: u32
localCount + locals: Value*
controlLabelCount + labels (each ControlLabel encoding below)
returnProgramCounterPresent: u8
if 1: functionIndex, instructionIndex
pendingHostCallCount: u32
for each pending HostCall:
pluginId, methodId: u32
argumentCount + arguments: Value*
capabilityCount: u32
capabilities: tableIndex+generation pairs
continuationIdentifier: u32
If activeContinuationPresent = 1, activeContinuationIdentifier MUST equal one continuationIdentifier in the list. Identifiers in continuations[] MUST be unique. Each pendingHostCalls continuationIdentifier MUST name a live continuation and MUST be unique among pending entries. continuationCount MUST NOT exceed the live-continuation cap.
Value encoding:
valueType: u8
sensitivityLabel: u8 ; low 2 bits; high bits MUST be 0
payload:
i32: 4 bytes
i64: 8 bytes
Unit: empty
Capability: tableIndex u32, generation u32
ControlLabel encoding:
labelKind: u8 ; 1 block (including implicit function label = controlStack[0]), 2 loop, 3 if. No distinct Function kind in snapshots.
parameterCount: u32
resultCount: u32
stackHeight: u32
branchInstructionIndex: u32
Round-trip MUST be deep-equal on all abstract fields. Implementations MAY use a different in-process representation if they convert losslessly.
Canonical encoding of the Echo worked example after host.invoke suspends, with initialFuel = 1000 so remainingFuel = 998. identityHash is SHA-256 of the RFC 0001 v0 canonical identity bytes for package Echo version 1.0.0 schema (schema Echo v1) (no implementation digest). Module bytes are the Echo .tirb in Worked binary: Echo host import (no memory).
54 49 52 53 01 00 00 00
80 00 00 00
; --- 128-byte Echo .tirb ---
54 49 52 00 01 00 00 00
01 17 00 00 00 02 00 00 00 01 00 00 00 01 01 00 00 00 01 00 00 00 00 01 00 00 00 01
02 18 00 00 00 01 00 00 00 04 00 00 00 45 63 68 6F 00 00 00 00 00 00 00 00 00 00 00 00
03 08 00 00 00 01 00 00 00 01 00 00 00
06 11 00 00 00 01 00 00 00 04 00 00 00 6D 61 69 6E 01 00 00 00 00
07 17 00 00 00 01 00 00 00 0F 00 00 00 00 00 00 00 10 29 00 00 00 70 00 00 00 00 64
; --- end moduleBytes ---
04 00 00 00 6D 61 69 6E ; entryName "main"
01 00 00 00 ; pluginIdentityCount 1
00 00 00 00 ; pluginId 0
50 84 4C 2B CD 5A 84 FE 5B BB 6F 36 45 2B 62 CA
33 D7 78 1B 7A 65 71 9F 06 1C 0A 60 B6 72 E0 49 ; identityHash (SHA-256)
04 00 00 00 45 63 68 6F ; name "Echo"
05 00 00 00 31 2E 30 2E 30 ; version "1.0.0"
E6 03 00 00 00 00 00 00 ; remainingFuel 998
00 00 00 00 ; quotaCount 0
00 00 00 00 ; memoryByteLength 0
00 00 00 00 ; globalCount 0
00 00 00 00 ; capabilityEntryCount 0
01 ; activeContinuationPresent
00 00 00 00 ; activeContinuationIdentifier 0
01 00 00 00 ; continuationCount 1
00 00 00 00 ; continuationIdentifier 0
00 00 00 00 ; valueStackCount 0
01 00 00 00 ; callFrameCount 1
00 00 00 00 ; functionIndex 0
02 00 00 00 ; instructionIndex 2
00 00 00 00 ; localCount 0
01 00 00 00 ; controlLabelCount 1
01 ; labelKind block
00 00 00 00 ; parameterCount 0
01 00 00 00 ; resultCount 1
00 00 00 00 ; stackHeight 0
02 00 00 00 ; branchInstructionIndex 2 (End)
00 ; returnProgramCounterPresent 0
01 00 00 00 ; pendingHostCallCount 1
00 00 00 00 ; pluginId 0
00 00 00 00 ; methodId 0
01 00 00 00 ; argumentCount 1
01 00 29 00 00 00 ; i32 Public 41
00 00 00 00 ; capabilityCount 0
00 00 00 00 ; continuationIdentifier 0
Program 3 uses the program 2 module (extra add), so its moduleBytes differ from this blob. Compare program 3 with TIRS of that instance, using this layout.
Canonical host operations (Rust API names; opcodes in the guest remain WASM text):
Instantiate(
module: Module,
pluginMap: host-owned map, // keys MUST be the module’s pluginId values
hostInjectedGlobals: Value[],
quotas: QuotaSlot[] // unique dimensions; MemoryBytes checked against allocated memory
) -> Instance | Reject
Invoke(
instance: Instance,
exportName: UTF-8, // default: "main"
arguments: Value[], // MAY include Capability from the host
initialFuel: u64
) -> Completed | Suspended | Trapped // MUST run Continue after building the entry frame
Continue(instance) -> Completed | Suspended | Trapped | HostInterfaceError
// Run the interpreter loop after entry checks. MUST be used after AddFuel.
// Selects the ready continuation with the lowest continuationIdentifier.
// If none is ready and pendingHostCalls is non-empty: MUST return HostCallPending
// (not a guest trap). If never invoked: MUST return InstanceIdle.
// If already Trapped: replay that outcome. If already Completed and no ready
// fiber remains: replay Completed.
Resume(instance, continuationIdentifier, results: Value[]) -> Completed | Suspended | Trapped
// ONLY for a pendingHostCalls entry with that identifier.
// MUST NOT be used in place of Continue after OutOfFuel or QuotaExhausted.
TrapPending(instance, continuationIdentifier, trapKind) -> Trapped
SpawnContinuation(instance, exportName, arguments: Value[])
-> (continuationIdentifier, Completed | Suspended | Trapped) | Reject
// Host-driven extra fiber. MUST NOT take initialFuel. MUST NOT assign or reset
// remainingFuel. MUST NOT add a guest opcode. Then Continue (lowest-ready-id).
CancelContinuation(instance, continuationIdentifier) -> () | Reject
// Remove that continuation and its pendingHostCalls entry if any. MUST NOT
// trap the instance. MUST NOT decrement remainingFuel. MUST NOT remove siblings.
// Missing identifier is a host API error.
AddFuel(instance, amount: u64) -> () // saturating; MUST NOT run the guest
AddQuota(instance, dimension, amount: u64) -> () // saturating; inserts the slot if absent; MUST NOT run the guest
ConsumeQuota(instance, dimension, amount: u64) -> ok | insufficient // no-op success if dimension absent
SnapshotCore(instance) -> CoreSnapshot
RestoreCore(snapshot: CoreSnapshot, hostRebind) -> Instance | Reject
- Decode
.tir/.tirb. Reject on format errors, size limits, invalid UTF-8, U+0000 in names. - Validate (Validation). Reject if invalid.
- Check
pluginMap: everyHostImportpair is present with matching arity/types; map keys equal the encodedpluginIds (no re-numbering). - Allocate
linearMemoryofpageCount × 65536bytes, all zeros. IfmemoryCount = 0, length 0. - Evaluate globals in order:
ConstantExpressionintoglobals[i];HostInjectedfromhostInjectedGlobalsin HostInjected order.ValueTypeMUST match. InjectedCapabilityvalues MUST be installed incapabilityTableas live entries. continuations = [],activeContinuationIdentifierabsent,pendingHostCalls = [],remainingFuel = 0.- Install
quotas. Duplicate or reserved/unknowndimensionMUST reject. Sort slots bydimensionascending. IfMemoryBytesis present,remainingMUST be ≥ allocatedlinearMemorylength; then subtract that length fromremaining. - Return the instance. MUST NOT execute guest instructions.
- Reject if the last guest outcome is
Trapped. - If
continuationsis non-empty orpendingHostCallsis non-empty, reject. Additional fibers useSpawnContinuation. - Resolve
exportNameto a function. Fail if missing or not a function. - Argument count and each
ValueTypeMUST match the function type; else reject (host error, notTrapKindof a running guest). - Install any
Capabilityarguments as live table entries if not already present (same handle is reused). - Set
remainingFuel = initialFuel. - Create
Continuationidentifier0; setactiveContinuationIdentifier = 0. EnterFramefor that function: locals = arguments then zero/null extras; implicit functionControlLabelas specified under Control;returnProgramCounterabsent.- Call
Continueand return its outcome.
- Reject if the last guest outcome is
Trapped. - Resolve
exportNameto a function. Fail if missing or not a function. - Argument count and each
ValueTypeMUST match the function type; else reject. - Install any
Capabilityarguments as live table entries if not already present. - If
continuations.lengthwould exceed the live-continuation cap, reject. - Allocate the lowest
u32identifier not currently incontinuations[]. Identifier0is the firstInvokefiber and is not otherwise reserved. - MUST NOT assign or reset
remainingFuel. MUST NOT modify other continuations orpendingHostCalls. - Append a
Continuationwith that identifier, empty value stack, and empty call frames. SetactiveContinuationIdentifierto it. EnterFramefor the export as inInvoke.- Call
Continueand return(continuationIdentifier, outcome).Continueuses the lowest-ready-id rule and MAY run a different ready fiber than the one just spawned.
- Reject if the last guest outcome is
Trapped. - If no live continuation has
continuationIdentifier, reject. - Remove that continuation from
continuations[]. Remove anypendingHostCallsentry with that identifier. MUST NOT remove other fibers or pending calls. - If
activeContinuationIdentifiernamed that fiber, clear it. - MUST NOT assign or reset
remainingFuel. MUST NOT mark the instanceTrapped.
Hierarchical cancel (goal trees) is host policy: the host MAY invoke CancelContinuation for each descendant. The core does not walk a goal tree.
MUST apply the entry checks then the step loop in interpreter loop. MUST NOT decrement fuel except as specified per executed instruction. After OutOfFuel, the host MUST AddFuel then Continue. After QuotaExhausted, the host MUST AddQuota then Continue. After host.invoke, the host MUST Resume or TrapPending by continuationIdentifier. Continue when no continuation is ready and pendingHostCalls is non-empty MUST return HostCallPending immediately (MUST NOT livelock in no-op steps). Continue when another fiber is ready MUST run that fiber even if other fibers have pending host calls.
HostInterfaceError names: HostCallPending, InstanceIdle, plus Resume/TrapPending used without a matching pendingHostCalls entry. These are not TrapKind values.
Resume pushes results onto the named continuation then MUST call Continue. Resume / TrapPending / AddFuel / AddQuota / ConsumeQuota / SpawnContinuation / CancelContinuation MUST NOT decrement remainingFuel.
Implementations MUST pass these programs. Traces use Value notation i32(n, Public) and ProgramCounter(functionIndex, instructionIndex). Initial remainingFuel is 1000 unless stated. Entry is export "main".
Purpose: interpreter loop, stack, return.
(module
(func (export "main") (result i32)
(i32.add (i32.const 1) (i32.const 2))))
Decoded: instructionIndex 0 push 1, 1 push 2, 2 i32.add, 3 end.
| After step | remainingFuel | ProgramCounter | valueStack |
|---|---|---|---|
| start | 1000 | (0, 0) | [] |
| push 1 | 999 | (0, 1) | [i32(1, Public)] |
| push 2 | 998 | (0, 2) | [i32(1, Public), i32(2, Public)] |
| add | 997 | (0, 3) | [i32(3, Public)] |
end / return |
996 | completed | results [i32(3, Public)] |
Expected: Completed { results: [i32(3, Public)] }. No pendingHostCalls entry. No trap.
Purpose: suspend captures Continuation; resume pushes result.
(module
(host.import Echo
(pluginId 0)
(methodId 0)
(param i32)
(result i32))
(func (export "main") (result i32)
(i32.add
(host.invoke Echo (i32.const 41))
(i32.const 1))))
Decoded:
| instructionIndex | Instruction |
|---|---|
| 0 | i32.const 41 |
| 1 | host.invoke 0 |
| 2 | i32.const 1 |
| 3 | i32.add |
| 4 | end |
Host Echo MUST return i32(41, Public) (identity).
Trace:
- Start fuel 1000, PC (0,0), stack
[]. - Push 41 → fuel 999, PC (0,1), stack
[i32(41, Public)]. host.invoke→ fuel 998, stack[], PC (0,2),Suspended host.invoke.HostCallas in the Echo worked example (arguments41).Resume(0, i32(41, Public))(fuel stays 998) thenContinue→ stack[i32(41, Public)], PC still (0,2).- Push 1 → fuel 997, stack
[i32(41, Public), i32(1, Public)]. - Add → fuel 996, stack
[i32(42, Public)]. - End → fuel 995,
Completed { [i32(42, Public)] }.
Purpose: exact core-state round-trip.
Procedure:
- Run program 2 until first suspend.
SnapshotCore.- Discard the instance.
RestoreCoreon a freshMachineState(new process allowed).- Assert TIRS / abstract fields equal (layout in IR-Level Snapshot Surface; not the simple-Echo hex, because this module has the extra add):
moduleBytesidentical- identity map: Echo
pluginId 0plus the testidentityHash remainingFuel == 998activeContinuationIdentifier == 0instructionIndex == 2,functionIndex == 0valueStackempty- implicit function
ControlLabelpresent (labelKind = block,resultCount = 1,stackHeight = 0) pendingHostCallsequal to program 2 (one entry,continuationIdentifier0)memoryByteLength == 0capabilityTableempty- one
Continuationidentifier 0 - every
Valuelabel preserved
Resume(0, i32(41, Public))(still fuel 998) and finish as program 2 steps 5–8.
Expected: same Completed { [i32(42, Public)] }. Fuel after complete: 995.
Purpose: bounds-checked linear memory, little-endian, unaligned allowed.
4a — aligned immediateOffset 0:
(module
(memory (pages 1))
(func (export "main") (result i32)
(i32.store (i32.const 0) (i32.const 0x01020304))
(i32.load (i32.const 0))))
Unfolded (immediateOffset defaults to 0):
i32.const 0
i32.const 0x01020304
i32.store
i32.const 0
i32.load
End
Memory at bytes [0..4) after store: 04 03 02 01. Load yields i32(0x01020304, Public) even if the stored value had a non-public label (stores strip to bytes; loads are Public).
Expected: Completed { [i32(0x01020304, Public)] }. Memory page remains 65536 bytes.
4b — unaligned store/load at address 1:
(module
(memory (pages 1))
(func (export "main") (result i32)
(i32.store (i32.const 1) (i32.const 0x01020304))
(i32.load (i32.const 1))))
Expected: Completed { [i32(0x01020304, Public)] }. Bytes [1..5) are 04 03 02 01. Byte 0 remains 00. MUST NOT trap (1 + 4 <= 65536).
(module
(memory (pages 1))
(func (export "main") (result i32)
(i32.load (i32.const 65535))))
immediateOffset 0. effectiveAddress = 65535, accessSize = 4, 65535+4 > 65536.
Expected: Trapped { trapKind: OutOfBoundsMemory, programCounter: (0, 1) } after charging fuel for the load (push then load: fuel 998 if started at 1000). instructionIndex remains 1 (the load); traps MUST NOT advance it. MUST NOT append a pendingHostCalls entry. MUST NOT use SuspendReason.
Address 65536 likewise traps. Address 0 with immediateOffset 65533 and i32.load traps (65533+4 > 65536).
Program 1 body, initial fuel 0:
Expected: Suspended { OutOfFuel } before executing i32.const. instructionIndex == 0. remainingFuel == 0. Stack empty. No trap.
Then AddFuel(instance, 4) (MUST NOT run the guest) followed by Continue (MUST NOT use Resume):
- With exactly 4 fuel:
Continuecompletesi32(3, Public),remainingFuel == 0afterend. - If the host adds 1,
Continueruns one instruction, thenOutOfFuelagain at the next boundary.
Continue after OutOfFuel MUST re-check fuel before the same instruction.
Fuel 1 at start of program 1 (Invoke then the run loop): executes push 1, then suspends at PC (0,1) with fuel 0.
Positive:
(module
(host.import UseCapability
(pluginId 0)
(methodId 0)
(param Capability)
(result i32))
(func (export "main") (param Capability) (result i32)
(host.invoke UseCapability (local.get 0))))
Host Invoke("main", [Capability { tableIndex: 1, generation: 1, label: Confidential }]) with a live table entry whose opaque identity is test-only "echo-cap".
Expected suspend:
arguments[0].valueType == Capabilityarguments[0].payload == { tableIndex: 1, generation: 1 }arguments[0].sensitivityLabel == Confidentialcapabilities == [{ tableIndex: 1, generation: 1 }]
Host resume i32(7, Public) → Completed { [i32(7, Public)] }.
Negative (MUST fail validation or trap, never produce a live capability):
(module
(host.import UseCapability
(pluginId 0)
(methodId 0)
(param Capability)
(result i32))
(func (export "main") (result i32)
(host.invoke UseCapability (i32.const 1))))
This module MUST be rejected at validation (i32 vs Capability). Implementations MUST NOT coerce 1 into { tableIndex: 1, generation: 0 } or any handle.
A second negative (runtime): valid module that local.get of an uninitialized Capability local (null) and invokes MUST trap InvalidCapability at the invoke, not suspend. instructionIndex remains on host.invoke; the null handle has been popped; pendingHostCalls is empty.
A third negative: take a valid snapshot that contains handle {tableIndex: 1, generation: 1} and a matching live table entry. RestoreCore with a table whose slot is {1, 2} (or not live) MUST reject the restore (capability mismatch, fail-fast). It MUST NOT return an instance that later traps on use. A correct restore keeps generation 1 and live.
Purpose: two live fibers, two in-flight HostCalls, shared remainingFuel, snapshot/restore, resume by continuationIdentifier.
(module
(host.import Echo
(pluginId 0)
(methodId 0)
(param i32)
(result i32))
(func (export "a") (result i32)
(host.invoke Echo (i32.const 1)))
(func (export "b") (result i32)
(host.invoke Echo (i32.const 2))))
Procedure:
Invoke("a", [], initialFuel = 1000)untilSuspended { host.invoke }. Continuation0pending.remainingFuel == 998.SpawnContinuation("b", []). MUST NOT resetremainingFuel. Continuation1runs under the lowest-ready-id rule (0 is not ready) until it suspends.remainingFuel == 996.continuationCount == 2. TwopendingHostCallsentries, identifiers0and1.SnapshotCore/RestoreCore. Both continuations and both pending calls MUST round-trip. Fuel MUST still be 996.Resume(0, i32(41, Public))thenResume(1, i32(42, Public))(or reverse). Each completed fiber is removed; the other remains until resumed.SpawnContinuation/ snapshot / restore MUST NOT decrement fuel. After both complete,remainingFuel == 994.
Invoke while either fiber is live MUST reject. Continue while both calls are pending MUST return HostCallPending. Continue after one fiber completes and the other is still pending MUST return HostCallPending.
Rejected. RFC 0001: the core’s native format is Tollana IR; external WASM runtimes are not part of the abstract machine. WASM does not have first-class Capability + Label on every value, host.invoke suspend, or fuel-as-suspend. Snapshots of a foreign engine are not exact MachineState. Compiling Tollana to wasm32 remains a deployment target, not an execution dependency.
Rejected. Runtime values are tagged. Type mismatches MUST trap. Validation is still required so most mismatches never run, but tags are the snapshot and capability story: a handle cannot be an integer in disguise.
Rejected. Structured block / loop / if / br / br_if keep validation decidable and snapshots’ controlStack small. Unstructured jumps complicate reducibility and host-tooling.
Rejected for v0. Fixed-width little-endian u32 / i32 / i64 are trivial to dump in tests and to snapshot. Size cost is acceptable for v0 modules. LEB128 MAY be reconsidered later; it is not v0.
Rejected. Opcode names follow WASM text (i32.add, local.get, host.invoke). Machine types stay full words (Instruction, not Instr). The binary encoding is still Tollana IR, not WASM.
Rejected. Version 0 does not need them. Adding Float32 later is a new ValueType code and new opcodes; reserved.
Rejected for v0. Stack machine maps directly to structured expression text, WASM-like validation, and a short interpreter loop. Registers MAY be a JIT artifact later; the interpreter remains source of truth.
Rejected for the binary immediate. Typed HostImport is the validation surface. The instruction indexes that table. Runtime still resolves pluginId / methodId from the import against the instance map.
Not in v0; listed so encodings do not paint the ISA into a corner:
| Extension | Sketch |
|---|---|
| Indirect call + tables | CallIndirect, function tables; new section |
Float32 / Float64 |
new ValueType codes 0x05/0x06, new opcodes in 0x80+ |
MemoryGrow |
opcode, max pages distinct from min |
| Memory-region labels | loads join region label; not Public-always |
| SIMD / v128 | deferred |
| Multiple memories | extra immediate memoryIndex |
| Capability attenuation opcodes | host still owns policy; IR might copy-with-subset |
| Tail call | new opcode; reuse frame |
| LEB128 or compressed sections | new formatVersion |
| Start function | explicit invoke remains v0 |
Opcode bytes 0x03–0x0F, 0x12–0x1F, 0x2C–0x2F, 0x3C–0x3F, 0x45–0x4F, 0x55–0x5F, 0x69–0x6F, 0x71–0xFF are reserved. MUST fail decode in v0.
Threat model: the guest is untrusted (including future LLM-generated bytecode). The host is trusted to implement plugins and policy. Other instances MUST NOT be observable.
| Threat | Mitigation |
|---|---|
| Forge a capability from integers | No opcode produces Capability; validation + tagged Value; stale generation traps |
| OOB read/write, sandbox escape | Every load/store bounds-checked; no syscalls; one linear memory per instance |
| Infinite loop / CPU DoS | Fuel pre-check; OutOfFuel suspend |
| Stack bombs | Finite value/call/control caps |
| Plugin confusion after migrate | Host restores content hashes; IR carries local pluginId only |
| Secret leakage via labels | Labels MUST snapshot and journal; IR does not send data off-machine; host policy on host.invoke arguments |
| Snapshot theft | Architecture AEAD container — not this spec; IR canonical encoding is for tests and is plaintext |
| Null handle confusion | {0,0} reserved; live generation >= 1 |
Guests have no ambient authority. host.invoke is the only side-effect channel. The core MUST NOT interpret capability meaning beyond table lookup (RFC 0001 §14).
Sensitivity: v0 carries labels and joins them on integer binops. It does not implement a full IFC type system. Hosts SHOULD refuse outbound plugin calls that violate policy (e.g. Secret to an external model).
Linear memory is not a label boundary. Stores write raw bits; loads MUST produce Public values. A guest MAY local.set a Secret integer and i32.store it; the subsequent load is Public. Hosts that need label integrity MUST NOT place labeled secrets only in linear memory (use Values, capabilities, or a later memory-region-label extension).
The journal is architecture-owned. The interpreter SHOULD emit these IR-level events (names canonical) for the host to record:
| Event | Fields (minimum) |
|---|---|
InstructionStepped |
functionIndex, instructionIndex, opcode name (optional; noisy) |
FuelSuspended |
remainingFuel (0), ProgramCounter |
FuelResumed |
remainingFuel after AddFuel (guest runs only on the following Continue) |
QuotaConsumed |
dimension, amount, remaining after |
QuotaExhausted |
dimension, ProgramCounter |
QuotaAdded |
dimension, remaining after |
HostCallSuspended |
pluginId, methodId, continuationIdentifier, arity, argument Values (redact payloads per label policy) |
HostCallResumed |
pluginId, methodId, continuationIdentifier, result Values (payload redaction host-defined) |
Trapped |
TrapKind, ProgramCounter |
Completed |
result types and labels |
SnapshotCoreTaken |
module length, fuel, memory length, continuation count |
SnapshotCoreRestored |
same |
InvalidCapabilityUse |
tableIndex, generation (no host secrets) |
Confidential / Secret payloads MUST NOT be written to unrestricted logs. Hosts MUST apply redaction (RFC 0001 §13).
The v0 event envelope (sequence, run_id, sensitivity, event_type, body), default-off InstructionStepped, container journalCursor as an advisory next-sequence, architecture-level SnapshotTaken / SnapshotRestored, and HostCallFailed (plugin invoke error; the interpreter MUST NOT emit it) are specified in RFC 0001 §13. This RFC names the IR-level events only.
This document has no IANA actions.
- RFC 2119 — Key words for use in RFCs to Indicate Requirement Levels
- RFC 3629 — UTF-8
- RFC 0001 — Tollana Architecture
- WebAssembly Core Specification — prior art for structured control, stack typing, and text opcode names. The binary format and abstract machine are Tollana IR, not WASM.
End of RFC 0002. ISA changes require a formatVersion bump or a new RFC.