Tasks 1-3 of docs/plans/embedding-abi-m1.1.md, a verified known-good
subset (cargo build --workspace exit 0; full workspace green; the
roundtrip_cli all-examples gate green):
- Task 1: CLI build-path MissingEntryMain baseline pin
(examples/embed_noentry_baseline.ail + embed_missing_main_baseline.rs;
triple-assertion, layered on the green codegen-unit pin lib.rs:3314).
- Task 2: additive FnDef.export: Option<String> (serde default +
skip_serializing_if, modelled on FnDef.doc); compile-driven thread
of export: None across ~85 FnDef/AstFnDef literals / 18 files incl.
all in-source #[cfg(test)] mod tests + drift/hash/spec-drift
literals + the codegen lib.rs:3320 in-source pin; hash-stability
golden pin; DESIGN.md fn-JSON "export" line. DONE_WITH_CONCERNS
(plan symbol content_hash_fn -> def_hash per hash_pin.rs, plan-
pseudo-vs-reality class, plan corrected).
- Task 3: Form-A (export "<sym>") modifier — parse_export +
parse_fn thread + write_fn_def emission + form_a.md grammar;
byte-identical round-trip.
Task 4 BLOCKED (Boss plan-defect: fixtures declared module != file
stem; AILang's loader hard-enforces module==stem at workspace.rs:438,
so ail check panicked on load before the gate). Boss resolution
(Option 2, overriding the orchestrator's Option 1, rationale in the
journal + plan Design-decision 6): keep descriptive embed_* filenames,
rename fixture MODULES to their stems — the C symbol backtest_step
stays via (export ...), demonstrating spec Decision 2's mangling-
decoupling rather than violating it; archive becomes
libembed_backtest_step.a, internal symbol @ail_embed_backtest_step_step,
host.c unchanged. Plan fixed (Design-decision 6 + Task-4 module==stem
+ Task-5/6/7 dependent strings + the no-*. Float-head + content_hash_fn
concerns folded). Headline fixture corrected; blocked-Task-4 WIP
discarded (recreated by the [4,7] re-dispatch from the fixed plan).
PARTIAL commit (not the iter's final commit): the implement Repair
path mandates committing the known-good subset so the [4,7]
re-dispatch starts from a clean tree that includes Tasks 1-3 (Tasks
4-7 structurally depend on the schema field + surface). INDEX line
added when the full iter lands post-re-dispatch.
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
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.
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).
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.
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.
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.
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).
Single iter shipping the post-milestone-24 follow-up named in
docs/specs/2026-05-14-retire-per-type-print-effects.md. After this
iter the only surviving direct-output effect-op is `io/print_str`;
all per-type print primitives are replaced by the polymorphic
`print` helper (prelude, iter 24.3).
Components:
- 92 examples/*.ail fixtures migrated (do io/print_<T> x) →
(app print x); 6 .prose.txt snapshots regenerated via `ail prose`.
- Four-site lockstep compiler deletion: crates/ailang-check/src/builtins.rs
(3 effect_ops.insert blocks + 3 list() rows + the
install_io_print_float_signature test + module + EffectOpSig
doc-comments); crates/ailang-codegen/src/lib.rs lower_app
(3 arms + lowers_io_print_float test); crates/ailang-codegen/src/synth.rs
builtin_effect_op_ret match-arm pattern. Dead `intern_string`
helper removed as a follow-up.
- Five incidental test-body migrations (ailang-check x2, ailang-core
spec_drift + design_schema_drift, ailang-surface/src/lex.rs,
ailang-prose/src/lib.rs round-trip test).
- Cat B test-harness patch: six IR-shape tests in
crates/ail/tests/e2e.rs gained a monomorphise_workspace call
before lower_workspace_with_alloc so they follow the same
pipeline as `ail build` (the home-rolled desugar+lift loop
stayed because mono's precondition is "already lifted"; mono
inserts after lift).
- Six doc-comment touch-ups (lex.rs module doc, parse.rs
diagnostic example, ail/src/main.rs x2, runtime/str.c %g anchor,
crates/ailang-core/specs/form_a.md surface-spec example).
- DESIGN.md seven-site sweep (Decision 11 example, Polymorphic
print past-tense, Heap-Str output sentence, effect-op
invocation comment, Float NaN paragraph re-anchored on
float_to_str, two "What is supported" lists).
- Three E2E test-comment polish + four IR-snapshot refresh + one
canonical-hash pin update (plan-unanticipated downstream
consequences of the corpus migration).
- bench/{check,compile_check,cross_lang}.py: all exit 0; no
ratification needed.
- Roadmap entry struck through; per-iter journal at
docs/journals/2026-05-14-iter-rpe.1.md.
Tests 564/0/3. cargo clippy and cargo doc: zero warnings.
Two upstream codegen bugs surfaced during the first BLOCKED
attempt and were fixed in separate iters before this retry:
- 1fb225e bugfix: mono cursor misalignment at poly-free-fn Var
with class-constrained Forall.
- feb9413 bugfix: print leak — propagate ret_mode through rigid
substitution + prelude Show.show ret_mode.
Known debt (carried forward for next /audit):
- Emitter.strings field is functionally dead post-iter (orphan
after intern_string removal); cycles over empty map harmlessly.
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.
Six-task post-fieldtest documentary tidy. No production-code behaviour
changes; 559 tests green at every per-task gate.
T1-T3: form_a.md additions
- §Definitions intro "Three kinds" -> "Five kinds".
- New `### Class — (class ...)` subsection: EBNF carries optional
superclass (0 or 1 per ClassDef.superclass schema), method
signatures with optional defaults; anchored to
examples/test_22c_user_class_e2e.ail (ok 24/2).
- New `### Instance — (instance ...)` subsection: EBNF carries the
(method NAME (body LAM-TERM))* shape; canonical-form CLASS-REF
rule explicitly stated (bare same-module / qualified cross-module);
two examples — same-module abbreviated from
mq3_class_eq_vs_fn_eq_classmod.ail and cross-module qualified
from show_user_adt.ail; bare-cross-module-class-ref diagnostic
named inline.
- §Types `(forall ...)` line extended with optional `(constraints
(constraint CLASS-REF TYPE)+)?` clause; explanatory paragraph
added with no-instance diagnostic anchor; fifth example added
to the Examples block anchored to cmp_max_smoke.ail.
T4: docs/specs/2026-05-13-form-a-default-authoring.md "seven
carve-outs" → "eight carve-outs" at 6 sites contradicting the
§C4(b) compile-time-embed amendment (commit 9fcda8b). Sites:
preamble line 11, §C1 line 170, §C2 line 191, §C3 line 218,
§"Data flow" lines 363 + 374. Post-edit grep returns 4 surviving
"seven" lines (233, 238, 463, 469), all correctly §C4(a)-scope
or arithmetic/future-state.
T5: crates/ailang-core/src/hash.rs:50-57 — delete empty
`#[cfg(test)] mod tests {}` placeholder + 6-line relocation
comment. Tests live in crates/ailang-core/tests/hash_pin.rs
since form-a.1 T5; placeholder served no purpose. hash_pin.rs
still 10/10 passing.
T6: crates/ailang-surface/tests/round_trip.rs — module-level
`//!` and inner `///` docstrings rewritten to the post-T10
four-property framing (parse-determinism / idempotency /
CLI-pipeline-idempotency / carve-out-anchor) instead of the
retired Direction-1/Direction-2 language. Sibling-crate
breadcrumbs added pointing at the other two enforcement points
(roundtrip_cli.rs, carve_out_inventory.rs).
Drift item B2 (audit-form-a "plan file two sites") dropped per
recon verification: docs/plans/2026-05-13-iter-form-a.1.md
contains zero defective "seven" sites; all four hits are
internally scoped to §C4(a), arithmetic, or future-state.
Decision recorded in the journal.
Closes 2 of 3 fieldtest-form-a spec_gap findings (#2 form_a.md
typeclass surface + #3 form_a.md class-qualifier rule for
instance) and 3 of 4 audit-form-a drift items.
First half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All five tasks DONE; cargo
test --workspace green at every per-task boundary.
T1 — Add three new tests:
- parse_is_deterministic_over_every_ail_fixture (round_trip.rs)
- cli_parse_then_render_then_parse_is_idempotent (roundtrip_cli.rs)
- carve_out_inventory.rs (new file; #[ignore]'d until T8 deletion)
T2 — Bulk-render the 99 missing examples/<stem>.ail files via
`ail render`. Corpus 58 .ail (pre-iter) -> 157 .ail. Eight .ail.json
carve-outs (7 §C4(a) subject-matter + 1 §C4(b) prelude) preserved.
One re-loop triggered: load_workspace prefers .ail siblings since
ext-cli.1, so the newly-rendered imports broke seven Group-A entries
whose JSON entry-paths now resolved imports to fresh .ail. Repair:
pre-emptive forward-pull of five T3 migrations + 4 transient
#[ignore]s on workspace.rs mod tests (cleanly relocated in T5).
T3 — 14 Group-A test files migrated from ailang_core::load_workspace
to ailang_surface::load_workspace + .ail paths. Carve-out lines
preserved verbatim (7 sites in typeclass_22b2.rs / typeclass_22b3.rs).
T4 — 12 Group-B test files: bulk regex flip on build_and_run /
build_and_run_with_alloc / build_and_run_with_rc_stats call sites
(~70 e2e.rs invocations + 11 subprocess sites). Four files
mis-classified Group-A as Group-B in plan recon (mono_hash_stability,
prelude_free_fns, print_mono_body_shape, show_mono_synthesis); two
files mis-classified Group-B as Group-A (mono_recursive_fn,
mono_xmod_qualified_ref). Migrated per actual shape, not plan label.
T5 — Relocated #[cfg(test)] mod tests from production source to
integration test crates with ailang-surface dev-dependency:
- crates/ailang-core/tests/hash_pin.rs (10 tests from hash.rs)
- crates/ailang-core/tests/workspace_pin.rs (10 non-carve-out tests
from workspace.rs)
- crates/ailang-codegen/tests/eq_primitives_pin.rs (3 tests from
codegen/src/lib.rs:3717-3799)
- ailang-prose/tests/snapshot.rs migrated (helper + 8 fixtures) to
.ail + ailang_surface::load_module
Carve-out tests in workspace.rs mod tests preserved in-place
(3× 22b2 + 3× ct1 = 6 tests). Tempdir-based loader-mechanism tests
(3 sites) also preserved — they don't consume examples/.
Tests: 560 passed, 0 failed, 4 ignored (was 558 + 3 T1 new -
1 transit carve_out_inventory #[ignore] = 560 active).
Tasks 6-12 (bench-driver suffix flips, e2e diff-test rewrite + 4
additional raw-JSON-inspect handlers, .ail.json deletion, retiring
obsolete roundtrip tests + schema_coverage corpus flip, §C3
DESIGN.md restatement, §A4 doctrine edits, WhatsNew + roadmap
strike) ship in the next dispatch on this iter ID.
Known debt inherited to T6-12: 4 raw-JSON-inspect tests in e2e.rs
(borrow_own_demo / reuse_as_demo / render_parse_round_trip_canonical
/ ail_run_accepts_ail_source_with_same_stdout_as_ail_json dual-form
smoke) need rewrite or #[ignore] before T8 deletion; recorded in
journal Concerns + Known debt sections.
Closes the loop opened by this morning's .ailx → .ail rename. Every
path-taking ail subcommand (check, build, run, manifest, render,
prose, merge-prose, describe, emit-ir, diff, workspace, deps) now
accepts a .ail (Form A) input as well as .ail.json (Form B). For
.ail paths the subcommand parses through ailang_surface::parse
in-line and proceeds with the same loaded Module value .ail.json
would have produced; binary semantics are extension-agnostic.
Architecture: a new ailang_surface::{load_module, load_workspace}
pair dispatches on file extension. A new injection point
ailang_core::workspace::load_workspace_with<F> lets the surface
crate plug in an extension-aware loader without core having to
import surface (which would close a crate cycle). Import resolution
in workspace::visit now prefers a .ail sibling over a .ail.json
sibling. The CLI binary's 18 callsites switched to the surface
loaders. Surface parse failures route through
workspace_error_to_diagnostic as a new SurfaceParse variant of
WorkspaceLoadError, so `ail check --json foo.ail` on a malformed
.ail returns a parseable JSON diagnostic with code
surface-parse-error instead of the misleading
`json: expected value at line 1 column 1` fall-through.
Boss pre-commit correction: the implementer followed the plan
verbatim and used snake_case `surface_parse_error`, but every
existing diagnostic code in the codebase is kebab-case (verified
via `git grep` over crates/ailang-check/src and crates/ail/src);
realigned to `surface-parse-error` at three sites (the diagnostic
arm + two ct1 test strings). Journal Concerns section records
the correction.
Tests: cargo test --workspace 487 green (+7 from this iter:
1 core unit, 4 surface integration, 1 ail E2E pair, 1 ct1 CLI
diagnostic-shape). bench/check.py, compile_check.py, cross_lang.py
clean on re-run; the latency.{explicit,implicit}_at_rc tail
cluster occasionally flagged on first runs is the same
nondeterministic cluster the previous two audits documented;
baseline left pristine for the third consecutive iter.
Roadmap follow-up "ail check/build/run accept .ail extension" (P2)
removed.
DESIGN.md §Decision 6 / "CLI" bullet gained the symmetric upward
sentence: both .ail and .ail.json are first-class CLI inputs.
The surface-form file extension changes from .ailx to .ail. AILang's
authoring surface now uses the same .ail stem as its canonical JSON
form (.ail.json), giving the language a single coherent extension
family: .ail is the LLM-authored Form A, .ail.json is the canonical
JSON-AST Form B.
Scope (touched):
- 61 example renames examples/**/*.ailx → .ail (git mv)
- 1 rename experiments/.../rendered/ailx.md → ail.md
- 35 content-edited live-toolchain files (crates/, docs/DESIGN.md,
docs/roadmap.md, docs/PROSE_ROUNDTRIP.md, skills/, bench/reference/*.c,
experiment crates under experiments/.../{render,harness,master})
- Experiment-crate cohort rename Cohort::Ailx → Cohort::Ail,
Form::Ailx → Form::Ail, per_cohort/ailx → per_cohort/ail,
{form-only: ailx} → {form-only: ail}, ```ailx → ```ail
Out of scope (deliberately untouched, to preserve honest history):
- docs/journal-archive.md (content-frozen per CLAUDE.md)
- docs/journals/, docs/specs/, docs/plans/, bench/orchestrator-stats/
- experiments/.../runs/ (frozen LLM-output artefacts; models actually
saw .ailx — renaming would falsify the experimental record)
Verification: cargo build/test --workspace green; experiment crate
cargo test green; bench/check.py + compile_check.py + cross_lang.py
all 0-regressed; negative grep for ailx|Ailx|AILX outside the
out-of-scope paths returns zero matches.
Opens immediate follow-up: roadmap.md P2 todo `ail check`/build/run
accept .ail extension — after this rename, .ail is canonical
authoring surface but the CLI still produces a misleading JSON-parse
error on `ail check foo.ail`. That's the next iter.
Milestone-close audit found three drift items (architect) plus
two carry-on items; bench all-green.
Fixes (rt.tidy, boss-direct):
- 5th test parse_then_print_then_parse_is_idempotent_on_every_ailx_fixture
enforces Direction 2 of the Roundtrip Invariant directly. The
DESIGN.md enforcement list grows from four to five tests.
- docs/roadmap.md P1 'Round-trip completeness invariant' entry
retired with one-line journal mirror per roadmap convention.
- DESIGN.md wording sync: <16-hex> → <16-lowercase-hex> for the
Float-bits-hex spelling in the new section (matches §Data model).
Milestone closed clean; five workspace-wide tests anchor the
.ail.json ↔ .ailx bijection plus AST-variant coverage.
Three new reader-only tests pin the .ail.json / .ailx bijection
plus AST-variant coverage. All three passed first observation
across the current corpus — no roundtrip, schema-coverage, or
CLI-drift gaps surfaced.
- every_ailx_fixture_matches_its_json_counterpart (replaces the
3-pair handwritten exhibits): 57 paired .ailx fixtures pass.
- every_ast_variant_is_observed_in_the_fixture_corpus (new): 34/34
AST variants exercised across 136 fixtures; exhaustive matches
without _ wildcard so AST drift fails the build.
- cli_render_then_parse_preserves_canonical_bytes_on_every_fixture
(new): 136 fixtures round-trip through ail render → tempfile →
ail parse with BLAKE3 identity on canonical bytes.
No production code, no DESIGN.md changes (those follow in later
iterations). Tests are pure readers of the repo per spec
acceptance #7 — tempfile crate added as workspace dep for the CLI
roundtrip's intermediate file outside the repo.
Adds seven fixtures under examples/test_22b1_* exercising the
workspace-load coherence checks plus four positive/negative tests in
ailang-core/src/workspace.rs:
- iter22b1_instance_in_class_module_loads_clean: positive case;
test_22b1_orphan_class.ail.json declares Show + instance Show Int
in the same module (the class's). Registry has one entry.
- iter22b1_orphan_instance_fires_diagnostic: declares Show in
test_22b1_orphan_third_classmod, declares instance Show Int in
test_22b1_orphan_third (a third module). Fires OrphanInstance.
- iter22b1_duplicate_instance_fires_diagnostic: per the JOURNAL hint,
module A defines class Show + instance Show MyInt (legal, A is
class's module), module B defines type MyInt + instance Show MyInt
(legal, B is type's module). Entry imports both. Fires
DuplicateInstance with first/second_module distinct.
- iter22b1_missing_method_fires_diagnostic: class Eq with two
non-default methods (eq, ne), instance Eq Int specifies only ne.
Fires MissingMethod { method: "eq" }.
The surface round-trip gate (ailang-surface/tests/round_trip.rs)
filters out test_22b1_* fixtures because the Form-B parser arms for
ClassDef/InstanceDef are deferred to 22b.4. Without the filter, every
fixture's printed text would re-parse-fail on the unknown `class` /
`instance` head.
Test count: 291 → 295 (4 new workspace tests, all green).
Adds Def::Class(ClassDef) and Def::Instance(InstanceDef) variants per
Decision 11 §"Form-A schema". All optional fields (superclass, doc,
default body) carry skip_serializing_if so canonical-JSON bytes of
pre-22b fixtures stay bit-identical.
Downstream match-on-Def sites are extended with explicit Class/Instance
arms. Behaviour for 22b.1 is placeholder (skip in typecheck/codegen,
one-line summary in pretty/manifest, placeholder marker in prose/print)
with TODO comments naming the deferred sub-iters (22b.2 typecheck,
22b.3 codegen, 22b.4 prose-projection arms).
When a fn-param is annotated (own T) but the body never consumes
it (consume_count == 0) and never destructures it via match, the
linearity pass now emits a Severity::Warning diagnostic with a
form-A-rendered (borrow T) rewrite as suggested_rewrite. Pure
advisory, Decision 10 unchanged.
First Warning-severity diagnostic the typechecker emits; three
CLI exit paths gated on Error-only.
JOURNAL: design entry + iter 19a shipping entry.
Closes the 18-arc's stack-recursion limit. Recursive drop
cascades from 18c.4 overflow on long ADT chains (Linux's 8 MB
default stack maxes out around 1M cells of List). The new
opt-in (drop-iterative) annotation on a Def::Type switches the
synthesised drop_<m>_<T> body from recursive to iterative-with-
explicit-worklist for that type.
Schema:
- TypeDef.drop_iterative: bool. Default false; serde-skip
when false so existing fixtures' canonical JSON hashes stay
stable.
- Form-A: (drop-iterative) clause inside (data T ...).
Worklist runtime (4 new ABI symbols in runtime/rc.c):
- ailang_drop_worklist_new(initial_capacity)
- ailang_drop_worklist_push(wl, ptr)
- ailang_drop_worklist_pop(wl) -> ptr
- ailang_drop_worklist_free(wl)
Heap stretchy buffer, doubling on overflow, null-filtering on
push. Lean 4 / Roc precedent documented in the runtime; the
slot-repurposing strategy was considered and rejected because
not every box has a free pointer-typed slot to thread the
worklist through (Cons head is i64, slot 1 is ptr but it's
the field we're following — no free slot).
Codegen (emit_iterative_drop_fn_for_type): for a
drop_iterative type, drop_<m>_<T>(ptr %p) emits a worklist
loop. Fields of the SAME annotated type push onto the
worklist (mono-typed); fields of DIFFERENT types call their
own drop fn directly (recursive on those, but only if THEY
are themselves recursive — i.e. one level of cascade jump
maximum). Mono-typed-worklist is sound for the deep-self-
recursion case the iter targets (List of List of T just
needs the spine flattened).
Tests:
- examples/rc_drop_iterative_long_list — 1M-cell List of Int
with (drop-iterative) annotation.
- alloc_rc_drop_iterative_handles_million_cell_list E2E —
builds + runs under --alloc=rc, asserts clean exit. With
annotation: exits 0. Without annotation (control): SIGSEGV
at exit code 139 (verified by hand). Worklist is load-
bearing.
- iter18e_drop_iterative_emits_worklist_body_no_self_recursion
IR-shape: worklist body has br to loop_head AND no direct
recursive call into drop_<m>_<T>.
- iter18e_no_annotation_keeps_recursive_drop_body — control:
unannotated variant still emits the 18c.4 recursive shape.
- 3 surface parse-tests for the annotation round-trip.
Test deltas: e2e 58 -> 61 (+3), surface 18 -> 21 (+3). All
other buckets unchanged. cargo test --workspace green.
Known debt (deliberate):
- Mono-typed worklist: cross-type drop-iterative fields call
the other type's drop fn directly. A heterogeneous
worklist would be more general but adds tag tracking
complexity for a case (drop-iterative T containing
drop-iterative T') that's narrower than the deep-self-
recursion target. Documented in
emit_iterative_drop_fn_for_type's doc.
- Closure / Type::Var / Type::Forall fields fall back to
shallow ailang_rc_dec via field_drop_call — same as the
recursive variant.
- Dynamic-tag partial-drop fallback (head_or_zero epilogue
shallow dec when moved_slots non-empty) — out of scope per
brief.
Schema floor for explicit reuse hints. Adds Term::ReuseAs
{ source: Box<Term>, body: Box<Term> } with serde tag "reuse-as",
form-A (reuse-as <source> <body>). Schema choice (wrapper, not
modifier-on-Ctor) and rationale recorded in DESIGN.md.
Codegen is identity under all --alloc strategies — lower body,
drop source on the floor. Iter 18d.2 will lower this as the
in-place rewrite under --alloc=rc.
User-facing diagnostics:
- typecheck reuse-as-non-allocating-body: body must be a
Term::Ctor or Term::Lam, the two AST shapes that allocate.
Other shapes (literal, var, app, ...) emit this with a
suggested_rewrite that drops the wrapper.
- linearity reuse-as-source-not-bare-var: source must be a
Term::Var referring to an in-scope binder. Anything else
(literal, nested expression) is rejected here. Suggested
rewrite drops the wrapper.
- linearity use-after-consume at a reuse-as site: source must
not have been consumed earlier in the body. Suggested rewrite
drops the wrapper.
- Visiting reuse-as marks source consumed for the rest of the
body, so a subsequent use of the same binder flags
use-after-consume against it.
Uniqueness inference treats source as Consume — the consume
shows up in the side table for the codegen consumer.
Tests:
- 4 surface parse-tests (round-trip, no-args, one-arg, full
parse_term/term_to_form_a round-trip).
- 1 typecheck unit test (non-allocating body).
- 2 linearity unit tests (non-var source, after-consume).
- 1 integration test (happy-path map_inc in all-explicit mode
is linearity-clean).
- 1 E2E test running the canonical map_inc-via-reuse-as fixture
under --alloc=gc, asserting stdout 9.
- New fixture examples/reuse_as_demo.{ailx,ail.json}.
Test deltas: E2E 55 -> 56, ailang-check unit 43 -> 46,
ailang-check workspace 8 -> 9, surface 14 -> 18. cargo test
--workspace green. No existing fixture's canonical JSON changed.
Adds a per-fn linearity check that runs on every fn whose
param_modes are all explicit (Borrow or Own, no Implicit), tracks
each binder's consume/borrow state through the AST, and emits
two diagnostics with form-A `suggested_rewrites`:
- `use-after-consume` — a binder is referenced after it has
already been consumed.
- `consume-while-borrowed` — a binder is consumed while a borrow
of it is still live (sibling arg slot, or an enclosing-fn
Borrow param).
Fns with any Implicit param skip the check entirely; that's the
back-compat lane that keeps every existing fixture green. Pure
diagnostic addition: no IR change, no codegen change, no runtime
change.
Each diagnostic carries a non-empty `suggested_rewrites` whose
`replacement` is form-A AILang text (typically `(clone <name>)`).
Adds two new public entrypoints to ailang-surface — `parse_term`
and `term_to_form_a` — so the round-trip
`parse_term(term_to_form_a(t)) ≡ t` is the contract every
emitted replacement must satisfy. Tests assert the contract.
Linearity pass runs only on modules that typechecked clean (any
upstream typecheck error suppresses it for that module — running
on partly-defined IR would produce noise).
Tests: 3 unit tests in `linearity::tests`, 3 integration tests
in `ailang-check/tests/workspace.rs`, 1 serialisation test in
`diagnostic.rs`. `cargo test --workspace` green.
Adds (clone X) as a new Term variant. Form-A:
(let p (expensive_fn x)
(do consume_a (clone p)) ; explicit RC inc — needs 18c.3
(do consume_b p))
Schema floor for the LLM-aware RC design (Decision 10): explicit
clone is the author-visible alternative to implicit sharing. In
18c.1 the variant is purely additive — typechecker treats it as
identity (same type as inner), codegen lowers it to the inner
term's IR with no extra calls. Iter 18c.3 will replace the
codegen identity-arm with a single call:
Term::Clone { value } =>
let v = self.lower_term(value, ...)?;
self.emit("call void @ailang_rc_inc(ptr {v})");
Ok(v)
Match-arm count: ~10 sites across 6 files, all mechanical
structural recursion through `value`. The codegen `lower_term`
arm carries an explicit comment marking the 18c.3 emission seam.
Hash invariant: `(clone X)` uses a new serde tag "clone" that no
pre-18c.1 fixture contains. Every existing fixture's canonical
JSON is bit-identical (git diff examples/ empty).
New fixture examples/clone_demo.{ailx,ail.json}:
(let x 42 (do io/print_int (clone x)))
Stdout 42 under both --alloc=gc and --alloc=rc.
Tail-position propagates through clone (verify_tail_positions
treats `(clone tail-call)` as itself a tail call), per the
identity-of-clone semantics in 18c.1.
Verified:
- cargo build --workspace clean
- cargo test --workspace green: 52 e2e (+1), 14 surface parse
(+2 for clone tests), all other buckets unchanged
- ail render round-trip exact
- clone_demo --alloc=gc → 42; --alloc=rc → 42
Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:
Type::Fn { params, param_modes, ret, ret_mode, effects }
enum ParamMode { Implicit, Own, Borrow }
This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).
Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.
Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.
New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.
Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.
Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
Add `(let-rec NAME (params ...) (type ...) (body ...) (in ...))`
as a form-A surface and a `Term::LetRec` AST variant. The 16a
desugar pass lifts each LetRec whose body has no captures from
the enclosing scope to a synthetic top-level fn `<hint>$lr_N`
and substitutes the original name; typecheck and codegen never
see LetRec. Capture detection panics at desugar time, queued
for 16b.2.
Tests: 95 → 99 (+1 e2e local_rec_factorial_demo, +2 desugar
unit, +1 parse unit). The new fixture `examples/local_rec_demo`
runs `fact` at n=1, 3, 5 → prints 1, 6, 120.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reconsidered 14d's removal of Term::If. The decision was wrong.
"No redundancies" requires judgment; reducibility (if -> match)
is not redundancy in the strong sense. Term::If is a primitive
control-flow shape; bool branching is the second most common
shape after sequencing, and removing it cost 3x tokens on every
branch site (`(if c a b)` 4 tokens vs the match-on-Bool form
12 tokens).
Meta-pattern fixed: I had been treating user observations as
directives. User said "if is a subset of match"; I jumped to
remove it citing CLAUDE.md, with no independent conviction.
The leak appeared in 14f's JOURNAL prose ("three lines for what
if used to do in one"), which read as regret. Two feedback
memories saved (memory/feedback_user_suggestions_not_directives,
memory/feedback_no_nostalgia_for_removed_features) to head this
off.
Implementation: mechanical reverse-application of 14d's diff at
every site (AST, check including the 14e tail-position arm,
codegen 4 sites, surface parser/printer, pretty, CLI walker,
e2e test mutation). Removed lower_bool_match helper — it existed
only because 14d's migration shape needed codegen for non-ptr
match scrutinees; with Term::If back, match-on-Bool returns to
its pre-14d unsupported state. Three fixtures (sum, sort, max3)
restored to pre-14d shape. gc_stress (added in 14f) also
migrated back to (if ...) since it was authored under the wrong
constraint.
14e (musttail) and 14f (GC_malloc) verified intact in IR.
Hashes restored to pre-14d values:
- sum.sum: db33f57cb329935e
- sort.insert: 697fcb9f30f8633a
- max3.max: 65c45d6a45dd0a72
- max3.max3: 624b14429bf302f5
All other defs across all 18 fixtures keep their post-14f
hashes. Tests 80/80 green; cargo doc 0 warnings. LOC delta
+265/-295 net -30.
DESIGN.md Decision 7 preserved with a "Status: REVERTED" header
for audit trail. Form-(A) `if-term` production restored.
Plan: back to 15a (std_maybe stdlib module).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>