T1-5 dispatch journal surfaced 4 additional e2e.rs tests beyond
the originally-named diff_detects_changed_def that raw-read
.ail.json bytes and will break at T8 deletion:
- borrow_own_demo_modes_are_metadata_only (substring assertions)
- reuse_as_demo_under_rc_uses_inplace_rewrite (substring assertions)
- render_parse_round_trip_canonical (whole-corpus property already
pinned by T1's cli_parse_then_render_then_parse_is_idempotent —
retire as redundant)
- ail_run_accepts_ail_source_with_same_stdout_as_ail_json (dual-form
smoke — re-author to derive the .ail.json on-the-fly via ail parse
on a working fixture like hello.ail)
T7 grows from 4 steps to 7 steps; net test-count change −1 (retire
render_parse_round_trip_canonical). T7 close: 559 tests green.
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.
Iter form-a.1 of the form-a-default-authoring milestone, Boss-decided
strategy C. End-of-milestone iter — all 156 non-carve-out .ail.json
files render+delete in this iter, plus all consumer migrations, plus
the roundtrip-CI restatement, plus the §A4 doctrine edits, plus
WhatsNew + roadmap strike.
Sequence: additive-then-subtractive.
- T1-2: new T1/T4 tests (additive) + render 99 missing .ail.
- T3-5: consumer migration (Group A load_workspace + Group B
subprocess + Group C production-code unit-test relocation).
- T6: bench-driver suffix updates.
- T7: e2e.rs synthesised-diff re-author to derive sum.ail.json
on-the-fly via ail parse.
- T8: bulk delete 156 .ail.json (carve-outs preserved).
- T9: retire obsolete roundtrip tests + flip schema_coverage corpus.
- T10: §C3 DESIGN.md roundtrip invariant restatement (4 properties
named, 5 enforcement tests pinned).
- T11: §A4 doctrine edits — CLAUDE.md + DESIGN.md "What this
Decision does not do" sentence.
- T12: WhatsNew + roadmap strike + final cargo test + bench check
+ 8-file inventory verification.
cargo test --workspace expected green at every task boundary. The
carve_out_inventory test is added #[ignore]'d in T1 and un-ignored
in T8 (RED-pinned until deletion happens).
Boss will dispatch implement in two task-ranges: [1,5] then [6,12]
to keep orchestrator-agent context bounded.
Iter form-a.1 recon surfaced a load-bearing constraint the original
spec didn't anticipate: ailang-core embeds prelude.ail.json at
compile time via include_str! + serde_json::from_str (workspace.rs:417,
main.rs:474), and ailang-core cannot import ailang-surface to switch
the embed to .ail (crate-cycle, documented at loader.rs:9-13).
§C4 splits into two carve-out categories:
- (a) seven subject-matter carve-outs (unchanged, the original list)
- (b) one compile-time-embed carve-out: prelude.ail.json
Acceptance criteria #1, #2 and §T4 carve-out inventory test updated
from 7 to 8.
Roadmap: new [milestone] "Prelude embed: Form-A as compile-time source"
queued to retire §C4(b) by either splitting the workspace loader
(prelude-init moves to ailang-surface) or generating prelude.ail.json
via build.rs. examples/prelude.ail (rendered iter form-a.0) is the
dual-form symptom that milestone exists to resolve. Strike the [todo]
"Author examples/prelude.ail" entry — satisfied by iter form-a.0.
Re-grounding-checked PASS over six load-bearing assumptions (embed
sites verified at workspace.rs:417 + main.rs:474; crate-cycle
constraint real; eight carve-outs all on disk; prelude.ail green
under both roundtrip tests).
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.
Iter 0 of the form-a-default-authoring milestone. One file
rendered (examples/prelude.ail.json -> examples/prelude.ail via
`ail render`), zero source-code edits, zero test-infra changes.
The migration mechanism is validated on the hardest single file
in the corpus (prelude exercises every language feature: classes,
superclasses, polymorphic free fns, IO effects, ctor patterns,
loader auto-injection). The two auto-discovering roundtrip tests
in round_trip.rs pick up the new .ail via read_dir without test
edits; both must pass for the iter to close.
Task 1: render + verify both roundtrip tests green + workspace
sanity check (5 steps).
Task 2: per-iter journal + INDEX append (2 steps).
prelude.ail.json retained per spec §A2 — this is the singular
dual-form iter; iter 1 deletes it with the bulk test-infra
refactor.
Form A (.ail) becomes the sole authoring surface for every program
in the working tree. The seven negative-test JSON-AST fixtures are
the only post-milestone .ail.json files; everything else is rendered
to .ail and the JSON counterpart is deleted (single representation
per program).
Spec decides the four open design questions inline:
- A1: in-process parse via ailang_surface::parse, no build-time
target/ artefacts (would reintroduce a second representation).
- A2: prelude.ail ships as iter-0 pilot, validated by the existing
every_ail_fixture_matches_its_json_counterpart gate.
- A3: per-file deletion cadence; iter 0 is the singular dual-form
window because the JSON counterpart is the witness the CI gate
uses.
- A4: CLAUDE.md and DESIGN.md reworded — canonical vs authoring
forms separated, JSON-AST stays canonical/hashable, .ail becomes
the authoring entry point.
Roundtrip invariant restates from "two forms agree byte-for-byte"
to "parse is deterministic + idempotent under print"; carve-out
inventory test pins the seven JSON-only fixtures against silent
list drift.
Grounding-check PASS over 8 load-bearing assumptions.
Iteration scope: iter 0 = prelude pilot only (render
prelude.ail.json -> prelude.ail; do not delete the JSON yet; do
not touch CLAUDE.md/DESIGN.md/tests yet).
Documents the three iter-24.3 strengthenings as load-bearing
invariants and tightens two error-handling sites:
T1: DESIGN.md gains new subsection §Cross-module references in
synthesised bodies (between Resolution-and-monomorphisation and
Defaults-and-superclasses) documenting three invariants installed
in iter 24.3 — (1) MonoTarget::FreeFn::type_args carries canonical
types post-collection via normalize_type_for_lookup; (2) post-mono
synthesised body cross-module refs may bypass the source template's
import_map (codegen falls back to module_user_fns / module_def_ail_types);
(3) FreeFnCall synth pushes one ResidualConstraint per declared
forall-constraint with rigid vars substituted by fresh metavars.
T2: codegen_import_map_fallback_pin.rs (integration test) asserts
the synthesised prelude.print__<IntBox> body references
show_user_adt.show__<IntBox> AND prelude module's imports do not
contain show_user_adt — proving the cross-module ref bypasses
import_map at codegen.
T3: polyfn_dot_qualified_branch_pin.rs (integration test) asserts
bare-name print f (f : Int -> Int) fires exactly one no-instance
diagnostic at typecheck with zero unknown-variable diagnostics —
proving the bare-name resolution reaches the dot-qualified synth
branch where the constraint-residual push fires.
T4: check/lib.rs:2858 unwrap_or_default() replaced with .expect()
carrying the registry-coherence message — class_methods index
drift now surfaces explicitly rather than rendering NoInstance
with an empty method name.
T5: mono.rs gains apply_subst_and_normalize helper (Option<Type>
return) extracted from two byte-identical call sites at
collect_mono_targets and collect_residuals_ordered. Each call site
retains its own rigid-var / unit-default policy in the None arm
(site 1: rigid → has_rigid+break, unbound → Type::unit; site 2:
non-concrete → Type::unit). Byte-identity invariant on mono-symbol
hashes enforced by construction.
Tests: 558 passed (was 556 + 2 new pins). No production semantic
change — pure documentation + test pin + error-handling tightening
+ helper refactor. bench/cross_lang exit 0; bench/compile_check +
bench/check exit 0 this run (latency.implicit_at_rc / latency.explicit_at_rc /
bench_list_sum.bump_s noise envelope unobserved, lineage continues
at 10th consecutive observation without firing this run).
Tasks: T1 DESIGN.md new subsection §Cross-module references in
synthesised bodies documenting three iter-24.3 strengthenings
(canonical-form type_args, post-mono cross-module-ref import_map-
bypass, FreeFnCall constraint-residual push). T2 codegen import_map-
fallback pin (integration-style in crates/ail/tests/). T3 bare-name
poly-fn dot-qualified-branch invariant pin (asserts NoInstance fires
at typecheck not codegen). T4 replace unwrap_or_default() at
check/lib.rs:2852-2858 with expect(class_methods registry coherence).
T5 extract apply_subst_and_normalize helper from duplicated mono.rs
sites at :685-714 + :1284-1303. T6 integration + bench verification.
Boss decisions in Pre-flight section: T1 placement = new subsection
between DESIGN.md:1693 and :1695 (anchorable block beats append-in-
place); T2+T3 = integration-style tests (canonical xmod-test pattern,
not in-crate codegen); T5 helper extracts common concrete-resolved
arm only, returns Option<Type> so each call site keeps its rigid-
var / unit-default policy explicit.
Ships fn print : forall a. Show a => (a borrow) -> () !IO in the
prelude with explicit-let body \\x -> let s = show x in do
io/print_str s. Three new E2E fixtures + tests verify the full
path:
- show_print_smoke: 4 primitives smoke (print 42 / true / 'hello' /
3.14) — compile, run, expected stdout
- show_user_adt: data IntBox + instance prelude.Show IntBox +
print (MkIntBox 7) — stdout '7'
- show_no_instance: let f : Int -> Int = \\x -> x in do print f —
fires Show-aware NoInstance with DESIGN.md §Prelude(built-in)
classes cross-reference
IR-shape pin in print_mono_body_shape.rs asserts post-mono
print__Int.body is structurally Term::Let → App(show__Int) →
Do(io/print_str). Protects the explicit let-binder for the heap-Str
RC discipline per eob.1 Str carve-out.
Three plan-defects-fixed-inline surfaced during user-ADT E2E,
necessary repairs to make the spec's stated user-ADT trajectory
work end-to-end:
(a) mono.rs (2 sites): MonoTarget::FreeFn::type_args were carrying
bare type-cons references; normalised to canonical
<owner>.<bare> form via workspace_registry.normalize_type_for_lookup
so synthesised cross-module bodies' post-mono walks reach the
registry-keyed instance entries. Symmetric to the existing
class-method-arm normalisation.
(b) codegen/lib.rs (3 sites: resolve_top_level_fn, lower_app
cross-module arm, synth_with_extras Var arm): post-mono
synthesised bodies may carry cross-module references to modules
their source template didn't import (prelude.print__<UserType>
references show_user_adt.show__<UserType> even though prelude
does not import user modules). Fall back to direct
module_user_fns lookup when prefix not in import_map. Both
ends were independently typechecked before mono ran; the
cross-module ref is created by mono not by source.
(c) lib.rs synth FreeFnCall arm: walked Type::Forall.constraints,
substituted rigid vars with fresh metavars, pushed one
ResidualConstraint per declared constraint. Without this,
print f at Int -> Int would silently typecheck and fail with
a confusing 'unknown variable: show' at codegen rather than
fire the right typecheck-time NoInstance diagnostic.
NoInstance Float-aware arm in check/lib.rs:770-779 extended with
a parallel class == 'prelude.Show' branch that cross-references
DESIGN.md §Prelude(built-in) classes verbatim. Negative-fixture
test asserts code 'no-instance' + Show substring + Prelude-(built-in)-
classes substring.
DESIGN.md §Prelude(built-in) classes milestone-24 paragraph flips
fn print from 'ships in 24.3' to 'shipped in iter 24.3' with body
shape + pin file reference. §Float semantics gains a Show-Float
NaN-spelling cross-reference paragraph linking show 1.5 / show nan /
show inf to instance Show Float via float_to_str.
Roadmap P1 'Post-22 Prelude — Show + print rewire' flipped to [x]
with closing summary naming all three shipped iters (24.1 / 24.2 /
24.3). New P2 entry 'Retire io/print_int|bool|float effect-ops +
migrate example corpus to print' inserted at top of P2.
Tests: 556 passed (was 552 + 4 new). bench/cross_lang exit 0;
bench/compile_check + bench/check exit 1 on documented noise-class
metrics per the audit-cma lineage envelope (8th consecutive
audit-grade observation, baseline pristine per conservative-call).
Milestone 24 closes structurally with this iter. Standard audit
pipeline next.
Plan for iter 24.3, the second + final half of milestone 24.
Task 1: append fn print to prelude.ail.json with explicit-let body
\\x -> let s = show x in do io/print_str s. Task 2: mono symbol
IR-shape pin (AST-level Term-pattern match on post-mono
print__Int.body — protects the load-bearing let-binder for the
heap-Str RC discipline per eob.1 Str carve-out). Tasks 3-5:
three E2E fixtures + tests (positive 4-prim smoke, user-ADT
data IntBox + instance prelude.Show IntBox, negative print f
where f : Int -> Int firing NoInstance). Task 5 also extends
the Float-aware NoInstance arm at lib.rs:770-779 with a parallel
Show-aware branch. Task 6: DESIGN.md amendments (§Prelude classes
flip print to shipped, §Float semantics gains Show-Float NaN
paragraph). Task 7: roadmap P1 Show+print flips to [x] + new
P2 entry for io/print_int|bool|float retirement (insert at top of P2).
Task 8: integration + bench.
Boss decisions in plan Pre-flight section: ship explicit-let
(no auto-desugarer); AST-level IR-shape pin (Term pattern match);
NoInstance Show diagnostic wording with literal DESIGN.md section
cross-reference; P2 top-insertion for the retirement entry;
let-bound concrete fn-type for the negative fixture.
Ships class Show a where show : (a borrow) -> Str in the prelude
plus primitive Show Int / Bool / Str / Float instances. Each
instance body is a single-application lambda invoking the
corresponding runtime primitive (int_to_str, bool_to_str,
str_clone, float_to_str) — first prelude instance bodies to call
runtime primitives directly. Float is included in Show (unlike
Eq/Ord) because textual representation is well-defined modulo the
NaN-spelling caveat at DESIGN.md §Float semantics.
The 22b typeclass test corpus is migrated preemptively: 21 fixture
files and 6 consumer files (typeclass_22b{2,3}.rs +
hash.rs ct4 pin + workspace.rs iter22b1 tests + the
instance_present.prose.txt snapshot) rename class Show / show
to class TShow / tshow, analogous to the existing TEq/TOrd
convention. Migration runs before the prelude additions so the
workspace stays green throughout.
Three new tests pin the post-mono shape: show_mono_synthesis.rs
(existence of show__Int/Bool/Str/Float as Def::Fn entries in the
prelude post-mono module), show_dispatch_pin.rs (bare show and
tshow both Step-2 singletons workspace-globally),
mono_hash_stability.rs::primitive_show_mono_symbol_hashes_stay_bit_identical
(body hashes pinned for the 4 new mono symbols).
DESIGN.md §Prelude (built-in) classes drops Show from the
deferred-features list and appends a milestone-24 paragraph
naming the class signature + the 4 instances + the body shape.
print rewire stays deferred to iter 24.3.
Tests: 552 passed (was 548 + 4 new). bench/compile_check +
bench/cross_lang exit 0. bench/check exits 1 on the recurring
noise envelope per the conservative-call lineage.
Plan for iter 24.2. Tasks 1-2: preemptive 22b-Show → TShow/tshow
rename across 21 fixtures + 2 Rust test files (the recon-corrected
count vs spec's '~14' undercount). Task 3: insert 5 new top-level
defs (class Show + 4 primitive instances Int/Bool/Str/Float) at
prelude.ail.json:223 before fn ne. Task 4: mono-synthesis existence
test for show__T. Task 5: dispatch-singleton pin (show vs tshow,
both Step-2 singletons post-migration). Task 6: hash-stability pin
extension. Task 7: DESIGN.md §Prelude(built-in) classes amendment.
Task 8: integration + bench verification.
Recon-Boss reconciliation captured in Pre-flight notes: 21 fixtures
(not 14), prelude line 223 insertion point, str_clone-reachability
verified by Task 3 round-trip.
Re-derives the deferred iters 24.2 (class Show + 4 prim instances)
and 24.3 (print free fn + E2E) against the post-mq architecture.
Iter 24.1 shipped at f38bad8 and is unchanged. The supersedes-note
in the spec frontmatter explains the relation to the predecessor at
docs/specs/2026-05-12-24-show-print.md.
Substantive deltas vs. predecessor:
- Dispatch routing through the mq.2/.3 5-step rule; constraint-driven
filter (mq.tidy T1) is load-bearing for show calls inside print's
body.
- 24.2 grows to include the 22b-Show -> TShow/tshow migration
(~14 fixtures + 2 Rust test files) to eliminate post-prelude.Show
ambiguity, analogous to the existing TEq/TOrd convention.
Grounding-check PASS (re-dispatched after two LBA path corrections;
12 load-bearing assumptions all ratified).
Two libraries can now each declare their own class with the same
method name; type-driven dispatch resolves at the call site, with
explicit module-qualified syntax as the author's disambiguation
tool when the type alone isn't decisive.
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.
Rigid-var refinement type-unification leg in refine_multi_candidate_residual
(high-1); same-module bare-class qualifier shape accepted (high-2);
class-method-shadowed-by-fn warning tightened to require an instance
in workspace_registry (medium-1); DESIGN.md Data Model schema fragments
cross-reference the canonical-form rule (medium-2).
Q1 revised from carrier "fix both sites" to "fix only discharge-time"
after code inspection: synth-time resolve_method_dispatch is invoked
with concrete_arg_type: None and the residual's metavar is constructed
AFTER the dispatch call, so the rigid-var leg has no residual type to
unify against — class-only filter is semantically correct there.
Q2 conservative tightening: filter via "any candidate class has any
registry instance" instead of deferring to App-arm via pending-warning
list (full spec rule).
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).
Installs the dispatch infrastructure for type-driven method
resolution without retiring MethodNameCollision yet. The new
multi-candidate path is exercised exclusively by 15 unit tests;
real workspaces continue producing single-class residuals
(candidates: None) and every pre-mq.2 fixture typechecks unchanged.
Three new CheckError variants:
- AmbiguousMethodResolution (multi-candidate after type-driven filter)
- UnknownClass (explicit qualifier names a class not in registry)
- NoInstance gains additive candidate_classes field (Vec<String>)
Schema/Env additions:
- Env.method_to_candidate_classes: BTreeMap<String, BTreeSet<String>>
workspace-flat inverse of class_methods, built in build_check_env.
- ResidualConstraint.candidates: Option<BTreeSet<String>> — None
preserves pre-mq.2 single-class semantics; Some(set) carries the
multi-candidate residual that discharge refines.
- ResidualConstraint visibility bumped pub(crate) → pub for
unit-test crate access.
New helpers:
- MethodDispatchOutcome enum + pure resolve_method_dispatch
implementing the spec's 5-step rule (qualifier → singleton →
type-driven filter → constraint-driven filter → Multi for
discharge-time refinement).
- parse_method_qualifier splits Term::Var.name into
(method_name, optional_qualifier_prefix) at the last dot.
- RefineOutcome enum + refine_multi_candidate_residual for
discharge-time refinement.
- resolve_residual_class_for_mono wires the refinement into mono's
collect_residuals_ordered residual-to-target mapping.
Synth Var-arm class-method branch rewritten via parse_method_qualifier
with inner-dot gate (qualifier must be <module>.<Class>; single-dot
names fall through to the existing qualified-fn path).
Constraint-discharge in check_fn uses expanded (post-superclass-
expansion) constraints for the rigid-var path — sounder than raw
declared_constraints.
Plan-invented format_type_for_display replaced with the existing
ailang_core::pretty::type_to_string (one less duplicate).
Synth-time declared_constraints: &[] is a deliberate gap documented
as known debt — load-bearing only post-mq.3 for the rigid-var
fallback (env-plumbing the active fn's constraints into the Var
arm is a ~10-line edit slated for mq.3).
9/9 tasks, 539 tests green (was 520 pre-mq.2; +15 mq.2 unit tests +
4 pre-existing). bench/compile_check.py + cross_lang.py clean;
bench/check.py 1 regression (latency noise — runtime cannot be
touched by a typecheck-side iter).
Installs the dispatch infrastructure without retiring
MethodNameCollision yet:
- 2 new CheckError variants (AmbiguousMethodResolution, UnknownClass)
- NoInstance gains optional candidate_classes field
- ResidualConstraint gains optional candidates: BTreeSet<String>
- Env gains workspace-flat method_to_candidate_classes index
- New resolve_method_dispatch helper implementing the 5-step rule
- Synth Var-arm rewritten to route through the helper
- Constraint-discharge refines multi-candidate residuals
(concrete-type path → registry filter; rigid-var path →
declared-constraint filter)
- Mono's residual-to-target mapping mirrors the discharge refinement
End-to-end multi-class fixtures still gated by MethodNameCollision
(iter mq.3); new path exercised in this iter only by 6 unit-test
cases on resolve_method_dispatch + 3 each on refine and mono
resolver.
Boss decisions on recon open questions: residual extension via
Option<BTreeSet<String>> field; index lives workspace-flat on Env;
NoInstance.candidate_classes wired through to_diagnostic this iter;
qualify_class_ref_in_check consolidation deferred per mq.1
known-debt.
Three schema fields move bare → canonical (bare for same-module,
<module>.<Class> for cross-module): InstanceDef.class,
Constraint.class, SuperclassRef.class. Symmetric to ct.1's
Type::Con.name rule. ClassDef.name stays bare.
validate_canonical_type_names gains three field-walks via new
check_class_ref helper + two sibling diagnostics
BareCrossModuleClassRef / BadCrossModuleClassRef.
check_class_name_fields narrowed to ClassDef.name-only.
Workspace-internal class-name keys all qualified:
class_def_module, class_by_name, registry entries.0,
ClassMethodEntry.class_name, class_superclasses, mono's
class_index, Origin::Class.class_name. New qualify_class_ref
helper at workspace.rs scope plus sibling
qualify_class_ref_in_check in ailang-check.
Two new positive on-disk fixtures (mq1_xmod_constraint_class{,_dep})
exercise the qualified Constraint.class shape.
Recon claim that all existing fixtures' class-refs are intra-module
was empirically wrong — 10 fixtures required minimal canonical-form
migration. Duplicate-instance test architecturally restructured:
post-mq.1 orphan-freedom makes two-modules-on-same-(class,type)
structurally impossible; new test_22b1_dup_same_module fixture
lands both instances in the class's module.
Plan defects fixed inline: Origin::Class.class_name single
construction site (plan recon off-by-one); build_class_index in
mono.rs needed qualifying; build_module_globals needed Def::Instance
carve-out; check_fn declared-constraint matching needed inline
canonical-form lifting; NoInstance Float-aware message arm needed
prelude.Eq/prelude.Ord match; coherence type-leg needed qualified-
head split-and-lookup. Tasks 3-6 ran as one coherent fix.
7/7 tasks, 520 tests green. bench/compile_check.py + cross_lang.py
exit 0; bench/check.py exit 1 (4 improvements-beyond-tolerance,
audit-ratifiable, not a regression).
MethodNameCollision + dispatch path unchanged; iters mq.2 + mq.3
land them.
Iter mq.1 plan: move InstanceDef.class / Constraint.class /
SuperclassRef.class from bare-only to canonical form per ct.1's
existing rule, extend validate_canonical_type_names with three new
field walks, narrow check_class_name_fields to ClassDef.name-only,
qualify build_registry's class_def_module + class_by_name + entries
+ four lookup sites, qualify ClassMethodEntry.class_name +
class_superclasses, update affected test-assertion strings.
Pre-flight recon flagged that the spec's '14 fixtures migrated' cost
claim is over-stated: all existing fixture class refs are intra-module
under the canonical-form rule. Plan does not add migration work that
does not need doing; Task 7 Step 3 verifies zero existing-fixture
diff.
Retires the workspace-global MethodNameCollision pre-pass
(crates/ailang-core/src/workspace.rs:547) via a phased three-iter
milestone. Iter 1 canonicalises InstanceDef.class, Constraint.class,
and SuperclassRef.class (extends ct.1's validator); Iter 2 installs
type-driven dispatch (method_to_candidate_classes index + multi-
candidate residual + AmbiguousMethodResolution / UnknownClass
diagnostics) while the workaround still gates real workspaces;
Iter 3 deletes the pre-pass and ships multi-class E2E plus DESIGN.md
sync. Unblocks the deferred milestone 24 (Show + print rewire).
Step 7.5 grounding-check PASS: all 18 load-bearing assumptions
ratified by named, currently-green tests.
24.2 plan-recon surfaced a spec-gap: adding `class Show` to the prelude
would collide with the user-class `Show` declared by 14 test fixtures
under examples/test_22b{1,2,3}_*.ail.json (plus hardcoded "Show" /
"show" assertions in crates/ail/tests/typeclass_22b{2,3}.rs) through
the workspace-global method-name-collision pre-pass at
crates/ailang-core/src/workspace.rs:547.
Three resolution paths surfaced: (A) prep-iter rename Show→Render in
fixtures+tests, (B) inline-rename in 24.2 (makes iter bigger),
(C) defer 24.2+24.3 until the P2 milestone "Module-qualified class
names + type-driven method dispatch" retires the MethodNameCollision
workaround. User picked C.
Spec status flips Draft → Partial; the document remains as historical
context for the future re-brainstorm. Iter 24.1 retains as standalone
runtime infrastructure (`bool_to_str` + `str_clone` heap-Str
primitives are user-callable today; they wait for the deferred Show
instances to find their primary caller). Roadmap P1 entry flips
`[ ] → [~]` with `depends on:` line pointing at the P2 milestone.
First iter of milestone 24 (Show + print rewire). Wires two new
heap-Str-producing primitives parallel to hs.4's int_to_str /
float_to_str:
- runtime/str.c gains ailang_bool_to_str(bool) → heap-Str "true" /
"false" and ailang_str_clone(const char *) → memcpy'd heap-Str
copy. Both use the existing str_alloc slab helper.
- builtins.rs + synth.rs install the two signatures lockstep with
ret_mode: Own; str_clone carries param_modes: [Borrow].
- IR-header preamble gains two unconditional `declare ptr @...`
lines; Emitter::lower_app gets two new arms; is_static_callee
whitelist extends with the two names.
- Five IR snapshots regenerate for the two new declares.
- Pre-existing-drift fix: int_to_str row added to builtins.rs::list()
(hs.4 installed env.globals entry but missed the list() row).
Substantive deviation flagged by orchestrator (DONE_WITH_CONCERNS):
builtin signatures registered in uniqueness.rs::infer_module and
linearity.rs::check_module_with_visible (8 LOC × 2 files), symmetric
to iter 23.4-prep's class-method registration in the same globals
maps. Without this fix str_clone's param_modes: [Borrow] is invisible
to the App-arg walker, src_heap walks as Position::Consume, the
scope-close ailang_rc_dec is gated off, and the
str_clone_cross_realisation_uniform_abi test's plan-literal
`frees == 3` assertion does not hold. The fix is the substantively
correct repair, not a design departure.
9 new tests: 2 builtins-install unit, 2 IR-shape unit pins, 5 E2E
(2 RC-stats, 2 stdout-smoke for both Bool branches, 1 cross-
realisation). 4 new .ail.json fixtures.
Full cargo test --workspace: 513 passed, 0 failed.
bench/compile_check.py: 24/24 stable. bench/cross_lang.py: 25/25
stable.
Six tasks: (1) runtime C functions in runtime/str.c, (2) builtin
install lockstep across builtins.rs + synth.rs + list() row + 2
unit tests, (3) codegen IR wiring (2 declares + 2 lower_app arms +
is_static_callee whitelist), (4) 2 IR-shape unit pins, (5) regen
5 IR snapshots, (6) 4 fixtures + 5 E2E tests + acceptance gate.
Mechanical mirror of hs.4's int_to_str/float_to_str pattern. Plan
captures every Edit verbatim (old_string + new_string) per planner
Iron Law. Pre-existing drift fix included inline: int_to_str row
missing from builtins.rs::list() since hs.4 — adding it adjacent
to the two new rows. Three open questions from plan-recon resolved
by Boss judgement (C uses bool from <stdbool.h>; tests live in
crates/ail/tests/e2e.rs as the existing real path; int_to_str
list() row drift fixed inline).
Milestone 24 closes the Post-22 Prelude roadmap entry's second half.
Architecture: `class Show a where show : (a borrow) -> Str`, four
primitive instances (Int/Bool/Str/Float — Float included because
float_to_str is semantically unproblematic, unlike Eq/Ord's IEEE-754
issues), and `print : forall a. Show a => (a borrow) -> () !IO` as
a polymorphic free fn following 23.5's `ne/lt`/etc. shape. Two new
runtime primitives (`bool_to_str`, `str_clone`) let Show Bool and
Show Str honour the uniform `(a borrow) -> Str Own` signature without
codegen intercept.
Three iters: 24.1 (runtime + codegen for the two new primitives),
24.2 (class + 4 instances in prelude.ail.json + DESIGN.md anchor),
24.3 (print free fn + positive/user-ADT/negative E2E + DESIGN.md
sync).
io/print_int|bool|float STAY this milestone — redundancy with `print`
ratified as transitional per the 23-spec precedent for ==/eq. Corpus
migration (86 fixtures) queued as separate P2 follow-up.
Grounding-check PASS: all 12 enumerated load-bearing assumptions plus
two additional implicit claims ratified by currently-green named tests
(eob.1 RC-discipline tests anchor 24.1's critical group; milestone-23
mono unification anchors 24.2/24.3's group).
Architect drift review (range 750f97e..78e8338): drift_found with
three [high — spec acceptance] DESIGN.md items left over from the
heap-str-abi spec's acceptance criteria, plus one [medium] record-
keeping note. All three high items fixed inline as eob.tidy (Boss-
mechanical edits — small scope, content fully known from the just-
closed milestone, no plan / implement dispatch warranted):
1. DESIGN.md (Float-semantics block): replaced the stale 'float_to_str
is type-installed but codegen-deferred' caveat with a one-paragraph
statement that float_to_str and int_to_str are fully wired,
allocate a heap-Str slab, and carry ret_mode: Own.
2. DESIGN.md ('What is supported' inventory): dropped the codegen-
deferred parenthetical from float_to_str; added int_to_str to the
inventory with the same Str-ABI cross-reference.
3. DESIGN.md (new 'Str ABI' subsection after Float-semantics): documents
the dual realisation (static-Str packed-struct globals vs heap-Str
malloc slabs), the shared consumer ABI (Str pointer at len-field
offset 0, bytes at payload+8, inherited strcmp semantics), and the
codegen-level non-RC invariant for static-Str (move-tracking +
non-escape lowering + Type::Con{name:"Str"} carve-outs in
field_drop_call and drop_symbol_for_binder's App arm).
Medium-severity hs.4-journal forward-pointer item skipped: INDEX.md
already chains hs.4 → eob.1 chronologically, and eob.1's journal
explicitly cross-references hs.4's deferred fix.
Bench-regression check:
- compile_check.py: exit 0; 24 metrics all stable
- cross_lang.py: exit 0; 25 metrics all stable
- check.py: exit 1; 2 regressed (bench_list_sum.bump_s +12.6%,
bench_hof_pipeline.bump_s +11.65%, both marginal, the other four
bump_s metrics stable — most parsimonious explanation is per-
fixture system noise on ~50ms benches), 5 improved beyond
tolerance (latency.explicit_at_rc.p99_us / p99_9_us / p99_over_median
cluster — third consecutive audit showing the same shift
uncorrelated with milestone work, plus two gc_over_bump mirrors of
the bump_s regressions).
Baseline left pristine on every script for the third consecutive
audit. Latency cluster has persisted across audit-cma → audit-ms →
audit-eob without an identified cause; ratifying via --update-baseline
would obscure the next attributable signal. bump_s cluster is first-
sighting; first-sighting rule says observe in the next audit, do not
ratify on first sight.
Heap-str-abi milestone fully closed:
- Static-Str layout migrated (hs.1, hs.2 + spec amend).
- Heap-Str runtime wired (hs.3, hs.4).
- Effect-op-borrow rule + RC-discipline (eob.1).
- DESIGN anchors landed across eob.1 (arg-position policy under
Decision 10) and eob.tidy (Str-ABI subsection, signatures-inventory
cleanup).
Language rule: Term::Do.args[*] are walked in Position::Borrow by
the uniqueness + linearity passes, symmetric to Term::Ctor.args[*]
being walked in Position::Consume. The kind itself carries the
ownership default — no per-op field on EffectOpSig. Three
analyser sites flip in lockstep: uniqueness.rs:289, linearity.rs:506,
and the shared doc-comment at linearity.rs:42.
Heap-Str-specific consequences (closing hs.4's leak):
- int_to_str / float_to_str ret_mode: Implicit → Own at four lockstep
sites (builtins.rs:200,210 + synth.rs:172,179). The codegen
trackability gate (drop.rs:464-475, iter 18g.2) now fires on these
callees, so the let-binder receives a scope-close drop.
- drop_symbol_for_binder's App arm gets a Type::Con{name:"Str"} →
"ailang_rc_dec" carve-out, symmetric to field_drop_call's existing
Str arm. Without it, the new Own-trackable App binder would emit
drop_<m>_Str (undefined).
Tests: the two pre-existing RED tests at e2e.rs (commit 592d87b)
flip to GREEN unchanged with predicted numbers (allocs=1,frees=1,
live=0 and allocs=2,frees=2,live=0). Two new positive tests pin
the rule's coverage: primitive-Int arg through io/print_int produces
zero RC traffic; heap-Str through 2× io/print_str in sequence
typechecks (no use-after-consume) and balances allocs=1,frees=1,
live=0.
DESIGN.md §Decision 10 gets the arg-position-policy table for both
AST node kinds (Ctor=Consume, Do=Borrow) plus a linking paragraph
explaining how Do=Borrow cooperates with ret_mode==Own.
heap-str-abi milestone closes here: WhatsNew entry shipped (Strings
produced at runtime now release cleanly), roadmap P1 entry checked
off, Post-22-Prelude depends-on line removed.
Workspace cargo test + cross_lang.py + compile_check.py all green.
check.py noisy latency cluster + bench_list_sum.bump_s ±12% — same
precedent as the previous two audits, not coupled to this milestone.
Single iter closes the milestone: language rule at three check-time
sites (uniqueness.rs + linearity.rs + linearity.rs doc-comment),
four lockstep ret_mode-Own edits (builtins.rs + synth.rs for
int_to_str and float_to_str), one Str carve-out in
drop_symbol_for_binder's App arm, RED→GREEN verification of the
two existing pinning tests, two new positive tests (Int-arg
primitive non-leak, repeated Str-arg borrow), DESIGN.md anchor
under Decision 10 covering BOTH arg-position rules (Ctor=Consume,
Do=Borrow), WhatsNew entry, roadmap close of the heap-Str ABI P1
milestone.
Plan-recon DONE: lint-side-effect surface (over-strict-mode on
(own T) params used solely via effect-ops) is empty for the
current corpus, so no follow-up sweep iter needed. IR-snapshot
regen confirmed no-op. Two inline IR-shape tests at
codegen/src/lib.rs:4127-4205 stay green (ret_mode flip does not
change their emitted-call assertions).
hs.4 surfaced a leak the heap-str-abi spec didn't account for:
heap-Str slabs handed back by int_to_str / float_to_str and
immediately printed via io/print_str leak at scope close under
--alloc=rc. RED tests at e2e.rs (commit 592d87b) pin it.
Brainstorm settled on naming the rule rather than working
around it: Term::Do.args[*] are walked in Borrow position by
uniqueness and linearity passes, symmetric to Term::Ctor.args[*]
being walked in Consume position. The kind itself carries the
ownership default — no per-op field on EffectOpSig.
The rule alone doesn't close the leak; two heap-Str-specific
shape fixes follow (int_to_str / float_to_str gain ret_mode: Own;
drop_symbol_for_binder's App arm gets a Str carve-out symmetric
to field_drop_call's existing one). DESIGN anchor + WhatsNew +
audit-close from hs.5 roll into this milestone.
Grounding-check PASS — all nine load-bearing assumptions
ratified against currently-green tests (with the two RED tests
ratifying the leak as the spec's premise).
Strengthen the two hs.4-shipped RC-stats E2E assertions back to the
plan-original `live == 0` + `allocs == frees` invariants:
- int_to_str_drop_balances_rc_stats: `let s = int_to_str(42) in
do io/print_str(s)` allocates a heap-Str (allocs=1) but the let-arm
dec at scope close does not fire (frees=0, live=1).
- str_field_in_adt_drops_heap_str_correctly: `Box(int_to_str(42))`
pattern-matched and printed allocates the ADT cell + the heap-Str
slab (allocs=2). The match-arm pattern walk calls
field_drop_call's Str arm on the extracted field (frees=1), but
the outer ADT cell's let-binder is not trackable (frees=1, live=1).
Root cause is the effect-op arg-mode gap surfaced during hs.4: the
uniqueness analyser at uniqueness.rs:289-292 walks every Term::Do arg
in Position::Consume, gating off the let-arm dec at lib.rs:1440
(consume_count == 0 required). EffectOpSig has no param_modes field
to override the Consume-by-default walk. The fix is structural and
queued as the next iter.
These tests were shipped GREEN in hs.4 (134441b) with weakened
`allocs >= N` asserts; that was a fixture-adapted-to-bug anti-pattern.
Asserts restored to the language invariant; tests now RED, locking
in the fix obligation.
Heap-Str ABI milestone's fourth iter. Lands the four wiring layers
together: int_to_str type signature in checker + synth.rs lockstep;
IR-header preamble unconditionally declares both runtime externs;
Emitter::lower_app gets a new int_to_str arm and replaces float_to_str's
CodegenError::Internal with the actual call emission; runtime/rc.c
hoists from --alloc=rc-only to unconditional link (the weak attr on
str.c's ailang_rc_alloc extern becomes the documented permanent no-op).
2 IR-shape pins + 4 E2E (2 stdout-smoke + 2 RC-stats) + 4 fixtures +
drop.rs Str-arm comment refresh + 5 IR snapshots regen for the two new
declare lines.
The acceptance goal "do io/print_str(int_to_str(42)) prints '42\n'" is
met. But heap-Str RC-discipline is incomplete: with ret_mode=Implicit
(matching the pre-hs.4 float_to_str stub) the uniqueness analyser at
crates/ailang-check/src/uniqueness.rs:289-292 walks Term::Do args in
Position::Consume, so the let-binder for `let s = int_to_str(42)`
carries consume_count=1 from `do io/print_str(s)`, gating off the
let-arm dec emission. Heap-Str slabs leak at program end. A speculative
fix (Own ret_mode + drop.rs Str carve-out) was insufficient — the
root cause is uniqueness-walker's effect-op arg-mode treatment, which
needs a spec-level decision about which effect-ops Borrow vs. Consume
their ptr-typed args. Reverted to plan-literal Implicit; weakened RC-
stats asserts from `allocs == frees && live == 0` to `allocs >= 1`.
Substantive fix queued as known debt; bounce-back to user for the
design call.
cargo test --workspace green; bench/cross_lang.py + compile_check.py
+ check.py within documented noise.