Files
AILang/docs/journals/2026-05-13-iter-mq.1.md
T
Brummel 0eb33235eb iter mq.1: class-ref canonical-form extension + workspace-internal class-name qualification
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.
2026-05-13 01:11:56 +02:00

9.0 KiB

iter mq.1 — Canonical-form extension for class-ref fields + workspace internal qualification

Date: 2026-05-13 Started from: 1a5f8289b7 Status: DONE Tasks completed: 7 of 7

Summary

mq.1 lands the canonical-form rule for the three class-reference schema fields (InstanceDef.class, Constraint.class, SuperclassRef.class) — bare for same-module, <module>.<Class> for cross-module — symmetric to ct.1's Type::Con.name rule. ClassDef.name stays bare (defining site, like TypeDef.name). ct.1's validate_canonical_type_names validator gains a sibling walk for class refs via the new check_class_ref helper, with two new sibling diagnostics BareCrossModuleClassRef and BadCrossModuleClassRef. Workspace-internal class-name keys (class_def_module, class_by_name, registry entries.0, ClassMethodEntry.class_name, class_superclasses, mono's class_index, Origin::Class.class_name) all carry the qualified form post-mq.1; the on-disk schema value is the canonical form (bare or qualified) that qualify_class_ref lifts to the workspace key at every lookup boundary. MethodNameCollision and the dispatch path are unchanged in this iter (iter 2 + 3 land them).

Per-task notes

  • iter mq.1.1: two new diagnostic variants BareCrossModuleClassRef / BadCrossModuleClassRef sibling to the type-ref variants; two Diagnostic-shape Display arms in ail/main.rs mirroring the existing arm. 2 RED unit tests, both GREEN.
  • iter mq.1.2: doc-comments on the four class-reference fields in ast.rs (ClassDef.name stays bare with cross-reference; SuperclassRef.class / InstanceDef.class / Constraint.class document canonical form). Zero semantic effect.
  • iter mq.1.3: check_class_ref helper plus three per-def walks inside validate_canonical_type_names; check_class_name_fields narrowed to ClassDef.name only; four in-test pin tests inverted (qualified is now accepted); on-disk test_ct1_qualified_class_rejected fixture-test repurposed (now expects OrphanInstance — the natural post-mq.1 rejection mode for the same shape); positive on-disk fixture pair mq1_xmod_constraint_class{,_dep} shipped plus the corresponding pin test. 4 new bare-xmod tests + 1 positive qualified test, all GREEN.
  • iter mq.1.4: qualify_class_ref helper at module scope; build_registry Pass-1 re-keyed (qualified); Pass-2 lookups use qualify_class_ref to lift inst.class to the registry-key shape; superclass walk threads current_class_module to qualify bare SuperclassRef.class; Origin::Class.class_name qualified (one construction site, not two as the plan claimed); type-leg of coherence check now also handles qualified head names symmetrically (split-and-lookup).
  • iter mq.1.5: ClassMethodEntry.class_name insert qualifies in build_module_globals; class_superclasses key + value both qualified; mono's build_class_index keyed qualified (plan said "no code change needed in mono.rs consumers" — that statement was wrong, the build-side needed the matching qualification).
  • iter mq.1.6: test assertions updated for the new qualified shape — the two MethodNameCollision pin tests, typeclass_22b2.rs:42 (class_method_class("show") now Some("...Show")), typeclass_22b3.rs:151 (MonoTarget.class qualified). The other lines plan flagged (217/232/295/301/341/353) are local-construction tests that never round-trip through the workspace — left as-is.
  • iter mq.1.7: full cargo test --workspace 520 passed / 0 failed; examples/prelude.ail.json zero diff (confirms spec assumption 17: intra-prelude refs stay bare); bench/compile_check.py exit 0, bench/cross_lang.py exit 0; bench/check.py exit 1 due to 4 improvements-beyond-tolerance (audit-ratifiable per convention, not a regression). Roundtrip on the new fixture: ail check examples/mq1_xmod_constraint_class.ail.json → "ok (16 symbols across 3 modules)", exit 0.

Concerns

  • Boss-note recon claim was empirically wrong. The boss-note said "every existing examples/test_22b*.ail.json class-ref field is intra-module under the canonical-form rule, so existing fixtures carry zero diff". Five test_22b* fixtures actually carried bare cross-module class refs and required migration: test_22b1_orphan_third.ail.json (bare Show from imported classmod), test_22b1_dup_a.ail.json + test_22b1_dup_b.ail.json (the duplicate-instance pair structurally relied on the pre-mq.1 bare cross-module shape), and test_22b2_unbound_constraint_var.ail.json (constraint references a class that doesn't exist anywhere → fired BareCrossModuleClassRef before reaching UnboundConstraintTypeVar). Six non-test_22b fixtures also needed migration (eq_ord_polymorphic, eq_ord_user_adt, cmp_max_smoke, ctt2_collision_lib, ctt2_collision_main). All migrations are minimal and aligned with the canonical-form rule; no proactive "fixture cleanup" added.
  • Duplicate-instance test restructured by structural necessity. Pre-mq.1 the test relied on two coherent instances on the same (class, type) declared from different modules; post-mq.1, the orphan-freedom invariant prevents this shape (any second instance in a module that owns neither the class nor the type fires OrphanInstance first). The new test_22b1_dup_same_module.ail.json fixture lands both instances in the same module — the only post-mq.1 way to land two on the same canonical key. The old dup_a/dup_b/entry fixtures stay as supporting modules (referenced by hash.rs's canonical-form hash pin) but no longer participate in the duplicate test. This is arguably a strengthening of the architecture: duplicate instances are now structurally impossible across coherent modules.
  • Plan's "no code change needed" claims in Task 4 Step 8 and Task 5 Step 5 were wrong. Task 4 Step 8 said Origin::Class.class_name qualified at "both sites" but there's only one construction site (the plan's recon was off-by-one). Task 5 Step 5 said mono's class_index.get(class) lookups would just work post-rekey, but build_class_index itself was bare-keyed. Both fixed inline.
  • Several adjacent fixes were necessary that the plan didn't anticipate: (a) build_module_globals rejected inst.class containing . — added a Def::Instance carve-out so qualified class refs survive. (b) check_fn's declared-constraint matching compared bare c.class to qualified residual r.class → added an inline lift via qualify_class_ref_in_check. (c) NoInstance Float-aware message arm matched on bare "Eq" / "Ord" — updated to "prelude.Eq" / "prelude.Ord". (d) Coherence check's type-leg lookup couldn't resolve qualified head names — extended with a split-and-lookup fallback. None of these widened the contract; each is a one-line bridge keeping the post-mq.1 qualified shape consistent across the pipeline.
  • Tasks 3-6 ran as one coherent fix. The plan structured them as sequential tasks, but Task 3 alone left several existing tests red (registry still bare-keyed, residual still bare). Walking the plan literally and expecting "green after Task 3" per the plan's Step 9 was impossible; the four tasks form one consistent rekey.

Known debt

  • The test_22b1_dup_a / test_22b1_dup_b / test_22b1_dup_entry fixtures are now orphan from the duplicate-instance test (which uses test_22b1_dup_same_module instead). They're still referenced by hash.rs:293 for the canonical-form hash pin. A cleanup pass could either retire them entirely (and replace the hash pin with the new fixture) or keep them as historical reference. Deferred — not load-bearing.
  • qualify_class_ref_in_check in ailang-check/src/lib.rs is a one-line duplicate of workspace::qualify_class_ref. The alternative is exposing qualify_class_ref as pub(crate) or re-exporting; the duplication was the minimum-edit path. A consolidation tidy could land in iter 2.

Files touched

Code:

  • crates/ail/src/main.rs
  • crates/ail/tests/ct1_check_cli.rs
  • crates/ail/tests/typeclass_22b2.rs
  • crates/ail/tests/typeclass_22b3.rs
  • crates/ailang-check/src/lib.rs
  • crates/ailang-check/src/mono.rs
  • crates/ailang-check/tests/env_construction_pin.rs
  • crates/ailang-core/src/ast.rs
  • crates/ailang-core/src/hash.rs
  • crates/ailang-core/src/workspace.rs
  • crates/ailang-core/tests/ctt2_registry_rekey.rs

Fixtures (existing, migrated):

  • examples/cmp_max_smoke.ail.json
  • examples/ctt2_collision_lib.ail.json
  • examples/ctt2_collision_main.ail.json
  • examples/eq_ord_polymorphic.ail.json
  • examples/eq_ord_user_adt.ail.json
  • examples/test_22b1_dup_a.ail.json
  • examples/test_22b1_dup_b.ail.json
  • examples/test_22b1_dup_entry.ail.json
  • examples/test_22b1_orphan_third.ail.json
  • examples/test_22b2_unbound_constraint_var.ail.json

Fixtures (new):

  • examples/mq1_xmod_constraint_class.ail.json
  • examples/mq1_xmod_constraint_class_dep.ail.json
  • examples/test_22b1_dup_classmod.ail.json
  • examples/test_22b1_dup_same_module.ail.json

Stats

bench/orchestrator-stats/2026-05-13-iter-mq.1.json