iter 24.tidy: close 5 actionable drift items from audit-24

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).
This commit is contained in:
2026-05-13 04:28:15 +02:00
parent 0e27533e73
commit 4e8447d15d
7 changed files with 502 additions and 41 deletions
+71
View File
@@ -1692,6 +1692,77 @@ unresolved at the entry point — is a static error, not a runtime
one. This is the LLVM-friendly form and is consistent with
Decision 10's performance commitment.
### Cross-module references in synthesised bodies
The unified mono pass (per Decision 11's milestone-23.4 reorganisation)
synthesises mono symbols for polymorphic free fns and class-method
instances in the symbol's owner module — e.g. `print__Int` lives in
`prelude` (because `print` is defined in the prelude), but a
user-ADT call site `print (MkIntBox 7)` causes synthesis of
`prelude.print__IntBox` whose body references
`show_user_adt.show__IntBox` (the user instance lives in the
user-defining module per Decision 11 coherence). The synthesised body
crosses a module boundary the source template did not.
Three invariants make this work, all installed in milestone 24's iter
24.3 and worth keeping load-bearing:
1. **`MonoTarget::FreeFn::type_args` carries canonical types
post-collection.** At every site where `subst.apply(m)` produces a
concrete substitution that enters `MonoTarget::FreeFn::type_args`
(`crates/ailang-check/src/mono.rs::collect_mono_targets` and
`::collect_residuals_ordered`), the resolved `Type` must be passed
through `Registry::normalize_type_for_lookup(caller_module, &t)`
before being pushed. The downstream synthesised body's Phase 3
rewrite cursor (which runs in the OWNER module's context, not the
caller's) keys lookups in `Registry::entries` by the canonical
qualified form; a bare type-con reference at the type_args layer
silently drops the cross-module mono-symbol synthesis and leaves a
bare `Var "show"` post-mono that codegen later rejects with
`unknown variable`.
2. **Post-mono synthesised body cross-module references may bypass
the source template's `import_map`.** `prelude` does not import
user modules (the auto-injection runs one-way: user workspaces
import prelude, never the inverse). But a synthesised body for
`prelude.print__<UserType>` references `<user_module>.show__<UserType>`,
created by mono — not by the prelude source. Codegen's
cross-module name-resolution must accept this: at
`crates/ailang-codegen/src/lib.rs::resolve_top_level_fn` (Var-
resolution), `::lower_app`'s cross-module call arm, and
`::synth_with_extras`'s Var arm, the resolution first tries the
current module's `import_map`, then falls back to a direct
`module_user_fns` / `module_def_ail_types` lookup against the
prefix. Both ends were independently typechecked under their own
module contexts before mono ran; the cross-module reference is a
post-mono construct, not a source-language one.
3. **FreeFnCall synth pushes one residual per declared forall-
constraint.** At `crates/ailang-check/src/lib.rs`'s synth Var arm
for the `prefix.suffix` free-fn path, when the type is a
`Type::Forall { vars, constraints, body }`, instantiation produces
fresh metavars for the `vars` AND pushes one `ResidualConstraint`
per entry in `constraints` (with rigid vars substituted by the
freshly-generated metavars). The downstream discharge loop at
`check_fn`'s post-synth phase resolves each residual against the
workspace registry; if no instance satisfies the residual at the
unified concrete type, the `NoInstance` diagnostic fires at
typecheck (correctly), not at codegen (confusingly). Without the
residual push, milestone-23-shape negative cases (e.g. `print f`
where `f : Int -> Int`) silently typecheck and surface as
`unknown variable: show` from codegen instead of the right
typecheck-phase NoInstance Show.
The three invariants are lockstep partners: invariant (1) creates the
cross-module reference, invariant (2) makes codegen able to resolve
it, invariant (3) makes the typecheck-time discharge fire correctly
when no instance exists. A future refactor that loosens any one of
the three breaks the user-ADT trajectory; the test pins at
`crates/ail/tests/codegen_import_map_fallback_pin.rs` (invariant 2),
`crates/ail/tests/polyfn_dot_qualified_branch_pin.rs` (invariant 3
+ lockstep), and the existing `crates/ail/tests/show_user_adt`
fixture (full trajectory) collectively protect the contract.
### Defaults and superclasses
**Defaults.** A `ClassDef.methods[i].default` is either `null` (the