Post-audit downstream-LLM-author field test of the shipped
loop/recur surface (DESIGN.md + public examples only). 3 real
iterative programs (Newton isqrt, Collatz, Euclidean gcd) + 5
plausible-mistake negatives + 2 no-termination probes, all run
through the public ail CLI. 0 bugs. 4 working findings on the
milestone's own axes: rejection diagnostics point-exact AND
self-fixing; recur tail-position threads through match/let/outer-if
(spec only showed if); loop composes as a value sub-expression +
byte-stable round-trip; no-termination boundary exact. This
empirically substantiates the "LLM author can now write iterative
programs" claim.
Two orthogonal non-blocking findings, neither in loop/recur scope,
both routed to P2 todos (refused the scope creep into a loop/recur
tidy): niladic (app f) spec_gap independently re-confirms the
existing mut-local-F3 roadmap item (the design-fork decision
deliberately NOT auto-ratified under /boss — parked,
priority-strengthened); module-level (doc) diagnostic-hint friction
(one-line tidy). Boss-verified independently (gcd->27;
recur-outside-loop fires exact).
The standalone loop/recur milestone is fully ratified and CLOSED:
3 iterations + tidy shipped, audit clean (drift resolved, bench
pristine carry-on), fieldtest clean on every axis. Roadmap P0
marked closed.
GREEN side of the RED audit-trail commit 39380d3. The loop/recur
milestone-close audit found a [high] correctness defect — a lambda
capturing a loop binder passed `ail check` then panicked
unreachable!() at codegen (crates/ailang-codegen/src/lambda.rs:102)
on type-correct input. Fixed symmetrically to mut.4-tidy: new
CheckError::LoopBinderCapturedByLambda (code
loop-binder-captured-by-lambda, bracket-free F2 Display), the
Term::Lam escape guard gained a parallel loop_stack pass. Minimal
mechanism: loop_stack element Vec<Type> -> Vec<(String,Type)> so
the already-threaded per-loop frame carries binder names; recur
still reads .1 by position (Boss-call-2 positional invariant
preserved verbatim — the name is a second field for the escape
guard only, a consumer iter-2 did not anticipate). Codegen
byte-unchanged (typecheck-only fix). carve_out 17->18 + ct1-F2 +
DESIGN.md note lockstep.
Folded in (Boss-side, audit resolution): the two [medium]
doc-honesty edits the audit surfaced (mut_var_allocas rustdoc now
states its mut-var+loop-binder dual use; Term::Loop doc-comment
now describes the real shipped state, not iter-1's stale
"per-binder phi" forward-look) + a [low] P2 roadmap todo
(plan-recon undercount countermeasure, pairs with planner Step-5
items 7+8). Bench gate: all 3 scripts exit 0, 25 metrics 0
regressed — pristine, carry-on (no baseline/ratify).
cargo test --workspace 619 -> 622 / 0 red (Boss-reran
independently, hash_pin 11/0). The loop/recur milestone is
audit-clean; fieldtest is the only remaining step before close.
Third and terminal iteration of the standalone loop/recur
milestone (plan eae73bf). Replaces the iter-1 lower_term
CodegenError::Internal stub for Term::Loop/Term::Recur with real
LLVM-IR lowering: loop binders as entry-block allocas (mut.3
pending_entry_allocas, reusing mut_var_allocas so the existing
Term::Var load path is byte-unchanged), a fresh loop-header block,
recur stores + back-edge br, a loop_frames stack saved/restored at
the lambda boundary. clang -O2 mem2reg promotes the allocas to
phi. Four Boss design calls implemented verbatim and journalled
(alloca-not-hand-phi; mut_var_allocas reuse; emergent loop-exit
via the if/match join; single block_terminated field + parallel
SET site). Diff confirmed surgical: codegen/lib.rs 3 hunks (field
+ init + stub->2-arms), lambda.rs 2 hunks (save+restore); zero
edits to any existing block_terminated SET/READ site, tail-app
lowering, or verify_tail_positions (Boss call 4, the spec-pinned
invariant). Three new .ail fixtures: sum_to->55,
deep-n 1e6->500000500000 (clause-2 correctness made executable),
infinite-loop build-only. Codegen-only: no schema/typecheck
change; hash pins + drift trio stay green untouched.
One DONE_WITH_CONCERNS (T3): the plan's `cargo test ... tail`
filter resolved to no tests; ran via real names + the full 619/0
which subsumes it (feedback_plan_pseudo_vs_reality class, no
behaviour change). Boss systemic fix folded in: planner SKILL.md
Step-5 gains item 8 (verification-command filter strings must
resolve) — the second planner-meta-gap this milestone surfaced.
cargo test --workspace 616 -> 619 / 0 red (Boss-reran
independently); the 3 loop/recur e2e explicitly green. All three
components shipped: the loop/recur milestone is structurally
complete. Milestone-close audit + fieldtest is the next step.
Terminal iteration of the standalone loop/recur milestone.
Replaces the iter-1 lower_term CodegenError::Internal stub with
real LLVM-IR lowering: loop binders as entry-block allocas (the
mut.3 pending_entry_allocas mechanism, reusing mut_var_allocas so
the existing Term::Var load path needs zero new code), a fresh
loop-header block reached by an unconditional br, recur stores +
back-edge br, a new loop_frames codegen stack saved/restored at
the lambda boundary (mut.3 triple precedent). Four Boss design
calls baked into the plan header, all on architectural-consistency
/ spec-pinned-invariant grounds (not effort): (1) alloca + clang
-O2 mem2reg, NOT hand-emitted phi (no linear-emit phi precedent;
spec's "phi" is the secondary impl-shape; mem2reg gives identical
optimized output); (2) reuse mut_var_allocas + the existing
Term::Var load path (Var-lowering byte-unchanged; representation-
sharing only); (3) loop-exit value is the emergent product of the
existing if/match join once recur sets block_terminated, no
separate result phi; (4) reuse the single block_terminated field,
recur sets it at its own emit site (parallel setter), zero edits
to existing SET/READ sites (tail-app byte-unchanged). Three new
.ail fixtures: sum_to->55 run, deep-n 1e6->500000500000 (clause-2
correctness made executable), infinite-loop build-only. iter-3 is
codegen-only: no schema/typecheck change, hash pins + drift trio
stay green untouched. Self-review item 7 (the systemic fix folded
into iter-2) applied: T1's field-add is not a fn-signature change,
no deferred-caller-past-gate.
Second of three iterations of the standalone loop/recur milestone
(plan 5ac57fe). Replaces the iter-1 synth CheckError::Internal stub
for Term::Loop/Term::Recur with real binder typing + positional
recur arity/type checking via a new loop_stack: &mut Vec<Vec<Type>>
frame threaded as mut.2's mut_scope_stack, a new private
verify_loop_body tail-position pass (sibling of the byte-frozen
verify_tail_positions — 0 deletions there), and the four Recur*
diagnostics firing point-exactly on four negative fixtures. The
iter-1 loop_sum_to.ail fixture now also typechecks clean; an
infinite loop typechecks (no termination claim). NO codegen (the
iter-1 lower_term stub stays — iter 3); NO Diverge/guardedness
(spec boundary).
Three Boss design calls implemented verbatim and journalled:
RecurTypeMismatch is an Assign-style structural pre-check (not a
unify-propagate); loop_stack is positional Vec<Type> (binder names
via ordinary locals); diagnostic-code precedence is by pass
ordering (no explicit logic).
Two DONE_WITH_CONCERNS, both journalled: a plan-ordering defect
(Task 2's compile gate forced the check_fn threading the plan
deferred to Task 4 — resolved byte-identically, only resequenced)
and the recurring mut.2-class recon-undercount of cross-module
synth callers (resolved via the plan's compile-sweep oracle).
Boss systemic fix folded in: planner SKILL.md Step-5 gains item 7
(compile-gate vs. deferred-caller ordering) so the plan-ordering
defect class is scrubbed at plan time. cargo test --workspace
608 -> 616 / 0 red (Boss-reran independently).
Second of three iterations of the standalone loop/recur milestone.
Replaces the iter-1 synth CheckError::Internal stub with real
binder typing + positional recur arity/type checking via a
loop_stack frame, a new private verify_loop_body tail-position
pass (sibling of the spec-frozen verify_tail_positions), and the
four Recur* diagnostics. Three Boss design calls baked into the
plan header: (1) RecurTypeMismatch is an Assign-style structural
subst-compare not a unify-propagate (so the dedicated code fires
point-exactly per spec acceptance); (2) loop_stack element type is
Vec<Type> (positional, not name-keyed IndexMap — recur rebinds by
position; binder names enter ordinary locals like Term::Let);
(3) diagnostic-code precedence is by pass ordering (synth before
verify_loop_body), already correct, no explicit logic needed.
Every code step literal — synth signature/threading, the two real
arms, verify_loop_body full body, four negative .ail.json
fixtures, carve-out 13->17 + stale-header fix, pin harness, ct1 F2
sibling — all verified against read source. Codegen stays the
iter-1 stub (iter 3). Self-review: reused iter-1's loop_sum_to.ail
as the positive-check fixture rather than create a near-duplicate.
First of three iterations of the standalone loop/recur milestone
(spec 97c1ed1). loop/recur become real parseable / printable /
round-trippable / hash-stable strictly-additive AST nodes across
every no-wildcard exhaustive Term match in all six crates, plus
parse/print, prose arms, DESIGN.md + form_a.md schema blocks,
drift/coverage/hash anchors, and the spec's worked sum_to program
as a green round-trip fixture. NO typecheck semantics and NO real
codegen this iter by design: synth and lower_term are stubbed
CheckError::Internal / CodegenError::Internal exactly as mut.1
(real typecheck = iter 2, real loop-header/phi/back-edge = iter 3).
Two binding Boss design calls recorded in the plan header and the
journal: (1) verify_tail_positions "byte-unchanged" = its tail-app
verification role is unchanged, not its source — it is an
exhaustive no-wildcard match so two descent-only arms are
mandatory; acceptance evidence is the tail-app non-regression test.
(2) codegen is in iter-1 scope as stubs/pass-throughs because the
workspace must compile for cargo test --workspace.
Three plan-pseudo-vs-reality substitutions (serde Type::Con
literal, bare Int -> (con Int), unqualified LoopBinder) and one
recon-undercount integration-test site, all intent-preserving and
journalled. Boss forward-fix folded in: the committed specs
themselves wrote bare Int in the loop-binder snippets (parses as
Type::Var) — corrected the three headline snippets to (con Int) for
doc-honesty (no design/schema change).
cargo test --workspace 600 -> 608 / 0 red (Boss-reran
independently); iter13a + new loop_recur hash pins green.
First iteration of the loop-recur milestone: Term::Loop /
Term::Recur / LoopBinder as strictly-additive nodes across all six
crates' no-wildcard Term matches, parse/print/round-trip,
hash-stability + drift anchors. No typecheck semantics (synth
stubs CheckError::Internal — iter 2), no real codegen (lower_term
stubs CodegenError::Internal — iter 3); both stub points mirror
mut.1. Two Boss calls baked into the plan header: (1)
verify_tail_positions 'byte-unchanged' = tail-app role unchanged,
operationalised via the tail-app IR-snapshot non-regression test
(literal source freeze impossible — exhaustive no-wildcard match);
(2) codegen is in iter-1 scope as stubs/pass-throughs because the
workspace must compile for cargo test --workspace (the iter goal).
Recon DONE_WITH_CONCERNS — all three open questions resolved at
plan time (Q3: print.rs:352 is a Type match, untouched).
Brainstorm milestone spec for first-class additive Term::Loop /
Term::Recur (Approach A). De-bundled from the reverted
Iteration-discipline milestone: no structural-guardedness checker,
no Diverge effect — the half-enforced-dichotomy failure that sank
the original bundle is explicitly out of scope. recur has no
non-tail spelling (structural jump, by-construction stack safety).
verify_tail_positions left byte-unchanged; new private
verify_loop_body pass owns recur-tail-position checking. Feature
acceptance: all 3 clauses pass. Grounding-check Step 7.5: PASS,
9 ratified load-bearing assumptions. User-approved.
Distillation of the effects→input→state→closures→recursion design
session. Ratifies one meta-principle (disciplined surface, mechanism
stays in the compiler) as standing orchestrator guidance, separates
the three bundled DESIGN.md commitments (content-address-definitions
is cheap and kept; total value-purity + RC-no-collector are the
expensive ones), and parks four open forks with epistemic status for
later brainstorm pickup. No feature ratified here.
Standalone documentation-honesty tidy (no language/checker/codegen
change; `ail check`/`run` byte-unchanged by construction). Corrects
three false effect-system claims the effect-subsystem recon surfaced,
plus two satellite mentions, guarded by a new doc-presence pin:
- DESIGN.md Decision 3: removed the "row-polymorphic (`![IO | r]`)"
claim (no EffectRow / row variable exists in any crate — effect
sets are a flat, unordered, closed set unified by set-equality);
reconciled "`IO` and `Diverge` are wired up" to IO-only +
`Diverge` reserved/unimplemented (zero code in any crate),
modelled on Decision 4's reserved-refinements precedent.
- DESIGN.md "What is not (yet) supported": "IO and Diverge ops"
bullet -> IO-only + Diverge-reserved.
- ast.rs Term::Do doc-comment: "resolved against the effect-handler
table at link time" -> the real mechanism (typecheck lookup in
Env::effect_ops + literal lower_effect_op codegen match; no
handler table, no link-time resolution).
- form_a.md:226 + rule 3, and main.rs merge-prose CONTRACT example:
Diverge mentions reconciled in lockstep.
- new crates/ailang-core/tests/effect_doc_honesty_pin.rs: 4 tests,
fiction-absent + corrected-anchor-present, single-line wrap-robust
substrings.
cargo test --workspace 600 -> 604 (4 new pin tests, 0 regressions);
design_schema_drift / spec_drift / schema_coverage stay green,
empirically confirming none scans the effect-prose region.
The effect-op-arg-modes brainstorm spec bundled a real, self-contained
DESIGN.md honesty fix with speculative build-ahead infra (an EffectOpSig
arg-mode field whose only consumer was a synthetic test op). Recon had
already destroyed the "effect-handler infrastructure" premise: the effect
raise/declare/subset/propagation machinery is fully generic over any
effect string; the only real prerequisite was one struct field, not a
milestone. The user rejected the bundle as incoherent and the
build-ahead half as the iteration-discipline trap repeated.
- Retire the uncommitted effect-op-arg-modes spec (deleted from the
working tree; never committed).
- roadmap: drop the false "sub-ms-2 = effect-handler infrastructure"
framing; mark Stateful-islands sub-milestone sequencing as UNDER
RE-THINK with the user (no !Mut planning until that conversation);
add the split-out effect-doc-honesty as an in-flight P2 [~] todo.
- plan docs/plans/2026-05-16-effect-doc-honesty.md: placeholder-free
tidy plan (the three DESIGN.md fictions + form_a.md + main.rs +
a new doc-presence pin) over recon-mapped exact byte sites.
- brainstorm SKILL: specs and design talk LEAD with the AILang (.ail)
program that should work (= the feature-acceptance clause-1
evidence); the Rust implementation shape is secondary, never a
substitute; a no-surface infra milestone still shows concrete .ail
(often a must-fail fixture). Codifies user feedback from this session.
Mirrors the done-state notify text verbatim (user-facing changelog,
no internals). The iteration-discipline-revert milestone is fully
closed (brainstorm → planner → implement → audit, clean); next move
surfaced is the effect-handler-infrastructure milestone.
Architect: revert byte-pristine vs 1ff7e81 (18 src + 5 test files
byte-identical; zero residual it.1/it.2 production surface; DESIGN.md
+ roadmap + spec hygiene correct). One [medium] drift — a dangling
loop/recur cross-reference into the reverted milestone inside the
LIVE Stateful-islands roadmap scope bullet — fixed inline: reworded
to keep the real, still-true scope guard (no while/for; this
milestone introduces none; repetition stays recursion as the
language has it; a mut block is a sealed expression, never a loop)
and drop the reverted-milestone reference.
Bench: compile_check.py / cross_lang.py exit 0; check.py exit 1 on a
single metric (bench_list_sum.bump_s +12.71%). Bencher localisation:
the HEAD and 1ff7e81 bump binaries are cmp-identical, an interleaved
3x60-run measurement shows head-oracle delta ~0 (±1.5% stdev) with
both ~+11% over the 2026-05-09 baseline — environmental/hardware
drift, NOT a revert regression (third independent proof codegen ==
1ff7e81, alongside the architect byte-diff and the Task-11
behavioural-equivalence sweep). Resolution: carry-on; baseline NOT
ratified (Iron Law — nothing intentionally moved the metric;
byte-identical codegen must not move the baseline; the spec's
PRISTINE mandate is satisfied: pristine = no regression introduced,
proven). The pre-existing *.bump_s baseline-vs-hardware staleness is
filed forward as a separate P2 [todo] (bench-harness recalibration,
no language change), not mis-attributed to the revert.
Fieldtest skipped (Boss judgement; non-mandatory): a revert restores
the already-field-tested pre-9973546 surface; the Task-11
164-fixture byte-equivalence is the stronger correctness proof.
Milestone iteration-discipline-revert closed clean.
One forward iteration; main never rewound. 1ff7e81 (the pre-9973546
commit) is the per-region byte oracle — every reverted source/test
file is byte-identical to it; crates/ailang-check/src/lib.rs fully so.
Root cause being corrected: the Iteration-discipline milestone was an
over-escalation of fieldtest finding F1 (a [friction] item whose own
minimal recommendation was a DESIGN.md note). Its totality dichotomy
made the maximally-LLM-natural build(d:Int)=Node(1,build(d-1),
build(d-1)) inexpressible (it.3 BLOCKED); the only in-thesis escape
(A1/it.2b) conceded the language's first documented-unenforced
totality precondition — a purity-pillar dilution the user rejected in
favour of a full revert + rebuild.
Removed: Term::Loop/Term::Recur/LoopBinder; the verify_structural_
recursion guardedness pass + term_contains_loop + Diverge-injection +
the transitively-it.2 module_fns plumbing; the five Recur*/
NonStructuralRecursion CheckError variants (+ code() + the 3 dedicated
ctx() arms); the it.1 codegen loop-header/phi/back-edge + parallel
block_terminated setter; all Loop/Recur walker arms; 16 it.1/it.2
fixtures; 2 pin files; bench/it3-oracle/. Restored: 2 RC fixtures to
1ff7e81 content.
Surgically kept (not in 1ff7e81, landed with the milestone but
independently sound): feature-acceptance clause 3 in DESIGN.md and
skills/brainstorm/SKILL.md, with its worked example de-claimed from
"shipped" to hypothetical-illustration form; the F3 P2 todo.
bench/orchestrator-stats/2026-05-15-iter-it.{1,2,3}.json kept as
historical record (like journals/plans).
Sole net addition: an honest F1/F4 documented-idiom note in DESIGN.md
(the tail-recursive accumulator fallback; examples/mut_counter.ail),
guarded by a doc-presence test — "a documentation note is not a
reshape", asserts nothing at the typecheck level.
Roadmap: the Iteration-discipline block + blocking-fork section
removed; the genuine total-Int-recursion ambition preserved as a
deferred P2 milestone sequenced behind a future Nat/refinement-types
milestone (not abandoned — correctly sequenced after the type
machinery it needs). 2026-05-15-iteration-discipline.md carries a
superseded header; it.1/it.2/it.3 journals + plans stay as history.
Correctness gate PRISTINE: 164 surviving 1ff7e81-era fixtures
ail check/ail run byte-identical to pre-milestone behaviour (verified
against a 1ff7e81 worktree reference compiler, zero drift);
cargo test --workspace 600/0; zero residual it.1/it.2 production
surface.
Spec docs/specs/2026-05-16-iteration-discipline-revert.md (b3853bf),
plan docs/plans/2026-05-16-iter-revert.md (abf0013).
11 tasks clustered on the recon's lockstep-invariant pairs so each
task leaves the workspace in a defined state. Forward revert against
git show 1ff7e81: as the per-region byte oracle (no git revert/reset;
main sacrosanct). Boss judgement folded in: bench/orchestrator-stats
it.* json KEPT (historical record like journals); A1 amendment
discard already done Boss-side pre-plan (the checkout step is a
no-op, only the superseded header remains on that file);
verify_structural_recursion helper provenance resolved by
oracle-diff method, not guess. carve_out_inventory corrected from
the spec's "recompute count" to the recon-accurate "remove 7
string-set entries". Net additions written verbatim (F1/F4 note,
de-claimed clause-3 sentence, deferred roadmap entry, superseded
header, doc-presence test) per the Iron Law. Behavioural-equivalence
gate (Task 11) builds a 1ff7e81 worktree reference compiler and
diffs the surviving corpus byte-for-byte — pristine, not ratified.
Parent spec: docs/specs/2026-05-16-iteration-discipline-revert.md
(b3853bf). Grounding-check PASS. Recon DONE_WITH_CONCERNS, all three
concerns Boss-resolved.
The Iteration-discipline milestone was scoped from fieldtest finding
F1 (a [friction] item whose own minimal recommendation was "tighten
DESIGN.md") and escalated into "retire tail-app, recursion total-by-
construction, add Diverge". it.3 BLOCKED on the most LLM-natural
tree-builder shape (build(d:Int)=Node(1,build(d-1),build(d-1))),
which the thesis makes inexpressible; the only escape (A1/it.2b)
concedes the language's first documented-unenforced totality
precondition (no Nat/refinement enforcement — Decision 4). it.2's
NonStructuralRecursion checker is not a stable additive stopping
point — it half-enforces a dichotomy only the destructive it.3
completes.
User decision (2026-05-16): full revert to the pre-milestone state.
Both it.1 and it.2 come out via one forward iteration (main
sacrosanct). tail-app/structural recursion remain the iteration
mechanism exactly as before 9973546. Feature-acceptance clause 3 is
kept (it is the tool that diagnosed the over-escalation); F1/F4 get
the honest DESIGN.md documentation note the fieldtester actually
recommended. The genuine total-Int-recursion ambition is re-
sequenced behind a future Nat/refinement milestone, not diluted to
fit today. Stateful-islands (P2, the real ~/sma_factory.myc
descendant) is untouched.
Grounding-check (brainstorm Step 7.5): PASS after one factual
correction (the ctx()-arm removal split). Spec is the contract for
the revert planner+implement.
it.1/it.2 shipped; it.2's D1 resolved the original two open spec
questions (same-family mutual rule; foldl=structural). it.3 BLOCKED
on a fundamental design-thesis gap: the totality story recognised
only ADT-structural recursion; the corpus sweep proved a second
canonical total scheme (Int-bounded well-founded recursion, incl.
branching build(d-1),build(d-1)) is overlooked and the milestone
as-specified makes it inexpressible — failing its own
feature-acceptance gate. Records the full option analysis with a
recommendation (A1: trichotomy + a documented non-negative-entry
obligation), flagged as a USER decision because it trades against
the purity pillar the user co-designed. Next: user decision →
it.2b → it.3 re-dispatch from c992eb9.
it.3 Task 1 (non-destructive oracle + spec-delegated class-(b)
sweep) ran complete; zero production code changed. The mandated
sweep surfaced that 6 corpus fixtures contain
build(d:Int)=if d==0 then Leaf else Node(1,build(d-1),build(d-1)) —
a terminating, maximally-LLM-natural, non-structural non-tail
BRANCHING recursion the milestone's totality dichotomy makes
inexpressible (not ADT-structural; not recur-able — branching is
not a single tail back-edge). The milestone as-specified would
fail its own feature-acceptance criterion. Orchestrator correctly
bounced rather than patch a 4th time (spec_over_plan_patches).
Records the blocked attempt + the reusable bench/it3-oracle/
(40-fixture behavioural oracle, valid for the post-decision
re-dispatch). main forward-only; nothing reverted. Resolution
(it.2b + a user purity-pillar decision) tracked in the roadmap.
Two HEAD-vs-spec defects the it.3 recon found, fixed at the spec:
(1) "remove is_false" was wrong — is_false is shared with
WorkspaceDef.drop_iterative; it survives, doc re-scoped only.
(2) "remove the Decision-3 Diverge line" struck — it.2 already
rewrote Decision 3 to make Diverge the real effect (the milestone's
payload); it.3 touches Decision 8 only.
(3) The codegen-rework bullet under-described the real risk: the
18g.1 pre-tail-call husk-dec (match_lower.rs:658-736) reads
Term::App{tail:true} directly, protects the 18f.2 RC-RSS pin, and
its field-forced deletion is RC-safe ONLY if it.1 loop codegen
drops superseded owned binders across recur — unverified. Spec now
names this as the load-bearing it.3 risk with a RED-gated
verification + a named remediation (owned-loop-binder-drop-on-recur
in scope for it.3). Only 3 (not 7) setters are tail-driven.
No design decision (D1-D4) changes; design validated by shipped
it.1/it.2. Not relitigating shipped work (spec_over_plan_patches).
The original spec premise "21 .ail fixtures / 34 (tail-app …) sites"
under-counted the corpus's non-structural-recursion surface. it.2
proved the true it.3 migration set is three classes: the ~20
tail-app-marked fixtures + ~18 no-ADT-candidate counter-recursion
fixtures (deferred via it.2's no-candidate skip) + the 2 RC fixtures
it.2 joined to the transitional grandfather. it.3 removes the
tail==false grandfather AND the no-ADT-candidate skip together.
No design decision (D1-D4) changes — factual scope-widening only,
traceable to 2026-05-15-iter-it.2.md §Concerns. Not relitigating
the complete, green it.2 (feedback: spec_over_plan_patches — fix
the spec where it governs, not the plan around it).
Iteration-discipline milestone, 2 of 3. Strictly additive (nothing
tail-related removed; that is it.3). New whole-body pass
verify_structural_recursion sibling of verify_tail_positions
(DD-1): smaller-set algorithm with implicit candidate inference +
unconstrained accumulators (DD-2, foldl=structural), self/mutual
via inline ADT-family union-find (DD-3), it.2-only tail==false
grandfather. CheckError::NonStructuralRecursion. term_contains_loop
(stops at Term::Lam, DD-4) injects Diverge so existing
UndeclaredEffect enforces it, no new variant; lam-arrow + LetRec
sub-effect sites wired. DESIGN.md Decision 3 synced. Four it.1
loop fixtures gained !Diverge.
Two spec-premise boundary defects surfaced + resolved within the
additive invariant (corpus clean, check not weakened), recorded as
corrected it.3 corpus-migration scope: (1) the "21 tail-app
fixtures" grandfather premise under-counts the corpus —
no-ADT-candidate counter recursions have no structural position to
verify, deferred to it.3; (2) two RC-regression fixtures joined the
spec's transitional tail-app grandfather as the other 20 do (RC==GC
guards verified still green). cargo test --workspace 622/0; 9
acceptance pins non-vacuous. Spec fda9b78, plan bc9f512.
Iteration-discipline milestone, 1 of 3. Adds named loop + recur as
strictly-additive first-class AST nodes: parse/print/prose/serde/
round-trip/schema lockstep, typecheck (binder typing + recur
arity/type unification via loop_stack threaded as mut.2's
mut_scope_stack; recur-tail-position via verify_loop_body), codegen
(loop-header + one phi per binder; recur back-edge br with a NEW
parallel block_terminated setter; lambda-boundary loop_frames
save/restore). Four Recur* CheckError variants. Strictly additive:
zero deletions touch tail-app/tail-do or the seven existing
block_terminated sites — this is what makes the destructive it.3
safe. recur synth = fresh metavar (resolves the plan's flagged
Type::unit() risk). loop_counter->55, loop_in_lambda->49, four
negatives fire, tail-app byte-identical, cargo test --workspace
green. Spec fda9b78, plan 7381a42.
Boss-resolved design decisions baked in (DD-1 loop_stack in synth like
mut.2's mut_scope_stack + RecurNotInTailPosition in verify_tail_positions;
DD-2 LoopBinder derives = MutVar's, no PartialEq; DD-3 stub-then-fill
sequencing). Recon-enumerated walker-arm site list incl. the test-side
exhaustive match (codegen_import_map_fallback_pin.rs:62) mut.1's plan
missed. it.2/it.3 explicitly out of scope.
User-approved 2026-05-15 (read, probed via the sma_factory.myc streaming
analysis, no D1/D2 change requested, proceed-by-/boss). Grounding-check
PASS holds: spec bytes unchanged since the Step 7.5 dispatch (the
non-load-bearing "34 vs 36 tail-app sites" wording left as attested
rather than re-grounding for a count the agent ruled non-defect; the
21-fixture migration unit is exact). Three ordered iterations: it.1
loop/recur additive, it.2 guardedness checker + Diverge effect, it.3
tail-app/tail-do retirement + corpus migration.
Keeps the mut-local fieldtest's last unrouted finding from rotting.
F1 is owned by the Iteration-discipline P1 milestone; F4 is absorbed
by its spec; F3 is an independent surface decision, parked here.
Sharpen the feature-acceptance criterion (DESIGN.md, mirrored in the
brainstorm gate) with a third conjunctive-necessary clause: a feature
reintroduces no bug class the core constraint exists to eliminate.
Criterion 1 ("an LLM reaches for it") does not discriminate — an LLM
reaches for every construct native to its imperative training
distribution, so utility alone would launder the imperative paradigm
back in one construct at a time. Clause 3 is the discriminator; a
documentation note is not a reshape (the gate is whether the wrong
code fails to typecheck).
Record the decided iteration story as a P1 milestone: structural
recursion permitted (total-by-construction under Decision 10's
acyclic-ADT invariant) + named loop/recur as the only other
repetition + tail-app retired. Discharges fieldtest finding F1 without
introducing an imperative construct; closes the residual "forgot to
mark tail" trap completely; unifies with Decision 10 (acyclic values
AND acyclic implicit call graph). Supersedes the "while-loops legal"
scope bullet of the Stateful-islands milestone, which is amended for
internal consistency. The escaping-state path (ref/MutArray/Stateful)
is unaffected and stays in Stateful-islands.
The four mut-local CheckError variants (MutAssignOutOfScope,
AssignTypeMismatch, UnsupportedMutVarType, MutVarCapturedByLambda)
embedded the bracketed kebab code in their thiserror Display body,
while the non-JSON CLI formatter independently prepends [code] from
CheckError::code() — doubling it. Non-mut variants never embedded
the bracket; this brings the four in line with that convention.
RED-first via the debug skill: ct1_check_cli.rs pins the observable
property (code appears exactly once in the rendered human
diagnostic). GREEN was a trivial four-string mechanical edit.
Tests 598 -> 599.
Boss-dispatched fieldtest after audit-mut-local closed. Six AIL
Surface (.ail) fixtures under examples/fieldtest/ exercise the
shipped mut-local surface from a downstream-LLM-author's perspective
(no compiler-source access; DESIGN.md + public examples only).
Positive fixtures all run end-to-end first-try:
- mut-local_1_factorial.ail: straight-line Int accumulator
unroll (5!), prints 120.
- mut-local_2_classify_temp.ail: nested-if assigns into a Unit-
typed mut block, prints classification code.
- mut-local_3_horner.ail: Float mut accumulator for polynomial
evaluation, prints 18.
- mut-local_4_has_small_factor.ail: Bool mut flag via four
if-then-assign checks, prints true.
Negative probes confirm diagnostics fire as documented:
- mut-local_5_lambda_capture_probe.ail: [mut-var-captured-by-lambda].
- mut-local_6_diag_probe.ail: [mut-var-unsupported-type].
Findings:
[friction] F1 — mut without iteration: the accumulator-over-an-
iteration shape never materializes. Without while/for, the LLM-
author still writes a tail-recursive helper (the very pattern
the milestone Goal said mut would replace). examples/mut_counter.ail
illustrates the degeneration. Routing: planner for a while/for
iteration OR tighten DESIGN.md to name the gap honestly.
[friction] F2 — all four mut-related diagnostics emit their
bracketed [code] twice ('error: [code] fn-name: [code] message').
Mechanical bug: the #[error('[code] ...')] Display attributes I
authored in mut.2/mut.4-tidy include the bracketed prefix in the
message body, and the cli-diag-human formatter adds another from
CheckError::code(). Routing: debug (mechanical message-body
cleanup).
[friction] F3 — no surface form to call a zero-arg fn ('(app f)'
rejected at parse with 'expected at least one argument').
Orthogonal to mut-local but surfaced building the closure-factory
probe. Routing: planner for a small tidy iter.
[spec_gap] F4 — DESIGN.md does not name the 'use a tail-rec
helper instead' workaround for the iteration-over-accumulator
shape that mut alone cannot express. Routing: ratify in DESIGN.md
alongside F1's resolution.
[working] W1/W2/W3 — surface reachable on first read; diagnostics
pinpoint cause (mut-assign-out-of-scope even lists available
vars); composes cleanly with if + lambda-without-capture +
final-expression position.
Spec: docs/specs/2026-05-15-fieldtest-mut-local.md (333 lines).
Refs: docs/specs/2026-05-15-mut-local.md, audit-mut-local close
at 8685e96.
- docs/WhatsNew.md: append a user-facing entry describing the new
local-mutable-state block — what it does, what it does not yet
cover (heap-Str and user ADTs), the two example fixtures, and
the broader streaming-workloads motivation. No internals.
- docs/roadmap.md: the Stateful-islands milestone flips from [ ] to
[~] (in progress). A Progress paragraph records sub-milestone 1
(mut-local) as closed with its commit references, and enumerates
the remaining sub-milestones (effect-handler infrastructure,
refs + !Mut, MutArray, Stateful + pipe) — each to be brainstormed
separately so each one stays small and audit-friendly.
Tidy iter addressing the audit-mut-local drift. Plus paired baseline
update on bench/baseline_compile.json (audit-skill discipline).
Architect [high] items closed:
1. CheckError::MutVarCapturedByLambda rejects lambdas whose body
free vars include a mut-var of the enclosing mut_scope_stack.
Uses the existing ailang_core::desugar::free_vars_in_term walker
(which honours Term::Match pattern bindings). The scan runs only
when mut_scope_stack is non-empty.
2. crates/ailang-codegen/src/lambda.rs: the capture-not-in-locals
path that previously raised CodegenError::Internal blaming the
typechecker now uses unreachable!. The companion comment block
was rewritten to state the current reality.
Architect [medium] items closed (stale mut.1-stub history comments):
3. docs/DESIGN.md §'Term (expression)' mut/assign block: the
trailing paragraph describing the iter mut.1 stub state was
replaced with one describing the current mut.3-end-state. The
inline jsonc comment on {'t': 'assign'} was updated to drop the
'deferred to mut.2/mut.3' language.
4. crates/ailang-codegen/src/lib.rs: the stale comment block above
the real Term::Mut arm describing the mut.1 stub was removed
entirely.
Other:
- Short-circuit on empty mut_scope_stack in synth's Term::Var arm:
the iter mut.2 prepend now skips the iter-and-find walk when
the stack is empty, eliminating any per-Var-resolution overhead
for the common case (programs with no mut blocks). The
short-circuit did NOT close the check_ms regression — see ratify
below.
- Spec docs/specs/2026-05-15-mut-local.md §'Out of scope' amended
with the lambda-capture rejection bullet.
- Negative fixture examples/test_mut_var_captured_by_lambda.ail.json
+ driver test extension in crates/ailang-check/tests/
mut_typecheck_pin.rs (6th test) +
crates/ailang-core/tests/carve_out_inventory.rs EXPECTED bumped
12 → 13.
Bench-regression ratify:
bench/compile_check.py check_ms showed a uniform ~30-50% relative
shift across the entire 11-fixture suite (~0.5ms absolute on a
1.4-1.5ms baseline). The uniformity across fixtures of very
different Var counts argues for a fixed-cost-per-invocation tax,
not a Var-proportional hot path. The Term::Var short-circuit
falsified the hot-path hypothesis. The plausible remaining causes
(synth-parameter-passing through ~19 recursive sites, and binary-
size startup tax from mut-* adding ~1400 LOC to typecheck/codegen)
are both feature-cost, not pathological. Ratified by paired
journal entry; bench/baseline_compile.json updated to the post-
mut-local numbers via 'bench/compile_check.py --update-baseline'.
bench/check.py continues to show the established tail-latency-noise
envelope from audit-pd (2026-05-14) — no separate ratify needed.
bench/cross_lang.py clean.
Tests: 594 → 598 green (4 new lib.rs mod tests + 1 driver test +
1 fixture-corpus uptake).
Journal: docs/journals/2026-05-15-iter-mut.4-tidy.md.
mut-local milestone end-to-end status:
- mut.1 (7b92719): AST + Form A surface.
- mut.2 (b24718a): typecheck.
- mut.3 (03fb633): codegen + e2e.
- mut.4-tidy (this commit): audit-drift close + bench ratify.
mut-local milestone CLOSED.
Refs: docs/specs/2026-05-15-mut-local.md,
docs/plans/2026-05-15-iter-mut.4-tidy.md.
Eight tasks addressing the architect's two [high] drift items
(lambda-captures-of-mut-var diagnostic + codegen comment cleanup),
two [medium] items (stale mut.1-stub history comments in DESIGN.md
and codegen), plus the bench compile_check.py check_ms regression
(~30-50% relative; closed by short-circuiting the empty
mut_scope_stack walk in synth's Term::Var arm).
Architect's [low] item (prose render of MutVar drops type
annotation) deliberately deferred per architect recommendation —
prose surface design is itself pending.
Plan unblocks the follow-on Stateful-islands milestone (effect-
handler infrastructure + !Mut + refs) per architect gate.
Replaces the iter mut.1 dispatch stubs in lower_term with real LLVM
lowering: mut-vars become alloca slots hoisted to the fn's entry
block, assign lowers to store, mut-var reads lower to load. Two
example fixtures run end-to-end printing 55 (Int via mut_counter,
Float via mut_sum_floats).
Concretely:
- Emitter struct gains three new fields: mut_var_allocas:
BTreeMap<String, (String, Type)> for per-fn name→(alloca SSA,
AIL type) tracking; pending_entry_allocas: String as a side
buffer accumulating alloca instructions during body lowering;
entry_block_end_marker: Option<usize> recording the byte position
in self.body immediately after the entry: label.
- start_block(label) captures the marker when label == 'entry'.
emit_fn resets all three new fields per fn body. At the end of
emit_fn, String::insert_str splices pending_entry_allocas into
self.body at the marker — alloca instructions land in the entry
block regardless of where in the source tree Term::Mut was
encountered (mem2reg eligibility preserved).
- Term::Mut arm: per var, fresh-name an alloca SSA, push the
alloca instruction into pending_entry_allocas, lower the init at
the current body position, emit a store to bind. The mut-var
binding is NOT visible during init (matches typecheck-side
ordering at lib.rs:3576). After all vars are bound, lower the
body in the extended scope. On block exit, restore any prior
bindings via a per-block save stack — supports nested
shadowing correctly.
- Term::Assign arm: look up the alloca + type via
mut_var_allocas.get(name); lower the value; emit store; yield
the canonical Unit SSA per the existing Term::Lit { lit:
Literal::Unit } convention.
- Term::Var arm: prepended with a mut-var lookup that emits a
load <ty>, ptr <alloca> and returns the load SSA. Innermost-wins
shadowing falls out of the BTreeMap's insert-overwrites
semantics combined with the per-block save/restore.
- examples/mut_counter.ail + examples/mut_sum_floats.ail:
recursive sum_helper accumulates 1..10 (Int) or 1.0..10.0 (Float)
inside a mut block whose single var is assigned the helper's
result. Both run end-to-end printing 55.
- crates/ail/tests/e2e.rs: mut_counter_prints_55 and
mut_sum_floats_prints_55 #[test] fns using the existing
build_and_run helper.
- DESIGN.md 'What is supported' subsection: 'Local mutable state'
bullet describes the new construct, points at the two example
fixtures, and reaffirms the seal-by-construction invariant under
Decision 10.
Beyond-plan adjustments absorbed in this iter:
- synth_with_extras's parallel Term::Var arm in
ailang-codegen/src/lib.rs needed the same mut-var lookup as
lower_term's. Plan only specified lower_term's arm.
- crates/ailang-codegen/src/lambda.rs needed save/restore of all
three new Emitter fields across the lambda-body boundary plus
in-thunk splice of pending_entry_allocas at the lambda's own
entry marker, so mut-blocks inside a closure body hoist into
the closure's entry block (not the outer fn's).
mut-local milestone end-to-end status:
- mut.1 (7b92719): AST + Form A surface.
- mut.2 (b24718a): typecheck.
- mut.3 (this commit): codegen + e2e.
Audit follows.
Tests: 592 → 594 green; cargo build green; both fixtures execute
and print 55.
Journal: docs/journals/2026-05-15-iter-mut.3.md.
Refs: docs/specs/2026-05-15-mut-local.md, docs/plans/2026-05-15-iter-mut.3.md.
Nine tasks: Emitter struct extension with mut_var_allocas +
pending_entry_allocas + entry_block_end_marker fields (Task 1);
start_block captures the entry-block byte marker (Task 2);
Term::Var resolution prepends mut-var lookup with load emission
(Task 3); Term::Mut codegen — alloca to side buffer, init at
current position, store to alloca, push binding (Task 4);
Term::Assign codegen — load alloca, lower value, store (Task 5);
final flush of pending_entry_allocas into self.body at the marker
(Task 6); examples/mut_counter.ail (Int) + examples/mut_sum_floats.ail
(Float) (Task 7); e2e tests in crates/ail/tests/e2e.rs (Task 8);
DESIGN.md bullet under 'What is supported' (Task 9).
Five Boss decisions encoded: entry-block hoist via String::insert_str
at the captured marker (chosen over post-pass or non-standard
emit-at-current); no LLVM helper wrapper — direct push_str matches
existing codebase convention; e2e tests append to the existing
crates/ail/tests/e2e.rs (reuse build_and_run helper); DESIGN.md
bullet lives in 'What is supported' subsection (recon misread —
the section exists at line 2680); Unit-codegen via the existing
Term::Lit { lit: Unit } canonical form.
Replaces the iter mut.1 dispatch stubs in synth with real typecheck
logic. Codegen stubs stay (deferred to mut.3); examples/mut.ail now
typechecks clean but still cannot run end-to-end.
Concretely:
- crates/ailang-check/src/lib.rs: three new CheckError variants
(MutAssignOutOfScope, AssignTypeMismatch, UnsupportedMutVarType)
with #[error('[code]: msg')] Display attrs matching the cli-diag-
human bracketed-code convention; code() + ctx() arms emit
kebab-case codes and structured JSON payloads.
- synth signature: new parameter mut_scope_stack: &mut Vec<IndexMap<
String, Type>> after locals, threaded through every recursive call
site (synth's 18 internal calls + mono.rs:712/1337 re-synth +
lift.rs:723 + builtins.rs test helpers + the mq.3 in-test helper
at lib.rs:6663). Stack is per-walk and lexically scoped — push on
Term::Mut entry, pop on exit. Each frame is an IndexMap so
declaration order is preserved for the diagnostic 'available' list.
- Term::Var resolution: prepended with innermost-first mut-scope
lookup. Mut-vars are monomorphic — no Forall instantiation, no
free_fn_owner recording. Innermost-wins shadowing falls out of
the iteration order.
- Term::Mut arm: gate var types to Int/Float/Bool/Unit only via
a small is_supported_mut_var_type helper; each var's init is
synth'd in the in-progress scope (the var itself is not yet
bound, so it does not self-shadow during init); push completed
frame, synth body, pop. Block's type is body's type.
- Term::Assign arm: walk mut_scope_stack innermost-first looking for
name. On miss: MutAssignOutOfScope with available flattened
across all frames (innermost-first, deduplicated, preserving
order). On hit: synth value, unify against declared type — on
mismatch emit AssignTypeMismatch with rendered types; on success
produce Type::unit().
- Five .ail.json fixtures under examples/ exercising each
diagnostic plus a positive nested-shadow sanity case, driven by
the new pin test crates/ailang-check/tests/mut_typecheck_pin.rs
(load_workspace + check_workspace + assert exact codes).
- carve_out_inventory.rs EXPECTED extended 7 → 12 to cover the new
negative-fixture set (consistent with the form-a-default-authoring
spec §C4(b) precedent for type-rejection fixtures staying as
.ail.json-only).
Plan deviations from recon: three additional synth() call sites
beyond the four enumerated (lift.rs:723, builtins.rs x2,
lib.rs:6663) surfaced via the build-red structural signal; each
threaded with a fresh empty stack. carve_out_inventory.rs extension
was required by the existing pin but not named in the recon — a
documentary concern for the next planner pass.
Tests: 579 → 592 green; examples/mut.ail typechecks clean
('ok (26 symbols across 2 modules)') via cargo run --bin ail --
check examples/mut.ail; cargo build green; full workspace test
green.
Journal: docs/journals/2026-05-15-iter-mut.2.md.
Refs: docs/specs/2026-05-15-mut-local.md, docs/plans/2026-05-15-iter-mut.2.md.
Seven tasks: three new CheckError variants + code() + ctx() arms
(Task 1); synth signature extended with mut_scope_stack threaded
through ~19 recursive call sites in lib.rs plus the two re-synth
sites in mono.rs:712/1337 (Task 2); Term::Var resolution prepended
with innermost-first mut-scope lookup (Task 3); Term::Mut arm
(Task 4) and Term::Assign arm (Task 5) replacing the iter mut.1
Internal stubs with real logic; five negative fixtures + driver
(Task 6); positive verification of examples/mut.ail typechecking
clean (Task 7).
Four Boss decisions encoded: fixtures under examples/ with driver
at crates/ailang-check/tests/ (convention wins over spec wording);
mut_scope_stack as a synth parameter not an Env field (matches the
locals/effects/subst threading convention); MutAssignOutOfScope's
available list flattens all frames innermost-first;
mono.rs:712/1337 the two re-synth sites that need the threading.
First iteration of the mut-local milestone (foundation step on the
Stateful-islands roadmap path). Lands the schema + surface tier:
Term::Mut, Term::Assign, and the nested MutVar struct become
first-class AST nodes that round-trip cleanly through Form A.
Typecheck and codegen recognition are deferred to mut.2 and mut.3
per the spec's out-of-iteration boundary; reaching either dispatch
entry point with these variants produces CheckError::Internal /
CodegenError::Internal with a 'deferred to iter mut.{2,3}' message.
Concretely:
- crates/ailang-core/src/ast.rs: two new Term variants behind
#[serde(tag = 't')]; pub struct MutVar { name, ty, init } adjacent
to Arm. Two canonical-bytes pin tests for the explicit-empty-vars
serialisation and the assign round-trip.
- ~25 substantive Term-walker arms across ailang-core/desugar,
ailang-core/workspace, ailang-check (lib + lift + linearity + mono
+ pre_desugar_validation + reuse_shape + uniqueness),
ailang-codegen (escape + lambda + lib), ailang-prose, and
crates/ail/src/main.rs. Universal policy: substantive recurse-into-
children at every site; only the two dispatch entry points
(synth in ailang-check, lower_term in ailang-codegen) stub with
Internal-error. One test-side walker arm in
crates/ail/tests/codegen_import_map_fallback_pin.rs not
enumerated by the plan was added as well (defensive recursion).
- ailang-surface: parse_mut + parse_assign helpers; Term::Mut
body desugared from a flat statement sequence into a right-folded
Term::Seq chain inside the JSON-AST. Print arms in print.rs match
the parser convention. EBNF prologue + crates/ailang-core/specs/
form_a.md productions updated. Four new parser pin tests cover
the empty-mut, single-var, body-required, and vars-only-no-body
cases.
- Drift + coverage tests extended: design_schema_drift.rs adds two
exemplars + match arms; schema_coverage.rs adds two VariantTag
entries + EXPECTED_VARIANTS + visit_term arms; spec_drift.rs adds
two exemplars + match arms. DESIGN.md §'Term (expression)' gets
jsonc-blocked schemas for the two new variants.
- examples/mut.ail: six-fn round-trip fixture exercising empty mut,
single-var, two-var, nested-shadow, and the four supported scalar
return types (Int, Float, Bool, Unit). The round_trip auto-glob
and schema_coverage corpus walker both pick it up.
Plan deviation: the plan named lib.rs:2572 as the typecheck
dispatch stub site, but that line is actually verify_tail_positions
(substantive walker). The real dispatch is synth (3403-area, stub
at 3489); the orchestrator routed correctly.
Tests: 564 → 579 green; cargo build green; round-trip green for
the new fixture; all drift + coverage tests green.
Journal: docs/journals/2026-05-15-iter-mut.1.md.
Refs: docs/specs/2026-05-15-mut-local.md, docs/plans/2026-05-15-iter-mut.1.md.
Six tasks: AST + serde (Task 1), substantive walker arms across the
~25 exhaustive Term match sites (Task 2), the two dispatch stubs in
check_term and lower_term that produce CheckError::Internal /
CodegenError::Internal per the spec's out-of-iteration boundary
(Task 3), Form A parser + printer (Task 4), schema-drift + coverage
+ spec-drift extensions + form_a.md + DESIGN.md amendments (Task 5),
examples/mut.ail fixture with round-trip + corpus-coverage gate
(Task 6).
Three Boss decisions made explicit in the plan: walker-arm policy
(substantive everywhere except the two dispatch entry points),
empty-vars canonical-bytes pin, parse rejection of (mut) without
body. Plan-recon flagged all three; the plan resolves them rather
than deferring.
First shippable milestone on the Stateful-islands roadmap path:
sealed-by-construction local mutable bindings in fn bodies, with
no ref-types, no effect handlers, no escape. Lowers entirely to
LLVM allocas with statically-bounded lifetime; fn signatures stay
pure (no !Mut effect leakage).
Three iterations: mut.1 (schema + surface), mut.2 (typecheck +
diagnostics), mut.3 (codegen + e2e). Var element types restricted
to Int/Float/Bool/Unit (non-RC-managed scalars) in this milestone;
Str/ADT/fn-value vars deferred.
Grounding-check PASS on iteration mut.1 assumptions: Term enum
extension is gated by design_schema_drift, round-trip by
ailang-surface round_trip auto-discovery, DESIGN.md amendment by
the same drift test, schema_coverage forces a corpus fixture.
P2 entry: Stateful a b first-class type + !Mut effect + mut syntactic
block, with uniqueness inference at the boundary. Motivated by the
2026-05-15 myc-vs-AILang analysis showing AILang's pure-only model
cannot match the stateful-closure-plus-pipe idiom for online /
streaming workloads (rolling indicators, IIR filters, online stats).
The signature-as-contract thesis is *better* served by explicit
Stateful + !Mut annotations than by myc's implicit closure-mutation:
time-identity becomes visible in signatures without body inspection.
Decision 10's no-shared-mutable-refs forbids aliased mutation; it
does not forbid uniqueness-bounded mutation, which is the Lean 4 /
Roc / Haskell-ST / Koka layered-design pattern.
Ten blockers identified, from effect-handler infrastructure (the
first non-IO/non-Diverge effect) through uniqueness inference for
var captures, mutable-array primitive, runST-equivalent escape
discharge, Form-A surface design, codegen for mutable struct-field
writes, escape analysis for var/ref values, Decision-10 amendment
vs. Decision 12, streaming bench corpus, and LLM-utility fieldtest.
Architect drift: 1 Important + 1 Minor fixed inline as audit-pd-tidy.
DESIGN.md §"Roundtrip Invariant" point 4 + §Enforcement asserted
"Eight `.ail.json`-only fixtures" with one being the prelude embed.
Post-pd.3 the count is seven and the embed clause is empty; rewrote
both paragraphs to reflect the post-milestone state with a forward-
pointer to the prelude-decouple resolution.
ail/src/main.rs's migrate-canonical-types subcommand carried a
`local_types.get("prelude")` fallback in `rewrite_type` that was
dead-by-construction after pd.3 retired the synthetic prelude load
in the same subcommand. Removed the or_else arm; replaced with a
multi-line comment naming the milestone and the repopulation path
for any future variant that needs prelude awareness.
Bench: check.py + compile_check.py exit 1 with the established
envelope-noise (15th consecutive observation since audit-cma);
cross_lang.py clean. pd.* iterations are workspace-loader / spec /
filesystem / test-code only — zero codegen / runtime / typecheck
path edited, so the regression cluster cannot be milestone-induced.
Baseline left pristine consistent with all 14 prior decisions.
Roadmap: P0 entry (prelude-decouple, [~]) struck and removed; P0 is
empty again. The pre-milestone P2 entry that this work superseded
("Prelude embed: Form-A as compile-time source") is gone too. Per-
iter journals stay in INDEX.md as chronological context.
Carried debt unchanged: 3 cargo doc warnings on dangling load_workspace
references in core's lib.rs (covered by the existing P2 "Rustdoc
warning sweep" todo).
Deleted examples/prelude.ail.json (-559 lines). The cross-form-identity
preflight test that ratified pd.3's load-bearing assumption at module
hash 3abe0d3fa3c11c99 has discharged its purpose and was removed
along with its supporting bytes; the long-term prelude_parse_yields_canonical_hash
anchor stays. The ail-CLI migrate-bare-cross-module-refs subcommand's
defensive include + lockstep skip-branch removed; the rewrite logic's
prelude-fallback capability silently retired with the synthetic insert
(known debt, doc-comment updated to reflect). One in-mod core test
relocated to crates/ailang-core/tests/workspace_pin.rs (pd.2.4 dev-dep
cycle precedent — in-mod call to ailang_surface still structurally
impossible).
carve_out_inventory.rs: 8 → 7 carve-outs, §C4(b) row dropped, header
sentence updated. form-a-default-authoring.md §C4(b) gets a "RETIRED
2026-05-14 by milestone prelude-decouple" status marker; original
historical text preserved. New prelude_decouple_carve_out_pin.rs
asserts examples/prelude.ail.json does NOT exist.
Milestone prelude-decouple closed: prelude exists on disk only as
examples/prelude.ail; ailang-core embeds zero prelude bytes; CLAUDE.md
"authors write .ail" doctrine holds without exception.
cargo test --workspace 573 green; bench/cross_lang.py +
bench/compile_check.py exit 0; bench/check.py exit 1 with the
established noise envelope (14th observation of bench_list_sum.bump_s
since audit-cma; pd.3 is filesystem + spec text + test deletion only,
no codegen / runtime / typecheck path edited — baseline pristine).
Folds in the orphan pd.2 INDEX entry that was left uncommitted in the
working tree.
8 tasks. Closes the prelude-decouple milestone by deleting
examples/prelude.ail.json and sweeping the surviving consumers + spec
text + inventory. Three production consumers retire: the migrate-bare-
cross-module-refs defensive include in ail/src/main.rs (with its
lockstep skip-branch), the cross-form-identity preflight test in
ailang-surface (its purpose discharged at pd.2-close), and one in-mod
test in core's workspace.rs (re-pointed from inline include_str! to
ailang_surface::parse_prelude via the dev-dep). Adds a carve-out
retirement pin asserting the JSON file does NOT exist. Updates
carve_out_inventory.rs (eight → seven; §C4(b) row dropped) and the
form-a-default-authoring spec §C4(b) (RETIRED status marker added,
historical text preserved).
Moved PRELUDE_AIL const + parse_prelude fn to ailang-surface;
re-exported from surface's lib.rs. surface::load_workspace rewritten
3-line shim-call → 7-line composition: load_modules_with → reserved-name
check → inject parse_prelude → build_workspace(&["prelude"]).
Five deletions in core: PRELUDE_JSON, load_prelude, load_workspace_with
(the pd.1 shim), the top-level cfg(test) load_one fn, and the
cfg(test) crate::load_module import. Production literal-"prelude" count
in crates/ailang-core/src/ is now zero. Cross-form-identity preflight
PASSED at module_hash 3abe0d3fa3c11c99 — parse(prelude.ail) ≡
deserialize(prelude.ail.json), so pd.3's deletion of the JSON is safe.
Three new pin tests in ailang-surface/tests/: prelude_parse_pin (succeeds
+ min defs); prelude_module_hash_pin (cross-form-identity + literal-hex
anchor); prelude_injection_pin (inject + reservation).
Plan deviations (recorded in journal): switched the regression-pin
fixture from non-existent examples/hello.ail.json to hello.ail; relocated
10 in-mod core tests to crates/ailang-core/tests/workspace_pin.rs
because the dev-dep cycle (core ↔ surface) prevents in-mod aliasing in
the lib-test target (form-a.1 T5 precedent); preserved load_one
production-symbol-deletion by adding a test-mod-private load_one helper
in core's mod tests for the 2 pd.1-introduced tests still consuming it.
cargo test --workspace 573 green (+9 net from pd.1 baseline).
bench/cross_lang.py + bench/compile_check.py exit 0; bench/check.py
exit 1 with established noise envelope (bench_list_sum.bump_s, 13th
consecutive observation; pd.2 is workspace-loader-only, no codegen
touch — baseline pristine).