Vera programs compile to WebAssembly (WASM) modules and execute in a host runtime. The reference implementation uses wasmtime as the WASM engine. The runtime is responsible for:
- Instantiating compiled WASM modules
- Providing host function implementations for effects (
IO,State<T>) - Managing linear memory (for string constants, heap-allocated ADTs, and arrays)
- Capturing output and state for the caller
- Handling traps and runtime errors
The runtime is deliberately minimal. It provides only what is needed to execute the compiled WASM — there is no scheduler. IO operations cover standard output (print), standard input (read_line), file access (read_file, write_file), command-line arguments (args), environment variables (get_env), and process exit (exit). Memory is managed automatically by a conservative mark-sweep garbage collector compiled into each WASM module (see Section 12.5.4). Future runtime features (networking, async, inference) will extend this model without changing its fundamentals.
A compiled Vera module is a standalone WASM module containing:
Every compilable top-level function is exported by name. The entry point for vera run is resolved as follows:
- If
--fn <name>is provided, call that function. - Otherwise, if a function named
mainexists, callmain. - Otherwise, call the first exported function.
Functions whose parameter or return types have no WASM representation (e.g., Array<T> return values, higher-kinded types) are skipped during compilation with a warning — they do not appear in the module's exports. Functions with String parameters are supported: the compiler emits a bump allocator and the host CLI allocates string arguments in linear memory before calling the function.
The module imports host functions for effects that the program uses:
| Import | Signature | Condition |
|---|---|---|
vera.print |
(i32, i32) -> () |
Program uses IO.print |
vera.read_line |
() -> (i32, i32) |
Program uses IO.read_line |
vera.read_file |
(i32, i32) -> (i32) |
Program uses IO.read_file |
vera.write_file |
(i32, i32, i32, i32) -> (i32) |
Program uses IO.write_file |
vera.args |
() -> (i32, i32) |
Program uses IO.args |
vera.exit |
(i64) -> () |
Program uses IO.exit |
vera.get_env |
(i32, i32) -> (i32) |
Program uses IO.get_env |
vera.state_get_{T} |
() -> {wasm_t} |
Program uses State<T>.get |
vera.state_put_{T} |
({wasm_t}) -> () |
Program uses State<T>.put |
vera.contract_fail |
(i32, i32) -> () |
Program has runtime contracts |
vera.md_parse |
(i32, i32) -> (i32) |
Program uses md_parse |
vera.md_render |
(i32) -> (i32, i32) |
Program uses md_render |
vera.md_has_heading |
(i32, i64) -> (i32) |
Program uses md_has_heading |
vera.md_has_code_block |
(i32, i32, i32) -> (i32) |
Program uses md_has_code_block |
vera.md_extract_code_blocks |
(i32, i32, i32) -> (i32, i32) |
Program uses md_extract_code_blocks |
Imports are only emitted when the program actually uses the corresponding effect operations. A pure program produces a module with no imports.
The module exports one page (64 KiB) of linear memory as "memory". The host runtime uses this export to read string data for IO.print and to write data returned by host functions (e.g., IO.read_line, IO.read_file).
When the program uses IO operations that return strings or ADTs (any operation other than print and exit), the module also exports the $alloc function so the host can allocate memory in the WASM linear memory for return values.
For the memory layout, see Section 12.5.
String constants are stored in the WASM data section at the start of linear memory (offset 0). The string pool deduplicates identical strings. Each string is stored as raw UTF-8 bytes with no null terminator.
For the string pool implementation, see Chapter 11, Section 11.5.
The reference runtime uses wasmtime's Python bindings. The execution pipeline is:
Engine → Module(engine, wat) → Linker(engine) → Store(engine)
→ linker.define_func(...) [register host functions]
→ linker.instantiate(store, module)
→ instance.exports(store).get(fn_name)
→ func(store, *args)
Each execution creates a fresh engine, module, linker, and store. There is no persistent state between invocations.
The linker resolves all imports before instantiation. If the module imports a host function that the linker has not defined, instantiation fails with an error.
The linker registers host functions before instantiation:
- IO host functions — registered for each IO operation the module imports (
vera.print,vera.read_line,vera.read_file,vera.write_file,vera.args,vera.exit,vera.get_env). vera.state_get_{T}/vera.state_put_{T}— registered for each concreteState<T>type used by the program.
After instantiation, the runtime resolves the function to call:
- If
--fn <name>is specified, look upnamein the module's exports. - Otherwise, look up
main. - Otherwise, use the first export.
- If no exports exist, raise an error.
Arguments are passed as WASM values. The CLI parses string arguments to integers or floats based on the function's WASM parameter types.
The IO effect provides seven host function bindings. Each is imported only when the program uses the corresponding IO.* qualified call.
Import: (import "vera" "print" (func $vera.print (param i32 i32)))
Parameters:
ptr(i32): byte offset into linear memory where the string data begins.len(i32): length of the string in bytes.
Behaviour:
- Read
lenbytes from linear memory starting at offsetptr. - Decode the bytes as UTF-8.
- Write the decoded string to standard output.
The output is captured in a buffer so the caller can inspect it programmatically (e.g., in tests). The ExecuteResult returned by execute() includes a stdout field containing all captured output.
Import: (import "vera" "read_line" (func $vera.read_line (result i32 i32)))
Returns: (ptr, len) — a String pair (byte offset and length).
Behaviour:
- Read one line from standard input (up to and including the newline character).
- Strip the trailing newline.
- Call the exported
$allocfunction to allocate memory in the WASM module. - Copy the UTF-8 bytes into linear memory.
- Return the
(ptr, len)pair.
The execute() function accepts an optional stdin parameter. If provided, read_line reads from that string (via a StringIO buffer). If not provided, it reads from the process's standard input.
Import: (import "vera" "read_file" (func $vera.read_file (param i32 i32) (result i32)))
Parameters:
path_ptr(i32): byte offset of the file path string.path_len(i32): length of the file path in bytes.
Returns: i32 — a heap pointer to a Result<String, String> ADT value.
Behaviour:
- Decode the file path from linear memory.
- Attempt to read the file contents as UTF-8.
- On success: construct a
Result.OkADT on the WASM heap containing the file contents as a String (tag=0, str_ptr, str_len). Return the heap pointer. - On failure: construct a
Result.ErrADT containing the error message (tag=1, str_ptr, str_len). Return the heap pointer.
The host allocates memory via the exported $alloc function.
Import: (import "vera" "write_file" (func $vera.write_file (param i32 i32 i32 i32) (result i32)))
Parameters:
path_ptr(i32): byte offset of the file path string.path_len(i32): length of the file path in bytes.data_ptr(i32): byte offset of the content string.data_len(i32): length of the content in bytes.
Returns: i32 — a heap pointer to a Result<Unit, String> ADT value.
Behaviour:
- Decode the file path and content from linear memory.
- Attempt to write the content to the file.
- On success: construct a
Result.OkADT with tag=0 (no payload for Unit). Return the heap pointer. - On failure: construct a
Result.ErrADT containing the error message (tag=1, str_ptr, str_len). Return the heap pointer.
Import: (import "vera" "args" (func $vera.args (result i32 i32)))
Returns: (ptr, count) — an Array<String> pair (pointer to element data and element count).
Behaviour:
- Retrieve the command-line arguments (passed via
execute(cli_args=...)or from the CLI--separator). - For each argument string, allocate memory in the WASM module and copy the UTF-8 bytes.
- Allocate backing storage for the array:
count * 8bytes (each element is a(ptr, len)pair of two i32 values). - Return
(backing_ptr, count).
Import: (import "vera" "exit" (func $vera.exit (param i64)))
Parameters:
code(i64): the exit code.
Behaviour:
- Record the exit code.
- Raise an exception to halt WASM execution.
The execute() function catches this exception and returns an ExecuteResult with exit_code set to the provided value. The CLI uses this as the process exit code. The WASM instruction sequence for IO.exit includes unreachable after the call, since the function never returns.
Import: (import "vera" "get_env" (func $vera.get_env (param i32 i32) (result i32)))
Parameters:
name_ptr(i32): byte offset of the environment variable name.name_len(i32): length of the name in bytes.
Returns: i32 — a heap pointer to an Option<String> ADT value.
Behaviour:
- Decode the variable name from linear memory.
- Look up the variable in the environment (from
execute(env_vars=...)oros.environ). - If found: construct an
Option.SomeADT containing the value as a String (tag=1, str_ptr, str_len). Return the heap pointer. - If not found: construct an
Option.NoneADT (tag=0). Return the heap pointer.
Imports: One pair per concrete state type:
(import "vera" "state_get_Int" (func $vera.state_get_Int (result i64)))
(import "vera" "state_put_Int" (func $vera.state_put_Int (param i64)))State cells: The host runtime maintains one mutable cell per concrete State<T> type. Cells are initialized to zero (0 for integers, 0.0 for floats). The execute() function accepts an optional initial_state parameter to override initial values for testing.
Type mapping:
| Vera State Type | WASM Type | Default |
|---|---|---|
State<Int> |
i64 |
0 |
State<Nat> |
i64 |
0 |
State<Bool> |
i32 |
0 |
State<Byte> |
i32 |
0 |
State<Float64> |
f64 |
0.0 |
get: Returns the current value of the state cell.
put: Replaces the value of the state cell with the argument.
Multiple independent state types can coexist — each has its own cell and its own pair of host functions.
Import: (import "vera" "contract_fail" (func $vera.contract_fail (param i32 i32)))
Parameters:
ptr(i32): byte offset into linear memory where the violation message begins.len(i32): length of the violation message in bytes.
Behaviour:
- Read
lenbytes from linear memory starting at offsetptr. - Decode the bytes as UTF-8.
- Store the decoded message for later reporting.
The WASM code always follows a call $vera.contract_fail with unreachable, causing a WASM trap. The host runtime catches the trap and converts it to an informative error using the stored violation message.
The import is only emitted when the program contains runtime contract assertions (Tier 3 contracts that the verifier could not prove statically). Programs where all contracts are verified at compile time do not import contract_fail.
The Markdown standard library provides five host function bindings for parsing and querying Markdown documents. Each is imported only when the program uses the corresponding builtin function. All five functions are pure — they have no side effects and produce deterministic results.
For the MdInline and MdBlock ADT definitions, see Section 9.3.5 and Section 9.3.6. For the function specifications, see Section 9.7.3.
Import: (import "vera" "md_parse" (func $vera.md_parse (param i32 i32) (result i32)))
Parameters:
ptr(i32): byte offset of the Markdown source string.len(i32): length of the source string in bytes.
Returns: i32 — a heap pointer to a Result<MdBlock, String> ADT value.
Behaviour:
- Decode the Markdown source from linear memory.
- Parse it into an
MdDocumentAST. - On success: construct a
Result.OkADT containing theMdDocumenttree on the WASM heap. Return the heap pointer. - On failure: construct a
Result.ErrADT containing the error message. Return the heap pointer.
The host allocates all tree nodes (including nested MdBlock and MdInline values) via the exported $alloc function.
Import: (import "vera" "md_render" (func $vera.md_render (param i32) (result i32 i32)))
Parameters:
block_ptr(i32): heap pointer to anMdBlockADT value.
Returns: (ptr, len) — a String pair containing the rendered Markdown text.
Behaviour:
- Read the
MdBlocktree from WASM linear memory by tag dispatch. - Render it to canonical Markdown text.
- Allocate memory for the result string via
$alloc. - Return the
(ptr, len)pair.
Import: (import "vera" "md_has_heading" (func $vera.md_has_heading (param i32 i64) (result i32)))
Parameters:
block_ptr(i32): heap pointer to anMdBlockADT value.level(i64): the heading level to search for (1–6).
Returns: i32 — 1 if a heading of the given level exists, 0 otherwise.
Import: (import "vera" "md_has_code_block" (func $vera.md_has_code_block (param i32 i32 i32) (result i32)))
Parameters:
block_ptr(i32): heap pointer to anMdBlockADT value.lang_ptr(i32): byte offset of the language string.lang_len(i32): length of the language string in bytes.
Returns: i32 — 1 if a fenced code block with the given language exists, 0 otherwise.
Import: (import "vera" "md_extract_code_blocks" (func $vera.md_extract_code_blocks (param i32 i32 i32) (result i32 i32)))
Parameters:
block_ptr(i32): heap pointer to anMdBlockADT value.lang_ptr(i32): byte offset of the language string.lang_len(i32): length of the language string in bytes.
Returns: (ptr, count) — an Array<String> pair (pointer to element data and element count).
Behaviour:
- Read the
MdBlocktree from WASM linear memory. - Recursively find all fenced code blocks whose language matches the given string.
- Allocate backing storage for the result array via
$alloc. - Return
(backing_ptr, count).
┌──────────────────────────────────┐ offset 0
│ String constants (data section) │
├──────────────────────────────────┤ data_end
│ GC shadow stack (4096 bytes) │
├──────────────────────────────────┤ data_end + 4096
│ GC mark worklist (4096 bytes) │
├──────────────────────────────────┤ data_end + 8192 = $heap_ptr (initial)
│ Heap-allocated data │
│ (ADTs, closures, arrays) │
│ ↓ grows downward │
├──────────────────────────────────┤
│ (unused) │
└──────────────────────────────────┘ 65536+ (64 KiB, growable)
String constants occupy the lowest addresses. The GC shadow stack and mark worklist each occupy 4096 bytes after the string data. The heap grows upward from data_end + 8192. The GC infrastructure (shadow stack, worklist, and heap offset) is only emitted when the program allocates heap data.
The heap uses a bump allocator with a free-list overlay. A mutable WASM global $heap_ptr tracks the next free byte. Every allocation prepends a 4-byte header before the payload:
Header (i32 at ptr - 4):
bit 0: GC mark flag (0=white, 1=black)
bits 1-16: payload size in bytes (max 65535)
bits 17-31: reserved
The internal $alloc(payload_size) function:
- Computes
total = align_up(payload_size + 4, 8)(header + payload, 8-byte aligned). - Searches the free list for a first-fit block with
header.size >= payload_size. If found, unlinks it and returns the payload pointer. - If
heap_ptr + totalexceeds available memory, triggers$gc_collectand retries the free list. - If still insufficient, calls
memory.growto extend linear memory. - Stores the header at
heap_ptr, advancesheap_ptrbytotal, and returnsheap_ptr_old + 4.
The allocator, GC infrastructure, and $heap_ptr global are only emitted when the program actually allocates heap data (ADTs, closures, or arrays). Programs that perform no allocation incur zero GC overhead.
All heap allocations are 8-byte aligned. This ensures correct access for all WASM value types:
| WASM Type | Size | Alignment |
|---|---|---|
i32 |
4 bytes | 4 bytes |
i64 |
8 bytes | 8 bytes |
f64 |
8 bytes | 8 bytes |
8-byte alignment satisfies all requirements.
The runtime implements a conservative mark-sweep garbage collector entirely in WASM (no host-side GC logic). The GC is triggered automatically when the bump allocator runs out of space.
Shadow stack. WASM does not support stack scanning, so the compiler maintains an explicit shadow stack in linear memory. The compiler pushes live heap pointers onto it at function entry (pointer-type parameters), after each call $alloc (newly allocated objects), and manages save/restore at function exit. Four globals track the shadow stack and GC state:
| Global | Type | Purpose |
|---|---|---|
$gc_sp |
mut i32 |
Shadow stack pointer (current top) |
$gc_stack_base |
i32 |
Shadow stack base address (data_end) |
$gc_heap_start |
i32 |
Heap start address (data_end + 8192) |
$gc_free_head |
mut i32 |
Free list head pointer |
Collection phases. The $gc_collect function performs three phases:
- Clear marks: Walk the heap linearly from
$gc_heap_startto$heap_ptr, clearing the mark bit in each object header. - Mark: Seed a worklist from shadow stack entries that point into the heap. Drain the worklist iteratively: for each object, set its mark bit, then conservatively scan every i32-aligned word in the payload. Any word that looks like a valid heap pointer (correct range and alignment) is pushed onto the worklist.
- Sweep: Walk the heap again, linking unmarked objects into the free list for reuse by
$alloc.
Conservative scanning. The collector treats any i32 word whose value falls within the heap range and has correct payload alignment as a potential pointer. This eliminates the need for type descriptors or GC maps. False positives merely retain dead objects (harmless for mark-sweep).
Memory growth. If collection does not free enough space, $alloc calls memory.grow to extend linear memory beyond the initial 64 KiB page. If memory growth fails, the program traps.
The full pipeline from source to result:
Source (.vera)
→ parse_file() Lark parse tree
→ transform() Typed AST
→ typecheck() Type diagnostics
→ compile() CompileResult (WAT + WASM bytes)
→ execute() ExecuteResult (value + stdout + state)
The compile() step produces WAT text and assembles it to WASM bytes via wasmtime.wat2wasm(). The execute() step instantiates the WASM module and calls the specified function.
Arguments are passed to the WASM function as typed values:
Int/Natarguments →i64valuesBool/Bytearguments →i32valuesFloat64arguments →f64values
The CLI (vera run file.vera --fn f -- 42 3.14) parses string arguments to the appropriate types using the function's WASM signature. Integer arguments become i64; decimal arguments become f64; true/false (case-insensitive) become i32 for Bool parameters; String arguments are allocated in WASM linear memory and passed as (ptr, len) pairs; Byte arguments are parsed as integers 0–255.
The raw WASM return value is extracted and returned as a Python int or float:
i64results → Pythoninti32results → Pythonintf64results → Pythonfloat- Void results (Unit) →
None
All IO.print calls during execution write to an in-memory buffer. The buffer contents are returned in ExecuteResult.stdout. This allows programmatic inspection of output without interfering with the host process's stdout.
The CLI prints ExecuteResult.stdout to the terminal after execution completes. If the function also returns a value, the value is printed after the captured output.
WASM traps are unrecoverable runtime errors. The following conditions cause traps:
| Condition | WASM Instruction | Source |
|---|---|---|
| Integer division by zero | i64.div_s |
/ operator on Int |
| Unreachable code | unreachable |
assert failure, bounds check failure |
| Out-of-bounds memory access | i64.load, etc. |
Invalid pointer dereference |
Integer overflow (in i64.div_s) |
— | Int.min_value / -1 |
When a trap occurs, the wasmtime engine raises a WasmtimeError or Trap exception. The CLI reports this as a runtime error and exits with a non-zero status.
Contracts that the verifier could not prove statically (Tier 3) are compiled as runtime assertions. A failed runtime precondition or postcondition executes unreachable, causing a WASM trap.
For the contract insertion strategy, see Chapter 11, Section 11.8.
The assert expression also compiles to a conditional trap: if the condition is false, the program traps (see Chapter 11, Section 11.14).
Array index expressions are bounds-checked at runtime. If the index is negative or greater than or equal to the array length, the program traps via unreachable.
For the bounds checking implementation, see Chapter 11, Section 11.12.
No open limitations.
The browser runtime (vera/browser/runtime.mjs) is a self-contained JavaScript module that provides host function implementations for running compiled Vera WASM modules in the browser or Node.js. It is an alternative to the Python/wasmtime reference runtime described in Section 12.3.
The runtime uses dynamic import introspection to work with any compiled Vera program. At initialization, it calls WebAssembly.Module.imports(module) to discover which host functions the module requires, then builds the import object containing only those bindings. This means the same runtime file works with every compiled Vera program — from a hello-world (1 import: print) to a markdown-heavy program (15+ imports).
State<T> bindings are pattern-matched from import names: state_get_Int and state_put_Int are recognized as State<Int> operations and dynamically paired.
import init, { call, getStdout, getState, resetState } from './vera-runtime.mjs';
// Initialize with a WASM module (URL or ArrayBuffer)
await init('module.wasm');
// Call exported functions
call('main');
// Retrieve captured output
const output = getStdout();
// Read/reset state
const state = getState();
resetState();The init() function follows the init-then-use pattern: async initialization, synchronous calls after. The module is cached — calling init() again with the same URL is a no-op.
The browser runtime provides browser-appropriate implementations of IO operations:
| Operation | Browser Behaviour | Reference (Python) Behaviour |
|---|---|---|
IO.print |
Appends to internal buffer, flushed via getStdout() |
Writes to stdout capture buffer |
IO.read_line |
Reads from pre-queued input array, falls back to prompt() |
Reads from stdin parameter or process stdin |
IO.read_file |
Returns Result.Err("File I/O not available in browser") |
Reads from filesystem |
IO.write_file |
Returns Result.Err("File I/O not available in browser") |
Writes to filesystem |
IO.args |
Returns configurable array (default empty) | Returns CLI arguments |
IO.exit |
Throws VeraExit error with exit code |
Raises _VeraExit exception |
IO.get_env |
Returns Option.None (configurable map) |
Reads from os.environ |
All non-IO operations (State, contracts, Markdown) produce identical results in both runtimes. This is enforced by mandatory parity tests.
The JavaScript runtime follows the same memory protocol as the Python runtime (Section 12.5):
- Never cache TypedArray views across WASM calls —
memory.buffercan be detached bymemory.grow. Always create fresh views before each access. - BigInt for i64: JavaScript WASM i64 values are
BigInt, notnumber. The runtime handles the conversion transparently. - ADT layout: All ADT values use the same byte layout as the Python runtime (tag at offset 0, fields at computed offsets matching
codegen/registration.py).
The vera compile --target browser command produces a ready-to-serve directory:
output_dir/
├── module.wasm # Compiled WASM binary
├── vera-runtime.mjs # Self-contained JavaScript runtime
└── index.html # Loads and runs the program
The index.html file uses an ES module script that imports from vera-runtime.mjs, initializes the WASM module, calls main(), and displays the captured stdout in a <pre> element.
The browser parity test suite (tests/test_browser.py) runs every compilable example through both the Python/wasmtime runtime and the Node.js/JS-runtime, asserting identical stdout output. This catches any drift between the two implementations. The tests cover IO operations, State operations, contract violations, Markdown parsing/rendering, and browser bundle emission.
Pre-commit hooks trigger parity tests on any change to the host binding surface (vera/browser/, vera/codegen/api.py, vera/wasm/markdown.py, vera/markdown.py). CI runs the full parity suite on every PR.