176821c2e7
The 3020-line docs/DESIGN.md is replaced by the design/ ledger:
design/INDEX.md (sole addressable spine, typed Contracts+Models tables,
polymorphic links — prose file OR authoritative source //!), 14
design/contracts/*.md test-linked invariants + 3 source-link-only
contracts (mangling/env-construction/qualified-xref, no prose file —
code is SoT), 5 design/models/*.md whitepapers, and
docs/journals/2026-05-19-design-decision-records.md (the
relitigation-guard archive — every why/rejected/does-not-do/rollback/
empirical ### moved out at ###-granularity). Clean cut: git rm
docs/DESIGN.md, no stub.
RED-first crates/ailang-core/tests/design_index_pin.rs — the 4-clause
anti-regrowth spine (DESIGN.md-gone / every-INDEX-link-resolves /
every-contract-names-a-resolvable-ratifier /
contracts-carry-no-decision-record-prose) — demonstrably RED before,
GREEN after. Build-atomic by task ordering: design_schema_drift.rs's
include_str! (the only compile-time consumer) retargeted to
design/contracts/data-model.md BEFORE the deletion; its
## Data model/## Pipeline slicer dropped (a simplification the split
enables). 2 NoInstance diagnostics + 2 lockstep E2Es retargeted to
design/contracts/{float-semantics,typeclasses}.md. ~12 agent reading
lists + 5 SKILL bodies + CLAUDE.md + skills/README.md + ~25
code/C/.ail/spec comment xrefs retargeted; OQ7 dangling 'Iter 13b'
cite deleted (no forward target — a pointer would be fiction).
honesty-rule.md rewritten so the rule names the new home
(rationale->journals), resolving the recon-found internal
contradiction; the two docs_honesty_pin.rs:70,72 pinned phrases kept
verbatim+contiguous.
Boss-verified independently: cargo test --workspace 646 passed /
0 failed; design_index_pin 4/4; acceptance grep CLEAN of live
DESIGN.md refs (residuals = only the spec-mandated clause-4
deletion-enforcer). 2 DONE_WITH_CONCERNS routed to the mandatory
milestone-close audit: (a) str-abi.md:23 '(iter str-concat,
2026-05-13)' provenance stamp trips advisory architect_sweeps Sweep-1
— Boss-confirmed byte-identical to DESIGN.md@deeffb1:2062-2065, a
faithfully-migrated PRE-EXISTING anchor (regexes verbatim, only path
retargeted), NOT split-introduced — RATIFY-or-tidy at audit; (b) a
now stale-direction intra-prose 'see Str ABI below' cross-ref in
float-semantics.md — audit-adjudication candidate. Plan defect noted:
Task 9 Step 4's verbatim acceptance grep used a ^./ anchor not
matching the system's grep -rIn output; substance re-verified CLEAN.
Spec grounding-check PASS x2. Journals INDEX + decision-records
pointer appended (Boss-only).
243 lines
8.5 KiB
Markdown
243 lines
8.5 KiB
Markdown
# Data model
|
|
|
|
## Data model
|
|
|
|
The on-disk JSON-AST is what the toolchain hashes, typechecks, and
|
|
lowers. **This section is the canonical schema.** The Rust types in
|
|
`crates/ailang-core/src/ast.rs` are the in-memory projection of it;
|
|
when the two disagree, this section wins, and the drift test
|
|
`crates/ailang-core/tests/design_schema_drift.rs` fires. 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
|
|
|
|
```jsonc
|
|
{
|
|
"schema": "ailang/v0",
|
|
"name": "<id>",
|
|
"imports": [{ "module": "<id>", "as": "<id>" }],
|
|
"defs": [Def...]
|
|
}
|
|
```
|
|
|
|
### Def
|
|
|
|
`kind ∈ { "fn", "const", "type", "class", "instance" }`. All five
|
|
are real surface forms.
|
|
|
|
```jsonc
|
|
// 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>",
|
|
"export": "<optional C symbol>", // omitted when absent (hash-stable when omitted); see §"Embedding ABI"
|
|
"suppress": [Suppress...] // omitted when empty
|
|
}
|
|
|
|
// const (top-level value; codegen emits as a global; body must be pure)
|
|
{ "kind": "const",
|
|
"name": "<id>",
|
|
"type": Type,
|
|
"value": Term,
|
|
"doc": "<optional string>"
|
|
}
|
|
|
|
// type (algebraic data type; parameterised)
|
|
{ "kind": "type",
|
|
"name": "<id>",
|
|
"vars": ["<id>"...], // type parameters; omitted when empty (hash-stable when omitted)
|
|
"ctors": [
|
|
{ "name": "<id>", "fields": [Type...] } // nullary ctor: fields = []
|
|
...
|
|
],
|
|
"doc": "<optional string>",
|
|
"drop-iterative": true // opt-in; omitted when false (hash-stable when omitted)
|
|
}
|
|
|
|
// class (typeclass declaration; see Decision 11)
|
|
{ "kind": "class",
|
|
"name": "<id>", // class name (e.g. "Show")
|
|
"param": "<id>", // single class parameter, kind *
|
|
"superclass": null, // or { "class": "<id>", "type": "<param>" } — "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1)
|
|
"methods": [
|
|
{ "name": "<id>",
|
|
"type": Type, // FnSig over the class param
|
|
"default": Term // optional fallback body; null = abstract-required
|
|
}
|
|
...
|
|
],
|
|
"doc": "<optional string>"
|
|
}
|
|
|
|
// instance (typeclass instance; see Decision 11)
|
|
{ "kind": "instance",
|
|
"class": "<id>", // class being instantiated; canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1)
|
|
"type": Type, // concrete type expression (never the class param)
|
|
"methods": [
|
|
{ "name": "<id>", "body": Term }
|
|
...
|
|
],
|
|
"doc": "<optional string>"
|
|
}
|
|
```
|
|
|
|
**`Suppress`** (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": Literal }
|
|
{ "t": "var", "name": "<id>" }
|
|
|
|
// fn application; tail flag triggers musttail under codegen.
|
|
// `tail` is omitted when false (hash-stable when omitted).
|
|
{ "t": "app", "fn": Term, "args": [Term...], "tail": false }
|
|
|
|
{ "t": "let", "name": "<id>", "value": Term, "body": Term }
|
|
|
|
// Local recursive let. Always fn-shaped. The desugar pass
|
|
// lifts most `letrec` to a synthetic top-level fn; `lift_letrecs`
|
|
// 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_str").
|
|
// `tail` triggers musttail (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; free vars captured from enclosing scope.
|
|
{ "t": "lam",
|
|
"params": ["<id>"...],
|
|
"paramTypes": [Type...],
|
|
"retType": Type,
|
|
"effects": ["<id>"...],
|
|
"body": Term }
|
|
|
|
// Sequencing. Semantically `let _ = lhs in rhs`; lhs must be Unit.
|
|
{ "t": "seq", "lhs": Term, "rhs": Term }
|
|
|
|
// Explicit RC clone. Codegen lowers as
|
|
// `call void @ailang_rc_inc(ptr %v)` before returning %v under `--alloc=rc`.
|
|
{ "t": "clone", "value": Term }
|
|
|
|
// 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 }
|
|
|
|
// loop-recur iter 1: strict iteration block. `binders` declares
|
|
// one or more loop parameters (name, type, init), evaluated in
|
|
// order on loop entry; `body` is in scope of all binders. The
|
|
// loop's value is `body`'s value on the iteration that exits via a
|
|
// non-`recur` branch. Strictly additive (no `skip_serializing_if`;
|
|
// pre-existing fixtures hash bit-identically — none carry the tag).
|
|
// No totality claim — an infinite loop is legal. See
|
|
// `docs/specs/2026-05-17-loop-recur.md`.
|
|
{ "t": "loop",
|
|
"binders": [ { "name": "<id>", "type": Type, "init": Term }, ... ],
|
|
"body": Term }
|
|
|
|
// loop-recur iter 1: re-enter the lexically innermost enclosing
|
|
// `loop`, rebinding its binders positionally to `args`. Transfers
|
|
// control (no fall-through); valid only in tail position of its
|
|
// enclosing loop (enforced at typecheck, `recur-not-in-tail-position`).
|
|
{ "t": "recur",
|
|
"args": [ 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.
|
|
|
|
Loop binders are alloca-resident: typecheck binds them in the
|
|
ordinary local scope plus a positional `loop_stack`, and codegen
|
|
lowers them as entry-block allocas. Capturing a `loop` binder into a
|
|
lambda body is rejected at typecheck via
|
|
`CheckError::LoopBinderCapturedByLambda`. See
|
|
`docs/specs/2026-05-17-loop-recur.md`.
|
|
|
|
**`Literal`**:
|
|
|
|
```jsonc
|
|
{ "kind": "int", "value": <i64> }
|
|
{ "kind": "bool", "value": <bool> }
|
|
{ "kind": "str", "value": "<utf-8>" }
|
|
{ "kind": "unit" }
|
|
{ "kind": "float", "bits": "<16-lowercase-hex>" }
|
|
```
|
|
|
|
**`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
|
|
// Type-constructor application. `args` omitted when empty
|
|
// (hash-stable when omitted, for non-parameterised cases like Int, Bool, ...).
|
|
{ "k": "con", "name": "<id>", "args": [Type...] } // "name": canonical form (bare for same-module / primitives, "<module>.<TypeName>" for cross-module; see §"Type::Con name scoping" / ct.1)
|
|
|
|
// Function type. Decision 10 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" (hash-stable when omitted).
|
|
{ "k": "fn",
|
|
"params": [Type...],
|
|
"paramModes": [ParamMode...],
|
|
"ret": Type,
|
|
"retMode": ParamMode,
|
|
"effects": ["<id>"...] }
|
|
|
|
{ "k": "var", "name": "<id>" }
|
|
|
|
// Top-level polymorphism only. `constraints` carries class
|
|
// constraints (Decision 11); omitted when empty (hash-stable when omitted).
|
|
{ "k": "forall",
|
|
"vars": ["<id>"...],
|
|
"constraints": [{ "class": "<id>", "type": "<id>" }, ...], // "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1)
|
|
"body": Type }
|
|
```
|
|
|
|
**`ParamMode`** (Decision 10):
|
|
|
|
```
|
|
"implicit" — unannotated / back-compat. 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 existing
|
|
unannotated fixtures continue to serialize without the mode wrapper and keep their
|
|
canonical-JSON hash.
|
|
|
|
Ratified by: `crates/ailang-core/tests/design_schema_drift.rs`.
|