Choose the register bytecode contract
Archived MVP specification. For ASPL 0.5.0, see the current reference and native HTTP guide.
Type: grilling Status: resolved Blocked by: 05, 07 Parent: Find the ASPL MVP specification
Question
What instruction model, register discipline, function encoding, constant representation, control-flow encoding, file format, versioning rule, and validation boundary should define ASPL's MVP bytecode without designing features beyond the semantic core?
Answer
ASPL bytecode is a private, documented, versioned format shared by the compiler, native VM, and browser VM. It uses fixed-width typed register instructions, per-function register and type tables, explicit branches, indexed constant tables, and a canonical little-endian file envelope. A VM accepts only the exact format version it implements and only after validating the complete artifact into an immutable VmProgram.
The format is not a public extension API or a compatibility promise between ASPL releases. Compiler output, bytes embedded in an ELF template, and bytes passed to the browser VM all cross the same validation boundary.
Scalar encodings
Every integer field uses little-endian encoding. Indices, counts, offsets, opcodes, and instruction operands use u32. The complete artifact must be smaller than 2^32 bytes.
Type IDs are part of version 1:
| ID | Type | Valid use |
|---|---|---|
| 1 | bool |
Register or function result |
| 2 | i64 |
Register or function result |
| 3 | f64 |
Register or function result |
| 4 | string |
Register or function result |
| 5 | c_ptr |
Register or function result |
| 6 | unit |
Function result only |
Zero and every unassigned type ID are invalid. unit never appears in a register-type entry.
Register and function discipline
Each function declares a fixed register count and one type for every register. Parameters occupy the first registers in declaration order. Their types come from those register-type entries. The compiler assigns locals and expression temporaries to the remaining registers. A register may be written more than once, but every write must produce its declared type. The bytecode has no operand stack, dynamically growing register file, SSA nodes, or phi instructions.
A function record contains seven u32 fields in this order:
- function-name string-reference index;
- result type ID;
- parameter count;
- register count;
- first register-type index;
- first instruction index; and
- instruction count.
Function table indices are function IDs. Register-type and instruction ranges appear in function-table order and exactly partition their sections. Every function has at least one instruction. Parameter count cannot exceed register count.
The header identifies the entry function by index. That function must take no parameters and return i64. Bytecode cannot call the entry function.
Calls pass arguments through a contiguous range of caller registers. The callee signature or foreign adapter supplies the argument count. For a zero-argument call, the first-argument operand must be zero. A value call has a destination register and requires a non-unit result of the destination's type. A discard call has no return destination and accepts either a unit result or a discarded non-unit result. The VM copies all arguments into a newly reserved frame as specified by Choose the runtime value model. A call destination may overlap its argument range because the VM copies arguments before the callee runs and writes the destination only after a normal return.
Instruction representation
Every instruction is 16 bytes containing four u32 words: [opcode, a, b, c]. Operands not named below must be zero. The validator rejects unknown opcodes and nonzero unused operands.
| Opcode | Instruction | Operands a, b, c |
|---|---|---|
| 1 | LOAD_BOOL |
destination, immediate 0 or 1, 0 |
| 2 | LOAD_I64 |
destination, i64 constant index, 0 |
| 3 | LOAD_F64 |
destination, f64 constant index, 0 |
| 4 | LOAD_STRING |
destination, string-reference index, 0 |
| 5 | LOAD_NULL |
destination, 0, 0 |
| 6 | COPY |
destination, source, 0 |
| 7 | I64_TO_F64 |
destination, source, 0 |
| 8 | F64_TO_I64 |
destination, source, 0 |
| 9 | NOT_BOOL |
destination, source, 0 |
| 10 | NEG_I64 |
destination, source, 0 |
| 11 | ADD_I64 |
destination, left, right |
| 12 | SUB_I64 |
destination, left, right |
| 13 | MUL_I64 |
destination, left, right |
| 14 | DIV_I64 |
destination, left, right |
| 15 | REM_I64 |
destination, left, right |
| 16 | NEG_F64 |
destination, source, 0 |
| 17 | ADD_F64 |
destination, left, right |
| 18 | SUB_F64 |
destination, left, right |
| 19 | MUL_F64 |
destination, left, right |
| 20 | DIV_F64 |
destination, left, right |
| 21 | EQ_BOOL |
destination, left, right |
| 22 | EQ_I64 |
destination, left, right |
| 23 | EQ_F64 |
destination, left, right |
| 24 | EQ_STRING |
destination, left, right |
| 25 | EQ_C_PTR |
destination, left, right |
| 26 | LT_I64 |
destination, left, right |
| 27 | LE_I64 |
destination, left, right |
| 28 | GT_I64 |
destination, left, right |
| 29 | GE_I64 |
destination, left, right |
| 30 | LT_F64 |
destination, left, right |
| 31 | LE_F64 |
destination, left, right |
| 32 | GT_F64 |
destination, left, right |
| 33 | GE_F64 |
destination, left, right |
| 34 | JUMP |
target instruction, 0, 0 |
| 35 | JUMP_IF_FALSE |
condition, target instruction, 0 |
| 36 | CALL_VALUE |
destination, function index, first argument |
| 37 | CALL_DISCARD |
function index, first argument, 0 |
| 38 | CALL_FOREIGN_VALUE |
destination, import index, first argument |
| 39 | CALL_FOREIGN_DISCARD |
import index, first argument, 0 |
| 40 | RETURN_VALUE |
source, 0, 0 |
| 41 | RETURN_UNIT |
0, 0, 0 |
| 42 | WRITE_STRING |
source string register, 0, 0 |
Load and movement instructions require the evident destination and source types. Integer and floating operations require matching typed operands. Comparisons write bool. COPY requires source and destination to have the same type. Numeric conversions use the checked semantics from Define the MVP semantic core and Choose the runtime value model.
Integer operations trap for the cases fixed by Define the MVP semantic core. Floating operations preserve the specified binary64 behavior and canonicalize every NaN result. Equality follows Choose the runtime value model for each named type.
WRITE_STRING requires an initialized string source register. It invokes the synchronous host-output contract from Choose the MVP builtin environment, creates no frame, and has no return destination. Success falls through. A host output limit or failure stops execution through the outcome channel and never becomes an ASPL trap.
There are no NOP, HALT, stack, allocation, generic trap, Boolean and or or, inequality, or extension opcodes. The compiler lowers inequality to typed equality followed by NOT_BOOL. It lowers and and or to branches so evaluation remains short-circuiting. Returning from the entry function produces the normal halt or invalid-process-status trap defined by Choose the runtime value model.
Constants and strings
Version 1 has separate i64, f64, and string-reference tables. An integer constant is its exact 64-bit two's-complement pattern. A floating constant is its exact IEEE 754 binary64 pattern. Both patterns are serialized least-significant byte first. Infinities and signed zero are valid. A NaN constant must use the canonical quiet NaN pattern 0x7ff8000000000000; the validator rejects every other NaN encoding.
A string-reference record is a u32 byte offset followed by a u32 byte length into the string-bytes section. Its contents must be strict UTF-8. Embedded NUL is ordinary content for an ASPL string literal. References may be empty, equal, overlapping, or shared because storage identity is unobservable. The loader gives an empty runtime string a non-null backing pointer as required by Choose the runtime value model.
Boolean values and null pointers use their load instructions instead of constant-table entries.
Control flow and instruction accounting
JUMP and JUMP_IF_FALSE use function-relative instruction indices. Every other non-returning instruction falls through to the next instruction. A branch cannot target another function or the position after a function. The validator rejects unreachable instructions and any reachable fallthrough past the function's instruction range.
The instruction counter starts at zero. Immediately before dispatch, the VM compares it with the configured ceiling. If the count equals the ceiling, execution stops without dispatching that instruction. Otherwise the VM increments the counter and executes the instruction. A ceiling of N therefore permits exactly N instructions. Loads, calls, foreign calls, host writes, branches, and returns each count as one instruction regardless of the work their helpers perform.
Foreign imports
A foreign-import record contains three u32 fields: library-name string-reference index, symbol-name string-reference index, and adapter-signature ID. The adapter catalog defines the parameter types, result type, and valid boundary conversions. Define dynamic C interoperability owns that catalog and the loading, lifetime, and failure rules.
Foreign value and discard calls use the same contiguous argument-register convention as ASPL calls. A foreign value call requires a non-unit adapter result of the destination's type. Validation rejects an unknown adapter ID or argument and destination types that do not match its catalog entry. Library and symbol names must be nonempty UTF-8 strings without embedded NUL. Source-level unsafe markers are not serialized; the source checker must verify them before bytecode generation.
File envelope
The version 1 header is 48 bytes:
| Byte offset | Field | Required value |
|---|---|---|
| 0 | Eight magic bytes | 41 53 50 4c 42 43 00 00, or ASPLBC\0\0 |
| 8 | Version | 1 |
| 12 | Header size | 48 |
| 16 | Total byte size | Exact file size, below 2^32 |
| 20 | Section count | 8 |
| 24 | Directory offset | 48 |
| 28 | Entry function index | Valid function-table index |
| 32 | Flags | 0 |
| 36 | CRC-32 | Integrity value described below |
| 40 | Reserved word 0 | 0 |
| 44 | Reserved word 1 | 0 |
The directory immediately follows the header and contains eight 16-byte entries. Each entry contains section ID, byte offset, byte length, and item count. Entries use this order:
| Section ID | Contents | Record size |
|---|---|---|
| 1 | Functions | 28 bytes |
| 2 | Register types | 1 byte |
| 3 | Instructions | 16 bytes |
| 4 | i64 constants |
8 bytes |
| 5 | f64 constants |
8 bytes |
| 6 | String references | 8 bytes |
| 7 | String bytes | 1 byte |
| 8 | Foreign imports | 12 bytes |
All eight sections are present even when empty. The functions section is not empty. The first section starts at the first eight-byte-aligned offset after the directory. Each later section starts at the first eight-byte-aligned offset after the previous section. Padding bytes are zero, and the declared total size equals the end of the foreign-import section. The byte length of every section equals its item count multiplied by its record size. Parsers perform every offset, addition, and multiplication check in a widened integer type and never cast file bytes to C structs.
The CRC uses CRC-32/ISO-HDLC with polynomial 0x04c11db7, reflected polynomial 0xedb88320, initial value 0xffffffff, reflected input and output, and final XOR 0xffffffff. It covers all declared bytes with header bytes 36 through 39 treated as zero. The CRC detects accidental corruption. It does not authenticate bytecode.
There is one version number and no minor-version negotiation. A VM accepts only version 1. Changing any numeric assignment, including a type, section, opcode, or adapter-signature ID, requires a new version. So does changing an operand meaning, record layout, validation rule, or execution semantic. Version 1 rejects unknown sections, opcodes, flags, adapter IDs, and nonzero reserved fields rather than trying to skip them.
Validation boundary
The loader treats every serialized artifact as untrusted, including fresh compiler output. It validates in this order:
- Check the host byte-size cap, minimum header size, magic, version, fixed header fields, exact total size, zero flags and reserved words, then the CRC.
- Validate the directory with widened arithmetic: the exact section set and order, eight-byte alignment, canonical offsets, non-overlap, record-size agreement, zero padding, and host count caps. Check all caps before allocating table storage.
- Validate every string reference independently as strict UTF-8. Function names and foreign library and symbol names must be nonempty and contain no NUL.
- Validate constant encodings, register type IDs, function result types and signatures, the entry-function signature, foreign adapter IDs, and exact register and instruction partitioning.
- Decode every instruction. Check its opcode, zero operands, constant and table indices, register bounds and types, argument range and signature, call destination, return type, branch target, and prohibition on calls to the entry function. Reject every value call whose ASPL function or foreign adapter returns
unit. - Build each function's control-flow graph. Reject unreachable instructions and reachable fallthrough past its range. Check that every reachable return matches the function result. Cycles are valid and need not terminate.
- Prove definite assignment. Parameters are initialized at entry and all other registers are initially uninitialized. Propagate initialized-register sets forward, intersect them at control-flow joins, and iterate loops to a fixed point. Reject a reachable read unless every incoming path initializes that register. A value call initializes its destination only on its normal continuation.
- Construct the immutable
VmProgramonly after every check passes. No interpreter entry point accepts raw bytes or a partially validated program.
The loader receives host policy caps for total bytecode bytes, functions, imports, constants, string bytes, instructions, and registers per function. Crossing one produces a load-time resource-limit failure, distinct from malformed bytecode and from a runtime instruction-limit stop. The artifact cannot raise these caps. The compiler checks the published caps for its selected host before emitting output.
The source checker remains responsible for lexical names, shadowing, unsafe markers, source-level reachability, and declaration resolution. The loader does not attempt to reconstruct source. Compiler-only diagnostics may retain source spans in memory, but version 1 bytecode contains no source paths, source text, or instruction spans. Native runtime locations use a function name and function-relative instruction index.
The interpreter still checks register tags and array bounds before use. Reaching an uninitialized register, seeing an unexpected tag, or failing another validated invariant reports a VM failure, not an ASPL trap. Runtime checks also implement the specified language traps and configured frame, slot, and instruction limits.
This contract contains no debug extension mechanism, bytecode extension opcodes, runtime-generated code, or native calling convention. Later work may add those only under a new bytecode version.