iter 22b.3: JOURNAL — monomorphisation pass shipped, e2e gate green

This commit is contained in:
2026-05-09 21:37:51 +02:00
parent 090f082215
commit eef5197f75
+104
View File
@@ -11499,3 +11499,107 @@ cost. First end-to-end exercise is a positive signal; the
remaining open follow-ups from 2026-05-09 (skill bugs surfaced by
real iter use) get queued as targeted SKILL.md tweaks rather than
a wholesale revisit.
## 2026-05-09 — Iteration 22b.3: Monomorphisation pass
Mono pass slots into `crates/ail/src/main.rs::build_to` between
`lift_letrecs` and `lower_workspace`. The pass is a `Workspace ->
Workspace` transformation living in `crates/ailang-check/src/mono.rs`,
mirroring `lift.rs`'s position and ownership model. For workspaces
with no `Def::Class` / `Def::Instance`, the early-out returns the
input clone byte-identically — pre-22b fixtures stay
hash-stable through the full pipeline. For typeclass-bearing
workspaces, the pass runs three phases: (1) build the full env via
`build_workspace_env` (same shape as `check_in_workspace`), (2)
fixpoint loop — each round walks every fn/const body, re-runs
`synth` to recover residual class constraints, applies `subst`,
filters fully-concrete residuals, dedupes via `(class, method,
type-hash)` key, synthesises one `Def::Fn` per unique target via
`synthesise_mono_fn` (instance body OR class default fallback,
class param substituted to instance type), and appends to the
registry's `defining_module`. Loop terminates when a round adds
no new entries — closes on chained class-method calls (e.g. an
instance body that itself calls a class method, exercised by
`test_22b3_chained_calls.ail.json`). (3) Phase 3 walks every
fn/const body in the post-fixpoint workspace and rewrites every
class-method `Term::Var` to its mono-symbol name, qualifying with
the defining module if cross-module. The walker is shadow-aware:
local bindings (Let / LetRec / Lam / Match-arm) suppress the
rewrite at locally-named Vars, mirroring synth's
locals-take-precedence rule.
End-to-end gate met: `examples/test_22b3_mono_synthetic.ail.json`
declares `class Foo a where foo : (a) -> Int` + `instance Foo Int
where foo = lam i. i` + `main = do io/print_int (foo 5)`. `ail
run` produces stdout `5\n`. All three bench gates green:
`bench/check.py` 63/63, `bench/compile_check.py` 24/24,
`bench/cross_lang.py` 25/25 — mono pass is genuinely no-op for
class-free fixtures.
Mono-symbol naming format decision: spec recommended
`<method>#<typename>` ("but the implementer chooses"). `#` turns
out to be invalid in LLVM IR global identifiers — clang rejected
`@ail_..._foo#Int_clos`. Switched to `<method>__<typename>`
(double underscore separator); same legibility, valid C-ABI.
DESIGN.md / spec amendment in the next sweep.
Per-task subjects:
- iter 22b.3.1: mono pass skeleton — identity for class-free workspaces
- iter 22b.3.2: mono_symbol — primitive surface forms + hash for compound
- iter 22b.3.3: collect_mono_targets — synth-replay residual gathering
- iter 22b.3.4: synthesise_mono_fn — instance body + class default fallback
- iter 22b.3.5: workspace fixpoint loop — dedup + synth-append (multi-round chained-call coverage)
- iter 22b.3.6: call-site rewrite — same/cross module qualified names (shadow-aware)
- iter 22b.3.7: synthetic fixture — class+instance compiles, runs, prints 5
- iter 22b.3.tester: e2e for coherence (two instances), default keyword, cross-module mono
Each task ran through the two-stage review (spec compliance →
code quality). Quality round caught: shadow-blind walker (Task
6), untested cross-module branch (Task 6), untested zero-arg
method branch (Task 4), untested multi-round chained-call case
(Task 5), defensive `unwrap_or_default` (Task 3), speculative
`Float` arm (Task 2), and module-doc-vs-implementation drift
(Task 1). All caught + fixed before iteration close.
**Plan-text defects surfaced during execution** (collected here
so a 22-arc audit / next-iter brainstorm can act on them):
- The plan's RED test for Task 5 used `Term::Seq` with `lhs:
Str`, but Seq requires `lhs: Unit`. Implementer substituted
`Term::Let { name: "_a", ... }`. The spec/plan template needs
a "discard-and-continue" idiom note.
- The plan's fixpoint loop scaffold filtered targets only across
rounds, not within a round. Two same-type call sites in round
1 would both append; implementer added `seen_this_round`.
- `Float` was speculatively included in `mono_symbol`'s primitive
table — `Float` is not in the language. Removed.
- Pre-existing `examples/test_22b2_*` fixtures had bare-Lit
instance bodies for arrow-typed methods. Worked under 22b.1/2
because instance-body typecheck is deferred follow-up.
Mono-pass synth requires Lam-shaped bodies; affected fixtures
Lam-wrapped during this iter (`test_22b2_instance_present`,
`test_22b2_xmod_classmod`). Instance-body typecheck remains
deferred — that's the right place to catch this user-side.
**Known debt / out-of-scope deferrals:**
- Form-B prose projection for `Def::Class` / `Def::Instance`
remains deferred to 22b.4. `crates/ailang-surface/tests/round_trip.rs`
skip-list excludes `test_22b2_*` and `test_22b3_*`.
- DESIGN.md mono-symbol naming amendment (`#``__`) needs a
small edit in 22b.4 alongside the Prelude wiring.
- Primitive-name set is hard-coded in three sites
(`linearity.rs`, `lib.rs` x2, `mono.rs`). Reviewer flagged the
duplication; consolidation deferred to milestone-22 audit.
- `pub mod mono` instead of `mod mono; pub use mono::...;`
pattern was the deliberate plan-level call (multiple pub items
reach by integration tests). Audit may revisit if the
convention drifts further.
**Next: 22b.4 — Prelude module + Form-B parser/printer arms.**
Prelude declares `Show`, `Eq`, `Ord` over `Int`, `Bool`, `Str`
(default-bearing where Decision 11 mandates), `print` re-routes
through `Show.show`, Form-B class/instance prose lands, round-trip
filter retires. End-to-end gate: any prior `print 42`-style fixture
must keep working through `Show.show#Int`.