The first formal-links milestone shipped clause-5 + 8 links across the
existing file layout. Browsing surfaced that file-only granularity is
only as precise as the file boundaries — three files mixed two or
three navigation targets under one address, so the 8 links could not
multiply without ambiguity. This commit fixes the substrate, then
applies the sweep the original milestone deferred.
Splits (each extracts an already-self-contained section into its own
file so links land on the topic, not the parent doc's TOC):
contracts/typeclasses.md
→ +contracts/prelude-classes.md (Eq/Ord/Show ships, polymorphic `print`)
→ +contracts/method-dispatch.md (5-step dispatch rule, candidate index)
contracts/memory-model.md
→ +contracts/language-constraints.md (the 4 binding constraints
making RC sound without a
cycle collector)
models/authoring-surface.md
→ +models/prose-projection.md (Form-B / `ail prose` / merge-prose)
Each new file enters design/INDEX.md as its own row (three contracts
share show_no_instance_e2e.rs / uniqueness.rs as ratifying tests;
prose-projection is a model). Two pre-existing links rebind to the
new topic-files (memory-model.md → method-dispatch.md;
float-semantics.md → prelude-classes.md).
Link sweep: 8 → 88 formal Markdown links over 23 files. Every file
in design/contracts/ + design/models/ now has at least one outgoing
link; the tree is fully connected. Links are file-relative
`[label](path)` per the established convention, fenced code blocks
are skipped (a `](` inside ```jsonc``` is literal text), the durable
tier (design/ + crates/ + runtime/) is enforced by clause-5.
Tests:
- design_index_pin.rs (5/5 clauses): clean-cut, INDEX resolution,
ratifying-test resolution, no decision-record prose in contracts/,
body links durable + resolving.
- docs_honesty_pin.rs (5/5): one assertion rebinds from typeclasses.md
to prelude-classes.md (where the gated sentence now lives);
design_corpus widens to include the 4 new files so the Wunschdenken
/ doc-archaeology sweeps continue to cover everything that used to
live in the parents.
No spec/plan/journal for this batch — interactive collaboration after
the milestone closed; the user gated the splits explicitly before the
sweep.
4.3 KiB
Pipeline and CLI
Pipeline
.ail.json ─┐
├─ load + validate schema
├─ resolve names + assign hashes
├─ desugar (AST → AST)
├─ typecheck (HM, effect rows; mode-strict per Decision 10)
├─ lift_letrecs (post-typecheck AST → AST)
├─ lower to MIR (SSA-like, named SSA values)
├─ emit LLVM IR (.ll)
└─ clang -O2 *.ll -o binary
--alloc=rc → emits inc/dec (@ailang_rc_inc / _dec; canonical, default)
--alloc=gc → links libgc (@GC_malloc; parity oracle)
Two allocator backends share the same MIR. --alloc=rc is the
canonical backend committed to in the
memory model and the CLI default.
The typechecker enforces
(own) / (borrow) modes, codegen emits ailang_rc_inc / _dec
calls at the points dictated by linearity, and Term::Clone /
Term::ReuseAs materialise into actual rc-bumps and in-place
rewrites respectively. --alloc=gc selects the transitional Boehm
backend (see RC + uniqueness);
--alloc=rc is the canonical backend and the CLI default.
The desugar pass
(ailang-core::desugar::desugar_module)
runs before typecheck and codegen in every entry point of
ailang-check and ailang-codegen. It is a pure AST → AST rewriter — currently
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)
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
that the desugar pass left in place (the case where at least one
capture is Term::Let-bound, so its type is only knowable after
inference). The output is a module with synthetic <hint>$lr_N
top-level fns appended, ready for codegen. Synthetic FnDefs added
by this pass do not appear in CheckedModule.symbols — same
invariant as the desugar-pass lifts.
CLI
ail check <module.ail.json> — loads, validates, typechecks
ail manifest <module.ail.json> — table: name :: type !effects [hash]
ail describe <module> <name> — detail of a definition (form-A body)
ail render <module.ail.json> — JSON-AST → form-A text (exact inverse of `parse`)
ail parse <module.ail> — form-A text → canonical JSON-AST
ail prose <module.ail.json> — JSON-AST → form-B (lossy human prose, no parser)
ail merge-prose <m.ail.json> <m.prose.txt>
— compose the LLM-mediator prompt for the prose round-trip
ail deps <module.ail.json> — list cross-module references
ail diff <a.ail.json> <b.ail.json> — content-addressed def-level diff
ail workspace <entry.ail.json> — list all modules transitively reachable from entry
(`--json` for machine output;
`manifest --workspace` and `diff --workspace`
extend single-module subcommands to workspaces)
ail builtins — list built-in fns and effect ops
ail emit-ir <module> [--emit=staticlib] — writes .ll (staticlib: a main-free kernel's IR, no @main)
ail build <module> [--emit=staticlib] — full pipeline → binary (staticlib: lib<entry>.a + libailang_rt.a)
ail run <module> — build + execute (tempdir), passthrough exit code
The text projections the CLI moves between are documented in
authoring surface (Form-A,
round-trippable) and prose projection
(Form-B, lossy, no parser); ail build --emit=staticlib produces
the layout fixed in embedding ABI
plus frozen value layout.