plan: mir.1b — codegen consumes MIR (the atomic switch)
Second half of spec iteration mir.1, planned against the landed mir.1a
infrastructure (135f4ce). This is the project's largest single
mechanical change and it is atomic: codegen's entire lowering walk
flips from &Term to &MTerm with no compiling intermediate (dual-path is
spec-forbidden). plan-recon proved the blast radius with a compile stub
(flipping only lower_term's signature → 58 cascading errors across
lib.rs/drop.rs/match_lower.rs/escape.rs/lambda.rs, baseline restored
green). The plan gives a Term→MTerm correspondence rule for the
mechanical arm renames (the build gate is the totality checker), exact
before→after for the judgement sites (the 9 synth_arg_type reads → ty(),
the dual-source Emitter, escape pointer-identity, entry-point
signatures, CLI diagnostics), and a satisfiable gate per task.
Four recon open-questions resolved by orchestrator judgement (within
the approved spec's intent; the spec's MirModule sketch is illustrative
and exact shape is the planner's):
- OQ1: MirModule gains `ast: Module` — codegen reads structural data
(Type/Const/ctor/intrinsic markers/symbol tables) from .ast, typed fn
bodies from .defs. MirWorkspace stays the single artefact (it now
contains the post-mono AST); structural reads were never a
re-derivation, so this does not reintroduce one. Also resolves the
intrinsic-fn gap (lower_module skips intrinsic bodies; codegen
iterates ast.defs and looks up MirDef bodies by name).
- OQ2: emit_fn borrows mir_def.body:&MTerm once and shares it between
escape::analyze_fn_body and lower_term (pointer identity is only
stable within one borrowed tree); casts flip to *const MTerm.
- OQ3: MTerm::New's codegen arm stays unreachable! — New is desugared
away before codegen (RawBuf→payload, monomorphic user-ADT new→dotted
App), exactly as Term::New is today; #51 builds unchanged.
- OQ4: the CLI build/emit-ir paths drop their separate check_workspace
call and let elaborate_workspace's Err drive diagnostics (one check,
not two); `ail check` stays on check_workspace.
- emit_ir keeps &Module, calls elaborate_workspace internally,
converts Err(diags)→CodegenError; its builtin-only in-source tests
check clean without a prelude.
Task structure: Task 1 (additive) extends MirModule + populates ast +
adds codegen's ailang-mir dep, gate cargo test --workspace. Task 2 (the
atomic flip) converts the whole codegen crate including its tests, gate
cargo test -p ailang-codegen (deliberately excludes crates/ail so the
not-yet-threaded external callers don't fail this gate). Task 3 threads
the crates/ail callers (CLI build/emit-ir/staticlib + 6 e2e callers),
gate cargo test --workspace == 102 ok (the mir.1a baseline), #51/#53
build+run, synth_with_extras/synth_arg_type grep-clean.
refs #49
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
# mir.1b — codegen consumes MIR (the atomic switch) — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/0060-typed-mir.md`
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement`
|
||||
> skill to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** Flip the entire codegen lowering walk from `&Term` to
|
||||
`&MTerm`, delete `synth_with_extras` + `synth_arg_type` (the type
|
||||
re-derivation mirror), switch their 9 call sites to read `MTerm::ty()`,
|
||||
move the public `lower_workspace*` entry points to take `&MirWorkspace`,
|
||||
and switch the CLI / test build paths to `elaborate_workspace` — so
|
||||
codegen re-derives **no types**. The three other re-derivers
|
||||
(`type_home_module`, `is_static_callee`, the 2nd `infer_module_with_cross`)
|
||||
STAY for mir.1b; they leave in mir.2/mir.3.
|
||||
|
||||
**Architecture:** This is the second half of spec iteration mir.1,
|
||||
planned against the landed mir.1a infrastructure (commit 135f4ce). It
|
||||
is the project's **largest single mechanical change** and it is
|
||||
**atomic**: there is no compiling intermediate between "codegen walks
|
||||
`&Term`" and "codegen walks `&MTerm`" (dual-path is spec-forbidden).
|
||||
The recon proved this — flipping only `lower_term`'s signature yields
|
||||
58 compiler errors cascading through `lib.rs` / `drop.rs` /
|
||||
`match_lower.rs` / `escape.rs` / `lambda.rs`. The compiler is therefore
|
||||
the totality checker for the mechanical arm-by-arm rename; this plan
|
||||
gives a **correspondence rule** for the mechanical flips, **exact
|
||||
before→after** for the judgement sites (the 9 synth sites, the
|
||||
dual-source Emitter, the escape pointer-identity, the entry-point
|
||||
signatures, the CLI diagnostics), and a build gate per task.
|
||||
|
||||
**Tech Stack:** `ailang-mir` (one field added), `ailang-check`
|
||||
(`lower_module` populates it), `ailang-codegen` (the bulk flip),
|
||||
`crates/ail` (CLI build paths + e2e callers).
|
||||
|
||||
---
|
||||
|
||||
## Design decisions baked into this plan (orchestrator, resolving recon's open questions)
|
||||
|
||||
The recon (read-only) surfaced four design ambiguities. All four are
|
||||
resolved here by orchestrator judgement, within the approved spec's
|
||||
intent (the spec's MirModule sketch is illustrative; the spec mandates
|
||||
removing the *re-derivers*, and exact MIR shape is the planner's to
|
||||
fix). None is a user fork.
|
||||
|
||||
- **OQ1 — Emitter dual-source → `MirModule` carries the AST module.**
|
||||
The Emitter reads `self.module.defs` (AST) for `Def::Type` drop-fns,
|
||||
`Def::Const`, intrinsic-`new` markers, and the pass-1 symbol tables
|
||||
(`module_user_fns` / `module_def_ail_types` / `module_ctor_index` /
|
||||
`module_consts`), but each fn **body** must now come from
|
||||
`MirDef.body` (the typed `MTerm`). `MirModule` (landed mir.1a) carries
|
||||
only fn bodies. **Resolution:** add one field — `MirModule.ast:
|
||||
Module` — so codegen reads structural data from `.ast` and typed
|
||||
bodies from `.defs`. `MirWorkspace` stays the **single** artefact
|
||||
codegen consumes (it now *contains* the post-mono AST). This is
|
||||
spec-consistent: structural module reads were never a re-derivation;
|
||||
the spec only requires deleting the type/callee/mode/rep
|
||||
re-derivers. Carrying the AST also resolves the intrinsic-fn gap:
|
||||
`lower_module` skips intrinsic-bodied fns (no `MirDef`), so codegen
|
||||
iterates `mir_module.ast.defs` (all fns, incl. intrinsic ones handled
|
||||
via the intercept registry) and looks up the `MirDef` body by name
|
||||
for the non-intrinsic ones.
|
||||
- **OQ2 — escape pointer-identity over `MTerm`.** `escape::analyze_fn_body`
|
||||
and `lower_term` key nodes by raw-pointer identity (`(t as *const
|
||||
Term) as usize`). After the flip they key `*const MTerm`, and the
|
||||
invariant is that **both passes walk the *same* borrowed `&MTerm`
|
||||
tree** — `emit_fn` borrows `mir_def.body: &MTerm` **once** and passes
|
||||
the same reference to `analyze_fn_body` and `lower_term`. A clone
|
||||
between the passes would silently break identity. The pointer-cast
|
||||
sites flip `*const Term` → `*const MTerm`; mechanical once the
|
||||
same-tree borrow is stated.
|
||||
- **OQ3 — `MTerm::New` stays `unreachable!` in codegen.** Exactly as
|
||||
`Term::New`'s `lower_term` arm is `unreachable!` today: every `New`
|
||||
node is desugared away before codegen (RawBuf → payload ops;
|
||||
monomorphic user-ADT `new` → dotted `App`, per mir.1a's commit). So
|
||||
`elaborate_workspace`'s desugar pass (unchanged) eliminates `New`
|
||||
before `lower_to_mir`, and codegen never hits a live `MTerm::New`.
|
||||
#51 keeps building via its desugared payload path — untouched by
|
||||
mir.1b. (The element-type work that makes `New` "real" is mir.5.)
|
||||
- **OQ4 — CLI diagnostics: drop the separate `check_workspace` call.**
|
||||
`elaborate_workspace` re-runs `check_workspace` internally and returns
|
||||
`Err(Vec<Diagnostic>)` on a type error. The `build` / `emit-ir`
|
||||
build paths currently print diagnostics from their *own*
|
||||
`check_workspace` call then `process::exit(1)`. **Resolution:** those
|
||||
build paths drop their separate check call and let
|
||||
`elaborate_workspace`'s `Err` drive the diagnostic print + exit(1)
|
||||
(one check, not two). The `ail check` subcommand stays on
|
||||
`check_workspace` (diagnostics-only, no build).
|
||||
- **emit_ir keeps `&Module`, calls `elaborate_workspace` internally,
|
||||
converts `Err(Vec<Diagnostic>)` → `CodegenError`.** Its in-source
|
||||
test callers build hand-written single-module `Module`s using
|
||||
*builtins* (`add`, `==`), which resolve from `env.globals` without a
|
||||
prelude, so `check_workspace` passes; the one ill-typed test
|
||||
(`eq` on an ADT, asserting `unwrap_err`) now gets its error from
|
||||
`check` (`NoInstance`) instead of lowering — same outcome
|
||||
(`unwrap_err` still holds). Any in-source `emit_ir` test that
|
||||
references a *prelude module* fn (not a builtin) and so fails the
|
||||
internal check is migrated to a `load_workspace` fixture — bounded,
|
||||
implement-surfaced, local.
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/ailang-mir/src/lib.rs` — add `ast: Module` to `MirModule`.
|
||||
- Modify: `crates/ailang-check/src/lower_to_mir.rs` — populate `ast`.
|
||||
- Modify: `crates/ailang-codegen/Cargo.toml` — add `ailang-mir` dep.
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs` — the bulk flip + deletes
|
||||
+ entry-point signatures + emit_ir.
|
||||
- Modify: `crates/ailang-codegen/src/drop.rs` — `&Term`→`&MTerm`, 4 synth sites.
|
||||
- Modify: `crates/ailang-codegen/src/match_lower.rs` — `&Term`/`&[Term]`/`&[Arm]`→MIR, 3 synth sites.
|
||||
- Modify: `crates/ailang-codegen/src/escape.rs` — analysis walkers `&Term`→`&MTerm`.
|
||||
- Modify: `crates/ailang-codegen/src/lambda.rs` — `&Term`→`&MTerm`.
|
||||
- Modify: `crates/ailang-codegen/tests/{embed_record_layout_pin,embed_staticlib_lowering,eq_primitives_pin}.rs` — callers build MIR.
|
||||
- Modify: `crates/ail/src/main.rs` — build / emit-ir / staticlib paths → `elaborate_workspace`.
|
||||
- Modify: `crates/ail/tests/e2e.rs` — 6 `lower_workspace_with_alloc` callers.
|
||||
|
||||
---
|
||||
|
||||
## The Term → MTerm correspondence rule (used throughout Task 2)
|
||||
|
||||
Every codegen `match` on a `Term` re-matches the `MTerm` counterpart.
|
||||
The mapping is structural and 1:1 except for the four noted shifts.
|
||||
**The build gate is the totality checker** — the compiler flags every
|
||||
unconverted arm. Apply this rule at every match site the recon
|
||||
enumerated; the judgement sites (synth reads, callee unwrap, escape
|
||||
pointer, dual-source) get exact code in the steps below.
|
||||
|
||||
| `Term` arm | `MTerm` arm | Shift |
|
||||
|---|---|---|
|
||||
| `Lit { lit }` | `Lit { lit, ty }` for non-Str; **`Literal::Str` → new `Str { lit, rep }` arm** | Str split (rep unused at mir.1b) |
|
||||
| `Var { name }` | `Var { name, ty }` | — |
|
||||
| `App { callee, args, tail }` | `App { callee, args, tail, ty }` | `callee: Callee` (unwrap `Indirect`), `args: Vec<MArg>` |
|
||||
| `Let { name, value, body }` | `Let { name, mode, init, body, ty }` | `value`→`init`; `mode` ignored (mir.3) |
|
||||
| `If { cond, then, else_ }` | `If { cond, then, else_, ty }` | children `Box<MTerm>` |
|
||||
| `Do { op, args, tail }` | `Do { op, args, tail, ty }` | `args: Vec<MArg>` |
|
||||
| `Ctor { type_name, ctor, args }` | `Ctor { type_name, ctor, args, ty }` | `args: Vec<MArg>` |
|
||||
| `Match { scrutinee, arms }` | `Match { scrutinee, arms, ty }` | `arms: Vec<MArm>` (`MArm.pat` reuses `ast::Pattern`) |
|
||||
| `Lam { … }` | `Lam { …, ty }` | `ret_ty: Type` (not `Box`), body `Box<MTerm>` |
|
||||
| `Seq { lhs, rhs }` | `Seq { lhs, rhs, ty }` | — |
|
||||
| `Clone { value }` | `Clone { value, ty }` | — |
|
||||
| `ReuseAs { source, body }` | `ReuseAs { source, body, ty }` | inner `MTerm::Ctor` destructure |
|
||||
| `Loop { binders, body }` | `Loop { binders, body, ty }` | `binders: Vec<MLoopBinder>` (`.init` is `MTerm`, not `Box`) |
|
||||
| `Recur { args }` | `Recur { args, ty }` | `args: Vec<MArg>` |
|
||||
| `New { … }` | `New { … }` | arm stays `unreachable!` (OQ3) |
|
||||
| `Intrinsic` | `Intrinsic { ty }` | unit → struct variant |
|
||||
|
||||
**Arg access:** wherever an arm reads `&args[i]` (a `&Term`), it now
|
||||
reads `&args[i].term` (the `MTerm` inside the `MArg`). Wherever it read
|
||||
`self.synth_arg_type(&args[i])`, it reads `args[i].term.ty()`.
|
||||
**Callee access:** `App`'s `callee` is `Callee::Indirect(Box<MTerm>)`
|
||||
at mir.1b; unwrap it to reach the inner `MTerm` (see Task 2 Step 4).
|
||||
**Imports:** each flipped file adds `use ailang_mir::{MTerm, MArg,
|
||||
MArm, MLoopBinder, Callee, …}` and keeps `use ailang_core::ast::{…,
|
||||
Pattern, Type, Literal}` (patterns/literals/types are unchanged).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `MirModule.ast` + codegen's `ailang-mir` dep (additive, green)
|
||||
|
||||
This task is additive — it extends a landed type and wires a dep;
|
||||
nothing consumes the new field yet, so the whole suite stays green.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-mir/src/lib.rs`
|
||||
- Modify: `crates/ailang-check/src/lower_to_mir.rs`
|
||||
- Modify: `crates/ailang-codegen/Cargo.toml`
|
||||
|
||||
- [ ] **Step 1: Add `ast: Module` to `MirModule`**
|
||||
|
||||
In `crates/ailang-mir/src/lib.rs`, extend the import and the struct:
|
||||
|
||||
```rust
|
||||
use ailang_core::ast::{Literal, Module, Pattern, Type};
|
||||
```
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MirModule {
|
||||
pub name: String,
|
||||
/// The post-mono AST module. codegen reads structural data
|
||||
/// (Def::Type / Def::Const / ctor layouts / intrinsic-fn markers /
|
||||
/// imports) from here; the typed fn bodies come from `defs`. This
|
||||
/// keeps `MirWorkspace` the single artefact codegen consumes —
|
||||
/// structural reads were never a re-derivation, so carrying the AST
|
||||
/// alongside the typed bodies does not reintroduce one.
|
||||
pub ast: Module,
|
||||
pub defs: Vec<MirDef>,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Populate `ast` in `lower_module`**
|
||||
|
||||
In `crates/ailang-check/src/lower_to_mir.rs`, the `lower_module` return
|
||||
already builds `MirModule { name, defs }` — add the `ast` field with
|
||||
the post-mono module clone:
|
||||
|
||||
```rust
|
||||
Ok(MirModule { name: module.name.clone(), ast: module.clone(), defs })
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add the `ailang-mir` dependency to codegen**
|
||||
|
||||
In `crates/ailang-codegen/Cargo.toml`, under `[dependencies]` (after
|
||||
`ailang-core.workspace = true`), add:
|
||||
|
||||
```toml
|
||||
ailang-mir.workspace = true
|
||||
```
|
||||
|
||||
(No cycle: codegen → check → mir → core; `ailang-mir` is a leaf. This
|
||||
edge lets codegen name `MTerm` / `MArg` / `Callee` / `MirWorkspace`
|
||||
directly rather than only transitively.)
|
||||
|
||||
- [ ] **Step 4: Build gate — additive, nothing breaks**
|
||||
|
||||
Run: `cargo test --workspace`
|
||||
Expected: PASS — whole suite green (the `ast` field is additive;
|
||||
`lower_module` populates it; the mir.1a `lower_to_mir_ty` pins still
|
||||
pass; codegen does not yet read `ast`). This confirms the foundation
|
||||
before the atomic flip.
|
||||
|
||||
---
|
||||
|
||||
## Task 2: the atomic codegen flip (lib + all codegen tests)
|
||||
|
||||
The whole of codegen's lowering walk flips `&Term` → `&MTerm` in one
|
||||
task — there is no compiling intermediate. Apply the correspondence
|
||||
rule (above) at every `Term::*` match the recon enumerated; the steps
|
||||
below give exact code for the judgement sites. The task's build gate
|
||||
(`cargo test -p ailang-codegen --no-run`) is the totality checker:
|
||||
it does not go green until every arm, signature, and caller inside the
|
||||
codegen crate (lib + in-source tests + `tests/`) is converted.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-codegen/src/lib.rs`
|
||||
- Modify: `crates/ailang-codegen/src/drop.rs`
|
||||
- Modify: `crates/ailang-codegen/src/match_lower.rs`
|
||||
- Modify: `crates/ailang-codegen/src/escape.rs`
|
||||
- Modify: `crates/ailang-codegen/src/lambda.rs`
|
||||
- Modify: `crates/ailang-codegen/tests/{embed_record_layout_pin,embed_staticlib_lowering,eq_primitives_pin}.rs`
|
||||
|
||||
- [ ] **Step 1: Flip the entry-point signatures + `lower_workspace_inner` pairing**
|
||||
|
||||
In `crates/ailang-codegen/src/lib.rs`, change the five entry points
|
||||
to take `&MirWorkspace`:
|
||||
|
||||
```rust
|
||||
pub fn lower_workspace_with_alloc(mir: &MirWorkspace, alloc: AllocStrategy) -> Result<String> {
|
||||
lower_workspace_inner(mir, alloc, Target::Executable)
|
||||
}
|
||||
pub fn lower_workspace_staticlib_with_alloc(mir: &MirWorkspace, alloc: AllocStrategy) -> Result<String> {
|
||||
lower_workspace_inner(mir, alloc, Target::StaticLib)
|
||||
}
|
||||
pub fn lower_workspace(mir: &MirWorkspace) -> Result<String> {
|
||||
lower_workspace_inner(mir, AllocStrategy::Rc, Target::Executable)
|
||||
}
|
||||
pub fn lower_workspace_staticlib(mir: &MirWorkspace) -> Result<String> {
|
||||
lower_workspace_inner(mir, AllocStrategy::Rc, Target::StaticLib)
|
||||
}
|
||||
fn lower_workspace_inner(mir: &MirWorkspace, alloc: AllocStrategy, target: Target) -> Result<String> {
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
In `lower_workspace_inner`: the desugar block (lib.rs:295-313) is
|
||||
**deleted** — `elaborate_workspace` already desugared + lifted + mono'd
|
||||
before producing the MIR. The pass-1 symbol tables (lib.rs:333-396)
|
||||
now iterate `mir.modules` and read each `mir_module.ast.defs` (AST
|
||||
structure) instead of `ws.modules`'s `m.defs`. The pass-2 per-module
|
||||
`Emitter` construction (lib.rs:400-455) hands the Emitter **both**
|
||||
`&mir_module.ast` (for structural reads) and `&mir_module.defs` (for
|
||||
bodies) — see Step 2. The entry-`main` check (lib.rs:460-472) reads
|
||||
`mir.modules[&mir.entry].ast` for the `main` signature.
|
||||
|
||||
> **Implementer note.** Read `lower_workspace_inner`'s current body
|
||||
> (lib.rs:294-472) and re-point every `ws`/`m.defs` read at
|
||||
> `mir`/`mir_module.ast.defs`. The structure of the two passes is
|
||||
> unchanged — only the source of each module's defs moves from the raw
|
||||
> `Workspace` to `mir_module.ast`, and a second source (`mir_module.defs`)
|
||||
> is added for bodies.
|
||||
|
||||
- [ ] **Step 2: Emitter holds AST module + MIR bodies; `emit_module` / `emit_fn` pairing**
|
||||
|
||||
The `Emitter` (lib.rs:730) currently holds `module: &Module`. Keep
|
||||
that (it now points at `mir_module.ast`) and add a body source — a map
|
||||
or slice of the module's `MirDef`s. Minimal shape: store
|
||||
`mir_bodies: &'a [MirDef]` (from `mir_module.defs`) on the Emitter, and
|
||||
have `emit_module` look up each fn's body by name.
|
||||
|
||||
`emit_module` (lib.rs:1054) iterates `self.module.defs` (AST) as today
|
||||
for `Def::Type` / `Def::Const` / intrinsic markers. For each
|
||||
`Def::Fn(f)`:
|
||||
- If `f` is intrinsic-bodied (`matches!(f.body, Term::Intrinsic)` —
|
||||
reads the AST `FnDef`, unchanged): handle via the intercept registry
|
||||
as today; no MIR body.
|
||||
- Else: look up the `MirDef` whose `name == f.name` in `self.mir_bodies`,
|
||||
and call `emit_fn(f, &mir_def.body)` — `f` (AST `FnDef`) supplies
|
||||
sig / params / export / the intrinsic marker; `&mir_def.body`
|
||||
(`&MTerm`) supplies the typed body.
|
||||
|
||||
`emit_fn` (lib.rs:1265) signature gains the body: `fn emit_fn(&mut
|
||||
self, f: &FnDef, body: &MTerm)`. Inside:
|
||||
- The param-bind loop (lib.rs:1344) that pushed the AST param `Type`
|
||||
into `locals`' 4th slot stays — param types come from `f.ty` via
|
||||
`crate::fn_param_types`-equivalent (the AST signature), unchanged.
|
||||
- `escape::analyze_fn_body(body)` (was `&f.body`) and
|
||||
`self.lower_term(body)` (was `&f.body`) both take the **same**
|
||||
borrowed `&MTerm` `body` (OQ2 — one borrow, shared, no clone).
|
||||
- The intrinsic early-return (`matches!(f.body, Term::Intrinsic)`,
|
||||
lib.rs:1394/1126) still reads the AST `f.body` — it is a
|
||||
signature-only marker; unchanged.
|
||||
|
||||
> **Implementer note.** The exact Emitter field plumbing (lifetime on
|
||||
> `mir_bodies`, how `emit_module` threads it) is yours to wire against
|
||||
> the landed Emitter struct; the contract is: structural reads off
|
||||
> `self.module` (= `mir_module.ast`), body off the matched
|
||||
> `MirDef.body`, one `&MTerm` borrow shared between escape + lowering.
|
||||
|
||||
- [ ] **Step 3: Flip `lower_term` (lib.rs:1604) per the correspondence rule**
|
||||
|
||||
`fn lower_term(&mut self, t: &MTerm) -> Result<(String, String)>`.
|
||||
Re-match every arm per the correspondence table. Children that were
|
||||
`&Box<Term>` are now `&Box<MTerm>` (same `self.lower_term(child)`
|
||||
call); arg lists that were `&[Term]` are `&[MArg]` (`self.lower_term(&arg.term)`).
|
||||
Two arms need explicit handling:
|
||||
|
||||
- **`Lit` split.** Replace the single `Lit` arm with two:
|
||||
```rust
|
||||
MTerm::Lit { lit, .. } => { /* the existing non-Str literal lowering, unchanged */ }
|
||||
MTerm::Str { lit, .. } => { /* the existing Literal::Str lowering body, lifted out of the old Lit arm */ }
|
||||
```
|
||||
(The old `Term::Lit` arm's `Literal::Str` branch becomes the `Str`
|
||||
arm's body; the rep field is ignored at mir.1b.)
|
||||
- **`Let` synth read (the binder type).** At lib.rs:1725, replace:
|
||||
```rust
|
||||
let val_ail = self.synth_arg_type(value)?;
|
||||
```
|
||||
with:
|
||||
```rust
|
||||
let val_ail = value.ty();
|
||||
```
|
||||
(`value` is the `&Box<MTerm>` init; `MTerm::ty()` returns the carried
|
||||
`Type` directly — no `?`.) The 4th `locals` slot (lib.rs:1727) is now
|
||||
sourced from this. The inherited-mode `Term::Var` read (lib.rs:1742)
|
||||
becomes `MTerm::Var`.
|
||||
|
||||
- [ ] **Step 4: Flip `lower_app` (lib.rs:2340) — callee unwrap + the 2 arith synth reads**
|
||||
|
||||
`lower_app`'s `args: &[Term]` → `&[MArg]`; every `self.lower_term(&args[i])`
|
||||
→ `self.lower_term(&args[i].term)`. The callee/name resolution
|
||||
(`is_static_callee`, `type_home_module`) takes a `&str` name and is
|
||||
**unchanged** (stays for mir.2). The `App` arm's static-dispatch test
|
||||
(lib.rs:1918-1927) that currently reads `if let Term::Var { name } =
|
||||
callee.as_ref()` now unwraps the `Callee`:
|
||||
|
||||
```rust
|
||||
// callee is Callee::Indirect(Box<MTerm>) at mir.1b; the static name,
|
||||
// if any, is the inner MTerm::Var. (mir.2 replaces this with
|
||||
// Callee::Static and deletes is_static_callee.)
|
||||
let callee_mterm: &MTerm = match callee {
|
||||
Callee::Indirect(inner) => inner.as_ref(),
|
||||
Callee::Static { .. } => unreachable!("callee is Indirect until mir.2"),
|
||||
};
|
||||
if let MTerm::Var { name, .. } = callee_mterm {
|
||||
// … existing static-dispatch path on `name`, unchanged …
|
||||
}
|
||||
```
|
||||
|
||||
The two arithmetic/`neg` arg-type reads — lib.rs:2359 and lib.rs:2394:
|
||||
```rust
|
||||
let arg_ty = self.synth_arg_type(&args[0])?;
|
||||
```
|
||||
both become:
|
||||
```rust
|
||||
let arg_ty = args[0].term.ty();
|
||||
```
|
||||
(then the existing `builtin_binop_typed(name, &arg_ty)` / Int-vs-Float
|
||||
match on `arg_ty` is unchanged).
|
||||
|
||||
> **Implementer note.** `lower_app` may be reached both from the
|
||||
> `MTerm::App` arm of `lower_term` (callee + args already split) and as
|
||||
> a helper. Confirm how the `App` arm calls `lower_app` (it currently
|
||||
> destructures `Term::App { callee, args, tail }` then dispatches on the
|
||||
> callee name); thread the `Callee` unwrap so `lower_app` still gets a
|
||||
> `&str` name and `&[MArg]` args. `emit_call` (lib.rs:2625),
|
||||
> `emit_indirect_call` (lib.rs:2724), `lower_effect_op` (lib.rs:2972)
|
||||
> all take `args: &[Term]` → `&[MArg]`; their `args.iter()` /
|
||||
> `.zip(...)` read `.term`.
|
||||
|
||||
- [ ] **Step 5: Delete `synth_with_extras` + `synth_arg_type`**
|
||||
|
||||
Delete `synth_arg_type` (lib.rs:3238-3240) and `synth_with_extras`
|
||||
(lib.rs:3242-3519) entirely (~280 lines). Their internal calls to
|
||||
`type_home_module` / `collect_owner_local_types` vanish with them
|
||||
(those helpers stay — used elsewhere). The `module_def_ail_types`
|
||||
reads *inside* `synth_with_extras` (lib.rs:3292/3301/3321) vanish; the
|
||||
field itself stays (read at lib.rs:2687 for mir.3 mode work) and so do
|
||||
its other readers.
|
||||
|
||||
Verify the field `module_def_ail_types` (lib.rs:759) has no remaining
|
||||
reader that was only `synth_with_extras` — the `:2687` read (in
|
||||
`emit_call`'s borrow-drop) stays. (If, after the delete, the field has
|
||||
*zero* readers, leave it — mir.3 needs it; a `#[allow(dead_code)]` is
|
||||
acceptable rather than removing and re-adding it. Confirm the `:2687`
|
||||
reader survives.)
|
||||
|
||||
- [ ] **Step 6: Flip `drop.rs` (4 synth sites + signatures)**
|
||||
|
||||
Apply the correspondence rule to `drop.rs`'s walkers and rewrite the 4
|
||||
synth reads:
|
||||
- `is_rc_heap_allocated` (`:457`, `value: &Term` → `&MTerm`); arms
|
||||
`:462/466/478` re-match `MTerm`; the `Loop` arm at `:487`:
|
||||
```rust
|
||||
match self.synth_arg_type(value) {
|
||||
Ok(t) => { /* is_ptr && !is_str on t */ }
|
||||
Err(_) => false,
|
||||
}
|
||||
```
|
||||
becomes (no `Result`):
|
||||
```rust
|
||||
let t = value.ty();
|
||||
let is_ptr = matches!(crate::synth::llvm_type(&t).as_deref(), Ok("ptr"));
|
||||
let is_str = matches!(&t, Type::Con { name, .. } if name == "Str");
|
||||
is_ptr && !is_str
|
||||
```
|
||||
- `synth_callee_ret_mode` (`:513`, `callee: &Term` → `&MTerm`): `:514`
|
||||
`let cty = self.synth_arg_type(callee).ok()?;` → `let cty =
|
||||
callee.ty();` (and the `match cty { Type::Fn { ret_mode, .. } =>
|
||||
Some(ret_mode), _ => None }` stays).
|
||||
- `drop_symbol_for_binder` (`:533`, `value: &Term` → `&MTerm`); arms
|
||||
`:535/546/560` re-match `MTerm`; `:561` `if let Ok(Type::Con { name,
|
||||
.. }) = self.synth_arg_type(value)` → `if let Type::Con { name, .. }
|
||||
= value.ty()`.
|
||||
- `emit_inlined_partial_drop` (`:844`, `value: &Term` → `&MTerm`); arm
|
||||
`:851` re-match; `:862`
|
||||
`self.synth_arg_type(value).ok().and_then(|ty| …)` →
|
||||
`self.partial_drop_symbol_for_type(&value.ty())` (wrap to match the
|
||||
surrounding `if let (Some(sym), Some(mask))` shape — `value.ty()`
|
||||
is infallible, so the `.ok()` disappears).
|
||||
- `:463` `term_ptr` identity cast `*const Term` → `*const MTerm`.
|
||||
|
||||
- [ ] **Step 7: Flip `match_lower.rs` (3 synth sites + signatures)**
|
||||
|
||||
- `lower_ctor` (`:42`, `args: &[Term]` → `&[MArg]`): `:82`
|
||||
`.map(|a| self.synth_arg_type(a)).collect::<Result<_>>()?` →
|
||||
`.map(|a| a.term.ty()).collect::<Vec<_>>()` (no `Result`); `:98/99`
|
||||
lower `a.term`.
|
||||
- `lower_reuse_as_rc` (`:179`, `source: &Term` → `&MTerm`, `body_args:
|
||||
&[Term]` → `&[MArg]`): `:201` `Term::Var` → `MTerm::Var`; `:263`
|
||||
same `.map(|a| a.term.ty())`; `:288` lower `a.term`.
|
||||
- `lower_match` (`:440`, `scrutinee: &Term` → `&MTerm`, `arms: &[Arm]`
|
||||
→ `&[MArm]`): `:445` `let s_ail = self.synth_arg_type(scrutinee)?;`
|
||||
→ `let s_ail = scrutinee.ty();`; `:459/460` `Term::Var` → `MTerm::Var`;
|
||||
`:476/477` the `Vec<(CtorRef, &Arm, …)>` → `&MArm`; `:480` `arm.pat`
|
||||
and `:481/484/488` `Pattern::*` are **unchanged** (`MArm.pat` reuses
|
||||
`ast::Pattern`); `arm.body` is now `MTerm`.
|
||||
|
||||
- [ ] **Step 8: Flip `escape.rs` + `lambda.rs` (pure walkers)**
|
||||
|
||||
`escape.rs`: `analyze_fn_body` (`:97`), `walk` (`:110`), `escapes`
|
||||
(`:227`), `collect_free_vars` (`:446`) flip `&Term` → `&MTerm`; all
|
||||
`Term::*` arms re-match `MTerm::*` (the `Str` literal becomes its own
|
||||
arm); `pattern_bound_names` (`:430`, `&Pattern`) is **unchanged**.
|
||||
Import (`:83`): add `use ailang_mir::MTerm;` (keep `Pattern`/`NewArg`
|
||||
from `ast`). The `#[cfg(test)]` `ctor(...) -> Term` helper (`:574`):
|
||||
if the escape unit tests build `Term` directly, either flip the helper
|
||||
to build `MTerm` or keep the tests AST-only by testing a thin `MTerm`
|
||||
wrapper — implementer judgement; the gate is that `escape`'s tests
|
||||
compile and pass.
|
||||
|
||||
`lambda.rs`: `lower_lambda` (`:39`, `lam_body: &Term` → `&MTerm`;
|
||||
`:222` calls `lower_term`), and the thunk-body walker / `collect_captures`
|
||||
(`:373`, `t: &Term` → `&MTerm`); `pattern_bound_names` (`:509`,
|
||||
`&Pattern`) unchanged.
|
||||
|
||||
- [ ] **Step 9: Flip `emit_ir` (lib.rs:213) — build MIR internally**
|
||||
|
||||
`emit_ir` keeps `&Module`. Replace its body's terminal
|
||||
`lower_workspace(&ws)` (lib.rs:230) with an `elaborate_workspace` call,
|
||||
converting the diagnostics error to a `CodegenError`:
|
||||
|
||||
```rust
|
||||
pub fn emit_ir(m: &Module) -> Result<String> {
|
||||
let mut modules = BTreeMap::new();
|
||||
modules.insert(m.name.clone(), m.clone());
|
||||
let ws = Workspace {
|
||||
entry: m.name.clone(),
|
||||
modules,
|
||||
root_dir: std::path::PathBuf::from("."),
|
||||
registry: ailang_core::workspace::Registry::default(),
|
||||
};
|
||||
let mir = ailang_check::elaborate_workspace(&ws)
|
||||
.map_err(|diags| CodegenError::Internal(format!(
|
||||
"elaborate failed: {}",
|
||||
diags.iter().map(|d| d.message.clone()).collect::<Vec<_>>().join("; ")
|
||||
)))?;
|
||||
lower_workspace(&mir)
|
||||
}
|
||||
```
|
||||
|
||||
> **Implementer note.** Confirm `CodegenError::Internal(String)` is the
|
||||
> right variant (grep the enum); confirm `Diagnostic` exposes `.message`
|
||||
> (else use its `Display`). `ailang_check` is a direct dep of codegen.
|
||||
> The two in-source `#[cfg(test)]` `lower_workspace_with_alloc` callers
|
||||
> (lib.rs:3709/3728) build a `ws` via test helpers — flip them to
|
||||
> `ailang_check::elaborate_workspace(&ws)` then pass `&mir`. The
|
||||
> ill-typed `emit_ir` test (`eq` on ADT, `unwrap_err`) now errors from
|
||||
> `check` inside `elaborate` — `unwrap_err` still holds.
|
||||
|
||||
- [ ] **Step 10: Flip codegen's `tests/` integration callers**
|
||||
|
||||
- `eq_primitives_pin.rs:30` `lower_workspace(&ws)` — it already runs
|
||||
`check_workspace` + `monomorphise_workspace` (`:23-29`); collapse
|
||||
that block to `let mir = ailang_check::elaborate_workspace(&ws).expect("elaborates"); lower_workspace(&mir)`.
|
||||
- `embed_record_layout_pin.rs:28` `lower_workspace_with_alloc(&ws, Rc)`
|
||||
and `embed_staticlib_lowering.rs:23/58/78`
|
||||
(`lower_workspace_staticlib_with_alloc` ×2, `lower_workspace_with_alloc`)
|
||||
— these do not yet import `ailang_check`; add
|
||||
`use ailang_check::elaborate_workspace;`, build `let mir =
|
||||
elaborate_workspace(&ws).expect("elaborates");` from the
|
||||
`load_workspace` `ws`, and pass `&mir` to the lowering call. (`ailang-check`
|
||||
is reachable from codegen's own integration tests — confirmed by
|
||||
`eq_primitives_pin.rs:9`'s existing `use ailang_check::…`.)
|
||||
|
||||
- [ ] **Step 11: Build gate — codegen crate fully converted**
|
||||
|
||||
Run: `cargo test -p ailang-codegen 2>&1 | tail -20`
|
||||
Expected: PASS — codegen lib + in-source tests + `tests/` integration
|
||||
all compile and pass. The `synth_with_extras` / `synth_arg_type`
|
||||
deletion is grep-clean within codegen:
|
||||
|
||||
Run: `git grep -nE 'synth_with_extras|synth_arg_type' crates/ailang-codegen/src/`
|
||||
Expected: no matches (only possibly a doc-comment mention to reword —
|
||||
if any remain in prose, reword them; the recon found module/fn
|
||||
doc-comments naming `synth_arg_type` at match_lower.rs:18,
|
||||
subst.rs:75, synth.rs:60/204, drop.rs:856 — reword these to describe
|
||||
the `MTerm::ty()` read).
|
||||
|
||||
---
|
||||
|
||||
## Task 3: external callers (CLI build paths + e2e)
|
||||
|
||||
The entry-point signature change to `&MirWorkspace` breaks the callers
|
||||
in `crates/ail`. Thread them all here; the gate is the full workspace.
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ail/src/main.rs`
|
||||
- Modify: `crates/ail/tests/e2e.rs`
|
||||
|
||||
- [ ] **Step 1: CLI `build` path → `elaborate_workspace` (+ OQ4 diagnostics)**
|
||||
|
||||
In `crates/ail/src/main.rs`, the `build_to` fn (`:2286+`): replace the
|
||||
block that runs `check_workspace` (`:2293`) + per-module desugar/lift
|
||||
(`:2320-2330`) + `monomorphise_workspace` (`:2346`) +
|
||||
`lower_workspace_with_alloc(&ws, alloc)` (`:2348`) with:
|
||||
|
||||
```rust
|
||||
let mir = match ailang_check::elaborate_workspace(&ws) {
|
||||
Ok(mir) => mir,
|
||||
Err(diags) => {
|
||||
for d in &diags {
|
||||
eprintln!("{}: [{}] {}", /* severity */ d.severity_str(), d.code, d.message);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
let ir = ailang_codegen::lower_workspace_with_alloc(&mir, alloc)?;
|
||||
```
|
||||
|
||||
> **Implementer note.** Match the existing diagnostic-print format used
|
||||
> by the `ail check` human path (main.rs ~:611-620) — reuse the same
|
||||
> `eprintln!` shape (severity / code / message / span) so build-time
|
||||
> and check-time diagnostics read identically. Drop `build_to`'s own
|
||||
> separate `check_workspace` call (OQ4: `elaborate_workspace` runs it
|
||||
> internally). The `has_export` gate and any read of `ws.modules…f.export`
|
||||
> stays on the **pre-elaborate AST `ws`** (it is structural, not in MIR
|
||||
> at the CLI level) — confirm it reads `ws`, not `mir`.
|
||||
|
||||
- [ ] **Step 2: CLI `staticlib` build path → `elaborate_workspace`**
|
||||
|
||||
`build_staticlib` (`:2456+`): same shape — replace its `check_workspace`
|
||||
(`:2464`) + lift (`:2482-2488`) + `monomorphise_workspace` (`:2495`) +
|
||||
`lower_workspace_staticlib_with_alloc(&ws, alloc)` (`:2512`) with an
|
||||
`elaborate_workspace` call + `lower_workspace_staticlib_with_alloc(&mir,
|
||||
alloc)`. The `has_export` check (`:2499-2505`) stays on the AST `ws`.
|
||||
|
||||
- [ ] **Step 3: CLI `emit-ir` path → `elaborate_workspace`**
|
||||
|
||||
`Cmd::EmitIr` arm (`:647+`): it currently prints diagnostics from its
|
||||
own `check_workspace` (`:651`) then lifts/monos (`:682-695`) then calls
|
||||
`lower_workspace` / `lower_workspace_staticlib` (`:705/707`). Replace
|
||||
with `elaborate_workspace`, driving diagnostics from its `Err` (OQ4):
|
||||
|
||||
```rust
|
||||
let mir = match ailang_check::elaborate_workspace(&ws) {
|
||||
Ok(mir) => mir,
|
||||
Err(diags) => { /* print diags as above */ std::process::exit(1); }
|
||||
};
|
||||
let ir = if emit == "staticlib" {
|
||||
ailang_codegen::lower_workspace_staticlib(&mir)?
|
||||
} else {
|
||||
ailang_codegen::lower_workspace(&mir)?
|
||||
};
|
||||
```
|
||||
|
||||
The `has_export` gate (`:697-704`) stays on the AST `ws`.
|
||||
|
||||
- [ ] **Step 4: e2e.rs — 6 `lower_workspace_with_alloc` callers**
|
||||
|
||||
In `crates/ail/tests/e2e.rs`, each of the 6 callers (`:1537/1708/1782/1871/1994/2084`)
|
||||
is preceded by its own load + lift + mono block. For each: replace the
|
||||
lift/mono block with `let mir = ailang_check::elaborate_workspace(&ws).expect("elaborates");`
|
||||
and pass `&mir` to `lower_workspace_with_alloc(&mir, AllocStrategy::Rc)`.
|
||||
|
||||
> **Implementer note.** Read each caller's preceding block (e.g.
|
||||
> `:1517-1536` for the first) and collapse its explicit
|
||||
> desugar/lift/mono into the single `elaborate_workspace` call. The
|
||||
> `ws` source (a `load_workspace` or hand-built workspace) stays; only
|
||||
> the lift→mono→lower bridge changes.
|
||||
|
||||
- [ ] **Step 5: Full workspace gate**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | grep -E 'test result:|error\[|FAILED' | grep -v 'ok\.' | sort | uniq -c`
|
||||
Expected: no `error[` / no `FAILED` lines.
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | grep -c 'test result: ok'`
|
||||
Expected: the same count as before mir.1b (102) — every existing test
|
||||
still green, the #49 pin still the single `ignored` (lifts at mir.4).
|
||||
|
||||
- [ ] **Step 6: Acceptance — #51/#53 still build + run; synth grep-clean**
|
||||
|
||||
Run (the two regression witnesses build and run unchanged):
|
||||
`cargo run -q -p ail -- run examples/new_counter_user_adt.ail`
|
||||
Expected: prints `ok` (exit 0).
|
||||
Run: `cargo run -q -p ail -- run examples/new_rawbuf_size_only.ail`
|
||||
Expected: builds and runs (prints the size; exit 0).
|
||||
|
||||
Run (no type re-derivation left in codegen):
|
||||
`git grep -nE 'synth_with_extras|synth_arg_type' crates/ailang-codegen/src/`
|
||||
Expected: no matches.
|
||||
|
||||
---
|
||||
|
||||
## Notes for the orchestrator (commit + handoff)
|
||||
|
||||
- **Commit shape:** one cohesive commit — the atomic switch is one
|
||||
logical change. Subject e.g. `feat(codegen): mir.1b — codegen
|
||||
consumes MIR; delete the synth_with_extras type-derivation mirror`.
|
||||
Body: the boundary now reads `ty` off MIR (one engine, not two); the
|
||||
three other re-derivers stay (mir.2/mir.3); MirModule carries the AST
|
||||
for structural reads (single artefact preserved); `refs #49` (closure
|
||||
at mir.4).
|
||||
- **Spec mir.1 row is now complete** across mir.1a + mir.1b: structural
|
||||
MIR + `ty` landed; `synth_with_extras` + `synth_arg_type` gone.
|
||||
- **Next (mir.2):** `Callee::Static` pre-resolved; delete
|
||||
`type_home_module` ×3 + `is_static_callee`; strike the `lower_app ↔
|
||||
is_static_callee` lockstep pair from CLAUDE.md.
|
||||
Reference in New Issue
Block a user