All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
18 KiB
22-tidy.6 Form-B prose printer arms for ClassDef + InstanceDef — Implementation Plan
Parent spec:
docs/specs/0002-22-typeclasses.md(milestone 22, closed 2026-05-09)Carried-debt anchor:
docs/JOURNAL.md2026-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/implementto 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— addswrite_class_def,write_instance_def,write_class_method,write_instance_method,write_forall_constraints; rewireswrite_def'sDef::Class/Def::Instancearms to call them; extendswrite_fn_defto render forall constraints. - Modify:
crates/ailang-prose/tests/snapshot.rs— adds two new#[test] fn snapshot_test_22b3_default_e2eandsnapshot_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)
// 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):
#[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:
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):
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
.actualdebug file the snapshot framework may have written
Run: rm -f examples/test_22b3_default_e2e.prose.txt.actual
- Step 9: commit
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 awhere-clause render after theforall<...>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)
// 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:
#[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_defto 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:
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:
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
.actualdebug file
Run: rm -f examples/test_22b2_constraint_declared_via_superclass.prose.txt.actual
- Step 8: commit
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:
## 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
git add docs/JOURNAL.md
git commit -m "iter 22-tidy.6: journal entry, milestone-22 carried debt empty"