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:
@@ -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 167–176) 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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user