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.
Five polymorphic prelude free fns (ne/lt/le/gt/ge) plus three E2E
fixtures: positive composition over Int/Bool/Str, user-ADT with
user-written Eq/Ord on IntBox, and Float-NoInstance with a
Float-aware diagnostic addendum cross-referencing DESIGN.md §"Float
semantics".
Two checker-side fixes were needed alongside the prelude additions,
both symmetric to the iter 23.4-prep visibility fix:
- linearity::check_module_with_visible — workspace-aware callee-mode
resolution so a Borrow-mode user fn forwarding into a prelude poly
fn (e.g. `at_most x y = not (gt x y)`) doesn't false-fire
consume-while-borrowed.
- mono::collect_mono_targets — distinguish rigid forall vars from
unbound metavars; rigid-bearing free-fn targets skip (they get
re-collected with concrete subst when the enclosing fn itself
monomorphises). Without this, a polymorphic body calling another
polymorphic free fn produced a spurious __Unit specialisation
whose body referenced compare at Unit.
Roadmap split: shipped Eq/Ord half checked, Show + print-rewire half
remains pending behind the heap-Str ABI prerequisite. DESIGN.md
§"Prelude classes" amended.
One pre-existing fixture (test_22b1_missing_method) renamed its
class method ne → tne to avoid collision with the new prelude `ne`.
Bench: check.py + cross_lang.py exit 0; compile_check.py exits 1
with one sub-ms check_ms.local_rec_capture at +26% (tolerance 25%) —
prelude-growth-related noise-band fluctuation, ratifiable per spec
§248-249 and journal Concerns.
Restructure ailang-check/src/mono.rs::monomorphise_workspace into a
single specialiser over every Type::Forall-quantified Def::Fn, covering
both class-method residuals AND polymorphic free-fn call sites in one
fixpoint pass. Remove the codegen-time specialiser (lower_polymorphic_call,
module_polymorphic_fns, mono_queue, mono_emitted, emit_specialised_fn,
plus the now-dead helpers apply_subst_to_term, descriptor_for_subst,
type_descriptor) entirely. Codegen sees only monomorphic Def::Fns after
this iter — no polymorphic call sites, no codegen-time queue.
Architecture changes:
- MonoTarget: struct → enum with ClassMethod / FreeFn variants; dedup
keys are guaranteed-disjoint via 'class'/'free' first component.
- collect_mono_targets: new FreeFnCall channel through synth (cleaner
than the plan's separate-walker proposal — one less data flow,
substitution tracking comes 'for free' via fresh metavars +
post-synth subst.apply). Plan Step 4.4 (polymorphic_free_fns env
field) consequently obsolete.
- synthesise_mono_fn_for_free_fn: new free-fn entry point; substitutes
rigid vars in BOTH the type AND the body (body substitution required
because free-fn bodies carry inner Lams whose param_tys reference
outer Forall vars — adapted from plan's 'body unchanged' sketch).
- mono_symbol_n: N-ary extension of mono_symbol; bit-stable for the
single-type-var case (hash-stability pin Task 1 fires zero times
through all eight refactor tasks).
- rewrite_class_method_calls → rewrite_mono_calls; interleaved-slot
collector preserves the positional-cursor invariant across both
channels.
- workspace_has_typeclasses → workspace_has_specialisable_targets
(principled correctness; old predicate happened to already be true
for every user workspace because the prelude is auto-injected).
Codegen cleanup:
- Two call sites of lower_polymorphic_call deleted (codegen/src/lib.rs
lines ~1939-1945, ~1965-1973).
- lower_polymorphic_call body, module_polymorphic_fns field +
population + Emitter wiring, mono_queue / mono_emitted, drain loop,
emit_specialised_fn — all removed.
- is_static_callee collapsed to module_user_fns-only.
- subst.rs apply_subst_to_term + descriptor_for_subst removed; synth.rs
type_descriptor removed (all dead post-removal).
- ail emit-ir command pipeline gains lift_letrecs + monomorphise_workspace
pre-passes (pre-iter-23.4 the codegen-time poly path was masking
this dependency — emit-ir is now consistent with build).
- Net codegen reduction: -285 lines lib.rs, -93 lines subst.rs.
Tests:
- mono_hash_stability.rs (new): regression pin for six primitive Eq/Ord
mono-symbol body hashes; stayed bit-stable through all eight refactor
tasks.
- mono_unification.rs (new): six tests covering variant-key disjointness,
free-fn target emission, free-fn synthesis with rigid substitution,
rewrite walker on free fns, identity for poly-free-fn-only workspaces,
end-to-end cmp_max_smoke runtime correctness.
- typeclass_22b3.rs: three test sites updated to MonoTarget::ClassMethod
enum form.
- 73 e2e + 18 typeclass + 81 codegen tests all green workspace-wide.
DESIGN.md §'Monomorphisation (post-typecheck, pre-codegen)' amended:
two-arm fixpoint described, disjoint-keyed dedup, cursor-aligned
walker, removed codegen-time path.
Bench check (all exit 0, 112 metrics stable): bench/check.py +
compile_check.py + cross_lang.py.
Plan parent: docs/plans/2026-05-11-iter-23.4.md (fab1685).
Spec parent: docs/specs/2026-05-11-23-eq-ord-prelude.md (841d65d).
Per-iter journal documents two adapted deviations from the plan
(synth-channel vs separate walker; body substitution; emit-ir fix);
no spec defects.
Quality-review follow-up on ct.2.2 (b3c3b60).
- Delete CheckError::AmbiguousCtor: its sole producer (the Pattern::Ctor
imports-fallback) was removed in ct.2.2, leaving the variant, the
code()/ctx() arms, and the diagnostic.rs doc-comment as dead surface.
Workspace grep confirms no remaining Rust consumers. AmbiguousType
stays (it still has a Term::Ctor producer, scheduled for symmetric
removal in ct.2.3).
- Refresh the per-module-overlay rationale comment in check_in_workspace:
it still cited the now-defunct Pattern::Ctor local-first / imports-
fallback path. The overlay's current job is duplicate detection plus
bare-name lookups for local Term::Ctor synth.
- Fold the awkward re-match in type_check_pattern: the first match on
expected now binds scrutinee_args alongside resolved_type_name /
resolved_td / resolved_owning_module, deleting the second match +
unreachable!(). Collapse the any-then-expect ctor lookup into a single
find().ok_or(UnknownCtorInPattern).
Build green, 449+ tests green across the workspace.
Five tests in crates/ail/tests/ct1_check_cli.rs, each pinning one named property:
- check_json_emits_bare_cross_module_type_ref: --json mode surfaces
`bare-cross-module-type-ref` with non-zero exit on the
test_ct1_bare_xmod_rejected fixture.
- check_json_emits_bad_cross_module_type_ref: same for
`bad-cross-module-type-ref`.
- check_json_emits_qualified_class_name: same for `qualified-class-name`.
- check_human_mode_emits_actionable_message_to_stderr: human (non-JSON)
mode prints both the offending type name and the migration-command
hint to stderr — this path goes through the anyhow/thiserror Display
impl, not workspace_error_to_diagnostic, so it needs its own pin.
- check_ordering_match_post_migration_is_clean: post-migration
ordering_match.ail.json typechecks via the CLI with zero diagnostics,
guarding against revert of the ct.1.5 migration or a regression in
Registry::normalize_type_for_lookup that would reject the canonical form.
Three Minor items from the ct.1.5a quality review:
1. normalize_type_for_registry now recurses into
Type::Forall.constraints[].type_, symmetric to walk_type. Closes a
latent gap: a Forall with bare cross-module constraint types reaching
the registry would have produced a key disagreeing with the qualified
form. RED test: ct1_5a_normalize_recurses_into_forall_constraints.
2. Documented the bare-name limitation on Registry::type_def_module:
two modules each defining 'type Foo' collide at insert; the proper
fix (re-key as (owning_module, bare_name)) is out of ct.1's scope.
3. Removed the four iter-tag explanatory paragraphs at the
normalize_type_for_lookup call sites (check/lib.rs check_fn,
mono.rs three sites). Replaced each with a one-line pointer to the
helper's docstring per project comment policy.
Adds BareCrossModuleTypeRef / BadCrossModuleTypeRef / QualifiedClassName
to WorkspaceLoadError plus the validate_canonical_type_names function
that enforces the canonical-form rule on every Type::Con name reachable
from any def (including Term::Lam.param_tys / .ret_ty and Term::LetRec.ty).
Term::Ctor.type_name and class-name fields land in Tasks 2 + 3; the
validator is not yet wired into load_workspace (Task 6). Seven RED-first
unit tests cover the cases enumerated in spec section Testing strategy
plus a Lam-embedded-type smuggle-prevention test.
main.rs gains diagnostic arms for the three new variants to keep the
exhaustive match compiling.
The auto-loaded prelude (iter 23.2 work-in-progress) injects
`class Eq` plus three primitive instances (Eq Int, Eq Bool, Eq Str)
into every workspace. Five workspace::tests assertions broke against
this new baseline:
- iter22b1_missing_method_fires_diagnostic
- instance_without_superclass_instance_fires
- instance_overriding_nonexistent_method_fires
Their fixtures redeclare `class Eq` (and `class Ord` in one case),
which now collides with the prelude. Renamed each fixture's
`Eq`/`eq` -> `TEq`/`teq` and `Ord`/`lt` -> `TOrd`/`tlt`, matching
the scheme already used in the two earlier-renamed test_22b2
fixtures. Method `ne` stays (does not collide). Assertion strings
in workspace.rs updated to match.
- iter22b1_workspace_with_no_classes_has_empty_registry
- iter22b1_instance_in_class_module_loads_clean
Their asserted registry counts assumed an otherwise-empty registry.
Rewritten to filter by `defining_module != "prelude"`: the property
is "no NON-prelude entries" / "exactly one fixture-owned entry",
not raw count.
Doc comments updated alongside each assertion change so the rationale
for TEq/TOrd (collision avoidance) is local. No production code
touched.
The mono pass re-runs synth on every fn body to recover residual class
constraints. build_workspace_env (delegating to build_check_env) leaves
env.types and env.ctor_index workspace-flat. The synth path for cross-
module Pattern::Ctor / Term::Ctor resolution depends on a per-module
shape — local-first lookup, then imports-fallback that produces a
qualified type name. Without the overlay, the local-flat hit short-
circuits the fallback and yields a bare type name, mismatching the
qualified scrutinee/pattern from the sibling path with
CheckError::PatternTypeMismatch.
Add a per-module overlay helper that clears and rebuilds both
env.types and env.ctor_index from the current module's Def::Type list
— mirroring check_in_workspace at lib.rs:1257-1287 — and apply it at
the two mono entry points that re-walk bodies: the Phase 3 rewrite
loop in monomorphise_workspace and collect_targets_workspace_wide.
Pinned by tests/mono_xmod_ctor_pattern.rs (e580f75); also unblocks
the latent E2E regressions nested_ctor_pattern_first_two_sum,
std_either_list_demo, and ordering_match_via_prelude_prints_1 that
surfaced once the typeclass gate in workspace_has_typeclasses flipped
to true.
`monomorphise_workspace` re-runs `synth` on every fn body to recover
residual class constraints; the env it uses is built by
`mono::build_workspace_env`, which delegates to `build_check_env` and
produces a workspace-flat `ctor_index` (every Def::Type ctor across
every module, keyed by bare ctor name → bare type name).
`check_in_workspace` (lib.rs:1247-1258) explicitly clears that flat
index after `build_check_env` and rebuilds it per-module so that
`Pattern::Ctor`'s local-first / imports-fallback resolution at
lib.rs:2486-2521 keeps the qualified-type-name comparison at
lib.rs:2526 intact: imports-fallback yields a qualified
`resolved_type_name` (`Mod.Type`), local-hit yields a bare one.
The mono pass never does the per-module overlay, so when a body in
module B pattern-matches a ctor `C` whose Def::Type lives in imported
module A, the workspace-flat index resolves `C` locally and yields the
bare type name. The scrutinee, however, was typed against the
qualified name, and the comparison fails with
`PatternTypeMismatch { ctor: "Cons", ty: "A.Type<...>" }`.
Surfaced by iter 23.2 Task 3, which adds `class Eq a` + Eq Int/Bool/Str
instances to the prelude. Pre-Task-3 the prelude is class-free, so
`workspace_has_typeclasses(ws) == false` and the mono pass early-outs
at `mono.rs:73`. Task 3 flips the gate; every workspace now traverses
bodies, which brings the latent bug to the surface for the
pre-existing `nested_ctor_pattern_first_two_sum` and
`std_either_list_demo` E2E tests.
Sibling regressions in the same family: `mono_xmod_qualified_ref.rs`
(env.imports not seeded), commit 13b36cc (env.globals not seeded for
self-recursive fns), commit 5c5180f (env.types / env.ctor_index not
seeded for user ADTs — the original "flat ctor_index" decision that
this bug now exposes as wrong-by-construction for cross-module ctor
pattern resolution).
The minimal fixture is two modules with five defs total:
test_mono_ctor_listmod (data List a = Nil | Cons a (List a)) and
test_mono_ctor_main (class Trivial a + instance Trivial Int + a fn
that pattern-matches Cons against the imported List<Int> + a main).
The test pins the inner cause: matches against the specific
`CheckError::PatternTypeMismatch` variant with the qualified
scrutinee type and bare ctor name, so it stays RED regardless of the
prelude's typeclass content.