Files
AILang/docs/roadmap.md
T
Brummel 54f0ced148 workflow: delete docs/journals/ and docs/journal-archive.md
Strict application of the "Future-Use, not Verlauf" criterion to the
two remaining journal artefacts:

- `docs/journals/` (110 files): the per-iter and audit journals from
  2026-05-11 onward. Pure history; no live reader after the previous
  sweep. Entire directory removed.
- `docs/journals/2026-05-19-design-decision-records.md`: the one file
  that had live readers (`docs_honesty_pin.rs:108`, `parse.rs:80`,
  `duplicate_ctor_pin.rs:8`, three roadmap.md mentions) and was framed
  as a "relitigation guard". On re-examination its three asserted
  pinned phrases ("Regions were considered and rejected", the
  "demands annotations *because*" rationale, the prose-render
  placeholder statement) are rationale-prose, not load-bearing
  invariants — the test pinned itself, not a system property. Any
  Decision that still holds lives in the code, `design/contracts/`,
  or `design/models/`. Removed with the rest.
- `docs/journal-archive.md` (pre-2026-05-11 history): content-frozen
  long-tail history with no live reader. Removed; if anyone ever
  needs the pre-cutoff rationale they can `git log --before=2026-05-11
  --grep=<keyword>`.

Live-file consequences:
- `docs_honesty_pin.rs` `design_md_present_tense_anchors_present`:
  the three `records.*` assertions and the `read("docs/journals/…")`
  are dropped; the contract/model anchor pins remain.
- `duplicate_ctor_pin.rs`: the "why-two-overlays rationale lives in
  docs/journals/…" comment is replaced with the rationale inline.
- `parse.rs`: the "form-refinement rationale lives in docs/journals/…"
  comment is dropped (the rule body already states the refinement).
- `roadmap.md`: the decision-records mention in the DESIGN.md →
  design/ entry's body is dropped; the journal-archive.md `context:`
  pointers across ~12 closed entries are either rephrased to point
  at the relevant iter/audit (no path), or simplified to a one-line
  comment when the iter name alone carries the story.
- `CLAUDE.md` Roles section: the `docs/journal-archive.md` slot is
  removed; vocabulary note rephrased.
- `design/INDEX.md`: the Docs bullet drops `journal-archive.md`.
- `skills/README.md` bootstrap-rationale pointer rephrased.

`docs/specs/` and `docs/plans/` (per-milestone specs and per-iteration
plans) are unaffected — they remain the structured-design artefacts
they were before this sweep.

Workspace builds, full test suite green.
2026-05-20 11:25:15 +02:00

1059 lines
61 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AILang Roadmap
Priority-ordered list of upcoming work — milestones, features, todos,
and ideas. The orchestrator maintains this file. The user can request
additions; the orchestrator chooses what to remove or reprioritise as
work progresses.
## Conventions
- One checkbox per entry. `- [ ]` is open; `- [~]` is in progress
(work has started — a plan exists or commits are landing); `- [x]`
is done. A finished entry may stay briefly for context, then is
removed; the durable record stays in `git log`.
- Each entry is tagged by **kind** and lives under a **priority**
bucket:
- **\[milestone\]** — big chunk that will get a `docs/specs/<milestone>.md`.
- **\[feature\]** — smaller addition inside a milestone, no full
spec.
- **\[todo\]** — concrete task that can run without a brainstorm
(cleanup, doc fix, mechanical refactor, test backfill).
- **\[idea\]** — not yet decision-ready, no commitment.
- Optional `depends on:` line names another entry that has to land
first.
- Optional `context:` line points to the rationale: a spec file
under `docs/specs/`, a commit hash (`git show <hash>`), or a
`git log --grep="<keyword>"` pointer for older entries. The
roadmap is intentionally terse; rationale stays in those sources.
- Priority buckets:
- **P0** — in flight. Spec or plan already exists.
- **P1** — next up. Decision made; not yet started.
- **P2** — medium-term. Decided in principle, scheduled later.
- **P3** — ideas. No commitment; may be cut.
## P0 — In flight
- [x] **\[milestone\]** Remove `mut` / `var` / `assign` — CLOSED
2026-05-18. The local-mutable-state construct removed entirely
and atomically (`Term::Mut`/`Term::Assign`/`MutVar`, the 3 Form-A
keywords, the 4 `mut` `CheckError` variants, `mut_scope_stack`,
every exhaustive `Term` arm across 17 src files) in lockstep with
DESIGN.md + fixtures + drift trio + carve-out + roadmap; `loop`/
`recur` + `let`/`if` are the surviving forms. Justified by the
feature-acceptance criterion applied inverted (clause 2 redundant,
clause 3 IS the iterated-mutable-state bug class). Approach A: no
deprecation window, JSON tags fail closed; shared codegen alloca
machinery kept (loop reuses it, renamed `binder_allocas`) + the
`Term::Lam` escape-guard loop half byte-equivalent. spec/plan/
iter (`d1ad50e`/`f355899`/`07f0802`), milestone-close `audit`
clean (architect `[high]` form_a.md fixed in `.tidy` `4837871`;
bench causally exonerated — HEAD vs pre-milestone bench binaries
`cmp`-byte-identical, no ratify), `fieldtest` clean (4 fresh
programs, all worked first try without mutable locals — the
removal thesis empirically confirmed). Decoupled from
Stateful-islands. Stays here briefly for context; remove once
stale (full record in `git log`).
- context: `docs/specs/2026-05-18-remove-mut-var-assign.md`;
`docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md`
- [x] **\[milestone\]** Prose `loop` binders — projection redesign —
CLOSED 2026-05-18. Form-B prose now renders loop binders as a
parenthesised init-list on the keyword (`loop(acc = 0, i = 1)
{ … }`, positionally isomorphic to `recur(...)`) instead of bare
`name = init;` statements inside the body block (which misread as
C/Rust re-init-every-iteration). Single-iteration, projection-only:
one `ailang-prose` `write_term` `Term::Loop` arm + 2 committed
byte-equality `.prose.txt` snapshots/tests; loop/recur AST, Form A,
JSON-AST, typecheck, codegen, and the Form-A↔JSON round-trip
invariant all byte-unchanged. brainstorm→plan→implement
(`c657e74`/`6533134`/`c9355d7`); milestone-close `audit` clean
(architect zero drift; bench carry-on — sole firing is the tracked
P2 `*.bump_s` environmental staleness, latency-tail proven sampling
noise by re-run); no fieldtest (projection-only, no authoring-
surface change). Stays here briefly for context; remove once stale.
- context: `docs/specs/2026-05-18-prose-loop-binders.md`; surfaced
from the loop/recur prose review (2026-05-18 chat).
- [x] **\[milestone\]** Standalone `loop` / `recur` — CLOSED 2026-05-18.
Strictly-additive strict-iteration surface, `recur` tail-position-only,
no totality claim. Shipped: iter 1 `a179ec3` (additive AST nodes),
iter 2 `1566ce0` (typecheck), iter 3 `edd2558` (codegen + run-to-value
E2E), tidy `39380d3`/`2ee9794` (lambda-captures-loop-binder rejected
at check). Milestone-close `audit` clean (architect drift resolved,
bench pristine 25/0 carry-on); `fieldtest` 2026-05-18 clean on all
four milestone axes (0 bugs; diagnostics point-exact+self-fixing,
recur-tail through match/let, loop-as-value-subexpr byte-stable,
no-termination exact). Two orthogonal non-blocking findings routed
to P2 todos (niladic `(app f)` — re-confirms mut-local F3; module
`(doc)` diagnostic hint). Stays here briefly for context; remove
once stale (full record in `git log`).
- context: `docs/specs/2026-05-17-loop-recur.md`;
`docs/specs/2026-05-18-fieldtest-loop-recur.md`; principles entry
`docs/specs/2026-05-17-llm-surface-discipline.md` §6.2.
- [x] **\[milestone\]** DESIGN.md / docs honesty lint — CLOSED
2026-05-18. `docs/DESIGN.md` + `docs/PROSE_ROUNDTRIP.md` now mirror
ONLY the current state, present-tense: 14 DESIGN.md corrections
(Wunschdenken stated-as-fact + non-citation post-mortem/doc-
archaeology stripped) + the PROSE tool-use/MCP paragraph; genuine
forward intent (LLM tool-use/MCP/LSP) relocated to P3. The
two-pronged tense+modality discriminator is codified present-tense
as a DESIGN.md `### What this document is …` meta-subsection
(Approach A) the architect cites. Anti-regrowth two ways: a
wrap-robust enumerated absent/present pin
`crates/ailang-core/tests/docs_honesty_pin.rs` (a `norm()`
whitespace-collapse helper structurally discharges the recurring
grep/contains line-wrap failure family — strictly better than the
effect-doc-honesty "keep-on-one-line" precedent) + wrap-robust
advisory Sweep 5 in `architect_sweeps.sh` (contiguous-fragment
regex) with full sweep-count lockstep + a new `ailang-architect.md`
"DESIGN.md honesty drift" bullet. Procedural enumeration vindicated:
the Task-4 Step-14 catch-all caught a genuine soft-wrapped extra FIX
(Form-B prose-projection bullet) a frozen list + the spec's
illustrative regex would both have missed. Single iter
(`aff25cd`/`de66eb7`/`7580d43`); milestone-close `audit` CLEAN
(architect clean, one `[medium]` advisory Sweep-5 self-match wart →
P3 below; bench causally exonerated — HEAD vs `5bb7211`/`de66eb7`
bench binaries `cmp`-byte-identical, NO ratify); no fieldtest (zero
authoring-surface change). Stays here briefly for context; remove
once stale (full record in `git log`).
- context: `docs/specs/2026-05-18-docs-honesty-lint.md`
(grounding-check PASS 10/10).
## P1 — Next
- [x] **\[milestone\]** Embedding ABI — M1: linkable artifact +
scalar C entrypoint + `main`-free lifecycle — CLOSED 2026-05-18.
First of the five-milestone arc (M1M5) making a compiled AILang
kernel callable in-process from a concurrent Rust host. Shipped:
additive `FnDef.export: Option<String>` (hash-stable, ~85-site
compile-driven thread); Form-A `(export "<sym>")` modifier
(round-trip-gated); check-side scalar-only+effect-free export gate
(`export-non-scalar-signature` / `export-has-effects` — the
feature-acceptance clause-3 discriminator *in code*); codegen
`Target::StaticLib` (suppresses `@main`+`MissingEntryMain`, emits
one external `@<sym>` forwarder per export, decoupled from
`ail_<module>_<fn>` mangling per Decision 2); CLI `ail build
--emit=staticlib` (`lib<entry>.a` + separate `libailang_rt.a`).
Coherent stop PROVEN: C host links both archives, calls the
scalar kernel, typed return holds. spec `51160e9` (grounding-check
PASS) → plan `b0bd7ef` (Repaired) → iter `818177d` (partial 13 +
Boss plan Repair: a module≠stem fixture defect, resolved Option 2)
+ `e406d07` (DONE 47). Milestone-close `audit` `425c4eb` CLEAN
(architect clean — Invariant 1 holds semantically, lockstep
intact; sole `[low]` rustfmt-divergence evidence-adjudicated
carry-on, 1137 pre-existing tree-wide divergences prove rustfmt
is not a project convention; bench `check.py` exit 1 decisively
causally exonerated — 21/21 bench binaries `cmp`-byte-identical
vs pre-milestone, the two tracked-P2 noise families, NO ratify).
`fieldtest` `6a4e866` substantiated the thesis (0 bugs; Int+Float
positive E2E first-try clean = clause-1; both clause-3 rejections
precise+self-correcting; 3 working) — 3 non-blocking findings
routed to the two follow-up items directly below. Stays here
briefly for context; remove once stale (full record in
`git log`).
- context: `docs/specs/2026-05-18-embedding-abi-m1.md`;
`docs/specs/2026-05-18-fieldtest-embedding-abi-m1.md`
- [x] **\[todo\]** form_a.md scalar-parameter mode carve-out — docs-
honesty tidy (M1 fieldtest friction + spec_gap#1, shared root) —
CLOSED 2026-05-18. `crates/ailang-core/specs/form_a.md` stated an
*unconditional* "every `(fn …)` param MUST carry an `(own/borrow)`
mode" rule in **four** places that contradicts shipped checker
behaviour (scalars take **and require** bare `(con Int)`; a mode on
a scalar trips body-pointing `use-after-consume` /
`consume-while-borrowed`). All four sites rewritten symmetric to
the pre-existing return-type carve-out; the already-correct
few-shot annotation left verbatim by design; DESIGN.md §"Embedding
ABI (M1)" gained the bare-scalar export-param rule + a
corpus-grounded `step` Form-A snippet; RED-first anti-regrowth pin
via `ailang_core::FORM_A_SPEC` + `norm()` (4 ABSENT + 4 PRESENT
form_a + 1 PRESENT DESIGN). Zero language/checker/codegen change;
Boss-verified docs_honesty_pin 5/0, spec_drift 8/8 (not a lockstep
partner), ailang-core 112/0, workspace 622→623 (+1 = the pin), zero
`crates/**/src/**` diff. Pure docs+pin tidy — no audit/fieldtest
gate (the pin IS the regression coverage). plan
`docs/plans/form-a-scalar-param-mode-carveout.md` (`4c266a6`) → iter
`7d7f04e`. Stays here briefly for context; remove once stale (full
record in `git log`).
- context: `docs/specs/2026-05-18-fieldtest-embedding-abi-m1.md`
findings [friction] + [spec_gap]#1.
- [x] **\[feature\]** `ail emit-ir --emit=staticlib` — restore the
Decision-5 IR-readability affordance for kernels (M1 fieldtest
spec_gap#2) — CLOSED 2026-05-18. `ail emit-ir` had no
`--emit=staticlib` (only `ail build` did) and on a `main`-free
kernel hit the executable-path `MissingEntryMain` rejection — an
author could not read the generated `@<sym>` forwarder for an
exported kernel. Resolution **added** (DESIGN.md *widened*, never
narrowed): a one-line symmetric codegen convenience
`lower_workspace_staticlib(ws)` routing through the M1-audited
unchanged `Target::StaticLib` path, an `emit` clap field on
`Cmd::EmitIr` symmetric with `Cmd::Build`'s, a zero-export guard
byte-identical to `build_staticlib`'s; DESIGN.md §"Embedding ABI
(M1)" affordance sentence + a CLI-synopsis correction also
discharging a pre-existing M1 `ail build --emit=staticlib`
omission. No new doc pin (E2E pins behaviour; architect's
milestone-close DESIGN.md read catches stale prose). Boss-verified:
3 new E2E 3/0 (incl. functional CLI spot-check — `@backtest_step`
external forwarder readable, no `@main`), `embed_staticlib_cli`
2/0 (build-side untouched), `docs_honesty_pin` 5/0, `ail` 186/0,
workspace 623→626 (+3), zero out-of-scope diff, no fixture minted.
Scoped feature — no audit/fieldtest gate (E2E is the coverage;
reuses the M1-audited codegen path, no invariant/surface change).
plan `docs/plans/emit-ir-staticlib.md` (`03493c9`) → iter
`bcfe554`. Stays here briefly for context; remove once stale (full
record in `git log`).
- context: `docs/specs/2026-05-18-fieldtest-embedding-abi-m1.md`
finding [spec_gap]#2.
- [x] **\[milestone\]** Embedding ABI — M2: per-thread runtime
context + concurrency safety — CLOSED 2026-05-18. A compiled
scalar AILang kernel is now callable concurrently from a host
thread swarm without an RC-runtime data race. `ailang_ctx_t*`
is a mandatory leading parameter of the (M3-frozen-shape) C
`@<sym>(ctx, scalars…)` ABI; the M1 forwarder publishes it via
a `__thread` slot around the **byte-unchanged** internal
`@ail_<mod>_<fn>` call (internal convention + `_adapter`/`_clos`
untouched — M1 decision held; TLS sound because the kernel call
is synchronous, ctx never held across a suspension). `runtime/rc.c`
de-globalised: the two RC-counter sites became `if (ctx)
ctx->{alloc,free}_count++ else g_rc_*++`, the `g_rc_*` statics +
`atexit` retained verbatim as the null-ctx single-threaded
executable fallback (executable-path leak readback byte-preserved).
ctx near-empty by discipline (`{alloc,free}_count` only — no
arena/lock/atomic/IO; the build-ahead trap named out-of-scope).
Staticlib is RC-only enforced (`--emit=staticlib` rejects
`--alloc=gc|bump`; Boehm never linked into a swarm artefact;
standing P2 Boehm-retirement untouched). No authoring-surface /
schema / `ailang-check` change — the `.ail` is byte-identical to
M1; the deliverable is a swarm-safe generated ABI + de-globalised
runtime, **sanitiser-verified** (per-thread-ctx scalar swarm
tsan-clean == oracle; direct rc-accounting per-ctx clean;
shared-ctx negative control a genuine `ThreadSanitizer` race at
the de-globalised counters, exit 66 — teeth non-vacuous). Process
note: the first implement dispatch correctly BLOCKED on a genuine
spec defect (a negative control on the non-allocating scalar
swarm is structurally impossible — the spec's own honesty point);
Boss adjudicated a spec-consistency repair (teeth relocated to the
rc-accounting harness) rather than a brainstorm bounce. spec
`1c58055` (grounding-check PASS 9/9) → plan `b3388c8`
(Boss-corrected) → iter `c9a84b3` (PARTIAL 4/9 + Boss repair) +
`fbeeade` (DONE 59); milestone-close `audit` `ad88dec` (architect
one `[medium]`+`[low]` doc-honesty drift; bench `check.py` exit 1
decisively causally exonerated — bencher H0-refuted, all 3 firings
the two tracked-P2 noise families, rc.c hunk branchless+free, NO
ratify) → tidy `a80d495` (the `## Embedding ABI` lockstep rename,
drift resolved). No fieldtest (zero authoring-surface change).
Stays here briefly for context; remove once stale (full record in
`git log`).
- context: `docs/specs/2026-05-18-embedding-abi-m2.md`.
- [x] **\[milestone\]** Embedding ABI — M3: frozen value layout +
single ADT/record crossing + RC ownership contract — CLOSED
2026-05-18. A single-constructor record of `Int`/`Float` fields
now crosses the embedding C ABI in **and** out; ownership follows
the declared `own`/`borrow` mode (mechanically inherited — the M2
forwarder body is byte-unchanged, all mode-driven RC stays in the
byte-unchanged internal `@ail_<mod>_<fn>`); the record memory
layout (header@p-8 / i64 tag@0 / fields i64-strided@8+i·8 /
size=8+n·8) is **frozen** as a one-way commitment — a new
DESIGN.md §"Embedding ABI" `### Frozen value layout` SSOT +
`// FROZEN ABI` lockstep pointers at the three encoders + an
enforceable `@ailang_rc_alloc` heap-box byte-pin (RED-on-offset-
perturbation proven); host construction via `ailang_rc_alloc`,
host-free via `ailang_rc_dec` (leak-free *because* M3 fields are
scalar — no boxed children; non-scalar recursive-free named M4-
additive). No authoring-surface / schema / Form-A / `CheckError`
change (M2-style — the `.ail` is the minimal evolution of the
M1/M2 scalar kernel into a record `State`). The `(State,Float)->
State` fold round-trip is proven **globally leak-free** for both
`own` and `borrow`. Process: spec `1fbb9c4` (grounding-check PASS
8/8) → plan `15ee3c5` → iter `d5c565d` (PARTIAL 5/7 + a Boss
spec-consistency repair: the orchestrator correctly BLOCKED on a
genuine spec defect — the single-ctx-readback proof model is
unsatisfiable for `borrow` by M2's TLS-ctx design; Boss
M2.1-precedent adjudication → a stronger global-leak-freedom proof
model) + `4ea8bc5` (DONE 6-7: the freeze). Milestone-close `audit`
`b8a60b1` (architect: Invariant 1 clean, M1/M2 byte-invariant
held, frozen-SSOT consistent — DRIFT only `[medium]`+`[low]`
doc-honesty → tidy; bench `check.py` exit 1 **decisively causally
exonerated** — byte-identical generated IR HEAD vs pre-M3
`9a609ae` for every firing bench, the two tracked-P2 noise
families, NO ratify) → tidy `63d7d60` (pin-safe; drift resolved).
No fieldtest (zero authoring-surface change). Stays here briefly
for context; remove once stale (full record in
`git log`).
- depends on: Embedding ABI — M2.
- context: `docs/specs/2026-05-18-embedding-abi-m3.md`.
- [x] **\[milestone\]** Embedding ABI — M5: `ail-embed` adapter +
`data-server` wiring + thread-swarm backtest — **DONE 2026-05-19**,
audited + ratified + doc-honest. Terminal milestone of the M1M5
Embedding ABI arc: a real backtest runs over **real Pepperstone
tick data** through the real external `data-server` crate into the
shipped M3-frozen AILang kernel, across a thread swarm of
independent per-thread embedding contexts. `ail-embed` is a lean
reusable embedding module (`extern "C"` to the M3-frozen ABI +
frozen-layout box helpers, zero finance knowledge) **plus** the
real E2E on top — in-repo but its own cargo workspace root
(`[workspace]`-excluded), the sole `data-server`↔AILang meeting
point (Invariant 1, architect-confirmed PASS at close). The
adapter (not the kernel) owns chunk iteration, unrolling each
chunk host-side into per-tick `(State, Tick) -> State` calls (M4's
cons-list crossing was retired as speculative infra). Symbol-fan
swarm = the headline existence proof (per-thread bit-exact vs an
independent host reference); time-shard swarm = the **first actual
proof** of the chunk/window-boundary invisibility the M4
retirement rests on (per-shard bit-exact). Globally leak-free,
deterministic. Zero language/compiler change; one runtime change
(`7bfa11e`: the global RC-stats fallback counters made
atomic-relaxed — the swarm exposed that AILang's first concurrent
consumer needed it; user-adjudicated bounce-back → RED-first fix,
then a build-dependency staleness root-caused + fixed). Audit:
Invariant 1 PASS, M3 frozen-layout SSOT unmoved, bench `gc_rss`
trio evidenced-RATIFY (a Boehm conservative-scan artefact of the
necessary atomic fix; Decision-9 Boehm-transitional), P2 noise
causally exonerated baseline-pristine; doc-honesty tidy clean.
**Friction-harvest deliverable: host-per-tick-FFI ≈ ~206 ns/tick
at real EURUSD volume** — the P2 flat-array decision input.
Stays one cycle for context, then prune.
- depends on: Embedding ABI — M3 (shipped); Tick-coverage on M3
(shipped `170464f`).
- context: `docs/specs/2026-05-19-embedding-abi-m5.md`.
- [x] **\[milestone\]** Heap-`Str` ABI — runtime infrastructure for
malloc-backed, refcounted `Str` values alongside the existing
static `@.str_*` globals. Today the `Str` path is static-only
(DESIGN.md §"Float semantics" notes this explicitly), so any
primitive that needs to produce a `Str` at runtime — `int_to_str`,
`float_to_str` (currently type-installed but `CodegenError::Internal`
on call), eventual `Show.show`, future `++` on strings — cannot
ship. Scope: pick the representation (likely a `{rc_header, len,
bytes…}` slab consistent with the rest of the RC runtime), teach
codegen to accept both static and heap `Str` at the same ABI slot,
wire RC `inc`/`drop`/`clone`/`compare`/`eq` on the heap form, and
ship `int_to_str` + `float_to_str` as the first two callers so the
ABI gets exercised end-to-end. Unblocks Show + print rewire below.
- context: DESIGN.md §"Float semantics" (`float_to_str` is type-
installed, codegen-deferred); spec 2026-05-11-23-eq-ord-prelude
§"Show. Defers behind a heap-Str-ABI milestone"; spec
2026-05-10-fieldtest-floats §F4 ("dynamic Str allocation in the
runtime"); spec 2026-05-09-22-typeclasses §22b.4b ("Show#Int
needs an `int_to_str` primitive returning heap-allocated Str").
- [x] **\[milestone\]** Post-22 Prelude — Show + print rewire — shipped
2026-05-13 as iters 24.1 (heap-Str runtime + codegen for `bool_to_str`
+ `str_clone`, f38bad8), 24.2 (prelude `class Show` + four primitive
instances Int/Bool/Str/Float + 22b TShow/tshow migration), and 24.3
(polymorphic `fn print : forall a. Show a => (a borrow) -> () !IO`
+ positive 4-prim E2E + user-ADT E2E + Show-aware NoInstance
diagnostic + DESIGN.md amendments to §"Prelude (built-in) classes"
and §"Float semantics"). The `MethodNameCollision` workaround that
blocked the original spec retired in mq.3 (2026-05-13); spec
`docs/specs/2026-05-13-24-show-print.md` re-derived 24.2 + 24.3
against the post-mq architecture.
- context: spec `docs/specs/2026-05-13-24-show-print.md`.
## P2 — Medium-term
- [x] **\[milestone\]** DESIGN.md → `design/` — CLOSED 2026-05-19.
The 3020-line `docs/DESIGN.md` is **deleted** (clean cut, no
stub) and replaced by the `design/` ledger: `design/INDEX.md`
(sole addressable spine, typed Contracts+Models tables with
polymorphic links — prose file OR authoritative source `//!`),
14 `design/contracts/*.md` test-linked invariants + 3
source-link-only contracts (mangling/env-construction/
qualified-xref — code is SoT), and 5 `design/models/*.md`
whitepapers. RED-first
`design_index_pin.rs` 4-clause anti-regrowth spine; clause-3 is
a hand-rolled **faithful Sweep-1 superset** so the in-code hard
gate enforces the honesty spirit (clause-3 GREEN ⟹ Sweep-1
clean in contracts/). Build-atomic by task ordering (the only
compile-time consumer's `include_str!` retargeted before the
deletion). Every live `DESIGN.md` reference — 4 executables, 2
diagnostics + 2 E2Es, ~12 agent reading lists, 5 SKILL bodies,
CLAUDE.md, README, ~25 code/C/.ail/spec comment xrefs —
retargeted in lockstep; the honesty rule rewritten to name the
new rationale home. Process: spec `a64b2cc`/`314e5e4`
(grounding-check PASS ×2) → plan `deeffb1` → iter .1 `176821c`
(DONE 9/9) → `audit` `2ba5e16` (architect drift_found
`[medium]`+`[low]`, relocation byte-faithful; bencher
causally-exonerated DECISIVE on byte-identical IR+binaries,
baseline pristine — M2/M3/M5 disposition) → tidy `f2cdd67`/
`f683f1a` (DONE 7/7; the audit Resolution mechanism+scope
corrected on planning-time evidence after a correctly-BLOCKED
plan defect — the "two+ defects ⇒ fix the upstream artifact"
discipline, not a third patch). No fieldtest (zero
authoring-surface change — reasoned exclusion). Stays briefly
for context; remove once stale (full record in
`git log`). Original decided-shape rationale
retained below for context until pruned.
**Motivation.** The just-closed `docs-honesty-lint` milestone is
the proof this is not a cleanup problem: it ran a full docs pass
and DESIGN.md stayed 2905 lines, because honesty-lint operates
*within* a job (tense / Wunschdenken / citation) and its own
audit-trail invariant *mandates retaining* the bulky
decision-record prose (DESIGN.md:242 "records the *why* … for
the audit trail"; the architect Sweep-5 discriminator explicitly
keeps "present-tense design rationale"). You cannot clean your
way out of conflation — the conflation itself is the structural
defect.
The three conflated jobs and their distinct consumers:
- **Contract layer** — prescriptive, test-linked invariants
(e.g. Decision 6's seven hard constraints, the Roundtrip
Invariant, the borrow/own binding rules). Consumer:
`architect` (mandatory full read at *every* milestone close),
`grounding-check` (every spec), the bench drift lints. Wants:
structured, addressable, test-linked, small, hot. This is the
only job that belongs on the per-milestone hot path.
- **Model exposition** — whitepaper-shaped model narratives
(RC + Uniqueness, typeclasses, effects). Consumer: onboarding
(fresh-context LLM, `fieldtester`). Irreducible — the code
cannot speak the gestalt; one whitepaper per model.
- **Decision-record / relitigating-guard** — why X was chosen,
why Y/Z were rejected (region inference, tracing GC, …).
Consumer: a future `brainstorm`, so it does not re-propose a
settled-and-rejected idea. (This tier was migrated out of
DESIGN.md at split time and later retired entirely; the
project's history lives in `git log`.)
Underlying principle: the code is authoritative for *what it
does* — delete every description of behaviour, the code speaks;
the spec's irreducible job is *what is promised* plus a pointer
to the green test that ratifies it (code answers *what*; spec
answers *what is promised*; test answers *does the promise still
hold*).
**Decided shape.** `design/` split on the consumer/lifetime
axis — **not** by Decision-number (that is cosmetic; the
architect would still read everything). `design/contracts/` (the
hot, test-linked invariant set), `design/models/` (one
whitepaper per model). `design/INDEX.md` is the typed ledger
spine: per entry a one-liner + metadata
`{kind, ratifying-test, consumer/lifetime}` + a content link
whose target is polymorphic — a `design/contracts/…` file, a
single-owner `//!` source header (mangling, env, roundtrip,
float, tail-calls, data-model), or a `design/models/…`
whitepaper. INDEX.md is the sole addressable entry point.
Decision-records move out of DESIGN.md; the honesty-lint
audit-trail invariant is repointed in the same milestone.
**Rejected — do not relitigate.** Quarto / `.qmd` was evaluated
and rejected: every distinctive Quarto feature (rendered
HTML/site, figures, citations, cross-ref resolution, executable
chunks) is render-time value for a *human reader who does not
exist here* — the `design/` consumer is an LLM reading raw
files. Executable chunks would reintroduce a second "is the
invariant green?" truth that can diverge from `cargo test` (the
duplicate-source-of-truth failure mode); Quarto `@ref`
cross-refs degrade the raw-file reader (unresolved tokens vs.
resolving relative-path links); plus a non-Rust toolchain
dependency for zero in-loop value. Plain Markdown + a typed
INDEX.md is strictly better for this consumer. A plain shard by
Decision-number is likewise rejected (changes nothing for the
architect hot path).
**Sequencing / blast radius.** Deliberately scheduled *after*
the in-flight Embedding ABI arc — a substantive sequencing
reason, not effort. `DESIGN.md` is referenced by name by at
least `bench/check.py` (history-anchor / `design_schema_drift`),
`bench/architect_sweeps.sh` (Sweep 5; exit 2 = "could not find
DESIGN.md"), the reading lists of `ailang-architect` /
`ailang-grounding-check` / `ailang-fieldtester`, the CLAUDE.md
"Roles of docs/…" section, and the honesty-lint invariant's own
scope — load-bearing invariants and agent contracts, not just
paths. Every Embedding-ABI milestone close runs `audit`
(architect + bench) against DESIGN.md; restructuring it mid-arc
would break the architect's reading list and the drift scripts
*under* an in-flight milestone. Hence no overlap with active
implementation that closes audits against the current
DESIGN.md. Brainstorm hard-gate before any plan/code — it
rewrites enforced invariants and agent contracts.
- context: spec `docs/specs/2026-05-19-design-md-rolesplit.md`
(incl. the Boss-adjudicated relocation Appendix; grounding-check
PASS ×2); origin 2026-05-18 chat (DESIGN.md-management
dialogue — six-turn convergence). CLOSED — audited +
drift-resolved; hard gate enforces the honesty spirit.
- [x] **\[milestone\]** Formal cross-links in the `design/` ledger
(browsable wiki) — CLOSED 2026-05-19. The positive completion
of DESIGN.md → `design/`: informal prose cross-references between
contracts/models are now **formal file-relative Markdown links**
resolved relative to the containing file, gated by RED-first
`design_index_pin.rs` **clause-5** that fails the build on any
body link that does not resolve into the durable tier (`design/`
+ `crates/**` + `runtime/**` only — never `docs/`, never a
`#fragment`). clause-5 ∘ clause-3 = complete invariant: every
contract cross-reference is a resolving durable file-link or
clause-3-forbidden history prose. INDEX spine stays
repo-root-relative (registry tier, clause-1 byte-unchanged) — the
move-fragility rationale that drives body prose to file-relative
is absent for the file that never moves. Shipped in one iter
(5/5 tasks): clause-5 with `strip_fences` (toggle on ```/~~~ so a
`](` inside a fence is not treated as a link) + RED-first via
identity-stubbed `strip_fences` synthetic vectors; 7 prose
conversions (8 link tokens — float-semantics:69/100,
embedding-abi:45, memory-model:44/105, scope-boundaries:48/88
with mixed-referent split into a source link + a Pipeline
cross-file link); clause-6 disposition (b) on the 2
PROSE_ROUNDTRIP homeless pointers (pointer removed, behavioural
`ail merge-prose` prose preserved); pin-safe positive-half
paragraph in `honesty-rule.md`.
- context: spec `docs/specs/2026-05-19-design-ledger-formal-links.md`
(grounding-check PASS ×3 across two corpus-grounded amendments —
clause-6 + cross-ref definition; clause-5 fence-skip + closed
convert-set enumeration); plan
`docs/plans/design-ledger-formal-links.1.md`; origin this
session's six-turn design conversation following the split close.
CLOSED — audit clean (zero drift, bench trio 0/0/0); no tidy,
no fieldtest (zero authoring-surface change — reasoned exclusion).
- [ ] **\[milestone\]** Flat array/slice primitive — performance
follow-up to the Embedding ABI arc, *not* a capability gap.
AILang's only sequence is the recursive `List` ADT (one RC cell
per element). The ABI arc resolved chunk-crossing by having the
M5 adapter unroll each `data-server` chunk host-side into per-tick
`(State, Tick) -> State` calls (M4's cons-list crossing was
retired as speculative infra) — so the residual perf question is
no longer "1024-cell cons-chain per chunk" but **host-per-tick-FFI
vs. an eventual batch/flat-array crossing**. M5's friction-harvest
measured the per-tick FFI at **≈ ~206 ns/tick at real EURUSD
volume** (≈ 658 ms / 3.19 M ticks). Open decision: whether a
contiguous array/slice primitive (to amortise that per-tick cost
via a batch crossing) is worth a *language* change — it interacts
with uniqueness inference, RC, and the
structurally-decreasing-recursion totality story, and is out of
the (now-closed) ABI arc's scope. Now decidable on the measured
number, not speculation.
- depends on: Embedding ABI — M5 (shipped; measured ~206 ns/tick
— the justification input now exists).
- context: 2026-05-18 chat (user deferred this to keep the ABI
arc clean); the friction-harvest number recorded in the M5
audit commit; M4 retirement decision recorded in the M5 closing
commits.
- [ ] **\[milestone\]** Iteration-totality story — structural +
Int-bounded total recursion with *enforced* non-negativity.
AILang's iteration story stays as-is (structural / tail recursion;
`tail-app` intact). The genuine ambition — make `f(n-1)`-family
recursion (incl. branching tree builders) total *by construction*
with the non-negative-entry precondition **enforced**, not merely
documented — is deferred here because doing it without a purity-
pillar concession requires refinement/`Nat` type machinery the
language has not built (Decision 4 keeps refinements opaque, no
SMT). Not abandoned; correctly sequenced after the type machinery.
- depends on: a future `Nat`/refinement-types milestone (no spec
yet).
- context: `docs/specs/2026-05-16-iteration-discipline-revert.md`
(why the 2026-05 attempt was reverted); the branching-builder
counter-example that surfaced the gap landed in the it.3 iter
commit.
- [x] **\[milestone\]** Retire `io/print_int` / `io/print_bool` /
`io/print_float` effect-ops + migrate example corpus to `print`.
Shipped 2026-05-14 as iter rpe.1.
- context: shipped in the rpe.1 iter commit.
- [x] **\[todo\]** Author `examples/prelude.ail` alongside
`examples/prelude.ail.json`. (Satisfied 2026-05-13 by iter
form-a.0 — `examples/prelude.ail` rendered via `ail render`,
116 lines / 6386 bytes, round-trip-CI green.)
- [x] **\[milestone\]** Form-A as the default authoring surface for
examples and docs. (Closed 2026-05-13 by iter form-a.1.) Render every `examples/*.ail.json` to its
`.ail` sibling via `ail render`, **delete the now-redundant
`.ail.json`**, and regenerate the JSON-AST per `ail parse` at
build / test time. Same for inline JSON-AST blocks in `docs/`
markdown (~7 in DESIGN.md plus ~29 other md files) — convert to
Form-A snippets where the snippet's purpose is to show "what the
language looks like", not "what the schema is".
After this milestone the working tree contains exactly one
representation per program: the `.ail` source. The JSON-AST is a
build artefact, not a checked-in twin. Tests and benches that
currently glob `*.ail.json` either parse-then-consume or are
rewired to point at `.ail` directly. The round-trip invariant
flips role: today it gates that the two forms agree; afterwards
it gates that `parse` is deterministic.
Carve-outs that MUST stay JSON-AST (no Form-A counterpart, and
no `.ail` sibling shall be created — these are the only seven
files that remain `.ail.json`-only post-milestone):
- Fixtures that test canonical-form rejection — Form A would
reject them at parse, defeating the test:
`test_ct1_bare_xmod_rejected.ail.json`,
`test_ct1_qualified_class_rejected.ail.json`,
`test_ct1_bad_qualifier.ail.json`, `broken_unbound.ail.json`,
`test_22b2_invalid_superclass_param.ail.json`,
`test_22b2_kind_mismatch.ail.json`,
`test_22b2_unbound_constraint_var.ail.json`.
- DESIGN.md schema documentation blocks where the JSON-AST shape
*is* the point (Decision 11, ParamMode, canonical-form
invariants).
- Any other case where the structured form is the artefact under
discussion, not a vehicle for a program.
Touches CLAUDE.md ("source of truth is structured data") — the
language doctrine doesn't change (JSON-AST is still the canonical
hashable form), but the *authoring* doctrine does: authors write
`.ail`, the build derives JSON-AST. Sentence in CLAUDE.md needs
rewording in the same milestone.
Mechanical for the bulk; per-fixture judgement call only for the
carve-out list. Run as one milestone so the corpus flip-over
happens at a single, audit-gated point.
- context: post-Form-A-as-canonical-authoring corollary of the
`examples/prelude.ail` entry above. Generalises the same idea
to the rest of the corpus and follows through on the cross-
model authoring data (textual form cheaper, more first-try
hits) by treating Form A as the privileged surface in the
working tree, not just in flavour text.
- [ ] **\[feature\]** Operator routing through `Eq` / `Ord` — `==`,
`<` etc. resolved via the typeclass instead of the built-in
primitive comparators. No commitment; gated on bench re-baselining
to make sure the indirection doesn't tank latency.
- context: `git log --before=2026-05-11 --grep=...` for the
pre-archive-cutoff rationale.
- depends on: Post-22 Prelude — Eq/Ord (shipped 23.5)
- [x] **\[todo\]** `types` / `ctor_index` overlay shape question —
decide whether the env's two parallel ctor maps should collapse
into one overlay, or stay split. Surfaced during the
env-construction unify audit.
- context: closed by iter ctt.1 — DESIGN.md §"Env construction" anchors the split decision.
- [x] **\[todo\]** CLI human-mode diagnostic surface for `WorkspaceLoadError`.
Shipped 2026-05-14 as iter cli-diag-human — a new `load_workspace_human`
helper in `crates/ail/src/main.rs` routes 9 non-JSON
`ailang_surface::load_workspace(&path)?` call sites through
`workspace_error_to_diagnostic`, so the bracketed `[code]` prefix is
preserved across `ail check`, `build`, `run`, `emit-ir`, `prose`,
`describe`, `deps`, `diff`, `manifest`.
- context: shipped in the cli-diag-human iter commit.
- [x] **\[todo\]** Retire dead `KindMismatch` arm — `validate_classdefs`'s
`walk_kind_mismatch` path is structurally unreachable through
well-formed schema post-ct.1 (the canonical-form validator catches
the malformed `Type::Con { name: param }` shape earlier). The
enum variant + `walk_kind_mismatch` helper stay as dead-but-defensive
code; a future tidy can delete both.
- context: closed by iter ctt.3 — variant + helper + dispatch + Display arm all deleted; canonical-form-rejection test stays green asserting `BareCrossModuleTypeRef`.
- [x] **\[todo\]** Re-key `Registry.type_def_module` to handle
bare-name-collision-across-modules — `BTreeMap<String, String>` keyed
by bare type name silently overwrites when two modules each define
`type Foo`; `normalize_type_for_registry` would then collapse `M.Foo`
and `N.Foo` to whichever insert won. Acceptable for current corpus
(distinct bare type names across modules), but the proper fix is to
key by `(owning_module, bare_name) → defining_module`.
- context: flagged by the ct.1.5a quality reviewer.
- [ ] **\[feature\]** 22c — typeclass corpus expansion. User-defined
classes beyond the prelude four; multi-parameter classes; superclass
chains; richer instance bodies. Deferred from milestone 22.
- context: `git log --before=2026-05-11 --grep=...` for the
pre-archive-cutoff rationale.
- [x] **\[milestone\]** Module-qualified class names + type-driven
method dispatch — retired the `MethodNameCollision` workaround;
shipped 2026-05-13 as iters mq.1 (canonical-form extension for
class-ref fields + workspace-internal qualification), mq.2 (type-
driven dispatch mechanism installed: `method_to_candidate_classes`
inverse index, multi-candidate `ResidualConstraint`,
`resolve_method_dispatch` 5-step rule, `AmbiguousMethodResolution` +
`UnknownClass` diagnostics), and mq.3 (retirement + multi-class E2E
+ `class-method-shadowed-by-fn` warning + DESIGN.md sync). Two
libraries can now each declare `class Eq` with their own `eq`;
cross-class method ambiguity is resolved at the call site via
type-driven dispatch with `<module>.<Class>.<method>` as the
disambiguation form.
- context: `docs/specs/2026-05-10-canonical-type-names.md` "Out of
scope: Class names" — the workaround was named there so it
stayed visible until this milestone retired it.
- [ ] **\[todo\]** Boehm full retirement — remove the transitional
Boehm GC path now that RC + uniqueness is the canonical memory
story.
- context: pre-milestone-22 "Boehm transitional" decision —
see commits from that window.
- [ ] **\[feature\]** Closure-pair slab / pool — codegen tweak to
pool the env+code closure pairs instead of one-shot heap allocs.
Bench-gated.
- [ ] **\[todo\]** `FnDef::synthetic` flag — formalise the
monomorphiser-emitted FnDefs so downstream passes can tell
user-authored from synthesised at a glance. Currently inferred from
symbol naming.
- [ ] **\[todo\]** 21'h iteration — final 21' carry-over (latency
methodology pass). Numbering kept for continuity with the 21' arc.
- [x] **\[todo\]** DESIGN.md effect-prose is fiction — standalone
documentation-honesty tidy. Shipped 2026-05-16 as iter
effect-doc-honesty (a29700c): the three false effect-system claims
(row-polymorphic `![IO | r]`; `IO`+`Diverge` both wired up;
`Term::Do` "effect-handler table at link time") reconciled to the
real flat-closed-set / IO-only / `IndexMap`+codegen-`match`
mechanism, satellite lockstep done, guarded by a 4-test
doc-presence pin. No language/checker/codegen change.
- context: shipped in the effect-doc-honesty iter commit.
- [ ] **\[todo\]** `io/print_float` always-emit-`.0` — surface
printer always emits `.` or `e/E` so re-lex routes to Float;
the runtime printer (`printf("%g\n", v)`) doesn't, so `2.0`
prints as `2` (Int-shaped). Asymmetric. Either switch the
runtime path to a `.0`-fallback printer (matching surface) or
document the `%g` contract in DESIGN.md §"Float semantics" so
the LLM-author knows `io/print_float`'s output is for-humans
not round-trip.
- context: `docs/specs/2026-05-10-fieldtest-floats.md` finding F1.
- [ ] **\[todo\]** Rustdoc warning sweep — `cargo doc --no-deps`
reports 16 pre-existing warnings (15 in `ailang-check`, 1 in
`ailang-core`: private-item links from public doc, unresolved
intra-crate links). All predate the design-md-consolidation
milestone; treat as a one-off sweep.
- context: surfaced in the design-md-consolidation audit close
(2026-05-10).
- [ ] **\[todo\]** `ailang-plan-recon` site-undercount countermeasure
(P2, **escalated** — now a structural recon-contract defect, no
longer a single-milestone curiosity) — the recon agent hand-lists
the change blast radius and has under-counted it **four times across
two milestones, through a different site type each time**:
loop-recur.1 walker arms, loop-recur.2 cross-module `synth` callers,
loop-recur.tidy implicit, and now remove-mut-var-assign.1 (in-source
`mod tests` fns + a drift-pin fn + 5 orphaned `.ail.json` carve-outs
+ a non-enumerated `codegen_import_map_fallback_pin.rs` E0599) **plus
a spec-named doc the recon was told did not exist**
(`crates/ailang-core/specs/form_a.md` — the spec listed it for
deletion; a Boss plan-time grep checked only `docs/form_a.md`,
concluded "no such file", and propagated that into the recon brief;
caught only at milestone-close audit). Two distinct gaps: (a) the
compile-driven sweep catches exhaustive-`match`/caller misses but
**not** non-compile-checked sites (docs, fixtures, drift pins) —
those need a separate "every spec-named path is verified to exist
and is handled" cross-check; (b) a recon brief must never inherit an
unverified "X does not exist" — existence claims feeding a recon are
themselves load-bearing and must be tree-wide, not dir-scoped.
Tighten `skills/planner/agents/ailang-plan-recon.md`: for any
signature-change / enum-variant / removal scope the recon REPORTS
the compile-driven enumeration as the authoritative code-site set
(hand-list advisory) AND emits a spec-named-path existence table
(every path the spec names, `ls`-verified, with its handling task).
No language change; an agent-definition discipline fix.
- context: loop/recur close audit (architect `[low]`, 2026-05-18) +
remove-mut-var-assign close audit (architect `[high]` form_a.md +
`[medium]` process-drift, 2026-05-18); pairs with planner Step-5
items 7+8 (those scrub the *plan*; this scrubs the *recon* and the
*recon brief*).
- [ ] **\[todo\]** `design_schema_drift.rs` fidelity widening —
current test checks anchor *presence* anywhere in DESIGN.md;
the audit found that `[high]` schema gaps in §"Data model"
are invisible because anchors live in Decision 11 instead.
Constrain the test to scan only §"Data model" + ParamMode
block, or extract JSON-schema blocks into a machine-readable
file the test consumes.
- context: surfaced in the 2026-05-10 audit close.
- [ ] **\[todo\]** Split `BadCrossModuleTypeRef` into two diagnostics —
the current single shape collapses unknown-owner and
known-owner / unknown-type-in-owner into one message. The two
cases suggest different fixes (add `(import <owner>)` vs. fix the
type name). In the known-owner branch, list the owner's available
type defs as candidates the way `bare-cross-module-type-ref`
lists candidates from imports.
- context: fieldtest 2026-05-11 — `examples/ct_3*.ail` exhibits both branches with identical-shape diagnostics.
- [ ] **\[todo\]** Zero-arg `(app f)` rejected at parse — the Form-A
parser refuses an application with an empty argument list, so a
nullary call has no surface form. Surfaced by the mut-local
fieldtest (F3) AND **independently re-confirmed by the loop/recur
fieldtest** (2026-05-18, finding spec_gap): the maximally-natural
infinite-event-loop shape is a niladic `run_forever : fn() -> Int`
called `(app run_forever)`, which dies at parse before reaching
loop/recur semantics. Two independent fieldtests hitting the same
gap raises the priority signal. Decide: accept `(app f)` as the
nullary-call surface, or ratify in DESIGN.md that nullary functions
are expressed differently (and say how). DESIGN.md's `Term::App`
`args:[Term...]` states no minimum, so the surface "expected at
least one argument" rule is unbacked by spec — a genuine design
fork, deliberately NOT auto-ratified under autonomous orchestration.
Not a blocker; no current corpus program needs it, but an LLM
author reaches for it.
- context: `docs/specs/2026-05-15-fieldtest-mut-local.md` finding F3;
`docs/specs/2026-05-18-fieldtest-loop-recur.md` spec_gap.
- [ ] **\[todo\]** Module-level `(doc …)` diagnostic omits where `doc`
belongs — a `(module NAME (doc "…") (fn …))` is correctly rejected
(`unknown def head \`doc\``) but the message lists valid def heads
without saying doc strings attach *inside* `fn`/`data` defs; an
author can read it and conclude doc strings are unsupported. One-line
tidy: append a hint like "(doc strings attach inside `fn`/`data`
defs, not at module level)". Low cost; verbose-diagnostic-philosophy
gap, not a bug.
- context: `docs/specs/2026-05-18-fieldtest-loop-recur.md` friction.
- [ ] **\[todo\]** Workspace search beyond entry-module's directory —
`load_workspace` only finds sibling `.ail.json` files in the same
directory as the entry module, so any consumer of prelude/std in a
subdirectory has no way to resolve cross-module imports. Add either
a `--workspace-root` flag on `ail check` / `ail build` / `ail run`,
or upward-search from the entry module's directory. Alternatively
ratify the flat-workspace assumption in DESIGN.md if intentional.
- context: fieldtest 2026-05-11 — fieldtest fixtures could not be
placed under `examples/fieldtest/` because of this; predates the
canonical-type-names milestone but surfaces every time.
- [ ] **\[todo\]** `*.bump_s` throughput baseline is stale vs current
hardware — `bench/check.py`'s `throughput.*.bump_s` family (the
fastest, most jitter-prone metrics) reads ~+513% over
`bench/baseline.json` on the current machine. Localised at the
iteration-discipline-revert audit (2026-05-16): an interleaved
3×60-run measurement of `bench_list_sum` bump_s built from the
byte-oracle commit `1ff7e81` shows the *same* ~+11% elevation as
HEAD, and the two bump binaries are `cmp`-identical — so this is
environmental drift relative to the 2026-05-09 baseline-capture
machine state, **not** a codegen regression. Re-capture the full
`*.bump_s` set on current hardware from a known-clean commit and
recalibrate the `bump_s` baseline+tolerance pair (or widen the
tolerance) so the noise floor stops tripping `check.py` exit 1.
Pure bench-harness recalibration; no language change.
- [ ] **\[todo\]** `check.py` RC-latency `*.max_us` is a structural
false-positive at `-n 5` — **distinct** from the `*.bump_s`
staleness above (different metric family, different root cause).
`latency.{explicit,implicit}_at_rc.max_us` is the single worst of
~5000 samples over only 5 runs, dominated by OS scheduling / THP /
IRQ jitter on a shared host, not allocator behaviour. It has fired
a false `REGRESSION` on **three consecutive no-runtime-change
milestones** (iteration-discipline-revert, prose-loop-binders,
remove-mut-var-assign) and vanished on identical-code re-run every
time (remove-mut-var-assign close: HEAD vs `48e7774` bench binaries
`cmp`-byte-identical, `explicit_at_rc.max_us` collapsed
+108%→-2.30% ok by rerun3) — a ~3-for-3 false-positive rate on null
changes, while the median/p99/p99.9 of the same benchmarks held
across all re-runs. Pick one mitigation: drop `*.max_us` from the
gating set (keep median/p99/p99.9, which are the trustworthy
signal), or raise `-n` substantially for the tail metrics, or pin
the latency arm to an isolated cpuset. Pure bench-harness change;
no language change.
- context: remove-mut-var-assign close audit, bencher localisation
(2026-05-18) — byte-identical-binary causal exoneration + the
3-milestone false-positive history.
- context: the bencher localisation evidence in the
iteration-discipline-revert audit commit.
- [x] **\[todo\]** `check_in_workspace` per-module overlay narrowing —
`crates/ailang-check/src/lib.rs:1234` still clears+rebuilds
`env.ctor_index` per-module; ct.3.2 narrowed the analogous mono
overlay to types-only. The typecheck-side overlay's `env.ctor_index`
half serves the duplicate-detection diagnostic at workspace-build
time, not the runtime ctor lookup (which is type-driven post-ct.2.2).
Narrowing is mechanical but needs a careful read to confirm no
other consumer survives.
- context: milestone close follow-up; closed by iter ctt.1 —
recon confirmed the rebuild is the load-bearing consumer of in-band DuplicateCtor (pinned by `crates/ailang-check/tests/duplicate_ctor_pin.rs`); the asymmetry with the mono side is intentional.
- [x] **\[feature\]** `str_concat : (borrow Str, borrow Str) -> Str` —
heap-Str concatenation primitive. Shipped 2026-05-13 as iter
str-concat (closes fieldtest-form-a friction #4). Symmetric to the
iter 24.1 `str_clone` / `int_to_str` / `bool_to_str` plumbing:
runtime C helper (`ailang_str_concat`), `ailang-check` builtin
registration, `ailang-codegen` extern + `lower_app` arm, plus a
fresh `examples/show_user_adt_with_label.ail` corpus fixture
exercising the LLM-natural Show-body shape.
- context: shipped in the str-concat iter commit.
- [~] **\[milestone\]** Stateful islands — bounded mutation for
streaming workloads (`Stateful a b` + `!Mut` effect + `mut`
syntactic block). Adds a sealed mutable-state layer on top of
the pure core: a `Stateful a b` first-class type whose interior
is mutable, whose exterior is a typed callable with `!Mut` in
its effect set, and whose state non-aliasing is enforced by
uniqueness inference at the block boundary. Targets the
online / streaming workload class — rolling indicators, IIR
filters, online aggregates, sensor fusion, online learning —
whose mathematics is "new sample + old state → new state +
output" per step, and which today's pure-functional state-
threading makes ergonomically expensive at scale (multi-record
explicit threading for the canonical sliding-window SMA, no
zero-allocation pipe combinator, growing tuple-state types as
pipelines lengthen).
**Sub-milestone sequencing is UNDER RE-THINK with the user
(2026-05-16) — no further sub-milestone is planned or brainstormed
until that conversation happens.** Why: the 2026-05-15 decomposition
named "(2) effect-handler infrastructure as a prerequisite for any
non-IO/non-Diverge effect" as the next step. A read-only recon of
the live effect subsystem (2026-05-16) showed that premise is false:
the effect raise / declared-set / `UndeclaredEffect`-subset / call-
propagation machinery is already fully generic over an arbitrary
effect string; `!Mut` needs **no** effect-handler machinery. The
only real code prerequisite the recon found is one hard-coded
assumption (`linearity.rs` walks every `Term::Do` arg as `Borrow`;
`EffectOpSig` has no per-arg mode field) — a single struct field,
not a milestone. A brainstorm spec that tried to make sub-ms-2 a
standalone deliverable bundled that speculative one-field capability
with an unrelated DESIGN.md honesty fix; the user correctly rejected
the bundle as incoherent and the build-ahead-of-consumer half as the
iteration-discipline trap repeated. **Resolution:** (a) the DESIGN.md
effect-honesty correction is split out and runs now as a standalone
documentation tidy (see the P2 `[todo]` "DESIGN.md effect-prose is
fiction" below); (b) the `arg_modes` finding is recorded as recon
context for the future `!Mut` design, NOT built ahead — it is
first-iteration material of whatever the `!Mut` milestone turns out
to be, validated there against a real consuming op, never a
synthetic test op; (c) the old "(2)…(5)" sequence (effect-handler
infra → `!Mut`+`ref a` → `MutArray a` → `Stateful a b`+`pipe`) is
no longer treated as settled — the whole ordering and granularity
is what the pending user conversation re-decides.
**Motivation.** AILang's signature-as-contract thesis is *better*
served by an explicit `Stateful a b` + `!Mut` annotation than by
the implicit-closure-mutation idiom of myc / Lua / JS factory
patterns: the signature tells the truth about time-identity
without the body needing to be read — exactly the LLM-author
correctness affordance AILang exists to deliver, extended to a
workload class it does not yet serve. Decision 10's constraint #3
forbids *shared* mutable refs (DESIGN.md:1082); it does not
forbid uniqueness-bounded mutation. Lean 4 (`ST`), Roc (`Task`),
Haskell (`ST`/`IO`), and Koka (effects) all use a layered design
of this shape to host exactly this workload. AILang's existing
`own` / `borrow` mode machinery plus its effect-slot architecture
are the foundation; this milestone supplies the layer that sits
on top.
**Scope (subject to brainstorm refinement).**
- `Stateful a b` as a first-class type — a sealed callable with a
hidden mutable env, externally a typed callable whose effect
set includes `!Mut`.
- `!Mut` effect, the first non-IO / non-Diverge effect; forces
the effect-handler infrastructure forward.
- `mut` block as the syntactic boundary in Form A — outside,
AILang's pure self; inside, `var` / `assign` / mutable arrays
legal. (Iteration is *not* part of this boundary: AILang has no
`while`/`for` and this milestone does not introduce one —
repetition stays recursion, exactly as the language has it today
(structural / tail recursion); a `mut` block is a sealed
expression, never a loop over mutable state.)
- `var x = expr` + `assign x expr` AST nodes, legal only inside
`mut`.
- Mutable array primitive (`MutArray a`, O(1) index/update under
uniqueness) — co-developed because the ring-buffer use case
that motivates the whole effort has no carrier today.
- `pipe : Stateful a b → Stateful b c → Stateful a c` as a
built-in combinator with zero-allocation lowering — composition
is fusion at codegen, not closure-pair layering at runtime.
- Decision 12 (or amendment to Decision 10) that names
uniqueness-bounded mutation as the legitimate exception to
"no shared mutable refs", and the pure / mutable-island layering
as the architectural shape.
**Blockers (hard prerequisites + open design questions).**
- *Effect handlers absent.* DESIGN.md:2663 — "No effect
handlers — only the built-in IO and Diverge ops." `!Mut` is the
first non-trivial effect and forces handler infrastructure to
ship. May warrant lifting out as its own prerequisite milestone;
brainstorm decides.
- *Uniqueness inference through `var` captures.* Current
inference operates over the immutable RC graph; mutable
bindings captured by closures need a more powerful pass. Lean 4
has a known algorithm; AILang does not implement it.
- *No mutable-array primitive.* No `Array a`, no slab container
of any kind in the language today. Separate spec needed inside
this milestone (representation, `own`-only or `borrow`-able,
bounds-checking discipline).
- *`runST`-equivalent escape discharge.* Producing a `Stateful`
that legitimately escapes its `mut` block while proving the
inner state doesn't leak. Haskell's rank-2 trick
(`runST :: (forall s. ST s a) -> a`) is unavailable —
DESIGN.md:1930 confirms higher-rank polymorphism is not
supported. Need an alternative — likely sealed-by-construction
at the factory boundary, or an effect-mode tag that gates
escape.
- *Form A surface design.* Decision 1 forbids macros (source =
data, not text), so `mut` / `var` / `assign` are genuine AST
nodes with round-trip-invariant coverage. Surface needs LLM-
utility validation (does the author reach for `var` / `mut`
unprompted?) before implementation.
- *Codegen for in-place struct-field writes.* `Term::ReuseAs`
today lowers ADT in-place rewrite; no direct mutable struct-
field-write path exists. `crates/ailang-codegen/src/escape.rs`
plus the lowering passes need extension.
- *Boundary escape analysis.* "Interior state doesn't leak" is an
escape analysis specifically over `var` / `ref` values; the
existing pass covers allocations, not mutable cells.
- *Decision-10 status.* Whether to amend Decision 10 inline or
add a Decision 12 that names the mutable-island layer alongside
the pure layer. The latter is probably cleaner — Decision 10
stays load-bearing for the pure layer, Decision 12 names the
extension and its discipline.
- *Streaming bench corpus.* Current corpus (list_sum, tree_walk,
closure_chain, hof_pipeline) is all pure. Streaming-specific
benches (SMA pipeline, IIR filter, online stats) plus an
external baseline (myc, hand-C, Python/NumPy) need to exist to
validate that the layered design hits the zero-alloc
performance target that myc demonstrates.
- *LLM-utility test.* DESIGN.md §"Feature-acceptance criterion"
is the gate: the surface must produce code an LLM author
naturally writes. Fieldtest after spec, before any code
commits.
- context: 2026-05-15 chat on the `~/sma_factory.myc` analysis —
myc's stateful-closure-plus-pipe idiom delivers a streaming
workload pattern (3-line SMA factory, 2-line pipeline, zero
runtime allocation per tick) that AILang's pure-only model
cannot match without this layered extension. Pending brainstorm
will produce `docs/specs/<date>-stateful-islands.md` and decide
the effect-handler-prerequisite question.
## P3 — Ideas
- [x] **\[todo\]** `compare_primitives_smoke.ail` counterpart.
Satisfied 2026-05-13 by milestone form-a-default-authoring —
`examples/compare_primitives_smoke.ail` is the canonical
authoring form for that fixture. Form A is now the default
authoring surface across the entire corpus.
- context: closed incidentally by form-a iter form-a.1.
- [ ] **\[todo\]** Codegen `lookup_ctor_in_pattern` type-anchoring —
`crates/ailang-codegen/src/lib.rs:1790` still walks every module's
ctor_index by bare ctor name. Plumbing the scrutinee's
qualified `Type::Con` through pattern lowering would type-anchor
it symmetric to the ct.2.2 typecheck-side fix. Not load-bearing
(uniqueness is enforced at typecheck), but cleaner.
- context: surfaced in iter ct.3 + ct.4 close (2026-05-11).
- [ ] **\[todo\]** `compare_primitives_smoke` IR-shape assertion —
observe `compare__Int` / `compare__Bool` / `compare__Str` symbols
in the emitted IR. Blocked on `emit-ir` CLI not running mono;
resolution paths include extending the CLI to run mono, using
library APIs directly in e2e.rs, or adding `--dump-ir` to `ail
build`. The E2E stdout assertion already covers correctness; the
IR-shape test would catch refactor regressions that rename
mono symbols.
- context: dropped from ct.4.4 when the BLOCKED condition surfaced.
- [ ] **\[idea\]** Latency methodology rework — switch from per-run
timing to a histogram-based approach so tail-latency regressions
are visible without re-running. Queued from 21'g.
- [ ] **\[idea\]** Parser-test backfill for 22b.4a-era duplicate-clause
sites (class × 3, class method × 2, instance × 3, instance method
× 1). No regression pin today; only worth it if a 22b.4a-era
diagnostic needs to change.
- context: surfaced in iter 22-tidy.7 (2026-05-10).
- [ ] **\[idea\]** `write_type` `Type::Forall` arm in
`crates/ailang-prose/src/lib.rs` silently drops constraints in
inline-type rendering. Dormant — surface forms today only carry
forall at fn-signature top level. Fix only if a future feature
carries forall + constraints inline.
- context: surfaced in iter 22-tidy.6 (2026-05-10).
- [ ] **\[idea\]** Richer integration paths between RC and
uniqueness — deferred from the 21' arc; revisit once the
uniqueness inference covers more program shapes.
- [ ] **\[idea\]** LLM tool-use schema / MCP server / LSP front-end
over the prose-roundtrip cycle — additive layers on top of the
static-prompt `ail merge-prose | client | ail parse | ail check`
path (LLM calls back into `ail parse`/`ail check` mid-turn; an
MCP-compatible client discovers AILang's resources/tools/prompts).
Relocated here from DESIGN.md §"prose roundtrip" + PROSE_ROUNDTRIP.md
by the docs-honesty-lint milestone (forward intent does not live in
the canonical docs).
- context: `docs/specs/2026-05-18-docs-honesty-lint.md`
- [ ] **\[todo\]** Sweep 5 self-match anchor-exclusion — `bench/architect_sweeps.sh`
Sweep 5's regex matches the DESIGN.md discriminator subsection's own
forbidden-phrasing catalogue (L62 `"planned / will back / on the
path to / if-when X arrives"`) forever, so the script inherits a
guaranteed self-referential EXIT 1 the architect must re-adjudicate
by hand each run. Not a new failure mode (Sweeps 1-4 already retain
legitimate date-anchors; non-zero is advisory-by-design per the
script header) — minor signal-erosion only. Optional fix: a
negative-lookbehind / line-range exclusion so the rule's own example
list is not a hit. P3 — may be cut; the architect adjudicates the
single known self-match trivially today.
- context: docs-honesty-lint audit commit (architect `[medium]`
advisory wart, carry-on).