refactor(check): unify the two Type-Con qualifiers over an is-local predicate

qualify_local_types and qualify_workspace_types were two ~55-line
recursive Type rewriters identical in every arm (Con skip-guards for
already-dotted and primitive names; Fn/Var/Forall recursion) except
the bare-name decision: the local form rewrites iff the name is in
local_types; the workspace form leaves names own to the module bare
and otherwise searches module_types for the defining module.

Factor the recursion into one rewrite_type_cons that takes the bare-
name decision as a `&dyn Fn(&str) -> Option<String>` (Some = rewrite to
this qualified name, None = leave bare). The two public functions are
now thin wrappers passing their respective closure. Recursion order
and Con-arm check order are preserved exactly; cross-module
qualification tests green.
This commit is contained in:
2026-06-02 02:05:57 +02:00
parent 7d9ab4d29d
commit d5e69148c6
+50 -69
View File
@@ -4739,50 +4739,53 @@ fn strip_own_module_qual(t: &Type, module: &str) -> Type {
}
}
fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<String, TypeDef>) -> Type {
/// Shared `Type::Con`-rewriting recursion behind [`qualify_local_types`]
/// and [`qualify_workspace_types`]. Walks every `Type` variant and, for
/// each bare (non-dotted, non-primitive) `Con` name, consults
/// `rewrite_bare`: `Some(qualified)` replaces the name, `None` leaves it
/// untouched. Already-qualified names (containing `.`) and primitive
/// names are never offered to the predicate — they always pass through
/// unchanged. The recursion is mode-preserving: `param_modes` /
/// `ret_mode` and `effects` / `vars` / constraint classes are cloned
/// verbatim, only nested `Con`-cons get renamed. The typed-MIR boundary
/// qualifies a cross-module callee sig through here, and the codegen
/// drop path reads `ret_mode == Own` off that node to decide RC
/// trackability; stripping the mode here erased the Own signal for a
/// monomorphised polymorphic intrinsic (e.g. `RawBuf.set`), leaking the
/// owned temporary at the call site.
fn rewrite_type_cons(t: &Type, rewrite_bare: &dyn Fn(&str) -> Option<String>) -> Type {
match t {
Type::Con { name, args } => {
// The first two branches share the body `name.clone()` but
// express semantically distinct reasons (already qualified;
// primitive needs no qualification). Combining them with
// `||` would obscure why each disqualifies the name.
#[allow(clippy::if_same_then_else)]
// The first two guards both pass the name through unchanged
// but express semantically distinct reasons (already
// qualified; primitive needs no qualification). Only a bare,
// non-primitive name is offered to `rewrite_bare`.
let qualified_name = if name.contains('.') {
name.clone()
} else if ailang_core::primitives::is_primitive_name(name) {
name.clone()
} else if local_types.contains_key(name) {
format!("{owner_module}.{name}")
} else {
name.clone()
rewrite_bare(name).unwrap_or_else(|| name.clone())
};
Type::Con {
name: qualified_name,
args: args
.iter()
.map(|a| qualify_local_types(a, owner_module, local_types))
.map(|a| rewrite_type_cons(a, rewrite_bare))
.collect(),
}
}
// Qualification only renames `Con` type-cons; it never changes
// the fn's arity, so the positional `param_modes` stay aligned.
// Preserve them (and `ret_mode`): this transform must be
// mode-preserving, mirroring the sister `qualify_workspace_types`
// and `Subst::apply`. The typed-MIR boundary qualifies a
// cross-module callee sig through here, and the codegen drop path
// reads `ret_mode == Own` off that node to decide RC trackability;
// stripping the mode here erased the Own signal for a
// monomorphised polymorphic intrinsic (e.g. `RawBuf.set`), leaking
// the owned temporary at the call site.
Type::Fn { params, param_modes, ret, ret_mode, effects } => Type::Fn {
params: params
.iter()
.map(|p| qualify_local_types(p, owner_module, local_types))
.map(|p| rewrite_type_cons(p, rewrite_bare))
.collect(),
ret: Box::new(qualify_local_types(ret, owner_module, local_types)),
effects: effects.clone(),
param_modes: param_modes.clone(),
ret: Box::new(rewrite_type_cons(ret, rewrite_bare)),
ret_mode: ret_mode.clone(),
effects: effects.clone(),
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
@@ -4790,15 +4793,27 @@ fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<Stri
.iter()
.map(|c| Constraint {
class: c.class.clone(),
type_: qualify_local_types(&c.type_, owner_module, local_types),
type_: rewrite_type_cons(&c.type_, rewrite_bare),
})
.collect(),
body: Box::new(qualify_local_types(body, owner_module, local_types)),
body: Box::new(rewrite_type_cons(body, rewrite_bare)),
},
Type::Var { .. } => t.clone(),
}
}
/// Rewrites bare `Type::Con` references that resolve against
/// `local_types` (the owner module's own types) into qualified
/// `<owner_module>.<name>` form. A bare name that is not a known local
/// type is left untouched.
fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap<String, TypeDef>) -> Type {
rewrite_type_cons(t, &|name| {
local_types
.contains_key(name)
.then(|| format!("{owner_module}.{name}"))
})
}
/// prep.1 (kernel-extension-mechanics): rewrites bare cross-module
/// `Type::Con` references in a consumer module's declared types into
/// qualified `<home>.<Type>` form. Symmetric to [`qualify_local_types`]
@@ -4813,53 +4828,19 @@ pub(crate) fn qualify_workspace_types(
own_local_types: &IndexMap<String, TypeDef>,
module_types: &BTreeMap<String, IndexMap<String, TypeDef>>,
) -> Type {
match t {
Type::Con { name, args } => {
let qualified_name = if name.contains('.') {
name.clone()
} else if ailang_core::primitives::is_primitive_name(name) {
name.clone()
} else if own_local_types.contains_key(name) {
name.clone()
} else if let Some(home) = module_types
.iter()
.find_map(|(m, types)| if types.contains_key(name) { Some(m.clone()) } else { None })
{
format!("{home}.{name}")
} else {
name.clone()
};
Type::Con {
name: qualified_name,
args: args
.iter()
.map(|a| qualify_workspace_types(a, own_local_types, module_types))
.collect(),
}
rewrite_type_cons(t, &|name| {
if own_local_types.contains_key(name) {
None
} else {
module_types.iter().find_map(|(m, types)| {
if types.contains_key(name) {
Some(format!("{m}.{name}"))
} else {
None
}
})
}
Type::Fn { params, ret, effects, param_modes, ret_mode } => Type::Fn {
params: params
.iter()
.map(|p| qualify_workspace_types(p, own_local_types, module_types))
.collect(),
ret: Box::new(qualify_workspace_types(ret, own_local_types, module_types)),
effects: effects.clone(),
param_modes: param_modes.clone(),
ret_mode: ret_mode.clone(),
},
Type::Forall { vars, constraints, body } => Type::Forall {
vars: vars.clone(),
constraints: constraints
.iter()
.map(|c| Constraint {
class: c.class.clone(),
type_: qualify_workspace_types(&c.type_, own_local_types, module_types),
})
.collect(),
body: Box::new(qualify_workspace_types(body, own_local_types, module_types)),
},
Type::Var { .. } => t.clone(),
}
})
}
/// prep.1: walk a [`Module`] and rewrite every `Type` annotation