iter ct.1.5a-followup: forall-constraints recursion + docstring + drop iteration-tag comments

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.
This commit is contained in:
2026-05-11 01:39:26 +02:00
parent 7872ccfc23
commit 5e653ff536
3 changed files with 76 additions and 18 deletions
+1 -5
View File
@@ -1538,11 +1538,7 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> {
// → no-instance. The hash uses `canonical::type_hash`, the
// same key shape `workspace::build_registry` writes with, so
// representation-equal types match deterministically.
// ct.1.5a: route `r_ty` through
// `Registry::normalize_type_for_lookup` so a bare
// `Type::Con` produced for an instance whose head-type lives
// in another module rewrites to the qualified form the
// registry actually keyed on.
// normalize to match Registry::normalize_type_for_lookup contract
let r_ty_norm = env.workspace_registry.normalize_type_for_lookup(&r_ty);
let key = (
r.class.clone(),
+3 -12
View File
@@ -115,12 +115,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
// never mutates the registry.
for t in &new {
let key = mono_target_key(t);
// ct.1.5a: route `t.type_` through
// `Registry::normalize_type_for_lookup` so a bare
// `Type::Con` produced for an instance whose head-type lives
// in another module rewrites to the qualified form the
// registry actually keyed on. Symmetric to the
// `collect_targets_*` lookups above.
// normalize to match Registry::normalize_type_for_lookup contract
let t_ty_norm = ws_owned.registry.normalize_type_for_lookup(&t.type_);
let registry_key =
(t.class.clone(), ailang_core::canonical::type_hash(&t_ty_norm));
@@ -521,9 +516,7 @@ pub fn collect_mono_targets(
if !crate::is_fully_concrete(&r_ty) {
continue;
}
// ct.1.5a: normalise `r_ty` to its always-qualified canonical
// form before hashing — symmetric to the write side in
// `workspace::build_registry`.
// normalize to match Registry::normalize_type_for_lookup contract
let r_ty_norm = env.workspace_registry.normalize_type_for_lookup(&r_ty);
let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm));
let entry = match env.workspace_registry.entries.get(&key) {
@@ -879,9 +872,7 @@ pub(crate) fn collect_residuals_ordered(
out.push(None);
continue;
}
// ct.1.5a: normalise `r_ty` to its always-qualified canonical
// form before hashing — symmetric to the write side in
// `workspace::build_registry`.
// normalize to match Registry::normalize_type_for_lookup contract
let r_ty_norm = env.workspace_registry.normalize_type_for_lookup(&r_ty);
let key = (r.class.clone(), ailang_core::canonical::type_hash(&r_ty_norm));
let entry = match env.workspace_registry.entries.get(&key) {
+72 -1
View File
@@ -92,6 +92,17 @@ pub struct Registry {
/// before computing the registry key. Primitives are not present.
/// Populated in [`build_registry`] from the same scan that builds
/// the entry map.
///
/// Note: keyed by bare type name only. If two modules each define a
/// type with the same bare name (e.g. `type Foo` in both `M` and
/// `N`), the second `insert` overwrites the first, and
/// [`Self::normalize_type_for_lookup`] would collapse `M.Foo` and
/// `N.Foo` to whichever module wins the race. Acceptable for the
/// current corpus (all in-tree fixtures use distinct bare type
/// names across modules); revisit if a future workspace breaks the
/// assumption. Proper fix is to re-key as
/// `(owning_module, bare_name) -> defining_module` and thread the
/// calling module through every consumer site — out of ct.1's scope.
pub type_def_module: BTreeMap<String, String>,
}
@@ -864,7 +875,13 @@ fn normalize_type_for_registry(
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
constraints: constraints.clone(),
constraints: constraints
.iter()
.map(|c| crate::ast::Constraint {
class: c.class.clone(),
type_: normalize_type_for_registry(&c.type_, type_def_module),
})
.collect(),
body: Box::new(normalize_type_for_registry(body, type_def_module)),
},
Type::Var { name } => Type::Var { name: name.clone() },
@@ -2430,6 +2447,60 @@ mod tests {
}
}
/// ct.1.5a follow-up: `normalize_type_for_registry` must recurse into
/// `Type::Forall.constraints[].type_` just like it does into
/// `Type::Forall.body`. A bare `Type::Con` nested inside a Forall
/// constraint that lives in a different module must be rewritten to
/// its qualified form, otherwise the registry key for a constrained
/// polymorphic type would disagree between bare-local and
/// qualified-cross-module call sites — the same divergence
/// `ct1_registry_duplicate_detection_survives_mixed_canonical_form`
/// guards against at the outer level.
#[test]
fn ct1_5a_normalize_recurses_into_forall_constraints() {
let mut type_def_module: BTreeMap<String, String> = BTreeMap::new();
type_def_module.insert("MyInt".to_string(), "other".to_string());
// Forall a. (TShow MyInt) => a -> a
// The constraint's type carries a bare `MyInt` that should be
// rewritten to `other.MyInt`.
let input = Type::Forall {
vars: vec!["a".to_string()],
constraints: vec![crate::ast::Constraint {
class: "TShow".to_string(),
type_: Type::Con {
name: "MyInt".to_string(),
args: vec![],
},
}],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".to_string() }],
param_modes: vec![],
ret: Box::new(Type::Var { name: "a".to_string() }),
ret_mode: Default::default(),
effects: vec![],
}),
};
let out = normalize_type_for_registry(&input, &type_def_module);
match out {
Type::Forall { constraints, .. } => {
assert_eq!(constraints.len(), 1);
match &constraints[0].type_ {
Type::Con { name, .. } => {
assert_eq!(
name, "other.MyInt",
"constraint type was not normalised; got {name}"
);
}
other => panic!("expected Type::Con, got {other:?}"),
}
}
other => panic!("expected Type::Forall, got {other:?}"),
}
}
/// ct.1: a qualified `Constraint.class` nested inside a
/// `ClassDef.methods[].ty.Forall.constraints` must fire
/// `QualifiedClassName`. Distinct from the `Def::Fn` site: the