From 5e653ff5366c4712f59b4b388bdf3d7f94eaa68f Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 11 May 2026 01:39:26 +0200 Subject: [PATCH] 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. --- crates/ailang-check/src/lib.rs | 6 +-- crates/ailang-check/src/mono.rs | 15 ++---- crates/ailang-core/src/workspace.rs | 73 ++++++++++++++++++++++++++++- 3 files changed, 76 insertions(+), 18 deletions(-) diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index e1a88df..bbe8220 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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(), diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index b530e6f..58116b7 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -115,12 +115,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { // 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) { diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index 6b242e5..fb98c24 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -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, } @@ -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 = 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