Files
AILang/design/contracts/0018-check-codegen-boundary.md
T
Brummel a378dad0aa docs(design): ratify the check→codegen boundary (mir.5, typed-MIR close)
mir.5 is the typed-MIR milestone's closing iteration. Its CODE half had
already converged before the iteration began, so mir.5 ships no code —
it ratifies into the design/ ledger the boundary the code already holds.

Verified at iteration entry (empirically, not from the spec sketch):
  - all four named re-derivers grep-clean in codegen: synth_with_extras,
    synth_arg_type, type_home_module, the second infer_module_with_cross;
  - lower_workspace takes &MirWorkspace (codegen consumes MIR);
  - MTerm::New is unreachable!() — raw-buf.4 desugars Term::New to
    (app T.new …) before codegen, so there is no element-type
    re-derivation left to relocate;
  - #51 / #53 (the element-type / new-T codegen crashes) are closed;
    their residue was fixed by ee4107c / 420f75f plus the New-desugar,
    not by a separate raw-buf patch track.

Ledger work (the mir.5 deliverable):
  - NEW design/contracts/0018-check-codegen-boundary.md: the invariant
    "codegen re-derives nothing; MIR is total over what check proved;
    a codegen arm that recomputes a fact instead of reading MIR is
    drift." Ratifier: lower_to_mir_ty.rs::callee_classification_builtin_and_static.
  - 0013-typeclasses invariant 2 retracted: codegen no longer re-resolves
    cross-module names via an import_map fallback; lower_to_mir::classify_callee
    resolves the reference once into Callee::Static and codegen consumes it.
  - 0003-pipeline.md: the "lower to MIR" line names the real stage
    (elaborate_workspace → MirWorkspace → lower_workspace) and a new
    paragraph states the boundary, cross-referencing 0018.
  - INDEX.md: boundary contract row added; qualified-xref re-pointed at
    lower_to_mir + 0018.
  - codegen_import_map_fallback_pin.rs doc-comment made honest — it pins
    the post-mono AST precondition classify_callee relies on, not a
    codegen-side resolution that no longer exists. Assertions unchanged;
    the test stays green.
  - spec 0060 gains a mir.5 refinement note recording the early code
    convergence.

Acceptance criteria 1-7 of docs/specs/0060-typed-mir.md are all met.
Full workspace suite green (exit 0, 0 failed, 2 ignored); 708 passed
carried from mir.4 (no test added or removed).

Not done here (deliberately): the milestone #7 (raw-buf) subsumption
note is an external Gitea tracker write; the /boss auto-mode classifier
declined it as an unauthorised external write and it is surfaced to the
user rather than worked around. The end-to-end milestone fieldtest
remains the deliberate manual close-gate before the tracker milestone
is marked done.

refs #51 #53
2026-06-01 01:34:37 +02:00

3.2 KiB

Check→codegen boundary

Check→codegen boundary

check produces a single elaborated, typed artefact — a MirWorkspace — that is total over every fact codegen needs. Codegen consumes that artefact and re-derives nothing: it performs no type synthesis, no callee resolution, no uniqueness inference. Every decision codegen makes reads a field MIR already carries; there is no recompute-or-guess fallback path.

The producer is ailang-check::lower_to_mir, driven by elaborate_workspace; the consumer is ailang-codegen::lower_workspace, whose public entry points take &MirWorkspace. The build path is

Workspace → elaborate_workspace → MirWorkspace → lower_workspace → LLVM IR

lower_to_mir re-enters check's own canonical synth on the post-mono AST, so the MIR each node carries is the same judgement check proved, not a parallel re-derivation. This is the source of the totality guarantee: a fact in MIR is a fact check ratified.

What MIR carries (the totality)

MIR is structurally complete over all 17 Term variants (ailang-core/src/ast.rs), and each node carries the annotation classes codegen formerly re-derived:

  • ty on every node — the synthesised type. Codegen reads layout, drop shape, and mangling off it.
  • Callee on every AppStatic { module, fn_name, sig } / Builtin { name, sig } / Indirect. The static target is resolved in lower_to_mir; codegen reads the identity, it does not walk a callee ladder.
  • Mode + consume — per-arg modes on MArg and the per-binder consume_count on MirDef.consume. Drop placement reads these; the modes/linearity contract is memory-model.
  • StrRep on every Str literal — Static (constexpr GEP into rodata) / Heap (promoted via str_clone). The producer decides the representation; codegen emits what the rep says (see str-abi).

What codegen no longer holds

The re-derivers that made codegen a second, weaker type pass are gone and stay grep-clean: synth_with_extras, synth_arg_type, type_home_module, and codegen's own infer_module_with_cross run. Cross-module name resolution in particular is resolved once, in lower_to_mir, into Callee::Static — codegen does not re-resolve it (the retraction recorded in typeclasses, § "Cross-module references in synthesised bodies", invariant 2).

Failure mode this contract names

A codegen arm that recomputes a fact instead of reading it off MIR is drift, even when it computes the right answer — it reintroduces the weaker parallel pass this boundary exists to delete, and the next post-mono inconsistency it silently tolerates is a latent miscompile. The architect agent walks codegen for type synthesis, callee-ladder walks, and uniqueness inference during drift review; any reappearance is a regression against this contract, not a local style choice.

The pipeline view of where this stage sits is pipeline.