plan: drop 23.4 — design assumption broke at branch verification
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.
This commit is contained in:
@@ -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<Type>, 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.
|
||||
/// `<fn-name>__<typename1>__<typename2>__…` 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<String> = 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<AstFnDef> {
|
||||
// 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<MonoTarget>` 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", "<captured value from Step 8.1>"),
|
||||
("eq__Bool", "<captured value>"),
|
||||
("eq__Str", "<captured value>"),
|
||||
("compare__Int", "<captured value>"),
|
||||
("compare__Bool", "<captured value>"),
|
||||
("compare__Str", "<captured value>"),
|
||||
];
|
||||
|
||||
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, Type>, 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.
|
||||
Reference in New Issue
Block a user