From ba94d7240fc1c630c34e44ba61385f54beb61ac4 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 11:58:22 +0200 Subject: [PATCH] design-md-consolidation 1.4: drop Iter-N tags from DESIGN.md --- docs/DESIGN.md | 263 ++++++++++++++++++++++++------------------------- 1 file changed, 128 insertions(+), 135 deletions(-) diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 70bc8aa..559675f 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -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 ` (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 18a–18g) 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 } ; `(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 ; 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 18d–18g) +### 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 - 18d–18g 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__` — even in the single-module case. Constants likewise (`@ail__`). Global @@ -1805,7 +1801,7 @@ is a `define i32 @main()` trampoline (C / LLVM ABI) that calls `@ail__main()`. `source_filename` exists exactly once per workspace and carries the entry-module name (`.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: `.`. @@ -1815,8 +1811,8 @@ with exactly one dot in the name is a qualified reference: `.`. - `` 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": [""...], // names bound in body, in type.params order "body": Term, "doc": "", - "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": "" } -// type (algebraic data type; Iter 5; parameterised since Iter 13a) +// type (algebraic data type; parameterised) { "kind": "type", "name": "", - "vars": [""...], // type parameters; omitted when empty (pre-13a hash-stable) + "vars": [""...], // type parameters; omitted when empty (hash-stable when omitted) "ctors": [ { "name": "", "fields": [Type...] } // nullary ctor: fields = [] ... ], "doc": "", - "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": "", // e.g. "over-strict-mode" @@ -1895,14 +1891,14 @@ the schema cheap. { "t": "lit", "lit": Literal } { "t": "var", "name": "" } -// 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": "", "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 "/" (e.g. "io/print_int"). -// `tail` per Iter 14e (omitted when false). +// `tail` triggers musttail (omitted when false). { "t": "do", "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": [""...], "paramTypes": [Type...], @@ -1928,14 +1924,14 @@ the schema cheap. "effects": [""...], "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": "", "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": [""...], "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 — 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 14b–14c, - 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____` (e.g. `id__I` for `id` at `Int`, `apply__I_I` for `apply` at `(Int, Int)`). -- **Parameterised ADTs** (Iter 13). `TypeDef.vars: Vec` +- **Parameterised ADTs.** `TypeDef.vars: Vec` declares type parameters; `Type::Con.args: Vec` 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`: `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`. -- `examples/std_maybe_demo.ail.json` (Iter 15c) → exercises `std_maybe` +- `examples/std_maybe_demo.ail.json` → exercises `std_maybe` combinators over `Maybe`, 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.