MIR
MIR is the middle intermediate representation used between semantic analysis and backend lowering.
It is source-facing enough to inspect, but executable enough for optimization. Source names are resolved before MIR. Rebindings are split into explicit binding versions. Runtime values are represented by SSA-style value ids. Lifetimes, escapes, materialization demand, copy-on-write eligibility, and slot reuse are computed on MIR.
Module
A MIR module contains executable bodies for:
- the script entry body;
- package value initializers;
- top-level functions;
- lambda bodies.
MirModule
module: ModulePath
kind: script | evil script | package
optimization: NOpt | IOpt | SOpt
bodies: [MirBody]
Body
Each body is a control-flow graph.
MirBody
id: BodyId
name: String
kind: script_entry | value_initializer | function | lambda
purity: pure | evil
rank: baseline | interactive | sealed-ownership | sealed-demand | sealed-materialization
params: [ValueId]
captures: [Capture]
bindings: [Binding]
values: [Value]
blocks: [Block]
analyses: AnalysisSummary
Blocks use block parameters as phi nodes. A predecessor passes the values needed
by the successor through jump or branch.
Block
id: BlockId
params: [ValueId]
ops: [Op]
term: Terminator
Binding Nodes
BindingId identifies a source declaration. Two declarations with the same
source name always have different ids.
Binding
id: BindingId
name: String
mutability: val | var
scope_depth: u32
declared_type: Type?
span: TextSpan
capture: local | captured | noncapturable
versions: [VersionId]
VersionId identifies the value currently held by a binding at a point in the
body. A val usually has one version. A var has one version for its
initializer and one for each assignment.
Version
id: VersionId
binding: BindingId
value: ValueId
source: initializer | assignment | compound_assignment | join | loop
The executable graph uses ValueId operands, not source names. Bindings remain
for inspection, diagnostics, and optimization explanations.
Value Nodes
Value
id: ValueId
type: Type?
def: parameter | capture | block_param | op | literal | unit
binding_version: VersionId?
uses: [Use]
lifetime: Lifetime
escape: Escape
demand: Demand
storage: Storage
Lifetime records first definition, last use, live-in blocks, live-out blocks,
and whether storage can be reused after the last use.
Escape records whether the value is returned, captured, stored in econ,
passed to an evil call, or passed to an unknown host boundary.
Demand records whether the full value is needed or only selected tuple slots
or record fields are demanded.
Storage records the current slot assignment and copy-on-write status:
fresh: new storage is required;reuse(ValueId): the value can reuse storage from an ended lifetime;cow(ValueId): the value can share storage until a write forces a copy;virtual: no materialized storage is required.
Operation Nodes
Op
result: ValueId?
kind: OpKind
args: [ValueId]
span: TextSpan?
Operation kinds:
literal(value);unary(op);binary(op);tuple(shape);record(shape);list;project(field | slot);index;updated(path);call(callee, purity);econ(type);non_null;safe_project(field);type_test(type);type_refine(type);iterator;iterator_next;cache_get(key);cache_put(key).
Short-circuit &&, ||, and ?: lower to branches and joins unless a prior
analysis proves eager evaluation is equivalent.
Terminator Nodes
Terminator
jump(target, args)
branch(condition, then_target, then_args, else_target, else_args)
return(value)
panic(message)
unreachable
if, when, loops, early return, and panic are represented with blocks and
terminators. Branch result values flow through join block parameters.
Lowering Semantics
The lowering environment maps each visible source name to its current binding version.
Declaration lowering:
- Lower the initializer.
- Create a new
BindingId. - Create version zero for the binding.
- Map the source name to that version in the current lexical scope.
Assignment lowering:
- Resolve the target name to a mutable binding.
- Lower the right-hand expression.
- Create a new version for the same binding.
- Update the environment so later references use the new version.
Compound assignment lowering:
- Resolve the target name to the current value.
- Lower the right-hand expression.
- Emit the matching
binaryoperation. - Create a new version for the target binding.
Shadowing lowering creates a new BindingId and restores the previous name
mapping when the lexical scope exits.
Branch joins compare the binding versions leaving each branch. If a binding has different outgoing versions, the join block receives a block parameter and the environment maps that binding to a join-created version.
Loops use loop header block parameters for loop-carried mutable bindings. The loop pattern is a fresh binding in the loop body.
when lowers the subject once, then emits type tests. An as binding is a
fresh binding whose value is the refined subject inside that arm.
Text Format
MIR text is an inspection format, not the executable storage format. It keeps the surface close to Vox while exposing compiler facts.
Example source:
var x = 1;
x = x + 2;
{
val x = "local";
x
}
x
Example MIR text:
body @script_entry pure rank=interactive {
binding %b0 var x scope=0 versions=[%v0,%v1]
binding %b1 val x scope=1 versions=[%v2]
block %bb0:
%0 = literal 1
bind %v0 -> %b0 = %0
%1 = use %v0
%2 = literal 2
%3 = binary add %1, %2
bind %v1 -> %b0 = %3 lifetime(%0..%3, reusable)
%4 = literal "local"
bind %v2 -> %b1 = %4 lifetime(%4..%5, reusable)
%5 = use %v2
drop %5
%6 = use %v1 lifetime(%3..return, escapes=return)
return %6
}
The text format includes:
- body kind, purity, and optimization rank;
- binding declarations and version lists;
- block labels;
- value-producing operations;
- bind statements showing
VersionIdcreation; - lifetime summaries;
- escape and demand summaries when non-default;
- storage summaries when slot reuse, copy-on-write, or virtualization applies.
Optimization Passes
Optimization is extensible through ordered MIR passes. A pass receives a mutable body plus analysis facts and returns whether it changed the body.
Required built-in passes:
- control-flow cleanup;
- def-use construction;
- lifetime analysis;
- active value caching for
IOpt; - demand analysis for tuple and record projections;
- function result culling for unused tuple slots and record fields;
- copy-on-write marking;
- storage slot reuse;
- sealed compaction for
SOpt.
Custom pass groups can be installed around the built-in groups:
- before cleanup;
- after analysis;
- before sealed compaction;
- before backend lowering.
Custom passes must preserve Vox behavior. Passes that move, remove, or share operations must respect evil call ordering and escape facts.
Backend Contract
MIR does not encode wasm layout.
The wasm backend chooses local allocation, stack use, memory layout, imported function ABI, runtime helper calls, and final instruction order. MIR supplies typed values, explicit control flow, effect metadata, lifetimes, demand, and storage reuse facts.