# 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, a branch is open, or commits are landing); `- [x]` is done. A finished entry may stay briefly for context, then is removed (with a one-line mirror in `docs/journals/`). - Each entry is tagged by **kind** and lives under a **priority** bucket: - **\[milestone\]** — big chunk that will get a `docs/specs/.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 journal entry (per-iter file under `docs/journals/` or, for pre-2026-05-11 entries, the archived `docs/journal-archive.md`) where the rationale lives. The roadmap is intentionally terse; rationale stays in the journals. - 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 `docs/journals/INDEX.md`). - context: `docs/specs/2026-05-18-remove-mut-var-assign.md`; `docs/specs/2026-05-18-fieldtest-remove-mut-var-assign.md`; `docs/journals/2026-05-18-iter-remove-mut-var-assign.1.md`; `docs/journals/2026-05-18-audit-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`; `docs/journals/2026-05-18-audit-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 `docs/journals/INDEX.md`). - 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. - [~] **\[milestone\]** DESIGN.md / docs honesty lint — spec'd + approved 2026-05-18 (`aff25cd`), open as P0 in-flight; dependency (Remove `mut`/`var`/`assign`) closed 2026-05-18; next `planner`. Sweep the canonical docs so they mirror ONLY the actual current state, present-tense. Single iteration (mirror effect-doc-honesty): a two-pronged tense+modality discriminator codified present-tense as a DESIGN.md meta-subsection (Approach A); procedural enumeration = discriminator applied to the full `architect_sweeps.sh` + new Sweep 5 output (not a frozen list — the sweep already surfaces post-mortem sites the architect waved through as legitimate quotes); anti-regrowth = enumerated `docs_honesty_pin.rs` (hard cargo-test gate, incl. protected-exception present-anchors against over-correction) + Sweep 5 + `ailang-architect.md` "DESIGN.md honesty drift" bullet (advisory standing forward check). No language/checker/codegen/runtime change. - context: `docs/specs/2026-05-18-docs-honesty-lint.md` (grounding-check PASS 10/10); 2026-05-18 user directive; reserved-vs-vapour precedent `docs/journals/2026-05-16-iter-effect-doc-honesty.md` ## P1 — Next - [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`; per-iter journals 2026-05-12-iter-24.1, 2026-05-13-iter-24.2, 2026-05-13-iter-24.3. ## P2 — Medium-term - [ ] **\[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) and `docs/journals/2026-05-15-iter-it.3.md` (the branching-builder counter-example that surfaced the gap). - [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: per-iter journal `docs/journals/2026-05-14-iter-rpe.1.md`. - [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: JOURNAL 2026-05-09 - 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: JOURNAL 2026-05-10 ("Audit close: env-construction unify"); 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: per-iter journal `docs/journals/2026-05-14-iter-cli-diag-human.md`. - [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: JOURNAL 2026-05-11 ("Iteration ct.1"); 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` 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: JOURNAL 2026-05-11 ("Iteration ct.1") — flagged by 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: JOURNAL 2026-05-09 - [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 `..` 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: JOURNAL pre-22, "Boehm transitional" - [ ] **\[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: per-iter journal `docs/journals/2026-05-16-iter-effect-doc-honesty.md`. - [ ] **\[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: JOURNAL 2026-05-10 ("Audit close: design-md-consolidation"). - [ ] **\[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: JOURNAL 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 )` 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 ~+5–13% 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: `docs/journals/2026-05-16-audit-iteration-discipline-revert.md` (the bencher localisation evidence). - [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: JOURNAL 2026-05-11 ("Iteration ct.4") — 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: per-iter journal 2026-05-13-iter-str-concat.md. - [~] **\[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/-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: JOURNAL 2026-05-11 ("Iteration ct.3" + ct.4 close). - [ ] **\[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: JOURNAL 2026-05-11 ("Iteration ct.4") — 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: JOURNAL 2026-05-10 ("Iteration 22-tidy.7") - [ ] **\[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: JOURNAL 2026-05-10 ("Iteration 22-tidy.6") - [ ] **\[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`