4e8447d15d
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).
72 lines
3.1 KiB
Rust
72 lines
3.1 KiB
Rust
//! Pin for the iter-24.3 FreeFnCall constraint-residual-push
|
|
//! invariant (DESIGN.md §"Cross-module references in synthesised
|
|
//! bodies" invariant 3).
|
|
//!
|
|
//! Property protected: bare-name references to polymorphic free fns
|
|
//! that resolve through the auto-injected-prelude path AT THE
|
|
//! DOT-QUALIFIED SYNTH BRANCH push residuals for the fn's declared
|
|
//! constraints. The discharge loop then fires `NoInstance` at
|
|
//! typecheck if no instance ships for the unified concrete type.
|
|
//!
|
|
//! Failure mode this pin catches: a future refactor changes the
|
|
//! prelude auto-injection resolution path so that bare-name `print`
|
|
//! reaches synth via a different branch (e.g. locals, env.module_globals
|
|
//! direct hit) that does NOT push residuals. Without this pin, the
|
|
//! regression surfaces as `unknown variable: show` from codegen for
|
|
//! the negative case — confusing diagnostic, hard to bisect.
|
|
|
|
use ailang_check::check_workspace;
|
|
use ailang_core::workspace::load_workspace;
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture_path() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("../../examples")
|
|
.join("show_no_instance.ail.json")
|
|
}
|
|
|
|
#[test]
|
|
fn bare_name_polyfn_fires_typecheck_no_instance_not_codegen_unknown_var() {
|
|
// The fixture calls `print f` bare-name (no `prelude.` qualifier)
|
|
// where `f : Int -> Int`. The auto-injected-prelude resolution
|
|
// must route this through the dot-qualified synth branch so that
|
|
// the `Show a` declared constraint of `print` produces a residual,
|
|
// and the discharge loop fires `no-instance`.
|
|
let ws = load_workspace(&fixture_path()).expect("workspace loads");
|
|
let diags = check_workspace(&ws);
|
|
|
|
// Exactly one `no-instance` diagnostic — proves:
|
|
// (a) the bare-name `print` resolved (was not "unknown variable")
|
|
// (b) the resolution reached the dot-qualified synth branch
|
|
// which pushes the declared-constraint residual
|
|
// (c) the discharge loop ran with the residual and fired the
|
|
// NoInstance because no `Show (Int -> Int)` instance exists.
|
|
let no_inst: Vec<_> = diags.iter().filter(|d| d.code == "no-instance").collect();
|
|
assert_eq!(
|
|
no_inst.len(),
|
|
1,
|
|
"expected exactly one 'no-instance' diagnostic — got {} (all diags: {diags:?})",
|
|
no_inst.len()
|
|
);
|
|
|
|
// No "unknown variable" or other codegen-grade errors at typecheck:
|
|
// if the residual push did NOT fire, the typecheck would pass
|
|
// silently and the error would only surface at codegen.
|
|
let unknown_vars: Vec<_> = diags
|
|
.iter()
|
|
.filter(|d| {
|
|
d.code == "unknown-variable"
|
|
|| d.message.contains("unknown variable")
|
|
|| d.code == "internal"
|
|
})
|
|
.collect();
|
|
assert!(
|
|
unknown_vars.is_empty(),
|
|
"expected zero 'unknown variable' or 'internal' diagnostics at typecheck — \
|
|
got {} (all diags: {diags:?}). If this fires, the bare-name `print` \
|
|
resolution bypassed the dot-qualified synth branch and the constraint \
|
|
residual was never pushed.",
|
|
unknown_vars.len()
|
|
);
|
|
}
|