design/ ledger: dense cross-linking via three topical splits + 88-link sweep

The first formal-links milestone shipped clause-5 + 8 links across the
existing file layout. Browsing surfaced that file-only granularity is
only as precise as the file boundaries — three files mixed two or
three navigation targets under one address, so the 8 links could not
multiply without ambiguity. This commit fixes the substrate, then
applies the sweep the original milestone deferred.

Splits (each extracts an already-self-contained section into its own
file so links land on the topic, not the parent doc's TOC):

  contracts/typeclasses.md
    → +contracts/prelude-classes.md  (Eq/Ord/Show ships, polymorphic `print`)
    → +contracts/method-dispatch.md  (5-step dispatch rule, candidate index)

  contracts/memory-model.md
    → +contracts/language-constraints.md  (the 4 binding constraints
                                           making RC sound without a
                                           cycle collector)

  models/authoring-surface.md
    → +models/prose-projection.md  (Form-B / `ail prose` / merge-prose)

Each new file enters design/INDEX.md as its own row (three contracts
share show_no_instance_e2e.rs / uniqueness.rs as ratifying tests;
prose-projection is a model). Two pre-existing links rebind to the
new topic-files (memory-model.md → method-dispatch.md;
float-semantics.md → prelude-classes.md).

Link sweep: 8 → 88 formal Markdown links over 23 files. Every file
in design/contracts/ + design/models/ now has at least one outgoing
link; the tree is fully connected. Links are file-relative
`[label](path)` per the established convention, fenced code blocks
are skipped (a `](` inside ```jsonc``` is literal text), the durable
tier (design/ + crates/ + runtime/) is enforced by clause-5.

Tests:
  - design_index_pin.rs (5/5 clauses): clean-cut, INDEX resolution,
    ratifying-test resolution, no decision-record prose in contracts/,
    body links durable + resolving.
  - docs_honesty_pin.rs (5/5): one assertion rebinds from typeclasses.md
    to prelude-classes.md (where the gated sentence now lives);
    design_corpus widens to include the 4 new files so the Wunschdenken
    / doc-archaeology sweeps continue to cover everything that used to
    live in the parents.

No spec/plan/journal for this batch — interactive collaboration after
the milestone closed; the user gated the splits explicitly before the
sweep.
This commit is contained in:
2026-05-20 00:24:08 +02:00
parent cca6e3959d
commit 19dc42f5ca
25 changed files with 386 additions and 263 deletions
+7 -83
View File
@@ -140,87 +140,11 @@ construction. **Cons:** the existing format mixes four mini-dialects
params, `<>` for type args). Formalising it crisply is harder than
designing a uniform form from scratch.
## Form (B) — human prose projection
The chosen form is (A), shipped as the `ailang-surface` crate (see
[authoring surface](../contracts/authoring-surface.md)); the
canonical JSON-AST it round-trips against is documented in
[Data model](../contracts/data-model.md), and the round-trip
property itself in [Roundtrip Invariant](../contracts/roundtrip-invariant.md).
AILang ships a
second textual projection of the AST: `ailang-prose`, a one-way
projection from `Module → human-readable text`. It is **not** an
authoring surface; it is the "display" projection that Decision 6's
architectural pin (line 167176) explicitly anticipated:
> *"Future projections are explicitly anticipated: a visual /
> graphical front-end is a plausible second projection for human
> review and inspection (display being the one case where non-AI
> eyes matter). The architecture leaves room: any producer of
> well-formed `ailang-core::ast::Module` values is a valid
> front-end."*
Form (B) targets the specific failure mode where a human reviewer
needs to read an AILang module quickly. Form (A) was designed to
fit a 30-production EBNF spec and to be parsed zero-shot by foreign
LLMs; that prioritisation makes it dense and visually noisy for
human readers. Form (B) inverts the trade-offs:
- **Rust-flavoured surface.** Braces and `=>` for match arms,
Rust-aligned 4-level operator precedence, infix arithmetic
(`a + b`, not `+(a, b)`), unary `!` for `not`.
- **Lossy by design.** Projection elides machinery the LLM can
re-derive: `(con T)` wrappers (`(con Int)``Int`), the
`(fn-type (params ...) (ret ...))` wrap, `(term-ctor T C ...)`
collapses to `C(...)`, redundant parens. Only the AST machinery
whose information is recoverable from typecheck context.
- **Lossless on load-bearing detail.** Mode annotations
(`own T`, `borrow T`), effects (`with IO`), explicit `clone`,
`reuse-as`, doc strings, type annotations on signatures and
lambdas, the `tail` flag — all preserved verbatim.
Critically, **form (B) has no parser**. Form (A) is round-trippable
by construction (Decision 6 constraint 2); form (B) deliberately is
not. Re-integrating prose edits requires an external LLM mediator,
not a compiler pass — the prompt template `ail merge-prose`
composes the six-step cycle.
Form (B) does not weaken any Decision 6 invariant:
- The JSON-AST remains the only hashable artefact. Prose is not
hashed, not content-addressed, not load-bearing for any
cross-module reference.
- Form (A) remains the canonical authoring surface. Foreign LLMs
still author against form (A); humans review and edit through
form (B).
- The 30-production grammar of form (A) is unchanged.
- `ailang-check` and `ailang-codegen` remain projection-agnostic;
`ailang-prose` is a downstream consumer of `ailang-core::ast`,
parallel to `ailang-surface` but in the rendering direction only.
The CLI gains `ail prose <m.ail.json>` (the deterministic
projection) and `ail merge-prose <m.ail.json> <edited.prose.txt>`
(the mediator-prompt composer); both are listed in the CLI section
below.
**Form-A spec embedding.** An earlier `merge-prose`
prompt instructed the LLM to emit JSON-AST and offered a 12-line
schema-essentials reminder; that combination did not give a
foreign LLM enough to produce valid output. The current prompt revises this:
- The LLM emits **Form-A** (the canonical authoring surface), not
JSON. JSON-AST stays the only hashable artefact, but it is not
a writing surface. The user runs `ail parse foo.new.ail`
before `ail check` to produce the canonical JSON.
- `crates/ailang-core/specs/form_a.md` is the complete LLM-targeted
Form-A specification — grammar, every term / pattern / type /
def keyword, schema invariants, pitfall catalogue, four
few-shot modules drawn from `examples/*.ail`. It is exported
as `ailang_core::FORM_A_SPEC` and embedded verbatim in every
`merge-prose` prompt.
- `crates/ailang-core/tests/spec_drift.rs` walks every variant of
`Term`, `Pattern`, `Type`, `Def`, `Literal` via exhaustive
`match` and asserts an anchor for each appears in the spec.
The exhaustive match is the load-bearing piece: adding a new
variant without updating the match fails compilation in this
test, before its assertions even run. Hand-written content,
mechanical drift detection.
The cycle's lowest-common-denominator path is the static prompt
(`ail merge-prose | client | ail parse | ail check`), which works
with any client.
For the prose projection used for human review (Form B), see
[prose projection](prose-projection.md).
+4 -1
View File
@@ -6,7 +6,10 @@ The default is total, pure functions. Effects are declared as a set in the
function type: `(Int) -> Int ![IO]`. The effect set is a
flat, unordered, closed set of effect names, unified by set-equality —
there is no effect row variable; a signature lists exactly the effects
its body may perform.
its body may perform. The schema-level effect-set field on `Type::Fn`
is documented in [Data model](../contracts/data-model.md); what is
currently in scope vs. excluded lives in
[scope boundaries](../contracts/scope-boundaries.md).
In the MVP only the effect `IO` is wired up (its sole op is `io/print_str`).
`Diverge` (for non-termination) is reserved as an effect name but is
unimplemented — no op, no codegen, no checker injection — in the same
+15 -6
View File
@@ -17,18 +17,20 @@
```
Two allocator backends share the same MIR. `--alloc=rc` is the
canonical backend committed to in Decision 10 and the CLI default.
canonical backend committed to in the
[memory model](../contracts/memory-model.md) and the CLI default.
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. `--alloc=gc` selects the transitional Boehm
backend; `--alloc=rc` is the canonical backend (Decision 10) and the
CLI default.
backend (see [RC + uniqueness](rc-uniqueness.md));
`--alloc=rc` is the canonical backend and the CLI default.
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
The **desugar** pass
([`ailang-core::desugar::desugar_module`](../../crates/ailang-core/src/desugar.rs))
runs before typecheck and codegen in every entry point of
`ailang-check` and `ailang-codegen`. It is a pure AST → AST rewriter — currently
only flattens nested constructor patterns, but is the chosen
home for any future surface-smoothing rewrites that should not bloat
the core AST or the backends. **Critical invariant:** `CheckedModule.symbols`
@@ -69,3 +71,10 @@ ail emit-ir <module> [--emit=staticlib] — writes .ll (staticlib: a main-fre
ail build <module> [--emit=staticlib] — full pipeline → binary (staticlib: lib<entry>.a + libailang_rt.a)
ail run <module> — build + execute (tempdir), passthrough exit code
```
The text projections the CLI moves between are documented in
[authoring surface](../contracts/authoring-surface.md) (Form-A,
round-trippable) and [prose projection](prose-projection.md)
(Form-B, lossy, no parser); `ail build --emit=staticlib` produces
the layout fixed in [embedding ABI](../contracts/embedding-abi.md)
plus [frozen value layout](../contracts/frozen-value-layout.md).
+89
View File
@@ -0,0 +1,89 @@
# Form (B) — prose projection whitepaper
## Form (B) — human prose projection
AILang ships a
second textual projection of the AST: `ailang-prose`, a one-way
projection from `Module → human-readable text`. It is **not** an
authoring surface; it is the "display" projection that Decision 6's
architectural pin (see [authoring surface](../contracts/authoring-surface.md))
explicitly anticipated:
> *"Future projections are explicitly anticipated: a visual /
> graphical front-end is a plausible second projection for human
> review and inspection (display being the one case where non-AI
> eyes matter). The architecture leaves room: any producer of
> well-formed `ailang-core::ast::Module` values is a valid
> front-end."*
Form (B) targets the specific failure mode where a human reviewer
needs to read an AILang module quickly. Form (A) was designed to
fit a 30-production EBNF spec and to be parsed zero-shot by foreign
LLMs; that prioritisation makes it dense and visually noisy for
human readers. Form (B) inverts the trade-offs:
- **Rust-flavoured surface.** Braces and `=>` for match arms,
Rust-aligned 4-level operator precedence, infix arithmetic
(`a + b`, not `+(a, b)`), unary `!` for `not`.
- **Lossy by design.** Projection elides machinery the LLM can
re-derive: `(con T)` wrappers (`(con Int)``Int`), the
`(fn-type (params ...) (ret ...))` wrap, `(term-ctor T C ...)`
collapses to `C(...)`, redundant parens. Only the AST machinery
whose information is recoverable from typecheck context.
- **Lossless on load-bearing detail.** Mode annotations
(`own T`, `borrow T`), effects (`with IO`), explicit `clone`,
`reuse-as`, doc strings, type annotations on signatures and
lambdas, the `tail` flag — all preserved verbatim.
Critically, **form (B) has no parser**. Form (A) is round-trippable
by construction (see [roundtrip invariant](../contracts/roundtrip-invariant.md));
form (B) deliberately is not. Re-integrating prose edits requires an
external LLM mediator, not a compiler pass — the prompt template
`ail merge-prose` composes the six-step cycle.
Form (B) does not weaken any Decision 6 invariant:
- The [JSON-AST](../contracts/data-model.md) remains the only
hashable artefact. Prose is not hashed, not content-addressed,
not load-bearing for any cross-module reference.
- Form (A) remains the canonical authoring surface. Foreign LLMs
still author against form (A); humans review and edit through
form (B).
- The 30-production grammar of form (A) is unchanged.
- `ailang-check` and `ailang-codegen` remain projection-agnostic;
`ailang-prose` is a downstream consumer of `ailang-core::ast`,
parallel to `ailang-surface` but in the rendering direction only.
The CLI gains `ail prose <m.ail.json>` (the deterministic
projection) and `ail merge-prose <m.ail.json> <edited.prose.txt>`
(the mediator-prompt composer); both are listed under
[pipeline](pipeline.md).
## Form-A spec embedding
An earlier `merge-prose`
prompt instructed the LLM to emit JSON-AST and offered a 12-line
schema-essentials reminder; that combination did not give a
foreign LLM enough to produce valid output. The current prompt revises this:
- The LLM emits **Form-A** (the canonical authoring surface), not
JSON. JSON-AST stays the only hashable artefact, but it is not
a writing surface. The user runs `ail parse foo.new.ail`
before `ail check` to produce the canonical JSON.
- `crates/ailang-core/specs/form_a.md` is the complete LLM-targeted
Form-A specification — grammar, every term / pattern / type /
def keyword, schema invariants, pitfall catalogue, four
few-shot modules drawn from `examples/*.ail`. It is exported
as `ailang_core::FORM_A_SPEC` and embedded verbatim in every
`merge-prose` prompt.
- `crates/ailang-core/tests/spec_drift.rs` walks every variant of
`Term`, `Pattern`, `Type`, `Def`, `Literal` via exhaustive
`match` and asserts an anchor for each appears in the spec.
The exhaustive match is the load-bearing piece: adding a new
variant without updating the match fails compilation in this
test, before its assertions even run. Hand-written content,
mechanical drift detection.
The cycle's lowest-common-denominator path is the static prompt
(`ail merge-prose | client | ail parse | ail check`), which works
with any client.
+16 -10
View File
@@ -5,7 +5,8 @@
AILang ships two allocator backends with an asymmetric role:
- **RC is canonical.** `--alloc=rc` is the CLI default for
`ail build` and `ail run`. The runtime AILang's memory model
`ail build` and `ail run` (see [pipeline](pipeline.md)). The
runtime AILang's [memory model](../contracts/memory-model.md)
(RC + uniqueness inference) is designed for. New examples,
benches, and corpus tests run under RC unless they explicitly
pin GC.
@@ -25,8 +26,11 @@ few iter families ship without the gc arm catching anything that
the rc arm did not already catch. Until then, the cost of
keeping libgc as a build dependency is accepted in exchange for
diagnostic leverage. Decision 10 (RC + uniqueness) holds as the
specification of the canonical runtime; the rest of this section
documents the Boehm half, retained as the oracle.
specification of the canonical runtime (the contract version is
[memory model](../contracts/memory-model.md), built on
[language constraints](../contracts/language-constraints.md));
the rest of this section documents the Boehm half, retained as
the oracle.
**Choice: Boehm-Demers-Weiser conservative GC.** The simplest
working option:
@@ -143,10 +147,10 @@ costs are bounded and analysable per program point. A corpus
committed to one memory model is expensive to switch — pre-
stdlib is the cheapest moment to commit.
**Choice.** AILang's canonical memory model is reference counting
with static uniqueness inference **and explicit LLM-author
annotations on fn signatures**, in the lineage of Lean 4 / Roc /
Koka. Boehm becomes a transitional allocator (Decision 9) and is
**Choice.** AILang's canonical [memory model](../contracts/memory-model.md)
is reference counting with static uniqueness inference **and
explicit LLM-author annotations on fn signatures**, in the lineage
of Lean 4 / Roc / Koka. Boehm becomes a transitional allocator (Decision 9) and is
retired when the RC pipeline matches the bump-allocator floor
within an acceptable margin (target 1.3× on `bench/run.sh`).
@@ -225,7 +229,9 @@ can carry the cognitive cost trivially, and the compiler gains a
precise contract at every call site instead of a probabilistic
guess.
**(2) Linear-by-default consumption with explicit `(clone X)`.**
**(2) Linear-by-default consumption with explicit `(clone X)`**
(the `Term::Clone` schema entry lives in
[Data model](../contracts/data-model.md)).
In bodies, every binder is consumed by exactly one `own`-mode
use. If the LLM writes:
@@ -300,8 +306,8 @@ compiles, but what the compiler tells the author when it doesn't.
## Inference algorithm
Post-typecheck, post-`lift_letrecs`, pre-codegen pass over the
elaborated module. For each `Term` node that produces or binds a
Post-typecheck, post-`lift_letrecs` (see [pipeline](pipeline.md)),
pre-codegen pass over the elaborated module. For each `Term` node that produces or binds a
boxed value, the pass computes a uniqueness flag:
- **Unique:** at this program point, the reference is the only
+19 -8
View File
@@ -1,10 +1,19 @@
# Typeclasses — resolution and monomorphisation whitepaper
The schema-and-diagnostics contract for typeclasses lives in
[typeclasses](../contracts/typeclasses.md); the call-site lookup
rule in [method dispatch](../contracts/method-dispatch.md); the
built-in classes shipped in the prelude in
[prelude classes](../contracts/prelude-classes.md). This file
covers the design choices and the resolution / monomorphisation
algorithm.
## Decision 11: typeclasses — Haskell-lite, monomorphised, coherent
**The design pass for typeclasses. Codified after the Feature-acceptance
criterion (this document, above) was committed; the criterion is
the explicit basis for the choices below.**
**The design pass for typeclasses. Codified after the
[Feature-acceptance criterion](../contracts/feature-acceptance.md)
was committed; the criterion is the explicit basis for the choices
below.**
AILang ships typeclasses to compress a real LLM-author redundancy:
without them, every comparable function must be written per-type
@@ -46,17 +55,19 @@ types) is one an LLM author does not unprompted produce.
**Constraint collection (per function body).** During typechecking
of a body, each method call generates a residual constraint of shape
`<Class> <Type>` where `<Type>` may still contain type variables.
`<Class> <Type>` (the schema for these lives in
[Data model](../contracts/data-model.md)) where `<Type>` may still
contain type variables.
After local typechecking, residual constraints are checked against
the function's declared constraints (modulo α-conversion and modulo
auto-expansion through superclasses; see below). Any residual not
covered by declared constraints fires `MissingConstraint`.
**Instance registry (workspace-global).** At workspace load (see
`crates/ailang-core/src/workspace.rs`), all `InstanceDef` nodes
across all reachable modules are collected into a registry keyed by
`(class-name, canonical-hash-of-instance-type)`. Registry build
performs three checks:
[`crates/ailang-core/src/workspace.rs`](../../crates/ailang-core/src/workspace.rs)),
all `InstanceDef` nodes across all reachable modules are collected
into a registry keyed by `(class-name, canonical-hash-of-instance-type)`.
Registry build performs three checks:
- **Coherence.** Each instance's module must be either the class's
defining module or the instance type's defining module. Otherwise