audit: prose-loop-binders milestone CLOSE — clean / carry-on

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).
This commit is contained in:
2026-05-18 01:10:20 +02:00
parent c9355d7d58
commit 48e7774d01
4 changed files with 115 additions and 12 deletions
+6
View File
@@ -192,3 +192,9 @@ It was field-tested by writing real programs from scratch against the documentat
Two unrelated, pre-existing minor surface gaps were noticed while testing and written down for later; neither affects loops.
The milestone is closed and fully verified — built across three steps plus a follow-up safety fix, drift-reviewed, benchmarked clean, and field-tested. The next milestone is a fresh decision; that is the natural place to pick up when you are back.
## 2026-05-18 — Loops read correctly in the prose view
The readable prose rendering of a loop no longer misleads. Until now, a loop's carried values were printed as plain assignment lines inside the loop body — which reads exactly like "reset these to their starting values on every pass", the opposite of what a loop actually does. The starting values now sit in parentheses on the loop keyword itself, the way function parameters do, and the body block contains only the body. This also makes the loop header line up one-to-one with the re-entry form that supplies the next round's values, so the relationship between the two is visible at a glance instead of obscured.
Nothing about how loops are written, type-checked, compiled, or run changed — this is purely the human-reading projection telling the truth about the code it shows. You raised this directly; it is a one-change milestone, drift-reviewed clean and benchmarked clean (the one flagged benchmark metric is the same pre-existing machine-drift item already on the list, not caused by this).
@@ -0,0 +1,91 @@
# audit prose-loop-binders — Milestone-prose-loop-binders tidy (clean)
**Date:** 2026-05-18
**Milestone:** prose-loop-binders (single-iteration)
**Commit range:** `6cbd0fe..c9355d7` (prev close = loop/recur at `6cbd0fe`)
**Status:** CLOSED — clean, carry-on (no fix, no ratify)
## Step 1 — Architect drift review
`ailang-architect` over `6cbd0fe..HEAD`: **clean**, zero drift, zero
debt. Verified against the diff (not journal trust):
- Single hunk in `crates/ailang-prose/src/lib.rs` (the `Term::Loop`
arm of `write_term`, lines 936953). The two non-render
`Term::Loop` sites (`count_free_var`, `subst_var_with_term`) and
all `Term::Recur` arms are absent from the diff — untouched.
- DESIGN.md and PROSE_ROUNDTRIP.md make **no** claim about how
prose renders `loop` (grep: no match), so the spec's "no doc
edit needed" is correct, not a skipped obligation. `DESIGN.md:714`
`loop(n-1)` is an ordinary recursive-call illustration in
Decision 8, unrelated to the `Term::Loop` construct — not stale.
- Diff touches only the four named code/fixture files plus expected
docs (spec/plan/journal/INDEX/roadmap/stats). No carve-out /
hash-pin / round-trip lockstep bypassed (prose is off the
Form-A↔JSON round-trip path by construction).
- The `architect_sweeps.sh` Sweep-1 DESIGN.md hits all pre-date
`6cbd0fe` and are outside this milestone's diff (zero DESIGN.md
lines changed) — flagged for the record, not attributable here.
Architect recommendation: carry on as planned.
## Step 2 — Bench-regression check
- `bench/compile_check.py`**exit 0**, 24 metrics, 0 regressed.
- `bench/cross_lang.py`**exit 0**, 25 metrics, 0 regressed.
Notably its independent `bench_list_sum.ail_bump_s` reads +1.50%
(±15% tol) → **ok** — corroborates that check.py's bump_s firing
is baseline-staleness, not a real bump regression.
- `bench/check.py` → exit 1. First run: 3 regressed
(`bench_list_sum.bump_s` +13.17%; `latency.implicit_at_rc.p99_9_us`
+37.30%; `latency.implicit_at_rc.max_us` +114.14%), 4 improved,
56 stable. Confirmatory identical re-run (no code change):
`p99_9_us` +10.22% → **ok**, `max_us` +22.17% → **ok** (median
and p99 within tol on both runs) — the two latency-tail firings
were single-sample 5-run RC-mode jitter, proven by re-run.
`bench_list_sum.bump_s` persisted at +11.79% (consistent with
the +13.17% first run): summary `63 metrics; 1 regressed`.
## Step 3 — Classification
| Item | Class | Rationale |
|------|-------|-----------|
| Architect drift | carry-on | clean — nothing actionable |
| `throughput.bench_list_sum.bump_s` +~12% | carry-on | The already-tracked P2 roadmap todo "`*.bump_s` throughput baseline is stale vs current hardware" — byte-oracle-proven environmental (not codegen) at the 2026-05-16 iteration-discipline-revert audit. Causally impossible to attribute to a projection-only `ailang-prose` change (prose is not in the bench runtime path; architect verified the diff is one render-arm + test fixtures). |
| `latency.implicit_at_rc` p99.9 / max | carry-on | Single-sample 5-run RC-mode tail jitter; vanished on identical re-run with median/p99 always within tolerance. Not a regression; the queued P3 "latency methodology rework" (histogram-based) exists precisely to de-noise this metric family. |
## Step 4 — Resolution
**Carry-on** on every item. No fix iteration, no baseline ratify.
Explicitly **not ratified**: the `*.bump_s` baseline is NOT bumped
here. The P2 roadmap todo scopes a holistic recapture+recalibration
of the entire `*.bump_s` set on current hardware from a known-clean
commit as a dedicated bench-harness iteration; opportunistically
bumping one metric's baseline inside an unrelated projection-only
milestone is exactly the "bump the baseline, the regression is
expected" rationalisation the audit Iron Law forbids, and would
fragment the recapture the P2 todo owns. The pointer stands; the
todo is unchanged.
## Fieldtest applicability
**Not applicable / not dispatched.** `fieldtest` is Boss-dispatched
for a milestone that touched the *authoring* surface — the
fieldtester writes `.ail` Form-A programs and runs them through the
public `ail` CLI. This milestone changed only the Form-B *reading*
projection (`ail prose` output); the Form-A authoring surface, the
typechecker, codegen, and runtime are byte-unchanged. A fieldtester
authoring `.ail` programs would not exercise the prose-render change
at all — there is no friction/bug/spec-gap axis for it to probe.
The two committed whole-file byte-equality `.prose.txt` snapshots +
the live-CLI `ail prose | diff` acceptance checks (iter journal) are
the end-to-end gate for a projection-only milestone.
## Milestone close
The **prose-loop-binders** milestone is **CLOSED, clean**. Architect
drift clear; bench carry-on (sole persistent firing is the tracked
P2 `*.bump_s` environmental staleness, latency-tail proven sampling
noise by re-run). Roadmap P0 entry flipped to closed; no follow-up
iteration spawned.
+1
View File
@@ -87,3 +87,4 @@
- 2026-05-18 — iter loop-recur.tidy: milestone-close audit resolution (RED `39380d3` + this GREEN). Architect drift review found a `[high]` correctness defect: a `(lam ...)` body capturing an enclosing `Term::Loop` binder passed `ail check` (exit 0) then panicked `unreachable!()` at `crates/ailang-codegen/src/lambda.rs:102` on type-correct input — a DESIGN.md "Robustness against hallucinations" violation. Root cause (debugger-confirmed RED-first): the `Term::Lam` escape guard at `crates/ailang-check/src/lib.rs:3608` iterated only `mut_scope_stack`; loop binders live in the ordinary `locals` (iter-2 design) and never entered any escape-checked scope, so the mut.4-tidy `MutVarCapturedByLambda`-class rejection had no loop-binder counterpart. Fix (GREEN, implement mini-mode): new `CheckError::LoopBinderCapturedByLambda { name }` (bracket-`[code]`-free F2 Display) + `code()` `loop-binder-captured-by-lambda` + `ctx() {name}`, symmetric to `MutVarCapturedByLambda`; the `Term::Lam` escape guard gained a parallel pass over `loop_stack` (gate widened to `!mut_scope_stack.is_empty() || !loop_stack.is_empty()`). Minimal-mechanism design call: `loop_stack`'s frame element extended `Vec<Type>``Vec<(String, Type)>` so the already-threaded per-loop scope frame also carries binder names (`Term::Recur` still reads `.1` BY POSITION — Boss-call-2's positional-recur invariant preserved verbatim, the name is a second field consumed only by the escape guard, a consumer iter-2 did not anticipate; the parallel-stack alternative would touch all 35 `synth` call sites, strictly larger). Codegen byte-unchanged (typecheck-only fix; the lambda.rs:102 `unreachable!()` is now genuinely unreachable, kept as a defensive assertion). Two mut.4-tidy-mirrored in-source unit tests; lockstep `carve_out_inventory` 17→18 + `ct1_check_cli` mut-F2 sibling `cases`; DESIGN.md one-line symmetric-rule note. Boss folded into this GREEN commit: the two `[medium]` doc-honesty edits the audit also surfaced (`mut_var_allocas` rustdoc now states its mut-var+loop-binder dual use truthfully; `Term::Loop` doc-comment now describes the real shipped iter-2/3 typecheck+alloca+mem2reg state, not iter-1's stale "per-binder phi" stub forward-look) + a `[low]` P2 roadmap `[todo]` (the recon-undercount 3×-pattern → an `ailang-plan-recon` agent-definition countermeasure; pairs with planner Step-5 items 7+8). Bench gate (audit Step 2): `check.py`/`compile_check.py`/`cross_lang.py` all exit 0, 25 metrics 0 regressed — **pristine** (strictly-additive surface, no hot-path; the pre-existing P2 `*.bump_s` hardware-staleness drift did not trip) → carry-on, no baseline/ratify. `cargo test --workspace` 619→622 / 0 red (Boss-reran independently incl. hash_pin 11/0 — the ast.rs/codegen doc-comment edits moved no canonical-JSON hash). **The loop/recur milestone is audit-clean** — architect drift cleared, bench pristine; the only remaining pipeline step before full close is `fieldtest` (loop/recur is user-visible Form-A surface) → 2026-05-17-iter-loop-recur.tidy.md
- 2026-05-18 — fieldtest loop-recur (milestone CLOSE, clean on all milestone axes): post-audit downstream-LLM-author field test of the shipped loop/recur surface. The fieldtester (DESIGN.md + public examples only, never compiler source) wrote 3 real iterative programs — integer Newton sqrt (multi-binder, recur buried in if/let/if, loop result into let/seq), Collatz length (recur in match-arm tails), Euclidean gcd (loop as a value sub-expression in argument position) — plus 5 plausible-mistake negatives and 2 no-termination probes, all run through the public `ail` CLI. **0 bugs; 4 working findings, all on the milestone's own axes**: the five rejection diagnostics (recur-outside-loop / -arity / -type / -not-in-tail / loop-binder-captured-by-lambda) are point-exact AND self-fixing (name the fn, describe the fix, no false positives, no crash — the clause-2 silent-failure-class-becomes-compile-error claim made real); recur tail-position threads correctly through match-arm/let/outer-if (the spec only demonstrated `if` — generalises exactly as an author assumes); loop composes as a value sub-expression everywhere + every loop/recur fixture is `ail parse|render|parse` byte-identical (Roundtrip Invariant holds in practice); the no-termination boundary is exactly as specified (infinite loop check+build clean, zero spurious diagnostics — the precise difference from the reverted Iteration-discipline milestone). This empirically substantiates the milestone's "an LLM author can now write iterative programs with loop/recur" claim — the gate fieldtest exists to provide. Two ORTHOGONAL non-blocking findings surfaced incidentally while probing the no-termination/negative axes, neither in loop/recur scope, both routed to P2 todos (not a loop/recur tidy — refusing the scope creep): (spec_gap) niladic `(app f)` is unrepresentable in Form A and DESIGN.md's `Term::App args:[Term...]` states no minimum — INDEPENDENTLY re-confirms the existing mut-local-F3 roadmap todo (two fieldtests now hit it; the accept-`(app f)`-vs-ratify-DESIGN.md decision is a genuine design fork deliberately NOT auto-ratified under autonomous /boss — parked, priority-strengthened); (friction) the module-level `(doc …)` rejection lists valid def heads but omits that doc attaches inside fn/data — a one-line diagnostic-hint tidy. Independent Boss verification: `loop_recur_4_gcd_value_pos.ail` re-run → stdout `27`; `loop_recur_3a` re-run → `ail check` exit 1 `[recur-outside-loop] countdown: …`. **The standalone loop/recur milestone is fully ratified and CLOSED**: 3 iterations + a tidy shipped, audit clean (architect drift resolved, bench pristine carry-on), fieldtest clean on every milestone axis. Fixtures `examples/fieldtest/loop_recur_*.ail` + spec `docs/specs/2026-05-18-fieldtest-loop-recur.md` committed; roadmap P0 entry marked closed; the two orthogonal findings tracked as P2 todos → 2026-05-18-fieldtest-loop-recur.md
- 2026-05-18 — iter prose-loop-binders.1 (single-iteration milestone, DONE): Form-B prose `loop` now renders binders as a parenthesised init-list on the keyword line — `loop(acc = 0, i = 1) { … }` — instead of bare `name = init;` statements inside the body block, which had implied C/Rust re-init-every-iteration semantics. Projection-only single-arm rewrite of the `Term::Loop` case of `write_term` (`crates/ailang-prose/src/lib.rs`); init exprs render at `level` (keyword line, not the indented block), mirroring the untouched adjacent `Term::Recur` `enumerate()`+`", "` idiom — making the binder list positionally isomorphic to `recur(...)`, which teaches the correct "recur re-binds these positionally" model instead of obscuring it. RED-first via the existing stem-explicit `check_snapshot` harness: two new committed `examples/{loop_sum_to_run,loop_forever_build}.prose.txt` whole-file byte-equality fixtures + two `snapshot_loop_*` `#[test]` fns, verified `0 passed; 2 failed` against the old renderer before the arm rewrite turned them green. Loop/recur AST, Form-A, JSON-AST, typecheck, codegen and the Form-A↔JSON round-trip invariant untouched by construction (no signature change, no caller touched, prose is a one-way lossy projection off the round-trip path — spec §Architecture states this explicitly so milestone-close audit does not chase a phantom). Full `ailang-prose` suite 10/0; both `ail prose | diff` live-CLI byte-matches `MATCH`; post-cleanup porcelain exactly the expected paths. Surfaced from the 2026-05-18 user prose review of the just-closed loop/recur milestone; brainstorm spec `docs/specs/2026-05-18-prose-loop-binders.md` (Step-7.5 grounding-check PASS), plan `docs/plans/prose-loop-binders.1.md`. Both implement review phases first-pass, 0 re-loops. Milestone-close `audit` is the Boss's next pipeline step (projection-only, no Form-A surface change → no fieldtest). → 2026-05-18-iter-prose-loop-binders.1.md
- 2026-05-18 — audit prose-loop-binders (milestone CLOSE, clean / carry-on): milestone-close tidy for the single-iteration prose-loop-binders milestone (range `6cbd0fe..c9355d7`). Architect drift review **clean** — zero drift, zero debt, verified against the diff not journal trust: single `Term::Loop` `write_term` hunk in `crates/ailang-prose/src/lib.rs`, the two non-render `Term::Loop` sites + all `Term::Recur` arms absent from the diff, DESIGN.md/PROSE_ROUNDTRIP.md make no claim about prose `loop` rendering (so the spec's "no doc edit needed" is correct not a skipped obligation), no carve-out/hash-pin/round-trip lockstep bypassed (prose is off the Form-A↔JSON round-trip path by construction). Bench: `compile_check.py` 0/24 + `cross_lang.py` 0/25 both **exit 0** (cross_lang's independent `ail_bump_s` +1.50% ±15% ok corroborates check.py's bump_s firing is baseline-staleness not a real regression); `check.py` exit 1, first run 3 regressed but a confirmatory identical re-run (no code change) returned `latency.implicit_at_rc.p99_9_us` +37.30%→+10.22% **ok** and `.max_us` +114.14%→+22.17% **ok** (median/p99 within tol both runs) — the two latency-tail firings proven single-sample 5-run RC-mode jitter by re-run; `bench_list_sum.bump_s` persisted ~+12% = the already-tracked P2 roadmap todo (byte-oracle-proven environmental at the 2026-05-16 iteration-discipline-revert audit, causally impossible to attribute to a projection-only `ailang-prose` change). **All items carry-on**: no fix iteration, no baseline ratify — explicitly NOT bumping the `*.bump_s` baseline opportunistically inside an unrelated projection-only milestone (the P2 todo owns a holistic recapture; piecemeal bump is the "regression is expected" rationalisation the Iron Law forbids). Fieldtest **not applicable / not dispatched**: projection-only change to the Form-B *reading* surface; the fieldtester authors `.ail` Form-A and would not exercise the prose-render change (no friction/bug/spec-gap axis); the two whole-file byte-equality `.prose.txt` snapshots + live-CLI `ail prose|diff` checks are the projection-only E2E gate. Milestone **CLOSED clean**; roadmap P0 entry flipped `[~]``[x]`, WhatsNew.md user-facing entry appended (user present → no notify.sh fired per notification policy). → 2026-05-18-audit-prose-loop-binders.md
+17 -12
View File
@@ -35,19 +35,24 @@ work progresses.
## P0 — In flight
- [~] **\[milestone\]** Prose `loop` binders — projection redesign.
Form-B prose currently renders loop binders as `name = init;`
statements *inside* the `loop { … }` body, which misreads as
C/Rust re-init-every-iteration semantics. Move them to a
parenthesised init-list on the keyword — `loop(acc = 0, i = 1)
{ … }` — positionally isomorphic to `recur(...)`. Projection-only:
one `ailang-prose` `write_term` `Term::Loop` arm + 2 snapshot
fixtures/tests; loop/recur AST, Form A, JSON-AST, typecheck,
codegen, and the Form-A↔JSON round-trip invariant all unchanged.
Spec'd + approved 2026-05-18; Step-7.5 grounding-check PASS. Plan
next.
- [x] **\[milestone\]** Prose `loop` binders — projection redesign
CLOSED 2026-05-18. Form-B prose now renders loop binders as a
parenthesised init-list on the keyword (`loop(acc = 0, i = 1)
{ … }`, positionally isomorphic to `recur(...)`) instead of bare
`name = init;` statements inside the body block (which misread as
C/Rust re-init-every-iteration). Single-iteration, projection-only:
one `ailang-prose` `write_term` `Term::Loop` arm + 2 committed
byte-equality `.prose.txt` snapshots/tests; loop/recur AST, Form A,
JSON-AST, typecheck, codegen, and the Form-A↔JSON round-trip
invariant all byte-unchanged. brainstorm→plan→implement
(`c657e74`/`6533134`/`c9355d7`); milestone-close `audit` clean
(architect zero drift; bench carry-on — sole firing is the tracked
P2 `*.bump_s` environmental staleness, latency-tail proven sampling
noise by re-run); no fieldtest (projection-only, no authoring-
surface change). Stays here briefly for context; remove once stale.
- context: `docs/specs/2026-05-18-prose-loop-binders.md`;
surfaced from the loop/recur prose review (2026-05-18 chat).
`docs/journals/2026-05-18-audit-prose-loop-binders.md`; surfaced
from the loop/recur prose review (2026-05-18 chat).
- [x] **\[milestone\]** Standalone `loop` / `recur` — CLOSED 2026-05-18.
Strictly-additive strict-iteration surface, `recur` tail-position-only,