plan: 22-tidy.6 Form-B prose printer arms for ClassDef + InstanceDef
This commit is contained in:
@@ -0,0 +1,488 @@
|
||||
# 22-tidy.6 Form-B prose printer arms for ClassDef + InstanceDef — Implementation Plan
|
||||
|
||||
> **Parent spec:** `docs/specs/2026-05-09-22-typeclasses.md` (milestone 22, closed 2026-05-09)
|
||||
>
|
||||
> **Carried-debt anchor:** `docs/JOURNAL.md` 2026-05-09 entry "Iteration 22-tidy" → "Form-B (prose) printer arms for ClassDef / InstanceDef. One-way, not gating."
|
||||
>
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
||||
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||
|
||||
**Goal:** retire the third (and last gating) milestone-22 carried-debt item: replace the placeholder Form-B render of `Def::Class` / `Def::Instance` (currently `// (class Foo a) -- full Form-B projection deferred (post-22)`) with a full Rust-flavoured projection that mirrors the Form-A schema. Includes forall-class-constraint rendering so a class-constrained fn (e.g. `forall<a>{Ord a => fn uses_eq_via_ord(...)}`) survives the projection without semantic loss.
|
||||
|
||||
**Architecture:** add two new helpers to `crates/ailang-prose/src/lib.rs` — `write_class_def` and `write_instance_def` — wired into the existing `write_def` dispatch. `write_fn_def` gets a 5-line constraint-render extension after the existing `forall<...>` line so a class-constrained `Type::Forall.constraints` list renders as `where <Class1> <param>, <Class2> <param>, …`. Snapshot tests use the established `tests/snapshot.rs` framework — one new fixture (`test_22b3_default_e2e`) for class-with-default + instance, one (`test_22b2_constraint_declared_via_superclass`) for class-with-superclass + class-constrained-fn.
|
||||
|
||||
**Tech Stack:** `crates/ailang-prose` (lib.rs, tests/snapshot.rs); `examples/` (two new `.prose.txt` snapshot files).
|
||||
|
||||
**Files this plan creates or modifies:**
|
||||
|
||||
- Modify: `crates/ailang-prose/src/lib.rs` — adds `write_class_def`, `write_instance_def`, `write_class_method`, `write_instance_method`, `write_forall_constraints`; rewires `write_def`'s `Def::Class` / `Def::Instance` arms to call them; extends `write_fn_def` to render forall constraints.
|
||||
- Modify: `crates/ailang-prose/tests/snapshot.rs` — adds two new `#[test] fn snapshot_test_22b3_default_e2e` and `snapshot_test_22b2_constraint_declared_via_superclass`.
|
||||
- Create: `examples/test_22b3_default_e2e.prose.txt` — snapshot fixture for class-with-default + instance.
|
||||
- Create: `examples/test_22b2_constraint_declared_via_superclass.prose.txt` — snapshot fixture for superclass + class-constrained fn.
|
||||
- Modify: `docs/JOURNAL.md` — append iteration entry.
|
||||
|
||||
**Form-B projection design:**
|
||||
|
||||
Class without superclass:
|
||||
```
|
||||
class Show a {
|
||||
fn show(x: a) -> Str
|
||||
}
|
||||
```
|
||||
|
||||
Class with superclass:
|
||||
```
|
||||
class Ord a extends Eq {
|
||||
fn lt(x: a, y: a) -> Bool
|
||||
}
|
||||
```
|
||||
|
||||
(Note: the superclass's `type_` field is validated to equal the class's own param, so it's redundant in the projection; render only the superclass *name*.)
|
||||
|
||||
Class method with default:
|
||||
```
|
||||
class D a {
|
||||
fn dval(x: a) -> Int default {
|
||||
99
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Instance:
|
||||
```
|
||||
instance D Int {
|
||||
}
|
||||
```
|
||||
|
||||
(No method overrides → empty body. Method-override case shown below.)
|
||||
|
||||
Instance with method override:
|
||||
```
|
||||
instance Show Int {
|
||||
fn show {
|
||||
int_to_str(x)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(InstanceMethod has only `name` + `body` — no signature; the LLM reads the class def for that, per the prose policy "deliberately lossy where the LLM can re-derive from typecheck context".)
|
||||
|
||||
Forall with class constraint on fn:
|
||||
```
|
||||
forall<a> where Ord a
|
||||
fn uses_eq_via_ord(x: a, y: a) -> Bool {
|
||||
eq(x, y)
|
||||
}
|
||||
```
|
||||
|
||||
(The `where` line goes between `forall<a>` and `fn ...`. Multiple constraints comma-separated.)
|
||||
|
||||
---
|
||||
|
||||
## Task 1: `write_class_def` + `write_instance_def` — full Form-B projection
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-prose/src/lib.rs` — replace the placeholder arms (lines 88-101); add four new helpers.
|
||||
- Create: `examples/test_22b3_default_e2e.prose.txt`.
|
||||
- Modify: `crates/ailang-prose/tests/snapshot.rs` — add one new test.
|
||||
|
||||
- [ ] **Step 1: write the expected snapshot at `examples/test_22b3_default_e2e.prose.txt`** (RED-first — file exists before the helpers do, so the test fails because the placeholder output differs)
|
||||
|
||||
```text
|
||||
// module test_22b3_default_e2e
|
||||
|
||||
class D a {
|
||||
fn dval(x: a) -> Int default {
|
||||
99
|
||||
}
|
||||
}
|
||||
|
||||
instance D Int {
|
||||
}
|
||||
|
||||
fn main() -> Unit with IO {
|
||||
io/print_int(dval(0))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: append the snapshot test to `crates/ailang-prose/tests/snapshot.rs`**
|
||||
|
||||
At the end of the file (just before the final closing brace if there is one — it appears the existing tests are at top level, so append at end):
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn snapshot_test_22b3_default_e2e() {
|
||||
check_snapshot("test_22b3_default_e2e");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: run RED — confirm snapshot mismatch**
|
||||
|
||||
Run: `cargo test --workspace -p ailang-prose snapshot_test_22b3_default_e2e 2>&1 | tail -15`
|
||||
Expected: FAIL — `prose snapshot mismatch for test_22b3_default_e2e` (the placeholder render contains `// (class D a) -- full Form-B projection deferred (post-22)` instead of the new full render).
|
||||
|
||||
- [ ] **Step 4: replace the placeholder arms in `crates/ailang-prose/src/lib.rs:88-101`**
|
||||
|
||||
Locate the `Def::Class` and `Def::Instance` arms in `write_def` (lines 88-101). Replace them with:
|
||||
|
||||
```rust
|
||||
Def::Class(c) => write_class_def(out, c, level),
|
||||
Def::Instance(i) => write_instance_def(out, i, level),
|
||||
```
|
||||
|
||||
And remove the placeholder comment block (lines 80-87) that introduced the deferred-placeholder note — those four lines of comment go too.
|
||||
|
||||
- [ ] **Step 5: add the four helpers**
|
||||
|
||||
Add these helpers at the end of the `// ---- defs --------` section (just before `// ---- types --------`, currently around line 344):
|
||||
|
||||
```rust
|
||||
fn write_class_def(out: &mut String, c: &ClassDef, level: usize) {
|
||||
write_doc(out, &c.doc, level);
|
||||
indent(out, level);
|
||||
out.push_str("class ");
|
||||
out.push_str(&c.name);
|
||||
out.push(' ');
|
||||
out.push_str(&c.param);
|
||||
if let Some(sc) = &c.superclass {
|
||||
out.push_str(" extends ");
|
||||
out.push_str(&sc.class);
|
||||
}
|
||||
out.push_str(" {\n");
|
||||
for m in &c.methods {
|
||||
write_class_method(out, m, level + 1);
|
||||
}
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
|
||||
fn write_class_method(out: &mut String, m: &ClassMethod, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("fn ");
|
||||
out.push_str(&m.name);
|
||||
out.push('(');
|
||||
if let Type::Fn { params, param_modes, ret, ret_mode, effects } = &m.ty {
|
||||
for (i, p) in params.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
// Class methods carry types but no parameter names — name slots
|
||||
// as `x`, `x1`, `x2`, … to keep the projection unambiguous
|
||||
// without inventing names that could collide with the body.
|
||||
if i == 0 {
|
||||
out.push_str("x");
|
||||
} else {
|
||||
out.push_str(&format!("x{i}"));
|
||||
}
|
||||
out.push_str(": ");
|
||||
let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit);
|
||||
write_mode_type(out, p, mode);
|
||||
}
|
||||
out.push_str(") -> ");
|
||||
write_mode_type(out, ret, *ret_mode);
|
||||
if !effects.is_empty() {
|
||||
out.push_str(" with ");
|
||||
for (i, e) in effects.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.push_str(") -> ");
|
||||
write_type(out, &m.ty);
|
||||
}
|
||||
if let Some(body) = &m.default {
|
||||
out.push_str(" default {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, body, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push_str("}\n");
|
||||
} else {
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn write_instance_def(out: &mut String, i: &InstanceDef, level: usize) {
|
||||
write_doc(out, &i.doc, level);
|
||||
indent(out, level);
|
||||
out.push_str("instance ");
|
||||
out.push_str(&i.class);
|
||||
out.push(' ');
|
||||
write_type(out, &i.type_);
|
||||
out.push_str(" {\n");
|
||||
for m in &i.methods {
|
||||
write_instance_method(out, m, level + 1);
|
||||
}
|
||||
indent(out, level);
|
||||
out.push('}');
|
||||
}
|
||||
|
||||
fn write_instance_method(out: &mut String, m: &InstanceMethod, level: usize) {
|
||||
indent(out, level);
|
||||
out.push_str("fn ");
|
||||
out.push_str(&m.name);
|
||||
out.push_str(" {\n");
|
||||
indent(out, level + 1);
|
||||
write_term(out, &m.body, level + 1);
|
||||
out.push('\n');
|
||||
indent(out, level);
|
||||
out.push_str("}\n");
|
||||
}
|
||||
```
|
||||
|
||||
Also add the four new types to the `use ailang_core::ast::{...}` import line (line 27-30): add `ClassDef, ClassMethod, InstanceDef, InstanceMethod` to the imported names.
|
||||
|
||||
- [ ] **Step 6: run GREEN — snapshot passes**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: builds clean (no unused-import warnings).
|
||||
|
||||
Run: `cargo test --workspace -p ailang-prose snapshot_test_22b3_default_e2e 2>&1 | tail -10`
|
||||
Expected: PASS — `1 passed; 0 failed`.
|
||||
|
||||
If the snapshot fails because the actual output differs from the expected `.prose.txt`, examine the `.actual` file the test framework writes to `examples/test_22b3_default_e2e.prose.txt.actual`. If the diff is a presentation-tweak (whitespace at end-of-line, indentation depth), update the expected file to match the actual; otherwise diagnose the helper.
|
||||
|
||||
- [ ] **Step 7: full ailang-prose sweep stays green**
|
||||
|
||||
Run: `cargo test --workspace -p ailang-prose 2>&1 | grep -E "^test result" | tail -5`
|
||||
Expected: every line `test result: ok.`; no FAILED. The 4 pre-existing snapshot tests (`rc_own_param_drop`, `rc_match_arm_partial_drop_leak`, `rc_app_let_partial_drop_leak`, `bench_list_sum`) must stay green — none of those fixtures contain class/instance, so they hit no changed code path.
|
||||
|
||||
- [ ] **Step 8: clean up any `.actual` debug file the snapshot framework may have written**
|
||||
|
||||
Run: `rm -f examples/test_22b3_default_e2e.prose.txt.actual`
|
||||
|
||||
- [ ] **Step 9: commit**
|
||||
|
||||
```bash
|
||||
git add crates/ailang-prose/src/lib.rs crates/ailang-prose/tests/snapshot.rs examples/test_22b3_default_e2e.prose.txt
|
||||
git commit -m "iter 22-tidy.6.1: write_class_def + write_instance_def — full Form-B projection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `write_fn_def` extends with forall-constraint rendering — superclass + class-constrained fn
|
||||
|
||||
**Files:**
|
||||
- Modify: `crates/ailang-prose/src/lib.rs` — `write_fn_def` (lines 248-331); adds a `where`-clause render after the `forall<...>` line.
|
||||
- Create: `examples/test_22b2_constraint_declared_via_superclass.prose.txt`.
|
||||
- Modify: `crates/ailang-prose/tests/snapshot.rs` — add one more test.
|
||||
|
||||
- [ ] **Step 1: write the expected snapshot at `examples/test_22b2_constraint_declared_via_superclass.prose.txt`** (RED-first)
|
||||
|
||||
```text
|
||||
// module test_22b2_constraint_declared_via_superclass
|
||||
|
||||
class Eq a {
|
||||
fn eq(x: a, x1: a) -> Bool
|
||||
}
|
||||
|
||||
class Ord a extends Eq {
|
||||
fn lt(x: a, x1: a) -> Bool
|
||||
}
|
||||
|
||||
forall<a> where Ord a
|
||||
fn uses_eq_via_ord(x: a, y: a) -> Bool {
|
||||
eq(x, y)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: append the snapshot test**
|
||||
|
||||
In `crates/ailang-prose/tests/snapshot.rs`, append:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn snapshot_test_22b2_constraint_declared_via_superclass() {
|
||||
check_snapshot("test_22b2_constraint_declared_via_superclass");
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: run RED — confirm snapshot mismatch**
|
||||
|
||||
Run: `cargo test --workspace -p ailang-prose snapshot_test_22b2_constraint_declared_via_superclass 2>&1 | tail -15`
|
||||
Expected: FAIL — `prose snapshot mismatch`. The class+superclass arm now works (Task 1 shipped it), but the constraint on `uses_eq_via_ord`'s forall is dropped — current `write_fn_def` discards `Type::Forall.constraints`.
|
||||
|
||||
- [ ] **Step 4: extend `write_fn_def` to render constraints**
|
||||
|
||||
Locate the forall-block in `write_fn_def` (lines 261-276 — the `let (forall_vars, fn_ty) = match &fd.ty { … }` block plus the `if let Some(vars) = &forall_vars { … out.push_str(">\n"); }` block).
|
||||
|
||||
Replace:
|
||||
|
||||
```rust
|
||||
let (forall_vars, fn_ty) = match &fd.ty {
|
||||
Type::Forall { vars, constraints: _, body } => (Some(vars.clone()), body.as_ref()),
|
||||
other => (None, other),
|
||||
};
|
||||
|
||||
if let Some(vars) = &forall_vars {
|
||||
indent(out, level);
|
||||
out.push_str("forall<");
|
||||
for (i, v) in vars.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push_str(">\n");
|
||||
}
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```rust
|
||||
let (forall_vars, forall_constraints, fn_ty) = match &fd.ty {
|
||||
Type::Forall { vars, constraints, body } => {
|
||||
(Some(vars.clone()), constraints.clone(), body.as_ref())
|
||||
}
|
||||
other => (None, Vec::new(), other),
|
||||
};
|
||||
|
||||
if let Some(vars) = &forall_vars {
|
||||
indent(out, level);
|
||||
out.push_str("forall<");
|
||||
for (i, v) in vars.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(v);
|
||||
}
|
||||
out.push('>');
|
||||
if !forall_constraints.is_empty() {
|
||||
out.push_str(" where ");
|
||||
for (i, c) in forall_constraints.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push_str(", ");
|
||||
}
|
||||
out.push_str(&c.class);
|
||||
out.push(' ');
|
||||
write_type(out, &c.type_);
|
||||
}
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
```
|
||||
|
||||
Also add `Constraint` to the `use ailang_core::ast::{...}` import line at the top of the file.
|
||||
|
||||
- [ ] **Step 5: run GREEN — snapshot passes**
|
||||
|
||||
Run: `cargo build --workspace 2>&1 | tail -5`
|
||||
Expected: builds clean.
|
||||
|
||||
Run: `cargo test --workspace -p ailang-prose snapshot_test_22b2_constraint_declared_via_superclass 2>&1 | tail -10`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: full ailang-prose sweep stays green** (the 4 pre-existing snapshots PLUS the new one from Task 1 PLUS this one — 6 tests total)
|
||||
|
||||
Run: `cargo test --workspace -p ailang-prose 2>&1 | grep -E "^test result" | tail -5`
|
||||
Expected: every line `test result: ok.`; no FAILED.
|
||||
|
||||
- [ ] **Step 7: clean up `.actual` debug file**
|
||||
|
||||
Run: `rm -f examples/test_22b2_constraint_declared_via_superclass.prose.txt.actual`
|
||||
|
||||
- [ ] **Step 8: commit**
|
||||
|
||||
```bash
|
||||
git add crates/ailang-prose/src/lib.rs crates/ailang-prose/tests/snapshot.rs examples/test_22b2_constraint_declared_via_superclass.prose.txt
|
||||
git commit -m "iter 22-tidy.6.2: write_fn_def renders forall class constraints"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: verify + JOURNAL
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/JOURNAL.md` — append iteration entry.
|
||||
|
||||
- [ ] **Step 1: full workspace test sweep**
|
||||
|
||||
Run: `cargo test --workspace 2>&1 | grep -c "FAILED"`
|
||||
Expected: `0`.
|
||||
|
||||
- [ ] **Step 2: bench gates**
|
||||
|
||||
Run: `python3 bench/check.py 2>&1 | tail -1 && python3 bench/compile_check.py 2>&1 | tail -1 && python3 bench/cross_lang.py 2>&1 | tail -1`
|
||||
Expected: each line ends `0 regressed`.
|
||||
|
||||
- [ ] **Step 3: confirm placeholder retired**
|
||||
|
||||
Run: `grep -n "full Form-B projection deferred" crates/`
|
||||
Expected: zero hits — the placeholder text and its containing comment block are gone from `crates/ailang-prose/src/lib.rs`.
|
||||
|
||||
- [ ] **Step 4: append JOURNAL entry**
|
||||
|
||||
Append to `docs/JOURNAL.md`:
|
||||
|
||||
```markdown
|
||||
## 2026-05-10 — Iteration 22-tidy.6: Form-B prose printer arms for ClassDef + InstanceDef
|
||||
|
||||
Closing the third (and final gating) milestone-22 carried-debt
|
||||
item: `crates/ailang-prose` previously rendered `Def::Class` /
|
||||
`Def::Instance` as a one-line placeholder
|
||||
(`// (class Foo a) -- full Form-B projection deferred (post-22)`).
|
||||
This iteration replaces the placeholder with a full Rust-flavoured
|
||||
projection covering class header, optional `extends <SuperClass>`,
|
||||
methods (signature plus optional `default { … }` body), and
|
||||
instance header + method bodies.
|
||||
|
||||
`write_fn_def` was extended in the same iter to render forall
|
||||
class constraints (`forall<a> where Ord a`) so a class-constrained
|
||||
fn signature survives the projection without semantic loss — a
|
||||
prerequisite for the superclass snapshot to be useful.
|
||||
|
||||
Two new snapshot fixtures pin the new render:
|
||||
|
||||
- `examples/test_22b3_default_e2e.prose.txt` — class with default
|
||||
method body + instance that inherits the default.
|
||||
- `examples/test_22b2_constraint_declared_via_superclass.prose.txt`
|
||||
— class with superclass + abstract method + class-constrained fn.
|
||||
|
||||
Tasks (commit subjects):
|
||||
|
||||
- 22-tidy.6.1: write_class_def + write_instance_def — full Form-B projection
|
||||
- 22-tidy.6.2: write_fn_def renders forall class constraints
|
||||
|
||||
Acceptance: 2 RED-first snapshot tests then GREEN; full workspace
|
||||
test sweep green; bench gates 0/0/0; the four pre-existing prose
|
||||
snapshots (rc_own_param_drop, rc_match_arm_partial_drop_leak,
|
||||
rc_app_let_partial_drop_leak, bench_list_sum) stay green
|
||||
(unaffected — none contain class/instance). The placeholder text
|
||||
and comment are gone from `crates/ailang-prose/src/lib.rs`.
|
||||
|
||||
Carried-debt status (after this iter, milestone 22 fully closed):
|
||||
|
||||
- ✅ Primitive-name-set consolidation (closed 22-tidy.4).
|
||||
- ✅ Strict duplicate-clause detection in fn/const/data (closed 22-tidy.5).
|
||||
- ✅ Form-B (prose) printer arms for ClassDef / InstanceDef (closed
|
||||
this iter).
|
||||
- ✅ Lib.rs gap-related sites at lib.rs:1266 / 1853-1856 / 1978 /
|
||||
2314-2316 — audit confirmed defensive (no action needed).
|
||||
|
||||
Milestone 22 carried debt is now empty. Next milestone targets
|
||||
(orchestrator's call): post-22 Prelude (gated on user-author
|
||||
demand), operator routing through Eq/Ord (gated on bench
|
||||
re-baselining), or the `types/ctor_index` overlay shape question
|
||||
queued from `2026-05-10 — Audit close: env-construction unify`.
|
||||
|
||||
Out-of-scope observation (not new debt — design choice): instance
|
||||
methods render as `fn <name> { <body> }` without a signature line.
|
||||
The signature is recoverable from the class declaration via
|
||||
`InstanceDef.class` lookup (a typecheck-context-dependent
|
||||
substitution of `class.method.ty` with `class.param → instance.type_`).
|
||||
Per the prose policy ("deliberately lossy where the LLM can
|
||||
re-derive the dropped machinery from typecheck context"), this is
|
||||
the correct projection — the class def is the authoritative
|
||||
signature source. If a future audit decides otherwise, the
|
||||
projection extends with a typecheck-context-aware substitutor.
|
||||
```
|
||||
|
||||
- [ ] **Step 5: commit**
|
||||
|
||||
```bash
|
||||
git add docs/JOURNAL.md
|
||||
git commit -m "iter 22-tidy.6: journal entry, milestone-22 carried debt empty"
|
||||
```
|
||||
Reference in New Issue
Block a user