design-md-consolidation 1.4: drop Iter-N tags from DESIGN.md
This commit is contained in:
+128
-135
@@ -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 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<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 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_<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 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_<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.
|
||||
|
||||
Reference in New Issue
Block a user