Adds three end-to-end tests for the 22b.3 mono pass, each with a
matching one-property fixture under examples/. Defends mono-pass
invariants 3, 4, and 6 from docs/specs/2026-05-09-22-typeclasses.md
at the binary level (stdout-asserting, not AST-asserting):
- coherence_two_instances_pick_correct_body_at_runtime — class R
with instances at Int and Bool, both reachable from one main.
Stdout `11\n22` proves both mono fns are synthesised AND each
call site picks the matching body (no collapse).
- class_default_method_runs_and_prints_default_value — empty
instance body + class-level `default` clause must lower, link,
and print the default value (`99`). Promotes the existing
synthesise_mono_fn unit test to a binary-level guarantee.
- cross_module_class_method_runs_and_prints_correct_value —
class+instance in module A, called from module B's main. The
qualified-name rewrite + cross-module synth-def placement was
AST-tested in 22b.3.6; this pins down the emit-and-link path.
All 18 typeclass_22b3 tests pass; full workspace green; 3 bench
gates 0/0/0.
Quality-review follow-up to 89311ee:
- Revert `substitute_rigids` to private. Task 3 does not call it; the
visibility bump was forward-looking for Task 4 (synthesise_mono_fn).
Task 4 will re-bump it in the same diff that uses it.
- Promote `build_module_types` to pub(crate); drop the inline
reimplementation in `mono::build_workspace_env`. Removes drift risk
between the two paths.
- Replace defensive `unwrap_or_default()` on `build_module_globals`
with `expect(...)` naming the pre-condition. Silent default violated
CLAUDE.md "no defensive validation in internal code paths".
- Add a one-line drift-risk reminder at the top of `build_workspace_env`
flagging that it and `check_in_workspace` both populate `Env`.
Build clean; all 5 typeclass_22b3 tests green; no workspace regressions.
Float is referenced nowhere else in the codebase; the plan-text
listed it aspirationally. Restored parity with the four other
primitive enumerations across the crate. When Float lands as a
real type (post-22b.4 Prelude work, if at all), the arm gets
re-added in the same iter that introduces Type::float().
Deferred (not part of this fix):
- Shared-constant extraction across the four primitive enumeration
sites (linearity.rs:143, lib.rs:1232/2214, mono.rs) — workspace-
level cleanup belonging to the milestone-22 audit phase.
- pub mod mono vs sibling pub use lift::lift_letrecs style — plan
prescribed pub mod for 22b.3 because Tasks 3-7 add multiple pub
items reached by integration tests; per-item pub use would
proliferate.
Four new E2E tests in typeclass_22b2.rs covering invariants the
per-task fixtures (all single-module, single-fn) did not exercise:
- cross_module_class_method_resolves_and_fires_no_instance:
module B's call to `show` (declared in module A) reaches the
no-instance arm, proving Env.class_methods is merged workspace-
wide and not restricted to the local module.
- cross_module_polymorphic_fn_without_constraint_fires_missing_constraint:
same merge guarantees the missing-constraint arm sees the residual
produced by a call to A's class method from inside B.
- cross_module_instance_satisfies_concrete_call: the workspace
registry built at load time over all modules must populate keys
visible from B; a concrete call resolves clean cross-module.
- two_fns_each_missing_constraint_produce_two_diagnostics: pins
check_in_workspace's per-def aggregation — two ill-formed fns
produce two diagnostics (one per fn), not one.
Six new fixtures (xmod_* trio + two_fns_*).
Adds the missing positive counterpart to the no-instance test:
`show 42` with `instance Show Int` PRESENT must typecheck green.
This guards against registry-keying or threading bugs that would
otherwise sneak past the negative-only test (an empty registry at
typecheck time, or a key shape mismatch, would silently fire
no-instance even when the instance exists).
Also drops the dead `let _ = ws;` and its stale comment in
`check_in_workspace` — the line above already consumes
`ws.registry`, so the explicit drop is redundant. The remaining
comment is folded into the registry-threading comment so the
"registry is the only thing threaded" rationale is preserved.
Address quality review of a9aa248:
- Replace string-prefix discriminator (`prior.starts_with("class ")`)
with a structural `Origin { Class | Fn }` enum; `kind` is now a
`match` on variants, formatting is reserved for error construction.
- Fix fn-fn collision misreport: in the `Def::Fn` arm, only fire
`MethodNameCollision` when the prior origin is a class. Two
`Def::Fn` with the same name are `CheckError::DuplicateDef`'s job
in `ailang-check`, not this pre-pass.
- Tests use `examples_dir()` like the rest of the module; path
construction is no longer ad-hoc.
- Tests assert `first_origin` / `second_origin` so an origin-format
regression cannot silently break the `kind` discriminator.
- Drop forward-looking 22b.4 prelude reservation note from the
variant doc — keep only what's load-bearing for 22b.2.
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 the workspace-global typeclass instance registry per Decision 11
"Resolution and monomorphisation". Built at the end of load_workspace
after the DFS over imports completes; keyed by (class-name, type-hash)
where type-hash uses the new canonical::type_hash (16-hex prefix,
parallel to def_hash and module_hash).
Three coherence checks fire from build_registry, each surfacing as a
distinct WorkspaceLoadError variant:
- OrphanInstance: `instance C T` not in C's or T's defining module.
- DuplicateInstance: two entries share the same (class, type-hash) key.
- MissingMethod: instance omits a required (non-default) method.
The CLI's workspace_error_to_diagnostic is extended with the three new
codes (orphan-instance / duplicate-instance / missing-method).
All existing Workspace { ... } construction sites get the new
`registry` field, defaulting to Registry::default() at synthetic /
test-only paths and threading through ws.registry.clone() at codepath
sites that already hold a real workspace.
Includes hash-stability regression tests (iter22b1_schema_extension_*
and iter22b1_classdef_empty_optionals_hash_stable) and the empty-
registry positive test against examples/sum.ail.json. Test count:
288 → 291 (3 new tests, all pass).
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).