This is the reference for the Compiler::* namespace exposed to Silicon
strata bodies. The API is the only seam between user-defined strata and the
compiler's internal lowering machinery; every definition kind and every
control-flow keyword in the standard library is implemented on top of it.
Audience: authors of
.sistrata files (built-in or user-supplied). Stability: the surface listed here is stable across patch releases. Anything not listed is internal and may change.
A strata definition has three parts: the dispatch header, the dispatch marker, and (optionally) a rich body.
@stratum_keyword LetDef ('@global', Node) = {
IR::def_function(); # 1. dispatch marker
name := Compiler::watId(Node.name.name); # 2. rich body
body := Compiler::lowerExpr(Node.binding.expression);
Compiler::ir::makeFunction(name, [], 'void', [], body);
};
-
Dispatch marker — the first
IR::xxx()orWASM::xxx()call. The strata loader uses it to identify the codegen kind (function,global,local,export, …) or the intrinsic this body handles. It is a runtime no-op; it carries no semantics during body execution. -
Rich body — any sequence of bare
:=bindings andCompiler::*()calls. The final expression's value is the result the compiler stores (typically an IR node fromCompiler::ir::*, orIR::null()for definitions that emit no WAT).
The body interpreter evaluates statements top-to-bottom. There is no
control flow inside a body — no @if, no @loop, no recursion. When you
need branching, use Compiler::choose(...); when you need iteration, call one
of the helpers that does the iteration internally
(lowerParams, lowerExternParams, expandMatchChain, …).
Each call's arguments are evaluated left-to-right, eagerly. Field
access on a JS object via the namespace path is allowed:
Node.name.typeAnnotation, funcResult.body, etc.
The third token of the strata header (Node in the examples above) is the
parameter name your body uses to refer to the AST node being processed.
What it is bound to depends on the strata type:
| Strata header | Node is … |
|---|---|
@stratum_keyword (def-kind body) |
the Definition AST node |
@stratum_keyword (builtin-call body) |
the rawArgs array (use Compiler::arg(...)) |
@stratum_operator (rich body — rare) |
the rawArgs array |
For builtin-call expanders the body interpreter also exposes
inferredType as an identifier, holding the type-checker's SiliconType
for the call site.
All APIs live under Compiler::*. Nested namespaces (Compiler::ctx::*,
Compiler::ir::*) are how the surface is organised; they are dispatched
by walking the path against the JS API object.
The current lowering context. Most calls have side effects — they mutate the locals / globals / loop-stack the compiler will use when assembling the rest of the module.
| Call | Effect |
|---|---|
Compiler::ctx::locals::get(name) |
Read a local's WASM type, or undefined. |
Compiler::ctx::locals::set(name, type) |
Record a local in the locals map. |
Compiler::ctx::globals::get(name) |
Read a global's WASM type, or undefined. |
Compiler::ctx::globals::set(name, type) |
Record a global in the globals map. |
Compiler::ctx::varNames::has(name) |
True if name is a real WAT global (a @mut binding / sum-type variant). |
Compiler::ctx::varNames::add(name) |
Mark name as a real WAT global. |
Compiler::ctx::pendingLocals::push(local) |
Hoist an IRLocal to the current function's preamble. |
Compiler::ctx::loopStack::push(id) |
Push a loop ID — needed when nesting @break / @continue targets. |
Compiler::ctx::loopStack::pop() |
Pop the innermost loop ID. |
Compiler::ctx::loopStack::peek() |
Peek the innermost loop ID without popping (undefined if empty). |
Compiler::ctx::nextLoopId() |
Allocate a fresh monotonic loop ID. |
Compiler::ctx::functionSigs::get(name) |
Look up a FunctionSig recorded by the type-checker. |
Build a typed IR node without writing object literals. The wasmType is either passed explicitly or inferred from the inputs.
| Call | Returns |
|---|---|
Compiler::ir::makeConst(value, wasmType) |
IRConst |
Compiler::ir::makeLocalGet(name, wasmType) |
IRLocalGet |
Compiler::ir::makeLocalSet(name, value) |
IRLocalSet |
Compiler::ir::makeGlobalGet(name, wasmType) |
IRGlobalGet |
Compiler::ir::makeGlobalSet(name, value) |
IRGlobalSet |
Compiler::ir::makeBinOp(instr, left, right, wasmType) |
IRBinOp |
Compiler::ir::makeCall(callee, args, wasmType, callKind?) |
IRCall |
Compiler::ir::makeBlock(stmts, trailing?, wasmType?) |
IRBlock |
Compiler::ir::makeIf(cond, then, else?, wasmType?) |
IRIf |
Compiler::ir::makeLoop(id, cond, body) |
IRLoop |
Compiler::ir::makeBreak(id) / makeContinue(id) |
IRBreak/Continue |
Compiler::ir::makeReturn(value?) |
IRReturn |
Compiler::ir::makeNop() / makeUnreachable() |
IRNop/Unreachable |
Compiler::ir::makeExport(alias, internalName, what) where what is 'func' | 'global' |
IRExport |
Compiler::ir::makeGlobal(name, wasmType, mutable, init) |
IRGlobal |
Compiler::ir::makeFunction(name, params, returnType, locals, body) |
IRFunction |
Compiler::ir::makeImport(env, field, name, params, result?) |
IRImport |
Compiler::ir::makeLocal(name, wasmType) |
IRLocal |
Compiler::ir::null() |
null |
| Call | Effect |
|---|---|
Compiler::lowerExpr(node) |
Recursively lower an expression AST node to IRExpr using the bound context. |
Compiler::lowerBlock(node) |
Lower a Block AST node to IRBlock. |
Compiler::lowerParam(param) |
Lower one function parameter to IRParam, or null for literal / untyped params. |
Compiler::lowerParams(node) |
Iterate node.params, lower each entry, return the IRParam[]. |
Compiler::lowerExprIfDefined(node) |
Like lowerExpr but returns undefined when node itself is null/undefined. |
Compiler::lowerFunctionBody(node, params) |
Create a child scope with params added to locals, lower node.binding, return {body, locals}. |
Compiler::lowerGlobalInit(node, defaultType) |
Lower a @mut initialiser or fall back to (const 0 : defaultType); refines wasmType from init. |
Compiler::lowerExternParams(node) |
Extract the WASM param types of an @extern. |
Compiler::lowerExternResult(node) |
Extract the WASM result type of an @extern, or undefined. |
Compiler::expandMatchChain(args, type) |
Build the nested if/else chain for @match. Used by match.si. |
Compiler::unwrapNode(node) |
Strip Element / Item / Statement wrappers from an AST node. |
| Call | Returns |
|---|---|
Compiler::resolveType(annotation) |
WasmValType from a type-annotation AST node (Float → 'f32', else 'i32'). |
Compiler::resolveTypeName(name) |
Same, takes a raw string. |
Compiler::resolveExprType(expr) |
The WasmType of an already-lowered IR expression. |
Compiler::resolveFunctionReturnType(node, name, body) |
Three-priority lookup: annotation → function-sig → body refinement. |
Compiler::isVarName(name) |
True if name is a real WAT global (delegates to ctx.varNames.has). |
| Call | Returns |
|---|---|
Compiler::watId(name) |
Sanitise a Silicon identifier to a valid WAT identifier (:: → _). |
Compiler::freshId(prefix?) |
Allocate a unique synthetic identifier — e.g. tmp_3. |
Compiler::resolveIntrinsic(name) |
Resolve an IR::foo or WASM::foo name to its WAT instruction string. |
Compiler::choose(cond, ifTrue, ifFalse) |
Eager ternary. Both branches are evaluated; pick one to return. |
Compiler::arg(node, index) |
node[index] — for stepping through the rawArgs array. |
| Call | Behaviour |
|---|---|
Compiler::assertDefined(value, msg) |
Throws CompilerAPIError if value is null / undefined. |
Compiler::error(msg, node?) |
Always throws CompilerAPIError. Optional node is used for source loc. |
Definition-kind strata may register all three callbacks. Built-in
strata only implement expand; the others are opt-in.
| Hook | When it fires | Typical use |
|---|---|---|
preScan |
Before the main lowering loop, once per definition AST node. | Pre-register names so forward references resolve. |
expand |
During the main loop, once per definition AST node. | Emit the IR for that definition. |
postExpand |
After the main loop, once per registered defExpander. | Emit module-level items derived from cross-def state. |
The body interpreter currently exposes only expand as a rich-body hook.
preScan and postExpand are TypeScript hooks on the
builtinDefExpanders registry.
@stratum_keyword Break ('@break', Node) = {
IR::control_break();
id := Compiler::ctx::loopStack::peek();
Compiler::assertDefined(id, '@break outside @loop');
Compiler::ir::makeBreak(id);
};
@stratum_keyword Return ('@return', Node) = {
IR::control_return();
valN := Compiler::arg(Node, 0);
value := Compiler::lowerExprIfDefined(valN);
Compiler::ir::makeReturn(value);
};
lowerExprIfDefined returns undefined rather than a Nop, so the
downstream makeReturn correctly emits a return with no value.
@stratum_keyword ExportDecl ('@export', Node) = {
IR::meta_export();
sname := Compiler::watId(Node.name.name);
isVar := Compiler::isVarName(sname);
kind := Compiler::choose(isVar, 'global', 'func');
Compiler::ir::makeExport(sname, sname, kind);
};
choose is eager — both branches must be safe to evaluate. For lazy
branching, pre-bind both branches with bare := first, then choose
between the bindings.
When a strata needs to lower a sub-expression in a fresh locals scope
(e.g. function bodies), use lowerFunctionBody rather than mutating
the outer locals map:
@stratum_keyword LetDef ('@global', Node) = {
IR::def_function();
name := Compiler::watId(Node.name.name);
params := Compiler::lowerParams(Node);
funcResult := Compiler::lowerFunctionBody(Node, params);
body := funcResult.body;
locals := funcResult.locals;
returnType := Compiler::resolveFunctionReturnType(Node, name, body);
Compiler::ctx::globals::set(name, 'i32');
Compiler::ir::makeFunction(name, params, returnType, locals, body);
};
lowerFunctionBody returns a struct with body (the lowered body) and
locals (the locals collected during body lowering — anything pushed
through pendingLocals::push inside the body).
- Booleans are
@true/@false, nottrue/false.trueparses as a namespace lookup and throws "Unknown identifier". - Strings use single quotes:
'global','@break outside @loop'. - Calls are always parenthesized.
Compiler::watId(Node.name.name)passes a field-access expression as an argument. When passing a function-call result as an argument, bind it to a bare:=first to keep each call's argument list unambiguous. - Field access uses
.—Node.name.typeAnnotation. Index access on arrays usesCompiler::arg(Node, i)because the grammar does not allow numeric segments in a namespace path. IR::xxx()andWASM::xxx()calls inside a rich body are silent dispatch markers. They are not invokable at runtime; build IR withCompiler::ir::*instead.
The complete strata, end to end, for the @mut / mutable-binding definition kind:
@stratum_keyword LocalDef ('@local', Node) = {
IR::def_local();
wasmType := Compiler::resolveType(Node.name.typeAnnotation);
sname := Compiler::watId(Node.name.name);
decl := Compiler::ir::makeLocal(sname, wasmType);
Compiler::ctx::pendingLocals::push(decl);
Compiler::ctx::locals::set(sname, wasmType);
IR::null();
};
What happens when a \\ x Int annotated @mut x := 5; is encountered:
Nodeis theDefinitionAST node ({type: 'Definition', keyword: '@mut', name: {name: 'x', typeAnnotation: {typename: 'Int'}}, binding: …}).wasmType←'i32'(fromresolveTypeof theIntannotation).sname←'x'(no::to sanitise).decl←{ name: 'x', wasmType: 'i32' }.- The pending-locals list and locals map are updated —
xis now a known local for the rest of the current function body. IR::null()returnsnull— the definition itself emits no top-level WAT node. The initialiser (5) is emitted as anIRLocalSetstatement when the surrounding block is lowered.
src/compiler-api/index.ts— interface + factory forCompilerAPI.src/elaborator/strataBody.ts— the body interpreter (isRichBody,evalExpr,evalCall).src/elaborator/strataLoader.ts— wires rich bodies intoregistry.defExpanders/registry.expanders.src/strata/*.si— built-in strata; read these as living examples.src/ir/lower.ts— the helpers exposed via the API (lowerParams,lowerFunctionBody, …) live here.