From 4bbcb947a371eadcb93d4e3f49c6cb866e76bca5 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 11 May 2026 22:01:31 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20drop=2023.4=20=E2=80=94=20design=20assu?= =?UTF-8?q?mption=20broke=20at=20branch=20verification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 23.4 mono-unification plan (ea0285b) assumed prep1 + prep2 + prep3 were on main. They never were — they sit only on iter/23.4. Spec component table was factually wrong. Plan Task 10 (revert prep commits) was impossible on main. Discarding the plan; the spec is next revised against the actual main state. --- docs/plans/2026-05-11-iter-23.4.md | 982 ----------------------------- 1 file changed, 982 deletions(-) delete mode 100644 docs/plans/2026-05-11-iter-23.4.md diff --git a/docs/plans/2026-05-11-iter-23.4.md b/docs/plans/2026-05-11-iter-23.4.md deleted file mode 100644 index 12e8732..0000000 --- a/docs/plans/2026-05-11-iter-23.4.md +++ /dev/null @@ -1,982 +0,0 @@ -# iter 23.4 — Mono-Pass Unification + prep2/prep3 Rollback — Implementation Plan - -> **Parent spec:** `docs/specs/2026-05-11-23-eq-ord-prelude.md` -> -> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement` -> to run this plan. Steps use `- [ ]` checkboxes for tracking. - -**Goal:** Unify the two specialisers (typecheck-time mono pass + codegen-time `lower_polymorphic_call`) into one typecheck-time mono pass that handles every `Type::Forall`-quantified `Def::Fn`. Remove the codegen-time specialiser and revert the two anticipatory codegen prep commits. - -**Architecture:** Mono pass gains a second source-body entry point (free-fn direct, in addition to class-method via `Registry::entries`). Target shape, dedup key, call-site rewriter, and `mono_symbol` naming all extend to accommodate free-fn targets with N-ary type vars. Codegen's `lower_polymorphic_call`, `module_polymorphic_fns`, and `mono_queue` are removed wholesale; the two codegen call sites (lines 1944, 1972 pre-edit) consume mono symbols rewritten by the unified pass. Anticipatory prep commits `a06159d` (prep2) and `c42a0f5` (prep3) revert cleanly. - -**Tech Stack:** `crates/ailang-check/src/mono.rs` (centrepiece), `crates/ailang-codegen/src/lib.rs` + `crates/ailang-codegen/src/subst.rs` (removals), `crates/ail/tests/typeclass_22b3.rs` + `crates/ail/tests/e2e.rs` (new tests), `crates/ailang-core/src/hash.rs` (mono-symbol hash pin), `docs/DESIGN.md` (Decision 11 §"Resolution and monomorphisation" amendment), `examples/cmp_max_smoke.ail.json` (new composition fixture). - ---- - -**Files this plan creates or modifies:** - -- Modify: `crates/ailang-check/src/mono.rs:1-870` — full restructure into unified specialiser. -- Modify: `crates/ailang-codegen/src/lib.rs:52, 287-330, 363-400, 555-780, 842-950, 1939-2100, 2260-2263` — removal of `lower_polymorphic_call`, `module_polymorphic_fns`, `mono_queue`. -- Modify: `crates/ailang-codegen/src/subst.rs:120-240, 270-320` — delete now-unused `apply_subst_to_term` + `descriptor_for_subst`. -- Modify: `crates/ail/tests/typeclass_22b3.rs` — add free-fn-side test parity (mirror naming). -- Test: `crates/ail/tests/e2e.rs` — new `cmp_max_smoke_emits_both_mono_defs` E2E. -- Modify: `crates/ailang-core/src/hash.rs:270-336` — new pin test `mono_symbols_hash_stable_under_unified_pass`. -- Create: `examples/cmp_max_smoke.ail.json` — composition fixture (poly free-fn + nested class method). -- Modify: `docs/DESIGN.md:1513-1556` — §"Resolution and monomorphisation" amendment. -- Revert: commits `a06159d` (iter 23.4-prep2) and `c42a0f5` (iter 23.4-prep3). - -**Implementer Open commitments (per Spec §Open commitments — leave default-or-NEEDS_CONTEXT):** - -- `MonoTarget` shape (enum variant vs Option-shaped fields) — implementer's call. -- Polymorphic-free-fn detection mechanism (extend `synth`-residual pathway vs separate post-synth walker) — implementer's call. -- Whether the original polymorphic `Def::Fn` source survives in the workspace post-mono — default: yes, parallel to `Def::Class`/`Def::Instance` preservation per `mono.rs:40-42`. -- `subst.rs` dead-code handling — plan calls for outright deletion of unused fns, see Task 9. - ---- - -## Task 1: `MonoTarget` shape extension for free-fn variant - -**Files:** -- Modify: `crates/ailang-check/src/mono.rs:351-372` — `MonoTarget` struct + `mono_target_key`. -- Test: `crates/ail/tests/typeclass_22b3.rs` — add `mono_target_key_distinguishes_class_method_from_free_fn`. - -- [ ] **Step 1.1: Write the failing test** - -Add to `crates/ail/tests/typeclass_22b3.rs`: - -```rust -#[test] -fn mono_target_key_distinguishes_class_method_from_free_fn() { - use ailang_check::mono::{MonoTarget, mono_target_key}; - use ailang_core::ast::Type; - - let int_ty = Type::Con { name: "Int".into() }; - - let class_method_target = MonoTarget::class_method( - "Eq".into(), - "eq".into(), - int_ty.clone(), - "prelude".into(), - ); - let free_fn_target = MonoTarget::free_fn( - "ne".into(), - vec![int_ty.clone()], - "prelude".into(), - ); - - let k1 = mono_target_key(&class_method_target); - let k2 = mono_target_key(&free_fn_target); - assert_ne!(k1, k2, "free-fn and class-method targets must have distinct keys"); -} -``` - -- [ ] **Step 1.2: Run test to verify it fails** - -Run: `cargo test --workspace -p ail mono_target_key_distinguishes_class_method_from_free_fn` -Expected: FAIL with compile error — `MonoTarget::class_method` and `MonoTarget::free_fn` constructors don't exist; `mono_target_key` may not be `pub`. - -- [ ] **Step 1.3: Restructure `MonoTarget` and `mono_target_key`** - -Edit `crates/ailang-check/src/mono.rs:351-372`. The struct currently is: - -```rust -pub struct MonoTarget { - pub class: String, - pub method: String, - pub type_: Type, - pub defining_module: String, -} -``` - -Replace with the implementer's choice of enum or Option-shaped variant. Per spec Open commitment, both are viable. The constructor surface MUST provide: - -```rust -impl MonoTarget { - pub fn class_method(class: String, method: String, type_: Type, defining_module: String) -> Self { /* ... */ } - pub fn free_fn(fn_name: String, type_subst: Vec, defining_module: String) -> Self { /* ... */ } - pub fn defining_module(&self) -> &str { /* ... */ } - pub fn type_for_synth(&self) -> &Type { /* ... — class_method returns type_; free_fn returns first subst or canonical hash anchor */ } -} -``` - -Update `mono_target_key` to a discriminating shape, e.g.: - -```rust -pub(crate) fn mono_target_key(t: &MonoTarget) -> (String, String, String) { - // For class-method: (class, method, type-hash) - // For free-fn: ("__freefn__", fn_name, concat-type-hashes) - // The first component discriminates; the rest is the identifier-tuple. - // ... implementer fills in based on chosen MonoTarget shape. -} -``` - -NOTE: Keep all today's class-method-shaped call sites compiling — every constructor of `MonoTarget` in this file (line 503 in `collect_mono_targets`, line 139 in `monomorphise_workspace`'s loop) must adapt to the new shape. This is mechanical adaptation, not redesign — the class-method constructor path stays identical in semantics. - -- [ ] **Step 1.4: Run test to verify it passes** - -Run: `cargo test --workspace -p ail mono_target_key_distinguishes_class_method_from_free_fn` -Expected: PASS - -- [ ] **Step 1.5: Verify no regression** - -Run: `cargo test --workspace -p ailang-check` -Expected: PASS (all 22b.3 tests still green; `MonoTarget` reshape preserves class-method behaviour). - -- [ ] **Step 1.6: Commit** - -```bash -git add crates/ailang-check/src/mono.rs crates/ail/tests/typeclass_22b3.rs -git commit -m "iter 23.4.1: MonoTarget shape — accommodate free-fn variant" -``` - ---- - -## Task 2: `mono_symbol` extension for N-ary type vars - -**Files:** -- Modify: `crates/ailang-check/src/mono.rs:319-343` — `mono_symbol` + `primitive_surface_name`. -- Test: `crates/ail/tests/typeclass_22b3.rs` — add `mono_symbol_handles_n_ary_type_vars`. - -- [ ] **Step 2.1: Write the failing test** - -Add to `crates/ail/tests/typeclass_22b3.rs`: - -```rust -#[test] -fn mono_symbol_handles_n_ary_type_vars() { - use ailang_check::mono::mono_symbol_n_ary; - use ailang_core::ast::Type; - - let int_ty = Type::Con { name: "Int".into() }; - let bool_ty = Type::Con { name: "Bool".into() }; - - // Two-ary case: poly_apply with (a=Int, b=Bool). - let sym = mono_symbol_n_ary("apply", &[int_ty.clone(), bool_ty.clone()]); - assert_eq!(sym, "apply__Int__Bool"); - - // Single-arity case matches the existing single-var helper. - let sym = mono_symbol_n_ary("id", &[int_ty.clone()]); - assert_eq!(sym, "id__Int"); -} -``` - -- [ ] **Step 2.2: Run test to verify it fails** - -Run: `cargo test --workspace -p ail mono_symbol_handles_n_ary_type_vars` -Expected: FAIL with compile error — `mono_symbol_n_ary` does not exist. - -- [ ] **Step 2.3: Add `mono_symbol_n_ary`** - -Add to `crates/ailang-check/src/mono.rs` near `mono_symbol` (line 319): - -```rust -/// Iter 23.4: N-ary mono-symbol naming for polymorphic free fns. -/// `______…` in Forall-vars declaration -/// order. Each `typename_i` is `primitive_surface_name(t)` if `t` is a -/// primitive, otherwise the 8-hex prefix of `type_hash(t)` (mirrors -/// the existing single-var `mono_symbol` fallback). -pub fn mono_symbol_n_ary(fn_name: &str, type_substs: &[Type]) -> String { - let mut parts: Vec = Vec::with_capacity(type_substs.len() + 1); - parts.push(fn_name.to_string()); - for t in type_substs { - parts.push(match primitive_surface_name(t) { - Some(name) => name.to_string(), - None => format!("{:016x}", ailang_core::canonical::type_hash(t))[..8].to_string(), - }); - } - parts.join("__") -} -``` - -Verify the single-var case still matches `mono_symbol` semantics — both should return identical strings for a single-type-var input. - -- [ ] **Step 2.4: Run test to verify it passes** - -Run: `cargo test --workspace -p ail mono_symbol_handles_n_ary_type_vars` -Expected: PASS - -- [ ] **Step 2.5: Commit** - -```bash -git add crates/ailang-check/src/mono.rs crates/ail/tests/typeclass_22b3.rs -git commit -m "iter 23.4.2: mono_symbol_n_ary — naming for polymorphic free fns" -``` - ---- - -## Task 3: Free-fn synth path in `synthesise_mono_fn` - -**Files:** -- Modify: `crates/ailang-check/src/mono.rs:539-606` — `synthesise_mono_fn` + new `synthesise_mono_fn_free`. -- Test: `crates/ail/tests/typeclass_22b3.rs` — add `synthesise_mono_fn_free_produces_substituted_body`. - -- [ ] **Step 3.1: Write the failing test** - -Add to `crates/ail/tests/typeclass_22b3.rs`: - -```rust -#[test] -fn synthesise_mono_fn_free_produces_substituted_body() { - use ailang_check::mono::{MonoTarget, synthesise_mono_fn_free}; - use ailang_core::ast::{FnDef as AstFnDef, Type, Term}; - - let int_ty = Type::Con { name: "Int".into() }; - - // Source: polymorphic fn id : forall a. (a) -> a, body = x. - let source_fn = AstFnDef { - name: "id".into(), - ty: Type::Forall { - vars: vec!["a".into()], - constraints: vec![], - body: Box::new(Type::Fn { - params: vec![Type::Var { name: "a".into() }], - ret: Box::new(Type::Var { name: "a".into() }), - effects: vec![], - }), - }, - params: vec!["x".into()], - body: Term::Var { name: "x".into() }, - }; - - let target = MonoTarget::free_fn("id".into(), vec![int_ty.clone()], "test".into()); - let synthesised = synthesise_mono_fn_free(&target, &source_fn).unwrap(); - - assert_eq!(synthesised.name, "id__Int"); - match &synthesised.ty { - Type::Fn { params, ret, .. } => { - assert_eq!(params, &vec![int_ty.clone()]); - assert_eq!(**ret, int_ty); - } - other => panic!("expected Type::Fn after substitution, got {other:?}"), - } - // Body unchanged (no class-method calls to rewrite at synth time; - // call-site rewriter handles those in Phase 3 of the pass). - assert!(matches!(synthesised.body, Term::Var { ref name } if name == "x")); -} -``` - -- [ ] **Step 3.2: Run test to verify it fails** - -Run: `cargo test --workspace -p ail synthesise_mono_fn_free_produces_substituted_body` -Expected: FAIL with compile error — `synthesise_mono_fn_free` does not exist. - -- [ ] **Step 3.3: Add `synthesise_mono_fn_free`** - -Add to `crates/ailang-check/src/mono.rs` after `synthesise_mono_fn` (line 606+): - -```rust -/// Iter 23.4: synth a monomorphic FnDef for a polymorphic free fn at -/// a concrete substitution. Mirror of `synthesise_mono_fn` for the -/// non-class-method case: body is taken directly from the source Def, -/// rigid-vars in the type are substituted via `crate::substitute_rigids`. -/// -/// The body is NOT walked here — nested class-method calls and other -/// polymorphic-free-fn calls are rewritten in Phase 3 of -/// `monomorphise_workspace`, the same way they are for synthesised -/// class-method bodies. -/// -/// Errors are `CheckError::Internal` — caller-contract violations. -pub fn synthesise_mono_fn_free( - target: &MonoTarget, - source_fn: &AstFnDef, -) -> Result { - // Verify target shape matches caller contract. - let type_substs = target.free_fn_type_substs().ok_or_else(|| { - crate::CheckError::Internal( - "synthesise_mono_fn_free: target is not a free-fn target".into(), - ) - })?; - - let (forall_vars, inner_ty) = match &source_fn.ty { - Type::Forall { vars, body, .. } => (vars.clone(), (**body).clone()), - other => return Err(crate::CheckError::Internal(format!( - "synthesise_mono_fn_free: source fn `{}` is not polymorphic, ty = {:?}", - source_fn.name, other, - ))), - }; - - if forall_vars.len() != type_substs.len() { - return Err(crate::CheckError::Internal(format!( - "synthesise_mono_fn_free: arity mismatch — forall has {} vars, target has {} substs", - forall_vars.len(), type_substs.len(), - ))); - } - - // Substitute every rigid-var in the Type::Fn body. Reuse - // crate::substitute_rigids — same call shape used by - // synthesise_mono_fn for class-method types. - let mut substituted_ty = inner_ty; - for (var, subst) in forall_vars.iter().zip(type_substs.iter()) { - substituted_ty = crate::substitute_rigids(&substituted_ty, var, subst); - } - - Ok(AstFnDef { - name: mono_symbol_n_ary(&source_fn.name, type_substs), - ty: substituted_ty, - params: source_fn.params.clone(), - body: source_fn.body.clone(), - }) -} -``` - -Add a helper on `MonoTarget`: - -```rust -impl MonoTarget { - /// Iter 23.4: free-fn substitutions, in Forall-vars declaration order. - /// Returns `None` for class-method targets. - pub fn free_fn_type_substs(&self) -> Option<&[Type]> { /* implementer fills in */ } -} -``` - -- [ ] **Step 3.4: Run test to verify it passes** - -Run: `cargo test --workspace -p ail synthesise_mono_fn_free_produces_substituted_body` -Expected: PASS - -- [ ] **Step 3.5: Verify class-method synth regression** - -Run: `cargo test --workspace -p ail synthesise_mono_fn_uses_instance_body_lam` -Expected: PASS — the unrelated `synthesise_mono_fn` path is untouched. - -- [ ] **Step 3.6: Commit** - -```bash -git add crates/ailang-check/src/mono.rs crates/ail/tests/typeclass_22b3.rs -git commit -m "iter 23.4.3: synthesise_mono_fn_free — substituted body for poly free fns" -``` - ---- - -## Task 4: Free-fn target collection in `collect_mono_targets` + `collect_residuals_ordered` - -**Files:** -- Modify: `crates/ailang-check/src/mono.rs:422-511` — `collect_mono_targets`. -- Modify: `crates/ailang-check/src/mono.rs:793-870` — `collect_residuals_ordered`. -- Test: `crates/ail/tests/typeclass_22b3.rs` — add `collect_mono_targets_emits_free_fn_target_for_concrete_call`. - -- [ ] **Step 4.1: Write the failing test** - -Add to `crates/ail/tests/typeclass_22b3.rs`: - -```rust -#[test] -fn collect_mono_targets_emits_free_fn_target_for_concrete_call() { - // Workspace: prelude with `fn id : forall a. (a) -> a, body = x`. - // Driver module with `fn main : () -> Int, body = id(42)`. - // collect_mono_targets on `main` must emit a free-fn target for - // id at type [Int]. - use ailang_check::mono::{collect_mono_targets, build_workspace_env}; - let ws = build_test_workspace_with_poly_id_call(); - - let env = build_workspace_env(&ws); - let driver_module = ws.modules.get("driver").unwrap(); - let main_fn = driver_module.defs.iter().find_map(|d| match d { - ailang_core::ast::Def::Fn(f) if f.name == "main" => Some(f), - _ => None, - }).unwrap(); - - let targets = collect_mono_targets(main_fn, "driver", &env).unwrap(); - - let has_free_fn_target = targets.iter().any(|t| { - t.free_fn_name() == Some("id") && - t.free_fn_type_substs() == Some(&[Type::Con { name: "Int".into() }][..]) - }); - assert!(has_free_fn_target, "expected free-fn target for id at Int; got {:?}", targets); -} - -// Helper: a minimal workspace with prelude::id (polymorphic) and -// driver::main calling id(42). Implementer mirrors the structure of -// the existing `build_test_workspace_with_class_method_call` helper -// at typeclass_22b3.rs:N (find existing helper via grep). -fn build_test_workspace_with_poly_id_call() -> Workspace { /* fill in */ } -``` - -- [ ] **Step 4.2: Run test to verify it fails** - -Run: `cargo test --workspace -p ail collect_mono_targets_emits_free_fn_target_for_concrete_call` -Expected: FAIL — collector doesn't yet detect polymorphic free-fn calls. (May fail with compile error if `free_fn_name` helper isn't defined; add it as a thin accessor on `MonoTarget` alongside `free_fn_type_substs` from Task 3.) - -- [ ] **Step 4.3: Extend `collect_mono_targets` with polymorphic-free-fn-call detection** - -Per spec Open commitment, two viable shapes: - -Option A — extend `synth`'s residual mechanism to surface polymorphic-free-fn-call residuals alongside class-constraint residuals. This is the deeper change; consult `crates/ailang-check/src/lib.rs` `synth` arms. - -Option B — add a separate post-synth AST walker over `f.body`. Pattern-match `Term::App { callee: Term::Var { name }, .. }` where `name` resolves to a `Type::Forall` `Def::Fn` in `env.module_globals` (per-module, including implicit-import fall-through per prep1 commit `aef4ab8`). Use `synth`'s subst to recover the concrete type substitution at the call site. - -Implementer picks A or B. Option B is the smaller delta and recommended unless plan-recon discoveries argue otherwise. Either way, the new code emits `MonoTarget::free_fn(...)` entries into `out` alongside the existing class-constraint targets (mono.rs:503). - -- [ ] **Step 4.4: Mirror the extension in `collect_residuals_ordered`** - -`collect_residuals_ordered` (mono.rs:793-870) MUST emit an aligned `Option` slot for every polymorphic-free-fn call site, in the same pre-order as `rewrite_class_method_calls` walks. Otherwise the cursor alignment Phase 3 depends on breaks. Mirror whichever mechanism (A or B) was chosen in Step 4.3. - -- [ ] **Step 4.5: Run test to verify it passes** - -Run: `cargo test --workspace -p ail collect_mono_targets_emits_free_fn_target_for_concrete_call` -Expected: PASS - -- [ ] **Step 4.6: Verify class-method-side regression** - -Run: `cargo test --workspace -p ail typeclass_22b3` -Expected: PASS — all existing 22b.3 tests still green. - -- [ ] **Step 4.7: Commit** - -```bash -git add crates/ailang-check/src/mono.rs crates/ail/tests/typeclass_22b3.rs -git commit -m "iter 23.4.4: collect_mono_targets — emit free-fn targets for concrete calls" -``` - ---- - -## Task 5: Free-fn call-site rewrite in `rewrite_class_method_calls` - -**Files:** -- Modify: `crates/ailang-check/src/mono.rs:637-764` — `rewrite_class_method_calls`. -- Test: `crates/ail/tests/typeclass_22b3.rs` — add `rewrite_replaces_free_fn_call_site_with_mono_symbol`. - -- [ ] **Step 5.1: Write the failing test** - -Add to `crates/ail/tests/typeclass_22b3.rs`: - -```rust -#[test] -fn rewrite_replaces_free_fn_call_site_with_mono_symbol() { - use ailang_check::mono::{rewrite_class_method_calls, MonoTarget}; - use ailang_core::ast::{Term, Type}; - use std::collections::BTreeSet; - - // Body: id(42) - let mut body = Term::App { - callee: Box::new(Term::Var { name: "id".into() }), - args: vec![Term::Lit { /* Int 42 */ }], - }; - let int_ty = Type::Con { name: "Int".into() }; - let ordered = vec![Some(MonoTarget::free_fn( - "id".into(), - vec![int_ty.clone()], - "prelude".into(), - ))]; - let class_methods = Default::default(); - let mut cursor = 0; - let mut locals = BTreeSet::new(); - - rewrite_class_method_calls( - &mut body, - &class_methods, - "driver", - &ordered, - &mut cursor, - &mut locals, - ); - - match &body { - Term::App { callee, .. } => match callee.as_ref() { - Term::Var { name } => assert_eq!(name, "id__Int"), - other => panic!("expected Term::Var callee after rewrite, got {other:?}"), - }, - other => panic!("expected Term::App, got {other:?}"), - } -} -``` - -- [ ] **Step 5.2: Run test to verify it fails** - -Run: `cargo test --workspace -p ail rewrite_replaces_free_fn_call_site_with_mono_symbol` -Expected: FAIL — rewriter does not yet handle free-fn targets. - -- [ ] **Step 5.3: Extend `rewrite_class_method_calls`** - -Edit `crates/ailang-check/src/mono.rs:637-764`. The `Term::App { callee: Term::Var { name }, .. }` arm currently consults `class_methods` only (around lines 646-668). Extend so that when the `ordered[cursor]` slot is `Some(MonoTarget::FreeFn { ... })`, the callee is rewritten to `Term::Var { name: mono_symbol_n_ary(fn_name, type_substs) }`. - -The function name `rewrite_class_method_calls` becomes a misnomer; rename to `rewrite_mono_targets_at_call_sites`. Update all call sites: -- `monomorphise_workspace` Phase 3 at `mono.rs:192-209` (two sites: `Def::Fn` and `Def::Const` branches). - -- [ ] **Step 5.4: Run test to verify it passes** - -Run: `cargo test --workspace -p ail rewrite_replaces_free_fn_call_site_with_mono_symbol` -Expected: PASS - -- [ ] **Step 5.5: Verify class-method-side regression** - -Run: `cargo test --workspace -p ail typeclass_22b3` -Expected: PASS - -- [ ] **Step 5.6: Commit** - -```bash -git add crates/ailang-check/src/mono.rs crates/ail/tests/typeclass_22b3.rs -git commit -m "iter 23.4.5: rewrite_mono_targets_at_call_sites — also rewrite free-fn calls" -``` - ---- - -## Task 6: Wire up the fixpoint dispatch + composition E2E - -**Files:** -- Modify: `crates/ailang-check/src/mono.rs:67-217` — `monomorphise_workspace` fixpoint dispatch. -- Create: `examples/cmp_max_smoke.ail.json`. -- Test: `crates/ail/tests/e2e.rs` — add `cmp_max_smoke_synthesises_both_mono_defs_in_one_fixpoint`. - -- [ ] **Step 6.1: Create `examples/cmp_max_smoke.ail.json`** - -Composition fixture: a polymorphic free fn whose body calls a class method, then a `main` that invokes it at `Int`. Structurally mirror `examples/test_22b3_chained_calls.ail.json` (per recon). - -```json -{ - "schema": "ailang/v0", - "name": "cmp_max_smoke", - "imports": [], - "defs": [ - { - "kind": "fn", - "name": "cmp_max", - "type": { - "k": "forall", - "vars": ["a"], - "constraints": [{"class": "Ord", "type": {"k": "var", "name": "a"}}], - "body": { - "k": "fn", - "params": [{"k": "var", "name": "a"}, {"k": "var", "name": "a"}], - "ret": {"k": "var", "name": "a"}, - "effects": [] - } - }, - "params": ["x", "y"], - "body": { /* match compare x y with LT -> y | _ -> x */ } - }, - { - "kind": "fn", - "name": "main", - "type": {"k": "fn", "params": [], "ret": {"k": "con", "name": "Int"}, "effects": []}, - "params": [], - "body": { /* cmp_max 3 7 */ } - } - ] -} -``` - -Implementer fills in the `body` Term shapes using the canonical Form-A schema — mirror `test_22b3_chained_calls.ail.json` for the match-statement body and `eq_primitives_smoke.ail.json` for the toplevel-call body. Expected stdout: `7`. - -- [ ] **Step 6.2: Write the failing test** - -Add to `crates/ail/tests/e2e.rs`: - -```rust -#[test] -fn cmp_max_smoke_synthesises_both_mono_defs_in_one_fixpoint() { - let stdout = build_and_run("cmp_max_smoke.ail.json"); - assert_eq!(stdout.trim(), "7"); -} - -#[test] -fn cmp_max_smoke_workspace_contains_cmp_max_int_and_compare_int_defs() { - use ailang_check::mono::monomorphise_workspace; - let ws = load_workspace_for_fixture("cmp_max_smoke.ail.json"); - let ws = monomorphise_workspace(&ws).unwrap(); - - let cmp_max_module = ws.modules.get("cmp_max_smoke").unwrap(); - let has_cmp_max_int = cmp_max_module.defs.iter().any(|d| matches!(d, - ailang_core::ast::Def::Fn(f) if f.name == "cmp_max__Int" - )); - - let prelude_module = ws.modules.get("prelude").unwrap(); - let has_compare_int = prelude_module.defs.iter().any(|d| matches!(d, - ailang_core::ast::Def::Fn(f) if f.name == "compare__Int" - )); - - assert!(has_cmp_max_int, "expected cmp_max__Int as workspace Def::Fn"); - assert!(has_compare_int, "expected compare__Int as workspace Def::Fn (nested specialisation)"); -} -``` - -- [ ] **Step 6.3: Run test to verify it fails** - -Run: `cargo test --workspace -p ail cmp_max_smoke` -Expected: FAIL — the fixpoint loop body in `monomorphise_workspace` (lines 116-151) currently calls `synthesise_mono_fn` unconditionally with `class_def` + `&entry.instance`; this errors on free-fn targets that have no class. - -- [ ] **Step 6.4: Branch the fixpoint synth dispatch** - -Edit `crates/ailang-check/src/mono.rs:116-151`. Replace the unconditional class-method synth with branching: - -```rust -for t in &new { - let key = mono_target_key(t); - let f: AstFnDef = if let Some(_class_name) = t.class_method_class() { - // Class-method target — existing path. - let t_ty_norm = ws_owned.registry.normalize_type_for_lookup(t.type_for_synth()); - let registry_key = (t.class_method_class().unwrap().to_string(), - ailang_core::canonical::type_hash(&t_ty_norm)); - let entry = ws_owned.registry.entries.get(®istry_key).ok_or_else(|| { - crate::CheckError::Internal(format!( - "monomorphise_workspace: target `{} {}` has no registry entry", - t.class_method_class().unwrap(), - ailang_core::pretty::type_to_string(t.type_for_synth()), - )) - })?; - let class_def = class_index.get(t.class_method_class().unwrap()).ok_or_else(|| { - crate::CheckError::Internal(format!( - "monomorphise_workspace: class `{}` not found", - t.class_method_class().unwrap(), - )) - })?; - synthesise_mono_fn(t, class_def, &entry.instance)? - } else { - // Free-fn target — new path. - let fn_name = t.free_fn_name().ok_or_else(|| { - crate::CheckError::Internal("monomorphise_workspace: target shape unrecognised".into()) - })?; - let module = ws_owned.modules.get(t.defining_module()).ok_or_else(|| { - crate::CheckError::Internal(format!( - "monomorphise_workspace: defining module `{}` missing", - t.defining_module(), - )) - })?; - let source_fn = module.defs.iter().find_map(|d| match d { - Def::Fn(f) if f.name == fn_name => Some(f), - _ => None, - }).ok_or_else(|| { - crate::CheckError::Internal(format!( - "monomorphise_workspace: source fn `{}::{}` not found", - t.defining_module(), fn_name, - )) - })?; - synthesise_mono_fn_free(t, source_fn)? - }; - - let target_module = ws_owned.modules.get_mut(t.defining_module()).ok_or_else(|| { - crate::CheckError::Internal(format!( - "monomorphise_workspace: defining module `{}` missing", - t.defining_module(), - )) - })?; - target_module.defs.push(Def::Fn(f)); - synthesised.insert(key); -} -``` - -Update the module-level doc-comment block at `mono.rs:1-52` to describe the unified shape — the existing wording ("class-free workspaces", "class-method calls only", "Synthesised `Def::Fn` entries — one per unique `(class, method, type-hash)` triple") becomes inaccurate. Rewrite to "any `Type::Forall`-quantified `Def::Fn`". - -- [ ] **Step 6.5: Run tests to verify they pass** - -Run: `cargo test --workspace -p ail cmp_max_smoke` -Expected: PASS (both `cmp_max_smoke_synthesises_both_mono_defs_in_one_fixpoint` and `cmp_max_smoke_workspace_contains_cmp_max_int_and_compare_int_defs`). - -- [ ] **Step 6.6: Run full workspace tests** - -Run: `cargo test --workspace` -Expected: PASS. Codegen still routes polymorphic-free-fn calls through `lower_polymorphic_call` at this point (Task 9 removes that path); the call-site rewriter in Phase 3 produces the mono-symbol name, but the polymorphic-free-fn map in codegen still recognises the (now-rewritten) symbol as a non-polymorphic name, falling through to direct `module_user_fns` lookup. No collision expected. If a test fails here, BLOCK to Boss — the integration is non-trivial. - -- [ ] **Step 6.7: Commit** - -```bash -git add crates/ailang-check/src/mono.rs crates/ail/tests/e2e.rs examples/cmp_max_smoke.ail.json -git commit -m "iter 23.4.6: monomorphise_workspace — branch synth dispatch on target shape" -``` - ---- - -## Task 7: Verify existing polymorphic fixtures stay green - -**Files:** -- Test: `crates/ail/tests/e2e.rs` — no code change; verify-only. - -- [ ] **Step 7.1: Run the three regression fixtures** - -```bash -cargo test --workspace -p ail polymorphic_id_at_int_and_bool -cargo test --workspace -p ail polymorphic_apply_with_fn_param -cargo test --workspace -p ail poly_rec_capture_demo -``` - -Expected: all PASS. These three are the load-bearing proof that codegen-time specialisation was structurally replaceable. They previously routed through `lower_polymorphic_call`; under the unified pass, the calls are rewritten by mono Phase 3 to direct mono-symbol calls, which `lower_call` resolves via `module_user_fns`. - -If any fails, BLOCK to Boss. The unification has a regression to debug. - -- [ ] **Step 7.2: Run the full typeclass_22b3 suite** - -```bash -cargo test --workspace -p ail typeclass_22b3 -``` - -Expected: PASS. All seven pre-existing class-method-side mono tests still green. - -- [ ] **Step 7.3: No commit (verify-only task)** - -No file changes in this task. Proceed to Task 8. - ---- - -## Task 8: Hash-stability pin for existing mono symbols - -**Files:** -- Modify: `crates/ailang-core/src/hash.rs:270-336` — append new test. -- Test: `crates/ailang-core/src/hash.rs::tests::mono_symbols_hash_stable_under_unified_pass`. - -- [ ] **Step 8.1: Capture the post-Task-6 hash values** - -Run a small driver locally that prints the canonical hashes of `eq__Int`, `compare__Int`, `eq__Str`, `compare__Str`, `eq__Bool`, `compare__Bool` from the mono'd `examples/prelude.ail.json`. The implementer instruments `monomorphise_workspace` temporarily or uses an existing `ail manifest`-style path. - -- [ ] **Step 8.2: Write the pin test** - -Append to `crates/ailang-core/src/hash.rs` after `ct4_unmigrated_fixtures_remain_bit_identical` (lines 319-335). Structural mirror of `ct4_migrated_fixtures_have_canonical_form_hashes`: - -```rust -#[test] -fn mono_symbols_hash_stable_under_unified_pass() { - // Iter 23.4: pin the canonical hashes of the six mono symbols - // shipped in iters 23.2 (eq__T) and 23.3 (compare__T). The - // unified mono pass must produce identical bodies (and therefore - // identical hashes) to the class-method-only pass that preceded - // it. A drift here means the unification changed observable - // class-method specialisation, which the spec explicitly - // forbids (§"Hash-stability of existing mono symbols"). - use ailang_check::mono::monomorphise_workspace; - use ailang_core::workspace::Workspace; - - let prelude = load_prelude_workspace(); // helper from tests - let mono_ws = monomorphise_workspace(&prelude).unwrap(); - let prelude_module = mono_ws.modules.get("prelude").unwrap(); - - let expected_hashes: &[(&str, &str)] = &[ - ("eq__Int", ""), - ("eq__Bool", ""), - ("eq__Str", ""), - ("compare__Int", ""), - ("compare__Bool", ""), - ("compare__Str", ""), - ]; - - for (name, expected) in expected_hashes { - let def = prelude_module.defs.iter().find_map(|d| match d { - ailang_core::ast::Def::Fn(f) if &f.name == name => Some(f), - _ => None, - }).unwrap_or_else(|| panic!("mono symbol `{}` not found in prelude module post-mono", name)); - - let actual = format!("{:016x}", ailang_core::canonical::fn_hash(def)); - assert_eq!(&actual, expected, "mono symbol `{}` hash drifted from baseline", name); - } -} -``` - -- [ ] **Step 8.3: Run test to verify it passes** - -Run: `cargo test --workspace -p ailang-core mono_symbols_hash_stable_under_unified_pass` -Expected: PASS — values pinned to the captured values from Step 8.1. - -- [ ] **Step 8.4: Commit** - -```bash -git add crates/ailang-core/src/hash.rs -git commit -m "iter 23.4.8: hash.rs — pin mono-symbol hashes under unified pass" -``` - ---- - -## Task 9: Codegen removal — `lower_polymorphic_call` + `module_polymorphic_fns` + `mono_queue` - -**Files:** -- Modify: `crates/ailang-codegen/src/lib.rs` — multiple line ranges (see recon). -- Modify: `crates/ailang-codegen/src/subst.rs` — delete `apply_subst_to_term`, `descriptor_for_subst`. - -- [ ] **Step 9.1: Pre-removal smoke** - -```bash -cargo test --workspace -``` - -Expected: PASS. This is the baseline; the removal must not regress it. - -- [ ] **Step 9.2: Remove codegen-time specialiser code** - -Apply in `crates/ailang-codegen/src/lib.rs`: - -| Line range (pre-edit) | Action | -|---|---| -| 52 | Edit import: keep `apply_subst_to_type, derive_substitution,` (still used by `synth_arg_type` at ~2833-2834); remove `apply_subst_to_term, descriptor_for_subst,`. | -| 297 | Remove `let mut module_polymorphic_fns: BTreeMap<...> = BTreeMap::new();`. | -| 312 | Remove the `let mut poly_fns: BTreeMap<...> = BTreeMap::new();` init inside Pass 1. | -| 325-330 | Remove the `Type::Forall { .. } => { poly_fns.insert(...) }` arm in the `Def::Fn` walk. | -| 363 | Remove `module_polymorphic_fns.insert(mname.clone(), poly_fns);`. | -| 399 | Remove `&module_polymorphic_fns,` argument from `Emitter::new` call. | -| 560 | Remove `module_polymorphic_fns: &'a BTreeMap<...>` field from `Emitter`. | -| 565-566 | Remove `mono_queue: Vec<(String, String, BTreeMap, String)>` and `mono_emitted: BTreeSet<(String, String, String)>` fields. | -| 726 | Remove `module_polymorphic_fns: &'a BTreeMap<...>` parameter from `Emitter::new` signature. | -| 777-779 | Remove `module_polymorphic_fns,`, `mono_queue: Vec::new(),`, `mono_emitted: BTreeSet::new(),` initialisers. | -| 842-850 | Remove the `while let Some((owner_module, def_name, subst, descriptor)) = self.mono_queue.pop() { self.emit_specialised_fn(...) }` block in `emit_module`. | -| 893-950 | Remove the `emit_specialised_fn` function entirely. | -| 1939-1945 | Remove the cross-module `if self.module_polymorphic_fns.get(&target_module)...` early-return in `lower_call`. | -| 1965-1973 | Remove the current-module `if self.module_polymorphic_fns.get(self.module_name)...` early-return in `lower_call`. | -| 1994-2100+ | Remove `lower_polymorphic_call` function entirely (~200 LOC). | -| 2260-2263 | Remove the `self.module_polymorphic_fns.get(self.module_name).is_some_and(...)` arm in `is_static_callee`. The function returns `false` on the unmatched path. | - -Apply in `crates/ailang-codegen/src/subst.rs`: - -- Delete `apply_subst_to_term` function (entire body). -- Delete `descriptor_for_subst` function (entire body). -- `apply_subst_to_type` and `derive_substitution` stay — both still used. - -- [ ] **Step 9.3: Run cargo build to verify clean compile** - -```bash -cargo build --workspace -``` - -Expected: clean compile. If any `unused import` or `dead code` warnings fire, fix at this step (e.g. drop `use` lines that pointed to removed items). - -- [ ] **Step 9.4: Run full workspace tests** - -```bash -cargo test --workspace -``` - -Expected: PASS — same set as Step 9.1, no regression. The mono pass's call-site rewrite is now load-bearing for every polymorphic-free-fn call; if any fixture fails, BLOCK to Boss. - -- [ ] **Step 9.5: Commit** - -```bash -git add crates/ailang-codegen/src/lib.rs crates/ailang-codegen/src/subst.rs -git commit -m "iter 23.4.9: codegen — remove lower_polymorphic_call + module_polymorphic_fns + mono_queue" -``` - ---- - -## Task 10: Revert prep2 + prep3 - -**Files:** -- Revert: commit `a06159d` (iter 23.4-prep2). -- Revert: commit `c42a0f5` (iter 23.4-prep3). - -- [ ] **Step 10.1: Pre-revert smoke** - -```bash -cargo test --workspace -``` - -Expected: PASS. Baseline. - -- [ ] **Step 10.2: Revert prep3 first, then prep2** - -Recon verified `git log a06159d..HEAD -- crates/ailang-codegen/src/lib.rs` is empty (no intervening commits touched the file). Reverts apply cleanly. - -```bash -git revert --no-edit c42a0f5 -git revert --no-edit a06159d -``` - -If either revert reports a merge conflict, BLOCK to Boss. The recon expected clean reverts; a conflict means the workspace has drifted in a way recon did not predict (likely from this iter's earlier tasks touching adjacent regions). - -- [ ] **Step 10.3: Run cargo test to verify revert is benign** - -```bash -cargo test --workspace -``` - -Expected: PASS. The two prep commits added codegen bare-name fall-through arms + their tests. After mono unification, both arms are dead code (no call site reaches them) and the tests were testing the dead arms directly — they get removed by the revert. No other test relies on the reverted code. - -- [ ] **Step 10.4: No additional commit needed** - -`git revert --no-edit` creates two revert commits (`iter 23.4.10a: Revert "iter 23.4-prep3..."` and `iter 23.4.10b: Revert "iter 23.4-prep2..."`); rename them with `git commit --amend` if cleaner messages are wanted: - -```bash -git log -2 --oneline -# If renaming desired: -# git commit --amend -m "iter 23.4.10: revert prep2 + prep3 — codegen bare-name fall-throughs now dead" -# (one amend per revert; or fold both reverts into one if the implementer prefers a single squash) -``` - -Default: keep the two reverts as separate commits matching `git revert`'s output. - ---- - -## Task 11: DESIGN.md amendment — §"Resolution and monomorphisation" - -**Files:** -- Modify: `docs/DESIGN.md:1513-1556`. - -- [ ] **Step 11.1: Edit the section** - -Current text at line 1513-1528 describes the pass on `(method, concrete-type)` pairs and "the IR contains no class machinery". The amendment must say the pass covers all `Type::Forall`-quantified `Def::Fn`. Replace the first paragraph of "Monomorphisation (post-typecheck, pre-codegen)" (lines 1513-1528) with: - -```markdown -**Monomorphisation (post-typecheck, pre-codegen).** A pass between -typechecking and codegen replaces every resolved polymorphic call — -both class-method calls and polymorphic-free-fn calls — with a call -to a synthesised monomorphic `FnDef`. The pass operates on all -`Type::Forall`-quantified `Def::Fn`s. - -For each unique target encountered: - -1. Synthesises a top-level `FnDef` named deterministically from the - target identifier and the canonical hash of the instance type(s). - For class methods, the source body is the resolved instance - method (or default body), with the class parameter substituted - to the concrete type. For polymorphic free fns, the source body - is the polymorphic `Def::Fn`'s body, with the Forall-bound type - variables substituted to their concrete types at the call site. -2. Caches the synthesised def by a structurally-discriminated key so - the same target is not emitted twice. -3. Rewrites the original `Call` to target the synthesised name. - -After this pass, the IR contains no class machinery and no -polymorphic call sites — only ordinary monomorphic functions and -direct calls. Codegen sees no difference between a hand-written -`show_int`, a synthesised `show__Int` (from a class-method -specialisation), or a synthesised `id__Int` (from a polymorphic- -free-fn specialisation). -``` - -Verify the surrounding paragraphs ("Why mono, not virtual dispatch" at line 1530+, separator paragraph, "No runtime dispatch, no dictionary passing" at line 1550+) still read consistently — adjust pronouns ("the call", "a call that cannot be monomorphised") if they tacitly assumed class-methods-only. The "No runtime dispatch" claim is strengthened: codegen now has zero polymorphism path. - -- [ ] **Step 11.2: Verify DESIGN.md round-trip stays green** - -Run: `cargo test --workspace -p ailang-prose round_trip` -Expected: PASS — DESIGN.md isn't prose-processed but the amendment must not break neighbouring markdown. - -- [ ] **Step 11.3: Commit** - -```bash -git add docs/DESIGN.md -git commit -m "iter 23.4.11: DESIGN.md — Resolution and monomorphisation covers all Type::Forall Defs" -``` - ---- - -## Final regression smoke: F.1 - F.4 - -- [ ] **F.1: Full workspace test suite** - -```bash -cargo test --workspace -``` - -Expected: PASS. All pre-existing tests green; the seven new tests (Tasks 1-8) green; the two prep-revert removals reflected in counts. - -- [ ] **F.2: Bench regression** - -```bash -python3 bench/check.py -python3 bench/compile_check.py -python3 bench/cross_lang.py -``` - -Expected: each script exit 0, or audit-ratified with a follow-up JOURNAL entry naming the metric and rationale. Spec §"Bench regression check" anticipates a compile-time shift (mono pass does more work); a single ratification on `bench/compile_check.py` is acceptable. Hard regressions on `bench/check.py` or `bench/cross_lang.py` BLOCK to Boss. - -- [ ] **F.3: Manual diff inspection** - -```bash -git log --oneline 18b49e1..HEAD -``` - -Expected: ~11 task commits + 2 revert commits + 1 DESIGN.md commit. Inspect the codegen-diff (Task 9) end-to-end: confirm no unrelated removals leaked in. - -- [ ] **F.4: Spec-acceptance partial check** - -The plan covers Acceptance Criteria 1, 2, 5 (partial), 6 (partial) of the spec. Criterion 3 (prelude free fns shipped), Criterion 4 (Decision-11 amendment), Criterion 7 (full E2E with Float diagnostic), Criterion 8 (roadmap update) are 23.5 scope — NOT verified here. - -Confirm in the per-iter journal: -- Criterion 1: lower_polymorphic_call + module_polymorphic_fns + mono_queue removed (grep verifies absence); prep commits reverted (`git log --oneline | grep "Revert"`). -- Criterion 2: DESIGN.md §"Resolution and monomorphisation" amendment lands. -- Criterion 5 (partial): poly_id / poly_apply / poly_rec_capture green; cmp_max_smoke green. -- Criterion 6 (partial): three bench scripts at expected exit codes.