diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 92fb915..23155b2 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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) -> 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) -> 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 t.clone(), } } +/// Rewrites bare `Type::Con` references that resolve against +/// `local_types` (the owner module's own types) into qualified +/// `.` 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) -> 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 `.` form. Symmetric to [`qualify_local_types`] @@ -4813,53 +4828,19 @@ pub(crate) fn qualify_workspace_types( own_local_types: &IndexMap, module_types: &BTreeMap>, ) -> 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