Architect clean (one [medium] advisory Sweep-5 self-match wart filed
as P3, not a fix iteration). Bench causally exonerated: HEAD vs
5bb7211/de66eb7 — all 5 firing bench binaries cmp-byte-identical, ail
sha256 identical at all 3 commits; the check.py firings are the two
tracked-P2 environmental classes (*.bump_s anchor staleness; *.max_us
-n5 tail-jitter, 4th on a zero-runtime-change milestone), NOT this
milestone — NO baseline ratify (Iron Law forbids ratifying noise).
compile_check 0/24, cross_lang 0/25 EXIT 0. No fieldtest (zero
authoring-surface change). docs-honesty-lint CLOSED: roadmap [~]->[x],
WhatsNew appended, INDEX line added.
Post-audit downstream-LLM-author field test of the post-removal
Form-A surface. 4 fresh real-world programs written from DESIGN.md +
form_a.md + public examples only (no compiler source): running
sum-of-squares accumulator (loop/recur → 385), grade cascade
(let-threaded if → 4/2/0), Horner polynomial straight-line build-up
(let-shadow chain → 73), multi-state bracket scanner threading
(rest,depth,ok) by tail recursion → true/false. 0 bugs, 0 friction,
0 spec_gap, 4 working — every task clean and correct on the first
try with only let/if/loop/recur; all parse|render|parse
byte-identical; zero mut/var/assign tokens produced (a doc-faithful
author did not reach for the removed construct).
The closest-to-friction case (multi-state machine restates the full
state tuple per tail-call) is correctly classified working, not a
spec_gap: that explicit-dataflow cost is the local-reasoning pillar
working as designed; mut's implicit cross-iteration persistence is
exactly the failure class the removal eliminates. mut-keyword
rejection is fail-closed with a diagnostic that enumerates the
surviving forms (no tombstone, no-nostalgia, as spec'd).
Removal thesis empirically CONFIRMED — mut was redundant with
let/if/loop/recur in 100% of the fresh tasks. The
remove-mut-var-assign milestone is fully ratified and CLOSED:
spec+plan+iter, audit clean (architect [high] form_a.md fixed in
.tidy, bench causally exonerated), fieldtest clean. Roadmap P0
flipped [~]→[x]; WhatsNew user-facing entry appended.
Milestone-close audit for remove-mut-var-assign (48e7774..07f0802).
Architect drift: one [high] — crates/ailang-core/specs/form_a.md
still documented the full mut/var/assign construct (EBNF 286-288,
prose 308-328, dangling `like mut` at 332). The spec named it for
deletion; the iter missed it. Root cause = the recon-undercount
class (4th recurrence) plus a Boss error: a plan-time grep checked
only docs/form_a.md (dir-scoped), got "No such file", and that
unverified non-existence claim propagated into the recon brief —
the real file is crates/ailang-core/specs/form_a.md. A stale doc
never compile-fails so the implement compile-sweep could not catch
it. Fixed inline as a Boss .tidy (CLAUDE.md context-already-loaded
carve-out; full review discipline kept — tree-wide verified zero
construct residue except the roadmap entry naming the milestone
itself; grammar now reads coherently reuse-as -> loop -> recur).
Bench causally exonerated: HEAD vs 48e7774 bench binaries
cmp-byte-identical (the -2520-line removal alters zero bytes of the
firing fixtures' machine code). check.py exit 1's 3 firings are the
tracked P2 bump_s environmental staleness + single-sample -n5
max_us jitter (explicit_at_rc.max_us +108% -> -2.30% ok on
identical-code rerun). PRISTINE met on the merits; NO baseline
ratify (Iron Law + spec foreclosure).
Roadmap: P0 refreshed (audit clean, fieldtest remains); the
ailang-plan-recon P2 escalated in place to a structural
recon-contract defect (2 named gaps + concrete fix); a new distinct
P2 filed for the check.py *.max_us -n5 3-for-3 false-positive.
mut/var/assign removed from AILang entirely and atomically. Deleted:
Term::Mut/Term::Assign/struct MutVar; the three Form-A keywords +
parse_mut/parse_assign + grammar EBNF; the 4 mut CheckError variants;
the mut_scope_stack synth threading (param dropped from synth + every
internal/external/test caller); the two lower_term arms; and every
exhaustive no-_ Term::Mut/Term::Assign match arm across 17 source
files — cut in lockstep with DESIGN.md, fixtures, the drift trio,
carve-out and roadmap so the schema is honest at every commit. No
catch-all wildcard introduced (verified). loop/recur + let/if are
the surviving forms.
The shared codegen alloca machinery survives (loop reuses it):
mut_var_allocas renamed binder_allocas (representation-only, loop
codegen byte-identical) and the shared Term::Lam escape guard
simplified to !loop_stack.is_empty() with the loop half
(LoopBinderCapturedByLambda) byte-equivalent. Feature-acceptance
applied inverted: the removed feature fails clause 2 (redundant)
and clause 3 (IS the iterated-mutable-state bug class).
Behaviour preservation is executable: mut_counter/mut_sum_floats
still print 55 after the faithful let/if rewrite. The removal is
made executable by the new mut_removed_pin.rs (4 must-fail pins).
Independent verification: cargo test --workspace 605/0, zero
residual mut symbols in any crate source, loop/recur non-regression
all green (55 / 500000500000 / infinite-compiles / the
lambda_capturing_loop_binder pin), roundtrip_cli PASS.
One DONE_WITH_CONCERNS: a 4th recurrence of the recon-undercount
class (in-source mod tests + a drift-pin fn + 5 orphaned mut
.ail.json carve-outs + a non-enumerated E0599); all resolved within
implementer remit, no behaviour change. Milestone-close audit then
fieldtest remain.
spec docs/specs/2026-05-18-remove-mut-var-assign.md (grounding PASS)
plan docs/plans/remove-mut-var-assign.1.md
Architect drift review clean (zero drift/debt, diff-verified: single
Term::Loop write_term hunk, no non-render site / Term::Recur / doc /
lockstep touched). Bench: compile_check 0/24 + cross_lang 0/25 exit
0; check.py's latency-tail firings proven single-sample jitter by an
identical confirmatory re-run (p99.9 +37%→+10% ok, max +114%→+22%
ok); the sole persistent firing is the pre-tracked P2 *.bump_s
environmental staleness, causally impossible to attribute to a
projection-only ailang-prose change. All items carry-on — no fix, no
baseline ratify. Fieldtest not applicable (no authoring-surface
change). Roadmap P0 flipped to closed; WhatsNew user-facing entry
appended (user present → notification suppressed per policy).
The Term::Loop arm of write_term rendered binders as bare
`name = init;` statements inside the `loop { … }` body block, which
reads as C/Rust re-init-every-iteration semantics — a projection
lying about a body the prose surface exists to let the reader trust.
Rewrite to a parenthesised init-list on the keyword line,
`loop(acc = 0, i = 1) { … }`, positionally isomorphic to the
unchanged Term::Recur arm. Projection-only: loop/recur AST, Form-A,
JSON-AST, typecheck, codegen, and the Form-A↔JSON round-trip
invariant untouched. RED-first via two new committed
examples/*.prose.txt byte-equality snapshots + two snapshot_loop_*
tests; full ailang-prose suite 10/0; both ail-prose CLI byte-matches
green. Single-iteration milestone, brainstorm→plan→implement.
Spec: docs/specs/2026-05-18-prose-loop-binders.md
Plan: docs/plans/prose-loop-binders.1.md
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.
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).
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
Original "Prelude embed: Form-A as compile-time source" P2 entry
removed; superseded by the wider "Prelude / core decoupling" P0 entry
that now reflects the shipped β.2 loader-split shape and the three-
iter sketch (pd.1 done, pd.2 + pd.3 to follow).
Hygiene sweep across the workspace under `cargo clippy --workspace
--all-targets`. Before: 61 warnings. After: zero. Tests stay 563
green, `cargo doc` stays at zero warnings, and all three bench
scripts exit 0 against existing baselines.
Twelve lint classes, three fix shapes:
- Documentation hygiene (~32 hits): `doc_lazy_continuation` resolved
by adding blank lines before continuation paragraphs or rephrasing
the offending line; plus four `empty_line_after_doc_comments` hits
in workspace.rs where eight `///` blocks left orphan by form-a.1
T5 test relocation were converted to plain `//`.
- Idiomatic refactors (~16 hits): `.err().expect()` → `.expect_err()`,
redundant `.into_iter()` and `.into()` removed, `|f| g(f)` →
`g`, `.trim().split_whitespace()` → `.split_whitespace()`,
`push_str("x")` → `push('x')`, two manual `impl Default` flipped
to `#[derive(Default)]` + `#[default]`, two nested `if let Some(_)
= …` collapsed.
- Block extraction (1 hit): a 13-line block inside an `else if`
condition in synth's Var-arm extracted to a new helper
`is_class_method_dispatch(name, env)` next to
`qualifier_is_class_shape`.
Three `#[allow]`s with inline rationale where clippy's suggestion
would lose meaning: `only_used_in_recursion` on walk_pattern (the
parameter preserves the five-fn walker-framework signature
uniformity), 2× `if_same_then_else` on qualify_local_types shapes
(two branches encode semantically distinct disqualification
reasons), `too_many_arguments` on `Emitter::new` (8 workspace-flat
tables; bundling would just rename boilerplate).
Two unrelated hygiene iters bundled because they shipped together
in one autonomous-while-Boss-away batch:
- iter rustdoc-sweep: cleared all 23 `cargo doc --workspace
--no-deps` warnings (17 in ailang-check + 4 in ailang-core + 2
in ailang-surface). Two warning classes: pub-doc-comment links
to pub(crate)/private items (15 hits — replaced with plain
backtick-code-spans), and unresolved/ambiguous links (8 hits
— fully-qualified, disambiguated to fn-form, or escaped).
No behaviour change; tests 562 → 562.
- iter drift-test-narrowing: design_schema_drift.rs now scans
§"Data model" only via new helper data_model_section(),
instead of full DESIGN.md. Closes the audit-form-a-precursor
[high] "anchor-elsewhere-passes-silently" failure mode. All
38 anchors verified pre-edit to live in §"Data model" so the
tightening doesn't regress to red. New pin
data_model_section_is_bounded guards the extractor against
silent regression. Tests 562 → 563.
Append one-line pointer to the new per-iter journal. Closes the
fieldtest-form-a queue: bug (bugfix-instance-body-unbound-var),
spec_gaps 2+3 (form-a.tidy), friction (str-concat).
Boss-dispatched fieldtest at milestone close. Agent authored 4 .ail
fixtures (factorial smoke + Show user-ADT + user-class Describe with
two instances + two-module workspace) from DESIGN.md + form_a.md only
(no compiler-source reads, no spec-of-milestone reads). All four
fixtures `ail check` ok and `ail run` produces expected stdout.
Findings (1 bug, 1 friction, 2 spec_gaps, 3 working):
- [bug] instance-method-body unbound-var bypasses `ail check` —
forma_3 first attempt called `str_concat` inside the instance
method body; `ail check` returned ok, `ail build` died with the
monomorphise_workspace "unknown identifier" diagnostic. Same
shape at fn-body level correctly fires `[unbound-var]` at check
time. Next: debug skill, RED-first against `ail check`.
- [friction] no `str_concat` primitive. Every realistic Show MyType
body wants string concatenation; the absence forced the agent to
shape examples around bare int_to_str / ctor-arity-1 patterns.
Next: small planner tidy iter wiring `str_concat` symmetric to
`str_clone` and `int_to_str`.
- [spec_gap] form_a.md has zero references to class, instance,
constraint, or method productions — pre-22 surface only. The
form-a-default-authoring milestone made .ail the authoring form,
but the form_a spec doc remains pre-typeclass.
- [spec_gap] form_a.md leaves the class-qualifier ambiguity in
`(instance (class X))` unspecified — bare-class-name vs canonical-
form rule from mq.1 is not documented at the surface level. The
agent picked the bare-name reading from the corpus; the canonical-
form reading is equally plausible from the schema.
Boss-side cleanup at commit time:
- Deleted 8 stale .ail.json sidecars in examples/fieldtest/ (4
forma_* derived by the fieldtester per old dual-form workflow + 4
pre-existing floats_* from the prior fieldtest milestone that
iter form-a.1 T8 missed because the bash deletion loop globbed
only examples/*.ail.json direct children).
- Updated skills/fieldtest/agents/ailang-fieldtester.md to remove
the now-obsolete "generate canonical JSON sidecar" step (Phase 3
rewritten + Iron Law + Reading list + Common Rationalisations +
Red Flags all aligned to the post-form-a single-form doctrine).
cargo test --workspace: 557 passed, 0 failed, 3 ignored (unchanged
from audit-form-a — the fieldtest fixtures are standalone, not
referenced by any test).
Findings deferred to follow-up iters; the milestone close itself is
still clean.
Architect drift_found, 4 documentary-only items:
- 2× [medium] spec + plan "seven carve-outs" orphans surviving the
9fcda8b §C4(b) amendment commit. The implementation is
eight-carve-out-correct; the spec/plan orphans are post-hoc
retro-clean-up, not active drift.
- 2× [low] hash.rs:57 empty `mod tests {}` placeholder after T5
relocation + round_trip.rs:16 stale "Direction 2" docstring
contradicting T10's framing rewrite.
Architect explicit recommendation: carry-on. No form-a.tidy queued.
Drift items deferred to documentary cleanup at next opportunity.
Bench: all 3 scripts exit 0, baseline pristine.
- check.py: 63 metrics, 2 regressed-by-magnitude
(latency.explicit_at_rc.max_us +124.99%; latency.implicit_at_rc.max_us
+126.13%) both paired with p99 improvements on the same stem.
12th consecutive observation of the metric-identity-migrating
noise envelope since audit-cma (2026-05-12). Baseline pristine.
- compile_check.py: 24 metrics, 2 regressed-by-magnitude
(check_ms.hello +32.44%; check_ms.borrow_own_demo +26.18%).
Sub-millisecond timing jitter on the two smallest fixtures.
iter form-a.1 T6 already flagged this pair as transient.
- cross_lang.py: 25 metrics, 25 stable.
Milestone [Form-A as the default authoring surface] fully closed.
Next-step pointer: P2 candidates are [Retire io/print_int/_bool/_float]
(top) and [Prelude embed: Form-A as compile-time source] (queued
behind). Starting either is a /boss bounce-back per the new-milestone
rule.
First iteration of the form-a-default-authoring milestone. Mechanical:
one `ail render examples/prelude.ail.json > examples/prelude.ail`
captures the canonical Form-A projection (116 lines / 6386 bytes).
No source-code edits, no test edits.
The two auto-discovering roundtrip tests in
crates/ailang-surface/tests/round_trip.rs pick up the new fixture
via read_dir without test changes and both pass:
- every_ail_fixture_matches_its_json_counterpart — the .ail parses
to canonical bytes byte-equal to prelude.ail.json (gate test).
- parse_then_print_then_parse_is_idempotent_on_every_ail_fixture —
the Form-A printer is idempotent over prelude.
cargo test --workspace green at 558 tests (identical to pre-iter
baseline; audit-24 / iter-24.tidy lineage).
prelude.ail.json retained per spec §A2 — this is the singular
dual-form iter; iter 1 deletes it alongside the bulk test-infra
refactor.
Zero re-loops, zero review-phase repairs. Recon predictions matched
reality exactly (116 lines, 6386 bytes, `(module prelude` header,
558-test baseline). Strong signal that the iter-1+ bulk migration
mechanism is sound.
T1 [high-1]: refine_multi_candidate_residual rigid-var filter now
requires class + type-unification via constraint_type_matches, so
forall a b. prelude.Show a, userlib.Show b => ... shapes discriminate
by which typevar the residual is on. Discharge-time only; synth-time
twin doesn't have a residual type to unify against (fresh metavar
constructed after dispatch).
T2 [high-2]: qualifier_is_class_shape extracted as pub(crate) helper
adjacent to parse_method_qualifier; broadened to accept PascalCase
single-segment qualifiers like Show.show alongside module-qualified
ones. Same-module bare-class call sites now reachable; symmetric to
mq.1 canonical-form rule.
T3 [medium-1]: any_candidate_class_has_instance pub(crate) helper
gates the class-method-shadowed-by-fn warning closure on registry
instance presence (any candidate class, any type). Conservative
tightening of the spec rule per Boss Q2 decision.
T4 [medium-2]: four DESIGN.md Data Model schema fragments
(SuperclassRef, InstanceDef.class, Type::Con.name, Constraint.class)
annotated with canonical-form cross-references.
Two trip-wires fixed inline: parse_method_qualifier docstring
coherence, mq3 in-crate test fixture-repair (registry instance
injection — analogous to mq.3 typeclass_22b3 pattern).
5/5 tasks. 548 tests green (was 545 + 3 new mq_tidy_* unit tests).
bench/compile_check.py + cross_lang.py exit 0; check.py exit 0
both runs with noise-class metric migration on
latency.implicit_at_rc.* max-tail (6th-consecutive observation
since audit-cma). Baseline pristine.
Architect drift review surfaces:
- 2x [high]: rigid-var refinement misses type-unification leg
(refine_multi_candidate_residual filters declared constraints on
class only, breaks forall a b. Show a, Show b => ... shapes);
same-module bare-class qualifier Show.show unreachable because
qualifier_is_class_shape = q.contains('.') excludes the no-dot
case (contradicts mq.1 canonical-form symmetry).
- 2x [medium]: class-method-shadowed-by-fn warning over-fires on
locals shadowing prelude method names without
class-candidate-for-arg-type check; DESIGN.md Data Model schema
fragments don't carry the canonical-form rule for class-ref
fields (only the prose at 1156-1162 does).
- 1x [medium] acknowledged debt without fix: synth(...) grown to
10 mut-ref parameters — refactor cost disproportionate to gain.
- 2x [low] roadmap backlog: Trajectories B + D have unit-test
coverage only; collect_mono_targets rebuilds env without
active_declared_constraints (currently latent).
Bench: compile_check.py + cross_lang.py exit 0; check.py exit 1
across 4 consecutive re-runs with metric identity shifting
between runs — 5th-consecutive audit observation of the same
noise envelope. Baseline pristine.
Resolution: mq.tidy iter covers 4 actionable items; 3 deferred.
Closes the module-qualified-class-names milestone. Deletes the
workspace-load workaround; the two-libraries case now resolves via
type-driven dispatch at the call site with explicit qualifier as the
LLM-author's disambiguation tool. Resolves both mq.2 known-debt items
(active_declared_constraints plumbing, (class,method) re-key). New
synth warnings channel emits class-method-shadowed-by-fn at all three
fn-precedence branches per spec section Class-fn collisions. Three new
E2E fixtures + integration tests cover the ambiguous, qualified, and
class-fn-shadow trajectories. DESIGN.md class-names paragraph rewritten,
new section Method dispatch added. Roadmap P2 marked done, milestone-24
unblocked for re-brainstorm.
9/9 tasks. 545 tests green. bench/compile_check.py + cross_lang.py
exit 0; bench/check.py exit 1 (2 noise-class regressions, runtime
uncoupled to typecheck iter).