Files
AILang/design/models/0003-pipeline.md
T
Brummel b151990028 audit: raw-buf close-fixes — honesty + ledger for the binder-rename (refs #43)
Architect drift review at raw-buf milestone close (after 55d76ae closed
#43) surfaced three items; this commit clears them. All three lockstep-
invariant pairs were confirmed intact and the effective-name keying was
confirmed consistent across the desugar→lift boundary (lift_letrecs reads
the desugared tree's effective names too).

1. [high] Honesty fix. `fresh_binder`'s doc-comment (and 55d76ae's body)
   claimed "authored names cannot contain `$` — the lexer reserves it".
   False: the lexer (ailang-surface) reserves only `.`, not `$`. The
   `$`-for-synthetic convention is not enforced. So collision-freedom
   rests solely on the `used` + in-scope-`scope` probe, which does not
   see an authored `<base>$<n>` that is out of scope at mint time but
   later binds under the same `(def, name)` key — the very collision
   class this fix closes. Latent (no fixture uses a `$` binder), but the
   stated rationale was wrong. Doc-comment now describes the probe
   honestly and names the gap + its two possible closures (enforce the
   reservation, or seed `used` with every authored binder name in the
   def). The enforce-or-retract decision is the next-direction follow-up.

2. [medium] design/models/0003-pipeline.md — "desugar currently only
   flattens nested constructor patterns" no longer matches the code
   (it now also alpha-renames shadowing binders). Corrected to current
   state (honesty-rule).

3. [low] design/contracts/0008-memory-model.md — the three drop gates
   all key on `(def_name, binder_name)` consume_count, silently relying
   on per-fn binder-name injectivity, which the ledger never stated.
   Added that invariant as an explicit precondition of the gates, with
   the desugar guarantee and its ratifying tests.

No code-logic change; doc-comment + ledger only.
2026-05-30 12:50:55 +02:00

4.6 KiB

Pipeline and CLI

Pipeline

.ail.json  ─┐
            ├─ load + validate schema
            ├─ resolve names + assign hashes
            ├─ desugar (AST → AST)
            ├─ typecheck (HM, effect rows; mode-strict per the memory model)
            ├─ 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=bump → links bump-floor (@bump_malloc; raw-alloc bench-floor)

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=bump selects the raw-alloc bench-floor (runtime/bump.c, no free, leak-only) and is used by bench/run.sh to measure RC overhead against the structurally cheapest allocator — it is not a production target.

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 — it flattens nested constructor patterns and alpha-renames any binder whose name shadows an enclosing binding to a fresh <name>$<n> (so the uniqueness side-table's (def_name, binder_name) key is injective per fn), and 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.