Anthropic now reserves /plan as a UI command, so the Skill tool refuses to
dispatch it. Rename the project's plan skill to planner, update the symlink
under .claude/skills/, and adjust references in CLAUDE.md, DESIGN.md,
skills/README.md, and the cross-references between brainstorm / implement /
audit / fieldtest / fieldtester. Plan files themselves (docs/plans/*.md)
keep their name — only the skill ID changes.
Hypothesis-driven measurement of "did monomorphisation actually
buy us performance?" on a 100M-iter LCG hot loop, AILang mono'd
code vs. four C reference variants (direct-inlinable, direct-
noinline, indirect-monomorphic, indirect-polymorphic). Zen 3,
clang -O2, median-of-15.
Headline: H1 supported, but the mechanism is inlining, not
dispatch shape. AILang mono = hand-C direct (1.000x). Indirect-
monomorphic = direct-noinline (1.000x) — saturating branch
predictor makes the indirect-call cost vanish on this hardware.
Inlining is the actual 3.31x win; polymorphic indirect adds
another 21% predictor-miss penalty.
DESIGN.md Decision 11 gains a rationale paragraph reframing mono
as inlining-enabler rather than indirect-call-eliminator, with
explicit pointer to the bench. JOURNAL entry records the full
methodology, ratios, limitations, and the side-effect mono-pass
env.globals-seeding bug surfaced while building the AILang fixture
(separate RED-first debug iter to follow).
Adds DESIGN.md "Feature-acceptance criterion" as a top-level section:
a feature ships only if (1) an LLM author naturally produces code
that uses it, and (2) it measurably improves correctness or removes
redundancy. Aesthetic appeal and human ergonomics do not count.
Mirrored in CLAUDE.md as "Feature acceptance: LLM utility", paired
with the existing "Design rationale != implementation effort". The
two together narrow valid feature rationales to one thing: what the
LLM author gets out of the feature.
Trigger: the typeclass-design conversation around 22a. Rule was
implicit in many past decisions (Decision 1's JSON-over-text choice,
Decision 10's "what LLMs are good at" reasoning) but never stated
as a feature-gate. Codifying it now means future feature proposals
get evaluated against an articulated criterion instead of being
re-derived each time.
Documentation-only; no Rust, schema, or bench changes. Test state
288/0/3 unchanged.
Three drift items from ailang-architect, plus one false-positive
surfaced during verification:
1. DESIGN.md silent on closure-pair 4.14x finding (21'b).
Decision-10 ratified: "Workload scope of the 1.3x target"
paragraph scopes the retirement gate to linear/tree/poly-ADT
workloads; closure-pair carve-out documented as
representational cost (closure cell + env struct = 2 allocs
per step) until a slab/pool answer ships.
2. bench/compile_check.py corpus drift. Three fixtures added
(bench_compute_intsum, bench_compute_collatz,
bench_list_sum_explicit), re-baselined. Now 12 fixtures x
2 ops = 24 compile-time metrics.
3. baseline.json convention not codified. Note field gains
"max-of-distribution gets wider band than percentile"
convention discovered in 21'd.
4. (verification finding) bench_compute_intsum cross_lang
tolerances at 15%/12% fire on subprocess-spawn jitter for
sub-millisecond fixtures. Widened to 35% across all five
intsum metrics; convention recorded in baseline_cross_lang
note field (sub-ms fixtures need looser bands).
All three bench gates re-run sequentially after edits:
bench/check.py — 63 metrics; 63 stable
bench/compile_check.py — 24 metrics; 24 stable
bench/cross_lang.py — 25 metrics; 25 stable
Total: 112 metrics under regression coverage, all green.
288 tests passing, 3 ignored. No Rust changes.
The dual-allocator policy from 2026-05-08 made Boehm the CLI default
"for general workloads", with rc selected explicitly for real-time-
sensitive code. That asymmetry contradicted Decision 10: rc + uniqueness
inference is the canonical memory model AILang's typechecker enforces,
and the CLI was teaching the opposite mental model to users and prompt-
fed LLMs.
This iter flips the asymmetry without removing Boehm:
- main.rs: default_value gc -> rc for build/run; help text rewritten
so rc is the canonical path and gc is the parity oracle.
- DESIGN.md Decision 9 retitled "RC canonical, Boehm parity oracle"
and rewritten end-to-end. Migration plan step 6 updated to a
two-step retirement (default-flip done, full removal gated).
- DESIGN.md pipeline diagram: rc-first, gc-as-oracle.
- JOURNAL: 2026-05-09 entry recording the call, what shipped,
the bug the flip surfaced, the gating condition for full
Boehm removal.
Bug surfaced by the flip and fixed in the same iter:
codegen/drop.rs build_pair_drop_fn emitted `getelementptr inbounds
{{ ptr, ptr }}, ...` via push_str (not format!), so the doubled
braces leaked verbatim into the IR. LLVM parsed it as a struct-of-
struct, the GEP referenced a non-existent field, and clang failed.
Invisible under the old gc default (no per-type drop fns) and
invisible to the 8 explicit _with_alloc("rc") tests (none of them
escaped a closure-with-captures). Caught immediately by two corpus
tests once rc became the baseline: closure_captures_let_n,
local_rec_as_value_capture_demo. Fix: single braces. Tests stay
as the regression guard.
The episode is the canonical justification for keeping the GC arm
as a parity oracle for now — a months-old codegen bug found by
flipping the baseline column.
288 passed / 0 failed / 3 ignored, unchanged.
Architect drift review after Family 20 surfaced four items; (1) and
(2) are doc drift fixed here, (3) and (4) are recorded as acceptable.
DESIGN.md changes:
- Data model section rewritten from "Data model (MVP)" claiming
only fn/const exist to "Data model" reflecting all three Def
kinds. Added the missing schema fields the architect named:
`tail` flag on app/do, the `seq` / `clone` / `reuse-as` / `letrec`
terms, `paramModes` / `retMode` on Type::Fn, `suppress` on FnDef,
`drop-iterative` on TypeDef, `args` on Type::Con, `vars` on
TypeDef. Each additive field's hash-stable serialisation is
called out once at the top so individual mentions stay terse.
`Suppress`, `ParamMode`, `Literal`, `Pattern` get their own
sub-blocks.
- Pipeline diagram now shows `--alloc=gc` (links libgc;
transitional) and `--alloc=rc` (emits ailang_rc_inc / _dec;
canonical) as two backends sharing the same MIR.
- Ecosystem inventory gained a "Surface forms" bullet for
ailang-surface (Iter 14c, lossless Form-A printer/parser) and
ailang-prose (Family 20, lossy Form-B projection). CLI bullet
now lists parse, render, prose, merge-prose.
JOURNAL entry records the architect findings, the resolutions for
items (1) and (2), and the acceptable-drift status of (3)
FnDef::synthetic factor-out (defer to next schema-additive
FnDef field) and (4) linearity.rs spurious warning (not
corpus-triggered).
Pure documentation iter — no code changes. 288 workspace tests
unchanged and green.
Closes a design hole shipped in 20d: the merge-prose prompt instructed
the LLM to emit JSON-AST and gave a 12-line schema-essentials reminder.
JSON-AST is the canonical hashable artefact, not a writing surface; the
reminder was not a language spec. Foreign LLMs had no realistic shot.
20f makes two coupled changes:
1) The LLM now emits Form-A (the canonical authoring surface fixed by
Decision 6). merge-prose loads the original via ailang_core::load_module
and re-renders via ailang_surface::print before embedding (round-trip
is a gating contract on the surface crate, so this is lossless). The
user runs `ail parse foo.new.ailx` to recover JSON, then `ail check`.
2) crates/ailang-core/specs/form_a.md is the complete LLM-targeted
Form-A specification — grammar, every term/pattern/type/def keyword,
schema invariants, pitfall catalogue, four few-shot modules from
examples/*.ailx. Exported as ailang_core::FORM_A_SPEC via include_str!
and embedded verbatim in every merge-prose prompt.
Drift detection in crates/ailang-core/tests/spec_drift.rs: every variant
of Term, Pattern, Type, Def, Literal is reached via exhaustive `match`.
The arms are not the assertion — adding a new variant without updating
the match is a compile error before the test runs. Once matched, an
anchor string is asserted to appear in FORM_A_SPEC. 8 tests, all green.
The hand-written + mechanical-drift-test combo addresses the user's
"distance to code is too big" concern about a docs-only spec. Generator
overkill rejected: structural drift is mechanically caught, but prose
explanation, schema-invariant catalogue, pitfall list, and few-shot
corpus cannot be emitted from AST shape alone.
Tests:
- ailang-core: +8 spec_drift tests; existing 12 unchanged
- ail unit (3): rewritten in lockstep — assert (own T)/(effects IO)
landmarks, FORM-A SPECIFICATION header, FORM_A_SPEC body verbatim
- ail e2e merge_prose_prints_framed_prompt: rewritten — assert
`(module foo` + `FORM-A SPECIFICATION` instead of `ailang/v0`
- Workspace: all green
Richer integration paths (LLM tool-use, MCP server, LSP) were named
in the design discussion and deferred per "kiss". All three layer
additively on the static-prompt path; static prompt remains the
lowest-common-denominator fallback.
Per CLAUDE.md tidy-iter at family boundaries. ailang-architect
flagged the canonical 'DESIGN.md silent' finding for the entire
19a-arc — same shape as family-20's 20e tidy.
Decision 10 schema-additions block gained 'Iter 19b —
FnDef.suppress' entry. New subsection 'Advisory diagnostics —
Iter 19a-arc' documents the over-strict-mode lint rule (incl.
heap-type filter rationale), Severity::Warning introduction, CLI
exit-on-Error gating, and the suppress mechanism. New subsection
'Why advisory + suppress instead of inference' pins the three
substantive reasons (annotation-states-intent, drift-bremse,
LLM-authoring forcing function) so future iters can't reopen the
decision without engaging the recorded rationale.
Migration plan extended with step 7 covering 19a/19a.1/19b.
Decision 10's mandatory-annotation rule is explicitly reaffirmed
as unchanged.
No code changes. Two architect findings deferred (codegen FnDef
fan-out, snapshot-fixture coupling). Data model (MVP) drift
predates 19b — separate iter.
Family-20 tidy-iter: ailang-architect drift review surfaced
DESIGN.md silence on the new prose surface as the highest-leverage
gap. Decision 6 now has a 'Form (B) — human prose projection'
subsection naming the lossy-vs-lossless contract, the no-parser-
by-design choice, and the LLM-mediated round-trip. CLI block
gained 'ail prose' and 'ail merge-prose'.
No code changes. Form (B) does not weaken any Decision 6
invariant: JSON-AST remains the only hashable artefact, form (A)
the canonical authoring surface, 30-production grammar unchanged.
Two architect findings deferred (prompt-template lockstep test;
snapshot-fixture cross-family coupling). Family 20 closes.
JOURNAL queue: empty.
The 18-arc validated the RC pipeline end-to-end; the orchestrator's
call (JOURNAL 2026-05-08) was to keep Boehm as default and RC as
the ready alternative rather than retire Boehm. Decision 9's
'transitional Boehm' framing was wrong shape for that policy —
this edit re-casts it as 'dual allocator: Boehm GC for general
workloads, RC for real-time-sensitive workloads', preserving the
rest of the section.
Decision 10 holds as the specification of the RC alternative; this
edit only changes the framing of how the two allocators relate.
Resolves the architect's drift report on the 18g sub-arc:
- DESIGN.md: ratify mode-metadata's codegen role. param_modes /
ret_mode were already on Type::Fn since 18a; with 18d.4 / 18g
they became load-bearing for drop-emission decisions in
codegen. The new 'Mode metadata is load-bearing for codegen'
subsection records the four seams (Iter A + Iter B + 18g.1 +
18g.2) and names the let-alias-of-borrow carve-out the gates
do not propagate through.
- bench/run.sh: post-throughput, invoke bench/latency_harness.py
on the three canonical arms (Implicit @ gc, explicit @ rc,
Implicit @ rc control). The Boehm-retirement bench numbers
are now reproducible by anyone running the harness, not just
by hand on a specific host.
- Negative coverage: examples/rc_let_implicit_returning_app
+ alloc_rc_let_binder_for_implicit_returning_app_does_not_drop
pin the asymmetry to the (own)-ret-mode test (live=0 there,
live=1 here). The Borrow-ret-mode case is covered by the
language design itself — typechecker rejects 'borrow-
passthrough' shapes with consume-while-borrowed.
JOURNAL entry records four items as deferred known debt:
emit_inlined_partial_drop dynamic-tag fallback, carve-out
diagnostics surface, bench-number stat-of-N, and the
cross-family ordering observation about CLAUDE.md's tidy-iter
rule.
The 18-arc is formally closed with this tidy. Next iter is
the orchestrator's Boehm-retirement decision (Path A vs Path B
from JOURNAL 2026-05-08 18f entry, joined by the post-18g.2
re-bench numbers).
The schema sketch in Decision 10 already committed to
Term::ReuseAs { source, body } as a wrapper, but did not record
WHY the wrapper form was chosen over a reuse_from: Option<String>
modifier on Term::Ctor. Adds the two substantive reasons:
- Compositional flexibility: wrapper generalises to record
literals, opaque box wrappers, capability cells — anything
future iters might add as an allocating construct. Modifier
would replicate per Term variant.
- Source-locality: (reuse-as SRC NEW-CTOR) reads as a sentence
with the source-binder at the head. Modifier scatters intent.
The trade-off accepted: schema permits Term::ReuseAs around a
non-allocating body. Caught at typecheck via
reuse-as-non-allocating-body diagnostic. Same shape as
Decision 7's Term::If composability accepts.
Recording before 18d.1 ships — per CLAUDE.md "design rationale
ne implementation effort", a schema choice has to name its
substantive reasons before code is written, not retroactively.
User correction during the post-RC-overnight check-in: the 18a
"Type::Fn metadata vs. new Type variant" call was justified
across DESIGN.md, JOURNAL, and the commit message primarily by
"avoids ~250 match-arm sites". That is an observation about the
current state of the code, not a design rationale.
CLAUDE.md gains two binding rules:
- Design rationale ≠ implementation effort. Effort is at most a
tiebreaker; a choice whose only stated reason is effort is
suspect. The rule names the 18a misstep as the canonical
anti-example so future sessions catch it earlier.
- Direction freedom + bounce-back conditions inlined (was
previously a cross-reference to a private auto-memory file
outside the repo, which the user couldn't see).
DESIGN.md Decision 10's Schema-additions block now leads with
the substantive reasons for per-position metadata: semantic
locality (modes belong to fn-parameter positions, not to types
in general — Decision 1 line); compositional clarity (type
identity vs. calling convention factor apart); future-proofing
(per-position metadata generalises; Type-variant approach
combinatoric blows up). The match-arm count remains parenthetical,
explicitly named a tiebreaker.
JOURNAL records the correction itself — the mistake stays as a
data point because how design discipline corrupts is informative.
Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:
Type::Fn { params, param_modes, ret, ret_mode, effects }
enum ParamMode { Implicit, Own, Borrow }
This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).
Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.
Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.
New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.
Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.
Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
Avoids the ~190 Type match-arms in the typechecker that a new
Type variant would require. ParamMode enum (Implicit | Own |
Borrow) attaches per-param to Type::Fn, with serde defaults so
existing fixture hashes stay bit-identical.
Replaces the earlier "RC + inference, no annotations" position
with a two-layer architecture: inference (intra-fn) + mandatory
LLM-author mode annotations on fn signatures (inter-fn). Adds
structured rationale for rejecting region inference (regions
assume stack-shaped lifetimes; real programs need
non-stack-shaped lifetimes for caches / memo tables) and linear
types as primary mechanism.
Schema additions enumerated for the 18-series:
Type::Borrow / Type::Own (Iter 18a)
Term::Clone (Iter 18c)
Term::ReuseAs (Iter 18d)
TypeDef.drop_iterative (Iter 18e)
Migration plan extended from 18a-d (4 iters) to 18a-f (6 iters).
JOURNAL records the regions excursion, the scope-shape reduction,
and the LLM-author lever as the structural advantage Decision 10
cashes in on.