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] def bindings cannot be reassigned; var can. All declarations are function-body level (never inside if/while/for/do blocks) 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 (counter pattern documented). Any slot written by a child closure cannot carry a stable type fact — this is why mark_closure_writes exists 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 the wary_true/wary_false coercion 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[] = v is 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 an is_func arm — a lean emitter can only drop it when the receiver is provably not a function.
  • [DOC] disrupt carries no value. disruption blocks 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/0 is 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 to length 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 only append dest, right.
  • Safety invariant: a pretext is uniquely referenced by exactly one slot. stone slot, slot must 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, b is 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_pc is 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] (and tail_call, its tail-position twin) is the only mcode instruction whose operand count is not determined by its opcode: it is 3 + argc, with position 3 an integer count, not a slot number. It replaces the frame/setarg×N/invoke sequence (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_count returns −1 for call; the lowerer computes 3 + argc itself). Grep for tail_call when 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 setarg had.
  • VM lowering: MACH_CALL A=dest B=fn C=argc followed by ⌈argc/3⌉ MACH_CALLARGS payload 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_call lowers identically to call today (as tail_invoke lowers to plain INVOKE) — the distinction is preserved in mcode for a future backend that implements real tail calls.
  • goframe/goinvoke (the go statement) 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; fp must be refreshed after GC-triggering calls.
  • Resume segments: each invoke point gets a segment number; function entry dispatches on frame->address to 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 itGuard 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)
comparisonstotally defined for all type pairs; eq cross-type → false; lt coerces when LEFT is number, string-compares otherwiseAdvisory
jump_true/jump_falseSTRICT equality with true/false; any non-bool falls through — silently wrong control flowCompiler obligation
container loadarray/text OOB → null; wrong obj kind → record path → nullAdvisory
array store key is intgarbage tag bits read as index → runaway auto-grow → OOMREQUIRED (safety)
push/pop target arrayraw PitArray* reinterpret of non-array — memory-unsafeREQUIRED (safety)
is_stone before store/push/appendmutation SUCCEEDS silently on stoned data — the VM store/push/append opcodes never check the S-bitREQUIRED (correctness)
callee is functionFRAME disrupts “ is not a function” itselfAdvisory
aritytoo 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/def cannot redeclare a const; def after var in 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_tag get 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 to def, at top level, exactly one text-literal argument; any other use() position errors.
  • go: must be a call; not in a function with a disruption clause; not in a function that defines inner functions. Stamps .tail.
  • break/continue outside 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 in ast.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/--xassign{x, x±1} in parse (LOSSY for LSP); postfix stays a ++/-- node. Compound assign x op= yassign{x, op{x,y}}.
  • a[] = v / x = a[] are assign nodes with .push/.pop flags.
  • Arrow fns → function nodes (.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 runtime format(fmt, [parts]) call.
  • Default params: a = expr OR a | expr; compiled as an entry prologue jump_not_null — so an EXPLICIT null argument triggers the default.
  • Ternary → then node. 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 with make:"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_semi accepts 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; while is eliminated only for literal false/null — NOT while(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 == len is APPEND (allowed); idx > len disrupts. Array GET at idx >= len is null. (get/set asymmetry.)
  • Panic text contract: + → “cannot apply ‘+’: tried to add to ” 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_invoke is lowered as a plain invoke (mach.c) — it is a MARKER, not a tail call. Real tail calls are only goframe/goinvoke (the go statement). mcode.md documents goinvoke but not tail_invoke.
  • jump_true/jump_false strictness (fall-through on non-bool) and the wary_* 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 switch keyword, yet mcode.cm contains a full switch emitter — dead code.
  • mcode.cm’s record literal 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