Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.
The compiler contract — constraints a resolve/emit rewrite must honor
Status: inventory extracted from the current implementation (tokenize.cm, parse.cm, fold.cm, mcode.cm, streamline.cm, mach.c, qbe_emit.cm) and the docs under docs/language/ and docs/spec/, 2026-07. This is the implicit contract the current pipeline implements. Anything not listed here that a rewrite changes should be treated as a behavior change, not a refactor.
Section markers: [DOC] documented, [CODE] implementation-only (undocumented), [BOTH] documented and confirmed in code, [STALE] doc contradicts code.
1. Language guarantees the compiler may exploit
- [BOTH]
defbindings cannot be reassigned;varcan. All declarations are function-body level (never insideif/while/for/doblocks) and must be initialized. There is no block scoping and no hoisting ambiguity. - [BOTH] Function parameters are
def— never reassignable. Documented consequence (docs/spec/pipeline.md): “Immutable def parameters keep their inferred type across label join points.” - [BOTH]
use()return values are stone: every module’s return value is automatically stoned (docs/spec/stone.md). Member reads through a module binding are stable for the whole program. - [DOC] Messages between actors are stoned before delivery.
- [BOTH] Closure captures are mutable (
counterpattern documented). Any slot written by a child closure cannot carry a stable type fact — this is whymark_closure_writesexists and why invoke-boundaries invalidate types. - [DOC] Functions have at most 4 parameters. Extra call arguments are ignored; missing arguments are null.
- [DOC] Truthiness: falsy is exactly
false,0,"",null. Everything else is truthy (this is thewary_true/wary_falsecoercion table). - [DOC]
+adds numbers or concatenates when BOTH operands are text; it never coerces mixed operands (disrupts).&is explicit concat with number→text coercion.|is null-selection (first non-null). - [DOC] Comparisons are strict, no coercion.
- [DOC] Logical ops
/\ \/ !short-circuit;&&/||are compatibility spellings of the same operators (tokenizer remap). - [DOC] Records: reading a missing/invalid key → null; writing an invalid key
disrupts; assigning null to a field deletes the key. Arrays: reading an
invalid index → null; writing an invalid index disrupts; valid indices are
integers ≥ 0.
a[] = vis push;var v = a[]is pop. - [DOC] Function property access is the proxy pattern: a function with two
params
(name, args)receives dot-method calls (f.hello(1)→f("hello", [1])). Bracket access on a function disrupts. This is why every property-access ladder has anis_funcarm — a lean emitter can only drop it when the receiver is provably not a function. - [DOC]
disruptcarries no value.disruptionblocks attach per function. Unhandled disruption crashes the actor. - [DOC]
go f(...)is an explicit tail call; functions with disruption handlers cannot tail-call (handler frame must remain). - [DOC] Identifiers may contain
?and!mid-name and as suffix.
2. Value model constraints (docs/spec/values.md, dec64.md)
- Numbers: immediate 31-bit ints, short floats (8-bit exponent); out-of-range
arithmetic produces null, not a disrupt (documented: “Arithmetic on
numbers that cannot be represented produces null”;
1/0is null). Any typed arithmetic fast path must preserve the null-producing overflow/divide behavior.is_fit= 56-bit integer check. - Short ASCII strings (≤7 chars) are immediates; type checks on text cannot assume heap pointers.
- No
undefined. Uninitialized is a VM-internal tag, never script-visible.
3. The pretext/stone contract (docs/spec/stone.md) — load-bearing
- Text
+lowers tolength a; length b; add cap; pretext dest,cap; append dest,a; append dest,b. Self-append fast path: when dest is the left operand and provably already pretext, emit onlyappend dest, right. - Safety invariant: a pretext is uniquely referenced by exactly one slot.
stone slot, slotmust be inserted before any instruction that exposes a pretext to a script-visible boundary or creates a second reference: return, call argument (setarg), closure capture/read, store key/value, push, put,is_stone, or a move whose source stays live. Script-visible text must be stone. concat dest, a, bis legal early IR but NOT a VM primitive — the mach lowerer rejects it (mach.c:3531). Some pass must lower concat before the VM or the QBE emitter sees it. In a resolve/emit design the emitter should emit the lowered form directly.- Stone insertion needs last-use liveness (with backward-jump extension for loops); it is one of the few genuinely whole-function post-emit analyses.
4. Wire format / ABI constraints (mach.c lowering)
- A function is
{name, nr_args, nr_close_slots, nr_slots, abi_flags, disruption_pc, instructions}; instructions are arrays[op, operands..., line, col]plus bare-string labels._nop_*strings are tombstones; both are zero-width at lowering (no MachInstr32 emitted). - 255-slot hard cap per function (8-bit A/B/C operands); the lowerer errors
above it, and adds its own scratch slots for
eq_tol/ne_tol/key loads before re-checking. Any emitter must leave headroom or compact. - Jumps: symbolic string labels in mcode; the C lowerer resolves to pc-relative offsets (16-bit signed conditional, 24-bit unconditional). Constant pool index is 16-bit; ints in [-32768, 32767] lower to inline LOADI. Labels make insert/delete/reorder cheap for the compiler.
abi_flags & 1= args start at slot 0.disruption_pcis an instruction index that every inserting/deleting pass must maintain.- mach handlers keep their own dynamic int/float dispatch (e.g. MACH_ADD
fast-paths both-int, else float path). Compiler-emitted
is_*guards exist for language semantics and error quality, not memory safety — in the VM. In AOT-unboxed native code that suspenders-and-belt property disappears; soundness of type facts becomes safety-critical there. - [STALE] docs/spec/mcode.md says instructions don’t carry line/col (debug sidecar instead). In-memory instructions DO carry trailing line/col through the whole pipeline; they are stripped only when the unit artifact is written (compiler.cm mcode_without_locations) and the sidecar is derived from them.
4b. The direct call op — the one variable-length instruction
["call", dest, fn, argc, arg0..argN-1, line, col](andtail_call, its tail-position twin) is the only mcode instruction whose operand count is not determined by its opcode: it is3 + argc, with position 3 an integer count, not a slot number. It replaces theframe/setarg×N/invokesequence (2 + argc instructions → 1).- Every generic operand walker must special-case it. The default fallbacks
in streamline (
get_slot_refs/get_slot_defs/get_slot_uses) treat all numeric positions as slots; without the special case, compress_slots remaps the argc operand as if it were a slot and the lowerer misreads the instruction. The same applies to compiler.cm’s location stripping and mach.c’s line/col extraction (mcode_operand_countreturns −1 for call; the lowerer computes3 + argcitself). Grep fortail_callwhen adding any new pass that touches operands positionally. - Refs: dest=1 (def), fn=2 (use), args=4..3+argc (uses). Named args escape at
the call boundary (insert_stone must stone live pretext args), same rule as
setarghad. - VM lowering:
MACH_CALL A=dest B=fn C=argcfollowed by ⌈argc/3⌉MACH_CALLARGSpayload words (three 8-bit slot indices per word; a stray CALLARGS reached by a jump disrupts). Register-to-register calls activate the callee frame directly in the CALL handler; C/native callees fall through to the INVOKE path.tail_calllowers identically tocalltoday (astail_invokelowers to plain INVOKE) — the distinction is preserved in mcode for a future backend that implements real tail calls. goframe/goinvoke(thegostatement) still use the decomposed form; so does nothing else — the emitter routes every synchronous call through emit_call.
5. What the AOT backend consumes today (pit-linker/qbe_emit.cm)
- Consumes the streamlined mcode stream directly: skips
_nop_, uses string labels, same terminator rules (return/jump/goinvoke/disrupt kill the block). - One native function per mcode function, signature
(ctx, frame_ptr); every slot read/write goes through frame memory (fp + slot*8) so frames stay GC-walkable;fpmust be refreshed after GC-triggering calls. - Resume segments: each invoke point gets a segment number; function entry
dispatches on
frame->addressto resume mid-function (actor turns). Constraint: slot state must be fully materialized in the frame at every invoke boundary — unboxing/registerization can only live between invokes. - It uses NO type facts: all truthiness/tag dispatch is open-coded in QBE IL per site. Carrying per-slot type facts in the unit format is the single biggest enabler for better native code.
5b. Guard necessity — what the VM actually enforces (mach.c handlers)
The single most important table for a lean emitter. In optimized builds the VM has NO type-guard machinery of its own (contract checks are debug-only). Per guard class, eliding the compiler’s ladder means:
| Guard before… | VM behavior without it | Guard is |
|---|---|---|
arithmetic (is_num) | int fast path, else ToFloat64 coercion; non-number → null result; the raised “cannot convert” disrupt is SWALLOWED (latent in current_exception) | Advisory (typed disrupt → silent null) |
| comparisons | totally defined for all type pairs; eq cross-type → false; lt coerces when LEFT is number, string-compares otherwise | Advisory |
jump_true/jump_false | STRICT equality with true/false; any non-bool falls through — silently wrong control flow | Compiler obligation |
container load | array/text OOB → null; wrong obj kind → record path → null | Advisory |
array store key is int | garbage tag bits read as index → runaway auto-grow → OOM | REQUIRED (safety) |
push/pop target array | raw PitArray* reinterpret of non-array — memory-unsafe | REQUIRED (safety) |
is_stone before store/push/append | mutation SUCCEEDS silently on stoned data — the VM store/push/append opcodes never check the S-bit | REQUIRED (correctness) |
| callee is function | FRAME disrupts “ | Advisory |
| arity | too many args → disrupt; too few → null-padded (defined behavior) | Advisory |
Other runtime semantics a typed emitter must preserve: div-by-zero → null
(never disrupts); modulo is floored (sign of divisor), remainder
truncated (sign of dividend); min/max return float and null on
non-numbers; overflowed int arith promotes to float, non-finite → null;
wary_* coercion via truthiness (objects always truthy); jump_empty
matches only the immediate empty-text sentinel, not heap empty strings;
APPEND coerces any appended value via Pit_ToString and does not check the
target’s stone bit; apply on a non-function returns the value unchanged.
Stone enforcement is entirely the compiler’s job. docs/spec/mach.md
claims writes to stoned objects disrupt — that is true only of the checked C
API (pit_array_set_checked etc.), which the mach STORE/PUSH/APPEND opcodes
do not call. Runtime immutability of arrays/records/pretext rests solely on
compiler-emitted is_stone ladders.
6. Front-end contract (parse/semantic_check/fold)
semantic_check enforces (parse.cm ~1400-2300; errors uncapped, parse
errors capped at 5; semantic_check skipped entirely if any parse error):
- No
$-prefixed declarations (endowments are reference-only). var/defcannot redeclare a const;defaftervarin the same scope UPGRADES the entry in place to const. Duplicate params are parse errors.- Assignment to unbound name / to any const errors — this is the whole
def-and-param immutability enforcement (params are
make:"input",is_const:true). - Const objects with known
type_tagget shape errors (property on array, push on non-array, text key on array, int key on record); unknown-tag consts get their tag INFERRED from access shape (.→record,a[]=→array, int subscript→array, text key→record). use(): must be assigned todef, at top level, exactly one text-literal argument; any other use() position errors.go: must be a call; not in a function with adisruptionclause; not in a function that defines inner functions. Stamps.tail.break/continueoutside a loop error. Return-of-call stamps.tail.- Forward refs are two-tier: parse emits a coarse warning (filtered out by
compiler.cm/mcode.cm), analysis.cm re-derives the precise error
(
name.forward_reference). Function STATEMENTS hoist; function EXPRESSIONS do not. - Unbound names must be in the fixed global set or
$-endowments; accepted ones are recorded inast.intrinsics. - Banned keywords with guidance: try/catch/finally/throw/class/new/this/ switch/case/let/const. (Note: mcode.cm still contains a switch emitter — dead code, since parse rejects the keyword.)
- Declarations only at function body level; never in for-initializers; max 4 params; all declarations initialized.
Scope schema (the fold/emitter interface, ast.scopes):
Per function (positionally indexed; global unshifted to index 0):
{function_nr, <name>: {make: var|def|input|function, function_nr, nr_uses, closure: bool, level, type_tag?}} plus nr_slots/nr_close_slots stamped
on function AST nodes. Loop scopes flatten into the enclosing function scope
(no block scoping). Closure capture detection: name resolution crossing a
function boundary sets closure=1 on the DEFINING var.
Desugarings (a source-faithful AST must re-home these):
- Prefix
++x/--x→assign{x, x±1}in parse (LOSSY for LSP); postfix stays a++/--node. Compound assignx op= y→assign{x, op{x,y}}. a[] = v/x = a[]are assign nodes with.push/.popflags.- Arrow fns →
functionnodes (.arrow), expression body wrapped in a synthetic return. Record method shorthand → function-valued pair. - Template literals: interpolations re-tokenized+re-parsed recursively;
node carries a
{0},{1}format string + expression list; emitter compiles it to a runtimeformat(fmt, [parts])call. - Default params:
a = exprORa | expr; compiled as an entry prologuejump_not_null— so an EXPLICIT null argument triggers the default. - Ternary →
thennode. No destructuring. Regex literals re-scan raw source and fast-forward the token cursor.
Tokenizer rules that carry language semantics:
/\→&&,\/→||(with&&/||accepted as compat spellings).//is floor-divide ONLY when heuristics say “operator position” (previous significant token can end an expression, not two-space-preceded, next char starts an operand); otherwise a comment.**lexes as two*.- Functino tokens (
+! -! *! /! %! <! >! <=! >=! =! !=! &&! ||! []!) lex as identifiers, parse as names withmake:"functino", and the emitter lowers them to direct opcodes (=!/!=!with 3 args → eq_tol/ne_tol;&&!/||!are EAGER and/or, unlike&&/||). - Bit-shift/bitwise ops are rejected with guidance toward
fit.*functions. - Newlines are ASI-significant:
expect_semiaccepts a crossed newline; return/go/break/continue stop at line end. - String escapes resolved at lex; template escapes kept raw and resolved at parse.
fold rules (all of this moves into resolve):
- Folds:
+ - * / %on number literals (div/mod-by-zero → null literal),+on two text literals,== != < <= > >=on numbers,==/!=ONLY on text,!bool, unary minus. Literal-condition ternary picks a branch. NOT folded://,&,&&&,+unary, text ordering comparisons. - Const-prop: def-only + literal initializer + non-captured + same function.
- Purity: literals, names, functions, pure-operand unary/binary/ternary,
array/record of pure parts, and calls ONLY to
is_*sensory intrinsics. - DCE: unused var/def/import/function (warning always, removal only if
initializer pure);
if(true/false)folds via full truthiness;whileis eliminated only for literalfalse/null— NOTwhile(0)/while("")(asymmetry to preserve or consciously fix). - Stamps:
arity(function statements and var-bound function exprs — NOT def-bound ones),type_tag(syntactic + intrinsic-constructor + usage inference),hint(array_length/text_length),is_*-on-known-type folds to bool.
7. Type system as implemented (streamline lattice + rules)
Lattice: unknown int float num text pretext bool null array record function blob. Merge (join) rules: int|float→num; num absorbs int/float;
text|pretext→text; any other disagreement→unknown. Subtype test (slot_is):
pretext counts as text; int/float count as num.
param_rules (backward constraints from uses): all arithmetic ops
constrain their operands to num; concat/concat_space→text×text; append→text;
and/or→bool×bool; push→arr operand array; pop→array; load with text key →
object is record; store classifies object record/array by value/key type;
is_* operands unconstrained. Constraints propagate through move chains to
a fixed point; only param slots (immutable) keep results across labels.
write_rules (produced types): is_*/compares/not/and/or→bool; arith→num with int-narrowing (both operands provably int AND op ∈ {add,subtract,multiply,integer_divide,remainder,modulo,max,min} — divide NEVER narrows); concat/pretext/append→pretext; character/lower/upper→text; array/record/function/context constructors→their type; access→literal’s type; length/move-from-unknown/load/pop/get/codepoint/invoke results→ unknown. Invoke-result typing machinery exists but is unpopulated — call results are always unknown (keep it that way or populate it consciously).
Forward tracking invalidation: slot_types reset to base (params +
write-invariants) at EVERY label (including _nop_ strings — a quirk);
closure-written slots additionally reset after every
invoke/tail_invoke/goinvoke/apply/call. stone on pretext/text produces
text. insert_stone’s own tracker never resets at labels (linear scan).
Type-check elimination catalog (what “guards only where unproven” must reproduce): is_null+jump_true/wary_true → jump_null (and false→ jump_not_null); known-match guard+branch both deleted (fall through) or jump made unconditional (taken); num-subsumes-int/float variants; known MISMATCH → guard deleted, branch → unconditional jump to the false arm; wary_true/false on a provably-bool slot downgrades to jump_true/false. Asymmetry: the jump_false path refines the checked slot’s type on fall-through; the jump_true path does not.
Implicit emitter obligation (found the hard way): simplify_booleans
fuses an ADJACENT not d,s + conditional branch on d into an inverted
branch and deletes the not, without checking whether d is live afterwards.
The classic emitter always computed ! into a throwaway temp, making this
sound by construction. Any emitter that writes not into a slot that is
both branched on and consumed as a value (e.g. a logical operator’s result
slot) must break the adjacency or keep the temp. Same class of hazard:
insert_stone’s move-alias rule treats move dest,src as aliasing unless
src is dead or the next instruction is null src (ownership transfer).
Convergence contract: the current optimizer runs its 9-pass cycle
exactly twice per function (hardcoded), then lowers concat, inserts stone,
inlines (2 cascade rounds max), compresses slots, compacts nops. _no_inline
means phase-1 only (no inline/compress/compact). A replacement owes
equivalent RESULTS, not this shape.
8. Guard/panic emission contract (mcode.cm)
Slot layout
Slot order: params (0..nr_args-1, always const) → closure locals (low
contiguous region so returned frames can be shortened) → plain locals →
temps (monotonic, never reused today). scan_scope assigns locals from
SORTED scope keys — deterministic layout. abi_flags is hardcoded 1
(args at slot 0). disruption_pc > 0 ⇔ handler exists; the handler region
is the tail [disruption_pc, end); a disrupt raised INSIDE the region
propagates to the caller (no self-recursion). Every function gets an
implicit null-return appended.
The null-vs-disrupt split (user-visible; preserve exactly)
- Containers are forgiving: bad/missing key, OOB read, non-int array key on READ → null. integer_divide is total → null on any bad input.
- Disrupting:
+/relational type mismatch, numeric-op non-number (“operands must be numbers”, ONE shared panic block per function), property read/call on a function (unless proxy), set_prop on non-record (“cannot set property on this value”), any mutation of stone (“cannot modify frozen object”), invalid SET key (“invalid property key”), push/pop on non-array-or-stoned, place arg non-integer. - Array set at
idx == lenis APPEND (allowed);idx > lendisrupts. Array GET atidx >= lenis null. (get/set asymmetry.) - Panic text contract:
+→ “cannot apply ‘+’: tried to addto ” where desc is “ ( )” for named operands; relational → “cannot apply ‘ ’: tried to compare …”. Panics emit via context log(“panic”, [msg]) then disrupt.
+, pretext, and the escape-stone rule
Known text×text → pretext/append (result stays PRETEXT, unstoned — enables
the self-append O(n) loop fast path when dest==left and already pretext).
&/&&& always stone (immutable result) — & coerces numbers via
textify. Known num×num → bare add. One-known-num → guarded numeric add.
Static mismatch → compile-time-shaped panic. Unknown×unknown → runtime
ladder (text-first, stones the text arm). ADDITIONALLY: every return and
every setarg emits a runtime is_text→stone snippet (emit_escape_text) —
ALL text crossing a call/return boundary is frozen. A lean emitter can drop
that snippet only where it can prove not-text or already-stone.
wary vs jump selection
User-written conditions (if/while/for/do, &&, ||, ternary, callback truth
tests) → wary_* (truthiness of any value). Compiler-produced booleans
(guard ladders, bounds, eq in switch) → jump_* (STRICT — non-bool falls
through, see §5b). &&/||/| produce the OPERAND value, not a coerced
bool; | uses jump_not_null.
Calls
frame(fr,fn,argc) + setarg×argc + invoke(fr,dest). argc = args actually
written; NO padding, NO arity check at emit (VM disrupts on over-supply,
null-pads under-supply). Args evaluated BEFORE the callee expression.
go → goframe/goinvoke (the only real tail calls — see §11). return-of-call
renames the last invoke to tail_invoke unless the function has a disruption
clause (and tail_invoke currently lowers as a plain invoke). Dot method
call: is_func receiver + length(receiver)==2 → proxy call
recv(name, [args]); other function receivers panic “cannot call property
on non-proxy function”; record receivers do get_prop then plain call — no
this binding. Bracket calls never proxy-dispatch. Inline callback
intrinsics (arrfor/filter/find/reduce) probe length(fn) at runtime for
0/1/2-arg dispatch unless the callback literal’s arity is known at emit.
log must be log.channel(...); emitted lazily behind a
context.$log_enabled(channel) test.
Intrinsics lowered to opcodes (not calls)
All is_* sensory predicates (21 of them), length (guarded 5-type), stone,
abs/sign/fraction/integer/neg, modulo/remainder/max/min,
floor/ceiling/round/trunc (place-checked), codepoint/lower/upper/character,
apply, push/pop forms, regexp literals. Everything else is a real call
through the context.
Names
Locals: no instruction on read (slot IS the value); move on write.
Closure vars: get/put with (parent_slot, level). Globals/intrinsics:
context + access(name) + load, with a ≤64-entry per-function preload cache.
Static use() bindings: context-load of the resolver-provided name.
Assignment to an unbound/global name silently evaluates the RHS and drops
the store — no error at emit or runtime. (Semantic_check catches most
cases first; the emitter behavior remains for what slips through.)
Emit-time typing is UNSOUND today (confirmed miscompile)
s_slot_types persists across branches and loops with no invalidation at
merges/labels; ++/– and closure put skip retagging. Live repro:
var x = 1; if (c) x = "a"; return x + x → f(false) returns “11”
(int coerced through the emitted concat path); the mirror case emits a bare
add on runtime text → swallowed VM disrupt → null → downstream crash. A
replacement emitter must attach types to bindings with write-merge
semantics (or clear var tags at merges); it must NOT inherit this tracker.
9. Transformations inventory (inlining, closures, slots)
inline_calls (if reimplemented, this is the current contract)
Eligible: same-module callees only (resolved via function-instr slot map);
NO closures (nr_close_slots>0, any get/put, any nested function instr all
disqualify); NO disruption handler; ≤40 instructions unless the callee slot
is single-use (then unlimited size); ≤20 inlines per caller; aborts on
overlapping frames or missing invoke. Mechanics: params bound by SLOT
ALIASING onto caller arg slots (no moves); unmapped params get an explicit
null-init (compress_slots interaction); labels prefixed _inl{N}_;
return x → move+jump-to-continuation. KNOWN GAP: does not maintain
disruption_pc (safe only because handler-owning callees are rejected).
Cascade: one round over everything, re-optimize changed functions, one more
round on those, done. The native/sensory inline path is dead code.
insert_stone (mandatory semantics — the §3 invariant)
Stones exactly-pretext slots (in place, stone s,s) BEFORE: setarg(3),
put(1), push(2), return(1), store value(2) AND key(3), is_stone(2),
setfield/setindex(3); before a move only if the source is pretext and
still live after the move (alias rule); at function ops for every
closure_read captured pretext slot; after any instruction that defines a
closure_read slot to pretext. Liveness = last-ref per slot extended across
backward jumps to a fixed point. Maintains disruption_pc.
Closure marking/seeding
mark_closure_writes: child get → ancestor.closure_read[slot], child put →
ancestor.closure_written[slot] (walking the parent map by level).
resolve_closure_types seeds child get result types ONLY from the
ancestor’s write-invariant types; closure-written slots are forced unknown;
unanalyzed ancestors seed nothing (degrades safe). Order: main first, then
functions in index order.
compress_slots invariants
Params pinned (identity-mapped); captured slots pinned with whole-function lifetime; liveness = first/last APPEARANCE (defs∪uses, conservative) with back-edge extension; linear scan reuses lowest free phys slot ≥ nr_args; child get/put parent-slot operands rewritten through the ancestor’s remap; nr_close_slots recomputed from the highest remapped captured slot. Runs after inlining, before final nop compaction. (With an emit-time temp free list, this pass becomes a >255-slot fallback only.)
lower_concat / lower_concat_space expansions
concat → length,length,add,pretext,append,append(,move if dest∈operands).
concat_space → same plus: cap+1, append left, jump_empty(left)→skip,
jump_empty(right)→skip, append " “, skip:, append right — i.e. the
separator is omitted if EITHER side is empty. Stone deliberately NOT baked
in (insert_stone freezes only on real escape). The VM and lowerer both
hard-reject a raw concat — some stage must always lower it.
10. Cross-phase metadata contract
Consumed by the emitter: ast.scopes (+per-var make/closure — the authoritative slot layout), callee.intrinsic, callee.make==“functino”, node.level (0/-1 local-or-global, >0 closure depth), access_kind (index/field/dynamic ladder selection), stmt.tail, func.disruption, function_nr, func.intrinsics (preload cache), hoisted function lists, node.push/.pop, expr.postfix, param.expression (defaults), ast._static_bindings (use() resolution; unresolved → error diagnostic), ast._parse_diagnostics (merged, minus coarse forward-ref warnings).
Computed by fold but NEVER read by the emitter: type_tag, pure, hint, arity. (fold consumes its own type_tag/pure internally; hint and def-bound-function arity are written and never used anywhere.) A resolve pass replaces this whole side-channel with one symbol table.
Optimizer side-channels: ir._warn (diagnose mode), ir._no_inline,
ir._diagnostics, ir._module_summaries, ir._debug_slots/_compress_log,
ir._parent_of/_parent_fc, func._write_types, func._closure_slot_types,
func.closure_read/closure_written, ir.compiler.streamline.compacted.
Stream conventions: labels are bare strings; tombstones are
_nop_<pass>_<n> strings (pass mnemonics: tc bl mv ur ucfg dj); label
counter is monotonic program-wide; final compaction strips nops once.
11. Known doc-vs-code divergences
- docs/spec/streamline.md lists 10 passes; the code runs ~16 pass functions, a hardcoded 2× cycle, an inline cascade, and register allocation. The spec’s “reverted as buggy” claims (copy propagation, move-type resolution) describe code that is live.
- docs/spec/pipeline.md’s test-file table references parse_test.ce, fold_test.ce, mcode_test.ce, streamline_test.ce, qbe_test.ce — these do not exist under those names; tests/compile.cm and tests/analysis.cm are the living compiler tests, plus tests/vm_suite.ce and the fuzzer.
- docs/spec/mcode.md line/col note — see §4.
- streamline’s simplify_algebra “constant value tracking” populates a table that nothing reads (dead scaffolding); the documented “algebraic identity” rewrites (add 0, multiply 1) are listed in pipeline.md but the trigger in code only fires on same-slot compares.
- docs/spec/mach.md “invalid array indexes and writes to immutable objects disrupt” and “arithmetic disrupts on invalid operations” are FALSE for the live opcodes: array store auto-grows, loads return null, stone is not checked by STORE/PUSH/APPEND (§5b), arithmetic coerces or yields null with the conversion disrupt swallowed. The docs describe the checked C API.
tail_invokeis lowered as a plaininvoke(mach.c) — it is a MARKER, not a tail call. Real tail calls are onlygoframe/goinvoke(thegostatement). mcode.md documents goinvoke but not tail_invoke.jump_true/jump_falsestrictness (fall-through on non-bool) and thewary_*opcodes are undocumented in mcode.md; its instruction tables omit ~30 live opcodes (remainder, min/max, rounding family, pretext/append, apply, context, most is_* predicates, integer_divide, jump_empty).- parse.cm bans the
switchkeyword, yet mcode.cm contains a full switch emitter — dead code. - mcode.cm’s
recordliteral instruction bypasses add_instr (no line/col);s_data(the mcode-level constant pool) is always empty — constants are inline and pooled later by the C lowerer. - CONFIRMED MISCOMPILE (2026-07-07, see §8): branch-poisoned emit-time slot
tags.
var x = 1; if (c) x = "a"; return x + x— f(false) = “11”; mirror case crashes. The opt-vs-noopt fuzzer cannot catch emitter bugs (both sides share mcode.cm); gates for a new emitter need direct tests or cross-compiler differentials. (FIXED 2026-07-07: merge/clear semantics in mcode.cm + six vm_suite regressions.) - Emit-time temp recycling is a TRAP with the current register allocator: compress_slots liveness is first-to-last APPEARANCE per slot, so recycled slots get merged ranges the linear scan cannot split — statement-level recycling pushed parse_primary from <255 to 352 required slots and broke the mach lowering during seed. Slot reuse at emit needs real def/use interval liveness in the allocator first (C3).
- Artifact hashes are non-canonical: code_hash/debug-sidecar hashes are blake2 over json.encode(record), and record key order follows internal iteration order (allocation/interning history), not a canonical order. Identical content can hash differently across compiler-internal changes — observed when removing the double parse (sidecar keys reordered, hash changed, content identical). Reproducible builds need sorted-key or canonical (nota/wota) serialization at every hash site.
Source: plans/archive/perf-2026-07/compiler-contract.md