# 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/<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 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

_(empty — canonical-type-names milestone closed 2026-05-11; Floats
closed 2026-05-10 and was removed once it stopped being load-bearing
context. Pick the next milestone from P1.)_

## 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

- [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.

- [ ] **\[milestone\]** Prelude embed: Form-A as compile-time source.
  Retire `examples/prelude.ail.json` by switching the
  `include_str!`-then-`serde_json::from_str` embed sites in
  `crates/ailang-core/src/workspace.rs:417` and
  `crates/ail/src/main.rs:474` to consume `examples/prelude.ail`
  at compile time, parsed via `ailang_surface::parse`. After this
  milestone the only surviving `.ail.json` carve-outs are the
  seven subject-matter rejection fixtures (§C4 (a) of the
  form-a-default-authoring spec); §C4 (b) becomes empty and is
  removed from the spec.

  **Motivation.** The form-a-default-authoring milestone (just
  closed) made `.ail` the authoring surface for every program in
  the corpus, with one exception: the prelude. The prelude is
  the language's most-read file and the canonical demonstration
  of every language feature stacked into one module; that it is
  the *one* file the user authors as `.ail.json` is a doctrine
  inconsistency relative to CLAUDE.md "authors write `.ail`; the
  build derives the JSON-AST in-process via `ailang_surface::parse`".

  **Problem.** `ailang-core` consumes the prelude at compile time
  via `include_str!("../../../examples/prelude.ail.json")` plus
  `serde_json::from_str`. Switching the embed to `prelude.ail`
  requires parsing via `ailang_surface::parse`, but `ailang-core`
  cannot import `ailang-surface` (the crate-cycle constraint
  documented at `crates/ailang-surface/src/loader.rs:9-13` —
  `ailang-surface` depends on `ailang-core` for the AST types).
  The same embed exists at `crates/ail/src/main.rs:474`; that
  site *can* import `ailang-surface` but is in lockstep with the
  `ailang-core` one (both must reproduce the same `Module` value
  for prelude semantics to be uniform between CLI dispatch and
  workspace loading).

  **Resolution options** (decide at brainstorm time):
  - (β) Split the workspace loader: move the prelude-injection
    step from `ailang-core::load_workspace` to a new
    `ailang-surface::load_workspace_with_prelude` (or wrapper).
    `ailang-core` retains a prelude-free workspace primitive;
    `ailang-surface` is the entry point that injects the parsed
    prelude. Substantive refactor of ~3-5 sites.
  - (γ) `build.rs` generates `examples/prelude.ail.json` at build
    time from `examples/prelude.ail` via either an in-tree parser
    (would re-create a similar crate-graph issue) or a snapshot
    test that fails on drift. Avoids the loader-split but introduces
    a build-time artefact and a bootstrap concern (the `ail` binary
    itself is built from `ailang-core`).
  - (δ) Some hybrid: prelude stays canonical as `.ail.json` in
    storage but is rendered to `prelude.ail` at every commit by a
    pre-commit hook; the dual-form is preserved as a lockstep
    invariant rather than retired. Trades one inconsistency for
    another but is the smallest change.

  **Carve-out today.** Per spec §C4 (b) (added 2026-05-13),
  `prelude.ail.json` is the sole compile-time-embed carve-out
  during the form-a-default-authoring milestone. `examples/prelude.ail`
  already exists alongside it (rendered in iter form-a.0) as the
  authoring surface; the dual-form for prelude is the symptom this
  milestone exists to retire.

  - context: surfaced 2026-05-13 during iter form-a.1 recon; the
    crate-cycle issue was not anticipated by the spec authors and
    forced a brainstorm-level amendment to §C4. The fix exceeds
    the form-a-default-authoring milestone's scope and is
    sequenced as its own milestone to keep the architectural
    change auditable in isolation.

- [ ] **\[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<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: 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 `<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: 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.

- [ ] **\[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\]** `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 <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\]** 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.
- [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.

## 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.
