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:

FieldTypeDescription
namestringModule name (typically the source filename)
filenamestringSource filename
mainfunctionThe top-level function (module body)
functionsarrayNested function definitions (referenced by function dest, id)
exportsarrayOptional. 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:

FieldTypeDescription
namestringMember name
kindstringfunction, constant, or other
function_idintegerkind: "function" only — index into functions
valueanykind: "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:

FieldTypeDescription
namestringFunction name ("<anonymous>" for lambdas)
filenamestringSource filename
nr_argsintegerNumber of parameters
nr_slotsintegerTotal register slots needed (args + locals + temporaries)
nr_close_slotsintegerNumber of closure slots captured from parent scope
abi_flagsintegerBytecode ABI flags. 1 means arguments start at slot 0.
disruption_pcintegerInstruction index of the disruption handler (0 if none)
instructionsarrayInstruction 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

InstructionOperandsDescription
accessdest, literalLoad an immutable literal value such as text or number
contextdestLoad the current run context record
intdest, valueLoad integer constant
truedestLoad boolean true
falsedestLoad boolean false
nulldestLoad null
movedest, srcCopy register value
functiondest, idLoad nested function by index
function_refdest, provider_unit, function_id, memberLoad a finalized same-program function export
regexpdest, patternCreate regexp object

Module Imports

InstructionOperandsDescription
importdest, import_idLoad the module result this unit’s import import_id names
member_importdest, import_id, memberLoad 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

InstructionOperandsDescription
adddest, a, bdest = a + b
subtractdest, a, bdest = a - b
multiplydest, a, bdest = a * b
dividedest, a, bdest = a / b
modulodest, a, bdest = a % b
negdest, srcdest = -src

Text

InstructionOperandsDescription
concatdest, a, bdest = a & b (text concatenation)
stonedest, srcStone 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

InstructionOperandsDescription
eqdest, a, bdest = a == b (type-specific)
nedest, a, bdest = a != b (type-specific)
ltdest, a, bdest = a < b (type-specific)
ledest, a, bdest = a <= b (type-specific)
gtdest, a, bdest = a > b (type-specific)
gedest, a, bdest = a >= b (type-specific)

Type Checks

Inlined from intrinsic function calls. Each sets dest to true or false.

InstructionOperandsDescription
is_intdest, srcCheck if integer
is_numdest, srcCheck if number (integer or float)
is_textdest, srcCheck if text
is_booldest, srcCheck if logical
is_nulldest, srcCheck if null
is_arraydest, srcCheck if array
is_funcdest, srcCheck if function
is_recorddest, srcCheck if record
is_stonedest, srcCheck if stone (immutable)

Logical

InstructionOperandsDescription
notdest, srcLogical NOT
anddest, a, bLogical AND
ordest, a, bLogical 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.

InstructionOperandsDescription
loaddest, obj, keyLoad from a guarded array or record
storeobj, val, keyStore into a guarded mutable array or record; storing null to a record removes the key
lengthdest, srcGet length of array or text

Object and Array Construction

InstructionOperandsDescription
recorddestCreate empty record {}
arraydest, nCreate empty array (elements added via push)
pusharr, valPush value to array
popdest, arrPop value from array

Function Calls

Function calls are decomposed into three instructions:

InstructionOperandsDescription
framedest, fn, argcAllocate call frame for fn with argc arguments
setargframe, idx, valSet argument idx in call frame
invokeframe, resultExecute the call, store result
goframedest, fn, argcAllocate frame for an explicit tail call
goinvokeframeTail-invoke the frame, replacing the current call

Variable Resolution

InstructionOperandsDescription
contextdestLoad the current run context record
getdest, level, slotGet closure variable from parent scope
putlevel, slot, srcSet 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

InstructionOperandsDescription
LABELnameDefine a named label (not executed)
jumplabelUnconditional jump
jump_truecond, labelJump if cond is true
jump_falsecond, labelJump if cond is false
jump_not_nullval, labelJump if val is not null
returnsrcReturn value from function
disruptTrigger 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 callEmitted 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.