Mcode IR
Overview
Mcode is the intermediate representation at the center of the ƿit compilation pipeline. All source code is lowered to mcode before execution or native compilation. The mcode instruction set is the authoritative reference for the operations supported by the ƿit runtime — the Mach VM bytecode is a direct binary encoding of these same instructions.
Source → Tokenize → Parse → Fold → Mcode → Streamline → Machine
Mcode is produced by mcode.cm, optimized by streamline.cm, then pressed into a Mach pool of 32-bit bytecode: mach_pool.c lowers a unit, shoplib/mach_pool_emit.cm links the fragments into the pool, and mach_vm.c runs it. That is the one execution lane — a pool is what an actor runs. See Compilation Pipeline for the full overview.
Module Structure
An .mcode file is a JSON object representing a compiled module:
| Field | Type | Description |
|---|---|---|
name | string | Module name (typically the source filename) |
filename | string | Source filename |
main | function | The top-level function (module body) |
functions | array | Nested function definitions (referenced by function dest, id) |
exports | array | Optional. The declared export shape — see below |
The exports declaration
exports is an optional, additive field of pit.mcode.unit@3. The compiler
emits it when the unit main returns a literal record, because it is building
that record itself and therefore already knows the whole shape. Each entry is:
| Field | Type | Description |
|---|---|---|
name | string | Member name |
kind | string | function, constant, or other |
function_id | integer | kind: "function" only — index into functions |
value | any | kind: "constant" only — the member’s compile-time value |
A member is function when its value is a function literal, or a bare name that
still resolves to a unit-main function declaration (those bind a const, so
nothing can rebind the name before the return). It is constant for a literal
number, text, logical, or null. Everything else is other — the member is
named, but its value is not a link-time fact.
The field is absent whenever the compiler cannot state the whole shape: a unit main that returns something other than a literal record, or a record built through a computed member key. A reader that does not find it recovers the export shape from the main’s instruction stream as before. The declaration is a shortcut past that recovery, never a new requirement — which is what makes it additive.
Literal values are carried inline by the instruction that loads them — see
access and int below. There is no constant pool at this level.
The stored pit.mcode.unit@3 artifact does pool them: it interns each distinct
constant and writes ["literal", dest, index] where mcode has an inline
access. That is an encoding of the artifact, not a change to this instruction
language — a reader restores the inline form before lowering, so nothing here
and nothing below it ever sees a literal. See
Artifact formats, “Portable mcode unit”.
Function Record
Each function (both main and entries in functions) has:
| Field | Type | Description |
|---|---|---|
name | string | Function name ("<anonymous>" for lambdas) |
filename | string | Source filename |
nr_args | integer | Number of parameters |
nr_slots | integer | Total register slots needed (args + locals + temporaries) |
nr_close_slots | integer | Number of closure slots captured from parent scope |
abi_flags | integer | Bytecode ABI flags. 1 means arguments start at slot 0. |
disruption_pc | integer | Instruction index of the disruption handler (0 if none) |
instructions | array | Instruction arrays and label strings |
Arguments start at slot 0. Slots 0 through nr_args - 1 hold parameters. Remaining slots up to nr_slots - 1 are locals, closure storage, and temporaries.
Instruction Format
Each instruction is a JSON array. The first element is the instruction name (string), followed by operands:
["add", dest, a, b]
["load", dest, obj, key]
["jump", "label_name"]
Operands are register slot numbers (integers), constant values (strings, numbers), or label names (strings).
Compiler debugging views may preserve source locations while a unit is being diagnosed, but final unit mcode stores line and column data in a debug sidecar instead of trailing operands on every instruction.
Instruction Reference
Loading and Constants
| Instruction | Operands | Description |
|---|---|---|
access | dest, literal | Load an immutable literal value such as text or number |
context | dest | Load the current run context record |
int | dest, value | Load integer constant |
true | dest | Load boolean true |
false | dest | Load boolean false |
null | dest | Load null |
move | dest, src | Copy register value |
function | dest, id | Load nested function by index |
function_ref | dest, provider_unit, function_id, member | Load a finalized same-program function export |
regexp | dest, pattern | Create regexp object |
Module Imports
| Instruction | Operands | Description |
|---|---|---|
import | dest, import_id | Load the module result this unit’s import import_id names |
member_import | dest, import_id, member | Load one named member through a member-granular import edge |
import_id is a dense index into the unit’s own imports array (see
Artifact formats, “Portable mcode unit”), and it is the
only thing the instruction says. What that import RESOLVES to is not in the
instruction and not in the artifact carrying it: the executable manifest binds
the ID to a provider unit, and the target finalizer decides per edge whether
the provider ended up in the same image — a same-image edge becomes a direct
index into the actor’s result array, a cross-image edge an entry in the Mach
image’s IMPORT table that a realization binding row fills in. That is what
lets one mapped image be shared by actors that resolve the same name
differently.
A unit reaches this form only when it is compiled for the pool lane. The
other lane resolves use() by static linking, lowering it to a context
load of the linked module’s name, and emits no import instruction at all.
Both lanes read the same source; the lane is part of the unit’s derivation key
because one text lowers two ways.
Every press compiles on the pool lane, and only the pool lane has edges to
finalize. A boot or ship press links its executable’s units as one program,
so a use() has to be an import instruction for the linker to have anything
to rewrite. The static lane is what a DEVELOPMENT realization takes, where each
unit is its own pool and a cross-pool edge has no result slot to name.
function_ref and member_import are linker-produced forms. They remain
portable mcode: neither contains a Mach opcode or native symbol-table index.
A direct function reference names the provider by its executable-local unit
ID and the callee by its durable mcode function ID; member says which value
in the provider’s initialized literal stone export record owns that closure
identity. Lowering must read that initialized value rather than create a fresh
closure in the importer, because the exported function may close over its
module’s frame.
A member import keeps the unit’s dense import ID plus the source member name. The linked-program import row resolves the module part to a locator. A target press may then finalize the reference for its own calling convention, including a native member later, but mcode always spells it as locator plus name. A symbol-table ordinal is target-final state and is never legal in either form.
Arithmetic
| Instruction | Operands | Description |
|---|---|---|
add | dest, a, b | dest = a + b |
subtract | dest, a, b | dest = a - b |
multiply | dest, a, b | dest = a * b |
divide | dest, a, b | dest = a / b |
modulo | dest, a, b | dest = a % b |
neg | dest, src | dest = -src |
Text
| Instruction | Operands | Description |
|---|---|---|
concat | dest, a, b | dest = a & b (text concatenation) |
stone | dest, src | Stone a value in place and write it to dest |
The streamline optimizer’s escape analysis pass emits stone slot, slot before a mutable text value escapes its defining slot — for example, before a move, setarg, store, push, or put. The instruction stones the heap object in place; using the same source and destination slot does not copy the text.
Comparison
| Instruction | Operands | Description |
|---|---|---|
eq | dest, a, b | dest = a == b (type-specific) |
ne | dest, a, b | dest = a != b (type-specific) |
lt | dest, a, b | dest = a < b (type-specific) |
le | dest, a, b | dest = a <= b (type-specific) |
gt | dest, a, b | dest = a > b (type-specific) |
ge | dest, a, b | dest = a >= b (type-specific) |
Type Checks
Inlined from intrinsic function calls. Each sets dest to true or false.
| Instruction | Operands | Description |
|---|---|---|
is_int | dest, src | Check if integer |
is_num | dest, src | Check if number (integer or float) |
is_text | dest, src | Check if text |
is_bool | dest, src | Check if logical |
is_null | dest, src | Check if null |
is_array | dest, src | Check if array |
is_func | dest, src | Check if function |
is_record | dest, src | Check if record |
is_stone | dest, src | Check if stone (immutable) |
Logical
| Instruction | Operands | Description |
|---|---|---|
not | dest, src | Logical NOT |
and | dest, a, b | Logical AND |
or | dest, a, b | Logical OR |
Property Access
Memory operations are canonical load and store instructions. The compiler emits guard mcode before them so Mach receives only valid array or record operations.
| Instruction | Operands | Description |
|---|---|---|
load | dest, obj, key | Load from a guarded array or record |
store | obj, val, key | Store into a guarded mutable array or record; storing null to a record removes the key |
length | dest, src | Get length of array or text |
Object and Array Construction
| Instruction | Operands | Description |
|---|---|---|
record | dest | Create empty record {} |
array | dest, n | Create empty array (elements added via push) |
push | arr, val | Push value to array |
pop | dest, arr | Pop value from array |
Function Calls
Function calls are decomposed into three instructions:
| Instruction | Operands | Description |
|---|---|---|
frame | dest, fn, argc | Allocate call frame for fn with argc arguments |
setarg | frame, idx, val | Set argument idx in call frame |
invoke | frame, result | Execute the call, store result |
goframe | dest, fn, argc | Allocate frame for an explicit tail call |
goinvoke | frame | Tail-invoke the frame, replacing the current call |
Variable Resolution
| Instruction | Operands | Description |
|---|---|---|
context | dest | Load the current run context record |
get | dest, level, slot | Get closure variable from parent scope |
put | level, slot, src | Set closure variable in parent scope |
Free names are not a distinct VM lookup. The compiler emits context into a
temporary slot, emits the name with ordinary text access, then uses ordinary
record load against the context record supplied to the mcode runner.
Control Flow
| Instruction | Operands | Description |
|---|---|---|
LABEL | name | Define a named label (not executed) |
jump | label | Unconditional jump |
jump_true | cond, label | Jump if cond is true |
jump_false | cond, label | Jump if cond is false |
jump_not_null | val, label | Jump if val is not null |
return | src | Return value from function |
disrupt | — | Trigger disruption (error) |
Instruction Dispatch and Type Checks
A key design principle of mcode is that every type check is an explicit instruction. Arithmetic and comparison operations use generic opcodes (add, subtract, eq, ne, etc.); type checks and branch guards establish type safety before those operations run.
When type information is available from the fold stage, the compiler emits the generic operation directly. When the type is unknown, the compiler emits a type-check/dispatch pattern:
["is_num", check, a]
["jump_false", check, "bad_type"]
["add", dest, a, b]
["jump", "done"]
["LABEL", "bad_type"]
["disrupt"]
["LABEL", "done"]
The optimizer eliminates dead branches when types are statically known, collapsing the dispatch to a single generic instruction.
Intrinsic Inlining
The mcode compiler recognizes calls to built-in intrinsic functions and emits direct opcodes instead of the generic frame/setarg/invoke call sequence:
| Source call | Emitted instruction |
|---|---|
is_array(x) | is_array dest, src |
is_function(x) | is_func dest, src |
is_record(x) | is_record dest, src |
is_stone(x) | is_stone dest, src |
is_integer(x) | is_int dest, src |
is_text(x) | is_text dest, src |
is_number(x) | is_num dest, src |
is_logical(x) | is_bool dest, src |
is_null(x) | is_null dest, src |
length(x) | length dest, src |
push(arr, val) | push arr, val |
Labels and Control Flow
Control flow uses named labels instead of numeric offsets:
["LABEL", "loop_start"]
["add", 1, 1, 2]
["jump_false", 3, "loop_end"]
["jump", "loop_start"]
["LABEL", "loop_end"]
Labels are collected into a name-to-index map during loading, enabling O(1) jump resolution. The Mach serializer converts label names to numeric offsets in the binary bytecode.
Nop Convention
The streamline optimizer replaces eliminated instructions with nop strings (e.g., _nop_tc_1, _nop_bl_2). Nop strings are skipped during interpretation and native code emission but preserved in the instruction array to maintain positional stability for jump targets.