design-md-consolidation 1.4: drop Iter-N tags from DESIGN.md

This commit is contained in:
2026-05-10 11:58:22 +02:00
parent 41c834008f
commit ba94d7240f
+128 -135
View File
@@ -27,7 +27,7 @@ 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)
the LLM-facing renderings of a module. `ailang-surface`
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`
@@ -163,24 +163,24 @@ Rationale:
Trade-off: no inline optimisations through the LLVM API. We rely on
`clang -O2` as the standard pipeline.
## Decision 6: authoring surface (Iter 14b)
## Decision 6: authoring surface
Form (A) is implemented as
the `ailang-surface` crate (parser + printer) in Iter 14c. Form-A is
the `ailang-surface` crate (parser + printer). Form-A is
gated against drift by `ailang-surface/tests/round_trip.rs`, which
parses every `.ailx` fixture, prints it back, re-parses, and demands
canonical-byte equality. In Iter 15e, `ail render` and both branches
canonical-byte equality. `ail render` and both branches
of `ail describe` were rewired to use `ailang_surface::print`, making
form (A) the **sole** text projection of a module — the legacy
non-round-tripping pretty-printer code in `pretty.rs` was deleted in
the same iter, leaving only diagnostic helpers (`type_to_string`,
non-round-tripping pretty-printer code in `pretty.rs` was deleted at
the same time, leaving only diagnostic helpers (`type_to_string`,
`pattern_to_string`, `manifest`) public. The rest of this section
records the *why* of Decision 6 for the audit trail; the constraints
listed below describe the surface as shipped.
### Why this is opening up
Iters 1 through 14a authored everything as raw `*.ail.json`. That worked
Early development authored everything as raw `*.ail.json`. That worked
for 17 fixture files (each ≤ 60 LOC of JSON) but does not scale. Two
breaking signals:
@@ -225,9 +225,9 @@ one projection among potentially many. Concretely:
layer their own projections on top of the same AST without
touching the surface or the core.
In code terms: `ailang-core` owns the AST. `ailang-surface` (new in
Iter 14c) is one producer/consumer pair: text-form-A → AST → text-
form-A. A hypothetical `ailang-visual` would be a different producer
In code terms: `ailang-core` owns the AST. `ailang-surface` is one
producer/consumer pair: text-form-A → AST → text-form-A. A hypothetical
`ailang-visual` would be a different producer
of the same AST. `ailang-check` and `ailang-codegen` consume only
the AST and remain projection-agnostic.
@@ -416,7 +416,7 @@ authoring (measurable: I make more wrong-paren errors than the
JSON-AST shape produced before), roll back and try (C). (B) stays
on the shelf for a future iter only if both fail.
### Implementation outline (Iter 14c onwards, not done in 14b)
### Implementation outline
- `crates/ailang-surface` — new crate. **Strictly additive.** PEG
parser produces existing `ailang-core::ast` types. No new AST
@@ -434,7 +434,7 @@ on the shelf for a future iter only if both fail.
existing `ail render`. **`.ail.json` remains a first-class input**
to every existing subcommand; the parser is a producer, not a
gatekeeper.
- Iter 14d: stdlib (`std_list`, `std_maybe`, ...) authored in
- Stdlib (`std_list`, `std_maybe`, ...) authored in
form (A) from day one **because that is the AI authoring
projection**, not because JSON authoring is forbidden. The
resulting `.ail.json` is what tests and downstream tools see.
@@ -451,7 +451,7 @@ on the shelf for a future iter only if both fail.
`build`, `run`, `manifest`, `deps`, `diff`, `workspace`,
`builtins`) keeps its current `.ail.json` interface.
### Form refinements during Iter 14c implementation
### Form refinements during implementation
Two productions in the original 14b sketch had to be widened
during implementation to round-trip the existing AST faithfully.
@@ -489,7 +489,7 @@ fixtures round-trip identically through `print → parse → canonical
JSON`; the three hand-written `.ailx` exhibits parse to canonical
JSON identical to their corresponding `.ail.json` files.
3. **Tail-call surface (Iter 14e).** Decision 8 ships two new
3. **Tail-call surface.** Decision 8 ships two new
productions, both positional analogues of their non-tail
counterparts. Their result terms set `Term::App.tail = true` /
`Term::Do.tail = true`; the typechecker's `verify_tail_positions`
@@ -500,7 +500,7 @@ JSON identical to their corresponding `.ail.json` files.
tail-do-term ::= "(" "tail-do" ident term* ")"
```
Production count after Iter 14e: ~30, still inside the 30-rule
Production count: ~30, still inside the 30-rule
constraint-1 budget. No new lexical rule (`tail-app` / `tail-do`
are bare ident tokens; no special casing).
@@ -562,10 +562,10 @@ projection) and `ail merge-prose <m.ail.json> <edited.prose.txt>`
(the mediator-prompt composer); both are listed in the CLI section
below.
**Iter 20f update — Form-A spec embedding.** The 20d `merge-prose`
**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. 20f revises this:
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
@@ -603,7 +603,7 @@ worse for token economy and worse for the natural shape of the language.
`Term::Match` on `Bool`. Per CLAUDE.md the language must contain no
redundancies; two AST nodes for the same operation produces an
authoring decision with no semantic content and an extra codegen
path. Iter 14d removes `Term::If`. Migration shape on the JSON side:
path. `Term::If` was removed. Migration shape on the JSON side:
{"t":"if","cond":C,"then":A,"else":B}
@@ -682,8 +682,7 @@ without a source-level rewrite.
## Decision 9: dual allocator — RC canonical, Boehm parity oracle
Originally framed
as "transitional Boehm until the RC pipeline
(Iters 18a18g) lands and the default flips"; then re-framed
as "transitional Boehm until the RC pipeline lands and the default flips"; then re-framed
as a symmetric dual-allocator policy with Boehm as
the CLI default. The revision flips the asymmetry to
match Decision 10:
@@ -715,14 +714,13 @@ documents the Boehm half, retained as the oracle.
The `--alloc=bump` mode introduced for the bench is a
measurement tool, not a production target.
Through Iter 14e, every ADT box, lambda env, and closure pair was
Originally, every ADT box, lambda env, and closure pair was
allocated with bare `malloc` and never freed. That worked for the
17 test fixtures (all small, all short-lived) but is incompatible
with any real workload — a stdlib `fold` over a million-element
list would leak a million boxes. The "Goal" section's "no GC for
the MVP" framing predates the parameterised-ADT pipeline (Iter 13)
and the explicit-recursion expectation (Iter 14e); both make a
collector necessary.
the MVP" framing predates the parameterised-ADT pipeline and the
explicit-recursion expectation; both make a collector necessary.
**Choice: Boehm-Demers-Weiser conservative GC.** The simplest
working option:
@@ -769,9 +767,9 @@ escape analysis, the corresponding AST/IR plumbing, and is its
own design pass. Boehm-everything is the floor; arena is an
optimisation above it.
### Per-fn arena via stack `alloca` (Iter 17a)
### Per-fn arena via stack `alloca`
Iter 17a layers exactly that optimisation, in its simplest form.
This optimisation is layered on top of Boehm in its simplest form.
`ailang-codegen` runs an escape-analysis pre-pass over every fn
body (and every lifted lambda thunk body); allocations the pass
proves do not outlive the fn frame are lowered to LLVM `alloca`
@@ -858,7 +856,7 @@ within an acceptable margin (target 1.3× on `bench/run.sh`).
calibrated on the original `bench/run.sh` corpus: linear list
sum (`bench_list_sum`) and tree walk (`bench_tree_walk`) — uniform
single-allocation-per-step workloads where one inc/dec pair
amortises against one allocation. Iter 21'b extended the corpus
amortises against one allocation. The corpus was later extended
with `bench_closure_chain` (closure-pair allocation: each step
allocates *two* heap objects, the closure cell and its captured
env struct) and `bench_hof_pipeline` (poly-ADT + indirect
@@ -1070,7 +1068,7 @@ extension (e.g. linear ownership) or be rejected at design time.
### Schema additions
**Iter 18a — parameter modes on `Type::Fn`.**
**Parameter modes on `Type::Fn`.**
The form-A surface for fn signatures gains mode wrappers:
@@ -1124,19 +1122,18 @@ The substantive reasons for per-position metadata over a
ordering that don't exist when modes live in a flat metadata
vector.
`Implicit` is the legacy (pre-18a) state — semantically
`Implicit` is the legacy / back-compat state — semantically
equivalent to `Own` but printed bare (`(con T)`, no wrapper).
`Own` and `Borrow` are explicitly annotated.
JSON canonical hash for every pre-18a fixture stays bit-
JSON canonical hash for every existing fixture stays bit-
identical: `param_modes` is skipped when every entry is
`Implicit`, `ret_mode` is skipped when `Implicit`. Existing
modules emit the same bytes as before.
The legacy `(con T)` form is treated as `(own T)` semantically
throughout the 18-series; a later iter (deferred) makes the
explicit annotation mandatory and rejects `Implicit` for boxed
parameter types.
The legacy `(con T)` form is treated as `(own T)` semantically;
a future iteration (deferred) makes the explicit annotation mandatory
and rejects `Implicit` for boxed parameter types.
(An incidental observation, not a design reason: keeping `Type`
itself unchanged also avoids touching ~250 sites across the
@@ -1144,7 +1141,7 @@ typechecker / desugar / codegen that match on `Type` variants.
This is a tiebreaker, not a rationale — the substantive reasons
above are what justify the choice.)
**Iter 18c/18d — new `Term` variants.**
**New `Term` variants.**
```
Term::Clone { value: Box<Term> } ; `(clone X)` — explicit RC inc
@@ -1179,7 +1176,7 @@ Decision 7 made for `Term::If`'s relationship to nested
`Term::Match`: prefer composability over schema-level
rejection where the typecheck rule is unambiguous.
**Iter 18e — `TypeDef` attribute.**
**`TypeDef` attribute.**
```
TypeDef.drop_iterative: bool ; `(drop-iterative)`
@@ -1189,7 +1186,7 @@ All four are skipped during serialisation when absent / false /
None so canonical-JSON hashes of every fixture remain stable
until the fixture intentionally adopts the feature.
**Iter 19b — `FnDef.suppress`.**
**`FnDef.suppress`.**
```rust
FnDef.suppress: Vec<Suppress> ; advisory diagnostic suppress list
@@ -1211,16 +1208,16 @@ bit-identical canonical-JSON hashes (regression-pinned by
`iter19b_empty_suppress_preserves_pre_19b_hashes` and
`iter19b_schema_extension_preserves_pre_19b_hashes`).
### Advisory diagnostics — Iter 19a-arc
### Advisory diagnostics
The 19a-arc (19a / 19a.1 / 19b) introduces the language's first
The advisory-diagnostics arc introduces the language's first
**advisory** typechecker diagnostic and the suppression mechanism
that goes with it. Decision 10's mandatory-annotation rule is
unchanged: `param_modes` and `ret_mode` remain author-required;
the typechecker does not infer them. What's new is feedback when
an authored annotation is *stricter than necessary*.
**The lint: `over-strict-mode` (Iter 19a + 19a.1).** Fires on a
**The lint: `over-strict-mode`.** Fires on a
fn-param `p` annotated `(own T)` when:
1. `p`'s `consume_count == 0` (uniqueness pass: the body
@@ -1237,11 +1234,11 @@ as over-strict (could be `borrow`) while staying silent on
`sum_list` where `t: List` *is* moved out.
Severity: `Warning`. The first ever Warning-level diagnostic;
prior to 19a all diagnostics were `Error`. CLI exit semantics
previously all diagnostics were `Error`. CLI exit semantics
adjusted: `ail check`, `ail build`, `ail emit-ir` exit 1 only on
at least one Error. Warnings print but do not abort.
**The suppression: `mode-strict-because` (Iter 19b).** Authors
**The suppression: `mode-strict-because`.** Authors
who want to keep an over-strict annotation deliberately (e.g.
RC codegen-test fixtures, fns reserved for planned in-place
mutation) attach a `Suppress` entry naming the diagnostic code
@@ -1278,7 +1275,7 @@ is required (not optional), and it becomes part of the contract
the next reader sees. CLAUDE.md's "preserve correctness across
development cycles" is what this directly serves.
### Inference algorithm (Iter 18c)
### Inference algorithm
Post-typecheck, post-`lift_letrecs`, pre-codegen pass over the
elaborated module. For each `Term` node that produces or binds a
@@ -1291,13 +1288,12 @@ boxed value, the pass computes a uniqueness flag:
A reference is *unique* if every path from its allocation to the
current program point passes through exactly one binding. The
inference is a forward dataflow over the AST. The annotations
from 18a (`borrow` / `own`) provide the inter-fn contract; the
inference fills in intra-fn detail. (Implementer-level detail in
the Iter 18c brief.)
(`borrow` / `own`) provide the inter-fn contract; the
inference fills in intra-fn detail.
### Codegen contract (Iter 18b, 18c)
### Codegen contract
Memory layout (Iter 18b):
Memory layout:
- Every heap allocation has an 8-byte refcount header, followed
by the payload. `ailang_rc_alloc(size)` returns a pointer to
@@ -1306,19 +1302,19 @@ Memory layout (Iter 18b):
(single-threaded).
- `ailang_rc_dec(ptr)`: load, -1, store; if zero, recurse-dec
child references and `free(ptr - 8)`. For `(drop-iterative)`
types, the recursion is replaced by a worklist loop (Iter 18e).
types, the recursion is replaced by a worklist loop (via `drop-iterative`).
Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under
`--alloc=rc` calls `ailang_rc_alloc(SIZE)`. Iter 18b stops there
— inc/dec instrumentation is added in Iter 18c, once the
`--alloc=rc` calls `ailang_rc_alloc(SIZE)`. The initial RC plumbing
stops there — inc/dec instrumentation is added once the
inference is wired up. Until then, `--alloc=rc` deliberately
leaks like the pre-Boehm era; this is purely about plumbing.
### Mode metadata is load-bearing for codegen (Iter 18d18g)
### Mode metadata is load-bearing for codegen
`param_modes` and `ret_mode` on `Type::Fn` are not merely
typechecker metadata — codegen consults both to decide where to
emit drop calls. The 18d.4 / 18g shipping work moved them from
emit drop calls. They were promoted from
"annotation that the typechecker enforces" to "annotation that
codegen reads to keep RC correct". Recorded here so the schema
metadata's role is explicit:
@@ -1343,7 +1339,7 @@ metadata's role is explicit:
the dec — `Borrow` and `Implicit` scrutinees would let the
arm dec memory the caller still references.
- **Iter 18g.1: pre-tail-call shallow-dec.** When a match-arm's
- **Pre-tail-call shallow-dec.** When a match-arm's
body IS a tail call, both Iter A and Iter B are skipped (the
block is terminated). A separate seam in `lower_match` emits
a shallow `ailang_rc_dec` on the scrutinee outer cell BEFORE
@@ -1353,7 +1349,7 @@ metadata's role is explicit:
**`ret_mode` — let-binder trackability.**
- **Iter 18g.2: `Term::App` drop at let-scope close.** A
- **`Term::App` drop at let-scope close.** A
let-binder whose value is `Term::App { callee, .. }` is
trackable for scope-close drop iff the callee's
`ret_mode == Own`. The signal is the callee's static
@@ -1374,8 +1370,8 @@ monomorphised copies resolve to concrete drop fns).
**What this widening does NOT do.**
- Does not change the canonical hash. `param_modes` /
`ret_mode` were already hash-load-bearing as of Iter 18a; the
18d18g iters add codegen consumers, not new schema fields.
`ret_mode` were already hash-load-bearing when introduced;
subsequent work added codegen consumers, not new schema fields.
- Does not introduce a new `Type` variant. Mode metadata stays
flat on `Type::Fn` (see "Schema additions" above on why).
- Does not cover let-aliases of borrowed values. A let-binder
@@ -1388,23 +1384,23 @@ monomorphised copies resolve to concrete drop fns).
### Migration plan
1. **Iter 18a:** `(borrow T)` / `(own T)` annotations as a
1. **`(borrow T)` / `(own T)` annotations** as a
language feature. Schema, parser, JSON, typechecker. No
codegen change. `(con T)` ≡ `(own T)`. Existing fixtures
unchanged.
2. **Iter 18b:** RC runtime. `runtime/rc.c` with header layout +
2. **RC runtime.** `runtime/rc.c` with header layout +
alloc/inc/dec. Codegen `--alloc=rc` routes allocation through
`rc_alloc`; **no inc/dec yet**. `--alloc=gc` remains default.
3. **Iter 18c:** uniqueness inference + codegen inc/dec
instrumentation. Combines 18a's annotations with intra-fn
3. **Uniqueness inference + codegen inc/dec
instrumentation.** Combines mode annotations with intra-fn
dataflow. Linear-by-default enforcement turns on. `(clone X)`
in the `Term` schema.
4. **Iter 18d:** reuse hints + reuse analysis. `(reuse-as ...)`
4. **Reuse hints + reuse analysis.** `(reuse-as ...)`
form. Codegen rewrites dec+malloc into in-place overwrite when
the precondition holds.
5. **Iter 18e:** `(drop-iterative)` data attr. Worklist-based
5. **`(drop-iterative)` data attr.** Worklist-based
free for annotated types.
6. **Iter 18f:** RC validation bench. RC validated within target
6. **RC validation bench.** RC validated within target
on `bench/run.sh` (live=0; tail latency 23× better than Boehm;
RSS lower). Retirement of Boehm executes in two steps: first a
default-flip (`--alloc=rc` is the CLI default,
@@ -1412,8 +1408,8 @@ monomorphised copies resolve to concrete drop fns).
full removal once the oracle stops paying its keep (gating
condition: a few iter families with no GC-only diagnostic
wins).
7. **Iter 19a / 19a.1 / 19b:** advisory `over-strict-mode` lint
+ precise sub-binder analysis with heap-type filter +
7. **Advisory `over-strict-mode` lint** + precise sub-binder
analysis with heap-type filter +
`FnDef.suppress` suppression mechanism with mandatory-reason.
First `Severity::Warning` diagnostic; CLI exit gated on Error
only. Decision 10's mandatory-annotation rule is unchanged.
@@ -1438,7 +1434,7 @@ materialises with a real workload.
author can produce them effortlessly. The compiler does
inference *plus* verification of contracts.
- **Does not require existing fixtures to migrate immediately.**
Iter 18a treats `(con T)` as `(own T)`. Existing JSON hashes
`(con T)` is treated as `(own T)`. Existing JSON hashes
stay bit-identical until the fixture is intentionally updated.
- **Does not commit to atomic refcounts.** AILang is currently
single-threaded. If/when concurrency arrives, atomic-vs-non-
@@ -1795,7 +1791,7 @@ would invoke literal-defaulting which axis-7 already excluded.
Prelude). A future Prelude milestone gates class additions on
the Feature-acceptance criterion at the time of proposal.
## Mangling scheme (Iter 5c)
## Mangling scheme
All AILang functions are mangled to `@ail_<module>_<def>` — even in the
single-module case. Constants likewise (`@ail_<module>_<const>`). Global
@@ -1805,7 +1801,7 @@ is a `define i32 @main()` trampoline (C / LLVM ABI) that calls
`@ail_<entry-module>_main()`. `source_filename` exists exactly once per
workspace and carries the entry-module name (`<entry-module>.ail`).
## Convention: qualified cross-module references (Iter 5b)
## Convention: qualified cross-module references
Cross-module calls use **no** new AST node. Instead, a `Term::Var { name }`
with exactly one dot in the name is a qualified reference: `<prefix>.<def>`.
@@ -1815,8 +1811,8 @@ with exactly one dot in the name is a qualified reference: `<prefix>.<def>`.
- `<def>` is the name of a top-level definition in the target module.
- Def names MUST NOT contain a dot — the typechecker reports
`invalid-def-name` with `ctx: { "reason": "contains-dot" }`.
- The workspace loader (Iter 5a) finds all reachable modules; the
typechecker (Iter 5b, `check_workspace`) resolves dotted names through the
- The workspace loader finds all reachable modules; the
typechecker (`check_workspace`) resolves dotted names through the
import map. Diagnostic codes: `unknown-module` (prefix not imported),
`unknown-import` (module found, def not).
@@ -1856,7 +1852,7 @@ the schema cheap.
"params": ["<id>"...], // names bound in body, in type.params order
"body": Term,
"doc": "<optional string>",
"suppress": [Suppress...] // Iter 19b, omitted when empty
"suppress": [Suppress...] // omitted when empty
}
// const (top-level value; codegen emits as a global; body must be pure)
@@ -1867,20 +1863,20 @@ the schema cheap.
"doc": "<optional string>"
}
// type (algebraic data type; Iter 5; parameterised since Iter 13a)
// type (algebraic data type; parameterised)
{ "kind": "type",
"name": "<id>",
"vars": ["<id>"...], // type parameters; omitted when empty (pre-13a hash-stable)
"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 // Iter 18e opt-in; omitted when false (pre-18e hash-stable)
"drop-iterative": true // opt-in; omitted when false (hash-stable when omitted)
}
```
**`Suppress`** (Iter 19b — entry in `FnDef.suppress`):
**`Suppress`** (entry in `FnDef.suppress`):
```jsonc
{ "code": "<diagnostic-code>", // e.g. "over-strict-mode"
@@ -1895,14 +1891,14 @@ the schema cheap.
{ "t": "lit", "lit": Literal }
{ "t": "var", "name": "<id>" }
// fn application; tail flag is Iter 14e (musttail under codegen).
// `tail` is omitted when false (pre-14e hash-stable).
// 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 (Iter 16b.1). Always fn-shaped. The desugar pass
// lifts most `letrec` to a synthetic top-level fn; `lift_letrecs` (16b.3)
// 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",
@@ -1912,7 +1908,7 @@ the schema cheap.
{ "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).
// `tail` triggers musttail (omitted when false).
{ "t": "do", "op": "<eff>/<op>", "args": [Term...], "tail": false }
// Ctor application; `args` omitted when empty.
@@ -1920,7 +1916,7 @@ the schema cheap.
{ "t": "match", "scrutinee": Term, "arms": [Arm...] }
// Anonymous fn value (Iter 8b); free vars captured from enclosing scope.
// Anonymous fn value; free vars captured from enclosing scope.
{ "t": "lam",
"params": ["<id>"...],
"paramTypes": [Type...],
@@ -1928,14 +1924,14 @@ the schema cheap.
"effects": ["<id>"...],
"body": Term }
// Sequencing (Iter 10). Semantically `let _ = lhs in rhs`; lhs must be Unit.
// Sequencing. Semantically `let _ = lhs in rhs`; lhs must be Unit.
{ "t": "seq", "lhs": Term, "rhs": Term }
// Iter 18c.1: explicit RC clone. Codegen lowers as
// 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
// 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 }
@@ -1944,7 +1940,7 @@ the schema cheap.
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).
closure conversion in JOURNAL).
**`Literal`**:
@@ -1970,14 +1966,14 @@ Patterns are linear: each pattern variable may appear at most once.
```jsonc
// Type-constructor application. `args` omitted when empty
// (pre-13a hash-stable for non-parameterised cases like Int, Bool, ...).
// (hash-stable when omitted, for non-parameterised cases like Int, Bool, ...).
{ "k": "con", "name": "<id>", "args": [Type...] }
// Function type. Decision 10 (Iter 18a) added paramModes/retMode as
// 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" (pre-18a hash-stable).
// `retMode` omitted when "implicit" (hash-stable when omitted).
{ "k": "fn",
"params": [Type...],
"paramModes": [ParamMode...],
@@ -1991,16 +1987,16 @@ Patterns are linear: each pattern variable may appear at most once.
{ "k": "forall", "vars": ["<id>"...], "body": Type }
```
**`ParamMode`** (Iter 18a / Decision 10):
**`ParamMode`** (Decision 10):
```
"implicit" — pre-18a / unannotated. Treated as `own` by the typechecker.
"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 pre-18a
fixtures continue to serialize without the mode wrapper and keep their
`implicit ≡ own` semantically; the distinction exists so existing
unannotated fixtures continue to serialize without the mode wrapper and keep their
canonical-JSON hash.
## Pipeline
@@ -2009,9 +2005,9 @@ canonical-JSON hash.
.ail.json ─┐
├─ load + validate schema
├─ resolve names + assign hashes
├─ desugar (AST → AST, Iter 16a)
├─ desugar (AST → AST)
├─ typecheck (HM, effect rows; mode-strict per Decision 10)
├─ lift_letrecs (post-typecheck AST → AST, Iter 16b.3)
├─ lift_letrecs (post-typecheck AST → AST)
├─ lower to MIR (SSA-like, named SSA values)
├─ emit LLVM IR (.ll)
└─ clang -O2 *.ll -o binary
@@ -2031,14 +2027,14 @@ 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
only flattens nested constructor patterns (16a), but is the chosen
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`
in the `check` entry point continues to hash from the *original*
on-disk module, not the desugared one, so `ail diff` and `ail manifest`
report identities that match the canonical JSON the user is editing.
The **lift_letrecs** pass (`ailang-check::lift_letrecs`, Iter 16b.3)
The **lift_letrecs** pass (`ailang-check::lift_letrecs`)
runs **after** typecheck and **before** codegen, but only on the
`build` / `run` paths — the `check` subcommand stops at typecheck
and never sees a lifted module. It eliminates every `Term::LetRec`
@@ -2083,25 +2079,24 @@ ail run <module> — build + execute (tempdir), passthro
hash.
5. **CI pin** of the outputs in `tests/expected/`.
6. **Rustdoc cleanliness**: `cargo doc --no-deps` runs warning-free.
Maintained by the `ailang-docwriter` agent (Iter 13d onward); fixing a
rustdoc warning is part of the iter that introduced it, not a follow-up.
Maintained by the `ailang-docwriter` agent; fixing a
rustdoc warning is part of the iteration that introduced it, not a follow-up.
## What is not (yet) supported
Snapshot of the boundary as of Iter 16a. Items move out of this list
as iterations land; the JOURNAL records the exact iteration. Recently
**lifted** gates that used to live here: cross-module ADTs (lifted in
Iter 14h via qualified `module.Type` / `module.Ctor` references in
Snapshot of the current boundary. Items move out of this list
as iterations land; the JOURNAL records when. Recently
**lifted** gates that used to live here: cross-module ADTs (via qualified `module.Type` / `module.Ctor` references in
both `(con ...)` and `(term-ctor ...)` / `(pat-ctor ...)` positions);
GC for ADT boxes, lambda envs, and closure pairs (Boehm conservative
collector wired up in Iter 14f, see Decision 9); nested constructor
sub-patterns inside `match` (lifted in Iter 16a via the desugar pass);
literal sub-patterns inside a Ctor pattern (lifted in Iter 16c — the
collector, see Decision 9); nested constructor
sub-patterns inside `match` (via the desugar pass);
literal sub-patterns inside a Ctor pattern (the
desugar pass rewrites every `Pattern::Lit` to a `Term::If` on `==`,
both at the top level of an arm and inside a Ctor sub-pattern);
`==` extended from Int-only to a polymorphic
`forall a. (a, a) -> Bool` over `Int`/`Bool`/`Str`/`Unit` (lifted
in Iter 16e — codegen monomorphises and dispatches on the
`forall a. (a, a) -> Bool` over `Int`/`Bool`/`Str`/`Unit` (codegen
monomorphises and dispatches on the
resolved arg type; ADT/Fn arg types are rejected at codegen).
- No effect handlers — only the built-in IO and Diverge ops.
@@ -2131,9 +2126,9 @@ What **is** supported (and used as the smoke test for the pipeline):
type `(Int, Int) -> Int`; ordering operators and `!=` (`!=`,
`<`, `<=`, `>`, `>=`) of type `(Int, Int) -> Bool`; logical
`not : (Bool) -> Bool`; the IO effect ops listed above;
**`==` : forall a. (a, a) -> Bool** (Iter 16e); and
**`__unreachable__ : forall a. a`** (Iter 16d).
- **`==` is polymorphic** (Iter 16e). The typechecker accepts
**`==` : forall a. (a, a) -> Bool**; and
**`__unreachable__ : forall a. a`**.
- **`==` is polymorphic.** The typechecker accepts
`==` at any type whose two sides agree (the rigid `a` of the
`Forall` is unified by HM at the use site). Codegen
monomorphises and dispatches on the resolved AIL arg type:
@@ -2160,35 +2155,33 @@ What **is** supported (and used as the smoke test for the pipeline):
impossible branches). Reference site is
`Term::Var { name = "__unreachable__" }` / form-A bare
`__unreachable__`.
- **ADTs + pattern matching** (Iter 3, extended in Iter 16a/16c).
Sub-patterns of a Ctor pattern may be `Var`, `Wild`, another `Ctor`
(Iter 16a), or a literal (Iter 16c). The desugar pass flattens
- **ADTs + pattern matching.** Sub-patterns of a Ctor pattern may be `Var`, `Wild`,
another `Ctor`, or a literal. The desugar pass flattens
nested Ctor patterns into a chain of let + match and rewrites every
`Pattern::Lit` (top-level or sub-) to a `Term::If` on `==` before
typecheck/codegen — see `ailang-core::desugar` and Pipeline above.
- Literal patterns at top level and inside Ctor sub-patterns (Iter 16c,
via desugar). `(pat-lit 0)` and `(pat-ctor Cons (pat-lit 0) _)` both
- Literal patterns at top level and inside Ctor sub-patterns (via desugar).
`(pat-lit 0)` and `(pat-ctor Cons (pat-lit 0) _)` both
parse and lower; the rewrite is to `Term::If { cond = (== sv lit) }`,
so any literal kind whose `==` is supported is authorable. After
Iter 16e (`==` polymorphic over `Int`/`Bool`/`Str`/`Unit`), that
so any literal kind whose `==` is supported is authorable. With `==`
polymorphic over `Int`/`Bool`/`Str`/`Unit`, that
covers every lit kind the AST ships — including `(pat-lit "hi")`
over a `Str` scrutinee, exercised by `examples/eq_demo.ail.json`.
- **Imports + qualified cross-module references** via dotted names
(Iter 5). Extends to **types and constructors** (Iter 14h): a foreign
- **Imports + qualified cross-module references** via dotted names.
Extends to **types and constructors**: a foreign
module's ADT is referenced as `(con std_pair.Pair a b)`, its ctors as
`(term-ctor std_pair.Pair MkPair x y)` and `(pat-ctor MkPair x y)`
inside that scrutinee. Std-library demos (`examples/std_*_demo.ail.json`)
exercise this end-to-end.
- **AI-authoring text surface, form (A)** (Decision 6 / Iter 14b14c,
exclusive in 15e). The `ailang-surface` crate parses `.ailx` form-A
- **AI-authoring text surface, form (A)** (Decision 6). The `ailang-surface` crate parses `.ailx` form-A
text into a canonical `ailang-core::ast::Module` and prints any module
back as form-A text. `ail render` and `ail describe` use it as the
sole text projection; `ail parse` is the inverse direction. Round-trip
identity (text → AST → JSON → AST → text) is gated by
`ailang-surface/tests/round_trip.rs` over every shipped fixture.
- **Memory management via Boehm conservative GC** (Decision 9 / Iter 14f),
- **Memory management via Boehm conservative GC** (Decision 9),
with **per-fn arena via stack `alloca` for non-escaping allocations**
layered on top (Iter 17a). Every ADT box, lambda env, and closure
layered on top. Every ADT box, lambda env, and closure
pair allocates either via `@GC_malloc` (escaping; Boehm-managed) or
via LLVM `alloca` (non-escaping; freed at fn return). The decision
is made by an escape-analysis pre-pass over the fn body — see
@@ -2197,26 +2190,26 @@ What **is** supported (and used as the smoke test for the pipeline):
and `examples/std_list_stress.ail.json` still allocate via
`@GC_malloc` because their boxes flow into other fns and escape.
The per-fn-arena path is exercised end-to-end by
`examples/escape_local_demo.ail.json` (Iter 17a fixture).
- **First-class function references** (Iter 7). A top-level fn name (or
`examples/escape_local_demo.ail.json`.
- **First-class function references.** A top-level fn name (or
qualified `prefix.def`) used as a `Term::Var` is a fn-value.
- **Anonymous lambdas with capture** (Iter 8). `Term::Lam` constructs a
- **Anonymous lambdas with capture.** `Term::Lam` constructs a
closure that captures any free variables of its body from the
enclosing scope. All fn-values share a single ABI: a `ptr` to a
closure pair `{ thunk_ptr, env_ptr }`. Top-level fns get an auto-
generated adapter and a static closure pair (env = null) so they
remain passable as values without heap overhead.
- **Polymorphism via `Type::Forall`** at top-level def types (Iter 12).
- **Polymorphism via `Type::Forall`** at top-level def types.
Use sites instantiate fresh metavars; unification pins them against
the concrete types of the call args. Codegen monomorphises on
demand: each unique instantiation emits a specialised LLVM fn
mangled `@ail_<m>_<def>__<descriptor>` (e.g. `id__I` for `id` at
`Int`, `apply__I_I` for `apply` at `(Int, Int)`).
- **Parameterised ADTs** (Iter 13). `TypeDef.vars: Vec<String>`
- **Parameterised ADTs.** `TypeDef.vars: Vec<String>`
declares type parameters; `Type::Con.args: Vec<Type>` carries the
type arguments at use sites. Both fields default to empty and are
skipped during serialization, so canonical-JSON hashes of every
pre-13a definition stay bit-identical (regression test in
existing definition stay bit-identical (regression test in
`crates/ailang-core/src/hash.rs`). Ctor and match codegen stay
inline at every use site — there is no specialised ADT symbol —
but LLVM field types are derived per use site by substituting
@@ -2245,18 +2238,18 @@ Pipeline regression smoke tests:
- `examples/maybe_int.ail.json` → prints 7 then 99 (pattern match
over `Maybe<Int>`: `or_else(Some(7), 99)` then
`or_else(None, 99)`).
- `examples/std_list_demo.ail.json` (Iter 15a/15b) → exercises
- `examples/std_list_demo.ail.json` → exercises
`std_list`'s combinators (length, sum, reverse, take/drop-style
uses) end-to-end against `std_list`'s `List<a>`.
- `examples/std_maybe_demo.ail.json` (Iter 15c) → exercises `std_maybe`
- `examples/std_maybe_demo.ail.json` → exercises `std_maybe`
combinators over `Maybe<Int>`, including `from_maybe` and `map`.
- `examples/std_either_demo.ail.json` (Iter 15d) → first program with
- `examples/std_either_demo.ail.json` → first program with
three distinct type variables in a single fn (the `either`
eliminator), monomorphised six different ways in the IR.
- `examples/std_pair_demo.ail.json` (Iter 15f) → drives every
- `examples/std_pair_demo.ail.json` → drives every
`std_pair` combinator (fst, snd, swap, map_first, map_second);
expected output 7, 9, 9, 7, 8, 18.
- `examples/nested_pat.ail.json` (Iter 16a) → first program to use a
- `examples/nested_pat.ail.json` → first program to use a
nested `(pat-ctor Cons a (pat-ctor Cons b _))`; the desugar pass
flattens it into a chain that the existing flat-match codegen
consumes. Prints 30 for a 3-element input list.