design: 20-family tidy-iter — refresh Data model + ecosystem inventory
Architect drift review after Family 20 surfaced four items; (1) and
(2) are doc drift fixed here, (3) and (4) are recorded as acceptable.
DESIGN.md changes:
- Data model section rewritten from "Data model (MVP)" claiming
only fn/const exist to "Data model" reflecting all three Def
kinds. Added the missing schema fields the architect named:
`tail` flag on app/do, the `seq` / `clone` / `reuse-as` / `letrec`
terms, `paramModes` / `retMode` on Type::Fn, `suppress` on FnDef,
`drop-iterative` on TypeDef, `args` on Type::Con, `vars` on
TypeDef. Each additive field's hash-stable serialisation is
called out once at the top so individual mentions stay terse.
`Suppress`, `ParamMode`, `Literal`, `Pattern` get their own
sub-blocks.
- Pipeline diagram now shows `--alloc=gc` (links libgc;
transitional) and `--alloc=rc` (emits ailang_rc_inc / _dec;
canonical) as two backends sharing the same MIR.
- Ecosystem inventory gained a "Surface forms" bullet for
ailang-surface (Iter 14c, lossless Form-A printer/parser) and
ailang-prose (Family 20, lossy Form-B projection). CLI bullet
now lists parse, render, prose, merge-prose.
JOURNAL entry records the architect findings, the resolutions for
items (1) and (2), and the acceptable-drift status of (3)
FnDef::synthetic factor-out (defer to next schema-additive
FnDef field) and (4) linearity.rs spurious warning (not
corpus-triggered).
Pure documentation iter — no code changes. 288 workspace tests
unchanged and green.
This commit is contained in:
+162
-28
@@ -26,9 +26,17 @@ evolving in lockstep with the language:
|
||||
|
||||
- **Language core** (`crates/ailang-core`, `crates/ailang-check`,
|
||||
`crates/ailang-codegen`): AST, type system, codegen.
|
||||
- **Surface forms** (`crates/ailang-surface`, `crates/ailang-prose`):
|
||||
the LLM-facing renderings of a module. `ailang-surface` (Iter 14c)
|
||||
is the lossless Form-A printer/parser — the canonical authoring
|
||||
surface fixed by Decision 6, with a round-trip property
|
||||
`parse ∘ print = id` gating every release. `ailang-prose` (Family 20)
|
||||
is the lossy Form-B projection — human-readable prose for review and
|
||||
edit, with no parser; re-integration goes through the
|
||||
LLM-mediator round-trip documented in `docs/PROSE_ROUNDTRIP.md`.
|
||||
- **CLI** (`crates/ail`): toolchain for tooling consumers — `manifest`,
|
||||
`describe`, `deps`, `check`, `build`, etc., preferably with `--json` for
|
||||
machine consumption.
|
||||
`describe`, `deps`, `check`, `build`, `parse`, `render`, `prose`,
|
||||
`merge-prose`, etc., preferably with `--json` for machine consumption.
|
||||
- **Examples** (`examples/`): canonical `.ail.json` programs. They are
|
||||
specification anchors, not demos — the E2E suite hangs off them.
|
||||
- **Agents** (`agents/`): specialised sub-prompts (implementer,
|
||||
@@ -1382,7 +1390,15 @@ with exactly one dot in the name is a qualified reference: `<prefix>.<def>`.
|
||||
Hash stability: no new AST node, no renamed fields — all previous module
|
||||
hashes stay bit-identical.
|
||||
|
||||
## Data model (MVP)
|
||||
## Data model
|
||||
|
||||
The on-disk JSON-AST is what the toolchain hashes, typechecks, and
|
||||
lowers. Every node in this section is the schema mirror of an enum or
|
||||
struct in `crates/ailang-core/src/ast.rs`; whenever the two disagree,
|
||||
`ast.rs` is the source of truth. Every additive field is declared with
|
||||
`skip_serializing_if` so pre-existing fixtures keep bit-identical
|
||||
canonical-JSON hashes — that gating contract is what makes growing
|
||||
the schema cheap.
|
||||
|
||||
### Module
|
||||
|
||||
@@ -1397,57 +1413,163 @@ hashes stay bit-identical.
|
||||
|
||||
### Def
|
||||
|
||||
`kind ∈ { "fn", "type", "effect", "const" }`. In the MVP only `fn` and `const`.
|
||||
`kind ∈ { "fn", "const", "type" }`. All three are real surface forms.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"kind": "fn",
|
||||
// fn (the unit that gets a content hash)
|
||||
{ "kind": "fn",
|
||||
"name": "<id>",
|
||||
"type": Type, // typically Type::Fn, optionally wrapped in Forall
|
||||
"params": ["<id>"...], // names bound in body, in type.params order
|
||||
"body": Term,
|
||||
"doc": "<optional string>",
|
||||
"suppress": [Suppress...] // Iter 19b, omitted when empty
|
||||
}
|
||||
|
||||
// const (top-level value; codegen emits as a global; body must be pure)
|
||||
{ "kind": "const",
|
||||
"name": "<id>",
|
||||
"type": Type,
|
||||
"params": ["<id>"...],
|
||||
"body": Term,
|
||||
"value": Term,
|
||||
"doc": "<optional string>"
|
||||
}
|
||||
|
||||
// type (algebraic data type; Iter 5; parameterised since Iter 13a)
|
||||
{ "kind": "type",
|
||||
"name": "<id>",
|
||||
"vars": ["<id>"...], // type parameters; omitted when empty (pre-13a hash-stable)
|
||||
"ctors": [
|
||||
{ "name": "<id>", "fields": [Type...] } // nullary ctor: fields = []
|
||||
...
|
||||
],
|
||||
"doc": "<optional string>",
|
||||
"drop-iterative": true // Iter 18e opt-in; omitted when false (pre-18e hash-stable)
|
||||
}
|
||||
```
|
||||
|
||||
**`Suppress`** (Iter 19b — entry in `FnDef.suppress`):
|
||||
|
||||
```jsonc
|
||||
{ "code": "<diagnostic-code>", // e.g. "over-strict-mode"
|
||||
"because": "<author reason>" // must be non-empty;
|
||||
// empty/whitespace fires `empty-suppress-reason` (Error)
|
||||
}
|
||||
```
|
||||
|
||||
### Term (expression)
|
||||
|
||||
```jsonc
|
||||
{ "t": "lit", "lit": { "kind": "int" | "bool" | "unit", "value": ... } }
|
||||
{ "t": "lit", "lit": Literal }
|
||||
{ "t": "var", "name": "<id>" }
|
||||
{ "t": "app", "fn": Term, "args": [Term...] }
|
||||
{ "t": "let", "name": "<id>", "value": Term, "body": Term }
|
||||
{ "t": "if", "cond": Term, "then": Term, "else": Term }
|
||||
{ "t": "do", "op": "<eff>/<op>", "args": [Term...] }
|
||||
|
||||
// fn application; tail flag is Iter 14e (musttail under codegen).
|
||||
// `tail` is omitted when false (pre-14e hash-stable).
|
||||
{ "t": "app", "fn": Term, "args": [Term...], "tail": false }
|
||||
|
||||
{ "t": "let", "name": "<id>", "value": Term, "body": Term }
|
||||
|
||||
// Local recursive let (Iter 16b.1). Always fn-shaped. The desugar pass
|
||||
// lifts most `letrec` to a synthetic top-level fn; `lift_letrecs` (16b.3)
|
||||
// finishes the job after typecheck for the residue that captures
|
||||
// let-bound names. Post-codegen, no `letrec` survives.
|
||||
{ "t": "letrec",
|
||||
"name": "<id>", "type": Type, "params": ["<id>"...],
|
||||
"body": Term, "in": Term }
|
||||
|
||||
{ "t": "if", "cond": Term, "then": Term, "else": Term }
|
||||
|
||||
// Effect-op invocation. `op` is "<eff>/<op>" (e.g. "io/print_int").
|
||||
// `tail` per Iter 14e (omitted when false).
|
||||
{ "t": "do", "op": "<eff>/<op>", "args": [Term...], "tail": false }
|
||||
|
||||
// Ctor application; `args` omitted when empty.
|
||||
{ "t": "ctor", "type": "<id>", "ctor": "<id>", "args": [Term...] }
|
||||
|
||||
{ "t": "match", "scrutinee": Term, "arms": [Arm...] }
|
||||
|
||||
// Anonymous fn value (Iter 8b); free vars captured from enclosing scope.
|
||||
{ "t": "lam",
|
||||
"params": ["<id>"...],
|
||||
"paramTypes": [Type...],
|
||||
"retType": Type,
|
||||
"effects": ["<id>"...],
|
||||
"body": Term }
|
||||
{ "t": "seq", "lhs": Term, "rhs": Term }
|
||||
|
||||
// Sequencing (Iter 10). Semantically `let _ = lhs in rhs`; lhs must be Unit.
|
||||
{ "t": "seq", "lhs": Term, "rhs": Term }
|
||||
|
||||
// Iter 18c.1: explicit RC clone. Codegen lowers as
|
||||
// `call void @ailang_rc_inc(ptr %v)` before returning %v under `--alloc=rc`.
|
||||
{ "t": "clone", "value": Term }
|
||||
|
||||
// Iter 18d.1: explicit reuse-as hint. `body` must be allocating
|
||||
// (typically `ctor` or `lam`); `source` must be a bare `var`. Codegen
|
||||
// lowers as in-place rewrite under `--alloc=rc`.
|
||||
{ "t": "reuse-as", "source": Term, "body": Term }
|
||||
```
|
||||
|
||||
In the MVP, `do` is only a direct call to a built-in effect op (no handler).
|
||||
A `lam` term constructs an anonymous function value; free variables of
|
||||
its body are captured from the enclosing scope (see Iter 8 closure
|
||||
conversion in JOURNAL). A `seq` term evaluates `lhs` for its effects
|
||||
(its result must be Unit) and yields `rhs`'s value — equivalent to
|
||||
`let _ = lhs in rhs`.
|
||||
In the MVP, `do` is only a direct call to a built-in effect op (no
|
||||
handler). A `lam` term constructs an anonymous function value; free
|
||||
variables of its body are captured from the enclosing scope (see
|
||||
Iter 8 closure conversion in JOURNAL).
|
||||
|
||||
**`Literal`**:
|
||||
|
||||
```jsonc
|
||||
{ "kind": "int", "value": <i64> }
|
||||
{ "kind": "bool", "value": <bool> }
|
||||
{ "kind": "str", "value": "<utf-8>" }
|
||||
{ "kind": "unit" }
|
||||
```
|
||||
|
||||
**`Pattern`** (the `pat` field of an `Arm`; discriminator `p`):
|
||||
|
||||
```jsonc
|
||||
{ "p": "wild" } // _
|
||||
{ "p": "var", "name": "<id>" } // x — binds the value
|
||||
{ "p": "lit", "lit": Literal }
|
||||
{ "p": "ctor", "ctor": "<id>", "fields": [Pattern...] } // fields omitted when empty
|
||||
```
|
||||
|
||||
Patterns are linear: each pattern variable may appear at most once.
|
||||
|
||||
### Type
|
||||
|
||||
```jsonc
|
||||
{ "k": "con", "name": "Int" }
|
||||
{ "k": "con", "name": "Bool" }
|
||||
{ "k": "con", "name": "Unit" }
|
||||
{ "k": "fn", "params": [Type...], "ret": Type, "effects": ["IO"...] }
|
||||
{ "k": "var", "name": "a" }
|
||||
{ "k": "forall", "vars": ["a"...], "body": Type }
|
||||
// Type-constructor application. `args` omitted when empty
|
||||
// (pre-13a hash-stable for non-parameterised cases like Int, Bool, ...).
|
||||
{ "k": "con", "name": "<id>", "args": [Type...] }
|
||||
|
||||
// Function type. Decision 10 (Iter 18a) added paramModes/retMode as
|
||||
// metadata on Type::Fn — they are NOT separate Type variants, so every
|
||||
// existing match-arm in the typechecker (unify, occurs, apply) keeps
|
||||
// working. `paramModes` omitted when every entry is "implicit";
|
||||
// `retMode` omitted when "implicit" (pre-18a hash-stable).
|
||||
{ "k": "fn",
|
||||
"params": [Type...],
|
||||
"paramModes": [ParamMode...],
|
||||
"ret": Type,
|
||||
"retMode": ParamMode,
|
||||
"effects": ["<id>"...] }
|
||||
|
||||
{ "k": "var", "name": "<id>" }
|
||||
|
||||
// Top-level polymorphism only.
|
||||
{ "k": "forall", "vars": ["<id>"...], "body": Type }
|
||||
```
|
||||
|
||||
**`ParamMode`** (Iter 18a / Decision 10):
|
||||
|
||||
```
|
||||
"implicit" — pre-18a / unannotated. Treated as `own` by the typechecker.
|
||||
"own" — (own T) — caller transfers ownership; callee consumes.
|
||||
"borrow" — (borrow T) — caller retains ownership; callee may not consume.
|
||||
```
|
||||
|
||||
`implicit ≡ own` semantically; the distinction exists so pre-18a
|
||||
fixtures continue to serialize without the mode wrapper and keep their
|
||||
canonical-JSON hash.
|
||||
|
||||
## Pipeline
|
||||
|
||||
```
|
||||
@@ -1455,13 +1577,25 @@ conversion in JOURNAL). A `seq` term evaluates `lhs` for its effects
|
||||
├─ load + validate schema
|
||||
├─ resolve names + assign hashes
|
||||
├─ desugar (AST → AST, Iter 16a)
|
||||
├─ typecheck (HM, effect rows)
|
||||
├─ typecheck (HM, effect rows; mode-strict per Decision 10)
|
||||
├─ lift_letrecs (post-typecheck AST → AST, Iter 16b.3)
|
||||
├─ lower to MIR (SSA-like, named SSA values)
|
||||
├─ emit LLVM IR (.ll)
|
||||
└─ clang -O2 *.ll -o binary (links libgc for @GC_malloc)
|
||||
└─ clang -O2 *.ll -o binary
|
||||
--alloc=gc → links libgc (@GC_malloc; transitional)
|
||||
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical)
|
||||
```
|
||||
|
||||
Two allocator backends share the same MIR. `--alloc=gc` links libgc
|
||||
and is the transitional fallback (no inc/dec instrumentation, no mode
|
||||
checks at codegen time). `--alloc=rc` is the canonical backend
|
||||
committed to in Decision 10: the typechecker enforces
|
||||
`(own)` / `(borrow)` modes, codegen emits `ailang_rc_inc` / `_dec`
|
||||
calls at the points dictated by linearity, and `Term::Clone` /
|
||||
`Term::ReuseAs` materialise into actual rc-bumps and in-place
|
||||
rewrites respectively. Boehm-on-`--alloc=gc` is on the path to
|
||||
retirement; see the JOURNAL queue.
|
||||
|
||||
The **desugar** pass (`ailang-core::desugar::desugar_module`) runs
|
||||
before typecheck and codegen in every entry point of `ailang-check`
|
||||
and `ailang-codegen`. It is a pure AST → AST rewriter — currently
|
||||
|
||||
Reference in New Issue
Block a user