spec: typed-mir — place lower_to_mir post-mono, correct one-engine framing

plan-recon flagged that monomorphise_workspace runs between check and
codegen, and codegen lowers the post-mono workspace. The first draft of
this spec placed lower_to_mir as the final phase of check_workspace
(pre-mono) and claimed "nothing re-walks the AST". That is wrong on
both counts:

- Built pre-mono, MIR would not carry the monomorphic specialisations
  monomorphise_workspace appends — exactly the post-mono bodies codegen
  emits today — reintroducing the check↔codegen gap this milestone
  exists to close.
- synth is a pure `&Term → Type` function and the authoring AST carries
  no node-ids, so MIR cannot be a side-table read off a check pass; it
  is built inline by a walk. The honest property is therefore not "no
  second walk" but "no second *engine*": codegen's synth_with_extras is
  a hand-copied mirror of synth that drifts (every raw-buf bug is a
  drift point). The milestone replaces the mirror with a single
  post-mono call to canonical synth, materialised as MIR.

Corrections: architecture diagram and data flow now run
check → lift → monomorphise → lower_to_mir; lower_to_mir is wrapped by
a new front-end entry elaborate_workspace that subsumes the lift+mono
orchestration the CLI does today; check_workspace stays for the
diagnostics-only path (`ail check`). Node count fixed 27 → 17 Term
variants (ast.rs:436).

Re-dispatched grounding-check after the edit (post-PASS edit
invalidates the prior report) — PASS, all load-bearing post-mono
assumptions ratified against live code (main.rs build order,
mono.rs synth use, 17-variant Term, no node-ids).
This commit is contained in:
2026-05-31 13:21:53 +02:00
parent 207c63649f
commit 8bc2972594
+54 -23
View File
@@ -78,10 +78,10 @@ exists (`crates/ailang-codegen/Cargo.toml`), so no cycle is created;
`ailang-core` (for `Type`).
```
.ail.json ─ parse ─ resolve ─ desugar ─ typecheck ─┐
│ lower_to_mir (NEW, in ailang-check)
AST + type env ───┴──▶ MirModule ──▶ emit LLVM IR
(typed, mode/callee/rep-annotated) (codegen: reads, derives nothing)
.ail.json ─ parse ─ resolve ─ desugar ─ typecheck ─ lift ─ monomorphise ─
│ lower_to_mir (NEW, in ailang-check)
monomorphised AST ─────────────────────────── ┴──▶ MirModule ──▶ emit LLVM IR
(typed, mode/callee/rep-annotated) (codegen: reads, derives nothing)
```
- The **authoring AST** (Form A, content-addressed, hashable) is
@@ -89,10 +89,27 @@ exists (`crates/ailang-codegen/Cargo.toml`), so no cycle is created;
authored, never hashed, never round-tripped. This preserves the
authoring contract: an author writes no type the checker already
infers (the feature-acceptance redundancy bar).
- `lower_to_mir` runs as the final phase of `check_workspace`, after
typecheck + lift, reusing the type environment the checker already
built. On success it yields a `MirWorkspace`; on type error it
yields the existing `Vec<Diagnostic>` and no MIR.
- `lower_to_mir` runs **after monomorphisation**, on the post-mono
workspace — the same artefact codegen lowers today. It is *not* a
phase of `check_workspace` (which stops at typecheck, pre-mono): if
MIR were built pre-mono it would not carry the monomorphic
specialisations `monomorphise_workspace` appends and codegen emits,
reintroducing the very check↔codegen gap this milestone closes. The
build path is wrapped by one front-end entry point,
`elaborate_workspace`, that runs check → lift → mono → lower_to_mir
and yields a `MirWorkspace` on success or the existing
`Vec<Diagnostic>` on a type error.
- **One type engine, not two.** `lower_to_mir` walks the post-mono AST
with check's own canonical `synth` to fill `ty` on every node. This
is still a type walk — `synth` is a pure `&Term → Type` function and
the AST carries no node-ids, so MIR cannot be a side-table read; it
is built inline by walking. What the milestone removes is not the
walk but the *second engine*: codegen's `synth_with_extras` is a
hand-copied mirror of `synth` that drifts from it (every raw-buf bug
is a drift point). Replacing the mirror with a single post-mono call
to the canonical `synth`, materialised as MIR, deletes the drift
class structurally — there is no longer a second implementation to
disagree.
- `lower_workspace*` in codegen take `&MirWorkspace` instead of
`&Workspace`. The `Emitter` fields and methods that re-derive
(`locals`' carried `Type`, `synth_with_extras`, `type_home_module`,
@@ -190,9 +207,11 @@ pub enum Mode { Owned, Borrow }
// before — crates/ailang-check/src/lib.rs:1283
pub struct CheckedModule { pub symbols: IndexMap<String, (Type, String)> } // signatures only
// after — check_workspace yields MIR on success
// after — one front-end entry wraps check → lift → mono → lower_to_mir
pub fn elaborate_workspace(ws: &Workspace) -> Result<MirWorkspace, Vec<Diagnostic>>;
// (check_workspace stays for the diagnostics-only callers; elaborate_* is the build path)
// runs the full front end and lowers the POST-mono workspace to MIR.
// (check_workspace stays for the diagnostics-only callers — `ail check`,
// which stops at typecheck; elaborate_workspace is the build path.)
```
**codegen boundary** (`ailang-codegen`):
@@ -217,25 +236,37 @@ pub fn lower_workspace(mir: &MirWorkspace) -> Result<String>;
`Callee` / `Mode` / `StrRep` / `MirWorkspace`. Leaf; depends only on
`ailang-core`.
- **`ailang-check::lower_to_mir` (new module):** walks the
typechecked, lifted AST with the live type environment and emits
**monomorphised** AST with check's canonical `synth` and emits
`MTerm`, filling `ty` on every node. Later iterations fill `callee`
(mir.2), `mode` / `consume_count` (mir.3), `rep` (mir.4).
- **`ailang-check::elaborate_workspace` (new entry):** the single
front-end function — runs `check_workspace` (diagnostics gate),
`lift_letrecs`, `monomorphise_workspace`, then `lower_to_mir` on the
post-mono workspace. Returns `MirWorkspace` or the diagnostics. This
moves the mono orchestration out of the CLI and behind one boundary.
- **`ailang-codegen` (rewired):** entry points take `&MirWorkspace`;
the four re-derivers are removed iteration by iteration as their
annotation lands in MIR.
- **CLI (`crates/ail/src/main.rs`):** the build path calls
`elaborate_workspace` and threads `MirWorkspace` into
`lower_workspace*`. `check` / `emit-ir` / diagnostics paths
unchanged where they only need diagnostics.
`elaborate_workspace` (which now subsumes the explicit `lift` + `mono`
steps the CLI orchestrated) and threads `MirWorkspace` into
`lower_workspace*`. `check` / diagnostics paths stay on
`check_workspace`; the `emit-ir` path builds MIR via the same
front-end entry.
## Data flow
`Workspace (AST)``typecheck` (unchanged) → `lift` (unchanged) →
`lower_to_mir` (type env → `MirWorkspace`) → `lower_workspace`
(`MirWorkspace` → LLVM IR text) → `clang`. The type environment that
`synth` builds inside check is consumed in-process by `lower_to_mir`
rather than discarded; nothing re-walks the AST for types after MIR
exists.
`monomorphise` (unchanged) → `lower_to_mir` (post-mono AST + canonical
`synth``MirWorkspace`) → `lower_workspace` (`MirWorkspace` → LLVM IR
text) → `clang`. The single type-inference engine is check's `synth`:
`lower_to_mir` calls it once over the post-mono bodies to materialise
MIR, and codegen reads the result rather than running its own
`synth_with_extras` mirror. The remaining `synth` walk inside
`typecheck` (pre-mono, for diagnostics) and the `lower_to_mir` walk
(post-mono, for MIR) are not redundant — they run on different
workspaces for different purposes, and both are the *same* canonical
engine, never a divergent copy.
## Error handling
@@ -267,10 +298,10 @@ is complete ⇒ codegen cannot fall through to an `UnknownVar`).
## Iteration decomposition
Strategy: MIR is structurally complete over all 27 node kinds from
mir.1 (codegen consumes it fully — no AST/MIR dual path). Each
iteration adds **one** annotation class and deletes the matching
codegen re-derivation.
Strategy: MIR is structurally complete over all 17 `Term` variants
(`crates/ailang-core/src/ast.rs:436`) from mir.1 (codegen consumes it
fully — no AST/MIR dual path). Each iteration adds **one** annotation
class and deletes the matching codegen re-derivation.
| Iter | Adds to MIR | Deletes from codegen | Acceptance |
|---|---|---|---|