832375f2ac
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.
1380 lines
48 KiB
Markdown
1380 lines
48 KiB
Markdown
# 22b.4a — Form-A parser+printer arms for ClassDef/InstanceDef + spec amendment
|
|
|
|
> **Parent spec:** `docs/specs/0002-22-typeclasses.md`
|
|
>
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
|
|
|
**Goal:** make `parse(print(M)) == M` (after canonicalisation) for every
|
|
`examples/*.ail.json` fixture, including the `test_22b1_*`,
|
|
`test_22b2_*`, and `test_22b3_*` typeclass fixtures that are currently
|
|
skipped. Retire the round-trip skip-list. Amend the parent spec to
|
|
record (a) the iteration split, (b) the `__` separator chosen in 22b.3
|
|
for mono-symbol names.
|
|
|
|
**Architecture:** the surface crate (`crates/ailang-surface`) is
|
|
**Form-A**, the s-expression projection. (The crate's docstring and
|
|
the round-trip-skip-list comment loosely call this "Form-B"; that is a
|
|
documentation defect — `crates/ailang-prose` is the actual Form-B and
|
|
has no parser. The 22 parent spec inherits the same conflation.) This
|
|
iteration adds Form-A parser arms for `Def::Class` / `Def::Instance`
|
|
under `parse_module`'s dispatch, plus matching printer arms in
|
|
`write_def`, both written so the existing canonical-equal round-trip
|
|
gate passes for every typeclass fixture in `examples/`. The 22b.1
|
|
placeholder printer arms (single-token `(class Name param)` /
|
|
`(instance Class)`) are replaced with the real implementations.
|
|
|
|
**Tech Stack:** `crates/ailang-surface` (parse + print), `ailang-core`
|
|
AST types (`ClassDef`, `InstanceDef`, `ClassMethod`, `InstanceMethod`,
|
|
`SuperclassRef` — all already present from 22b.1).
|
|
|
|
**Out of scope (deferred to 22b.4b):** Prelude module with `Show`
|
|
class, `instance Show Int`, the `int_to_str` C-runtime primitive, the
|
|
end-to-end `do io/print_str (show 42)` gate. Substantive reason: the
|
|
Prelude requires a new C-runtime function, codegen wiring for
|
|
`int_to_str`, and an examples-level fixture; that is a different risk
|
|
profile from pure-surface plumbing and benefits from its own review
|
|
loop. Eq/Ord deferred further (require `str_eq` / `str_compare`
|
|
primitives that the runtime does not yet expose). Top-level `print`
|
|
deferred until polymorphic-fn monomorphisation is on the table — the
|
|
current mono pass synthesises only class-method instances, not
|
|
arbitrary polymorphic top-levels.
|
|
|
|
---
|
|
|
|
## Files this plan creates or modifies
|
|
|
|
- Modify: `crates/ailang-surface/src/parse.rs` — adds `parse_class`,
|
|
`parse_instance`, two helper sub-parsers (`parse_class_method`,
|
|
`parse_instance_method`, `parse_superclass`); extends the
|
|
`parse_module` def-head dispatch.
|
|
- Modify: `crates/ailang-surface/src/print.rs` — replaces the 22b.1
|
|
placeholder arms in `write_def`; adds `write_class_def`,
|
|
`write_instance_def`, plus per-method writers
|
|
(`write_class_method`, `write_instance_method`, `write_superclass`).
|
|
- Modify: `crates/ailang-surface/tests/round_trip.rs` — removes the
|
|
`test_22b1_*` / `test_22b2_*` / `test_22b3_*` filter clauses; lets
|
|
the gate exercise every fixture.
|
|
- Modify: `docs/specs/0002-22-typeclasses.md` — appends an
|
|
"Amendments" section recording the 22b.4a / 22b.4b split, the
|
|
Form-A vs Form-B terminology fix, the `__` separator decision, and
|
|
the 22c-and-beyond impact.
|
|
- Modify: `docs/DESIGN.md` (Decision 11 §"Resolution and
|
|
monomorphisation") — one-paragraph note on `__` separator, citing
|
|
LLVM IR identifier legality.
|
|
|
|
No new files. No test crate changes (round-trip + the existing
|
|
`examples/test_22b*` fixtures are the regression surface).
|
|
|
|
---
|
|
|
|
## Cross-task invariants
|
|
|
|
Three constraints every task in this plan obeys:
|
|
|
|
1. **Round-trip property is sacred.** Any task whose final command is
|
|
`cargo test -p ailang-surface --test round_trip` MUST observe
|
|
green before commit.
|
|
2. **No new fixture files.** Hand-written inline test inputs only;
|
|
the existing `examples/test_22b*` set is the round-trip integration
|
|
target. New fixtures would expand 22b.4a beyond surface plumbing.
|
|
3. **No primitive additions and no codegen changes.** This is a pure
|
|
surface-crate iteration. If a task is tempted to touch
|
|
`ailang-codegen` or `ailang-check`, that is a sign it belongs in
|
|
22b.4b (or further).
|
|
|
|
---
|
|
|
|
## S-expression form (locked here, implemented in tasks)
|
|
|
|
The following Form-A surface syntax is committed for ClassDef and
|
|
InstanceDef. The shape mirrors `(fn ...)` and `(data ...)`: a
|
|
keyword head, the def name (where applicable), then `(...)`
|
|
attribute-clauses in declaration order. Unknown attribute keywords
|
|
are rejected with `ParseError::Production` so future schema
|
|
extensions fail loudly.
|
|
|
|
**ClassDef:**
|
|
|
|
```
|
|
(class <ClassName>
|
|
(param <param-ident>)
|
|
[(superclass (class <Name>) (type <param-ident>))]
|
|
[(doc "<docstring>")]
|
|
(method <name>
|
|
(type <fn-type-expr>)
|
|
[(default <body-term>)])*
|
|
)
|
|
```
|
|
|
|
- `(param ...)` is required; matches `ClassDef.param: String`.
|
|
- `(superclass ...)` is optional; at most one.
|
|
- `(doc ...)` is optional; at most one. Same shape as the
|
|
fn-def `(doc ...)` clause.
|
|
- `(method ...)` clauses appear in declaration order. Each method
|
|
requires `(type ...)`; `(default ...)` is optional and represents
|
|
the explicit-`default`-keyword body from Decision 11.
|
|
|
|
**InstanceDef:**
|
|
|
|
```
|
|
(instance
|
|
(class <ClassName>)
|
|
(type <type-expr>)
|
|
[(doc "<docstring>")]
|
|
(method <name>
|
|
(body <term>))*
|
|
)
|
|
```
|
|
|
|
- No name follows `instance` — the (class, type) pair identifies it.
|
|
- `(class ...)` and `(type ...)` are required; one of each.
|
|
- `(doc ...)` is optional.
|
|
- `(method ...)` clauses each contain exactly one `(body ...)`
|
|
clause. Method order matches declaration order.
|
|
|
|
Both heads use the existing helpers:
|
|
- `parse_type_attr` for `(type ...)` (class method types and instance
|
|
type both go through this).
|
|
- `parse_body_attr` for `(default ...)` and instance `(body ...)`
|
|
bodies — note `parse_body_attr` consumes literally `(body ...)`,
|
|
so the class-default clause is parsed by a *new* sibling helper
|
|
`parse_default_attr` that mirrors `parse_body_attr` but expects
|
|
the keyword `default` instead of `body`.
|
|
|
|
---
|
|
|
|
## Task 1: Form-A parser arm for ClassDef
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/parse.rs:281-320` (extend
|
|
`parse_module` dispatch); append `parse_class`, `parse_class_method`,
|
|
`parse_superclass`, `parse_default_attr` after `parse_const`
|
|
(around line 700).
|
|
|
|
- [ ] **Step 1.1: Write the failing test (RED)**
|
|
|
|
Append inside the existing `#[cfg(test)] mod tests { ... }` block in
|
|
`crates/ailang-surface/src/parse.rs` (the module starts at parse.rs:1353).
|
|
Put the new tests just before the closing `}` of that module, with
|
|
the existing `use super::*;` already in scope.
|
|
|
|
```rust
|
|
#[test]
|
|
fn parses_minimal_class_def() {
|
|
let src = r#"(module M
|
|
(class Foo
|
|
(param a)
|
|
(method m
|
|
(type (fn-type (params (con a)) (ret (con Int)) (effects))))))"#;
|
|
let m = parse(src).expect("parse ok");
|
|
assert_eq!(m.defs.len(), 1, "one def");
|
|
match &m.defs[0] {
|
|
ailang_core::ast::Def::Class(c) => {
|
|
assert_eq!(c.name, "Foo");
|
|
assert_eq!(c.param, "a");
|
|
assert!(c.superclass.is_none(), "no superclass");
|
|
assert!(c.doc.is_none(), "no doc");
|
|
assert_eq!(c.methods.len(), 1, "one method");
|
|
assert_eq!(c.methods[0].name, "m");
|
|
assert!(c.methods[0].default.is_none(),
|
|
"no default");
|
|
}
|
|
other => panic!("expected Def::Class, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_class_def_with_superclass_and_default() {
|
|
let src = r#"(module M
|
|
(class Bar
|
|
(param a)
|
|
(superclass (class Foo) (type a))
|
|
(doc "bar extends foo")
|
|
(method m
|
|
(type (fn-type (params (con a)) (ret (con Int)) (effects)))
|
|
(default (lit (int 0))))))"#;
|
|
let m = parse(src).expect("parse ok");
|
|
let c = match &m.defs[0] {
|
|
ailang_core::ast::Def::Class(c) => c,
|
|
_ => panic!("expected Class"),
|
|
};
|
|
let sc = c.superclass.as_ref().expect("has superclass");
|
|
assert_eq!(sc.class, "Foo");
|
|
assert_eq!(sc.type_, "a");
|
|
assert_eq!(c.doc.as_deref(), Some("bar extends foo"));
|
|
assert!(c.methods[0].default.is_some(),
|
|
"method default present");
|
|
}
|
|
```
|
|
|
|
(`parse` is the same `pub fn parse(...)` that lives at the top of
|
|
this file and is re-exported from `ailang_surface::parse`. The Form-A
|
|
literal `(lit (int 0))` matches the existing `Term::Lit` parse arm —
|
|
verify by grepping `parse_term` for `"int"` if uncertain. The
|
|
`(fn-type ...)` form is what `parse_type_attr → parse_type` accepts;
|
|
the existing `parse_fn` tests upstream use the same shape.)
|
|
|
|
- [ ] **Step 1.2: Run test, observe RED**
|
|
|
|
```
|
|
cargo test -p ailang-surface parses_minimal_class_def
|
|
```
|
|
|
|
Expected: FAIL with parse error along the lines of `unknown def head
|
|
"class"; expected "data", "fn", "const", or "import"` (the existing
|
|
`parse_module` error path at parse.rs:312).
|
|
|
|
- [ ] **Step 1.3: Add `Class` to the def-head dispatch**
|
|
|
|
In `parse_module` (parse.rs around line 309-318), extend the inner
|
|
match to recognise `class`. Leave `instance` for Task 2 (so Task 2's
|
|
RED is a parse error rather than a build error). Replace:
|
|
|
|
```rust
|
|
match head {
|
|
"import" => imports.push(self.parse_import()?),
|
|
"data" => defs.push(Def::Type(self.parse_data()?)),
|
|
"fn" => defs.push(Def::Fn(self.parse_fn()?)),
|
|
"const" => defs.push(Def::Const(self.parse_const()?)),
|
|
other => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "module",
|
|
message: format!(
|
|
"unknown def head `{other}`; expected `data`, `fn`, `const`, or `import`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
with:
|
|
|
|
```rust
|
|
match head {
|
|
"import" => imports.push(self.parse_import()?),
|
|
"data" => defs.push(Def::Type(self.parse_data()?)),
|
|
"fn" => defs.push(Def::Fn(self.parse_fn()?)),
|
|
"const" => defs.push(Def::Const(self.parse_const()?)),
|
|
"class" => defs.push(Def::Class(self.parse_class()?)),
|
|
other => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "module",
|
|
message: format!(
|
|
"unknown def head `{other}`; expected `data`, `fn`, `const`, `class`, or `import`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
Also add the AST imports at parse.rs:88. Replace:
|
|
|
|
```rust
|
|
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Suppress, Term,
|
|
```
|
|
|
|
with:
|
|
|
|
```rust
|
|
Arm, ClassDef, ClassMethod, ConstDef, Ctor, Def, FnDef, Import, Literal, Module,
|
|
ParamMode, Pattern, SuperclassRef, Suppress, Term,
|
|
```
|
|
|
|
(Add the AST imports for `ClassDef`, `ClassMethod`, `SuperclassRef`
|
|
from `ailang_core::ast`. `InstanceDef` / `InstanceMethod` are added
|
|
by Task 2.)
|
|
|
|
- [ ] **Step 1.4: Implement `parse_class` and helpers**
|
|
|
|
Append to parse.rs after the existing `parse_const` block (around
|
|
line 700, between `parse_const` and `parse_type_attr`):
|
|
|
|
```rust
|
|
// ---- class def -----------------------------------------------------
|
|
|
|
fn parse_class(&mut self) -> Result<ClassDef, ParseError> {
|
|
self.expect_lparen("class-def")?;
|
|
self.expect_keyword("class")?;
|
|
let name = self.expect_ident("class name")?;
|
|
let mut param: Option<String> = None;
|
|
let mut superclass: Option<SuperclassRef> = None;
|
|
let mut doc: Option<String> = None;
|
|
let mut methods: Vec<ClassMethod> = Vec::new();
|
|
loop {
|
|
match self.peek_head_ident() {
|
|
Some("param") => {
|
|
if param.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class-def",
|
|
message: format!(
|
|
"class `{name}` has duplicate `(param ...)` clause"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
self.expect_lparen("class.param")?;
|
|
self.expect_keyword("param")?;
|
|
param = Some(self.expect_ident("class param ident")?);
|
|
self.expect_rparen("class.param")?;
|
|
}
|
|
Some("superclass") => {
|
|
if superclass.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class-def",
|
|
message: format!(
|
|
"class `{name}` has duplicate `(superclass ...)` clause"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
superclass = Some(self.parse_superclass()?);
|
|
}
|
|
Some("doc") => {
|
|
if doc.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class-def",
|
|
message: format!(
|
|
"class `{name}` has duplicate `(doc ...)` clause"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
doc = Some(self.parse_doc()?);
|
|
}
|
|
Some("method") => {
|
|
methods.push(self.parse_class_method()?);
|
|
}
|
|
Some(other) => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class-def",
|
|
message: format!(
|
|
"unknown class attribute `{other}`; expected `param`, `superclass`, `doc`, or `method`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
self.expect_rparen("class-def")?;
|
|
let param = param.ok_or_else(|| ParseError::Production {
|
|
production: "class-def",
|
|
message: format!("class `{name}` is missing required `(param ...)` clause"),
|
|
pos: 0,
|
|
})?;
|
|
Ok(ClassDef {
|
|
name,
|
|
param,
|
|
superclass,
|
|
methods,
|
|
doc,
|
|
})
|
|
}
|
|
|
|
fn parse_superclass(&mut self) -> Result<SuperclassRef, ParseError> {
|
|
self.expect_lparen("superclass-clause")?;
|
|
self.expect_keyword("superclass")?;
|
|
// (class Name)
|
|
self.expect_lparen("superclass.class")?;
|
|
self.expect_keyword("class")?;
|
|
let class = self.expect_ident("superclass name")?;
|
|
self.expect_rparen("superclass.class")?;
|
|
// (type ident)
|
|
self.expect_lparen("superclass.type")?;
|
|
self.expect_keyword("type")?;
|
|
let type_ = self.expect_ident("superclass param ident")?;
|
|
self.expect_rparen("superclass.type")?;
|
|
self.expect_rparen("superclass-clause")?;
|
|
Ok(SuperclassRef { class, type_ })
|
|
}
|
|
|
|
fn parse_class_method(&mut self) -> Result<ClassMethod, ParseError> {
|
|
self.expect_lparen("class.method")?;
|
|
self.expect_keyword("method")?;
|
|
let name = self.expect_ident("class method name")?;
|
|
let mut ty: Option<Type> = None;
|
|
let mut default: Option<Term> = None;
|
|
loop {
|
|
match self.peek_head_ident() {
|
|
Some("type") => {
|
|
if ty.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class.method",
|
|
message: format!(
|
|
"method `{name}` has duplicate `(type ...)` clause"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
ty = Some(self.parse_type_attr()?);
|
|
}
|
|
Some("default") => {
|
|
if default.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class.method",
|
|
message: format!(
|
|
"method `{name}` has duplicate `(default ...)` clause"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
default = Some(self.parse_default_attr()?);
|
|
}
|
|
Some(other) => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "class.method",
|
|
message: format!(
|
|
"unknown class.method attribute `{other}`; expected `type` or `default`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
self.expect_rparen("class.method")?;
|
|
let ty = ty.ok_or_else(|| ParseError::Production {
|
|
production: "class.method",
|
|
message: format!("method `{name}` is missing required `(type ...)` clause"),
|
|
pos: 0,
|
|
})?;
|
|
Ok(ClassMethod { name, ty, default })
|
|
}
|
|
|
|
fn parse_default_attr(&mut self) -> Result<Term, ParseError> {
|
|
self.expect_lparen("default-attr")?;
|
|
self.expect_keyword("default")?;
|
|
let t = self.parse_term()?;
|
|
self.expect_rparen("default-attr")?;
|
|
Ok(t)
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 1.5: Run RED tests, observe GREEN**
|
|
|
|
```
|
|
cargo test -p ailang-surface parses_minimal_class_def
|
|
cargo test -p ailang-surface parses_class_def_with_superclass_and_default
|
|
cargo build --workspace
|
|
```
|
|
|
|
Expected: both tests PASS. Workspace build green.
|
|
|
|
- [ ] **Step 1.6: Commit**
|
|
|
|
```
|
|
git add crates/ailang-surface/src/parse.rs
|
|
git commit -m "iter 22b.4a.1: form-a parser arm for ClassDef"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 2: Form-A parser arm for InstanceDef
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/parse.rs` — append
|
|
`parse_instance` and `parse_instance_method` after `parse_class`
|
|
block.
|
|
|
|
- [ ] **Step 2.1: Write the failing test (RED)**
|
|
|
|
Append inside the same `#[cfg(test)] mod tests { ... }` block as
|
|
Task 1:
|
|
|
|
```rust
|
|
#[test]
|
|
fn parses_minimal_instance_def() {
|
|
let src = r#"(module M
|
|
(instance
|
|
(class Foo)
|
|
(type (con Int))
|
|
(method m
|
|
(body 5))))"#;
|
|
let m = parse(src).expect("parse ok");
|
|
match &m.defs[0] {
|
|
ailang_core::ast::Def::Instance(i) => {
|
|
assert_eq!(i.class, "Foo");
|
|
match &i.type_ {
|
|
ailang_core::ast::Type::Con { name, args } => {
|
|
assert_eq!(name, "Int");
|
|
assert!(args.is_empty(), "Int has no args");
|
|
}
|
|
other => panic!("expected Type::Con, got {other:?}"),
|
|
}
|
|
assert!(i.doc.is_none());
|
|
assert_eq!(i.methods.len(), 1);
|
|
assert_eq!(i.methods[0].name, "m");
|
|
}
|
|
other => panic!("expected Def::Instance, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn parses_instance_def_with_doc() {
|
|
let src = r#"(module M
|
|
(instance
|
|
(class Foo)
|
|
(type (con Int))
|
|
(doc "instance for ints")
|
|
(method m (body 7))))"#;
|
|
let m = parse(src).expect("parse ok");
|
|
let i = match &m.defs[0] {
|
|
ailang_core::ast::Def::Instance(i) => i,
|
|
_ => panic!("expected Instance"),
|
|
};
|
|
assert_eq!(i.doc.as_deref(), Some("instance for ints"));
|
|
}
|
|
```
|
|
|
|
(`Type::Con` carries `{ name, args }` — `args` is `Vec<Type>`; for
|
|
`Int` it is empty.)
|
|
|
|
- [ ] **Step 2.2: Run test, observe RED**
|
|
|
|
```
|
|
cargo test -p ailang-surface parses_minimal_instance_def
|
|
```
|
|
|
|
Expected: FAIL with parse error `unknown def head "instance"; expected …`
|
|
emitted by the `parse_module` dispatch arm.
|
|
|
|
- [ ] **Step 2.3: Add `Instance` to the def-head dispatch**
|
|
|
|
Locate the same `parse_module` match block edited in Task 1.3 and
|
|
add the `instance` arm + extend the error message:
|
|
|
|
```rust
|
|
match head {
|
|
"import" => imports.push(self.parse_import()?),
|
|
"data" => defs.push(Def::Type(self.parse_data()?)),
|
|
"fn" => defs.push(Def::Fn(self.parse_fn()?)),
|
|
"const" => defs.push(Def::Const(self.parse_const()?)),
|
|
"class" => defs.push(Def::Class(self.parse_class()?)),
|
|
"instance" => defs.push(Def::Instance(self.parse_instance()?)),
|
|
other => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "module",
|
|
message: format!(
|
|
"unknown def head `{other}`; expected `data`, `fn`, `const`, `class`, `instance`, or `import`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
Extend the AST import line at parse.rs:88 to include `InstanceDef`
|
|
and `InstanceMethod`:
|
|
|
|
```rust
|
|
Arm, ClassDef, ClassMethod, ConstDef, Ctor, Def, FnDef, Import, InstanceDef,
|
|
InstanceMethod, Literal, Module, ParamMode, Pattern, SuperclassRef, Suppress, Term,
|
|
```
|
|
|
|
- [ ] **Step 2.4: Implement `parse_instance` and `parse_instance_method`**
|
|
|
|
Append to parse.rs after the `parse_class_method` / `parse_default_attr`
|
|
block from Task 1:
|
|
|
|
```rust
|
|
// ---- instance def --------------------------------------------------
|
|
|
|
fn parse_instance(&mut self) -> Result<InstanceDef, ParseError> {
|
|
self.expect_lparen("instance-def")?;
|
|
self.expect_keyword("instance")?;
|
|
let mut class: Option<String> = None;
|
|
let mut type_: Option<Type> = None;
|
|
let mut doc: Option<String> = None;
|
|
let mut methods: Vec<InstanceMethod> = Vec::new();
|
|
loop {
|
|
match self.peek_head_ident() {
|
|
Some("class") => {
|
|
if class.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "instance-def",
|
|
message: "instance has duplicate `(class ...)` clause".into(),
|
|
pos,
|
|
});
|
|
}
|
|
self.expect_lparen("instance.class")?;
|
|
self.expect_keyword("class")?;
|
|
class = Some(self.expect_ident("instance class name")?);
|
|
self.expect_rparen("instance.class")?;
|
|
}
|
|
Some("type") => {
|
|
if type_.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "instance-def",
|
|
message: "instance has duplicate `(type ...)` clause".into(),
|
|
pos,
|
|
});
|
|
}
|
|
type_ = Some(self.parse_type_attr()?);
|
|
}
|
|
Some("doc") => {
|
|
if doc.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "instance-def",
|
|
message: "instance has duplicate `(doc ...)` clause".into(),
|
|
pos,
|
|
});
|
|
}
|
|
doc = Some(self.parse_doc()?);
|
|
}
|
|
Some("method") => {
|
|
methods.push(self.parse_instance_method()?);
|
|
}
|
|
Some(other) => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "instance-def",
|
|
message: format!(
|
|
"unknown instance attribute `{other}`; expected `class`, `type`, `doc`, or `method`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
self.expect_rparen("instance-def")?;
|
|
let class = class.ok_or_else(|| ParseError::Production {
|
|
production: "instance-def",
|
|
message: "instance is missing required `(class ...)` clause".into(),
|
|
pos: 0,
|
|
})?;
|
|
let type_ = type_.ok_or_else(|| ParseError::Production {
|
|
production: "instance-def",
|
|
message: "instance is missing required `(type ...)` clause".into(),
|
|
pos: 0,
|
|
})?;
|
|
Ok(InstanceDef {
|
|
class,
|
|
type_,
|
|
methods,
|
|
doc,
|
|
})
|
|
}
|
|
|
|
fn parse_instance_method(&mut self) -> Result<InstanceMethod, ParseError> {
|
|
self.expect_lparen("instance.method")?;
|
|
self.expect_keyword("method")?;
|
|
let name = self.expect_ident("instance method name")?;
|
|
let mut body: Option<Term> = None;
|
|
loop {
|
|
match self.peek_head_ident() {
|
|
Some("body") => {
|
|
if body.is_some() {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "instance.method",
|
|
message: format!(
|
|
"instance method `{name}` has duplicate `(body ...)` clause"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
body = Some(self.parse_body_attr()?);
|
|
}
|
|
Some(other) => {
|
|
let pos = self.peek().map(|t| t.span.start).unwrap_or(0);
|
|
return Err(ParseError::Production {
|
|
production: "instance.method",
|
|
message: format!(
|
|
"unknown instance.method attribute `{other}`; expected `body`"
|
|
),
|
|
pos,
|
|
});
|
|
}
|
|
None => break,
|
|
}
|
|
}
|
|
self.expect_rparen("instance.method")?;
|
|
let body = body.ok_or_else(|| ParseError::Production {
|
|
production: "instance.method",
|
|
message: format!(
|
|
"instance method `{name}` is missing required `(body ...)` clause"
|
|
),
|
|
pos: 0,
|
|
})?;
|
|
Ok(InstanceMethod { name, body })
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2.5: Run RED tests, observe GREEN**
|
|
|
|
```
|
|
cargo test -p ailang-surface parses_minimal_instance_def
|
|
cargo test -p ailang-surface parses_instance_def_with_doc
|
|
cargo build --workspace
|
|
```
|
|
|
|
Expected: tests PASS. Build green.
|
|
|
|
- [ ] **Step 2.6: Commit**
|
|
|
|
```
|
|
git add crates/ailang-surface/src/parse.rs
|
|
git commit -m "iter 22b.4a.2: form-a parser arm for InstanceDef"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 3: Form-A printer arm for ClassDef
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/print.rs` — replace the `Def::Class`
|
|
placeholder arm in `write_def` (print.rs:99-107); append
|
|
`write_class_def`, `write_class_method`, `write_superclass`,
|
|
`write_class_default` after `write_const_def`.
|
|
|
|
- [ ] **Step 3.1: Write the failing test (RED)**
|
|
|
|
`crates/ailang-surface/src/print.rs` does NOT have an existing
|
|
`#[cfg(test)] mod tests` block (verified 2026-05-09). Add one at
|
|
end of file:
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ailang_core::ast::{
|
|
ClassDef, ClassMethod, Def, InstanceDef, InstanceMethod, Literal, Module,
|
|
ParamMode, SuperclassRef, Term, Type,
|
|
};
|
|
|
|
fn round_trip(m: Module, label: &str) {
|
|
let printed = print(&m);
|
|
let parsed = crate::parse::parse(&printed)
|
|
.unwrap_or_else(|e| panic!(
|
|
"{label}: re-parse failed: {e:?}\nprinted:\n{printed}"
|
|
));
|
|
let bytes_orig = ailang_core::canonical::to_bytes(&m);
|
|
let bytes_back = ailang_core::canonical::to_bytes(&parsed);
|
|
if bytes_orig != bytes_back {
|
|
let s_orig = String::from_utf8_lossy(&bytes_orig).into_owned();
|
|
let s_back = String::from_utf8_lossy(&bytes_back).into_owned();
|
|
panic!(
|
|
"{label}: canonical bytes differ.\noriginal: {s_orig}\nround: {s_back}\nprinted:\n{printed}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn print_then_parse_round_trip_minimal_class() {
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.to_string(),
|
|
name: "M".into(),
|
|
imports: vec![],
|
|
defs: vec![Def::Class(ClassDef {
|
|
name: "Foo".into(),
|
|
param: "a".into(),
|
|
superclass: None,
|
|
methods: vec![ClassMethod {
|
|
name: "m".into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Var { name: "a".into() }],
|
|
param_modes: vec![ParamMode::Implicit],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Implicit,
|
|
effects: vec![],
|
|
},
|
|
default: None,
|
|
}],
|
|
doc: None,
|
|
})],
|
|
};
|
|
round_trip(m, "minimal_class");
|
|
}
|
|
|
|
#[test]
|
|
fn print_then_parse_round_trip_class_with_superclass_and_default() {
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.to_string(),
|
|
name: "M".into(),
|
|
imports: vec![],
|
|
defs: vec![Def::Class(ClassDef {
|
|
name: "Bar".into(),
|
|
param: "a".into(),
|
|
superclass: Some(SuperclassRef {
|
|
class: "Foo".into(),
|
|
type_: "a".into(),
|
|
}),
|
|
methods: vec![ClassMethod {
|
|
name: "m".into(),
|
|
ty: Type::Fn {
|
|
params: vec![Type::Var { name: "a".into() }],
|
|
param_modes: vec![ParamMode::Implicit],
|
|
ret: Box::new(Type::int()),
|
|
ret_mode: ParamMode::Implicit,
|
|
effects: vec![],
|
|
},
|
|
default: Some(Term::Lit { lit: Literal::Int { value: 0 } }),
|
|
}],
|
|
doc: Some("docline".into()),
|
|
})],
|
|
};
|
|
round_trip(m, "class_with_superclass_and_default");
|
|
}
|
|
}
|
|
```
|
|
|
|
(Verified against `crates/ailang-core/src/ast.rs:585` —
|
|
`Type::Fn` has fields `params, param_modes, ret, ret_mode, effects`
|
|
ONLY; constraints live on `Type::Forall`, not on `Type::Fn`. The
|
|
canonical comparison helper is `ailang_core::canonical::to_bytes`,
|
|
returning `Vec<u8>` — same one used in
|
|
`crates/ailang-surface/tests/round_trip.rs:90`.)
|
|
|
|
- [ ] **Step 3.2: Run test, observe RED**
|
|
|
|
```
|
|
cargo test -p ailang-surface print_then_parse_round_trip_minimal_class
|
|
```
|
|
|
|
Expected: FAIL — the existing `Def::Class` arm in `write_def` (print.rs:99-107)
|
|
emits `(class Foo a)` only, which loses methods, so re-parse either
|
|
errors (missing `(param ...)` clause, missing methods) or canonical
|
|
JSON diverges.
|
|
|
|
- [ ] **Step 3.3: Replace placeholder + add helpers**
|
|
|
|
Edit print.rs's `write_def`. Replace the entire `Def::Class` and
|
|
`Def::Instance` placeholder arms (the 22b.1 stub block at print.rs:97-114)
|
|
with:
|
|
|
|
```rust
|
|
Def::Class(c) => write_class_def(out, c, level),
|
|
Def::Instance(i) => write_instance_def(out, i, level),
|
|
```
|
|
|
|
Append below `write_const_def` (around print.rs:240):
|
|
|
|
```rust
|
|
fn write_class_def(out: &mut String, c: &ClassDef, level: usize) {
|
|
indent(out, level);
|
|
out.push_str("(class ");
|
|
out.push_str(&c.name);
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(param ");
|
|
out.push_str(&c.param);
|
|
out.push(')');
|
|
if let Some(sc) = &c.superclass {
|
|
out.push('\n');
|
|
write_superclass(out, sc, level + 1);
|
|
}
|
|
if let Some(doc) = &c.doc {
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(doc ");
|
|
write_string_lit(out, doc);
|
|
out.push(')');
|
|
}
|
|
for m in &c.methods {
|
|
out.push('\n');
|
|
write_class_method(out, m, level + 1);
|
|
}
|
|
out.push(')');
|
|
}
|
|
|
|
fn write_superclass(out: &mut String, sc: &SuperclassRef, level: usize) {
|
|
indent(out, level);
|
|
out.push_str("(superclass (class ");
|
|
out.push_str(&sc.class);
|
|
out.push_str(") (type ");
|
|
out.push_str(&sc.type_);
|
|
out.push_str("))");
|
|
}
|
|
|
|
fn write_class_method(out: &mut String, m: &ClassMethod, level: usize) {
|
|
indent(out, level);
|
|
out.push_str("(method ");
|
|
out.push_str(&m.name);
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(type ");
|
|
write_type(out, &m.ty);
|
|
out.push(')');
|
|
if let Some(default) = &m.default {
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(default ");
|
|
write_term(out, default, level + 2);
|
|
out.push(')');
|
|
}
|
|
out.push(')');
|
|
}
|
|
```
|
|
|
|
Add the AST imports at print.rs:11. Replace:
|
|
|
|
```rust
|
|
Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Suppress, Term,
|
|
Type, TypeDef,
|
|
```
|
|
|
|
with:
|
|
|
|
```rust
|
|
Arm, ClassDef, ClassMethod, ConstDef, Ctor, Def, FnDef, Import, InstanceDef,
|
|
InstanceMethod, Literal, Module, ParamMode, Pattern, SuperclassRef, Suppress, Term,
|
|
Type, TypeDef,
|
|
```
|
|
|
|
(`InstanceDef` / `InstanceMethod` will be unused at end of this
|
|
task; mirrors Task 1's transient-warning shape — the `#[allow]`
|
|
gate or the next task closes it.)
|
|
|
|
- [ ] **Step 3.4: Run RED tests, observe GREEN**
|
|
|
|
```
|
|
cargo test -p ailang-surface print_then_parse_round_trip_minimal_class
|
|
cargo test -p ailang-surface print_then_parse_round_trip_class_with_superclass_and_default
|
|
cargo build --workspace
|
|
```
|
|
|
|
Expected: both PASS.
|
|
|
|
- [ ] **Step 3.5: Commit**
|
|
|
|
```
|
|
git add crates/ailang-surface/src/print.rs
|
|
git commit -m "iter 22b.4a.3: form-a printer arm for ClassDef"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 4: Form-A printer arm for InstanceDef
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/src/print.rs` — append
|
|
`write_instance_def` and `write_instance_method` after Task 3's
|
|
block.
|
|
|
|
- [ ] **Step 4.1: Write the failing test (RED)**
|
|
|
|
Append inside the `#[cfg(test)] mod tests { ... }` block created in
|
|
Task 3 (in print.rs):
|
|
|
|
```rust
|
|
#[test]
|
|
fn print_then_parse_round_trip_minimal_instance() {
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.to_string(),
|
|
name: "M".into(),
|
|
imports: vec![],
|
|
defs: vec![Def::Instance(InstanceDef {
|
|
class: "Foo".into(),
|
|
type_: Type::int(),
|
|
methods: vec![InstanceMethod {
|
|
name: "m".into(),
|
|
body: Term::Lit { lit: Literal::Int { value: 5 } },
|
|
}],
|
|
doc: None,
|
|
})],
|
|
};
|
|
round_trip(m, "minimal_instance");
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4.2: Run test, observe RED**
|
|
|
|
```
|
|
cargo test -p ailang-surface print_then_parse_round_trip_minimal_instance
|
|
```
|
|
|
|
Expected: FAIL — the original `Def::Instance` placeholder
|
|
(`(instance Class)`) was already replaced in Task 3 with a call to
|
|
`write_instance_def`, which doesn't exist yet. This is a build
|
|
failure (rather than an assertion failure), which still counts as RED.
|
|
|
|
- [ ] **Step 4.3: Implement the helpers**
|
|
|
|
Append to print.rs after `write_class_method`:
|
|
|
|
```rust
|
|
fn write_instance_def(out: &mut String, i: &InstanceDef, level: usize) {
|
|
indent(out, level);
|
|
out.push_str("(instance");
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(class ");
|
|
out.push_str(&i.class);
|
|
out.push(')');
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(type ");
|
|
write_type(out, &i.type_);
|
|
out.push(')');
|
|
if let Some(doc) = &i.doc {
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(doc ");
|
|
write_string_lit(out, doc);
|
|
out.push(')');
|
|
}
|
|
for m in &i.methods {
|
|
out.push('\n');
|
|
write_instance_method(out, m, level + 1);
|
|
}
|
|
out.push(')');
|
|
}
|
|
|
|
fn write_instance_method(out: &mut String, m: &InstanceMethod, level: usize) {
|
|
indent(out, level);
|
|
out.push_str("(method ");
|
|
out.push_str(&m.name);
|
|
out.push('\n');
|
|
indent(out, level + 1);
|
|
out.push_str("(body ");
|
|
write_term(out, &m.body, level + 2);
|
|
out.push(')');
|
|
out.push(')');
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4.4: Run RED test + full surface tests, observe GREEN**
|
|
|
|
```
|
|
cargo test -p ailang-surface print_then_parse_round_trip_minimal_instance
|
|
cargo test -p ailang-surface
|
|
cargo build --workspace
|
|
```
|
|
|
|
Expected: GREEN. Including the existing 22b.1 round-trip placeholder
|
|
test (if one exists) — the printer no longer emits the lossy stub.
|
|
|
|
- [ ] **Step 4.5: Commit**
|
|
|
|
```
|
|
git add crates/ailang-surface/src/print.rs
|
|
git commit -m "iter 22b.4a.4: form-a printer arm for InstanceDef"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 5: Retire round-trip skip-list
|
|
|
|
**Files:**
|
|
- Modify: `crates/ailang-surface/tests/round_trip.rs:36-50` — remove
|
|
the `test_22b1_*` / `test_22b2_*` / `test_22b3_*` filter clauses.
|
|
|
|
- [ ] **Step 5.1: Run the round-trip test against the unfiltered set first**
|
|
|
|
Before editing, sanity-check that the printer/parser arms cover every
|
|
22b fixture. From the workspace root:
|
|
|
|
```
|
|
for f in examples/test_22b1_*.ail.json examples/test_22b2_*.ail.json examples/test_22b3_*.ail.json; do
|
|
cargo run -q -p ail -- render "$f" > /tmp/rt.txt 2>&1 || echo "RENDER FAIL: $f"
|
|
done
|
|
```
|
|
|
|
Expected: every fixture renders without error. `ail render` is the
|
|
existing CLI subcommand wired to `ailang_surface::print`. If a
|
|
fixture errors, that is a Task 5 blocker — diagnose under `debug`
|
|
skill before retiring the filter.
|
|
|
|
- [ ] **Step 5.2: Remove the filter clauses**
|
|
|
|
Edit `crates/ailang-surface/tests/round_trip.rs`. Find the
|
|
`list_json_fixtures` function (round_trip.rs:25-55) and replace the
|
|
filter block:
|
|
|
|
```rust
|
|
n.ends_with(".ail.json")
|
|
&& !n.starts_with("test_22b1_")
|
|
&& !n.starts_with("test_22b2_")
|
|
&& !n.starts_with("test_22b3_")
|
|
```
|
|
|
|
with:
|
|
|
|
```rust
|
|
n.ends_with(".ail.json")
|
|
```
|
|
|
|
Also remove the now-stale comment block immediately above (the
|
|
`// Iter 22b.1 / 22b.2 / 22b.3 ...` comment); replace with a
|
|
one-line comment:
|
|
|
|
```rust
|
|
// Round-trip every fixture. The 22b.1/22b.2/22b.3 filter
|
|
// was retired in 22b.4a once the Form-A parser+printer
|
|
// arms for ClassDef / InstanceDef landed.
|
|
```
|
|
|
|
- [ ] **Step 5.3: Run the gate**
|
|
|
|
```
|
|
cargo test -p ailang-surface --test round_trip
|
|
```
|
|
|
|
Expected: PASS, with `passed: <N>` strictly greater than the
|
|
previous filtered count. If a fixture fails, do NOT re-add the
|
|
filter — that is silent regression. Diagnose via `debug` skill
|
|
mini-mode and fix before continuing.
|
|
|
|
- [ ] **Step 5.4: Run full bench gates**
|
|
|
|
The mono pass and the surface arms must coexist with the regression
|
|
suite:
|
|
|
|
```
|
|
python3 bench/check.py
|
|
python3 bench/compile_check.py
|
|
python3 bench/cross_lang.py
|
|
```
|
|
|
|
Expected: all three exit 0/0/0 (matches 22b.3 close).
|
|
|
|
- [ ] **Step 5.5: Commit**
|
|
|
|
```
|
|
git add crates/ailang-surface/tests/round_trip.rs
|
|
git commit -m "iter 22b.4a.5: retire round-trip skip-list for class/instance fixtures"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 6: Spec amendment
|
|
|
|
**Files:**
|
|
- Modify: `docs/specs/0002-22-typeclasses.md` — append an
|
|
"Amendments" section.
|
|
- Modify: `docs/DESIGN.md` — locate Decision 11 §"Resolution and
|
|
monomorphisation" (DESIGN.md mono-symbol naming subsection) and
|
|
add a one-paragraph note on the `__` separator.
|
|
|
|
- [ ] **Step 6.1: Append the spec amendments section**
|
|
|
|
Open `docs/specs/0002-22-typeclasses.md`. Append at end of
|
|
file:
|
|
|
|
```markdown
|
|
|
|
## Amendments
|
|
|
|
### 2026-05-09 — Iteration split: 22b.4 → 22b.4a + 22b.4b
|
|
|
|
Original 22b.4 scope ("Prelude module Show/Eq/Ord on Int/Float/Bool/String,
|
|
print rewiring, Form-B parser/printer arms, round-trip filter retired")
|
|
splits into two iterations:
|
|
|
|
- **22b.4a (this iter):** Form-A parser+printer arms for ClassDef
|
|
and InstanceDef; round-trip skip-list retired for `test_22b1_*`,
|
|
`test_22b2_*`, `test_22b3_*`. Pure surface plumbing, no codegen
|
|
or runtime change.
|
|
- **22b.4b (queued):** Prelude module containing `class Show a where
|
|
show : (a borrow) -> Str`, `instance Show Int`, the new
|
|
`int_to_str` C-runtime primitive, codegen wiring for it, and an
|
|
end-to-end `show 42` fixture that prints `42` through the existing
|
|
mono pass.
|
|
|
|
Substantive rationale: 22b.4a touches one crate (`ailang-surface`)
|
|
with no runtime/codegen risk; 22b.4b touches `runtime/`,
|
|
`ailang-codegen`, `ailang-check::builtins`, plus a new fixture, with
|
|
a different review surface and a different bench-gate exposure.
|
|
Bundling them was a brainstorm-time underestimate of the
|
|
runtime-side work; running them as separate iterations preserves
|
|
the bite-sized cycle the skill system enforces.
|
|
|
|
### 2026-05-09 — Form-A vs Form-B terminology fix
|
|
|
|
The original 22 spec ("Form-B parser/printer arms for ClassDef
|
|
/InstanceDef") and the 22b.1 round-trip-skip-list comment both
|
|
loosely call the s-expression projection "Form-B". That is a
|
|
terminology error: `crates/ailang-surface` is **Form-A** (the
|
|
parseable s-expression). `crates/ailang-prose` is **Form-B**
|
|
(human-readable, one-way projection — no parser by design). The
|
|
22b.4a arms are Form-A. Form-B printer arms for ClassDef /
|
|
InstanceDef are NOT required for the round-trip gate (it is
|
|
unparser-side); they are queued as an audit-cycle nicety, not as
|
|
a milestone-22 acceptance criterion.
|
|
|
|
The skip-list comment that read "the surface (Form-B) parser/printer
|
|
arms" was retired alongside the filter (Task 5).
|
|
|
|
### 2026-05-09 — Mono-symbol separator: `__` chosen over `#`
|
|
|
|
22a's brainstorm recommended `<method>#<typesurfacename>` for the
|
|
mono-symbol naming format with the implementer free to choose. 22b.3
|
|
implemented `<method>__<typesurfacename>` instead (e.g. `show__Int`,
|
|
`eq__Bool`). Reason: `#` is an invalid character in LLVM IR global
|
|
identifiers (`@ail_..._foo#Int_clos` is rejected by the LLVM
|
|
verifier). `__` is verifier-legal, identifier-legal in C-glue
|
|
contexts, and parses unambiguously into method + type surface name
|
|
(neither method names nor type surface names contain `__` by
|
|
convention). The recommendation in §"Open commitments (22a → 22b
|
|
implementer)" is updated to read `<method>__<typesurfacename>`.
|
|
|
|
Hash-suffix form for compound types (`<method>__<8-hex>`) follows
|
|
the same separator.
|
|
|
|
### 2026-05-09 — Acceptance criteria 4 still binding
|
|
|
|
Acceptance criterion 4 ("Round-trip filter `test_22b1_*` is removed;
|
|
all `examples/*.ail.json` pass round-trip") is satisfied by 22b.4a.
|
|
The `test_22b2_*` and `test_22b3_*` filters added subsequently are
|
|
also retired here. No further round-trip gate work is required for
|
|
22 milestone close.
|
|
```
|
|
|
|
- [ ] **Step 6.2: Add the `__` separator note to DESIGN.md**
|
|
|
|
Locate the DESIGN.md section that describes mono-symbol naming
|
|
under Decision 11. Find the subsection that mentions the symbol
|
|
shape (search DESIGN.md for `mono-symbol` or `monomorphis` or
|
|
`show#Int`).
|
|
|
|
```
|
|
grep -n "mono.*symbol\|show#Int\|show@Int\|monomorphi" docs/DESIGN.md
|
|
```
|
|
|
|
If a paragraph references `show@Int` or `show#Int`, replace the
|
|
example to read `show__Int` and append one sentence:
|
|
|
|
```
|
|
The separator is `__` rather than `#` or `@` because `#` and `@`
|
|
are invalid in LLVM IR global identifiers (the IR verifier rejects
|
|
them inside `@ail_<module>_<def>` mangled names). `__` is legal in
|
|
both LLVM IR and the C ABI used by the runtime glue, and parses
|
|
unambiguously into `<method>__<type-surface-name>` because neither
|
|
component contains `__` by project convention.
|
|
```
|
|
|
|
If DESIGN.md does not yet have a mono-symbol-naming paragraph
|
|
(possible — Decision 11 is high-level), then this step is a no-op
|
|
for DESIGN.md. The 22b.4a JOURNAL entry (Task 7) and the spec
|
|
amendment above suffice; do not invent a DESIGN.md section for the
|
|
sole purpose of placing the note. (DESIGN.md is canonical; spec is
|
|
the right home for naming-convention micro-decisions.)
|
|
|
|
- [ ] **Step 6.3: Commit**
|
|
|
|
```
|
|
git add docs/specs/0002-22-typeclasses.md docs/DESIGN.md
|
|
git commit -m "iter 22b.4a.6: spec + DESIGN amendments — 22b.4 split, terminology fix, '__' separator"
|
|
```
|
|
|
|
---
|
|
|
|
## Task 7: JOURNAL entry
|
|
|
|
**Files:**
|
|
- Modify: `docs/JOURNAL.md` — append iteration entry.
|
|
|
|
- [ ] **Step 7.1: Append the iteration entry**
|
|
|
|
```markdown
|
|
## 2026-05-09 — Iteration 22b.4a: Form-A parser+printer arms for ClassDef/InstanceDef
|
|
|
|
Closes the round-trip gap that 22b.1 deliberately deferred: every
|
|
`examples/*.ail.json` fixture now round-trips through
|
|
`parse(print(M))` with canonical-equal JSON, including the
|
|
`test_22b1_*`, `test_22b2_*`, `test_22b3_*` typeclass fixtures that
|
|
the skip-list excluded.
|
|
|
|
**Per-task subjects (mirror commit messages):**
|
|
|
|
- 22b.4a.1: form-a parser arm for ClassDef
|
|
- 22b.4a.2: form-a parser arm for InstanceDef
|
|
- 22b.4a.3: form-a printer arm for ClassDef
|
|
- 22b.4a.4: form-a printer arm for InstanceDef
|
|
- 22b.4a.5: retire round-trip skip-list for class/instance fixtures
|
|
- 22b.4a.6: spec + DESIGN amendments — 22b.4 split, terminology fix, '__' separator
|
|
|
|
**S-expression form locked:**
|
|
|
|
ClassDef:
|
|
```
|
|
(class <Name>
|
|
(param <ident>)
|
|
[(superclass (class <Name>) (type <ident>))]
|
|
[(doc "...")]
|
|
(method <name> (type <fn-type>) [(default <body>)])*)
|
|
```
|
|
|
|
InstanceDef:
|
|
```
|
|
(instance
|
|
(class <Name>)
|
|
(type <type-expr>)
|
|
[(doc "...")]
|
|
(method <name> (body <term>))*)
|
|
```
|
|
|
|
**Iteration split rationale:** the brainstorm-original 22b.4 bundled
|
|
surface arms with the Prelude module + `int_to_str` C-runtime
|
|
primitive + end-to-end `show 42` fixture. Surface arms touch one
|
|
crate with no runtime risk; the Prelude work touches `runtime/`,
|
|
`ailang-codegen`, `ailang-check::builtins`, plus codegen wiring for
|
|
the new primitive. Different review surface, different risk profile,
|
|
different bench-gate exposure. The split (22b.4a / 22b.4b) is
|
|
recorded in the spec amendments section and is the substantive
|
|
follow-up for 22b.4b.
|
|
|
|
**Terminology fix:** the original spec called the surface arms
|
|
"Form-B parser/printer arms". `crates/ailang-surface` is in fact
|
|
**Form-A** (parseable s-expression); `crates/ailang-prose` is Form-B
|
|
(one-way prose, no parser by design). The 22b.1 round-trip-skip-list
|
|
comment carried the same conflation. Both corrected in the spec
|
|
amendment.
|
|
|
|
**Known debt (deferred to 22b.4b):**
|
|
|
|
- `Form-B` (prose) printer arm for ClassDef / InstanceDef. One-way,
|
|
not gating; queued for the audit cycle or 22b.4b cleanup.
|
|
- DESIGN.md mono-symbol-naming paragraph: if Decision 11 acquires
|
|
one in a future revision, the `__` rationale lands there. For now
|
|
the spec amendment is the canonical record.
|
|
|
|
**Bench gates:** all three (`bench/check.py`, `bench/compile_check.py`,
|
|
`bench/cross_lang.py`) exited 0/0/0 (matches 22b.3 close).
|
|
|
|
**Next iter (queued):** 22b.4b — Prelude module with `class Show
|
|
a where show : (a borrow) -> Str`, `instance Show Int`, the
|
|
`int_to_str` C-runtime primitive, codegen wiring, and an end-to-end
|
|
`show 42` fixture printing `42` through the mono pass.
|
|
```
|
|
|
|
- [ ] **Step 7.2: Commit**
|
|
|
|
```
|
|
git add docs/JOURNAL.md
|
|
git commit -m "iter 22b.4a: journal entry"
|
|
```
|
|
|
|
---
|
|
|
|
## Acceptance gate (run after Task 7)
|
|
|
|
```
|
|
cargo test --workspace
|
|
python3 bench/check.py
|
|
python3 bench/compile_check.py
|
|
python3 bench/cross_lang.py
|
|
```
|
|
|
|
All four green is the iteration close. If any fail, the most
|
|
likely culprit is round-trip on a fixture that uses an unusual
|
|
Term shape inside a class default or instance body — diagnose via
|
|
`debug` skill, NOT by re-introducing the skip-list.
|