From 98935b860206affac8dfd28d259043e96b612d99 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 13:37:33 +0000 Subject: [PATCH 01/25] feat(defs): identify `impl` blocks and their methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An `impl` block was the one declaration the table did not identify, so everything keyed on one — `ImplBlockRef`, `impl_sigs`, `method_sigs`, the impl index — spelled it `(ModuleSource, AstId)` instead. It writes no name, so `name` renders empty and no scope can hold it; a local block is kept out of its block's name map for the same reason. Its methods are members, reached by dispatch like a trait's. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/defs.rs | 136 +++++++++++++++++++++++++++++++---- wado-compiler/src/resolve.rs | 3 + 2 files changed, 124 insertions(+), 15 deletions(-) diff --git a/wado-compiler/src/defs.rs b/wado-compiler/src/defs.rs index 079629cbfbf..96077d40a23 100644 --- a/wado-compiler/src/defs.rs +++ b/wado-compiler/src/defs.rs @@ -83,7 +83,12 @@ pub enum DefKind { VariantCase, /// A `flags` member. FlagsMember, - /// A method declared by a trait, a resource, or an effect interface. + /// An `impl` block. It writes no name, so nothing can import it and no + /// scope answers for it; its identity exists so the methods it declares + /// have an owner and its own facts have a key. + Impl, + /// A method declared by a trait, a resource, an effect interface, or an + /// `impl` block. Method, } @@ -206,20 +211,24 @@ impl DefTable { // it would otherwise be the one type in the language with no // declaration to name it. for item in &ast.items { - if let Item::TupleTypeDecl(decl) = item - && !table.by_ast_id.contains_key(&decl.id) - { - table.declare(Def { - ast_id: decl.id, - module: module.clone(), - name: crate::name::TUPLE_TYPE_NAME.to_string(), - kind: DefKind::BuiltinType, - visibility: decl.visibility, - span: Some(decl.span), - parent: None, - function_local: false, - members: Vec::new(), - }); + match item { + Item::TupleTypeDecl(decl) if !table.by_ast_id.contains_key(&decl.id) => { + table.declare(Def { + ast_id: decl.id, + module: module.clone(), + name: crate::name::TUPLE_TYPE_NAME.to_string(), + kind: DefKind::BuiltinType, + visibility: decl.visibility, + span: Some(decl.span), + parent: None, + function_local: false, + members: Vec::new(), + }); + } + // An `impl` block is a declaration the symbol table never + // collected, since it writes no name to collect it under. + Item::Impl(block) => table.declare_impl_block(module, block, false), + _ => {} } } table.declare_members(module, ast); @@ -256,12 +265,45 @@ impl DefTable { } } + /// Identify an `impl` block, which no symbol table row names. + /// + /// Its name is empty because it writes none: `name` is a rendering, and an + /// `impl` block has nothing to render. That is also what keeps it out of + /// every scope — a name-keyed layer can only be reached by a spelling, and + /// no spelling reaches this. + fn declare_impl_block( + &mut self, + module: &ModuleSource, + block: &crate::ast::ImplBlock, + function_local: bool, + ) { + if self.by_ast_id.contains_key(&block.id) { + return; + } + self.declare(Def { + ast_id: block.id, + module: module.clone(), + name: String::new(), + kind: DefKind::Impl, + visibility: Visibility::Private, + span: Some(block.span), + parent: None, + function_local, + members: Vec::new(), + }); + } + /// Identify a function-local item (`Stmt::Item`) and its members. /// /// A local `struct` is a declaration like any other — two functions writing /// the same spelling declare two of them — so it gets an identity rather /// than a mangled storage name standing in for one. fn declare_local_item(&mut self, module: &ModuleSource, item: &Item) { + if let Item::Impl(block) = item { + self.declare_impl_block(module, block, true); + self.declare_item_members(module, item); + return; + } let (kind, name) = match item { Item::Struct(d) => (DefKind::Struct, &d.name), Item::Enum(d) => (DefKind::Enum, &d.name), @@ -343,6 +385,15 @@ impl DefTable { i.methods.iter().map(|m| (m.id, &m.name, None, m.span)), ), ), + Item::Impl(b) => ( + b.id, + members( + DefKind::Method, + b.methods + .iter() + .map(|m| (m.id, &m.name, Some(m.visibility), m.span)), + ), + ), _ => return, }; let Some(owner) = self.of_ast_id(owner) else { @@ -637,6 +688,61 @@ mod tests { assert_eq!(field_names(widgets[1]), ["b"]); } + /// An `impl` block is a declaration: two blocks writing the same method + /// name declare two methods, and each block owns its own. + #[test] + fn an_impl_block_and_its_methods_are_declarations() { + let source = r#" + pub struct Point { x: i32 } + pub struct Line { a: i32 } + impl Point { pub fn len(&self) -> i32 { return self.x; } } + impl Line { pub fn len(&self) -> i32 { return self.a; } } + "#; + let (defs, module) = build_from_source(source); + let blocks: Vec = defs.iter().filter(|d| defs.kind(*d) == DefKind::Impl).collect(); + assert_eq!(blocks.len(), 2); + assert_ne!(blocks[0], blocks[1]); + // No spelling reaches an `impl` block, so it renders none. + assert_eq!(defs.name(blocks[0]), ""); + assert_eq!(defs.module(blocks[0]), &module); + + for block in &blocks { + assert_eq!(defs.members(*block).len(), 1); + let method = defs.members(*block)[0]; + assert_eq!(defs.name(method), "len"); + assert_eq!(defs.kind(method), DefKind::Method); + assert_eq!(defs.parent(method), Some(*block)); + } + assert_ne!(defs.members(blocks[0])[0], defs.members(blocks[1])[0]); + } + + /// A local `impl` block is declared by the function that writes it, so its + /// methods are identified like any other block's. + #[test] + fn a_function_local_impl_block_is_a_declaration_of_its_own() { + let source = r#" + pub fn run() -> i32 { + struct Widget { a: i32 } + impl Widget { fn get(&self) -> i32 { return self.a; } } + let w = Widget { a: 1 }; + return w.get(); + } + "#; + let (defs, _) = build_from_source(source); + let block = defs + .iter() + .find(|d| defs.kind(*d) == DefKind::Impl) + .expect("the local impl block is a declaration"); + assert!(defs.is_function_local(block)); + assert_eq!( + defs.members(block) + .iter() + .map(|m| defs.name(*m)) + .collect::>(), + ["get"] + ); + } + /// A cached declaration fact carries a [`DefId`], so a later compile that /// re-identifies the same declarations must hand back the same ones — the /// stdlib snapshot reads its `ImplSig`s back this way. diff --git a/wado-compiler/src/resolve.rs b/wado-compiler/src/resolve.rs index 7489fb6e123..219b9a14b46 100644 --- a/wado-compiler/src/resolve.rs +++ b/wado-compiler/src/resolve.rs @@ -471,7 +471,10 @@ impl AstVisitor for Resolver<'_> { fn visit_block(&mut self, block: &ast::Block) { let mut scope = IndexMap::default(); for stmt in &block.stmts { + // A local `impl` block writes no name for the scope to hold — its + // methods reach their receiver's type, never this map. if let ast::Stmt::Item(item) = stmt + && !matches!(**item, ast::Item::Impl(_)) && let Some(def) = self.defs.of_ast_id(item.id()) { scope.insert(self.defs.name(def).to_string(), def); From 9745fae27a42223f8d70eb9d18c3800a33a58cc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 14:00:32 +0000 Subject: [PATCH 02/25] refactor(elaborator): key the impl index by `DefId` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every impl index spelled an impl block `(ModuleSource, AstId)` — a pair standing in for the identity the block now has. `TraitImplIndex`, `impl_headers`, `blanket_pack_assocs`, `blanket_param_sources`, `ImplBlockRef`, `BlanketImpl` and `Signatures::impl_sigs` key by the block's `DefId` instead, and a consumer that needed the module reads it off the declaration. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/expr.rs | 7 +- wado-compiler/src/elaborator/item.rs | 8 +- wado-compiler/src/elaborator/method_call.rs | 42 +++---- wado-compiler/src/elaborator/method_lookup.rs | 56 ++++----- wado-compiler/src/elaborator/orchestration.rs | 4 +- wado-compiler/src/elaborator/sem/decls.rs | 4 +- wado-compiler/src/elaborator/sig.rs | 10 +- wado-compiler/src/elaborator/trait_env.rs | 106 +++++++++--------- wado-compiler/src/elaborator/trait_query.rs | 2 +- 9 files changed, 120 insertions(+), 119 deletions(-) diff --git a/wado-compiler/src/elaborator/expr.rs b/wado-compiler/src/elaborator/expr.rs index 83f80608870..db1c6c0e2a1 100644 --- a/wado-compiler/src/elaborator/expr.rs +++ b/wado-compiler/src/elaborator/expr.rs @@ -4925,7 +4925,7 @@ impl Elaborator<'_, H> { // argument are header facts, so the impls are reached by the target's // canonical key rather than by scanning every module for one whose // written target name matches. - let declares_from = |key: &(ModuleSource, crate::ast::AstId)| -> bool { + let declares_from = |key: &crate::defs::DefId| -> bool { self.tysys .trait_env .impl_headers @@ -4947,10 +4947,11 @@ impl Elaborator<'_, H> { .trait_env .all_impl_keys(&self.impl_target(target_name)); // The current module wins a tie. + let defs = self.tysys.resolutions.defs(); keys.iter() - .find(|key| key.0 == self.current_module_source && declares_from(key)) + .find(|key| *defs.module(**key) == self.current_module_source && declares_from(key)) .or_else(|| keys.iter().find(|key| declares_from(key))) - .map(|(module, _)| module.clone()) + .map(|key| defs.module(*key).clone()) // The `From` impl may be synthesized later, so a miss is not an error. .unwrap_or_else(|| self.current_module_source.clone()) } diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index 611d6b4a4f3..1872fcd211c 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -888,8 +888,14 @@ impl TypeParamScope<'_, '_, H> { .self_type .expect("entering an impl frame binds Self to the target"); + let impl_def = scope + .tysys + .resolutions + .defs() + .of_ast_id(impl_block.id) + .expect("every impl block is a declaration"); scope.sem.decls.impl_sigs.insert( - impl_block.id, + impl_def, super::sig::ImplSig { self_type, target_type_args, diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index ce8410f1db1..b3c30a9810c 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -2426,7 +2426,7 @@ impl Elaborator<'_, H> { .tysys .trait_env .impl_headers - .get(&(b.module.clone(), b.ast_id)) + .get(&b.def) else { return false; }; @@ -2451,7 +2451,7 @@ impl Elaborator<'_, H> { .tysys .trait_env .impl_headers - .get(&(b.module.clone(), b.ast_id))?; + .get(&b.def)?; Some(( self.tysys .trait_env @@ -2762,23 +2762,19 @@ impl Elaborator<'_, H> { /// parameter (`impl Trait for V`) keys under that binder instead. /// Both are searched in the current module, only the declaration namespace /// outside it. Consumers re-check the impl's spelling. - fn trait_impl_keys_current_first(&self, struct_name: &str) -> Vec<(ModuleSource, AstId)> { + fn trait_impl_keys_current_first(&self, struct_name: &str) -> Vec { let env = &self.tysys.trait_env; - let declared = env.entries_by_receiver_vec( - &self - .impl_target(struct_name) - .receiver(self.tysys.resolutions.defs()), - ); + let defs = self.tysys.resolutions.defs(); + let declared = env.entries_by_receiver_vec(&self.impl_target(struct_name).receiver(defs)); let binder = env.entries_by_receiver_vec(&Receiver::Type(FqTypeName::binder(struct_name))); - let is_current = - |(module, _): &&(ModuleSource, AstId)| *module == self.current_module_source; - let mut keys: Vec<(ModuleSource, AstId)> = declared + let is_current = |k: &&crate::defs::DefId| *defs.module(**k) == self.current_module_source; + let mut keys: Vec = declared .iter() .chain(binder.iter()) .filter(is_current) - .cloned() + .copied() .collect(); - keys.extend(declared.iter().filter(|k| !is_current(k)).cloned()); + keys.extend(declared.iter().filter(|k| !is_current(k)).copied()); keys } @@ -2804,7 +2800,7 @@ impl Elaborator<'_, H> { .signatures .method_sig(method.ast_id) .expect("the decl pass records every impl-declared method's signature"); - if key.0 == self.current_module_source { + if *self.tysys.resolutions.defs().module(*key) == self.current_module_source { current.push(sig); } else { others.push(sig); @@ -3043,11 +3039,7 @@ impl Elaborator<'_, H> { /// whether each takes `self`, so the question is answered without an /// impl-block AST — and keyed canonically, so two modules' same-named /// types cannot answer for each other. - fn keys_declare_static_method( - &self, - keys: &[(ModuleSource, crate::ast::AstId)], - method_name: &str, - ) -> bool { + fn keys_declare_static_method(&self, keys: &[crate::defs::DefId], method_name: &str) -> bool { keys.iter().any(|key| { self.tysys .trait_env @@ -3134,8 +3126,9 @@ impl Elaborator<'_, H> { .to_string(); let mut candidates: Vec = Vec::new(); let mut has_blanket = false; - for (module, impl_id) in self.trait_impl_keys_current_first(struct_name) { - let header = &self.tysys.trait_env.impl_headers[&(module.clone(), impl_id)]; + for impl_def in self.trait_impl_keys_current_first(struct_name) { + let header = &self.tysys.trait_env.impl_headers[&impl_def]; + let module = self.tysys.resolutions.defs().module(impl_def).clone(); let Some(trait_type) = header.trait_type.as_ref() else { continue; }; @@ -3178,7 +3171,7 @@ impl Elaborator<'_, H> { let source = *self .tysys .signatures - .impl_sig(impl_id) + .impl_sig(impl_def) .expect("the decl pass records every impl block's declaration facts") .trait_type_args .first() @@ -3335,8 +3328,9 @@ impl Elaborator<'_, H> { None }; - for (module_source, impl_id) in self.trait_impl_keys_current_first(struct_name) { - let header = &self.tysys.trait_env.impl_headers[&(module_source.clone(), impl_id)]; + for impl_def in self.trait_impl_keys_current_first(struct_name) { + let header = &self.tysys.trait_env.impl_headers[&impl_def]; + let module_source = self.tysys.resolutions.defs().module(impl_def).clone(); if let Some((trait_name, method_id)) = check_impl(header, &module_source) { return Some(StaticMethodRef::new( module_source, diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index 51bbed66f42..c85544813ab 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -6,9 +6,10 @@ use std::sync::Arc; use crate::hashmap::{IndexMap, IndexSet}; -use crate::ast::{self, AstId, BinaryOp, Expr, Type}; +use crate::ast::{self, BinaryOp, Expr, Type}; use crate::compiler_host::CompilerHost; use crate::compiler_item::CompilerItem; +use crate::defs::DefId; use crate::module_source::ModuleSource; use crate::name::{LocalMethodName, MethodName, RefKind}; use crate::tir::{ @@ -36,10 +37,10 @@ use super::util::placeholder; pub(super) const REPLACE_ON_ASSIGN_PLACE: &str = "a field or element of a replace-on-assign type (primitive, enum, flags, fn); \ use the containing value's reference directly"; -/// Lightweight reference to an impl block. Stores `(module_source, -/// item_id)` and resolves to the block's digested [`ImplHeader`] via -/// [`impl_header`]. Dispatch cannot reach the impl AST at all. -struct ImplBlockRef(ModuleSource, AstId); +/// Lightweight reference to an impl block: its identity, resolving to the +/// block's digested [`ImplHeader`] via [`impl_header`]. Dispatch cannot reach +/// the impl AST at all. +struct ImplBlockRef(DefId); /// The digested header of the impl block `r` points at. Borrowed from the /// caller's `TraitEnv` handle rather than from `&self`, so the header stays @@ -51,7 +52,7 @@ struct ImplBlockRef(ModuleSource, AstId); fn impl_header<'a>(trait_env: &'a TraitEnv, r: &ImplBlockRef) -> &'a ImplHeader { trait_env .impl_headers - .get(&(r.0.clone(), r.1)) + .get(&r.0) .expect("every indexed impl block has an ImplHeader") } @@ -60,7 +61,7 @@ impl Elaborator<'_, H> { fn impl_sig(&self, r: &ImplBlockRef) -> &super::sig::ImplSig { self.tysys .signatures - .impl_sig(r.1) + .impl_sig(r.0) .expect("the decl pass records every impl block's declaration facts") } } @@ -334,7 +335,7 @@ impl TypeSystem { impl Elaborator<'_, H> { /// Get the module source for an `ImplBlockRef`. fn impl_block_module_source(&self, r: &ImplBlockRef) -> ModuleSource { - r.0.clone() + self.tysys.resolutions.defs().module(r.0).clone() } /// Collect trait impl block references for a given type name. @@ -350,7 +351,7 @@ impl Elaborator<'_, H> { .get(entry) .is_some_and(|h| h.trait_name.is_some()) { - refs.push(ImplBlockRef(entry.0.clone(), entry.1)); + refs.push(ImplBlockRef(*entry)); } } } @@ -370,7 +371,7 @@ impl Elaborator<'_, H> { .get(entry) .is_some_and(|h| h.trait_name.is_some()) { - refs.push(ImplBlockRef(entry.0.clone(), entry.1)); + refs.push(ImplBlockRef(*entry)); } } } @@ -408,7 +409,7 @@ impl Elaborator<'_, H> { continue; } let impl_sig = signatures - .impl_sig(impl_ref.1) + .impl_sig(impl_ref.0) .expect("the decl pass records every impl block's declaration facts") .instantiate(&self.tysys.type_table, concrete_type_args); let declared = self @@ -869,14 +870,14 @@ impl Elaborator<'_, H> { // Coherence lets any same-package module host an `impl `. if struct_module_source.is_some() { - let entries: Vec<(ModuleSource, AstId)> = self.tysys.trait_env.inherent_impl_keys( + let entries: Vec = self.tysys.trait_env.inherent_impl_keys( &self.impl_target_of(base_type_id, &crate::name::DeclName::new(&struct_name)), ); // The receiver's own declaration, which is what an impl header // targeting it must name. let receiver_decl = self.tysys.type_table.borrow().nominal_def(base_type_id); - for (impl_module, item_id) in &entries { - let impl_ref = ImplBlockRef(impl_module.clone(), *item_id); + for entry in &entries { + let impl_ref = ImplBlockRef(*entry); let trait_env = Arc::clone(&self.tysys.trait_env); let header = impl_header(&trait_env, &impl_ref); // The header names its target at a site of its own, answered @@ -907,11 +908,11 @@ impl Elaborator<'_, H> { } if struct_module_source.is_none() { - let entries: Vec<(ModuleSource, AstId)> = self.tysys.trait_env.inherent_impl_keys( + let entries: Vec = self.tysys.trait_env.inherent_impl_keys( &self.impl_target_of(base_type_id, &crate::name::DeclName::new(&struct_name)), ); - for (search_module_source, item_id) in &entries { - let impl_ref = ImplBlockRef(search_module_source.clone(), *item_id); + for entry in &entries { + let impl_ref = ImplBlockRef(*entry); let trait_env = Arc::clone(&self.tysys.trait_env); let header = impl_header(&trait_env, &impl_ref); if self.get_type_name(&header.ty) != struct_name @@ -995,7 +996,7 @@ impl Elaborator<'_, H> { let method_header = header.methods.iter().find(|m| m.name == method_name)?; let sig = signatures.method_sig(method_header.ast_id)?; let impl_sig = signatures - .impl_sig(impl_ref.1) + .impl_sig(impl_ref.0) .expect("the decl pass records every impl block's declaration facts"); let slots = impl_sig.slots(&self.tysys.type_table, receiver_type_args.unwrap_or(&[])); @@ -1013,7 +1014,7 @@ impl Elaborator<'_, H> { is_ref_impl: false, method_type_param_ids: sig.own_type_param_ids(), method_own_params: sig.own_params.clone(), - impl_module: Some(impl_ref.0.clone()), + impl_module: Some(self.impl_block_module_source(impl_ref)), from_concrete_impl: self.impl_is_concrete_instantiation(&header.ty), param_defaults: sig.params.iter().map(|p| p.default.clone()).collect(), param_names: super::sig::Param::names(&sig.params), @@ -1578,19 +1579,19 @@ impl Elaborator<'_, H> { // Blanket impl fallback: check `impl Trait for T` where the receiver // type satisfies the bound. e.g., `impl IntoIterator for I` matches // any concrete type that implements Iterator. Snapshot the value blankets - // (module, ast id, bound names) so the per-bound checks below borrow `self` + // (block, bound names) so the per-bound checks below borrow `self` // without holding a `trait_env` borrow. - let value_blankets: Vec<(ModuleSource, AstId, Vec)> = self + let value_blankets: Vec<(DefId, Vec)> = self .tysys .trait_env .blanket_impls .values() .flatten() .filter(|b| b.receiver == super::trait_env::BlanketReceiver::Value) - .map(|b| (b.module.clone(), b.ast_id, b.bounds.clone())) + .map(|b| (b.def, b.bounds.clone())) .collect(); let type_lookup = self.type_lookup(); - for (module, ast_id, bounds) in &value_blankets { + for (blanket, bounds) in &value_blankets { // Gate on all bounds. The receiver-`TypeId` check is preferred: // it recognises synthesized bounds (`ReflectStruct`, `Default`) with no // explicit `impl`, which the name-based lookup misses. A viable @@ -1627,7 +1628,7 @@ impl Elaborator<'_, H> { .tysys .blanket_assoc_constraints_hold(receiver_type_id, bounds) { - impl_refs.push(ImplBlockRef(module.clone(), *ast_id)); + impl_refs.push(ImplBlockRef(*blanket)); } } impl_refs @@ -1791,9 +1792,10 @@ impl Elaborator<'_, H> { // frame exactly when a target argument names a type the impl's module // cannot see, and the target then carries a slot this filter believes // is concrete. + let impl_module = self.impl_block_module_source(impl_ref); let is_target_slot = |name: &str| { self.tysys - .is_impl_target_param(&impl_ref.0, &header.type_params, name) + .is_impl_target_param(&impl_module, &header.type_params, name) }; let is_blanket_tp = matches!(&header.ty, Type::Named(n) if is_target_slot(&n.name)); let generic_is_parametric = matches!(&header.ty, Type::Generic(g) @@ -2109,7 +2111,7 @@ impl Elaborator<'_, H> { // this query, resolved it. let signatures = Rc::clone(&scope.tysys.signatures); let impl_sig = signatures - .impl_sig(impl_ref.1) + .impl_sig(impl_ref.0) .expect("the decl pass records every impl block's declaration facts") .instantiate_slots(&scope.tysys.type_table, &impl_slots); scope.annotate_ctx.trait_ctx.assoc_type_bindings.extend( @@ -2167,7 +2169,7 @@ impl Elaborator<'_, H> { // the spelling it wrote, which is how an erroneous block reaches a // lookup at all; a candidate built without an identity keys on nothing. let Some(trait_decl) = signatures - .impl_sig(impl_ref.1) + .impl_sig(impl_ref.0) .expect("the decl pass records every impl block's declaration facts") .trait_decl else { diff --git a/wado-compiler/src/elaborator/orchestration.rs b/wado-compiler/src/elaborator/orchestration.rs index 0a5f6b40c1f..eea03cf6b94 100644 --- a/wado-compiler/src/elaborator/orchestration.rs +++ b/wado-compiler/src/elaborator/orchestration.rs @@ -1420,8 +1420,8 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .resource_method_ids .insert(key.clone(), *method_id); } - for (ast_id, sig) in &sem.decls.impl_sigs { - signatures.impl_sigs.insert(*ast_id, sig.clone()); + for (def, sig) in &sem.decls.impl_sigs { + signatures.impl_sigs.insert(*def, sig.clone()); } for (ast_id, sig) in &sem.decls.trait_sigs { signatures.trait_sigs.insert(*ast_id, sig.clone()); diff --git a/wado-compiler/src/elaborator/sem/decls.rs b/wado-compiler/src/elaborator/sem/decls.rs index 0d4fba336e3..7b5222f444a 100644 --- a/wado-compiler/src/elaborator/sem/decls.rs +++ b/wado-compiler/src/elaborator/sem/decls.rs @@ -97,8 +97,8 @@ pub(crate) struct ModuleDecls { /// Facts of this module's `impl` blocks that belong to the block rather /// than to one method — its target and trait type arguments and its /// associated-type bindings — resolved once in the block's own frame and - /// keyed by the block's `AstId`. - pub(crate) impl_sigs: IndexMap, + /// keyed by the block's [`crate::defs::DefId`]. + pub(crate) impl_sigs: IndexMap, /// Facts of this module's `trait` declarations, resolved once in each /// trait's own frame (`Self` at slot 0) and keyed by the declaration's /// `AstId`, so a use site instantiates instead of re-resolving the trait diff --git a/wado-compiler/src/elaborator/sig.rs b/wado-compiler/src/elaborator/sig.rs index a7c3ec9b002..e7c1c422932 100644 --- a/wado-compiler/src/elaborator/sig.rs +++ b/wado-compiler/src/elaborator/sig.rs @@ -30,8 +30,8 @@ pub(crate) struct Signatures { pub(crate) resource_method_ids: IndexMap<(AstId, String), AstId>, /// Per-`impl`-block facts shared by the block's methods, keyed by the - /// block's `AstId`. - pub(crate) impl_sigs: IndexMap, + /// block's [`crate::defs::DefId`]. + pub(crate) impl_sigs: IndexMap, /// Per-`trait`-declaration facts, keyed by the declaration's `AstId`. /// `TraitEnv::decl_index` maps a canonical trait name to that `AstId`, @@ -73,9 +73,9 @@ impl Signatures { self.method_sig(*method_id) } - /// Declaration facts of the `impl` block at `ast_id`. - pub(crate) fn impl_sig(&self, ast_id: AstId) -> Option<&ImplSig> { - self.impl_sigs.get(&ast_id) + /// Declaration facts of the `impl` block `def`. + pub(crate) fn impl_sig(&self, def: crate::defs::DefId) -> Option<&ImplSig> { + self.impl_sigs.get(&def) } /// Declaration facts of the `trait` declared at `ast_id`. diff --git a/wado-compiler/src/elaborator/trait_env.rs b/wado-compiler/src/elaborator/trait_env.rs index 533a6da8d7b..36aa140e96a 100644 --- a/wado-compiler/src/elaborator/trait_env.rs +++ b/wado-compiler/src/elaborator/trait_env.rs @@ -190,9 +190,9 @@ pub(crate) fn render_decl_name(defs: &crate::defs::DefTable, def: DefId) -> Stri /// Target type → the trait impl blocks written for it. Built once from all /// loaded modules so a method call costs a lookup rather than a scan. -pub(super) type TraitImplIndex = IndexMap>; +pub(super) type TraitImplIndex = IndexMap>; -type ReceiverImplIndex = IndexMap>; +type ReceiverImplIndex = IndexMap>; fn index_by_receiver(index: &TraitImplIndex, defs: &crate::defs::DefTable) -> ReceiverImplIndex { let mut out: ReceiverImplIndex = IndexMap::default(); @@ -207,7 +207,7 @@ fn index_by_receiver(index: &TraitImplIndex, defs: &crate::defs::DefTable) -> Re /// Digested header of an `impl` block, pre-extracted at [`TraitEnv::build`] /// time so trait/method queries read its trait name, target type, methods, /// and type parameters without re-fetching the impl block from -/// `loaded_modules`. Keyed by `(ModuleSource, AstId)` in +/// `loaded_modules`. Keyed by the block's [`DefId`] in /// [`TraitEnv::impl_headers`]. #[derive(Clone, Debug)] pub(super) struct ImplHeader { @@ -288,14 +288,13 @@ impl ImplHeader { /// type, so two modules' same-named bounds stay apart — the spelling the /// blanket wrote cannot answer that. fn blanket_pack_assocs( - impl_headers: &IndexMap<(ModuleSource, AstId), ImplHeader>, + impl_headers: &IndexMap, blanket_impls: &IndexMap>, resolutions: &crate::resolve::Resolutions, -) -> IndexMap<(ModuleSource, AstId), Vec<(DefId, String)>> { - let mut out: IndexMap<(ModuleSource, AstId), Vec<(DefId, String)>> = IndexMap::default(); +) -> IndexMap> { + let mut out: IndexMap> = IndexMap::default(); for blanket in blanket_impls.values().flatten() { - let key = (blanket.module.clone(), blanket.ast_id); - let Some(header) = impl_headers.get(&key) else { + let Some(header) = impl_headers.get(&blanket.def) else { continue; }; let pairs: Vec<(DefId, String)> = header @@ -314,7 +313,7 @@ fn blanket_pack_assocs( }) .collect(); if !pairs.is_empty() { - out.insert(key, pairs); + out.insert(blanket.def, pairs); } } out @@ -340,14 +339,13 @@ pub(crate) enum BlanketParamSource { /// is the point — type arguments are consumed positionally, so a receiver /// written after another parameter sits at a slot the caller never fills. fn blanket_param_sources( - impl_headers: &IndexMap<(ModuleSource, AstId), ImplHeader>, + impl_headers: &IndexMap, blanket_impls: &IndexMap>, resolutions: &crate::resolve::Resolutions, -) -> IndexMap<(ModuleSource, AstId), Vec> { - let mut out: IndexMap<(ModuleSource, AstId), Vec> = IndexMap::default(); +) -> IndexMap> { + let mut out: IndexMap> = IndexMap::default(); for blanket in blanket_impls.values().flatten() { - let key = (blanket.module.clone(), blanket.ast_id); - let Some(header) = impl_headers.get(&key) else { + let Some(header) = impl_headers.get(&blanket.def) else { continue; }; let sources: Vec = header @@ -377,7 +375,7 @@ fn blanket_param_sources( BlanketParamSource::Projection(def, assoc.name.clone()) }) .collect(); - out.insert(key, sources); + out.insert(blanket.def, sources); } out } @@ -434,9 +432,9 @@ pub(crate) struct BlanketBound { #[derive(Clone, Debug)] pub(crate) struct BlanketImpl { pub(crate) module: ModuleSource, - /// The impl block's AST id; with `module`, the key into `impl_headers` for + /// The impl block's identity, and the key into `impl_headers` for /// consumers needing the full header (associated types, bound constraints). - pub(crate) ast_id: AstId, + pub(crate) def: DefId, pub(crate) receiver: BlanketReceiver, /// Receiver param name (`T` in `impl Trait for T`). pub(crate) param: String, @@ -481,7 +479,7 @@ fn classify_blanket_receiver( /// Digested header of a `trait` declaration: its name plus per-method /// signatures method-lookup queries read off the AST. Built in -/// [`TraitEnv::build`] and keyed by `(ModuleSource, AstId)` in +/// [`TraitEnv::build`] and keyed by the declaration's [`DefId`] in /// [`TraitEnv::trait_decl_headers`]. Reuses [`ImplMethodHeader`] for the /// per-method digest (name + type parameters). #[derive(Clone, Debug)] @@ -661,7 +659,7 @@ fn push_module( /// a substituted call to a concrete impl's own module, while a generic impl's /// instance is materialised in the receiver type's. fn index_impl_modules( - impl_headers: &IndexMap<(ModuleSource, AstId), ImplHeader>, + impl_headers: &IndexMap, resolutions: &crate::resolve::Resolutions, concrete_only: bool, ) -> ImplModuleIndex { @@ -731,21 +729,21 @@ pub struct TraitEnv { /// `effect_decl_index` to recognise handler-installable kinds in `with` /// clauses and `impl R for T` blocks. pub(super) resource_decl_index: ResourceDeclIndex, - /// Digested headers for every indexed impl block, keyed by - /// `(ModuleSource, AstId)`. Trait/method queries read this instead of - /// re-fetching the impl block AST from `loaded_modules`. See [`ImplHeader`]. - pub(super) impl_headers: IndexMap<(ModuleSource, AstId), ImplHeader>, + /// Digested headers for every indexed impl block, keyed by the block's + /// [`DefId`]. Trait/method queries read this instead of re-fetching the + /// impl block AST from `loaded_modules`. See [`ImplHeader`]. + pub(super) impl_headers: IndexMap, /// Per blanket impl, the `(declaring trait, associated type)` pairs whose /// binding is a type pack. Resolved once at build time from each bound's /// own reference site, so the trait is a declaration rather than the /// spelling the blanket wrote (WEP 2026-08-12). - pub(super) blanket_pack_assocs: IndexMap<(ModuleSource, AstId), Vec<(DefId, String)>>, + pub(super) blanket_pack_assocs: IndexMap>, /// Per blanket impl, what determines each of its parameters, in /// declaration order. Resolved once at build time from each bound's own /// reference site. - pub(super) blanket_param_sources: IndexMap<(ModuleSource, AstId), Vec>, - /// Digested headers for every `trait` declaration, keyed by - /// `(ModuleSource, AstId)`. Lets method-lookup queries read trait + pub(super) blanket_param_sources: IndexMap>, + /// Digested headers for every `trait` declaration, keyed by its + /// [`DefId`]. Lets method-lookup queries read trait /// method signatures without re-fetching the trait AST. See /// [`TraitDeclHeader`]. pub(super) trait_decl_headers: IndexMap, @@ -886,7 +884,7 @@ impl TraitEnv { IndexMap::default(); let mut resource_decl_index: ResourceDeclIndex = IndexSet::default(); let mut blanket_impls: IndexMap> = IndexMap::default(); - let mut impl_headers: IndexMap<(ModuleSource, AstId), ImplHeader> = IndexMap::default(); + let mut impl_headers: IndexMap = IndexMap::default(); let mut trait_decl_headers: IndexMap = IndexMap::default(); let mut function_type_params: IndexMap<(ModuleSource, String), Vec> = IndexMap::default(); @@ -1097,6 +1095,11 @@ impl TraitEnv { let Item::Impl(impl_block) = item else { continue; }; + // Every loaded module's `impl` blocks are identified by + // `DefTable::build`, so a miss means the two walks diverged. + let impl_def = defs + .of_ast_id(impl_block.id) + .expect("every impl block is a declaration"); let type_key = impl_target_key_at(&impl_block.ty, module_source, resolutions); let trait_ref: Option = impl_block .trait_type @@ -1113,7 +1116,7 @@ impl TraitEnv { ) }); impl_headers.insert( - (module_source.clone(), impl_block.id), + impl_def, ImplHeader { module: module_source.clone(), target: type_key.clone(), @@ -1149,7 +1152,7 @@ impl TraitEnv { all_impl_index .entry(type_key.clone()) .or_default() - .push((module_source.clone(), impl_block.id)); + .push(impl_def); if impl_block.trait_type.is_some() { if let Some((receiver, param)) = classify_blanket_receiver(&impl_block.ty, &impl_block.type_params) @@ -1182,7 +1185,7 @@ impl TraitEnv { .or_default() .push(BlanketImpl { module: module_source.clone(), - ast_id: impl_block.id, + def: impl_def, receiver, param, bounds, @@ -1192,7 +1195,7 @@ impl TraitEnv { impl_index .entry(type_key.clone()) .or_default() - .push((module_source.clone(), impl_block.id)); + .push(impl_def); // Static methods on trait impl blocks (no `self` // parameter) join the same canonical bucket as // inherent statics. `f64::from_bits` and friends in @@ -1356,7 +1359,7 @@ impl TraitEnv { /// Keys of every impl block on `type_key`, in global build order — /// inherent and trait alike. - pub(super) fn all_impl_keys(&self, type_key: &ImplTargetKey) -> Vec<(ModuleSource, AstId)> { + pub(super) fn all_impl_keys(&self, type_key: &ImplTargetKey) -> Vec { self.all_impl_index .get(type_key) .cloned() @@ -1366,10 +1369,7 @@ impl TraitEnv { /// Keys of the **inherent** impls on `type_name`, in global build order — /// the `trait_name.is_none()` subset of [`Self::all_impl_index`]. Used by /// instance-method lookup, which must not treat trait impls as inherent. - pub(super) fn inherent_impl_keys( - &self, - type_key: &ImplTargetKey, - ) -> Vec<(ModuleSource, AstId)> { + pub(super) fn inherent_impl_keys(&self, type_key: &ImplTargetKey) -> Vec { self.all_impl_index .get(type_key) .map(|keys| { @@ -1468,11 +1468,11 @@ impl TraitEnv { pub(crate) fn entries_by_receiver<'a>( &'a self, receiver: &'a name::Receiver, - ) -> impl Iterator + 'a { + ) -> impl Iterator + 'a { self.by_receiver .get(receiver) .into_iter() - .flat_map(|entries| entries.iter()) + .flat_map(|entries| entries.iter().copied()) } /// Collected form of [`Self::entries_by_receiver`], for callers that need @@ -1480,8 +1480,8 @@ impl TraitEnv { pub(crate) fn entries_by_receiver_vec( &self, receiver: &name::Receiver, - ) -> Vec<(ModuleSource, AstId)> { - self.entries_by_receiver(receiver).cloned().collect() + ) -> Vec { + self.entries_by_receiver(receiver).collect() } /// Receiver-matched form of [`Self::has_any_methodful_impl`]. @@ -1514,8 +1514,10 @@ impl TraitEnv { trait_: crate::defs::DefId, module_source: &ModuleSource, ) -> bool { - self.entries_by_receiver(receiver) - .any(|entry| entry.0 == *module_source && self.methodful_header_matches(entry, trait_)) + self.entries_by_receiver(receiver).any(|entry| { + self.defs.module(entry) == module_source + && self.methodful_header_matches(entry, trait_) + }) } /// Whether an inherent `impl` on `receiver` declares `method_name`. @@ -1535,13 +1537,9 @@ impl TraitEnv { }) } - fn methodful_header_matches( - &self, - entry: &(ModuleSource, AstId), - trait_: crate::defs::DefId, - ) -> bool { + fn methodful_header_matches(&self, entry: DefId, trait_: crate::defs::DefId) -> bool { self.impl_headers - .get(entry) + .get(&entry) .is_some_and(|header| !header.methods.is_empty() && header.trait_ref == Some(trait_)) } @@ -1622,7 +1620,7 @@ impl TraitEnv { /// order — see [`blanket_param_sources`]. pub(crate) fn blanket_param_sources(&self, blanket: &BlanketImpl) -> Vec { self.blanket_param_sources - .get(&(blanket.module.clone(), blanket.ast_id)) + .get(&blanket.def) .cloned() .unwrap_or_default() } @@ -1635,7 +1633,7 @@ impl TraitEnv { /// `Members`. pub(crate) fn pack_assocs_of_blanket(&self, blanket: &BlanketImpl) -> Vec<(DefId, String)> { self.blanket_pack_assocs - .get(&(blanket.module.clone(), blanket.ast_id)) + .get(&blanket.def) .cloned() .unwrap_or_default() } @@ -2205,7 +2203,7 @@ impl VariadicImpl<'_> { /// their own. The same walk refuses a target the compiler cannot implement. fn check_variadic_impl_overlap( defs: &crate::defs::DefTable, - impl_headers: &IndexMap<(ModuleSource, AstId), ImplHeader>, + impl_headers: &IndexMap, ) -> Vec<(ModuleSource, TypeError)> { let mut violations = Vec::new(); let mut groups: IndexMap<&ImplTargetKey, Vec>> = IndexMap::default(); @@ -2311,7 +2309,7 @@ fn target_mentions_impl_param(ty: &ast::Type, params: &IndexSet<&str>) -> bool { /// written head — two modules' `Box_` are two types, and a spelling cannot say so. fn check_inherent_impl_collisions( defs: &crate::defs::DefTable, - impl_headers: &IndexMap<(ModuleSource, AstId), ImplHeader>, + impl_headers: &IndexMap, resolutions: &crate::resolve::Resolutions, ) -> Vec<(ModuleSource, TypeError)> { let mut generic_methods_by_target: IndexMap<&ImplTargetKey, IndexSet<&str>> = @@ -2388,7 +2386,7 @@ fn check_inherent_impl_collisions( /// paired with the offending impl's [`ModuleSource`] for file attribution. fn check_all_orphan_rules( defs: &crate::defs::DefTable, - impl_headers: &IndexMap<(ModuleSource, AstId), ImplHeader>, + impl_headers: &IndexMap, decl_index: &TraitDeclIndex, type_decl_index: &IndexSet, resolve: ResolveWritten<'_>, diff --git a/wado-compiler/src/elaborator/trait_query.rs b/wado-compiler/src/elaborator/trait_query.rs index d7fc7ac9ef6..da26e146679 100644 --- a/wado-compiler/src/elaborator/trait_query.rs +++ b/wado-compiler/src/elaborator/trait_query.rs @@ -2444,7 +2444,7 @@ impl Elaborator<'_, H> { for blanket in trait_env.blanket_impls.get(&trait_).into_iter().flatten() { let Some(header) = trait_env .impl_headers - .get(&(blanket.module.clone(), blanket.ast_id)) + .get(&blanket.def) else { continue; }; From 5d48d180ab3a8829ab60e22ba8ab9f6ff3a722e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 14:54:03 +0000 Subject: [PATCH 03/25] refactor(elaborator): key every method and trait signature by `DefId` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Signatures` and `ModuleDecls` filed a method, a trait, and an `interface` / `resource` operation under the declaring node. Each is a declaration, so each is keyed by its `DefId` now, and so are the headers and indices that reach them: `ImplMethodHeader`, `MethodSig`, `StaticMethodEntry`, `StaticMethodRef`, `ResourceStaticMethodIndex` and `MethodInfo`. Three lookups that read a declaring node off an identity only to key a map by it lose the round trip. The use→def edge map still names nodes on both sides — navigation recovers a def's module from its id space — so `record_reference_to_decl` reads that node once, at the sink. `trait_env` digested a method header twice, once for a trait's methods and once for an impl's; one producer now answers both. Also fixes two `copied`/`cloned` clippy lints the previous commit introduced. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator.rs | 33 +++++++- wado-compiler/src/elaborator/call.rs | 16 ++-- wado-compiler/src/elaborator/callee.rs | 8 +- wado-compiler/src/elaborator/item.rs | 41 +++++++-- wado-compiler/src/elaborator/method_call.rs | 35 ++++---- wado-compiler/src/elaborator/method_lookup.rs | 25 +++--- wado-compiler/src/elaborator/reify.rs | 1 - wado-compiler/src/elaborator/sem/decls.rs | 17 ++-- wado-compiler/src/elaborator/sig.rs | 48 ++++++----- wado-compiler/src/elaborator/trait_env.rs | 84 +++++++++---------- wado-compiler/src/elaborator/trait_query.rs | 20 ++--- wado-compiler/src/elaborator/types.rs | 4 +- 12 files changed, 183 insertions(+), 149 deletions(-) diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 9f8c4df335c..9699f66111e 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -444,6 +444,20 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { self.insert_reference(use_id, def_id); } + /// Record a use→def edge naming the declaration `def`. + /// + /// The edge map is keyed by node on both sides — navigation recovers a + /// def's module from its id space — so the declaring node is read off the + /// identity here rather than carried beside it. + pub(super) fn record_reference_to_decl( + &mut self, + use_id: crate::ast::AstId, + def: crate::defs::DefId, + ) { + let node = self.tysys.resolutions.defs().ast_id(def); + self.insert_reference(use_id, node); + } + /// Record that an identifier resolved to a declared symbol reachable from /// the current module under `name` (local item, imported item, imported /// namespace member, etc.). Looks up the defining [`AstId`](crate::ast::AstId) through @@ -530,7 +544,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { &self, receiver: &trait_env::ImplTargetKey, method_name: &str, - ) -> Option { + ) -> Option { self.tysys .trait_env .static_method_index @@ -1735,11 +1749,18 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .then(|| self.tysys.resolutions.defs().of_ast_id(decl_id)) .flatten(); let ops = self.resolve_effect_ops(&type_params, &methods, resource_self); + let defs = std::sync::Arc::clone(self.tysys.resolutions.defs()); + let owner = defs + .of_ast_id(decl_id) + .expect("every interface / resource declaration is a declaration"); for method in &methods { + let op = defs + .of_ast_id(method.id) + .expect("every declared operation is a declaration"); self.sem .decls .resource_method_ids - .insert((decl_id, method.name.clone()), method.id); + .insert((owner, method.name.clone()), op); } self.sem.decls.effect_ops.insert(decl_id, ops); } @@ -2035,10 +2056,16 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { for method in &impl_block.methods { // Records-only: reify emits the method `TirFunction` // from the recorded signature facts + the AST. + let method_def = scope + .tysys + .resolutions + .defs() + .of_ast_id(method.id) + .expect("every impl method is a declaration"); let recorded_sig = scope .tysys .signatures - .method_sig(method.id) + .method_sig(method_def) .cloned() .expect("the decl pass records every impl-declared method's canonical signature"); scope.resolve_method( diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index 9f4ca8eac73..13aeca56057 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -681,7 +681,7 @@ impl Elaborator<'_, H> { } else { None }; - let method_ast_id = self + let method_def = self .locate_static_method_impl(prefix, suffix, arg_hint.as_deref()) .and_then(|r| r.method_id) .or_else(|| { @@ -690,8 +690,8 @@ impl Elaborator<'_, H> { suffix, ) }); - if let Some(method_ast_id) = method_ast_id { - self.record_reference_to_def(suffix_seg.id, method_ast_id); + if let Some(method_def) = method_def { + self.record_reference_to_decl(suffix_seg.id, method_def); } } // Resolve method-level type args (e.g., i32::deserialize::) @@ -1158,7 +1158,7 @@ impl Elaborator<'_, H> { // `ns::Type::method`, so the method is its third segment — // the position `record_namespaced_case` also reads. if let Some(method_seg) = ident.segments.get(2) - && let Some(method_ast_id) = method_ref.method_id.or_else(|| { + && let Some(method_def) = method_ref.method_id.or_else(|| { // The receiver is `ns::Type`, whose middle segment // the resolve walk answered for under the `ns$Type` // alias. No spelling is re-resolved from the call @@ -1171,7 +1171,7 @@ impl Elaborator<'_, H> { self.static_method_decl_id(&receiver, method_name) }) { - self.record_reference_to_def(method_seg.id, method_ast_id); + self.record_reference_to_decl(method_seg.id, method_def); } // Qualify by the module the impl was located in: @@ -1706,11 +1706,7 @@ impl Elaborator<'_, H> { effect: crate::defs::DefId, operation: &str, ) -> Option<(Vec, Option)> { - let decl_id = self.tysys.resolutions.defs().ast_id(effect); - let sig = self - .tysys - .signatures - .resource_method_sig(decl_id, operation)?; + let sig = self.tysys.signatures.resource_method_sig(effect, operation)?; Some((sig.decl.param_types.clone(), sig.decl.return_type)) } diff --git a/wado-compiler/src/elaborator/callee.rs b/wado-compiler/src/elaborator/callee.rs index b69f6e221e5..dc92d1f565c 100644 --- a/wado-compiler/src/elaborator/callee.rs +++ b/wado-compiler/src/elaborator/callee.rs @@ -63,9 +63,9 @@ pub(super) struct StaticMethodRef { pub type_name: String, pub method_name: String, pub trait_name: Option, - /// The node declaring the method this selection picked. `None` when no - /// declaration backs it — the auto-derived `Default::default`. - pub method_id: Option, + /// The method this selection picked. `None` when no declaration backs + /// it — the auto-derived `Default::default`. + pub method_id: Option, } impl StaticMethodRef { @@ -74,7 +74,7 @@ impl StaticMethodRef { type_name: impl Into, method_name: impl Into, trait_name: Option, - method_id: Option, + method_id: Option, ) -> Self { Self { module, diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index 1872fcd211c..67541964a9e 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -1264,10 +1264,16 @@ impl Elaborator<'_, H> { .first() .map(|p| p.self_kind) .unwrap_or(ast::SelfKind::None); + let method_def = frame_scope + .tysys + .resolutions + .defs() + .of_ast_id(method.id) + .expect("every impl method is a declaration"); frame_scope.sem.decls.method_sigs.insert( - method.id, + method_def, MethodSig { - ast_id: method.id, + def: method_def, decl: DeclSig { type_params, param_types, @@ -1618,11 +1624,17 @@ impl Elaborator<'_, H> { let mut type_params = decl_slots.clone(); type_params.extend(method_slots); + let method_def = method_scope + .tysys + .resolutions + .defs() + .of_ast_id(method.id) + .expect("every trait method is a declaration"); methods.insert( method.name.clone(), super::sig::TraitMethod { sig: MethodSig { - ast_id: method.id, + def: method_def, decl: DeclSig { type_params, param_types, @@ -1660,11 +1672,17 @@ impl Elaborator<'_, H> { } let module = scope.current_module_source.clone(); + let trait_def = scope + .tysys + .resolutions + .defs() + .of_ast_id(trait_decl.id) + .expect("every trait declaration is a declaration"); scope .sem .decls .trait_sigs - .insert(trait_decl.id, super::sig::TraitSig { module, methods }); + .insert(trait_def, super::sig::TraitSig { module, methods }); } /// Lower an effect or resource declaration's method list to [`TirEffectOp`]s, @@ -1830,10 +1848,16 @@ impl Elaborator<'_, H> { } else { SelfKind::None }; + let method_def = scope + .tysys + .resolutions + .defs() + .of_ast_id(method.id) + .expect("every declared operation is a declaration"); scope.sem.decls.method_sigs.insert( - method.id, + method_def, MethodSig { - ast_id: method.id, + def: method_def, decl: DeclSig { type_params: decl_slots.clone(), param_types: params.iter().map(|p| p.type_id).collect(), @@ -2625,12 +2649,11 @@ impl Elaborator<'_, H> { (None, None) => (None, false), }; let async_op = decl_ref - .map(|key| scope.tysys.resolutions.defs().ast_id(key)) - .and_then(|decl_id| { + .and_then(|decl| { scope .tysys .signatures - .resource_method_sig(decl_id, &func.name) + .resource_method_sig(decl, &func.name) .filter(|op| op.is_async) .map(|op| op.cm_name.is_some()) }); diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index b3c30a9810c..d2e6470b01d 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -466,7 +466,7 @@ impl Elaborator<'_, H> { // reify would try to lower a call to a function that does not exist. let method_found = method_info.is_some(); let MethodInfo { - method_ast_id: dispatched_method_ast_id, + method_def: dispatched_method_def, mut return_type, self_kind, param_types, @@ -493,7 +493,7 @@ impl Elaborator<'_, H> { }); // Default to Unknown type for error recovery MethodInfo { - method_ast_id: None, + method_def: None, return_type: TypeTable::UNKNOWN, self_kind: ast::SelfKind::Ref, param_types: vec![], @@ -1078,8 +1078,8 @@ impl Elaborator<'_, H> { // The target is the declaration dispatch selected, carried on its // signature. A name scan cannot stand in: two impls on one type can // declare the same method, and only dispatch knows which answered. - if let (Some(method_id), Some(def_id)) = (method_id, dispatched_method_ast_id) { - self.record_reference_to_def(method_id, def_id); + if let (Some(method_id), Some(def)) = (method_id, dispatched_method_def) { + self.record_reference_to_decl(method_id, def); } let func = FunctionRef { @@ -2169,12 +2169,12 @@ impl Elaborator<'_, H> { // The selection covers trait impls only; an inherent static has none // and reaches the index instead. - if let Some(method_ast_id) = selected.as_ref().and_then(|r| r.method_id).or_else(|| { + if let Some(method_def) = selected.as_ref().and_then(|r| r.method_id).or_else(|| { let receiver = self.impl_target_of(target_type_id, &crate::name::DeclName::new(&struct_name)); self.static_method_decl_id(&receiver, &static_call.method) }) { - self.record_reference_to_def(static_call.method_id, method_ast_id); + self.record_reference_to_decl(static_call.method_id, method_def); } let func_ref = FunctionRef { @@ -2440,7 +2440,7 @@ impl Elaborator<'_, H> { .is_some_and(|entries| { entries.iter().any(|e| { e.name == method_name - && header.methods.iter().any(|m| m.ast_id == e.method_id) + && header.methods.iter().any(|m| m.def == e.method_id) }) }) }) @@ -2798,7 +2798,7 @@ impl Elaborator<'_, H> { let sig = self .tysys .signatures - .method_sig(method.ast_id) + .method_sig(method.def) .expect("the decl pass records every impl-declared method's signature"); if *self.tysys.resolutions.defs().module(*key) == self.current_module_source { current.push(sig); @@ -3052,7 +3052,7 @@ impl Elaborator<'_, H> { && self .tysys .signatures - .method_sig(m.ast_id) + .method_sig(m.def) .is_some_and(|sig| sig.self_kind == ast::SelfKind::None) }) }) @@ -3287,13 +3287,13 @@ impl Elaborator<'_, H> { !is_from_or_try_from(&base) }; - // Returns the trait the impl names and the node declaring the method - // there — the identity of what this selection picked, so a caller - // recording a use→def edge names the impl the argument chose rather - // than the receiver's first same-named method. + // Returns the trait the impl names and the method it declares there — + // the identity of what this selection picked, so a caller recording a + // use→def edge names the impl the argument chose rather than the + // receiver's first same-named method. let check_impl = |header: &super::trait_env::ImplHeader, impl_module: &ModuleSource| - -> Option<(crate::name::FqTraitName, AstId)> { + -> Option<(crate::name::FqTraitName, crate::defs::DefId)> { let trait_type = header.trait_type.as_ref()?; if super::trait_env::get_type_name_static(&header.ty) != struct_name || !matches_arg_type(trait_type, impl_module, &header.type_params) @@ -3304,10 +3304,10 @@ impl Elaborator<'_, H> { let sig = self .tysys .signatures - .method_sig(method.ast_id) + .method_sig(method.def) .expect("the decl pass records every impl-declared method's signature"); if sig.self_kind == ast::SelfKind::None { - return Some((resolve_trait_name(header)?, method.ast_id)); + return Some((resolve_trait_name(header)?, method.def)); } } // Fall back to the trait declaration's default methods: when @@ -3323,7 +3323,7 @@ impl Elaborator<'_, H> { && method.default_body.is_some() && method.sig.self_kind == ast::SelfKind::None { - return Some((resolve_trait_name(header)?, method.sig.ast_id)); + return Some((resolve_trait_name(header)?, method.sig.def)); } None }; @@ -3670,7 +3670,6 @@ impl Elaborator<'_, H> { // resource is reached through it rather than through its spelling. let cm_owner = method_ref .method_id - .and_then(|id| self.tysys.resolutions.defs().of_ast_id(id)) .and_then(|method| self.tysys.resolutions.defs().parent(method)) .or_else(|| self.decl_key_or_local(&actual_struct_name)); let cm_name = self.lookup_resource_static_cm(cm_owner, method_name); diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index c85544813ab..b8908140f95 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -705,7 +705,7 @@ impl Elaborator<'_, H> { let elems = type_args; if method_name == "len" { return Some(MethodInfo { - method_ast_id: None, + method_def: None, return_type: TypeTable::I32, self_kind: ast::SelfKind::Ref, param_types: vec![], @@ -747,7 +747,7 @@ impl Elaborator<'_, H> { } let return_type = self.tysys.type_table.borrow_mut().make_tuple(transposed); return Some(MethodInfo { - method_ast_id: None, + method_def: None, return_type, self_kind: ast::SelfKind::Ref, param_types: vec![], @@ -994,7 +994,7 @@ impl Elaborator<'_, H> { let signatures = Rc::clone(&self.tysys.signatures); let header = impl_header(&trait_env, impl_ref); let method_header = header.methods.iter().find(|m| m.name == method_name)?; - let sig = signatures.method_sig(method_header.ast_id)?; + let sig = signatures.method_sig(method_header.def)?; let impl_sig = signatures .impl_sig(impl_ref.0) .expect("the decl pass records every impl block's declaration facts"); @@ -1004,7 +1004,7 @@ impl Elaborator<'_, H> { let first_value = sig.first_value_param().min(instantiated.param_types.len()); Some(MethodInfo { - method_ast_id: Some(sig.ast_id), + method_def: Some(sig.def), return_type: instantiated.return_type, self_kind: sig.self_kind, param_types: instantiated.param_types[first_value..].to_vec(), @@ -1033,11 +1033,10 @@ impl Elaborator<'_, H> { method_name: &str, receiver_type_args: Option<&[TypeId]>, ) -> Option { - let decl_id = self.tysys.all_resource_types.get(&def)?.defined_at; let sig = self .tysys .signatures - .resource_method_sig(decl_id, method_name)? + .resource_method_sig(def, method_name)? .clone(); if sig.self_kind == ast::SelfKind::None { return None; @@ -1050,7 +1049,7 @@ impl Elaborator<'_, H> { let method_type_param_ids = sig.own_type_param_ids(); Some(MethodInfo { - method_ast_id: Some(sig.ast_id), + method_def: Some(sig.def), return_type: instantiated.return_type, self_kind: sig.self_kind, param_types: instantiated.param_types[first_value..].to_vec(), @@ -2151,7 +2150,7 @@ impl Elaborator<'_, H> { let sig = scope .tysys .signatures - .method_sig(m.ast_id) + .method_sig(m.def) .expect("the decl pass records every impl-declared method's signature") .clone(); (sig, m.type_params.clone()) @@ -2283,7 +2282,7 @@ impl Elaborator<'_, H> { trait_decl, trait_args: trait_args.clone(), method_info: MethodInfo { - method_ast_id: Some(method_sig.ast_id), + method_def: Some(method_sig.def), return_type, self_kind, param_types, @@ -2344,7 +2343,7 @@ impl Elaborator<'_, H> { trait_decl, trait_args: trait_args.clone(), method_info: MethodInfo { - method_ast_id: Some(default_method.sig.ast_id), + method_def: Some(default_method.sig.def), return_type: instantiated.return_type, self_kind, param_types: instantiated.param_types[first_value_param..].to_vec(), @@ -3064,7 +3063,7 @@ impl Elaborator<'_, H> { // type arguments is what the by-name re-resolution below used // to approximate. let method_header = header.methods.iter().find(|m| m.name == method_name)?; - let method_sig = s.tysys.signatures.method_sig(method_header.ast_id)?; + let method_sig = s.tysys.signatures.method_sig(method_header.def)?; let self_kind = method_sig.self_kind; let rhs_index = usize::from(self_kind != ast::SelfKind::None); let rhs_type = method_sig @@ -3227,7 +3226,7 @@ impl Elaborator<'_, H> { let self_kind = s .tysys .signatures - .method_sig(method_header.ast_id)? + .method_sig(method_header.def)? .self_kind; let impl_source = s.impl_block_module_source(impl_ref); @@ -3357,7 +3356,7 @@ impl Elaborator<'_, H> { } let MethodInfo { - method_ast_id: _, + method_def: _, return_type, self_kind, param_types, diff --git a/wado-compiler/src/elaborator/reify.rs b/wado-compiler/src/elaborator/reify.rs index 380ec985bbe..b48dde82567 100644 --- a/wado-compiler/src/elaborator/reify.rs +++ b/wado-compiler/src/elaborator/reify.rs @@ -1457,7 +1457,6 @@ impl<'a, H: CompilerHost> Reify<'a, H> { }; let Some(trait_sig) = super::trait_query::trait_sig_of_with( trait_decl, - &self.tysys.resolutions, &self.tysys.trait_env, &self.tysys.signatures, ) else { diff --git a/wado-compiler/src/elaborator/sem/decls.rs b/wado-compiler/src/elaborator/sem/decls.rs index 7b5222f444a..70309e08293 100644 --- a/wado-compiler/src/elaborator/sem/decls.rs +++ b/wado-compiler/src/elaborator/sem/decls.rs @@ -89,21 +89,20 @@ pub(crate) struct ModuleDecls { /// to the impl target. An `interface` / `resource` operation is resolved /// in the declaration's frame. Either way a use site instantiates /// instead of re-resolving the method AST. - pub(crate) method_sigs: IndexMap, - /// A declaration's `AstId` paired with an operation name → the - /// `AstId`, so a caller holding only a name reaches its `method_sigs` - /// entry. - pub(crate) resource_method_ids: IndexMap<(crate::ast::AstId, String), crate::ast::AstId>, + pub(crate) method_sigs: IndexMap, + /// A declaration paired with an operation name → the operation, so a + /// caller holding only a name reaches its `method_sigs` entry. + pub(crate) resource_method_ids: + IndexMap<(crate::defs::DefId, String), crate::defs::DefId>, /// Facts of this module's `impl` blocks that belong to the block rather /// than to one method — its target and trait type arguments and its /// associated-type bindings — resolved once in the block's own frame and /// keyed by the block's [`crate::defs::DefId`]. pub(crate) impl_sigs: IndexMap, /// Facts of this module's `trait` declarations, resolved once in each - /// trait's own frame (`Self` at slot 0) and keyed by the declaration's - /// `AstId`, so a use site instantiates instead of re-resolving the trait - /// method AST. - pub(crate) trait_sigs: IndexMap, + /// trait's own frame (`Self` at slot 0) and keyed by the declaration, so + /// a use site instantiates instead of re-resolving the trait method AST. + pub(crate) trait_sigs: IndexMap, /// Resolved operation signatures of this module's `interface` and /// `resource` declarations, keyed by the declaration's `AstId`. /// diff --git a/wado-compiler/src/elaborator/sig.rs b/wado-compiler/src/elaborator/sig.rs index e7c1c422932..924e644a010 100644 --- a/wado-compiler/src/elaborator/sig.rs +++ b/wado-compiler/src/elaborator/sig.rs @@ -3,7 +3,6 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::ast::AstId; use crate::hashmap::IndexMap; use crate::module_source::ModuleSource; use crate::tir::{TypeId, TypeTable}; @@ -20,24 +19,23 @@ pub(crate) struct Signatures { /// Canonical free-function signatures, declaring module → name. pub(crate) function_sigs: IndexMap>>, - /// Canonical method signatures, keyed by the method's globally-unique - /// `AstId` — `impl`-block methods and `interface` / `resource` - /// operations alike. Dispatch goes index → signature, never AST. - pub(crate) method_sigs: IndexMap, + /// Canonical method signatures, keyed by the method's [`crate::defs::DefId`] + /// — `impl`-block methods and `interface` / `resource` operations alike. + /// Dispatch goes index → signature, never AST. + pub(crate) method_sigs: IndexMap, /// The name-keyed index over [`Self::method_sigs`] for `interface` / /// `resource` operations, which callers reach by name, not by node. - pub(crate) resource_method_ids: IndexMap<(AstId, String), AstId>, + pub(crate) resource_method_ids: IndexMap<(crate::defs::DefId, String), crate::defs::DefId>, /// Per-`impl`-block facts shared by the block's methods, keyed by the /// block's [`crate::defs::DefId`]. pub(crate) impl_sigs: IndexMap, - /// Per-`trait`-declaration facts, keyed by the declaration's `AstId`. - /// `TraitEnv::decl_index` maps a canonical trait name to that `AstId`, - /// so a query reaches a trait's methods by name without loading the - /// declaring module's AST. - pub(crate) trait_sigs: IndexMap, + /// Per-`trait`-declaration facts, keyed by the declaration's + /// [`crate::defs::DefId`], so a query reaches a trait's methods without + /// loading the declaring module's AST. + pub(crate) trait_sigs: IndexMap, /// Global-variable declarations, declaring module → name → /// `(declared type, is_mut)`. @@ -61,16 +59,20 @@ impl Signatures { self.function_sigs.get(module)?.get(name) } - /// Canonical signature of the method declared at `ast_id`. - pub(crate) fn method_sig(&self, ast_id: AstId) -> Option<&MethodSig> { - self.method_sigs.get(&ast_id) + /// Canonical signature of the method `def` declares. + pub(crate) fn method_sig(&self, def: crate::defs::DefId) -> Option<&MethodSig> { + self.method_sigs.get(&def) } /// Canonical signature of the operation `name` on the `interface` / - /// `resource` declared at `decl_id`. - pub(crate) fn resource_method_sig(&self, decl_id: AstId, name: &str) -> Option<&MethodSig> { - let method_id = self.resource_method_ids.get(&(decl_id, name.to_string()))?; - self.method_sig(*method_id) + /// `resource` declaration `decl`. + pub(crate) fn resource_method_sig( + &self, + decl: crate::defs::DefId, + name: &str, + ) -> Option<&MethodSig> { + let method = self.resource_method_ids.get(&(decl, name.to_string()))?; + self.method_sig(*method) } /// Declaration facts of the `impl` block `def`. @@ -78,9 +80,9 @@ impl Signatures { self.impl_sigs.get(&def) } - /// Declaration facts of the `trait` declared at `ast_id`. - pub(crate) fn trait_sig(&self, ast_id: AstId) -> Option<&TraitSig> { - self.trait_sigs.get(&ast_id) + /// Declaration facts of the `trait` `def` declares. + pub(crate) fn trait_sig(&self, def: crate::defs::DefId) -> Option<&TraitSig> { + self.trait_sigs.get(&def) } /// Declared type and mutability of the global `name` in `module`. @@ -125,10 +127,10 @@ pub(crate) struct DeclSig { /// declaration's — because dispatch asks them the same questions. #[derive(Clone, Debug)] pub(crate) struct MethodSig { - /// The declaring node — the key this signature is filed under, carried + /// The declaration — the key this signature is filed under, carried /// inside so a consumer holding the signature holds the identity too. /// A use→def edge is recorded from here, never from a name re-scan. - pub(crate) ast_id: AstId, + pub(crate) def: crate::defs::DefId, pub(crate) decl: DeclSig, pub(crate) self_kind: crate::ast::SelfKind, /// The non-receiver parameters, in order. `decl.param_types` includes diff --git a/wado-compiler/src/elaborator/trait_env.rs b/wado-compiler/src/elaborator/trait_env.rs index 36aa140e96a..a9673fac237 100644 --- a/wado-compiler/src/elaborator/trait_env.rs +++ b/wado-compiler/src/elaborator/trait_env.rs @@ -6,7 +6,7 @@ use std::sync::Arc; -use crate::ast::{self, AstId, Item, Module, Type}; +use crate::ast::{self, Item, Module, Type}; use crate::defs::DefId; use crate::hashmap::{IndexMap, IndexSet}; use crate::kiln::InvocationIndex; @@ -199,7 +199,7 @@ fn index_by_receiver(index: &TraitImplIndex, defs: &crate::defs::DefTable) -> Re for (key, entries) in index { out.entry(key.receiver(defs)) .or_default() - .extend(entries.iter().cloned()); + .extend(entries.iter().copied()); } out } @@ -386,9 +386,9 @@ fn blanket_param_sources( #[derive(Clone, Debug)] pub(super) struct ImplMethodHeader { pub(super) name: String, - /// The method's own `AstId` — the key into the canonical-signature + /// The method's own identity — the key into the canonical-signature /// digest, so a header lookup reaches the signature without the AST. - pub(super) ast_id: AstId, + pub(super) def: DefId, pub(super) type_params: Vec, /// Where the method is written, so a whole-program check reporting on it /// needs no second walk of the module AST to find the span. @@ -401,6 +401,30 @@ pub(super) struct ImplMethodHeader { pub(super) param_count: usize, } +/// Digest each method a `trait` or `impl` block declares, for its header. +/// +/// One producer, so a trait's methods and an impl's are digested the same way +/// and cannot disagree about what a header says. +fn method_headers(defs: &crate::defs::DefTable, methods: &[ast::Function]) -> Vec { + methods + .iter() + .map(|m| ImplMethodHeader { + name: m.name.clone(), + def: defs + .of_ast_id(m.id) + .expect("every declared method is a declaration"), + type_params: m.type_params.clone(), + span: m.span, + name_span: m.name_span, + param_count: m + .params + .iter() + .filter(|p| p.self_kind == ast::SelfKind::None) + .count(), + }) + .collect() +} + /// The receiver shape of a blanket impl. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum BlanketReceiver { @@ -547,7 +571,7 @@ pub(super) struct StaticMethodEntry { /// The method itself: the key into the signature digest, which carries /// everything a lookup needs — resolved in the impl's own frame and its /// own module's perspective. - pub(super) method_id: AstId, + pub(super) method_id: DefId, } /// Pre-built index of static methods, for O(1) lookup instead of a scan over @@ -561,10 +585,10 @@ pub(super) type StaticMethodIndex = IndexMap>; + IndexMap>; /// `(type_name, trait_name)` → modules holding that `impl` block. Keyed by bare /// names rather than [`DefId`]: the multi-value `Vec` plus the caller's @@ -948,7 +972,7 @@ impl TraitEnv { .push(( method.name.clone(), module_source.clone(), - resource.id, + resource_key, method_idx, )); } @@ -1070,22 +1094,7 @@ impl TraitEnv { name: trait_decl.name.clone(), type_params: trait_decl.type_params.clone(), supertraits: trait_decl.supertraits.clone(), - methods: trait_decl - .methods - .iter() - .map(|m| ImplMethodHeader { - name: m.name.clone(), - ast_id: m.id, - type_params: m.type_params.clone(), - span: m.span, - name_span: m.name_span, - param_count: m - .params - .iter() - .filter(|p| p.self_kind == ast::SelfKind::None) - .count(), - }) - .collect(), + methods: method_headers(defs, &trait_decl.methods), assoc_types: trait_decl.associated_types.clone(), span: trait_decl.span, }, @@ -1126,22 +1135,7 @@ impl TraitEnv { trait_type: impl_block.trait_type.clone(), ty: impl_block.ty.clone(), type_params: impl_block.type_params.clone(), - methods: impl_block - .methods - .iter() - .map(|m| ImplMethodHeader { - name: m.name.clone(), - ast_id: m.id, - type_params: m.type_params.clone(), - span: m.span, - name_span: m.name_span, - param_count: m - .params - .iter() - .filter(|p| p.self_kind == ast::SelfKind::None) - .count(), - }) - .collect(), + methods: method_headers(defs, &impl_block.methods), associated_types: impl_block.associated_types.clone(), is_synthesize_request: impl_block.is_synthesize_request, span: impl_block.span, @@ -1212,7 +1206,9 @@ impl TraitEnv { .or_default() .push(StaticMethodEntry { name: method.name.clone(), - method_id: method.id, + method_id: defs + .of_ast_id(method.id) + .expect("every impl method is a declaration"), }); } } @@ -1231,7 +1227,9 @@ impl TraitEnv { .or_default() .push(StaticMethodEntry { name: method.name.clone(), - method_id: method.id, + method_id: defs + .of_ast_id(method.id) + .expect("every impl method is a declaration"), }); } } @@ -1379,7 +1377,7 @@ impl TraitEnv { .get(*key) .is_some_and(|h| h.trait_name.is_none()) }) - .cloned() + .copied() .collect() }) .unwrap_or_default() diff --git a/wado-compiler/src/elaborator/trait_query.rs b/wado-compiler/src/elaborator/trait_query.rs index da26e146679..eb2f68ebf17 100644 --- a/wado-compiler/src/elaborator/trait_query.rs +++ b/wado-compiler/src/elaborator/trait_query.rs @@ -204,14 +204,13 @@ fn mentions_self(ty: &ast::Type) -> bool { /// AST, and answers `None` for a declaration that is no trait. pub(crate) fn trait_sig_of_with<'a>( decl: crate::defs::DefId, - resolutions: &crate::resolve::Resolutions, trait_env: &super::trait_env::TraitEnv, signatures: &'a super::sig::Signatures, ) -> Option<&'a super::sig::TraitSig> { if !trait_env.decl_index.contains(&decl) { return None; } - signatures.trait_sig(resolutions.defs().ast_id(decl)) + signatures.trait_sig(decl) } /// The structural-conformance rule's answer for one type: whether every member @@ -371,12 +370,7 @@ impl Elaborator<'_, H> { /// [`Self::trait_sig_of`] for a caller still holding a trait's spelling. pub(super) fn trait_sig_by_name(&self, trait_name: &str) -> Option<&TraitSig> { let decl = self.decl_key_or_local(trait_name)?; - trait_sig_of_with( - decl, - &self.tysys.resolutions, - &self.tysys.trait_env, - &self.tysys.signatures, - ) + trait_sig_of_with(decl, &self.tysys.trait_env, &self.tysys.signatures) } /// The declaration header of the trait `trait_name` names in this frame. @@ -1067,7 +1061,7 @@ impl TypeSystem { // A receiverless method has no receiver to deref, so `&T` inherits it // by forwarding — which works only where `Self` is absent from the // signature: `kind() -> String` forwards, `-> Option` cannot. - trait_sig_of_with(trait_, &self.resolutions, &self.trait_env, &self.signatures).is_some_and( + trait_sig_of_with(trait_, &self.trait_env, &self.signatures).is_some_and( |sig| { sig.methods.values().any(|m| { m.sig.self_kind == crate::ast::SelfKind::None @@ -1751,9 +1745,7 @@ impl Elaborator<'_, H> { if !self.tysys.trait_env.decl_index.contains(key) { return None; } - self.tysys - .signatures - .trait_sig(self.tysys.resolutions.defs().ast_id(*key)) + self.tysys.signatures.trait_sig(*key) } /// What `Self::X` means for a receiver reached through a trait bound, for @@ -1934,7 +1926,7 @@ impl Elaborator<'_, H> { Some(( fq_trait_name, MethodInfo { - method_ast_id: Some(sig.ast_id), + method_def: Some(sig.def), return_type: instantiated.return_type, self_kind: sig.self_kind, param_types: instantiated.param_types[first_value_param..].to_vec(), @@ -2625,7 +2617,7 @@ impl Elaborator<'_, H> { .borrow_mut() .intern(ResolvedType::Ref(base_type_id)); let method_info = MethodInfo { - method_ast_id: None, + method_def: None, return_type, self_kind: ast::SelfKind::Ref, param_types: vec![ref_self_ty], diff --git a/wado-compiler/src/elaborator/types.rs b/wado-compiler/src/elaborator/types.rs index 7c8f4cf1b4d..b28188ee245 100644 --- a/wado-compiler/src/elaborator/types.rs +++ b/wado-compiler/src/elaborator/types.rs @@ -1676,12 +1676,12 @@ impl MethodOwner { #[derive(Debug, Clone)] pub(super) struct MethodInfo { - /// The declaring node of the method this lookup selected, taken from its + /// The method this lookup selected, taken from its /// [`crate::elaborator::sig::MethodSig`]. The use→def edge for a call is /// recorded from here, so it names the impl dispatch actually chose. /// `None` where no declaration backs the signature: the tuple builtins, /// an auto-derived `Eq` / `Ord`, and the error-recovery placeholder. - pub(super) method_ast_id: Option, + pub(super) method_def: Option, pub(super) return_type: TypeId, pub(super) self_kind: ast::SelfKind, /// Parameter types (excluding self) From 43e3c880e22097a9853f88aab379cdb8f0625e04 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 20:47:58 +0000 Subject: [PATCH 04/25] fix(elaborator): resolve a bare call from its own reference site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A parameter default is written in the callee's module and walked from the call site, so a tier order over the *walking* module's names answered with the caller's same-named declaration. The use→def edge then named the caller's function while reify called the callee's, liveness never marked the real one reachable, and the call minted an extern stub for a function the package defines — an ICE. The call's own site was already answered, by the module that wrote it. Read that first. `panic` / `unreachable` reach codegen by name rather than as ordinary callees, so they answer ahead of it. Fixture: cross_module_same_name_default_fn.wado Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/call.rs | 40 +++++++++++++++---- .../cross_module_same_name_default_fn.wado | 14 +++++++ .../cross_module_same_name_default_fn_a.wado | 11 +++++ 3 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado create mode 100644 wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index 13aeca56057..046cf42296e 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -1332,6 +1332,38 @@ impl Elaborator<'_, H> { (None, effective_name.to_string()) } } + // Check for prelude functions (panic, unreachable). These are + // defined in core:rt and re-exported by core:prelude, and reach + // codegen by name rather than as an ordinary callee, so they answer + // ahead of the site below. + else if matches!(effective_name, "panic" | "unreachable") { + ( + Some(CalleeRef::rt_prelude(effective_name)), + effective_name.to_string(), + ) + } + // The call's own reference site, answered by the module that wrote it + // (WEP 2026-08-12). This is what makes a parameter default name the + // callee module's function: the default is written in the declaring + // module and walked from the call site, so a tier order over the + // *walking* module's names answers with the caller's same-named + // declaration instead. + else if let Some(callee) = self + .tysys + .resolutions + .declared_if_walked(ident.id) + .filter(|def| self.tysys.resolutions.defs().kind(*def) == crate::defs::DefKind::Function) + { + self.record_reference_to_decl(ident.id, callee); + let defs = self.tysys.resolutions.defs(); + ( + Some(CalleeRef::new( + defs.module(callee).clone(), + defs.name(callee).to_string(), + )), + effective_name.to_string(), + ) + } // Check if it's a local function (defined in this module) or // a built-in type constructor (Ok, Err, Some, None). // The four constructor names flow through the @@ -1364,14 +1396,6 @@ impl Elaborator<'_, H> { effective_name.to_string(), ) } - // Check for prelude functions (panic, unreachable) - // These are defined in core:rt and re-exported by core:prelude - else if matches!(effective_name, "panic" | "unreachable") { - ( - Some(CalleeRef::rt_prelude(effective_name)), - effective_name.to_string(), - ) - } // Check if this is an imported function (per-module imports). // We go through the `Symbol` directly so both the use→def edge // and the `CalleeRef` come from the same resolution — this is diff --git a/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado b/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado new file mode 100644 index 00000000000..18fbbe4a2e3 --- /dev/null +++ b/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado @@ -0,0 +1,14 @@ +// A parameter default is written in the callee's module and resolved at the +// call site, so the function it names is the *callee's* — even where the +// caller declares one under the same spelling. + +use { labelled } from "./sub/cross_module_same_name_default_fn_a.wado"; + +fn tag() -> i32 { + return 2; +} + +test "a parameter default names the callee module's function" { + assert labelled() == 1; + assert tag() == 2; +} diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado new file mode 100644 index 00000000000..9d59bacffaf --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado @@ -0,0 +1,11 @@ +// Module A: a parameter default calls A's own private `tag`. +// The entry module declares a `tag` of its own, so a default resolved +// from the *caller's* scope reaches the wrong declaration. + +fn tag() -> i32 { + return 1; +} + +pub fn labelled(v: i32 = tag()) -> i32 { + return v; +} From 7cce36bd1fd3d0008d2cfe2b3901ebabaed86d9c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 21:26:13 +0000 Subject: [PATCH 05/25] refactor(elaborator): key every free-function signature by `DefId` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `function_sigs` was keyed `(ModuleSource, String)`, and seven lookups each spelled their own tier order over it — this module, then the import, then the callee's scope. Three answers to one question, disagreeing exactly where two modules share a spelling. The site was already answered, once, by the module that wrote it. Each lookup now reads it: `free_function_at` / `free_function_sig_at`. The two positions with no site — `builtin::f`, a namespace member's signature — name their module rather than search for one. `CalleeRef` carries the declaration, with the module and name its one constructor reads off the table, so TIR emission needs no table and no rendering is read back. `Rendered` holds what names no declaration in this currency: an effect operation, and the unknown-callee sentinel. Falls out as dead: `symbol_at`, four `CalleeRef` constructors, and the `imported_functions` / `default_scope_module` branches of callee classification. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator.rs | 55 +++- wado-compiler/src/elaborator/call.rs | 283 ++++++------------ wado-compiler/src/elaborator/callee.rs | 85 ++++-- wado-compiler/src/elaborator/expr.rs | 36 +-- wado-compiler/src/elaborator/item.rs | 8 +- wado-compiler/src/elaborator/method_lookup.rs | 4 +- wado-compiler/src/elaborator/orchestration.rs | 9 +- wado-compiler/src/elaborator/sem/decls.rs | 2 +- wado-compiler/src/elaborator/sig.rs | 10 +- wado-compiler/src/elaborator/synth.rs | 21 +- 10 files changed, 226 insertions(+), 287 deletions(-) diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 9699f66111e..438fa3c6b36 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -265,16 +265,6 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { self.symbols.get(&self.tysys.resolutions.defs().ast_id(def)) } - /// The symbol row behind a reference site. - /// - /// The walk answered for the site, so the declaration — and with it the - /// row — comes from what the name means where it was *written*, with no - /// second scope run beside the first. - pub(crate) fn symbol_at(&self, site: crate::ast::AstId) -> Option<&'a crate::symbol::Symbol> { - let def = self.tysys.resolutions.declared_if_walked(site)?; - self.symbols.get(&self.tysys.resolutions.defs().ast_id(def)) - } - /// Construct a [`TypeLookup`] view over the elaborator's current import /// context and shared `all_*` tables. Use this for any type-name /// resolution; never reach into `all_*` directly. @@ -444,6 +434,40 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { self.insert_reference(use_id, def_id); } + /// The free function the reference site `site` names, or `None` where it + /// names something else — a binder, a variant case, a node no walk saw. + /// + /// One read replaces the "this module, then the import, then the callee's + /// scope" tier order each caller used to spell for itself: the site was + /// answered once, by the module that wrote it (WEP 2026-08-12). + pub(super) fn free_function_at(&self, site: crate::ast::AstId) -> Option { + let def = self.tysys.resolutions.declared_if_walked(site)?; + (self.tysys.resolutions.defs().kind(def) == crate::defs::DefKind::Function).then_some(def) + } + + /// The canonical signature of the free function the site names. + pub(super) fn free_function_sig_at( + &self, + site: crate::ast::AstId, + ) -> Option<&sem::decls::FunctionSig> { + self.tysys.signatures.function_sig(self.free_function_at(site)?) + } + + /// The declaration `module` declares under `name`. + /// + /// The module is named rather than searched for: a qualified path spelled + /// it. For the two positions no reference site answers — `builtin::f`, and + /// a member of a namespace import. + pub(super) fn decl_in_module( + &self, + module: &ModuleSource, + name: &str, + ) -> Option { + self.symbols + .lookup_in_module(module, name) + .and_then(|sym| self.tysys.resolutions.defs().of_ast_id(sym.defined_at)) + } + /// Record a use→def edge naming the declaration `def`. /// /// The edge map is keyed by node on both sides — navigation recovers a @@ -1771,11 +1795,18 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { // later in the file) to infer type arguments at the call site // during body resolution, without relying on a later // monomorphization-time fallback. - let mut function_sigs: IndexMap = IndexMap::default(); + let mut function_sigs: IndexMap = + IndexMap::default(); for item in &module.items { if let Item::Function(func) = item { + let def = self + .tysys + .resolutions + .defs() + .of_ast_id(func.id) + .expect("every free function is a declaration"); let sig = self.record_function_sig(func); - function_sigs.insert(func.name.clone(), sig); + function_sigs.insert(def, sig); } } for item in &module.items { diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index 046cf42296e..1b53798f805 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -13,7 +13,6 @@ use super::callee::{CalleeRef, StaticMethodRef}; use super::infer::InferCtx; use super::instantiate::Instantiation; use super::scope::Scope; -use super::sem::decls::FunctionSig; use super::sig::MethodSig; use super::trait_env; use super::types::{FunctionContext, TypeError}; @@ -141,6 +140,16 @@ impl CalleeIdentKind<'_> { /// Two segments exactly: consumers pair this with the prefix they split off /// `effective_name`, and only here are the two the same segment. A namespace /// prefix, an unqualified call and `Rewritten` all answer `None`. + fn callee_site(&self) -> Option { + match self { + Self::AsIs(ident) => Some(ident.id), + // A rewritten `Self::m` spelling names no node the walk answered + // for; the qualified paths above it resolve through their own + // segments instead. + Self::Rewritten(_) | Self::AbstractTypeParam { .. } => None, + } + } + fn receiver_site(&self) -> Option { match self { Self::AsIs(ident) => match ident.segments.as_slice() { @@ -490,7 +499,7 @@ impl Elaborator<'_, H> { // First, determine expected parameter types to handle coercion. let (mut param_types, callee_slots) = - self.lookup_function_signature(effective_name, receiver_site); + self.lookup_function_signature(effective_name, receiver_site, callee_kind.callee_site()); // Instantiate the callee's slots before an argument is resolved // against one of its parameter types. A rigid slot is the callee's @@ -644,6 +653,18 @@ impl Elaborator<'_, H> { // dispatches on `effective_name` (after any `Self::` / `T::` // prefix rewriting) while `ident` is kept around for LSP // segment-edge recording and other AST-id needs. + let is_result_or_option_case = { + let tt = self.tysys.type_table.borrow(); + let items = tt.compiler_items(); + [ + crate::compiler_item::CompilerItem::ResultOk, + crate::compiler_item::CompilerItem::ResultErr, + crate::compiler_item::CompilerItem::OptionSome, + crate::compiler_item::CompilerItem::OptionNone, + ] + .into_iter() + .any(|item| effective_name == items.variant_case_name(item)) + }; let (callee_opt, display_name): (Option, String) = if let Some(pos) = effective_name.find("::") { @@ -653,7 +674,8 @@ impl Elaborator<'_, H> { // Builtin functions: resolve through core:builtin module if prefix == "builtin" { ( - Some(CalleeRef::new(ModuleSource::builtin(), suffix)), + self.decl_in_module(&ModuleSource::builtin(), suffix) + .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), effective_name.to_string(), ) } @@ -1302,7 +1324,8 @@ impl Elaborator<'_, H> { } } ( - Some(CalleeRef::new(ns_source, suffix)), + self.decl_in_module(&ns_source, suffix) + .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), effective_name.to_string(), ) } @@ -1337,8 +1360,12 @@ impl Elaborator<'_, H> { // codegen by name rather than as an ordinary callee, so they answer // ahead of the site below. else if matches!(effective_name, "panic" | "unreachable") { + // Declarations of `core:rt`, named by the module rather than + // searched for: codegen keys them on that module, and their `!` + // return type is a signature fact like any other. ( - Some(CalleeRef::rt_prelude(effective_name)), + self.decl_in_module(&ModuleSource::rt(), effective_name) + .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), effective_name.to_string(), ) } @@ -1355,90 +1382,24 @@ impl Elaborator<'_, H> { .filter(|def| self.tysys.resolutions.defs().kind(*def) == crate::defs::DefKind::Function) { self.record_reference_to_decl(ident.id, callee); - let defs = self.tysys.resolutions.defs(); ( - Some(CalleeRef::new( - defs.module(callee).clone(), - defs.name(callee).to_string(), - )), + Some(CalleeRef::declared(self.tysys.resolutions.defs(), callee)), effective_name.to_string(), ) } - // Check if it's a local function (defined in this module) or - // a built-in type constructor (Ok, Err, Some, None). - // The four constructor names flow through the - // `CompilerItem` registry so a stdlib rename of any of - // them is picked up here without re-editing the literal set. - else if self - .sem - .decls - .function_return_types - .contains_key(effective_name) - || { - let tt = self.tysys.type_table.borrow(); - let items = tt.compiler_items(); - effective_name - == items.variant_case_name(crate::compiler_item::CompilerItem::ResultOk) - || effective_name - == items.variant_case_name(crate::compiler_item::CompilerItem::ResultErr) - || effective_name - == items.variant_case_name(crate::compiler_item::CompilerItem::OptionSome) - || effective_name - == items.variant_case_name(crate::compiler_item::CompilerItem::OptionNone) - } - { + // A built-in type constructor (Ok, Err, Some, None) — a variant case, + // not a function, so the site above declines it. The four names flow + // through the `CompilerItem` registry so a stdlib rename is picked up + // here without re-editing the literal set. + else if is_result_or_option_case { self.record_item_reference_by_name(ident.id, effective_name); ( - Some(CalleeRef::local( - &self.current_module_source, - effective_name.to_string(), + Some(CalleeRef::rendered( + self.current_module_source.clone(), + effective_name, )), effective_name.to_string(), ) - } - // Check if this is an imported function (per-module imports). - // We go through the `Symbol` directly so both the use→def edge - // and the `CalleeRef` come from the same resolution — this is - // the single place the alias→defining-name translation happens. - else if self.sem.decls.imported_functions.contains(effective_name) { - if let Some(symbol) = self.symbol_named(&self.current_module_source, effective_name) { - self.record_reference_to_def(ident.id, symbol.defined_at); - ( - Some(CalleeRef::from_imported_symbol(symbol)), - effective_name.to_string(), - ) - } else { - // Imported but not in symbols - shouldn't happen but allow - ( - Some(CalleeRef::local( - &self.current_module_source, - effective_name.to_string(), - )), - effective_name.to_string(), - ) - } - } - // Fallback: when resolving a default expression, a free-function - // call may target a private function of the callee's declaring - // module (see `default_scope_module`). `resolve_ident` already - // consults this fallback for bare identifiers / function refs; - // mirror it here for the *call* case so `helper()` in a default - // resolves in the defining module instead of erroring at the use - // site. The `CalleeRef`'s module drives `lookup_function_return_type` - // / `lookup_function_signature`, so the signature resolves in the - // defining module too. - else if let Some(fallback) = self.annotate_ctx.default_scope_module.clone() - && fallback != self.current_module_source - && self - .tysys - .signatures - .function_sig(&fallback, effective_name) - .is_some() - { - ( - Some(CalleeRef::new(fallback, effective_name.to_string())), - effective_name.to_string(), - ) } else { // Unknown function - will report error (None, effective_name.to_string()) @@ -1454,7 +1415,7 @@ impl Elaborator<'_, H> { name: display_name.clone(), span: call.span, }); - CalleeRef::local(&self.current_module_source, display_name) + CalleeRef::rendered(self.current_module_source.clone(), display_name) }; // Resolve explicit type arguments (`_` resolves to UNKNOWN). @@ -1586,8 +1547,8 @@ impl Elaborator<'_, H> { let param_is_mut = self.lookup_function_param_is_mut(&call.callee); let func_ref = FunctionRef { - module_source: callee.module, - name: callee.name, + module_source: callee.module().clone(), + name: callee.name().to_string(), monomorph_info: None, method_info: None, // Free function call, }; @@ -1681,8 +1642,8 @@ impl Elaborator<'_, H> { callee: &CalleeRef, receiver_site: Option, ) -> TypeId { - let callee_module = &callee.module; - let func_name = callee.name.as_str(); + let callee_module = callee.module(); + let func_name = callee.name(); // Handle builtin functions if callee_module.is_core_builtin() { return self.get_builtin_return_type(func_name); @@ -1708,8 +1669,8 @@ impl Elaborator<'_, H> { return return_type; } - if !callee_module.is_entry_point() - && let Some(sig) = self.tysys.signatures.function_sig(callee_module, func_name) + if let Some(def) = callee.def() + && let Some(sig) = self.tysys.signatures.function_sig(def) && let Some(return_type) = sig.decl.return_type { return return_type; @@ -1767,6 +1728,7 @@ impl Elaborator<'_, H> { &mut self, name: &str, receiver_site: Option, + callee_site: Option, ) -> (Vec, Vec) { // Check for qualified name (Type::method or Effect::operation) if let Some(pos) = name.find("::") { @@ -1787,10 +1749,8 @@ impl Elaborator<'_, H> { // Builtin functions: look up param types from core:builtin module if prefix == "builtin" - && let Some(sig) = self - .tysys - .signatures - .function_sig(&ModuleSource::builtin(), suffix) + && let Some(def) = self.decl_in_module(&ModuleSource::builtin(), suffix) + && let Some(sig) = self.tysys.signatures.function_sig(def) { return (sig.decl.param_types.clone(), Vec::new()); } @@ -1807,7 +1767,9 @@ impl Elaborator<'_, H> { // parameter and reaches codegen mismatched. if self.sem.imports.namespace_imports.contains_key(prefix) { let ns_source = self.sem.imports.namespace_imports[prefix].clone(); - if let Some(sig) = self.tysys.signatures.function_sig(&ns_source, suffix) { + if let Some(def) = self.decl_in_module(&ns_source, suffix) + && let Some(sig) = self.tysys.signatures.function_sig(def) + { return ( sig.decl.param_types.clone(), sig.decl.type_params.iter().map(|(_, id)| *id).collect(), @@ -1834,37 +1796,16 @@ impl Elaborator<'_, H> { return (Vec::new(), Vec::new()); } - let slots = |sig: &FunctionSig| sig.decl.type_params.iter().map(|(_, id)| *id).collect(); - - if let Some(sig) = self - .tysys - .signatures - .function_sig(&self.current_module_source, name) - { - return (sig.decl.param_types.clone(), slots(sig)); - } - - // Imported functions: the canonical signature resolved in the - // definition module's perspective, so same-named types from - // different modules can't be confused. - if let Some(symbol) = self.symbol_named(&self.current_module_source, name) { - let src = symbol.module_source().clone(); - let sym_name = symbol.name.clone(); - if let Some(sig) = self.tysys.signatures.function_sig(&src, &sym_name) { - return (sig.decl.param_types.clone(), slots(sig)); - } - } - - // A default expression may call a private free function of its - // declaring module (`default_scope_module`). - if let Some(fallback) = self.annotate_ctx.default_scope_module.clone() - && fallback != self.current_module_source - && let Some(sig) = self.tysys.signatures.function_sig(&fallback, name) - { - return (sig.decl.param_types.clone(), slots(sig)); - } - - (Vec::new(), Vec::new()) + // The callee's own site, answered by the module that wrote it — which + // covers this module's functions, its imports under either spelling, + // and a default expression's callee scope, all at once. + let Some(sig) = callee_site.and_then(|site| self.free_function_sig_at(site)) else { + return (Vec::new(), Vec::new()); + }; + ( + sig.decl.param_types.clone(), + sig.decl.type_params.iter().map(|(_, id)| *id).collect(), + ) } /// Fill missing trailing arguments from the callee's declared defaults, @@ -1941,30 +1882,18 @@ impl Elaborator<'_, H> { if let Some(defaults) = ctx.closure_defaults.get(&ident.name) { return (defaults.clone(), None); } - // A qualified name is never a function of the writing module, so only - // the site below can answer for one — `ns::f` included. - if !ident.name.contains("::") - && let Some(sig) = self - .tysys - .signatures - .function_sig(&self.current_module_source, &ident.name) - { - return ( - crate::elaborator::sig::Param::named_defaults(&sig.params), - Some(self.current_module_source.clone()), - ); - } - if let Some(symbol) = self.symbol_at(ident.id) { - let src = symbol.module_source().clone(); - let name = symbol.name.clone(); - if let Some(sig) = self.tysys.signatures.function_sig(&src, &name) { - return ( - crate::elaborator::sig::Param::named_defaults(&sig.params), - Some(src), - ); - } - } - (Vec::new(), None) + let Some(def) = self.free_function_at(ident.id) else { + return (Vec::new(), None); + }; + let Some(sig) = self.tysys.signatures.function_sig(def) else { + return (Vec::new(), None); + }; + // A default resolves in the declaring module's scope, which is the + // callee's own — never the caller's, even under the same spelling. + ( + crate::elaborator::sig::Param::named_defaults(&sig.params), + Some(self.tysys.resolutions.defs().module(def).clone()), + ) } /// Look up whether each parameter of a free function is `mut`. @@ -1979,24 +1908,9 @@ impl Elaborator<'_, H> { return Vec::new(); } - if let Some(sig) = self - .tysys - .signatures - .function_sig(&self.current_module_source, &ident.name) - { - return crate::elaborator::sig::Param::is_mut_flags(&sig.params); - } - - // Imported function - if let Some(symbol) = self.symbol_at(ident.id) { - let src = symbol.module_source().clone(); - let name = symbol.name.clone(); - if let Some(sig) = self.tysys.signatures.function_sig(&src, &name) { - return crate::elaborator::sig::Param::is_mut_flags(&sig.params); - } - } - - Vec::new() + self.free_function_sig_at(ident.id) + .map(|sig| crate::elaborator::sig::Param::is_mut_flags(&sig.params)) + .unwrap_or_default() } /// Infer a generic call's type arguments from its actual argument types, in @@ -2012,7 +1926,7 @@ impl Elaborator<'_, H> { expected_type: Option, span: crate::token::Span, ) -> Vec { - let func_name = callee.name.as_str(); + let func_name = callee.name(); // Builtin functions: pull type-param / param / return info from the // BuiltinRegistry so that calls like `builtin::select(a, b, c)` and // `builtin::array_new(n)` infer their generic type parameters from @@ -2246,7 +2160,7 @@ impl Elaborator<'_, H> { .map(|n| format!("`{n}`")) .collect::>() .join(", "); - let func_name = callee.name.as_str(); + let func_name = callee.name(); let _ = self.emit(TypeError::CannotInferType { message: format!( "cannot infer type parameter {names} of function `{func_name}`; \ @@ -2341,7 +2255,7 @@ impl Elaborator<'_, H> { let n = space.len(); let defaults: Vec> = - self.with_default_scope_module(Some(callee.module.clone()), |s| { + self.with_default_scope_module(Some(callee.module().clone()), |s| { let mut scope = s.enter_inherited_type_param_scope(); scope.annotate_ctx.trait_ctx.type_params.clear(); scope.register_generic_params(¶ms, 0); @@ -2460,7 +2374,7 @@ impl Elaborator<'_, H> { return; } - let func_name = callee.name.as_str(); + let func_name = callee.name(); let message = format!( "cannot infer type parameter {} of function `{func_name}`; \ add a turbofish (`{func_name}::<...>()`) or a type annotation", @@ -2606,10 +2520,7 @@ impl Elaborator<'_, H> { &self, callee: &CalleeRef, ) -> Option<(Vec<(String, TypeId)>, Vec, Option)> { - let sig = self - .tysys - .signatures - .function_sig(&callee.module, &callee.name)?; + let sig = self.tysys.signatures.function_sig(callee.def()?)?; if sig.type_param_ids.is_empty() { return Some((vec![], vec![], None)); } @@ -2648,7 +2559,7 @@ impl Elaborator<'_, H> { expected_type: Option, span: crate::token::Span, ) -> (Vec, Vec) { - let probe = CalleeRef::local(&self.current_module_source, suffix.to_string()); + let probe = CalleeRef::rendered(self.current_module_source.clone(), suffix); let method_args = self.infer_fn_type_args(&probe, raw_args, args, expected_type, span); if !method_args.is_empty() { return (Vec::new(), method_args); @@ -2745,30 +2656,10 @@ impl Elaborator<'_, H> { return Vec::new(); }; - let param_types: Vec = if self - .sem - .decls - .function_return_types - .contains_key(&ident.name) - { - match self - .tysys - .signatures - .function_sig(&self.current_module_source, &ident.name) - { - Some(sig) => sig.decl.param_types.clone(), - None => return Vec::new(), - } - } else if let Some(symbol) = self.symbol_at(ident.id) { - let src = symbol.module_source().clone(); - let name = symbol.name.clone(); - match self.tysys.signatures.function_sig(&src, &name) { - Some(sig) => sig.decl.param_types.clone(), - None => return Vec::new(), - } - } else { + let Some(sig) = self.free_function_sig_at(ident.id) else { return Vec::new(); }; + let param_types: Vec = sig.decl.param_types.clone(); // Substitute type params with explicit type args param_types diff --git a/wado-compiler/src/elaborator/callee.rs b/wado-compiler/src/elaborator/callee.rs index dc92d1f565c..d7f137ff8d2 100644 --- a/wado-compiler/src/elaborator/callee.rs +++ b/wado-compiler/src/elaborator/callee.rs @@ -1,47 +1,48 @@ -//! Resolved call-target identities: `CalleeRef` and `StaticMethodRef` bundle the -//! `(module, name)` pair naming a free function or static method. Threaded -//! separately, the defining-module name and the caller-visible alias could drift -//! apart; the bundle leaves no way to split them and confines the -//! alias→defining-name translation to the factory methods below. +//! Resolved call-target identities. A free function callee is the declaration +//! it names (WEP 2026-08-12); a static method callee bundles the receiver and +//! method names dispatch picked, alongside the method's own declaration. use crate::module_source::{ModuleSource, ModuleSourceInterner}; -use crate::symbol::Symbol; -/// Identity of a free function callee: the module where it is defined and the -/// name under which it is defined in that module. +/// Identity of a free function callee. +/// +/// `Declared` is the ordinary case: the declaration, plus the module and name +/// its one constructor reads off the table, so a consumer emitting TIR needs no +/// table at hand while only `def` says which declaration this is. Nothing reads +/// the rendering back into an identity. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(super) struct CalleeRef { - pub module: ModuleSource, - pub name: String, +pub(super) enum CalleeRef { + Declared { + def: crate::defs::DefId, + module: ModuleSource, + name: String, + }, + /// A callee no declaration names in this currency: an effect operation, + /// whose module is the namespace that signature resolution, the effect + /// check, dispatch and WIR all key on rather than one any module declares. + Rendered { module: ModuleSource, name: String }, } impl CalleeRef { - pub fn new(module: ModuleSource, name: impl Into) -> Self { - Self { - module, - name: name.into(), + /// The free function `def` declares, rendered once from the table. + pub fn declared(defs: &crate::defs::DefTable, def: crate::defs::DefId) -> Self { + Self::Declared { + def, + module: defs.module(def).clone(), + name: defs.name(def).to_string(), } } - /// A callee defined in the current module (local function). - pub fn local(current_module: &ModuleSource, name: impl Into) -> Self { - Self::new(current_module.clone(), name) - } - - /// A callee imported into the current module via `use`. Translates the - /// caller-visible alias (under which the symbol is keyed in the current - /// module's symbol table) to the defining-module name in a single place. - pub fn from_imported_symbol(symbol: &Symbol) -> Self { - Self::new(symbol.module_source().clone(), symbol.name.clone()) - } - - /// A callee in `core:rt` (prelude functions like `panic`, `unreachable`). - pub fn rt_prelude(name: impl Into) -> Self { - Self::new(ModuleSource::rt(), name) + /// A callee with no declaration behind it. See [`Self::Rendered`]. + pub fn rendered(module: ModuleSource, name: impl Into) -> Self { + Self::Rendered { + module, + name: name.into(), + } } /// A callee reached through a namespace-qualified call `Prefix::name` - /// where `Prefix` is a module path (e.g. `Stdout::write`). The + /// where `Prefix` names an effect or resource rather than a module. The /// `prefix` is interned through the elaborator's /// [`crate::module_source::ModuleSourceInterner`] and wrapped in a /// `ModuleSource::Local`. @@ -50,7 +51,27 @@ impl CalleeRef { prefix: &str, name: impl Into, ) -> Self { - Self::new(interner.local(prefix), name) + Self::rendered(interner.local(prefix), name) + } + + /// The declaration this names, or `None` for a [`Self::Rendered`] callee. + pub fn def(&self) -> Option { + match self { + Self::Declared { def, .. } => Some(*def), + Self::Rendered { .. } => None, + } + } + + pub fn module(&self) -> &ModuleSource { + match self { + Self::Declared { module, .. } | Self::Rendered { module, .. } => module, + } + } + + pub fn name(&self) -> &str { + match self { + Self::Declared { name, .. } | Self::Rendered { name, .. } => name, + } } } diff --git a/wado-compiler/src/elaborator/expr.rs b/wado-compiler/src/elaborator/expr.rs index db1c6c0e2a1..87a65c16c3a 100644 --- a/wado-compiler/src/elaborator/expr.rs +++ b/wado-compiler/src/elaborator/expr.rs @@ -688,7 +688,7 @@ impl Elaborator<'_, H> { // and functions (issue #1486). if let Some(fallback) = self.annotate_ctx.default_scope_module.clone() && fallback != self.current_module_source - && let Some(result) = self.resolve_ident_in_fallback_module(&ident.name, &fallback) + && let Some(result) = self.resolve_ident_in_fallback_module(ident, &fallback) { return result; } @@ -705,17 +705,17 @@ impl Elaborator<'_, H> { /// default-expression resolution. Supports globals and function refs. fn resolve_ident_in_fallback_module( &mut self, - name: &str, + ident: &ast::IdentExpr, fallback: &ModuleSource, ) -> Option { // Reify resolves the fallback-module global / `FuncRef` its own // way; project the type only. This default-expr path is never an // assignment target, so no place is recorded. - let (owner, name) = self.declaring_module_of_ident(name, fallback); + let (owner, name) = self.declaring_module_of_ident(&ident.name, fallback); if let Some((ty, _)) = self.tysys.signatures.global(&owner, &name) { return Some(ty); } - let sig = self.tysys.signatures.function_sig(&owner, &name)?.clone(); + let sig = self.free_function_sig_at(ident.id)?.clone(); Some( self.compute_func_ref_type_from_sig(&sig, &[]) .unwrap_or(TypeTable::UNKNOWN), @@ -731,7 +731,9 @@ impl Elaborator<'_, H> { fallback: &ModuleSource, ) -> (ModuleSource, String) { if self.tysys.signatures.global(fallback, name).is_some() - || self.tysys.signatures.function_sig(fallback, name).is_some() + || self + .decl_in_module(fallback, name) + .is_some_and(|def| self.tysys.signatures.function_sig(def).is_some()) { return (fallback.clone(), name.to_string()); } @@ -951,7 +953,7 @@ impl Elaborator<'_, H> { ) -> TypeId { self.record_item_reference_by_name(ident.id, &ident.name); - let Some((sig, _def_module, _defining_name)) = self.lookup_func_sig_for_ref(&ident.name) + let Some((sig, _def_module, _defining_name)) = self.lookup_func_sig_for_ref(ident) else { // Fallback: known function but its signature is unreachable // (shouldn't normally happen). Emit a stub FuncRef so downstream @@ -1069,24 +1071,12 @@ impl Elaborator<'_, H> { /// key space. fn lookup_func_sig_for_ref( &self, - name: &str, + ident: &ast::IdentExpr, ) -> Option<(super::sem::decls::FunctionSig, ModuleSource, String)> { - if let Some(sig) = self - .tysys - .signatures - .function_sig(&self.current_module_source, name) - { - return Some(( - sig.clone(), - self.current_module_source.clone(), - name.to_string(), - )); - } - let symbol = self.symbol_named(&self.current_module_source, name)?; - let src = symbol.module_source().clone(); - let original = symbol.name.clone(); - let sig = self.tysys.signatures.function_sig(&src, &original)?.clone(); - Some((sig, src, original)) + let def = self.free_function_at(ident.id)?; + let sig = self.tysys.signatures.function_sig(def)?.clone(); + let defs = self.tysys.resolutions.defs(); + Some((sig, defs.module(def).clone(), defs.name(def).to_string())) } /// Derive type arguments for a generic function reference from an expected diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index 67541964a9e..01f2c86007e 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -2217,11 +2217,17 @@ impl Elaborator<'_, H> { /// re-resolution. Returns the declared return type for callers that /// need it (`resolve_function`'s `task_return_type`). fn populate_generic_function_cache(&mut self, func: &Function) -> TypeId { + let def = self + .tysys + .resolutions + .defs() + .of_ast_id(func.id) + .expect("every free function is a declaration"); let sig = self .sem .decls .function_sigs - .get(&func.name) + .get(&def) .expect("decl pass records every free function's canonical signature"); let type_param_list = sig.decl.type_params.clone(); let resolved_param_types = sig.decl.param_types.clone(); diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index b8908140f95..fd6ece89a99 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -3135,8 +3135,8 @@ impl Elaborator<'_, H> { &self, callee: &super::callee::CalleeRef, ) -> Vec { - let callee_module = &callee.module; - let func_name = callee.name.as_str(); + let callee_module = callee.module(); + let func_name = callee.name(); let fn_type_params = &self.tysys.trait_env.function_type_params; // Entry-point callees are looked up in the current module's functions first. if callee_module.is_entry_point() diff --git a/wado-compiler/src/elaborator/orchestration.rs b/wado-compiler/src/elaborator/orchestration.rs index eea03cf6b94..248c9cf85f7 100644 --- a/wado-compiler/src/elaborator/orchestration.rs +++ b/wado-compiler/src/elaborator/orchestration.rs @@ -1426,9 +1426,12 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { for (ast_id, sig) in &sem.decls.trait_sigs { signatures.trait_sigs.insert(*ast_id, sig.clone()); } - signatures - .function_sigs - .insert(module_source.clone(), Rc::clone(&sem.decls.function_sigs)); + signatures.function_sigs.extend( + sem.decls + .function_sigs + .iter() + .map(|(def, sig)| (*def, sig.clone())), + ); signatures.globals.insert( module_source.clone(), sem.decls.current_module_globals.clone(), diff --git a/wado-compiler/src/elaborator/sem/decls.rs b/wado-compiler/src/elaborator/sem/decls.rs index 70309e08293..aafd9cf7419 100644 --- a/wado-compiler/src/elaborator/sem/decls.rs +++ b/wado-compiler/src/elaborator/sem/decls.rs @@ -60,7 +60,7 @@ pub(crate) struct ModuleDecls { /// Canonical signatures of this module's own free functions, frozen /// behind `Rc` so the program-wide assembly and the stdlib-snapshot /// seeding share the map instead of deep-cloning every signature. - pub(crate) function_sigs: std::rc::Rc>, + pub(crate) function_sigs: std::rc::Rc>, /// `func_name → return TypeId` for functions defined in this module. pub(crate) function_return_types: IndexMap, /// Names visible via `use` declarations in this module (the union of diff --git a/wado-compiler/src/elaborator/sig.rs b/wado-compiler/src/elaborator/sig.rs index 924e644a010..85a55706d7e 100644 --- a/wado-compiler/src/elaborator/sig.rs +++ b/wado-compiler/src/elaborator/sig.rs @@ -16,8 +16,8 @@ use super::sem::decls::FunctionSig; /// associated-const values, `__DATA__`. Assembled from `ModuleDecls` digests. #[derive(Default)] pub(crate) struct Signatures { - /// Canonical free-function signatures, declaring module → name. - pub(crate) function_sigs: IndexMap>>, + /// Canonical free-function signatures, keyed by the declaration. + pub(crate) function_sigs: IndexMap, /// Canonical method signatures, keyed by the method's [`crate::defs::DefId`] /// — `impl`-block methods and `interface` / `resource` operations alike. @@ -54,9 +54,9 @@ pub(crate) struct Signatures { } impl Signatures { - /// Canonical signature of the free function `name` declared in `module`. - pub(crate) fn function_sig(&self, module: &ModuleSource, name: &str) -> Option<&FunctionSig> { - self.function_sigs.get(module)?.get(name) + /// Canonical signature of the free function `def` declares. + pub(crate) fn function_sig(&self, def: crate::defs::DefId) -> Option<&FunctionSig> { + self.function_sigs.get(&def) } /// Canonical signature of the method `def` declares. diff --git a/wado-compiler/src/elaborator/synth.rs b/wado-compiler/src/elaborator/synth.rs index 0f2dade3c74..69ce452f105 100644 --- a/wado-compiler/src/elaborator/synth.rs +++ b/wado-compiler/src/elaborator/synth.rs @@ -625,7 +625,7 @@ impl Elaborator<'_, H> { { return self.class_of_type(sig.return_type); } - let Some(callee) = self.synth_callee_ref(&ident.name) else { + let Some(callee) = self.synth_callee_ref(ident) else { return ArgClass::Opaque(OpaqueReason::Inference); }; if !self.lookup_function_type_params(&callee).is_empty() { @@ -635,19 +635,16 @@ impl Elaborator<'_, H> { self.class_of_type(return_type) } - /// The callee identity of a plain `name(…)` call — a function the module - /// declares or imports. Anything else (a variant constructor, a static - /// path, an effect operation) is left to the expected type. - fn synth_callee_ref(&self, name: &str) -> Option { - if name.contains("::") { + /// The callee identity of a plain `name(…)` call, read off its own + /// reference site. Anything else (a variant constructor, a static path, an + /// effect operation) names no function there and is left to the expected + /// type. + fn synth_callee_ref(&self, ident: &ast::IdentExpr) -> Option { + if ident.name.contains("::") { return None; } - if self.sem.decls.function_return_types.contains_key(name) { - return Some(CalleeRef::local(&self.current_module_source, name)); - } - let symbol = self.symbol_named(&self.current_module_source, name)?; - matches!(symbol.kind, crate::symbol::SymbolKind::Function(_)) - .then(|| CalleeRef::from_imported_symbol(symbol)) + let def = self.free_function_at(ident.id)?; + Some(CalleeRef::declared(self.tysys.resolutions.defs(), def)) } fn synth_method_call( From a3d661db8d10779d553b3e98df544d7aefb0618b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 23 Aug 2026 22:33:53 +0000 Subject: [PATCH 06/25] refactor(elaborator): name the `impl` block a dispatch was recorded against MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `effect_ops` was the last `AstId`-keyed declaration map; the operator path derived an impl's module by walking the receiver's newtype chain comparing renderings, with a by-name lookup behind it. `ArithmeticTraitInfo` carries the block the lookup already matched, so where there is one the module is read off it. The three paths that name no block — an auto-derived `Eq` / `Ord`, and a method reached through a type parameter's bound, whose block monomorphization picks — keep the derivation. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator.rs | 6 ++- wado-compiler/src/elaborator/item.rs | 11 ++++- wado-compiler/src/elaborator/method_lookup.rs | 1 + wado-compiler/src/elaborator/operators.rs | 40 ++++++++++++------- wado-compiler/src/elaborator/sem/decls.rs | 4 +- wado-compiler/src/elaborator/trait_query.rs | 13 +++++- wado-compiler/src/elaborator/types.rs | 8 ++++ 7 files changed, 62 insertions(+), 21 deletions(-) diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 438fa3c6b36..1357fda1067 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -1786,7 +1786,11 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .resource_method_ids .insert((owner, method.name.clone()), op); } - self.sem.decls.effect_ops.insert(decl_id, ops); + self.sem.decls.effect_ops.insert( + defs.of_ast_id(decl_id) + .expect("every interface / resource declaration is a declaration"), + ops, + ); } // Pre-populate the generic-function inference caches for every diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index 01f2c86007e..2fb5cf4d341 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -1516,12 +1516,19 @@ impl Elaborator<'_, H> { } } - /// Operation signatures the decl pass recorded for `decl_id`. + /// Operation signatures the decl pass recorded for the declaration at + /// `decl_id`. fn declared_effect_ops(&self, decl_id: ast::AstId) -> Vec { + let decl = self + .tysys + .resolutions + .defs() + .of_ast_id(decl_id) + .expect("every interface / resource declaration is a declaration"); self.sem .decls .effect_ops - .get(&decl_id) + .get(&decl) .cloned() .expect("the decl pass records every interface / resource declaration's operations") } diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index fd6ece89a99..41fef1a2853 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -3094,6 +3094,7 @@ impl Elaborator<'_, H> { .unwrap_or(base_type_id); Some(ArithmeticTraitInfo { + impl_def: impl_ref.0, output_type, self_kind, // The *full* spelling (`Add`), not the operator's diff --git a/wado-compiler/src/elaborator/operators.rs b/wado-compiler/src/elaborator/operators.rs index 54a4fbc3a07..897cb4b00a4 100644 --- a/wado-compiler/src/elaborator/operators.rs +++ b/wado-compiler/src/elaborator/operators.rs @@ -528,6 +528,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: eq_trait_name, method_name: "eq".to_string(), + impl_def: None, impl_name: name.clone(), impl_type_id: None, self_kind: info.self_kind, @@ -569,6 +570,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: ord_trait_name, method_name: "cmp".to_string(), + impl_def: None, impl_name: name.clone(), impl_type_id: None, self_kind: info.self_kind, @@ -680,6 +682,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: trait_info.trait_name, method_name: method_name.to_string(), + impl_def: Some(trait_info.impl_def), impl_name, impl_type_id: Some(impl_type_id), self_kind: trait_info.self_kind, @@ -722,6 +725,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: found_trait, method_name: method_name.to_string(), + impl_def: None, impl_name: name.clone(), impl_type_id: None, self_kind: info.self_kind, @@ -787,6 +791,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: found_trait, method_name: shift_method.to_string(), + impl_def: None, impl_name: name.clone(), impl_type_id: None, self_kind: info.self_kind, @@ -870,6 +875,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: trait_info.trait_name, method_name: method_name.to_string(), + impl_def: Some(trait_info.impl_def), impl_name, impl_type_id: Some(impl_type_id), self_kind: trait_info.self_kind, @@ -1161,6 +1167,7 @@ impl Elaborator<'_, H> { let resolved = ResolvedTraitMethod { trait_name: found_trait, method_name: method_name.to_string(), + impl_def: None, impl_name: name.clone(), impl_type_id: None, self_kind: info.self_kind, @@ -1960,20 +1967,25 @@ impl Elaborator<'_, H> { ); method_info.is_type_param_receiver = resolved.is_type_param_receiver; - // Resolve the impl's module from the receiver's *actual* type, not its - // bare name: same-named structs in different modules each have their own - // operator impls (e.g. auto-derived `Eq`/`Ord`), and a by-name lookup - // would route every call to whichever registered first. `impl_name` is - // the newtype-chain link the impl was found on, so key off that link — - // peeling to the base instead would send an impl written on the newtype - // to the base's module. Fall back to the by-name lookup when the - // receiver carries no declaring module. - let module_source = self - .impl_target_decl_key(receiver.type_id, &resolved.impl_name) - .map_or_else( - || self.declaring_module_of(&resolved.impl_name), - |def| self.tysys.resolutions.defs().module(def).clone(), - ); + // Where a block was matched, the module is its own — read off the + // declaration dispatch selected rather than derived from the receiver. + // + // The rest name no block: an auto-derived `Eq` / `Ord`, and a method + // reached through a type parameter's bound, whose block + // monomorphization picks. There the receiver's newtype chain answers, + // keyed on the link the lookup was made on — peeling to the base + // instead would send an impl written on the newtype to the base's + // module — and a by-name lookup is the last resort for a receiver + // carrying no declaring module. + let module_source = match resolved.impl_def { + Some(def) => self.tysys.resolutions.defs().module(def).clone(), + None => self + .impl_target_decl_key(receiver.type_id, &resolved.impl_name) + .map_or_else( + || self.declaring_module_of(&resolved.impl_name), + |def| self.tysys.resolutions.defs().module(def).clone(), + ), + }; let function_ref = FunctionRef { module_source, name: mangled_method_name, diff --git a/wado-compiler/src/elaborator/sem/decls.rs b/wado-compiler/src/elaborator/sem/decls.rs index aafd9cf7419..b6cab5728c7 100644 --- a/wado-compiler/src/elaborator/sem/decls.rs +++ b/wado-compiler/src/elaborator/sem/decls.rs @@ -104,12 +104,12 @@ pub(crate) struct ModuleDecls { /// a use site instantiates instead of re-resolving the trait method AST. pub(crate) trait_sigs: IndexMap, /// Resolved operation signatures of this module's `interface` and - /// `resource` declarations, keyed by the declaration's `AstId`. + /// `resource` declarations, keyed by the declaration. /// /// Resolved in the declaration's own frame — type params registered and /// `Self` constructed — which is why the body pass reads these back /// instead of resolving the same methods a second time. - pub(crate) effect_ops: IndexMap>, + pub(crate) effect_ops: IndexMap>, /// `func_name → type_params` for generic functions in this module. pub(crate) generic_function_params: IndexMap>, diff --git a/wado-compiler/src/elaborator/trait_query.rs b/wado-compiler/src/elaborator/trait_query.rs index eb2f68ebf17..c1388112096 100644 --- a/wado-compiler/src/elaborator/trait_query.rs +++ b/wado-compiler/src/elaborator/trait_query.rs @@ -2545,12 +2545,18 @@ impl Elaborator<'_, H> { // `output_type` to the receiver type absent a `type Output`. The set and // the types come from `TypeSystem::auto_derive_by_trait`. let auto_derive = self.tysys.auto_derive_by_trait(trait_name); - let (info_trait_name, self_kind, param_types, return_type) = if let Some(info) = + let (info_trait_name, self_kind, param_types, return_type, impl_def) = if let Some(info) = self.find_arithmetic_trait_impl(struct_name, lookup_type_id, trait_, method_name, None) { let return_type = auto_derive.map_or(info.output_type, |(_, ty)| ty); let param_types = info.rhs_type.map(|t| vec![t]).unwrap_or_default(); - (info.trait_name, info.self_kind, param_types, return_type) + ( + info.trait_name, + info.self_kind, + param_types, + return_type, + Some(info.impl_def), + ) } else if let Some((item, return_type)) = auto_derive && let Some(trait_) = self.tysys.compiler_trait_def(item) && self.tysys.type_implements_trait( @@ -2565,11 +2571,13 @@ impl Elaborator<'_, H> { .type_table .borrow_mut() .intern(ResolvedType::Ref(lookup_type_id)); + // Auto-derived: no `impl` block is written, so none is named. ( self.tysys.type_table.borrow().compiler_trait_fq(item), ast::SelfKind::Ref, vec![ref_self_ty], return_type, + None, ) } else { return None; @@ -2577,6 +2585,7 @@ impl Elaborator<'_, H> { Some(ResolvedTraitMethod { trait_name: info_trait_name, method_name: method_name.to_string(), + impl_def, impl_name: struct_name.to_string(), impl_type_id: (!is_type_param).then_some(lookup_type_id), self_kind, diff --git a/wado-compiler/src/elaborator/types.rs b/wado-compiler/src/elaborator/types.rs index b28188ee245..4f074d3eda2 100644 --- a/wado-compiler/src/elaborator/types.rs +++ b/wado-compiler/src/elaborator/types.rs @@ -2440,6 +2440,9 @@ pub(super) struct IndexValueTraitInfo { /// Info about an operator trait implementation #[derive(Clone)] pub(super) struct ArithmeticTraitInfo { + /// The `impl` block that matched. The module a dispatch is recorded + /// against is read off it, so no rendering is compared to find one. + pub(super) impl_def: crate::defs::DefId, /// The Output associated type pub(super) output_type: TypeId, /// Self kind for the method (&self) @@ -2462,6 +2465,11 @@ pub(super) struct ResolvedTraitMethod { pub(super) trait_name: crate::name::FqTraitName, /// Method name (e.g., "eq", "cmp", "add", "shl", "neg", "bitnot"). pub(super) method_name: String, + /// The `impl` block dispatch selected, where one was matched. `None` + /// where no block is named: an auto-derived `Eq` / `Ord`, and a method + /// reached through a type parameter's bound, whose block monomorphization + /// picks. + pub(super) impl_def: Option, /// Written name of the type whose impl matched — the impl-index key. For /// newtypes this may be the ultimate base-type name when dispatch falls /// back to the base impl. From 6de4d18ccec527a0d46efcea2fe46483d4f60a2b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 00:25:21 +0000 Subject: [PATCH 07/25] fix(elaborator): name a generic impl's target at its own header site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registering a generic impl's `type X = …` looked its target up by the written name in the impl's module, then scanned every loaded module and took the first match — a build-order pick with no ambiguity check, which WEP 2026-08-12 names as what a derivation may not be. Instrumenting it shows the pick firing: with two modules declaring `Node`, an impl written outside either registers against whichever loaded first. The header names its target at a site of its own, which the writing module answered for — the same read `trait_key` above it already does. The whole-program scan is dropped rather than repaired: the suite passes without it, so nothing consulted what it found. The direct tier is not dead — removing the registration entirely fails the stdlib snapshot. The key stays `AstId`-shaped: its readers arrive through `decl_of_type`, which also answers for monomorphized instances and `BuiltinArray`, neither of which carries a `DefId`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/orchestration.rs | 26 ++++++++++--------- .../cross_module_same_name_gassoc.wado | 17 ++++++++++++ .../sub/cross_module_same_name_gassoc_a.wado | 4 +++ .../sub/cross_module_same_name_gassoc_b.wado | 16 ++++++++++++ .../cross_module_same_name_gassoc_impl.wado | 11 ++++++++ .../cross_module_same_name_gassoc_trait.wado | 5 ++++ 6 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado create mode 100644 wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_a.wado create mode 100644 wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado create mode 100644 wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado create mode 100644 wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado diff --git a/wado-compiler/src/elaborator/orchestration.rs b/wado-compiler/src/elaborator/orchestration.rs index 248c9cf85f7..af9eebdc65b 100644 --- a/wado-compiler/src/elaborator/orchestration.rs +++ b/wado-compiler/src/elaborator/orchestration.rs @@ -3453,8 +3453,6 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { if impl_block.trait_type.is_none() || impl_block.associated_types.is_empty() { continue; } - // Determine the struct name (base name without type args) - let struct_name = super::trait_env::get_type_name_static(&impl_block.ty); // The header's own reference site says which trait declares // these bindings; a block naming a trait that reaches no // declaration registers nothing. @@ -3510,16 +3508,20 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { _ => continue, }; - // The impl need not live in the type's module, so fall - // back to every loaded module before giving up. - let base_decl = { - let tt = type_table.borrow(); - tt.decl_by_name(&struct_name, module_source).or_else(|| { - modules - .keys() - .find_map(|ms| tt.decl_by_name(&struct_name, ms)) - }) - }; + // The target is named at the header's own site, which the + // module that wrote it answered for — the same way + // `trait_key` above is read. Looking it up by the written + // name instead reached whichever module declares that + // spelling first, a build-order pick rather than an answer. + // + // Rendered back to the declaring node because this key is + // still `AstId`-shaped: its readers arrive through + // `decl_of_type`, which also answers for monomorphized + // instances and `BuiltinArray`, neither of which carries a + // `DefId`. + let base_decl = crate::resolve::head_site(&impl_block.ty) + .and_then(|site| resolutions.declared(site)) + .map(|def| resolutions.defs().ast_id(def)); if let Some(base_decl) = base_decl { type_table.borrow_mut().register_generic_assoc_type_def( base_decl, diff --git a/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado b/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado new file mode 100644 index 00000000000..1db71c78da7 --- /dev/null +++ b/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado @@ -0,0 +1,17 @@ +// A generic impl's `type Out = …` belongs to the declaration its header names, +// not to whichever module declares that spelling first. Module A's `Node` +// binds `Out` to slot 0 from an impl written outside A; module B's `Node` +// binds it to slot 1 from an impl in its own module. + +use { Unwrap } from "./sub/cross_module_same_name_gassoc_trait.wado"; +use { Node as NodeB } from "./sub/cross_module_same_name_gassoc_b.wado"; +use { Node as NodeA } from "./sub/cross_module_same_name_gassoc_a.wado"; +use {} from "./sub/cross_module_same_name_gassoc_impl.wado"; + +test "a generic impl's associated type belongs to the declaration its header names" { + let a = NodeA { v: 7 }; + assert a.unwrap_it() == 7; + + let b = NodeB { k: "key", v: 9 }; + assert b.unwrap_it() == 9; +} diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_a.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_a.wado new file mode 100644 index 00000000000..845530b056c --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_a.wado @@ -0,0 +1,4 @@ +// Module A's `Node`, whose generic impl is written elsewhere. +pub struct Node { + pub v: T, +} diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado new file mode 100644 index 00000000000..2958e014db7 --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado @@ -0,0 +1,16 @@ +// Module B declares a `Node` of the same name and implements `Unwrap` for it +// in its own module, binding `Out` to its *second* slot. A registration +// mis-routed here by name would overwrite that with a first-slot answer. +use { Unwrap } from "./cross_module_same_name_gassoc_trait.wado"; + +pub struct Node { + pub k: K, + pub v: V, +} + +impl Unwrap for Node { + type Out = V; + fn unwrap_it(&self) -> Self::Out { + return self.v; + } +} diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado new file mode 100644 index 00000000000..c19739ff26a --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado @@ -0,0 +1,11 @@ +// The impl for A's `Node`, written outside A. Its target is named at the +// header's own site; a scan by the written name reaches B's `Node` too. +use { Unwrap } from "./cross_module_same_name_gassoc_trait.wado"; +use { Node } from "./cross_module_same_name_gassoc_a.wado"; + +impl Unwrap for Node { + type Out = T; + fn unwrap_it(&self) -> Self::Out { + return self.v; + } +} diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado new file mode 100644 index 00000000000..9551051c08d --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado @@ -0,0 +1,5 @@ +// The trait both same-named `Node`s would implement. +pub trait Unwrap { + type Out; + fn unwrap_it(&self) -> Self::Out; +} From 26e8ffb3fb5a6883d31a6bde3b81b9414ff5eb0d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 05:14:35 +0000 Subject: [PATCH 08/25] fix(elaborator): answer a call from its site before reaching for a name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six findings from review over the `DefId` re-keying, four of them regressions this branch introduced. `panic` / `unreachable` answered ahead of the call's own reference site, so a module declaring either name of its own lost it: `fn panic(msg) -> i32` compiled to a `core:rt/panic` call that trapped, with only an unused-function warning. The site answers first now and the prelude branch is its fallback, for a synthesised node no walk saw. Operator dispatch took its module from the matched `impl` block for a generic block too, but a generic block's post-substitution instance is materialised in the receiver type's module — the convention `concrete_impl_module_for` encodes. Only a concrete block names it. `cm_owner` read the selected method's parent, which is the `impl` block that declares it rather than a resource, so the spelling fallback it suppressed never ran and `#[cm("…")]` was dropped. Only a resource's own method names one. `function_sigs` assembled by cloning every signature, parameter-default AST and all, where the per-module digests are shared by `Rc`. Plus two stale docs: `receiver_site`'s comment had been left above `callee_site`, and `method_sigs` still said `AstId`. The local-`impl` identity test now says it covers identity alone: `TraitEnv::build` walks a module's own items, so nothing a local block declares is dispatchable yet, which predates this branch. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/defs.rs | 4 ++ wado-compiler/src/elaborator.rs | 4 +- wado-compiler/src/elaborator/call.rs | 43 +++++++++---------- wado-compiler/src/elaborator/method_call.rs | 11 +++-- wado-compiler/src/elaborator/operators.rs | 15 ++++++- wado-compiler/src/elaborator/orchestration.rs | 2 +- wado-compiler/src/elaborator/sem/decls.rs | 5 ++- wado-compiler/src/elaborator/sig.rs | 8 ++-- .../tests/fixtures/shadow_prelude_panic.wado | 19 ++++++++ 9 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 wado-compiler/tests/fixtures/shadow_prelude_panic.wado diff --git a/wado-compiler/src/defs.rs b/wado-compiler/src/defs.rs index 96077d40a23..59a87bc9baa 100644 --- a/wado-compiler/src/defs.rs +++ b/wado-compiler/src/defs.rs @@ -718,6 +718,10 @@ mod tests { /// A local `impl` block is declared by the function that writes it, so its /// methods are identified like any other block's. + /// + /// Identity only: `TraitEnv::build` walks a module's own items, so nothing + /// a local block declares is dispatchable yet. Giving it one is what that + /// walk needs to reach `Stmt::Item`, not something this table withholds. #[test] fn a_function_local_impl_block_is_a_declaration_of_its_own() { let source = r#" diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 1357fda1067..0bacecbb5e8 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -1799,7 +1799,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { // later in the file) to infer type arguments at the call site // during body resolution, without relying on a later // monomorphization-time fallback. - let mut function_sigs: IndexMap = + let mut function_sigs: IndexMap> = IndexMap::default(); for item in &module.items { if let Item::Function(func) = item { @@ -1810,7 +1810,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .of_ast_id(func.id) .expect("every free function is a declaration"); let sig = self.record_function_sig(func); - function_sigs.insert(def, sig); + function_sigs.insert(def, Rc::new(sig)); } } for item in &module.items { diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index 1b53798f805..bc966e0d613 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -134,22 +134,25 @@ impl CalleeIdentKind<'_> { } } - /// The reference site of a qualified callee's receiver segment — the `Type` of - /// `Type::method`, which the walk answered for in the module that wrote it. + /// The reference site of the callee itself, which the walk answered for in + /// the module that wrote it — the read that says which declaration a bare + /// `name(…)` means, whichever module the walk is standing in. /// - /// Two segments exactly: consumers pair this with the prefix they split off - /// `effective_name`, and only here are the two the same segment. A namespace - /// prefix, an unqualified call and `Rewritten` all answer `None`. + /// `Rewritten` is synthesised from an already-resolved `Self::` / `T::` + /// prefix, so it names no node any walk saw. fn callee_site(&self) -> Option { match self { Self::AsIs(ident) => Some(ident.id), - // A rewritten `Self::m` spelling names no node the walk answered - // for; the qualified paths above it resolve through their own - // segments instead. Self::Rewritten(_) | Self::AbstractTypeParam { .. } => None, } } + /// The reference site of a qualified callee's receiver segment — the `Type` of + /// `Type::method`, which the walk answered for in the module that wrote it. + /// + /// Two segments exactly: consumers pair this with the prefix they split off + /// `effective_name`, and only here are the two the same segment. A namespace + /// prefix, an unqualified call and `Rewritten` all answer `None`. fn receiver_site(&self) -> Option { match self { Self::AsIs(ident) => match ident.segments.as_slice() { @@ -1355,20 +1358,6 @@ impl Elaborator<'_, H> { (None, effective_name.to_string()) } } - // Check for prelude functions (panic, unreachable). These are - // defined in core:rt and re-exported by core:prelude, and reach - // codegen by name rather than as an ordinary callee, so they answer - // ahead of the site below. - else if matches!(effective_name, "panic" | "unreachable") { - // Declarations of `core:rt`, named by the module rather than - // searched for: codegen keys them on that module, and their `!` - // return type is a signature fact like any other. - ( - self.decl_in_module(&ModuleSource::rt(), effective_name) - .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), - effective_name.to_string(), - ) - } // The call's own reference site, answered by the module that wrote it // (WEP 2026-08-12). This is what makes a parameter default name the // callee module's function: the default is written in the declaring @@ -1387,6 +1376,16 @@ impl Elaborator<'_, H> { effective_name.to_string(), ) } + // `panic` / `unreachable` where no site answered — a synthesised call, + // whose node no walk saw. A module that declares either name of its own + // is answered by the site above, so this reaches only `core:rt`'s. + else if matches!(effective_name, "panic" | "unreachable") { + ( + self.decl_in_module(&ModuleSource::rt(), effective_name) + .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), + effective_name.to_string(), + ) + } // A built-in type constructor (Ok, Err, Some, None) — a variant case, // not a function, so the site above declines it. The four names flow // through the `CompilerItem` registry so a stdlib rename is picked up diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index d2e6470b01d..fc79b64c33c 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -3665,12 +3665,15 @@ impl Elaborator<'_, H> { let param_defaults = self.lookup_static_method_param_defaults_keyed(&actual_struct_name, method_name, None); - // Propagate #[cm("...")] from resource static methods - // The selected method's own declaration knows its owner, so the - // resource is reached through it rather than through its spelling. + // Propagate #[cm("...")] from resource static methods. A method the + // *resource* declares names it as its own owner; one an `impl` block + // declares owns to the block, which names no resource, so the spelling + // answers there as it did before impl methods were identified. + let defs = std::sync::Arc::clone(self.tysys.resolutions.defs()); let cm_owner = method_ref .method_id - .and_then(|method| self.tysys.resolutions.defs().parent(method)) + .and_then(|method| defs.parent(method)) + .filter(|owner| defs.kind(*owner) == crate::defs::DefKind::Resource) .or_else(|| self.decl_key_or_local(&actual_struct_name)); let cm_name = self.lookup_resource_static_cm(cm_owner, method_name); diff --git a/wado-compiler/src/elaborator/operators.rs b/wado-compiler/src/elaborator/operators.rs index 897cb4b00a4..4fddcbaeb20 100644 --- a/wado-compiler/src/elaborator/operators.rs +++ b/wado-compiler/src/elaborator/operators.rs @@ -1977,7 +1977,20 @@ impl Elaborator<'_, H> { // instead would send an impl written on the newtype to the base's // module — and a by-name lookup is the last resort for a receiver // carrying no declaring module. - let module_source = match resolved.impl_def { + // Only a *concrete* block's function lives in the block's module: a + // generic block's post-substitution instance is materialised in the + // receiver type's module, the convention + // `TraitEnv::concrete_impl_module_for` encodes for monomorphization. + // Naming the block for one of those would send the call to a module + // that never defines it. + let concrete_impl = resolved.impl_def.filter(|def| { + self.tysys + .trait_env + .impl_headers + .get(def) + .is_some_and(|h| h.type_params.is_empty()) + }); + let module_source = match concrete_impl { Some(def) => self.tysys.resolutions.defs().module(def).clone(), None => self .impl_target_decl_key(receiver.type_id, &resolved.impl_name) diff --git a/wado-compiler/src/elaborator/orchestration.rs b/wado-compiler/src/elaborator/orchestration.rs index af9eebdc65b..1afd4fb658b 100644 --- a/wado-compiler/src/elaborator/orchestration.rs +++ b/wado-compiler/src/elaborator/orchestration.rs @@ -1430,7 +1430,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { sem.decls .function_sigs .iter() - .map(|(def, sig)| (*def, sig.clone())), + .map(|(def, sig)| (*def, Rc::clone(sig))), ); signatures.globals.insert( module_source.clone(), diff --git a/wado-compiler/src/elaborator/sem/decls.rs b/wado-compiler/src/elaborator/sem/decls.rs index b6cab5728c7..49337b59180 100644 --- a/wado-compiler/src/elaborator/sem/decls.rs +++ b/wado-compiler/src/elaborator/sem/decls.rs @@ -60,7 +60,8 @@ pub(crate) struct ModuleDecls { /// Canonical signatures of this module's own free functions, frozen /// behind `Rc` so the program-wide assembly and the stdlib-snapshot /// seeding share the map instead of deep-cloning every signature. - pub(crate) function_sigs: std::rc::Rc>, + pub(crate) function_sigs: + std::rc::Rc>>, /// `func_name → return TypeId` for functions defined in this module. pub(crate) function_return_types: IndexMap, /// Names visible via `use` declarations in this module (the union of @@ -82,7 +83,7 @@ pub(crate) struct ModuleDecls { pub(crate) associated_constants: IndexMap<(crate::defs::DefId, String), (ModuleSource, TypeId, ast::Expr)>, /// Canonical signatures of this module's method declarations, keyed by - /// the method's globally-unique `AstId`. + /// the method's declaration. /// /// An `impl` method is resolved in the impl's frame — impl type params /// in their positional slots, the method's own after them, `Self` bound diff --git a/wado-compiler/src/elaborator/sig.rs b/wado-compiler/src/elaborator/sig.rs index 85a55706d7e..d9905bd8fc8 100644 --- a/wado-compiler/src/elaborator/sig.rs +++ b/wado-compiler/src/elaborator/sig.rs @@ -16,8 +16,10 @@ use super::sem::decls::FunctionSig; /// associated-const values, `__DATA__`. Assembled from `ModuleDecls` digests. #[derive(Default)] pub(crate) struct Signatures { - /// Canonical free-function signatures, keyed by the declaration. - pub(crate) function_sigs: IndexMap, + /// Canonical free-function signatures, keyed by the declaration. The + /// entries are shared with the per-module digests they are assembled from + /// rather than copied — a signature carries its parameter defaults' AST. + pub(crate) function_sigs: IndexMap>, /// Canonical method signatures, keyed by the method's [`crate::defs::DefId`] /// — `impl`-block methods and `interface` / `resource` operations alike. @@ -56,7 +58,7 @@ pub(crate) struct Signatures { impl Signatures { /// Canonical signature of the free function `def` declares. pub(crate) fn function_sig(&self, def: crate::defs::DefId) -> Option<&FunctionSig> { - self.function_sigs.get(&def) + self.function_sigs.get(&def).map(Rc::as_ref) } /// Canonical signature of the method `def` declares. diff --git a/wado-compiler/tests/fixtures/shadow_prelude_panic.wado b/wado-compiler/tests/fixtures/shadow_prelude_panic.wado new file mode 100644 index 00000000000..7256b58c302 --- /dev/null +++ b/wado-compiler/tests/fixtures/shadow_prelude_panic.wado @@ -0,0 +1,19 @@ +// `panic` and `unreachable` are prelude functions, and a module's own +// declaration outranks the prelude — the call's reference site says so, and +// dispatch takes that answer rather than reaching for `core:rt` by name. + +fn panic(msg: String) -> i32 { + return 7; +} + +fn unreachable() -> i32 { + return 9; +} + +test "a module's own panic outranks the prelude's" { + assert panic("x") == 7; +} + +test "a module's own unreachable outranks the prelude's" { + assert unreachable() == 9; +} From 8c8f7cfbca0810ca95a0e2e9d59bdd76bf532d15 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 08:58:09 +0000 Subject: [PATCH 09/25] refactor(elaborator): ask the declaration table once per question MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `of_ast_id(node).expect("every … is a declaration")` had grown to seventeen sites under eight wordings, all asking the one question `DefTable::def_at` now answers — and its panic names the node, which none of the eight did. `CalleeRef::declared(resolutions.defs(), …)` likewise: `callee_of` takes the declaration and `callee_in_module` the two-step behind it, so no call site threads the table to build a callee. The rest is comments the code says for itself, and one duplicated lookup: `effect_ops` re-derived the owner two lines under the binding that already held it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/defs.rs | 23 ++++--- wado-compiler/src/elaborator.rs | 67 ++++++++----------- wado-compiler/src/elaborator/call.rs | 64 ++++++++---------- wado-compiler/src/elaborator/callee.rs | 15 ++--- wado-compiler/src/elaborator/expr.rs | 3 +- wado-compiler/src/elaborator/item.rs | 66 +++++------------- wado-compiler/src/elaborator/method_call.rs | 13 +--- wado-compiler/src/elaborator/method_lookup.rs | 6 +- wado-compiler/src/elaborator/operators.rs | 21 ++---- wado-compiler/src/elaborator/orchestration.rs | 16 ++--- wado-compiler/src/elaborator/sem/decls.rs | 6 +- wado-compiler/src/elaborator/synth.rs | 3 +- wado-compiler/src/elaborator/trait_env.rs | 37 ++++------ wado-compiler/src/elaborator/trait_query.rs | 19 ++---- wado-compiler/src/elaborator/types.rs | 16 ++--- wado-compiler/src/resolve.rs | 3 +- wado-compiler/src/tir.rs | 5 +- 17 files changed, 136 insertions(+), 247 deletions(-) diff --git a/wado-compiler/src/defs.rs b/wado-compiler/src/defs.rs index 59a87bc9baa..7868e75ab83 100644 --- a/wado-compiler/src/defs.rs +++ b/wado-compiler/src/defs.rs @@ -225,8 +225,6 @@ impl DefTable { members: Vec::new(), }); } - // An `impl` block is a declaration the symbol table never - // collected, since it writes no name to collect it under. Item::Impl(block) => table.declare_impl_block(module, block, false), _ => {} } @@ -265,12 +263,8 @@ impl DefTable { } } - /// Identify an `impl` block, which no symbol table row names. - /// - /// Its name is empty because it writes none: `name` is a rendering, and an - /// `impl` block has nothing to render. That is also what keeps it out of - /// every scope — a name-keyed layer can only be reached by a spelling, and - /// no spelling reaches this. + /// Identify an `impl` block, which no symbol table row names. Its name is + /// empty because it writes none, and no spelling reaches what has none. fn declare_impl_block( &mut self, module: &ModuleSource, @@ -490,6 +484,14 @@ impl DefTable { /// nothing. /// /// This is not a name lookup: the node is already the declaration. + /// [`Self::of_ast_id`] for a node the collect pass identified, so a miss is + /// a hole in that pass rather than a case to handle. + #[must_use] + pub fn def_at(&self, id: AstId) -> DefId { + self.of_ast_id(id) + .unwrap_or_else(|| panic!("{id:?} declares nothing")) + } + #[must_use] pub fn of_ast_id(&self, id: AstId) -> Option { self.by_ast_id.get(&id).copied() @@ -699,7 +701,10 @@ mod tests { impl Line { pub fn len(&self) -> i32 { return self.a; } } "#; let (defs, module) = build_from_source(source); - let blocks: Vec = defs.iter().filter(|d| defs.kind(*d) == DefKind::Impl).collect(); + let blocks: Vec = defs + .iter() + .filter(|d| defs.kind(*d) == DefKind::Impl) + .collect(); assert_eq!(blocks.len(), 2); assert_ne!(blocks[0], blocks[1]); // No spelling reaches an `impl` block, so it renders none. diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 0bacecbb5e8..2750b5073cd 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -434,12 +434,9 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { self.insert_reference(use_id, def_id); } - /// The free function the reference site `site` names, or `None` where it - /// names something else — a binder, a variant case, a node no walk saw. - /// - /// One read replaces the "this module, then the import, then the callee's - /// scope" tier order each caller used to spell for itself: the site was - /// answered once, by the module that wrote it (WEP 2026-08-12). + /// The free function the reference site `site` names, answered by the + /// module that wrote it (WEP 2026-08-12). `None` where it names something + /// else — a binder, a variant case, a node no walk saw. pub(super) fn free_function_at(&self, site: crate::ast::AstId) -> Option { let def = self.tysys.resolutions.declared_if_walked(site)?; (self.tysys.resolutions.defs().kind(def) == crate::defs::DefKind::Function).then_some(def) @@ -450,14 +447,14 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { &self, site: crate::ast::AstId, ) -> Option<&sem::decls::FunctionSig> { - self.tysys.signatures.function_sig(self.free_function_at(site)?) + self.tysys + .signatures + .function_sig(self.free_function_at(site)?) } - /// The declaration `module` declares under `name`. - /// - /// The module is named rather than searched for: a qualified path spelled - /// it. For the two positions no reference site answers — `builtin::f`, and - /// a member of a namespace import. + /// The declaration `module` declares under `name`, for the positions no + /// reference site answers: `builtin::f`, a namespace member, `core:rt`'s + /// `panic`. The module is named by the path, not searched for. pub(super) fn decl_in_module( &self, module: &ModuleSource, @@ -468,11 +465,19 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .and_then(|sym| self.tysys.resolutions.defs().of_ast_id(sym.defined_at)) } - /// Record a use→def edge naming the declaration `def`. - /// - /// The edge map is keyed by node on both sides — navigation recovers a - /// def's module from its id space — so the declaring node is read off the - /// identity here rather than carried beside it. + /// [`Self::decl_in_module`] as a callee identity. + fn callee_in_module(&self, module: &ModuleSource, name: &str) -> Option { + Some(self.callee_of(self.decl_in_module(module, name)?)) + } + + /// The callee identity of the declaration `def`. + fn callee_of(&self, def: crate::defs::DefId) -> callee::CalleeRef { + callee::CalleeRef::declared(self.tysys.resolutions.defs(), def) + } + + /// Record a use→def edge naming the declaration `def`. The map is keyed by + /// node on both sides, so the declaring node is read off the identity here + /// rather than carried beside it. pub(super) fn record_reference_to_decl( &mut self, use_id: crate::ast::AstId, @@ -1774,23 +1779,15 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .flatten(); let ops = self.resolve_effect_ops(&type_params, &methods, resource_self); let defs = std::sync::Arc::clone(self.tysys.resolutions.defs()); - let owner = defs - .of_ast_id(decl_id) - .expect("every interface / resource declaration is a declaration"); + let owner = defs.def_at(decl_id); for method in &methods { - let op = defs - .of_ast_id(method.id) - .expect("every declared operation is a declaration"); + let op = defs.def_at(method.id); self.sem .decls .resource_method_ids .insert((owner, method.name.clone()), op); } - self.sem.decls.effect_ops.insert( - defs.of_ast_id(decl_id) - .expect("every interface / resource declaration is a declaration"), - ops, - ); + self.sem.decls.effect_ops.insert(owner, ops); } // Pre-populate the generic-function inference caches for every @@ -1803,12 +1800,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { IndexMap::default(); for item in &module.items { if let Item::Function(func) = item { - let def = self - .tysys - .resolutions - .defs() - .of_ast_id(func.id) - .expect("every free function is a declaration"); + let def = self.tysys.resolutions.defs().def_at(func.id); let sig = self.record_function_sig(func); function_sigs.insert(def, Rc::new(sig)); } @@ -2091,12 +2083,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { for method in &impl_block.methods { // Records-only: reify emits the method `TirFunction` // from the recorded signature facts + the AST. - let method_def = scope - .tysys - .resolutions - .defs() - .of_ast_id(method.id) - .expect("every impl method is a declaration"); + let method_def = scope.tysys.resolutions.defs().def_at(method.id); let recorded_sig = scope .tysys .signatures diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index bc966e0d613..8ad19d3a8c5 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -134,12 +134,9 @@ impl CalleeIdentKind<'_> { } } - /// The reference site of the callee itself, which the walk answered for in - /// the module that wrote it — the read that says which declaration a bare - /// `name(…)` means, whichever module the walk is standing in. - /// - /// `Rewritten` is synthesised from an already-resolved `Self::` / `T::` - /// prefix, so it names no node any walk saw. + /// The reference site of the callee itself, which says which declaration a + /// bare `name(…)` means. `Rewritten` is synthesised from an already-resolved + /// `Self::` / `T::` prefix, so no walk saw it. fn callee_site(&self) -> Option { match self { Self::AsIs(ident) => Some(ident.id), @@ -501,8 +498,11 @@ impl Elaborator<'_, H> { let receiver_site = callee_kind.receiver_site(); // First, determine expected parameter types to handle coercion. - let (mut param_types, callee_slots) = - self.lookup_function_signature(effective_name, receiver_site, callee_kind.callee_site()); + let (mut param_types, callee_slots) = self.lookup_function_signature( + effective_name, + receiver_site, + callee_kind.callee_site(), + ); // Instantiate the callee's slots before an argument is resolved // against one of its parameter types. A rigid slot is the callee's @@ -677,8 +677,7 @@ impl Elaborator<'_, H> { // Builtin functions: resolve through core:builtin module if prefix == "builtin" { ( - self.decl_in_module(&ModuleSource::builtin(), suffix) - .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), + self.callee_in_module(&ModuleSource::builtin(), suffix), effective_name.to_string(), ) } @@ -1327,8 +1326,7 @@ impl Elaborator<'_, H> { } } ( - self.decl_in_module(&ns_source, suffix) - .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), + self.callee_in_module(&ns_source, suffix), effective_name.to_string(), ) } @@ -1359,30 +1357,24 @@ impl Elaborator<'_, H> { } } // The call's own reference site, answered by the module that wrote it - // (WEP 2026-08-12). This is what makes a parameter default name the - // callee module's function: the default is written in the declaring - // module and walked from the call site, so a tier order over the - // *walking* module's names answers with the caller's same-named - // declaration instead. - else if let Some(callee) = self - .tysys - .resolutions - .declared_if_walked(ident.id) - .filter(|def| self.tysys.resolutions.defs().kind(*def) == crate::defs::DefKind::Function) + // (WEP 2026-08-12) — not by the module the walk is standing in, which + // for a parameter default is the caller's. + else if let Some(callee) = + self.tysys + .resolutions + .declared_if_walked(ident.id) + .filter(|def| { + self.tysys.resolutions.defs().kind(*def) == crate::defs::DefKind::Function + }) { self.record_reference_to_decl(ident.id, callee); - ( - Some(CalleeRef::declared(self.tysys.resolutions.defs(), callee)), - effective_name.to_string(), - ) + (Some(self.callee_of(callee)), effective_name.to_string()) } - // `panic` / `unreachable` where no site answered — a synthesised call, - // whose node no walk saw. A module that declares either name of its own - // is answered by the site above, so this reaches only `core:rt`'s. + // `panic` / `unreachable` where no site answered — a synthesised call. + // A module declaring either name of its own is answered above. else if matches!(effective_name, "panic" | "unreachable") { ( - self.decl_in_module(&ModuleSource::rt(), effective_name) - .map(|def| CalleeRef::declared(self.tysys.resolutions.defs(), def)), + self.callee_in_module(&ModuleSource::rt(), effective_name), effective_name.to_string(), ) } @@ -1690,7 +1682,10 @@ impl Elaborator<'_, H> { effect: crate::defs::DefId, operation: &str, ) -> Option<(Vec, Option)> { - let sig = self.tysys.signatures.resource_method_sig(effect, operation)?; + let sig = self + .tysys + .signatures + .resource_method_sig(effect, operation)?; Some((sig.decl.param_types.clone(), sig.decl.return_type)) } @@ -1795,9 +1790,8 @@ impl Elaborator<'_, H> { return (Vec::new(), Vec::new()); } - // The callee's own site, answered by the module that wrote it — which - // covers this module's functions, its imports under either spelling, - // and a default expression's callee scope, all at once. + // One read for this module's functions, its imports under either + // spelling, and a default expression's callee scope. let Some(sig) = callee_site.and_then(|site| self.free_function_sig_at(site)) else { return (Vec::new(), Vec::new()); }; diff --git a/wado-compiler/src/elaborator/callee.rs b/wado-compiler/src/elaborator/callee.rs index d7f137ff8d2..d05d3466012 100644 --- a/wado-compiler/src/elaborator/callee.rs +++ b/wado-compiler/src/elaborator/callee.rs @@ -1,15 +1,11 @@ -//! Resolved call-target identities. A free function callee is the declaration -//! it names (WEP 2026-08-12); a static method callee bundles the receiver and -//! method names dispatch picked, alongside the method's own declaration. +//! Resolved call-target identities: a free function callee is the declaration +//! it names (WEP 2026-08-12), a static method callee what dispatch picked. use crate::module_source::{ModuleSource, ModuleSourceInterner}; -/// Identity of a free function callee. -/// -/// `Declared` is the ordinary case: the declaration, plus the module and name -/// its one constructor reads off the table, so a consumer emitting TIR needs no -/// table at hand while only `def` says which declaration this is. Nothing reads -/// the rendering back into an identity. +/// Identity of a free function callee. `Declared` carries the module and name +/// its one constructor reads off the table, so TIR emission needs none at hand; +/// only `def` says which declaration this is, and nothing reads a name back. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(super) enum CalleeRef { Declared { @@ -33,7 +29,6 @@ impl CalleeRef { } } - /// A callee with no declaration behind it. See [`Self::Rendered`]. pub fn rendered(module: ModuleSource, name: impl Into) -> Self { Self::Rendered { module, diff --git a/wado-compiler/src/elaborator/expr.rs b/wado-compiler/src/elaborator/expr.rs index 87a65c16c3a..6120d1de751 100644 --- a/wado-compiler/src/elaborator/expr.rs +++ b/wado-compiler/src/elaborator/expr.rs @@ -953,8 +953,7 @@ impl Elaborator<'_, H> { ) -> TypeId { self.record_item_reference_by_name(ident.id, &ident.name); - let Some((sig, _def_module, _defining_name)) = self.lookup_func_sig_for_ref(ident) - else { + let Some((sig, _def_module, _defining_name)) = self.lookup_func_sig_for_ref(ident) else { // Fallback: known function but its signature is unreachable // (shouldn't normally happen). Emit a stub FuncRef so downstream // stays sane. diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index 2fb5cf4d341..e32c5e6767d 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -888,12 +888,7 @@ impl TypeParamScope<'_, '_, H> { .self_type .expect("entering an impl frame binds Self to the target"); - let impl_def = scope - .tysys - .resolutions - .defs() - .of_ast_id(impl_block.id) - .expect("every impl block is a declaration"); + let impl_def = scope.tysys.resolutions.defs().def_at(impl_block.id); scope.sem.decls.impl_sigs.insert( impl_def, super::sig::ImplSig { @@ -1264,12 +1259,7 @@ impl Elaborator<'_, H> { .first() .map(|p| p.self_kind) .unwrap_or(ast::SelfKind::None); - let method_def = frame_scope - .tysys - .resolutions - .defs() - .of_ast_id(method.id) - .expect("every impl method is a declaration"); + let method_def = frame_scope.tysys.resolutions.defs().def_at(method.id); frame_scope.sem.decls.method_sigs.insert( method_def, MethodSig { @@ -1519,12 +1509,7 @@ impl Elaborator<'_, H> { /// Operation signatures the decl pass recorded for the declaration at /// `decl_id`. fn declared_effect_ops(&self, decl_id: ast::AstId) -> Vec { - let decl = self - .tysys - .resolutions - .defs() - .of_ast_id(decl_id) - .expect("every interface / resource declaration is a declaration"); + let decl = self.tysys.resolutions.defs().def_at(decl_id); self.sem .decls .effect_ops @@ -1631,12 +1616,7 @@ impl Elaborator<'_, H> { let mut type_params = decl_slots.clone(); type_params.extend(method_slots); - let method_def = method_scope - .tysys - .resolutions - .defs() - .of_ast_id(method.id) - .expect("every trait method is a declaration"); + let method_def = method_scope.tysys.resolutions.defs().def_at(method.id); methods.insert( method.name.clone(), super::sig::TraitMethod { @@ -1679,12 +1659,7 @@ impl Elaborator<'_, H> { } let module = scope.current_module_source.clone(); - let trait_def = scope - .tysys - .resolutions - .defs() - .of_ast_id(trait_decl.id) - .expect("every trait declaration is a declaration"); + let trait_def = scope.tysys.resolutions.defs().def_at(trait_decl.id); scope .sem .decls @@ -1855,12 +1830,7 @@ impl Elaborator<'_, H> { } else { SelfKind::None }; - let method_def = scope - .tysys - .resolutions - .defs() - .of_ast_id(method.id) - .expect("every declared operation is a declaration"); + let method_def = scope.tysys.resolutions.defs().def_at(method.id); scope.sem.decls.method_sigs.insert( method_def, MethodSig { @@ -2224,12 +2194,7 @@ impl Elaborator<'_, H> { /// re-resolution. Returns the declared return type for callers that /// need it (`resolve_function`'s `task_return_type`). fn populate_generic_function_cache(&mut self, func: &Function) -> TypeId { - let def = self - .tysys - .resolutions - .defs() - .of_ast_id(func.id) - .expect("every free function is a declaration"); + let def = self.tysys.resolutions.defs().def_at(func.id); let sig = self .sem .decls @@ -2661,15 +2626,14 @@ impl Elaborator<'_, H> { (None, Some(d)) => (Some(d), true), (None, None) => (None, false), }; - let async_op = decl_ref - .and_then(|decl| { - scope - .tysys - .signatures - .resource_method_sig(decl, &func.name) - .filter(|op| op.is_async) - .map(|op| op.cm_name.is_some()) - }); + let async_op = decl_ref.and_then(|decl| { + scope + .tysys + .signatures + .resource_method_sig(decl, &func.name) + .filter(|op| op.is_async) + .map(|op| op.cm_name.is_some()) + }); if let Some(cm_backed) = async_op && (is_resource_effect || !cm_backed) { diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index fc79b64c33c..d37600d107a 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -2422,12 +2422,7 @@ impl Elaborator<'_, H> { // declares the method. The header alone would match an instance // method of the same name; both indices together will not. .filter(|(_, b)| { - let Some(header) = self - .tysys - .trait_env - .impl_headers - .get(&b.def) - else { + let Some(header) = self.tysys.trait_env.impl_headers.get(&b.def) else { return false; }; self.tysys @@ -2447,11 +2442,7 @@ impl Elaborator<'_, H> { // The trait comes off the impl's own header, so the blanket // index's bare-name key never reaches a mangled name. .filter_map(|(_, b)| { - let header = self - .tysys - .trait_env - .impl_headers - .get(&b.def)?; + let header = self.tysys.trait_env.impl_headers.get(&b.def)?; Some(( self.tysys .trait_env diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index 41fef1a2853..f3d587a6d0b 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -3224,11 +3224,7 @@ impl Elaborator<'_, H> { // the indexing types come from the impl's associated-type // bindings. let method_header = header.methods.iter().find(|m| m.name == method_name)?; - let self_kind = s - .tysys - .signatures - .method_sig(method_header.def)? - .self_kind; + let self_kind = s.tysys.signatures.method_sig(method_header.def)?.self_kind; let impl_source = s.impl_block_module_source(impl_ref); let assoc_type = impl_sig diff --git a/wado-compiler/src/elaborator/operators.rs b/wado-compiler/src/elaborator/operators.rs index 4fddcbaeb20..768bba16726 100644 --- a/wado-compiler/src/elaborator/operators.rs +++ b/wado-compiler/src/elaborator/operators.rs @@ -1967,22 +1967,13 @@ impl Elaborator<'_, H> { ); method_info.is_type_param_receiver = resolved.is_type_param_receiver; - // Where a block was matched, the module is its own — read off the - // declaration dispatch selected rather than derived from the receiver. - // - // The rest name no block: an auto-derived `Eq` / `Ord`, and a method - // reached through a type parameter's bound, whose block - // monomorphization picks. There the receiver's newtype chain answers, - // keyed on the link the lookup was made on — peeling to the base - // instead would send an impl written on the newtype to the base's - // module — and a by-name lookup is the last resort for a receiver - // carrying no declaring module. // Only a *concrete* block's function lives in the block's module: a - // generic block's post-substitution instance is materialised in the - // receiver type's module, the convention - // `TraitEnv::concrete_impl_module_for` encodes for monomorphization. - // Naming the block for one of those would send the call to a module - // that never defines it. + // generic block's instance is materialised in the receiver type's, + // the convention `TraitEnv::concrete_impl_module_for` encodes. + // + // Everything else answers from the receiver's newtype chain, keyed on + // the link the lookup was made on — peeling to the base would send an + // impl written on the newtype to the base's module. let concrete_impl = resolved.impl_def.filter(|def| { self.tysys .trait_env diff --git a/wado-compiler/src/elaborator/orchestration.rs b/wado-compiler/src/elaborator/orchestration.rs index 1afd4fb658b..5b8ea7ec23d 100644 --- a/wado-compiler/src/elaborator/orchestration.rs +++ b/wado-compiler/src/elaborator/orchestration.rs @@ -3508,17 +3508,11 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { _ => continue, }; - // The target is named at the header's own site, which the - // module that wrote it answered for — the same way - // `trait_key` above is read. Looking it up by the written - // name instead reached whichever module declares that - // spelling first, a build-order pick rather than an answer. - // - // Rendered back to the declaring node because this key is - // still `AstId`-shaped: its readers arrive through - // `decl_of_type`, which also answers for monomorphized - // instances and `BuiltinArray`, neither of which carries a - // `DefId`. + // The target is named at the header's own site, like + // `trait_key` above. Rendered back to the declaring node + // because the key stays `AstId`-shaped: its readers arrive + // through `decl_of_type`, which also answers for + // monomorphized instances and `BuiltinArray`. let base_decl = crate::resolve::head_site(&impl_block.ty) .and_then(|site| resolutions.declared(site)) .map(|def| resolutions.defs().ast_id(def)); diff --git a/wado-compiler/src/elaborator/sem/decls.rs b/wado-compiler/src/elaborator/sem/decls.rs index 49337b59180..f291d69d9c8 100644 --- a/wado-compiler/src/elaborator/sem/decls.rs +++ b/wado-compiler/src/elaborator/sem/decls.rs @@ -60,8 +60,7 @@ pub(crate) struct ModuleDecls { /// Canonical signatures of this module's own free functions, frozen /// behind `Rc` so the program-wide assembly and the stdlib-snapshot /// seeding share the map instead of deep-cloning every signature. - pub(crate) function_sigs: - std::rc::Rc>>, + pub(crate) function_sigs: std::rc::Rc>>, /// `func_name → return TypeId` for functions defined in this module. pub(crate) function_return_types: IndexMap, /// Names visible via `use` declarations in this module (the union of @@ -93,8 +92,7 @@ pub(crate) struct ModuleDecls { pub(crate) method_sigs: IndexMap, /// A declaration paired with an operation name → the operation, so a /// caller holding only a name reaches its `method_sigs` entry. - pub(crate) resource_method_ids: - IndexMap<(crate::defs::DefId, String), crate::defs::DefId>, + pub(crate) resource_method_ids: IndexMap<(crate::defs::DefId, String), crate::defs::DefId>, /// Facts of this module's `impl` blocks that belong to the block rather /// than to one method — its target and trait type arguments and its /// associated-type bindings — resolved once in the block's own frame and diff --git a/wado-compiler/src/elaborator/synth.rs b/wado-compiler/src/elaborator/synth.rs index 69ce452f105..c9ed624c935 100644 --- a/wado-compiler/src/elaborator/synth.rs +++ b/wado-compiler/src/elaborator/synth.rs @@ -643,8 +643,7 @@ impl Elaborator<'_, H> { if ident.name.contains("::") { return None; } - let def = self.free_function_at(ident.id)?; - Some(CalleeRef::declared(self.tysys.resolutions.defs(), def)) + Some(self.callee_of(self.free_function_at(ident.id)?)) } fn synth_method_call( diff --git a/wado-compiler/src/elaborator/trait_env.rs b/wado-compiler/src/elaborator/trait_env.rs index a9673fac237..ca3de03a2cc 100644 --- a/wado-compiler/src/elaborator/trait_env.rs +++ b/wado-compiler/src/elaborator/trait_env.rs @@ -401,18 +401,17 @@ pub(super) struct ImplMethodHeader { pub(super) param_count: usize, } -/// Digest each method a `trait` or `impl` block declares, for its header. -/// -/// One producer, so a trait's methods and an impl's are digested the same way -/// and cannot disagree about what a header says. -fn method_headers(defs: &crate::defs::DefTable, methods: &[ast::Function]) -> Vec { +/// Digest each method a `trait` or `impl` block declares. One producer, so the +/// two cannot disagree about what a header says. +fn method_headers( + defs: &crate::defs::DefTable, + methods: &[ast::Function], +) -> Vec { methods .iter() .map(|m| ImplMethodHeader { name: m.name.clone(), - def: defs - .of_ast_id(m.id) - .expect("every declared method is a declaration"), + def: defs.def_at(m.id), type_params: m.type_params.clone(), span: m.span, name_span: m.name_span, @@ -1104,11 +1103,7 @@ impl TraitEnv { let Item::Impl(impl_block) = item else { continue; }; - // Every loaded module's `impl` blocks are identified by - // `DefTable::build`, so a miss means the two walks diverged. - let impl_def = defs - .of_ast_id(impl_block.id) - .expect("every impl block is a declaration"); + let impl_def = defs.def_at(impl_block.id); let type_key = impl_target_key_at(&impl_block.ty, module_source, resolutions); let trait_ref: Option = impl_block .trait_type @@ -1206,9 +1201,7 @@ impl TraitEnv { .or_default() .push(StaticMethodEntry { name: method.name.clone(), - method_id: defs - .of_ast_id(method.id) - .expect("every impl method is a declaration"), + method_id: defs.def_at(method.id), }); } } @@ -1227,9 +1220,7 @@ impl TraitEnv { .or_default() .push(StaticMethodEntry { name: method.name.clone(), - method_id: defs - .of_ast_id(method.id) - .expect("every impl method is a declaration"), + method_id: defs.def_at(method.id), }); } } @@ -1475,10 +1466,7 @@ impl TraitEnv { /// Collected form of [`Self::entries_by_receiver`], for callers that need /// to iterate the widened match more than once. - pub(crate) fn entries_by_receiver_vec( - &self, - receiver: &name::Receiver, - ) -> Vec { + pub(crate) fn entries_by_receiver_vec(&self, receiver: &name::Receiver) -> Vec { self.entries_by_receiver(receiver).collect() } @@ -1513,8 +1501,7 @@ impl TraitEnv { module_source: &ModuleSource, ) -> bool { self.entries_by_receiver(receiver).any(|entry| { - self.defs.module(entry) == module_source - && self.methodful_header_matches(entry, trait_) + self.defs.module(entry) == module_source && self.methodful_header_matches(entry, trait_) }) } diff --git a/wado-compiler/src/elaborator/trait_query.rs b/wado-compiler/src/elaborator/trait_query.rs index c1388112096..3c407630729 100644 --- a/wado-compiler/src/elaborator/trait_query.rs +++ b/wado-compiler/src/elaborator/trait_query.rs @@ -1061,14 +1061,12 @@ impl TypeSystem { // A receiverless method has no receiver to deref, so `&T` inherits it // by forwarding — which works only where `Self` is absent from the // signature: `kind() -> String` forwards, `-> Option` cannot. - trait_sig_of_with(trait_, &self.trait_env, &self.signatures).is_some_and( - |sig| { - sig.methods.values().any(|m| { - m.sig.self_kind == crate::ast::SelfKind::None - && self.receiverless_method_mentions_self(&m.sig) - }) - }, - ) + trait_sig_of_with(trait_, &self.trait_env, &self.signatures).is_some_and(|sig| { + sig.methods.values().any(|m| { + m.sig.self_kind == crate::ast::SelfKind::None + && self.receiverless_method_mentions_self(&m.sig) + }) + }) } /// Whether a receiverless method's signature names `Self` — in a parameter, @@ -2434,10 +2432,7 @@ impl Elaborator<'_, H> { let blanket_infos: Vec = { let mut result = vec![]; for blanket in trait_env.blanket_impls.get(&trait_).into_iter().flatten() { - let Some(header) = trait_env - .impl_headers - .get(&blanket.def) - else { + let Some(header) = trait_env.impl_headers.get(&blanket.def) else { continue; }; if header.associated_types.is_empty() { diff --git a/wado-compiler/src/elaborator/types.rs b/wado-compiler/src/elaborator/types.rs index 4f074d3eda2..2ca10b40464 100644 --- a/wado-compiler/src/elaborator/types.rs +++ b/wado-compiler/src/elaborator/types.rs @@ -1676,11 +1676,10 @@ impl MethodOwner { #[derive(Debug, Clone)] pub(super) struct MethodInfo { - /// The method this lookup selected, taken from its - /// [`crate::elaborator::sig::MethodSig`]. The use→def edge for a call is - /// recorded from here, so it names the impl dispatch actually chose. - /// `None` where no declaration backs the signature: the tuple builtins, - /// an auto-derived `Eq` / `Ord`, and the error-recovery placeholder. + /// The method this lookup selected. A call's use→def edge is recorded from + /// here, so it names the impl dispatch chose. `None` where no declaration + /// backs the signature: tuple builtins, auto-derived `Eq` / `Ord`, the + /// error-recovery placeholder. pub(super) method_def: Option, pub(super) return_type: TypeId, pub(super) self_kind: ast::SelfKind, @@ -2465,10 +2464,9 @@ pub(super) struct ResolvedTraitMethod { pub(super) trait_name: crate::name::FqTraitName, /// Method name (e.g., "eq", "cmp", "add", "shl", "neg", "bitnot"). pub(super) method_name: String, - /// The `impl` block dispatch selected, where one was matched. `None` - /// where no block is named: an auto-derived `Eq` / `Ord`, and a method - /// reached through a type parameter's bound, whose block monomorphization - /// picks. + /// The `impl` block dispatch matched. `None` where none is named: an + /// auto-derived `Eq` / `Ord`, and a method reached through a type + /// parameter's bound, whose block monomorphization picks. pub(super) impl_def: Option, /// Written name of the type whose impl matched — the impl-index key. For /// newtypes this may be the ultimate base-type name when dispatch falls diff --git a/wado-compiler/src/resolve.rs b/wado-compiler/src/resolve.rs index 219b9a14b46..65fbdab953f 100644 --- a/wado-compiler/src/resolve.rs +++ b/wado-compiler/src/resolve.rs @@ -471,8 +471,7 @@ impl AstVisitor for Resolver<'_> { fn visit_block(&mut self, block: &ast::Block) { let mut scope = IndexMap::default(); for stmt in &block.stmts { - // A local `impl` block writes no name for the scope to hold — its - // methods reach their receiver's type, never this map. + // A local `impl` block writes no name for the scope to hold. if let ast::Stmt::Item(item) = stmt && !matches!(**item, ast::Item::Impl(_)) && let Some(def) = self.defs.of_ast_id(item.id()) diff --git a/wado-compiler/src/tir.rs b/wado-compiler/src/tir.rs index f32c05bd49a..506c89f66e9 100644 --- a/wado-compiler/src/tir.rs +++ b/wado-compiler/src/tir.rs @@ -1735,10 +1735,7 @@ impl TypeTable { .compiler_items .struct_decl(item) .expect("a registered struct item records its declaring node"); - let def = self - .defs - .of_ast_id(decl) - .expect("a compiler item's declaring node is a declaration"); + let def = self.defs.def_at(decl); crate::name::FqTypeName::declared(&self.defs, def) } From ed1b22025fe85cc85fff7817b946291b02489a7a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 09:02:03 +0000 Subject: [PATCH 10/25] docs(compiler): name the phases `elaborate` actually runs The pipeline table merged everything from analyze to TIR emission into one `Annotate | TIR + facts` row, so nothing pointed at resolve, liveness or reify, and `TirModule` was attributed to annotate. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- docs/compiler.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index 1334b93eec3..c68993aeb4a 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -11,7 +11,8 @@ The Wado compiler (`wado-compiler/`) translates `.wado` source into a Wasm compo ``` Source (.wado) → Lex → Parse → Bind (per module, in loader) - → Annotate (Analyze + Resolve + lower TIR) + → Analyze → Resolve + → Annotate (decls, then bodies) → Liveness → Reify (TIR) → Default-purity Check → Synthesis (auto-derives, template, From, serde, pre-CM effect dispatch, CM bindings) → Effect Check → Stores Check @@ -31,7 +32,11 @@ The driver is `compile_after_load` in `src/lib.rs`. | Lex / Parse | AST | `lexer.rs`, `parser.rs`, `token.rs`, `syntax.rs` | | Bind | AST + bindings | `bind.rs` | | Loader | All modules | `loader.rs` | -| Annotate | TIR + facts | `semantics.rs`, `analyze.rs`, `elaborator/` | +| Analyze | `SymbolTable` | `analyze.rs` | +| Resolve | Declarations | `defs.rs`, `resolve.rs` | +| Annotate | `Semantics` | `semantics.rs`, `elaborator/` | +| Liveness | Reachability | `elaborator/liveness.rs` | +| Reify | `TirModule` | `elaborator/reify.rs` | | Default-purity Check | (validation) | `effect_check.rs::check_default_purity` | | Synthesis | TIR (extended) | `synthesis/` | | Effect / Stores | TIR (validated) | `effect_check.rs` | @@ -51,7 +56,7 @@ The driver is `compile_after_load` in `src/lib.rs`. | Unit | Layer | | --------------------------------- | ------------------------------------------------------------------------------------------------------------ | | `Module` (`ast.rs`) | Surface AST. Preserves source-level syntax to support `wado format`. | -| `TirModule` (`tir.rs`) | Typed IR. One per source module after annotate. | +| `TirModule` (`tir.rs`) | Typed IR. One per source module, emitted by reify. | | `Package` (`package.rs`) | Per-module compilation context, used from synthesis through link. | | `FlatPackage` (`flat_package.rs`) | Flat list of all functions, types, and globals; used from monomorphize through the lower pipeline's planner. | | `NirPackage` (`nir_package.rs`) | Normalized IR. Output of lower, input to optimize / WIR build / codegen. | @@ -69,9 +74,16 @@ The loader runs `lexer → parser → bind` on every loaded module: The AST is parser-immutable from this point on. The desugar-replacement surface rewrites — compound assignment (`x += y` → `x = x + y`), `while` / C-style `for` → explicit `loop`, `for x of expr` iteration (the `.into_iter()` / `.next()` dispatch and the `match Some(x) => body, _ => break` shape), the `assert` statement, the `matches` operator, the comparison chain `a < b < c`, template-string interpolations, `use … namespace` prefix stripping (`helper::foo`), and `Self::method` / `T::method` (T bound to concrete) static-call dispatch — happen inside the elaborator and are built TIR-direct: each rewrite resolves the user AST and constructs `TirExpr` / `TirStmt` nodes directly without producing synthetic AST. The implementations live in `elaborator/{stmt,operators,assert,matches}.rs` (`resolve_while`, `resolve_for`, `resolve_iterator_for_of`, `resolve_compound_assign`, `desugar_assert`, `desugar_matches_expr`, `desugar_comparison_chain`), `Elaborator::strip_ns_prefix` in `elaborator.rs`, and `CalleeIdentKind` / `classify_call_callee` in `elaborator/call.rs` (the prefix is resolved to its concrete type name before parameter-type lookup so argument resolution runs once with the correct expected-type hints). Synthetic call sites that need to dispatch a method on an already-resolved receiver TIR (the for-of `.into_iter()` / `.next()` calls today) reuse the AST-driven method dispatch via `Elaborator::resolve_method_call_with` (`elaborator/method_call.rs`) — that helper takes a pre-resolved receiver plus a method name and signals "no source AST" with `method_id: None` so no use→def edge is recorded against the synthesis site. Keeping the AST parser-shaped is what lets LSP queries land on the user's text rather than on a synthesised replacement. -## Annotate (Analyze + Resolve + TIR Lowering) +## Analyze, Resolve, Annotate, Reify -`semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for symbol-table construction and `elaborator/` for type checking; bodies are then lowered into TIR. +`semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for the symbol table, then `elaborator/` for the phases below ([WEP 2026-05-26](./wep-2026-05-26-elaborator-rearchitecture.md)): + +- **Resolve** answers every reference site once, from the module that wrote it, and identifies every declaration (`defs.rs`, `resolve.rs`, [WEP 2026-08-12](./wep-2026-08-12-declaration-identity.md)). The only place a name becomes a `DefId`. +- **Annotate** runs a declaration pass over every module — types, `TraitEnv`, `Signatures` — then a body walk whose sole output is a populated `ModuleSemantics`. +- **Liveness** computes source-level reachability, which gates what reify emits and feeds the unused diagnostics. +- **Reify** reads the recorded facts back and emits one `TirModule` per module. No inference, no dispatch decisions. + +The LSP path stops after liveness and builds no TIR. The result, `Semantics`, carries the TIR modules plus an `AstIndex` and a use→def map (`AstId → AstId`, globally-unique ids). This is what makes the architecture LSP-friendly: facts are attached to AST nodes without mutating them, so cross-file navigation, hover, and rename all fall out of the same data the batch compiler uses. See the [LSP](#lsp) section below. From 15a6c444e913ef6116e2351f9469d9acbf226fcd Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 10:12:37 +0000 Subject: [PATCH 11/25] refactor(elaborator): reach the declaration table through the walker `X.tysys.resolutions.defs().def_at(id)` stood at ten call sites across four receivers; `TypeParamScope` derefs to the walker, so one method serves them all. An `Arc::clone` guarded a borrow that was never taken: `cm_owner`'s `or_else` reads `&self` like everything around it. The fixtures said in a header what their own `test` names already said, and each sub-module re-explained the entry module's half. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- docs/compiler.md | 2 +- wado-compiler/src/elaborator.rs | 9 +++++++-- wado-compiler/src/elaborator/item.rs | 14 +++++++------- wado-compiler/src/elaborator/method_call.rs | 2 +- wado-compiler/src/elaborator/synth.rs | 5 ++--- .../cross_module_same_name_default_fn.wado | 5 ++--- .../fixtures/cross_module_same_name_gassoc.wado | 7 +++---- .../tests/fixtures/shadow_prelude_panic.wado | 4 +--- .../sub/cross_module_same_name_default_fn_a.wado | 4 +--- .../sub/cross_module_same_name_gassoc_b.wado | 4 +--- .../sub/cross_module_same_name_gassoc_impl.wado | 3 +-- .../sub/cross_module_same_name_gassoc_trait.wado | 2 +- 12 files changed, 28 insertions(+), 33 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index c68993aeb4a..35778d2a8f0 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -74,7 +74,7 @@ The loader runs `lexer → parser → bind` on every loaded module: The AST is parser-immutable from this point on. The desugar-replacement surface rewrites — compound assignment (`x += y` → `x = x + y`), `while` / C-style `for` → explicit `loop`, `for x of expr` iteration (the `.into_iter()` / `.next()` dispatch and the `match Some(x) => body, _ => break` shape), the `assert` statement, the `matches` operator, the comparison chain `a < b < c`, template-string interpolations, `use … namespace` prefix stripping (`helper::foo`), and `Self::method` / `T::method` (T bound to concrete) static-call dispatch — happen inside the elaborator and are built TIR-direct: each rewrite resolves the user AST and constructs `TirExpr` / `TirStmt` nodes directly without producing synthetic AST. The implementations live in `elaborator/{stmt,operators,assert,matches}.rs` (`resolve_while`, `resolve_for`, `resolve_iterator_for_of`, `resolve_compound_assign`, `desugar_assert`, `desugar_matches_expr`, `desugar_comparison_chain`), `Elaborator::strip_ns_prefix` in `elaborator.rs`, and `CalleeIdentKind` / `classify_call_callee` in `elaborator/call.rs` (the prefix is resolved to its concrete type name before parameter-type lookup so argument resolution runs once with the correct expected-type hints). Synthetic call sites that need to dispatch a method on an already-resolved receiver TIR (the for-of `.into_iter()` / `.next()` calls today) reuse the AST-driven method dispatch via `Elaborator::resolve_method_call_with` (`elaborator/method_call.rs`) — that helper takes a pre-resolved receiver plus a method name and signals "no source AST" with `method_id: None` so no use→def edge is recorded against the synthesis site. Keeping the AST parser-shaped is what lets LSP queries land on the user's text rather than on a synthesised replacement. -## Analyze, Resolve, Annotate, Reify +## Analyze and Elaborate `semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for the symbol table, then `elaborator/` for the phases below ([WEP 2026-05-26](./wep-2026-05-26-elaborator-rearchitecture.md)): diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 2750b5073cd..34f2b65c1df 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -452,6 +452,11 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { .function_sig(self.free_function_at(site)?) } + /// The declaration `id` declares. See [`crate::defs::DefTable::def_at`]. + pub(super) fn def_at(&self, id: crate::ast::AstId) -> crate::defs::DefId { + self.tysys.resolutions.defs().def_at(id) + } + /// The declaration `module` declares under `name`, for the positions no /// reference site answers: `builtin::f`, a namespace member, `core:rt`'s /// `panic`. The module is named by the path, not searched for. @@ -1800,7 +1805,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { IndexMap::default(); for item in &module.items { if let Item::Function(func) = item { - let def = self.tysys.resolutions.defs().def_at(func.id); + let def = self.def_at(func.id); let sig = self.record_function_sig(func); function_sigs.insert(def, Rc::new(sig)); } @@ -2083,7 +2088,7 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { for method in &impl_block.methods { // Records-only: reify emits the method `TirFunction` // from the recorded signature facts + the AST. - let method_def = scope.tysys.resolutions.defs().def_at(method.id); + let method_def = scope.def_at(method.id); let recorded_sig = scope .tysys .signatures diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index e32c5e6767d..d194fef29ff 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -888,7 +888,7 @@ impl TypeParamScope<'_, '_, H> { .self_type .expect("entering an impl frame binds Self to the target"); - let impl_def = scope.tysys.resolutions.defs().def_at(impl_block.id); + let impl_def = scope.def_at(impl_block.id); scope.sem.decls.impl_sigs.insert( impl_def, super::sig::ImplSig { @@ -1259,7 +1259,7 @@ impl Elaborator<'_, H> { .first() .map(|p| p.self_kind) .unwrap_or(ast::SelfKind::None); - let method_def = frame_scope.tysys.resolutions.defs().def_at(method.id); + let method_def = frame_scope.def_at(method.id); frame_scope.sem.decls.method_sigs.insert( method_def, MethodSig { @@ -1509,7 +1509,7 @@ impl Elaborator<'_, H> { /// Operation signatures the decl pass recorded for the declaration at /// `decl_id`. fn declared_effect_ops(&self, decl_id: ast::AstId) -> Vec { - let decl = self.tysys.resolutions.defs().def_at(decl_id); + let decl = self.def_at(decl_id); self.sem .decls .effect_ops @@ -1616,7 +1616,7 @@ impl Elaborator<'_, H> { let mut type_params = decl_slots.clone(); type_params.extend(method_slots); - let method_def = method_scope.tysys.resolutions.defs().def_at(method.id); + let method_def = method_scope.def_at(method.id); methods.insert( method.name.clone(), super::sig::TraitMethod { @@ -1659,7 +1659,7 @@ impl Elaborator<'_, H> { } let module = scope.current_module_source.clone(); - let trait_def = scope.tysys.resolutions.defs().def_at(trait_decl.id); + let trait_def = scope.def_at(trait_decl.id); scope .sem .decls @@ -1830,7 +1830,7 @@ impl Elaborator<'_, H> { } else { SelfKind::None }; - let method_def = scope.tysys.resolutions.defs().def_at(method.id); + let method_def = scope.def_at(method.id); scope.sem.decls.method_sigs.insert( method_def, MethodSig { @@ -2194,7 +2194,7 @@ impl Elaborator<'_, H> { /// re-resolution. Returns the declared return type for callers that /// need it (`resolve_function`'s `task_return_type`). fn populate_generic_function_cache(&mut self, func: &Function) -> TypeId { - let def = self.tysys.resolutions.defs().def_at(func.id); + let def = self.def_at(func.id); let sig = self .sem .decls diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index d37600d107a..48cdcd6532e 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -3660,7 +3660,7 @@ impl Elaborator<'_, H> { // *resource* declares names it as its own owner; one an `impl` block // declares owns to the block, which names no resource, so the spelling // answers there as it did before impl methods were identified. - let defs = std::sync::Arc::clone(self.tysys.resolutions.defs()); + let defs = self.tysys.resolutions.defs(); let cm_owner = method_ref .method_id .and_then(|method| defs.parent(method)) diff --git a/wado-compiler/src/elaborator/synth.rs b/wado-compiler/src/elaborator/synth.rs index c9ed624c935..6b1147e67d4 100644 --- a/wado-compiler/src/elaborator/synth.rs +++ b/wado-compiler/src/elaborator/synth.rs @@ -636,9 +636,8 @@ impl Elaborator<'_, H> { } /// The callee identity of a plain `name(…)` call, read off its own - /// reference site. Anything else (a variant constructor, a static path, an - /// effect operation) names no function there and is left to the expected - /// type. + /// reference site. A variant constructor, a static path or an effect + /// operation names no function there and is left to the expected type. fn synth_callee_ref(&self, ident: &ast::IdentExpr) -> Option { if ident.name.contains("::") { return None; diff --git a/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado b/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado index 18fbbe4a2e3..81a6af4fa93 100644 --- a/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado +++ b/wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado @@ -1,6 +1,5 @@ -// A parameter default is written in the callee's module and resolved at the -// call site, so the function it names is the *callee's* — even where the -// caller declares one under the same spelling. +// The caller declares a `tag` of its own, so a default resolved from the +// walking module's scope reaches the wrong one. use { labelled } from "./sub/cross_module_same_name_default_fn_a.wado"; diff --git a/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado b/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado index 1db71c78da7..d64e48108cb 100644 --- a/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado +++ b/wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado @@ -1,7 +1,6 @@ -// A generic impl's `type Out = …` belongs to the declaration its header names, -// not to whichever module declares that spelling first. Module A's `Node` -// binds `Out` to slot 0 from an impl written outside A; module B's `Node` -// binds it to slot 1 from an impl in its own module. +// A's `Node` binds `Out` to slot 0 from an impl written outside A; B's +// `Node` binds it to slot 1 from an impl in its own module. A +// build-order pick between the two spellings swaps the answers. use { Unwrap } from "./sub/cross_module_same_name_gassoc_trait.wado"; use { Node as NodeB } from "./sub/cross_module_same_name_gassoc_b.wado"; diff --git a/wado-compiler/tests/fixtures/shadow_prelude_panic.wado b/wado-compiler/tests/fixtures/shadow_prelude_panic.wado index 7256b58c302..6c3d0c8b112 100644 --- a/wado-compiler/tests/fixtures/shadow_prelude_panic.wado +++ b/wado-compiler/tests/fixtures/shadow_prelude_panic.wado @@ -1,6 +1,4 @@ -// `panic` and `unreachable` are prelude functions, and a module's own -// declaration outranks the prelude — the call's reference site says so, and -// dispatch takes that answer rather than reaching for `core:rt` by name. +// Both names are the prelude's, and a module's own declaration outranks it. fn panic(msg: String) -> i32 { return 7; diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado index 9d59bacffaf..064dd964d92 100644 --- a/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_default_fn_a.wado @@ -1,6 +1,4 @@ -// Module A: a parameter default calls A's own private `tag`. -// The entry module declares a `tag` of its own, so a default resolved -// from the *caller's* scope reaches the wrong declaration. +// A parameter default calling this module's own private `tag`. fn tag() -> i32 { return 1; diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado index 2958e014db7..34c1074d874 100644 --- a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_b.wado @@ -1,6 +1,4 @@ -// Module B declares a `Node` of the same name and implements `Unwrap` for it -// in its own module, binding `Out` to its *second* slot. A registration -// mis-routed here by name would overwrite that with a first-slot answer. +// The same spelling as A's `Node`, implemented in its own module. use { Unwrap } from "./cross_module_same_name_gassoc_trait.wado"; pub struct Node { diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado index c19739ff26a..406e97f4b8f 100644 --- a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_impl.wado @@ -1,5 +1,4 @@ -// The impl for A's `Node`, written outside A. Its target is named at the -// header's own site; a scan by the written name reaches B's `Node` too. +// The impl for A's `Node`, written outside A. use { Unwrap } from "./cross_module_same_name_gassoc_trait.wado"; use { Node } from "./cross_module_same_name_gassoc_a.wado"; diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado index 9551051c08d..499e4a86b55 100644 --- a/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_gassoc_trait.wado @@ -1,4 +1,4 @@ -// The trait both same-named `Node`s would implement. +// The trait both same-named `Node`s implement. pub trait Unwrap { type Out; fn unwrap_it(&self) -> Self::Out; From de855ad0bfbf75cb2e7f866801ee225648a92820 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 10:43:33 +0000 Subject: [PATCH 12/25] docs(wep): record what the impl index carrying `DefId`s changed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `decl_in_module` joins the sanctioned list: it is `lookup_in_module` read back as an identity, for the qualified positions no reference site answers. `impl_target_decl_key` was listed as going once the impl index carried `DefId`s. It carries them now, and the entry survives — a dispatch that matched a concrete block reads the module off it, but an auto-derived `Eq` / `Ord` and a bound-reached method name no block to read. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- docs/wep-2026-08-12-declaration-identity.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/wep-2026-08-12-declaration-identity.md b/docs/wep-2026-08-12-declaration-identity.md index 5503e0854b2..884f9b92c80 100644 --- a/docs/wep-2026-08-12-declaration-identity.md +++ b/docs/wep-2026-08-12-declaration-identity.md @@ -467,9 +467,16 @@ a sited entry point a caller with a reference site reaches instead. The same derivation in the `Symbol` currency, which §5's `DefId` columns subsume: - `symbol_named`, `imported`, `lookup_in_module`, `lookup_in_module_with_visited` - -One rendering still compared against a declaration's own, which goes when the -impl index carries `DefId`s: +- `decl_in_module` — `lookup_in_module` read back as an identity, for the two + qualified positions no reference site answers: `builtin::f`, and a member of + a namespace import. The module is named by the path rather than searched for, + so no vantage is supplied. + +One rendering still compared against a declaration's own. The impl index now +carries `DefId`s, so a dispatch that matched a concrete block reads its module +off that block; what is left is the paths where no block is named — an +auto-derived `Eq` / `Ord`, and a method reached through a type parameter's +bound, whose block monomorphization picks: - `impl_target_decl_key` — a receiver's newtype chain against an impl's head From 063dd9ad60d9631614a275c754dd18e00a28efc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 12:18:44 +0000 Subject: [PATCH 13/25] refactor(elaborator): ask an impl header once whether it is concrete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `type_params.is_empty()` stood at three sites asking it — the module index's `concrete_only` filter, method lookup's `skip_filter`, and operator dispatch — so `ImplHeader::is_concrete` answers instead. The WEP entry this branch added said `decl_in_module` served "the two qualified positions". It serves four, and one of them — a default expression's own module — is not qualified. Its neighbour narrated the change ("now carries `DefId`s") where a WEP states what stands. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- docs/wep-2026-08-12-declaration-identity.md | 20 +++++++++---------- wado-compiler/src/elaborator.rs | 4 ++-- wado-compiler/src/elaborator/method_lookup.rs | 2 +- wado-compiler/src/elaborator/operators.rs | 7 ++++--- wado-compiler/src/elaborator/trait_env.rs | 9 ++++++++- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/docs/wep-2026-08-12-declaration-identity.md b/docs/wep-2026-08-12-declaration-identity.md index 884f9b92c80..7741c5c6474 100644 --- a/docs/wep-2026-08-12-declaration-identity.md +++ b/docs/wep-2026-08-12-declaration-identity.md @@ -467,16 +467,16 @@ a sited entry point a caller with a reference site reaches instead. The same derivation in the `Symbol` currency, which §5's `DefId` columns subsume: - `symbol_named`, `imported`, `lookup_in_module`, `lookup_in_module_with_visited` -- `decl_in_module` — `lookup_in_module` read back as an identity, for the two - qualified positions no reference site answers: `builtin::f`, and a member of - a namespace import. The module is named by the path rather than searched for, - so no vantage is supplied. - -One rendering still compared against a declaration's own. The impl index now -carries `DefId`s, so a dispatch that matched a concrete block reads its module -off that block; what is left is the paths where no block is named — an -auto-derived `Eq` / `Ord`, and a method reached through a type parameter's -bound, whose block monomorphization picks: +- `decl_in_module` — `lookup_in_module` read back as an identity, for the + positions no reference site answers: `builtin::f`, a namespace member, + `core:rt`'s `panic` at a synthesised call, and a default expression's own + module. Each names its module rather than searching for one, so no vantage + is supplied. + +One rendering still compared against a declaration's own, on the paths that +name no block: an auto-derived `Eq` / `Ord`, and a method reached through a +type parameter's bound, whose block monomorphization picks. A dispatch that +matched a concrete block reads the module off it instead. - `impl_target_decl_key` — a receiver's newtype chain against an impl's head diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 34f2b65c1df..bc71bf768ed 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -458,8 +458,8 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { } /// The declaration `module` declares under `name`, for the positions no - /// reference site answers: `builtin::f`, a namespace member, `core:rt`'s - /// `panic`. The module is named by the path, not searched for. + /// reference site answers. The module is named by the caller, not searched + /// for. pub(super) fn decl_in_module( &self, module: &ModuleSource, diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index f3d587a6d0b..472a25471ba 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -1799,7 +1799,7 @@ impl Elaborator<'_, H> { let is_blanket_tp = matches!(&header.ty, Type::Named(n) if is_target_slot(&n.name)); let generic_is_parametric = matches!(&header.ty, Type::Generic(g) if g.args.iter().any(|a| matches!(a, Type::Named(n) if is_target_slot(&n.name)))); - let skip_filter = !header.type_params.is_empty() + let skip_filter = !header.is_concrete() || is_blanket_tp || matches!(&header.ty, Type::Reference(_) | Type::MutReference(_)) || generic_is_parametric; diff --git a/wado-compiler/src/elaborator/operators.rs b/wado-compiler/src/elaborator/operators.rs index 768bba16726..1b8c9a83333 100644 --- a/wado-compiler/src/elaborator/operators.rs +++ b/wado-compiler/src/elaborator/operators.rs @@ -1974,20 +1974,21 @@ impl Elaborator<'_, H> { // Everything else answers from the receiver's newtype chain, keyed on // the link the lookup was made on — peeling to the base would send an // impl written on the newtype to the base's module. + let defs = self.tysys.resolutions.defs(); let concrete_impl = resolved.impl_def.filter(|def| { self.tysys .trait_env .impl_headers .get(def) - .is_some_and(|h| h.type_params.is_empty()) + .is_some_and(super::trait_env::ImplHeader::is_concrete) }); let module_source = match concrete_impl { - Some(def) => self.tysys.resolutions.defs().module(def).clone(), + Some(def) => defs.module(def).clone(), None => self .impl_target_decl_key(receiver.type_id, &resolved.impl_name) .map_or_else( || self.declaring_module_of(&resolved.impl_name), - |def| self.tysys.resolutions.defs().module(def).clone(), + |def| defs.module(def).clone(), ), }; let function_ref = FunctionRef { diff --git a/wado-compiler/src/elaborator/trait_env.rs b/wado-compiler/src/elaborator/trait_env.rs index ca3de03a2cc..38d8d758ee6 100644 --- a/wado-compiler/src/elaborator/trait_env.rs +++ b/wado-compiler/src/elaborator/trait_env.rs @@ -259,6 +259,13 @@ pub(super) struct ImplHeader { } impl ImplHeader { + /// Whether the block writes no type parameters. A concrete block hosts its + /// own function; a generic one's instance is materialised in the + /// receiver's module. + pub(super) fn is_concrete(&self) -> bool { + self.type_params.is_empty() + } + /// The implemented trait as a mangled method name embeds it: named by the /// module that declares it, carrying the header's written type arguments. /// `None` for an inherent impl, and for a trait position filled by a @@ -701,7 +708,7 @@ fn index_impl_modules( if matches!(header.target, ImplTargetKey::TypeParam(..)) { continue; } - if concrete_only && !header.type_params.is_empty() { + if concrete_only && !header.is_concrete() { continue; } let Some(fq_trait) = header.fq_trait(resolutions) else { From 3916000312a601d01e7336e8a13db87341cd9484 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 12:45:06 +0000 Subject: [PATCH 14/25] docs(wep): state what closing the local-`impl` gap takes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gap named one registry built before bodies are walked. There are two, and only `TraitEnv` yields to a body walk — `Signatures` is frozen between the declaration pass and that walk, while a local `impl`'s target interns its `TypeId` inside it. Recorded with what a first attempt hits: the orphan rule reads a function-local target as foreign, and indexing `TraitEnv` alone turns the diagnostic into a panic. The walker slim-down drops a field count that has since moved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- ...ep-2026-05-26-elaborator-rearchitecture.md | 2 +- docs/wep-2026-07-09-local-item-definitions.md | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/docs/wep-2026-05-26-elaborator-rearchitecture.md b/docs/wep-2026-05-26-elaborator-rearchitecture.md index e4932d236a6..6b0e9b84a4e 100644 --- a/docs/wep-2026-05-26-elaborator-rearchitecture.md +++ b/docs/wep-2026-05-26-elaborator-rearchitecture.md @@ -518,7 +518,7 @@ and the surviving suppression is the argument-classification probe. logger, interner, invocations, entry module) behind one `ElabEnv` field, dissolve `AnnotateState` — `tysys` and `module_semantics` land on `Semantics`, the rest are driver locals — and collapse the per-module - construction site. Takes `Elaborator` from 11 fields to 7. + construction site. ## Consequences diff --git a/docs/wep-2026-07-09-local-item-definitions.md b/docs/wep-2026-07-09-local-item-definitions.md index e7acb154cc6..857bd26e880 100644 --- a/docs/wep-2026-07-09-local-item-definitions.md +++ b/docs/wep-2026-07-09-local-item-definitions.md @@ -94,6 +94,30 @@ whether reify or this WEP's eager annotate-time emission produced it. generic `type` surfaces as a type mismatch, since it needs a different mechanism — `GenericNewtypeInfo` + AST substitution, not the monomorphized template generic structs use — that this WEP does not wire up. Methods on - any local type surface as `no method 'x' found on type`, since a local - `impl`/`trait` block parses but is not connected to method dispatch, a - module-wide registry built once before any function body is walked. + any local type surface as `no method 'x' found on type`. + +## Known gap: methods on a local type + +Two module-wide registries are built before any function body is walked, and a +local `impl` needs both. + +- `TraitEnv` indexes `module.items`, so no local block reaches it. A body walk + can be added: the target's head has its own reference site, and a block whose + target is a function-local declaration is scoped by that identity, since two + sibling blocks' same-named types are two declarations. +- `Signatures` is assembled between the declaration pass and the body walk and + is read-only after. A local `impl`'s target is a block-local type whose + `TypeId` is interned _during_ that walk, so its method signatures cannot + exist before the freeze. + +Closing it means either giving a local type declaration its durable identity +and `TypeId` in the declaration pass — `declare_local_struct` needs only the +`DefId`, which `DefTable::build` already mints, and sits in the body walk for +_visibility_ — or making `Signatures` extensible after assembly, which gives up +the read-only rule [`wep-2026-05-26`](./wep-2026-05-26-elaborator-rearchitecture.md) +rests on. + +Two things a first attempt hits: the orphan rule reads a function-local target +as foreign, because `type_decl_index` holds module-level declarations only; and +indexing `TraitEnv` alone is _worse_ than the gap, since the header then +resolves where the signature does not and the diagnostic becomes a panic. From 0d2727b4386f9b39919a1e24e15b5730b74293d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 13:05:08 +0000 Subject: [PATCH 15/25] resolve merge conflicts --- wado-compiler/src/elaborator/method_lookup.rs | 16 +--- wado-compiler/src/elaborator/trait_env.rs | 93 +------------------ 2 files changed, 6 insertions(+), 103 deletions(-) diff --git a/wado-compiler/src/elaborator/method_lookup.rs b/wado-compiler/src/elaborator/method_lookup.rs index a968633069d..074866f764b 100644 --- a/wado-compiler/src/elaborator/method_lookup.rs +++ b/wado-compiler/src/elaborator/method_lookup.rs @@ -1036,10 +1036,6 @@ impl Elaborator<'_, H> { method_name: &str, receiver_type_args: Option<&[TypeId]>, ) -> Option { -<<<<<<< HEAD -||||||| effa03c1d - let decl_id = self.tysys.all_resource_types.get(&def)?.defined_at; -======= // The nearest declaration answers, keeping its own signature. Only the // receiver's takes type arguments — a generic resource is rejected. self.resource_chain_of(def) @@ -1067,11 +1063,10 @@ impl Elaborator<'_, H> { method_name: &str, ) -> Option<(crate::defs::DefId, super::sig::MethodSig)> { self.resource_chain_of(def).into_iter().find_map(|current| { - let info = self.tysys.all_resource_types.get(¤t)?; let sig = self .tysys .signatures - .resource_method_sig(info.defined_at, method_name)?; + .resource_method_sig(current, method_name)?; (sig.self_kind != ast::SelfKind::None).then(|| (current, sig.clone())) }) } @@ -1085,12 +1080,7 @@ impl Elaborator<'_, H> { method_name: &str, ) -> Option { for impl_ref in self.collect_trait_impl_refs_multi(std::slice::from_ref(type_key)) { - let Some(header) = self - .tysys - .trait_env - .impl_headers - .get(&(impl_ref.0.clone(), impl_ref.1)) - else { + let Some(header) = self.tysys.trait_env.impl_headers.get(&impl_ref.0) else { continue; }; if header.methods.iter().any(|m| m.name == method_name) @@ -1109,8 +1099,6 @@ impl Elaborator<'_, H> { method_name: &str, receiver_type_args: Option<&[TypeId]>, ) -> Option { - let decl_id = self.tysys.all_resource_types.get(&def)?.defined_at; ->>>>>>> origin/main let sig = self .tysys .signatures diff --git a/wado-compiler/src/elaborator/trait_env.rs b/wado-compiler/src/elaborator/trait_env.rs index b92baa6cbd9..9210d8a3f9f 100644 --- a/wado-compiler/src/elaborator/trait_env.rs +++ b/wado-compiler/src/elaborator/trait_env.rs @@ -429,6 +429,7 @@ fn method_headers( .iter() .filter(|p| p.self_kind == ast::SelfKind::None) .count(), + visibility: m.visibility, }) .collect() } @@ -897,7 +898,7 @@ impl TraitEnv { &self, key: &ImplTargetKey, method_name: &str, - ) -> Option<&(String, ModuleSource, AstId, usize)> { + ) -> Option<&(String, ModuleSource, DefId, usize)> { self.resource_static_method_index .get(key)? .iter() @@ -1118,44 +1119,7 @@ impl TraitEnv { name: trait_decl.name.clone(), type_params: trait_decl.type_params.clone(), supertraits: trait_decl.supertraits.clone(), -<<<<<<< HEAD methods: method_headers(defs, &trait_decl.methods), -||||||| effa03c1d - methods: trait_decl - .methods - .iter() - .map(|m| ImplMethodHeader { - name: m.name.clone(), - ast_id: m.id, - type_params: m.type_params.clone(), - span: m.span, - name_span: m.name_span, - param_count: m - .params - .iter() - .filter(|p| p.self_kind == ast::SelfKind::None) - .count(), - }) - .collect(), -======= - methods: trait_decl - .methods - .iter() - .map(|m| ImplMethodHeader { - name: m.name.clone(), - ast_id: m.id, - type_params: m.type_params.clone(), - span: m.span, - name_span: m.name_span, - param_count: m - .params - .iter() - .filter(|p| p.self_kind == ast::SelfKind::None) - .count(), - visibility: m.visibility, - }) - .collect(), ->>>>>>> origin/main assoc_types: trait_decl.associated_types.clone(), span: trait_decl.span, }, @@ -1192,44 +1156,7 @@ impl TraitEnv { trait_type: impl_block.trait_type.clone(), ty: impl_block.ty.clone(), type_params: impl_block.type_params.clone(), -<<<<<<< HEAD methods: method_headers(defs, &impl_block.methods), -||||||| effa03c1d - methods: impl_block - .methods - .iter() - .map(|m| ImplMethodHeader { - name: m.name.clone(), - ast_id: m.id, - type_params: m.type_params.clone(), - span: m.span, - name_span: m.name_span, - param_count: m - .params - .iter() - .filter(|p| p.self_kind == ast::SelfKind::None) - .count(), - }) - .collect(), -======= - methods: impl_block - .methods - .iter() - .map(|m| ImplMethodHeader { - name: m.name.clone(), - ast_id: m.id, - type_params: m.type_params.clone(), - span: m.span, - name_span: m.name_span, - param_count: m - .params - .iter() - .filter(|p| p.self_kind == ast::SelfKind::None) - .count(), - visibility: m.visibility, - }) - .collect(), ->>>>>>> origin/main associated_types: impl_block.associated_types.clone(), is_synthesize_request: impl_block.is_synthesize_request, span: impl_block.span, @@ -1300,15 +1227,9 @@ impl TraitEnv { .or_default() .push(StaticMethodEntry { name: method.name.clone(), -<<<<<<< HEAD - method_id: defs.def_at(method.id), -||||||| effa03c1d - method_id: method.id, -======= module: module_source.clone(), inherent_visibility: None, - method_id: method.id, ->>>>>>> origin/main + method_id: defs.def_at(method.id), }); } } @@ -1327,15 +1248,9 @@ impl TraitEnv { .or_default() .push(StaticMethodEntry { name: method.name.clone(), -<<<<<<< HEAD - method_id: defs.def_at(method.id), -||||||| effa03c1d - method_id: method.id, -======= module: module_source.clone(), inherent_visibility: Some(method.visibility), - method_id: method.id, ->>>>>>> origin/main + method_id: defs.def_at(method.id), }); } } From 3d7d6c8807d86e3ea059a028c73291b2637840db Mon Sep 17 00:00:00 2001 From: "wado-bot[bot]" <277220012+wado-bot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 14:33:49 +0000 Subject: [PATCH 16/25] chore: tidy --- ...cross_module_same_name_default_fn.wir.wado | 32 +++++++++++++ .../cross_module_same_name_gassoc.wir.wado | 32 +++++++++++++ .../fixtures/shadow_prelude_panic.wir.wado | 45 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 wado-compiler/tests/generated/fixtures/cross_module_same_name_default_fn.wir.wado create mode 100644 wado-compiler/tests/generated/fixtures/cross_module_same_name_gassoc.wir.wado create mode 100644 wado-compiler/tests/generated/fixtures/shadow_prelude_panic.wir.wado diff --git a/wado-compiler/tests/generated/fixtures/cross_module_same_name_default_fn.wir.wado b/wado-compiler/tests/generated/fixtures/cross_module_same_name_default_fn.wir.wado new file mode 100644 index 00000000000..050e399b155 --- /dev/null +++ b/wado-compiler/tests/generated/fixtures/cross_module_same_name_default_fn.wir.wado @@ -0,0 +1,32 @@ +// Golden file: WIR with -O2 optimization +// Source: wado-compiler/tests/fixtures/cross_module_same_name_default_fn.wado +// Generated by: mise run update-golden-fixtures + +type "functype//mem/realloc" = fn(i32, i32, i32, i32) -> i32; // TypeId(0) + +type "functype//wasi/task-return" = fn(i32); // TypeId(1) + +type "functype//cross_module_same_name_default_fn.wado/__cm_export____test_0_a_parameter_default_names_the_callee_module_s_function" = fn(); // TypeId(2) + +type "functype//core:prelude/fpfmt.wado/__initialize_module" = fn(); // TypeId(3) + +import fn mem/realloc from "mem/realloc"; +import fn wasi/task-return from "wasi/task-return"; +import memory (1) from "mem/memory"; + +global mut global:cross_module_same_name_default_fn.wado::__modules_initialized: bool = 0; + +fn "cross_module_same_name_default_fn.wado/__cm_export____test_0_a_parameter_default_names_the_callee_module_s_function"() { + __inline___initialize_modules_0: block { + break_if __inline___initialize_modules_0 @likely(global:cross_module_same_name_default_fn.wado::__modules_initialized); + cold_path; + "core:prelude/fpfmt.wado/__initialize_module"(); + global:cross_module_same_name_default_fn.wado::__modules_initialized = 1; + }; + "wasi/task-return"(0); +} + +fn "core:prelude/fpfmt.wado/__initialize_module"() { +} + +export fn "cross_module_same_name_default_fn.wado/__cm_export____test_0_a_parameter_default_names_the_callee_module_s_function" as "__test_0_a_parameter_default_names_the_callee_module_s_function" diff --git a/wado-compiler/tests/generated/fixtures/cross_module_same_name_gassoc.wir.wado b/wado-compiler/tests/generated/fixtures/cross_module_same_name_gassoc.wir.wado new file mode 100644 index 00000000000..f2a27de9a54 --- /dev/null +++ b/wado-compiler/tests/generated/fixtures/cross_module_same_name_gassoc.wir.wado @@ -0,0 +1,32 @@ +// Golden file: WIR with -O2 optimization +// Source: wado-compiler/tests/fixtures/cross_module_same_name_gassoc.wado +// Generated by: mise run update-golden-fixtures + +type "functype//mem/realloc" = fn(i32, i32, i32, i32) -> i32; // TypeId(0) + +type "functype//wasi/task-return" = fn(i32); // TypeId(1) + +type "functype//cross_module_same_name_gassoc.wado/__cm_export____test_0_a_generic_impl_s_associated_type_belongs_to_the_declaration_its_header_names" = fn(); // TypeId(2) + +type "functype//core:prelude/fpfmt.wado/__initialize_module" = fn(); // TypeId(3) + +import fn mem/realloc from "mem/realloc"; +import fn wasi/task-return from "wasi/task-return"; +import memory (1) from "mem/memory"; + +global mut global:cross_module_same_name_gassoc.wado::__modules_initialized: bool = 0; + +fn "cross_module_same_name_gassoc.wado/__cm_export____test_0_a_generic_impl_s_associated_type_belongs_to_the_declaration_its_header_names"() { + __inline___initialize_modules_0: block { + break_if __inline___initialize_modules_0 @likely(global:cross_module_same_name_gassoc.wado::__modules_initialized); + cold_path; + "core:prelude/fpfmt.wado/__initialize_module"(); + global:cross_module_same_name_gassoc.wado::__modules_initialized = 1; + }; + "wasi/task-return"(0); +} + +fn "core:prelude/fpfmt.wado/__initialize_module"() { +} + +export fn "cross_module_same_name_gassoc.wado/__cm_export____test_0_a_generic_impl_s_associated_type_belongs_to_the_declaration_its_header_names" as "__test_0_a_generic_impl_s_associated_type_belongs_to_the_declaration_its_header_names" diff --git a/wado-compiler/tests/generated/fixtures/shadow_prelude_panic.wir.wado b/wado-compiler/tests/generated/fixtures/shadow_prelude_panic.wir.wado new file mode 100644 index 00000000000..dca1566a034 --- /dev/null +++ b/wado-compiler/tests/generated/fixtures/shadow_prelude_panic.wir.wado @@ -0,0 +1,45 @@ +// Golden file: WIR with -O2 optimization +// Source: wado-compiler/tests/fixtures/shadow_prelude_panic.wado +// Generated by: mise run update-golden-fixtures + +type "functype//mem/realloc" = fn(i32, i32, i32, i32) -> i32; // TypeId(0) + +type "functype//wasi/task-return" = fn(i32); // TypeId(1) + +type "functype//shadow_prelude_panic.wado/__cm_export____test_0_a_module_s_own_panic_outranks_the_prelude_s" = fn(); // TypeId(2) + +type "functype//shadow_prelude_panic.wado/__cm_export____test_1_a_module_s_own_unreachable_outranks_the_prelude_s" = fn(); // TypeId(3) + +type "functype//core:prelude/fpfmt.wado/__initialize_module" = fn(); // TypeId(4) + +import fn mem/realloc from "mem/realloc"; +import fn wasi/task-return from "wasi/task-return"; +import memory (1) from "mem/memory"; + +global mut global:shadow_prelude_panic.wado::__modules_initialized: bool = 0; + +fn "shadow_prelude_panic.wado/__cm_export____test_0_a_module_s_own_panic_outranks_the_prelude_s"() { + __inline___initialize_modules_0: block { + break_if __inline___initialize_modules_0 @likely(global:shadow_prelude_panic.wado::__modules_initialized); + cold_path; + "core:prelude/fpfmt.wado/__initialize_module"(); + global:shadow_prelude_panic.wado::__modules_initialized = 1; + }; + "wasi/task-return"(0); +} + +fn "shadow_prelude_panic.wado/__cm_export____test_1_a_module_s_own_unreachable_outranks_the_prelude_s"() { + __inline___initialize_modules_0: block { + break_if __inline___initialize_modules_0 @likely(global:shadow_prelude_panic.wado::__modules_initialized); + cold_path; + "core:prelude/fpfmt.wado/__initialize_module"(); + global:shadow_prelude_panic.wado::__modules_initialized = 1; + }; + "wasi/task-return"(0); +} + +fn "core:prelude/fpfmt.wado/__initialize_module"() { +} + +export fn "shadow_prelude_panic.wado/__cm_export____test_0_a_module_s_own_panic_outranks_the_prelude_s" as "__test_0_a_module_s_own_panic_outranks_the_prelude_s" +export fn "shadow_prelude_panic.wado/__cm_export____test_1_a_module_s_own_unreachable_outranks_the_prelude_s" as "__test_1_a_module_s_own_unreachable_outranks_the_prelude_s" From 2a53f7ff51971a3f7195ea4a10aa2254f472dc14 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 15:17:03 +0000 Subject: [PATCH 17/25] fix(elaborator): key a static call's impl lookup on the receiver's declaration A static call resolved its impl blocks from the receiver's written spelling. Under `use { Meters as M }`, `M::from(3)` compared the impl header's head (`Meters`) against the call's spelling (`M`), matched nothing, and reached WIR build with an unresolved call. Where the spelling was the declared name that the caller never imported, the derived key indexed nothing at all. `locate_static_method_impl` and `conversion_impl_survey` now compare against the target declaration's own name, and take the resolved `ImplTargetKey` from the paths that already hold one: `resolve_static_method_call` off the receiver type, and the newtype base in `resolve_static_method_call_from_qualified`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- docs/compiler.md | 2 +- wado-compiler/src/defs.rs | 1 + wado-compiler/src/elaborator.rs | 1 - wado-compiler/src/elaborator/call.rs | 40 +++--- wado-compiler/src/elaborator/method_call.rs | 124 ++++++++++++++---- wado-compiler/src/elaborator/trait_env.rs | 5 +- .../cross_module_same_name_conversion.wado | 22 ++++ ...type_base_conversion_keys_base_module.wado | 26 ++++ .../cross_module_same_name_conversion_a.wado | 11 ++ .../sub/newtype_base_conversion_module_a.wado | 13 ++ 10 files changed, 195 insertions(+), 50 deletions(-) create mode 100644 wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado create mode 100644 wado-compiler/tests/fixtures/newtype_base_conversion_keys_base_module.wado create mode 100644 wado-compiler/tests/fixtures/sub/cross_module_same_name_conversion_a.wado create mode 100644 wado-compiler/tests/fixtures/sub/newtype_base_conversion_module_a.wado diff --git a/docs/compiler.md b/docs/compiler.md index 35778d2a8f0..6c1bb092d5b 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -78,7 +78,7 @@ The AST is parser-immutable from this point on. The desugar-replacement surface `semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for the symbol table, then `elaborator/` for the phases below ([WEP 2026-05-26](./wep-2026-05-26-elaborator-rearchitecture.md)): -- **Resolve** answers every reference site once, from the module that wrote it, and identifies every declaration (`defs.rs`, `resolve.rs`, [WEP 2026-08-12](./wep-2026-08-12-declaration-identity.md)). The only place a name becomes a `DefId`. +- **Resolve** answers every reference site once, from the module that wrote it, and identifies every declaration (`defs.rs`, `resolve.rs`, [WEP 2026-08-12](./wep-2026-08-12-declaration-identity.md)). A reference site is answered nowhere else; the positions no site answers derive their `DefId` from a module the caller names. - **Annotate** runs a declaration pass over every module — types, `TraitEnv`, `Signatures` — then a body walk whose sole output is a populated `ModuleSemantics`. - **Liveness** computes source-level reachability, which gates what reify emits and feeds the unused diagnostics. - **Reify** reads the recorded facts back and emits one `TirModule` per module. No inference, no dispatch decisions. diff --git a/wado-compiler/src/defs.rs b/wado-compiler/src/defs.rs index 7868e75ab83..43d044830c7 100644 --- a/wado-compiler/src/defs.rs +++ b/wado-compiler/src/defs.rs @@ -760,6 +760,7 @@ mod tests { let source = r#" pub struct Point { x: i32, y: i32 } pub trait Greet { fn hello(&self) -> i32; } + impl Point { pub fn len(&self) -> i32 { return self.x; } } "#; let (seed, modules, symbols, _) = analyze_source(source); let again = DefTable::build_seeded(Some(&seed), &modules, &symbols); diff --git a/wado-compiler/src/elaborator.rs b/wado-compiler/src/elaborator.rs index 9cc068800cc..c5f798dcebc 100644 --- a/wado-compiler/src/elaborator.rs +++ b/wado-compiler/src/elaborator.rs @@ -256,7 +256,6 @@ impl<'a, H: CompilerHost> Elaborator<'a, H> { /// The symbol `name` reaches from `module`, for a caller whose reference site /// is not at hand — a mangled name, a synthesis target. No scope is run. - /// Prefer [`Self::symbol_at`], which reads the answer the walk recorded. pub(crate) fn symbol_named( &self, module: &ModuleSource, diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index 3f0d1304dfa..da3463603ed 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -327,6 +327,22 @@ impl Elaborator<'_, H> { self.is_effect_or_resource_decl(def).then_some(def) } + /// Whether the name spells a built-in variant case (`Ok`, `Err`, `Some`, + /// `None`). Read from the `CompilerItem` registry, so a stdlib rename + /// carries here without re-editing a literal set. + fn names_result_or_option_case(&self, name: &str) -> bool { + let tt = self.tysys.type_table.borrow(); + let items = tt.compiler_items(); + [ + crate::compiler_item::CompilerItem::ResultOk, + crate::compiler_item::CompilerItem::ResultErr, + crate::compiler_item::CompilerItem::OptionSome, + crate::compiler_item::CompilerItem::OptionNone, + ] + .into_iter() + .any(|item| name == items.variant_case_name(item)) + } + pub(super) fn resolve_call( &mut self, call: &ast::CallExpr, @@ -545,6 +561,7 @@ impl Elaborator<'_, H> { call.span, ctx, &mut param_types, + None, ) { return TypeTable::ERROR; } @@ -661,18 +678,6 @@ impl Elaborator<'_, H> { // dispatches on `effective_name` (after any `Self::` / `T::` // prefix rewriting) while `ident` is kept around for LSP // segment-edge recording and other AST-id needs. - let is_result_or_option_case = { - let tt = self.tysys.type_table.borrow(); - let items = tt.compiler_items(); - [ - crate::compiler_item::CompilerItem::ResultOk, - crate::compiler_item::CompilerItem::ResultErr, - crate::compiler_item::CompilerItem::OptionSome, - crate::compiler_item::CompilerItem::OptionNone, - ] - .into_iter() - .any(|item| effective_name == items.variant_case_name(item)) - }; let (callee_opt, display_name): (Option, String) = if let Some(pos) = effective_name.find("::") { @@ -711,7 +716,7 @@ impl Elaborator<'_, H> { None }; let method_def = self - .locate_static_method_impl(prefix, suffix, arg_hint.as_deref()) + .locate_static_method_impl(prefix, suffix, arg_hint.as_deref(), None) .and_then(|r| r.method_id) .or_else(|| { self.static_method_decl_id( @@ -1222,6 +1227,7 @@ impl Elaborator<'_, H> { type_name, method_name, arg_type_hint.as_deref(), + None, ); let method_ref = resolved.unwrap_or_else(|| { StaticMethodRef::new(ns_source.clone(), type_name, method_name, None, None) @@ -1431,11 +1437,9 @@ impl Elaborator<'_, H> { effective_name.to_string(), ) } - // A built-in type constructor (Ok, Err, Some, None) — a variant case, - // not a function, so the site above declines it. The four names flow - // through the `CompilerItem` registry so a stdlib rename is picked up - // here without re-editing the literal set. - else if is_result_or_option_case { + // A built-in type constructor — a variant case, not a function, so the + // site above declines it. + else if self.names_result_or_option_case(&effective_name) { self.record_item_reference_by_name(ident.id, effective_name); ( Some(CalleeRef::rendered( diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index 1db85056331..f6ea215b265 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -1507,6 +1507,7 @@ impl Elaborator<'_, H> { static_call.span, ctx, &mut param_types, + struct_key_for_lookup.as_ref(), ) { return TypeTable::ERROR; @@ -2113,11 +2114,16 @@ impl Elaborator<'_, H> { // its `method_id` is what the use→def edge below is recorded against. // A name lookup cannot stand in — two conversion impls on one type // declare the same `from`, and only the argument's type separates - // them. + // them. The receiver comes off the resolved type: re-deriving it from + // `struct_name` searches the caller's frame, which an aliased import + // leaves without that name at all. + let receiver_key = + self.impl_target_of(target_type_id, &crate::name::DeclName::new(&struct_name)); let selected = self.locate_static_method_impl( &struct_name, &static_call.method, arg_type_hint.as_deref(), + Some(&receiver_key), ); let trait_name_opt = selected.as_ref().and_then(|r| r.trait_name.clone()); @@ -2129,12 +2135,17 @@ impl Elaborator<'_, H> { // disagreement is reported here instead of ICE-ing there. if trait_name_opt.is_none() && let Some(arg_type) = arg_type_hint.as_deref() - && !self.has_inherent_static_method(&struct_name, &static_call.method) + && !self.has_inherent_static_method( + &struct_name, + &static_call.method, + Some(&receiver_key), + ) && self.report_unmatched_conversion( &struct_name, &static_call.method, arg_type, static_call.span, + Some(&receiver_key), ) { return TypeTable::ERROR; @@ -2840,17 +2851,40 @@ impl Elaborator<'_, H> { Vec::new() } - /// Keys of the *trait* impl blocks whose target head is written - /// `struct_name`, current-module-first. A written name reaches two receiver - /// namespaces: usually a declaration, but an impl binding it as its own type - /// parameter (`impl Trait for V`) keys under that binder instead. - /// Both are searched in the current module, only the declaration namespace - /// outside it. Consumers re-check the impl's spelling. - fn trait_impl_keys_current_first(&self, struct_name: &str) -> Vec { + /// The receiver a static lookup written `struct_name` keys on: the + /// declaration, and the name an impl header spells it with. A call site's + /// alias is not that name, and an impl block never writes one. + fn static_receiver_target( + &self, + struct_name: &str, + target_hint: Option<&ImplTargetKey>, + ) -> (ImplTargetKey, String) { + let target = target_hint + .cloned() + .unwrap_or_else(|| self.impl_target(struct_name)); + let declared = target + .type_name(self.tysys.resolutions.defs()) + .unwrap_or(struct_name) + .to_string(); + (target, declared) + } + + /// Keys of the *trait* impl blocks whose target head is `target`, + /// current-module-first. A receiver reaches two namespaces: usually a + /// declaration, but an impl binding its name as its own type parameter + /// (`impl Trait for V`) keys under that binder instead. Both are + /// searched in the current module, only the declaration namespace outside + /// it. Consumers re-check the impl's spelling against `declared_name`. + fn trait_impl_keys_current_first( + &self, + target: &ImplTargetKey, + declared_name: &str, + ) -> Vec { let env = &self.tysys.trait_env; let defs = self.tysys.resolutions.defs(); - let declared = env.entries_by_receiver_vec(&self.impl_target(struct_name).receiver(defs)); - let binder = env.entries_by_receiver_vec(&Receiver::Type(FqTypeName::binder(struct_name))); + let declared = env.entries_by_receiver_vec(&target.receiver(defs)); + let binder = + env.entries_by_receiver_vec(&Receiver::Type(FqTypeName::binder(declared_name))); let is_current = |k: &&crate::defs::DefId| *defs.module(**k) == self.current_module_source; let mut keys: Vec = declared .iter() @@ -3025,7 +3059,7 @@ impl Elaborator<'_, H> { struct_name: &str, method_name: &str, ) -> Option { - self.locate_static_method_impl(struct_name, method_name, None) + self.locate_static_method_impl(struct_name, method_name, None, None) .and_then(|r| r.trait_name) } @@ -3054,8 +3088,10 @@ impl Elaborator<'_, H> { method_name: &str, arg_type: &str, span: Span, + target_hint: Option<&ImplTargetKey>, ) -> bool { - let (candidates, has_blanket) = self.conversion_impl_survey(struct_name, method_name); + let (candidates, has_blanket) = + self.conversion_impl_survey(struct_name, method_name, target_hint); if has_blanket { let _ = self.emit(TypeError::UnsupportedBlanketConversion { trait_name: self.conversion_trait_name(method_name), @@ -3093,14 +3129,15 @@ impl Elaborator<'_, H> { span: Span, ctx: &mut FunctionContext, param_types: &mut Vec, + target_hint: Option<&ImplTargetKey>, ) -> bool { if (method_name != "from" && method_name != "try_from") - || self.has_inherent_static_method(recv_name, method_name) + || self.has_inherent_static_method(recv_name, method_name, target_hint) { return false; } let class = self.synthesize_arg_class(arg, ctx); - match self.conversion_preselect(recv_name, method_name, &class) { + match self.conversion_preselect(recv_name, method_name, &class, target_hint) { ConversionPreselect::Selected(source) => { *param_types = vec![source]; false @@ -3146,11 +3183,16 @@ impl Elaborator<'_, H> { /// of this name. A conversion-call guard needs the distinction: a trait /// lookup returning `None` is a failure only when no inherent static can /// answer instead. - pub(super) fn has_inherent_static_method(&self, struct_name: &str, method_name: &str) -> bool { - let keys = self - .tysys - .trait_env - .inherent_impl_keys(&self.impl_target(struct_name)); + pub(super) fn has_inherent_static_method( + &self, + struct_name: &str, + method_name: &str, + target_hint: Option<&ImplTargetKey>, + ) -> bool { + let target = target_hint + .cloned() + .unwrap_or_else(|| self.impl_target(struct_name)); + let keys = self.tysys.trait_env.inherent_impl_keys(&target); self.keys_declare_static_method(&keys, method_name) } @@ -3164,12 +3206,14 @@ impl Elaborator<'_, H> { struct_name: &str, method_name: &str, class: &super::synth::ArgClass, + target_hint: Option<&ImplTargetKey>, ) -> ConversionPreselect { use super::synth::ArgClass; if matches!(class, ArgClass::Opaque(_)) { return ConversionPreselect::Pass; } - let (candidates, _has_blanket) = self.conversion_impl_survey(struct_name, method_name); + let (candidates, _has_blanket) = + self.conversion_impl_survey(struct_name, method_name, target_hint); let admitted: Vec = candidates .into_iter() .filter(|c| { @@ -3201,6 +3245,7 @@ impl Elaborator<'_, H> { &self, struct_name: &str, method_name: &str, + target_hint: Option<&ImplTargetKey>, ) -> (Vec, bool) { let from_trait_name = self .tysys @@ -3210,14 +3255,15 @@ impl Elaborator<'_, H> { .to_string(); let mut candidates: Vec = Vec::new(); let mut has_blanket = false; - for impl_def in self.trait_impl_keys_current_first(struct_name) { + let (target, declared_name) = self.static_receiver_target(struct_name, target_hint); + for impl_def in self.trait_impl_keys_current_first(&target, &declared_name) { let header = &self.tysys.trait_env.impl_headers[&impl_def]; let module = self.tysys.resolutions.defs().module(impl_def).clone(); let Some(trait_type) = header.trait_type.as_ref() else { continue; }; let base = super::trait_env::get_type_name_static(trait_type); - if super::trait_env::get_type_name_static(&header.ty) != struct_name + if super::trait_env::get_type_name_static(&header.ty) != declared_name || (base != from_trait_name && base != "TryFrom") || !header.methods.iter().any(|m| m.name == method_name) { @@ -3288,7 +3334,9 @@ impl Elaborator<'_, H> { struct_name: &str, method_name: &str, arg_type_name: Option<&str>, + target_hint: Option<&ImplTargetKey>, ) -> Option { + let (target, declared_name) = self.static_receiver_target(struct_name, target_hint); let from_trait_name = self .tysys .type_table @@ -3379,7 +3427,7 @@ impl Elaborator<'_, H> { impl_module: &ModuleSource| -> Option<(crate::name::FqTraitName, crate::defs::DefId)> { let trait_type = header.trait_type.as_ref()?; - if super::trait_env::get_type_name_static(&header.ty) != struct_name + if super::trait_env::get_type_name_static(&header.ty) != declared_name || !matches_arg_type(trait_type, impl_module, &header.type_params) { return None; @@ -3412,7 +3460,7 @@ impl Elaborator<'_, H> { None }; - for impl_def in self.trait_impl_keys_current_first(struct_name) { + for impl_def in self.trait_impl_keys_current_first(&target, &declared_name) { let header = &self.tysys.trait_env.impl_headers[&impl_def]; let module_source = self.tysys.resolutions.defs().module(impl_def).clone(); if let Some((trait_name, method_id)) = check_impl(header, &module_source) { @@ -3565,7 +3613,7 @@ impl Elaborator<'_, H> { // `Type::method` must still resolve. `locate_static_method_impl` // applies the same fallback to find the trait name and module. if self - .locate_static_method_impl(struct_name, method_name, None) + .locate_static_method_impl(struct_name, method_name, None, None) .is_some() { return true; @@ -3677,10 +3725,20 @@ impl Elaborator<'_, H> { } else { None }; + // A newtype's static call dispatches to its base, whose name is not + // the caller's to resolve — that frame can hold a same-named + // declaration of its own. + let receiver_key = newtype_dispatch.as_ref().map(|(_, base_type_id, _)| { + self.impl_target_of( + *base_type_id, + &crate::name::DeclName::new(&actual_struct_name), + ) + }); let resolved = self.locate_static_method_impl( &actual_struct_name, method_name, arg_type_hint.as_deref(), + receiver_key.as_ref(), ); // The expected type that shaped the argument came from // `lookup_static_method_param_types_keyed`, which keys on (receiver, @@ -3690,8 +3748,18 @@ impl Elaborator<'_, H> { // disagreement is reported here instead of ICE-ing there. if resolved.is_none() && let Some(arg_type) = arg_type_hint.as_deref() - && !self.has_inherent_static_method(&actual_struct_name, method_name) - && self.report_unmatched_conversion(&actual_struct_name, method_name, arg_type, span) + && !self.has_inherent_static_method( + &actual_struct_name, + method_name, + receiver_key.as_ref(), + ) + && self.report_unmatched_conversion( + &actual_struct_name, + method_name, + arg_type, + span, + receiver_key.as_ref(), + ) { return placeholder(TypeTable::ERROR, span); } diff --git a/wado-compiler/src/elaborator/trait_env.rs b/wado-compiler/src/elaborator/trait_env.rs index 9210d8a3f9f..79013eb9b5b 100644 --- a/wado-compiler/src/elaborator/trait_env.rs +++ b/wado-compiler/src/elaborator/trait_env.rs @@ -597,8 +597,9 @@ pub(super) struct StaticMethodEntry { pub(super) type StaticMethodIndex = IndexMap>; /// Pre-built index of static methods from resource declarations. -/// Key: canonical receiver [`DefId`] → `[(method_name, ModuleSource, -/// declaration, method_index)]`. Same disambiguation rationale as +/// Key: canonical receiver [`DefId`] → `[(method_name, ModuleSource, owning +/// resource declaration, method_index)]` — the resource, not the method, unlike +/// [`StaticMethodEntry::method_id`]. Same disambiguation rationale as /// [`StaticMethodIndex`]. pub(super) type ResourceStaticMethodIndex = IndexMap>; diff --git a/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado b/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado new file mode 100644 index 00000000000..4f1e4fd77c1 --- /dev/null +++ b/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado @@ -0,0 +1,22 @@ +// The caller declares a `Meters` of its own, so a conversion surveyed by the +// written name reaches the wrong module's impl. + +use { Meters as M } from "./sub/cross_module_same_name_conversion_a.wado"; + +struct Meters { + v: i32, +} + +impl From for Meters { + fn from(value: i32) -> Meters { + return Meters { v: value + 1000 }; + } +} + +test "a conversion keys to the receiver's declaration, not the caller's same-named type" { + let imported = M::from(3); + assert imported.v == 30; + + let local = Meters::from(3); + assert local.v == 1003; +} diff --git a/wado-compiler/tests/fixtures/newtype_base_conversion_keys_base_module.wado b/wado-compiler/tests/fixtures/newtype_base_conversion_keys_base_module.wado new file mode 100644 index 00000000000..92e9b0eb15d --- /dev/null +++ b/wado-compiler/tests/fixtures/newtype_base_conversion_keys_base_module.wado @@ -0,0 +1,26 @@ +// A newtype's static call dispatches to its base, whose declaration is the +// imported module's. The caller's same-named `Meters` must not answer for it. +// +// A base's *trait* static is not inherited yet, so the call still fails; what +// this pins is which module's impls the report reads — `i64` is the base's, +// `String` the caller's. + +use { Km } from "./sub/newtype_base_conversion_module_a.wado"; + +struct Meters { + v: i64, +} + +impl From for Meters { + fn from(value: String) -> Meters { + return Meters { v: 1000 }; + } +} + +test "a newtype's conversion report names its base's impls" { + let k = Km::from(5); + assert k.v == 50; +} + +__DATA__ +{"test": {}, "compile_error": "'Meters::from' is available for 'i64'"} diff --git a/wado-compiler/tests/fixtures/sub/cross_module_same_name_conversion_a.wado b/wado-compiler/tests/fixtures/sub/cross_module_same_name_conversion_a.wado new file mode 100644 index 00000000000..01545a201b2 --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/cross_module_same_name_conversion_a.wado @@ -0,0 +1,11 @@ +// The receiver the call actually names, declared outside the caller. + +pub struct Meters { + pub v: i32, +} + +impl From for Meters { + fn from(value: i32) -> Meters { + return Meters { v: value * 10 }; + } +} diff --git a/wado-compiler/tests/fixtures/sub/newtype_base_conversion_module_a.wado b/wado-compiler/tests/fixtures/sub/newtype_base_conversion_module_a.wado new file mode 100644 index 00000000000..60d102f0aba --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/newtype_base_conversion_module_a.wado @@ -0,0 +1,13 @@ +// The base the newtype dispatches to, and its own conversion. + +pub struct Meters { + pub v: i64, +} + +impl From for Meters { + fn from(value: i64) -> Meters { + return Meters { v: value * 10 }; + } +} + +pub type Km = Meters; From d798f16d2b63d1e9d3dbce19cb0336c0104bff70 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 16:02:52 +0000 Subject: [PATCH 18/25] test: golden WIR for the cross-module conversion fixture Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- ...cross_module_same_name_conversion.wir.wado | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 wado-compiler/tests/generated/fixtures/cross_module_same_name_conversion.wir.wado diff --git a/wado-compiler/tests/generated/fixtures/cross_module_same_name_conversion.wir.wado b/wado-compiler/tests/generated/fixtures/cross_module_same_name_conversion.wir.wado new file mode 100644 index 00000000000..768e205348a --- /dev/null +++ b/wado-compiler/tests/generated/fixtures/cross_module_same_name_conversion.wir.wado @@ -0,0 +1,32 @@ +// Golden file: WIR with -O2 optimization +// Source: wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado +// Generated by: mise run update-golden-fixtures + +type "functype//mem/realloc" = fn(i32, i32, i32, i32) -> i32; // TypeId(0) + +type "functype//wasi/task-return" = fn(i32); // TypeId(1) + +type "functype//cross_module_same_name_conversion.wado/__cm_export____test_0_a_conversion_keys_to_the_receiver_s_declaration__not_the_caller_s_same_named_type" = fn(); // TypeId(2) + +type "functype//core:prelude/fpfmt.wado/__initialize_module" = fn(); // TypeId(3) + +import fn mem/realloc from "mem/realloc"; +import fn wasi/task-return from "wasi/task-return"; +import memory (1) from "mem/memory"; + +global mut global:cross_module_same_name_conversion.wado::__modules_initialized: bool = 0; + +fn "cross_module_same_name_conversion.wado/__cm_export____test_0_a_conversion_keys_to_the_receiver_s_declaration__not_the_caller_s_same_named_type"() { + __inline___initialize_modules_0: block { + break_if __inline___initialize_modules_0 @likely(global:cross_module_same_name_conversion.wado::__modules_initialized); + cold_path; + "core:prelude/fpfmt.wado/__initialize_module"(); + global:cross_module_same_name_conversion.wado::__modules_initialized = 1; + }; + "wasi/task-return"(0); +} + +fn "core:prelude/fpfmt.wado/__initialize_module"() { +} + +export fn "cross_module_same_name_conversion.wado/__cm_export____test_0_a_conversion_keys_to_the_receiver_s_declaration__not_the_caller_s_same_named_type" as "__test_0_a_conversion_keys_to_the_receiver_s_declaration__not_the_caller_s_same_named_type" From f142ffd95576a0c5d2b216302adca9116c5ef663 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 16:29:35 +0000 Subject: [PATCH 19/25] resolve merge conflicts Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/item.rs | 3 ++- wado-compiler/src/elaborator/method_call.rs | 8 -------- wado-compiler/src/elaborator/sig.rs | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/wado-compiler/src/elaborator/item.rs b/wado-compiler/src/elaborator/item.rs index 3aaba0497b6..61a7f291362 100644 --- a/wado-compiler/src/elaborator/item.rs +++ b/wado-compiler/src/elaborator/item.rs @@ -1185,6 +1185,7 @@ impl Elaborator<'_, H> { /// instantiates a recorded signature instead of re-resolving the method /// AST under the *caller's* perspective (WEP 2026-05-26). pub(super) fn record_impl_decls(&mut self, impl_block: &ast::ImplBlock) { + let impl_def = self.def_at(impl_block.id); let mut block = self.enter_inherited_type_param_scope(); block.annotate_ctx.trait_ctx.type_params.clear(); block.annotate_ctx.trait_ctx.type_param_bounds.clear(); @@ -1281,7 +1282,7 @@ impl Elaborator<'_, H> { }) .collect(), declaring_slot_count, - declaring_impl: Some(impl_block.id), + declaring_impl: Some(impl_def), own_params: super::sig::own_params_of(&method.type_params), cm_name: method .attrs diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index 52a40ce6067..83d0e409bcc 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -3488,16 +3488,8 @@ impl Elaborator<'_, H> { impl_module: &ModuleSource| -> Option<(crate::name::FqTraitName, crate::defs::DefId)> { let trait_type = header.trait_type.as_ref()?; -<<<<<<< HEAD if super::trait_env::get_type_name_static(&header.ty) != declared_name - || !matches_arg_type(trait_type, impl_module, &header.type_params) -||||||| a2e111909 - if super::trait_env::get_type_name_static(&header.ty) != struct_name - || !matches_arg_type(trait_type, impl_module, &header.type_params) -======= - if super::trait_env::get_type_name_static(&header.ty) != struct_name || !matches_arg_type(trait_type, &header.ty, impl_module, &header.type_params) ->>>>>>> origin/main { return None; } diff --git a/wado-compiler/src/elaborator/sig.rs b/wado-compiler/src/elaborator/sig.rs index c6758cf55e7..191b7634ce0 100644 --- a/wado-compiler/src/elaborator/sig.rs +++ b/wado-compiler/src/elaborator/sig.rs @@ -157,7 +157,7 @@ pub(crate) struct MethodSig { /// The `impl` block that declares this method, where one does. How a caller /// reaches [`ImplSig::spelled_slots`], which aligns a spelled turbofish /// with the block's slots. - pub(crate) declaring_impl: Option, + pub(crate) declaring_impl: Option, /// The method's own slots as the declaration wrote them, parallel to /// [`Self::own_type_params`]. Bounds and defaults are irreducibly AST and /// live nowhere else, and a use site needs them to enforce the one and From 70379b76e42e3120f93017fcef8e578167b233b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:11:52 +0000 Subject: [PATCH 20/25] fix(elaborator): key static-dispatch metadata by the resolved receiver Mutability, defaults and parameter types of a static method were read back through the written name, so a caller declaring the same spelling answered for a receiver declared elsewhere. Both call paths pass the key they already hold. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/method_call.rs | 43 ++++++++++++--------- wado-compiler/src/elaborator/sem/types.rs | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index 83d0e409bcc..81debe760e2 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -2248,7 +2248,13 @@ impl Elaborator<'_, H> { let param_is_mut = struct_name_for_lookup .as_deref() - .map(|name| self.lookup_static_method_param_is_mut(name, &static_call.method)) + .map(|name| { + self.lookup_static_method_param_is_mut_keyed( + name, + &static_call.method, + struct_key_for_lookup.as_ref(), + ) + }) .unwrap_or_default(); // Build method_info with base struct name and trait name (if applicable) @@ -2978,16 +2984,6 @@ impl Elaborator<'_, H> { .unwrap_or_default() } - /// Look up whether each parameter of a static method is `mut`. - /// Returns empty vec (conservative) for unknown methods. - pub(super) fn lookup_static_method_param_is_mut( - &self, - struct_name: &str, - method_name: &str, - ) -> Vec { - self.lookup_static_method_param_is_mut_keyed(struct_name, method_name, None) - } - /// The return type every static method under this name agrees on, `None` when /// they disagree. An overload set still answers: every `From` impl returns the /// receiver, so which one this call reaches cannot change the result. @@ -3043,8 +3039,9 @@ impl Elaborator<'_, H> { .unwrap_or_default() } - /// Like [`Self::lookup_static_method_param_is_mut`] but takes a pre-resolved - /// receiver key, which a namespace member's bare spelling cannot reach. + /// Whether each parameter of a static method is `mut`, empty for an unknown + /// method. The receiver key is pre-resolved where the caller holds one — a + /// namespace member's bare spelling cannot reach it. pub(super) fn lookup_static_method_param_is_mut_keyed( &self, struct_name: &str, @@ -3878,10 +3875,17 @@ impl Elaborator<'_, H> { }) }; - let param_is_mut = self.lookup_static_method_param_is_mut(&actual_struct_name, method_name); + let param_is_mut = self.lookup_static_method_param_is_mut_keyed( + &actual_struct_name, + method_name, + receiver_key.as_ref(), + ); - let param_defaults = - self.lookup_static_method_param_defaults_keyed(&actual_struct_name, method_name, None); + let param_defaults = self.lookup_static_method_param_defaults_keyed( + &actual_struct_name, + method_name, + receiver_key.as_ref(), + ); // Propagate #[cm("...")] from resource static methods. A method the // *resource* declares names it as its own owner; one an `impl` block @@ -3921,8 +3925,11 @@ impl Elaborator<'_, H> { .zip(param_is_mut.iter().copied().chain(std::iter::repeat(false))) .map(|(_, is_mut)| is_mut) .collect(); - let param_types = - self.lookup_static_method_param_types_keyed(&actual_struct_name, method_name, None); + let param_types = self.lookup_static_method_param_types_keyed( + &actual_struct_name, + method_name, + receiver_key.as_ref(), + ); self.sem.types.static_method_dispatch.insert( call_id, super::sem::types::StaticMethodDispatch { diff --git a/wado-compiler/src/elaborator/sem/types.rs b/wado-compiler/src/elaborator/sem/types.rs index dca6f31fd60..4dfbd2df454 100644 --- a/wado-compiler/src/elaborator/sem/types.rs +++ b/wado-compiler/src/elaborator/sem/types.rs @@ -545,7 +545,7 @@ pub(crate) struct StaticMethodDispatch { /// it after impl lookup and mangling. pub(crate) function_ref: crate::tir::FunctionRef, /// Per-argument `is_mut` flag derived from the resolved parameter - /// signature (`lookup_static_method_param_is_mut`). Reify zips this + /// signature (`lookup_static_method_param_is_mut_keyed`). Reify zips this /// with the reified argument exprs to build [`crate::tir::CallArg`]s /// with the same `is_mut` shape annotate produced. pub(crate) param_is_mut: Vec, From a62c723a211f221a243e58faae911f77a9954f5d Mon Sep 17 00:00:00 2001 From: "wado-bot[bot]" <277220012+wado-bot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 17:38:48 +0000 Subject: [PATCH 21/25] chore: tidy --- wado-compiler/src/elaborator/call.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index da3463603ed..c1757d55caa 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -1439,7 +1439,7 @@ impl Elaborator<'_, H> { } // A built-in type constructor — a variant case, not a function, so the // site above declines it. - else if self.names_result_or_option_case(&effective_name) { + else if self.names_result_or_option_case(effective_name) { self.record_item_reference_by_name(ident.id, effective_name); ( Some(CalleeRef::rendered( From d18175d76cdbdf4aaa65522e58c922b7a4c16949 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 17:52:00 +0000 Subject: [PATCH 22/25] fix(elaborator): un-alias an impl header's head before matching a receiver An impl block writes its target under whatever name its own module imported, so a header's head is neither the declaration's name nor a call site's: `impl From for ClockInstant` in core:temporal targets wasi:clocks' `Instant`. Comparing the head against the declared name dropped such an impl and left the mangled call unresolved at WIR build. The head is canonicalised in the impl's own module first, as the argument-type match beside it already does. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/method_call.rs | 15 +++++++++++---- .../tests/fixtures/aliased_impl_head.wado | 19 +++++++++++++++++++ .../fixtures/sub/aliased_impl_head_a.wado | 5 +++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 wado-compiler/tests/fixtures/aliased_impl_head.wado create mode 100644 wado-compiler/tests/fixtures/sub/aliased_impl_head_a.wado diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index 81debe760e2..81872b35219 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -2898,8 +2898,9 @@ impl Elaborator<'_, H> { } /// The receiver a static lookup written `struct_name` keys on: the - /// declaration, and the name an impl header spells it with. A call site's - /// alias is not that name, and an impl block never writes one. + /// declaration, and its declared name. Neither side's spelling is that + /// name — a call site and an impl header each write whatever their own + /// module imported — so a header's head is un-aliased before comparing. fn static_receiver_target( &self, struct_name: &str, @@ -3300,7 +3301,9 @@ impl Elaborator<'_, H> { continue; }; let base = super::trait_env::get_type_name_static(trait_type); - if super::trait_env::get_type_name_static(&header.ty) != declared_name + let head = self + .import_original_name(&super::trait_env::get_type_name_static(&header.ty), &module); + if head != declared_name || (base != from_trait_name && base != "TryFrom") || !header.methods.iter().any(|m| m.name == method_name) { @@ -3485,7 +3488,11 @@ impl Elaborator<'_, H> { impl_module: &ModuleSource| -> Option<(crate::name::FqTraitName, crate::defs::DefId)> { let trait_type = header.trait_type.as_ref()?; - if super::trait_env::get_type_name_static(&header.ty) != declared_name + let head = self.import_original_name( + &super::trait_env::get_type_name_static(&header.ty), + impl_module, + ); + if head != declared_name || !matches_arg_type(trait_type, &header.ty, impl_module, &header.type_params) { return None; diff --git a/wado-compiler/tests/fixtures/aliased_impl_head.wado b/wado-compiler/tests/fixtures/aliased_impl_head.wado new file mode 100644 index 00000000000..9e95139fad7 --- /dev/null +++ b/wado-compiler/tests/fixtures/aliased_impl_head.wado @@ -0,0 +1,19 @@ +// An impl block writes its target under the alias its own module imported, so +// the header's head is neither the declaration's name nor a call site's. + +use { Far as Near } from "./sub/aliased_impl_head_a.wado"; + +struct Local { + v: i32, +} + +impl From for Near { + fn from(value: Local) -> Near { + return Near { v: value.v * 10 }; + } +} + +test "a conversion impl written under an alias answers its target" { + let n = Near::from(Local { v: 3 }); + assert n.v == 30; +} diff --git a/wado-compiler/tests/fixtures/sub/aliased_impl_head_a.wado b/wado-compiler/tests/fixtures/sub/aliased_impl_head_a.wado new file mode 100644 index 00000000000..30d070e1c89 --- /dev/null +++ b/wado-compiler/tests/fixtures/sub/aliased_impl_head_a.wado @@ -0,0 +1,5 @@ +// The receiver, declared under a name the impl block never writes. + +pub struct Far { + pub v: i32, +} From f1233aa2dcf0c78bcec03972daf0266bad399c25 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 18:10:00 +0000 Subject: [PATCH 23/25] test: golden WIR for the aliased impl head fixture Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- .../fixtures/aliased_impl_head.wir.wado | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 wado-compiler/tests/generated/fixtures/aliased_impl_head.wir.wado diff --git a/wado-compiler/tests/generated/fixtures/aliased_impl_head.wir.wado b/wado-compiler/tests/generated/fixtures/aliased_impl_head.wir.wado new file mode 100644 index 00000000000..a69d30fc0a7 --- /dev/null +++ b/wado-compiler/tests/generated/fixtures/aliased_impl_head.wir.wado @@ -0,0 +1,32 @@ +// Golden file: WIR with -O2 optimization +// Source: wado-compiler/tests/fixtures/aliased_impl_head.wado +// Generated by: mise run update-golden-fixtures + +type "functype//mem/realloc" = fn(i32, i32, i32, i32) -> i32; // TypeId(0) + +type "functype//wasi/task-return" = fn(i32); // TypeId(1) + +type "functype//aliased_impl_head.wado/__cm_export____test_0_a_conversion_impl_written_under_an_alias_answers_its_target" = fn(); // TypeId(2) + +type "functype//core:prelude/fpfmt.wado/__initialize_module" = fn(); // TypeId(3) + +import fn mem/realloc from "mem/realloc"; +import fn wasi/task-return from "wasi/task-return"; +import memory (1) from "mem/memory"; + +global mut global:aliased_impl_head.wado::__modules_initialized: bool = 0; + +fn "aliased_impl_head.wado/__cm_export____test_0_a_conversion_impl_written_under_an_alias_answers_its_target"() { + __inline___initialize_modules_0: block { + break_if __inline___initialize_modules_0 @likely(global:aliased_impl_head.wado::__modules_initialized); + cold_path; + "core:prelude/fpfmt.wado/__initialize_module"(); + global:aliased_impl_head.wado::__modules_initialized = 1; + }; + "wasi/task-return"(0); +} + +fn "core:prelude/fpfmt.wado/__initialize_module"() { +} + +export fn "aliased_impl_head.wado/__cm_export____test_0_a_conversion_impl_written_under_an_alias_answers_its_target" as "__test_0_a_conversion_impl_written_under_an_alias_answers_its_target" From dcea3bd91bf612c2ac00b826c6c71263dd6b9224 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 18:50:30 +0000 Subject: [PATCH 24/25] fix(elaborator): keep an impl header's binder head unaliased A block's own type parameter shadows any import, so a head its `type_params` bind is already a declaration name and must not be resolved through the module's aliases. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- wado-compiler/src/elaborator/method_call.rs | 25 ++++++++++++++------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index 81872b35219..ef8fa4c7b6c 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -3301,9 +3301,7 @@ impl Elaborator<'_, H> { continue; }; let base = super::trait_env::get_type_name_static(trait_type); - let head = self - .import_original_name(&super::trait_env::get_type_name_static(&header.ty), &module); - if head != declared_name + if self.impl_head_decl_name(header, &module) != declared_name || (base != from_trait_name && base != "TryFrom") || !header.methods.iter().any(|m| m.name == method_name) { @@ -3369,6 +3367,21 @@ impl Elaborator<'_, H> { ) } + /// An impl header's target head as a declaration name. The head is written + /// in the impl's own module, so its alias is un-aliased there — unless the + /// block's own type parameter binds the spelling, which shadows any import. + fn impl_head_decl_name( + &self, + header: &super::trait_env::ImplHeader, + impl_module: &ModuleSource, + ) -> String { + let head = super::trait_env::get_type_name_static(&header.ty); + if header.type_params.iter().any(|p| p.name == head) { + return head; + } + self.import_original_name(&head, impl_module) + } + /// Whether `rendered` names `param` as a whole segment — the spelling-level /// stand-in for "this type mentions the impl's type parameter". fn mentions_type_param(rendered: &str, param: &str) -> bool { @@ -3488,11 +3501,7 @@ impl Elaborator<'_, H> { impl_module: &ModuleSource| -> Option<(crate::name::FqTraitName, crate::defs::DefId)> { let trait_type = header.trait_type.as_ref()?; - let head = self.import_original_name( - &super::trait_env::get_type_name_static(&header.ty), - impl_module, - ); - if head != declared_name + if self.impl_head_decl_name(header, impl_module) != declared_name || !matches_arg_type(trait_type, &header.ty, impl_module, &header.type_params) { return None; From 0870af99f1faa594e544dedaccae05d83b546a0a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 24 Aug 2026 19:57:15 +0000 Subject: [PATCH 25/25] refactor(elaborator): one receiver-key derivation for static lookups The five copies of "the caller's resolved key, else the written name" become one helper, and the receiver-target step folds into the impl-key lookup it only ever fed. Drops the `_keyed` suffix where no unkeyed sibling remains. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XhVvHrRoLBEabo8aZqSkHG --- docs/compiler.md | 2 +- docs/wep-2026-08-12-declaration-identity.md | 15 ++- wado-compiler/src/elaborator/call.rs | 6 +- wado-compiler/src/elaborator/callee.rs | 7 +- wado-compiler/src/elaborator/method_call.rs | 118 ++++++++---------- .../cross_module_same_name_conversion.wado | 4 +- 6 files changed, 67 insertions(+), 85 deletions(-) diff --git a/docs/compiler.md b/docs/compiler.md index 6c1bb092d5b..5f322ecb091 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -78,7 +78,7 @@ The AST is parser-immutable from this point on. The desugar-replacement surface `semantics_of` (`semantics.rs`) is the entry point shared by LSP and batch compilation. It runs `analyze.rs` for the symbol table, then `elaborator/` for the phases below ([WEP 2026-05-26](./wep-2026-05-26-elaborator-rearchitecture.md)): -- **Resolve** answers every reference site once, from the module that wrote it, and identifies every declaration (`defs.rs`, `resolve.rs`, [WEP 2026-08-12](./wep-2026-08-12-declaration-identity.md)). A reference site is answered nowhere else; the positions no site answers derive their `DefId` from a module the caller names. +- **Resolve** answers every reference site once, from the module that wrote it, and identifies every declaration (`defs.rs`, `resolve.rs`, [WEP 2026-08-12](./wep-2026-08-12-declaration-identity.md)). Nothing else answers a site; a position that has none derives its `DefId` from a module the caller names. - **Annotate** runs a declaration pass over every module — types, `TraitEnv`, `Signatures` — then a body walk whose sole output is a populated `ModuleSemantics`. - **Liveness** computes source-level reachability, which gates what reify emits and feeds the unused diagnostics. - **Reify** reads the recorded facts back and emits one `TirModule` per module. No inference, no dispatch decisions. diff --git a/docs/wep-2026-08-12-declaration-identity.md b/docs/wep-2026-08-12-declaration-identity.md index 7741c5c6474..4b8c64fe7b9 100644 --- a/docs/wep-2026-08-12-declaration-identity.md +++ b/docs/wep-2026-08-12-declaration-identity.md @@ -473,12 +473,15 @@ The same derivation in the `Symbol` currency, which §5's `DefId` columns subsum module. Each names its module rather than searching for one, so no vantage is supplied. -One rendering still compared against a declaration's own, on the paths that -name no block: an auto-derived `Eq` / `Ord`, and a method reached through a -type parameter's bound, whose block monomorphization picks. A dispatch that -matched a concrete block reads the module off it instead. - -- `impl_target_decl_key` — a receiver's newtype chain against an impl's head +Renderings still compared against a declaration's own name: + +- `impl_target_decl_key` — a receiver's newtype chain against an impl's head, on + the paths that name no block: an auto-derived `Eq` / `Ord`, and a method + reached through a type parameter's bound, whose block monomorphization picks. + A dispatch that matched a concrete block reads the module off it instead. +- `impl_head_decl_name` — an impl header's own head, filtering a static call's + candidate blocks. Each side resolves in the module that wrote it, so an alias + on either steers neither. The Component Model boundary, permanent for the reason §9 gives: diff --git a/wado-compiler/src/elaborator/call.rs b/wado-compiler/src/elaborator/call.rs index c1757d55caa..a832e28bd10 100644 --- a/wado-compiler/src/elaborator/call.rs +++ b/wado-compiler/src/elaborator/call.rs @@ -1296,7 +1296,7 @@ impl Elaborator<'_, H> { let ns_key = self.namespace_member(prefix, type_name).map(|def| { trait_env::ImplTargetKey::of_decl(self.tysys.resolutions.defs(), def) }); - let param_is_mut = self.lookup_static_method_param_is_mut_keyed( + let param_is_mut = self.lookup_static_method_param_is_mut( type_name, method_name, ns_key.as_ref(), @@ -1319,7 +1319,7 @@ impl Elaborator<'_, H> { // Recorded so reify replays this Call shape without re-running // dispatch. Empty defaults would leave codegen a call short an // argument; empty types would leave reify padding untyped. - let param_defaults = self.lookup_static_method_param_defaults_keyed( + let param_defaults = self.lookup_static_method_param_defaults( type_name, method_name, ns_key.as_ref(), @@ -1839,7 +1839,7 @@ impl Elaborator<'_, H> { Some(&ns_key), ); if !params.is_empty() { - let slots = self.lookup_static_method_slots_keyed(method_name, &ns_key); + let slots = self.lookup_static_method_slots(method_name, &ns_key); return (params, slots); } } diff --git a/wado-compiler/src/elaborator/callee.rs b/wado-compiler/src/elaborator/callee.rs index d05d3466012..5b2d119f6c5 100644 --- a/wado-compiler/src/elaborator/callee.rs +++ b/wado-compiler/src/elaborator/callee.rs @@ -36,11 +36,8 @@ impl CalleeRef { } } - /// A callee reached through a namespace-qualified call `Prefix::name` - /// where `Prefix` names an effect or resource rather than a module. The - /// `prefix` is interned through the elaborator's - /// [`crate::module_source::ModuleSourceInterner`] and wrapped in a - /// `ModuleSource::Local`. + /// A callee reached through `Prefix::name` where `Prefix` names an effect + /// or resource rather than a module, so the prefix itself is the namespace. pub fn local_namespace( interner: &mut ModuleSourceInterner, prefix: &str, diff --git a/wado-compiler/src/elaborator/method_call.rs b/wado-compiler/src/elaborator/method_call.rs index ef8fa4c7b6c..1746c5a4d28 100644 --- a/wado-compiler/src/elaborator/method_call.rs +++ b/wado-compiler/src/elaborator/method_call.rs @@ -1465,11 +1465,9 @@ impl Elaborator<'_, H> { // `Type::::method()` parses as a static-method call and never // reaches `resolve_call`, which checks the bare spelling. The receiver // key comes from the resolved target type, as every lookup below does. - let static_receiver = struct_name_for_lookup.as_ref().map(|name| { - struct_key_for_lookup - .clone() - .unwrap_or_else(|| self.impl_target(name)) - }); + let static_receiver = struct_name_for_lookup + .as_ref() + .map(|name| self.static_receiver_key(name, struct_key_for_lookup.as_ref())); if let (Some(name), Some(receiver)) = (&struct_name_for_lookup, &static_receiver) { self.check_static_call_visibility( receiver, @@ -1517,7 +1515,7 @@ impl Elaborator<'_, H> { let static_method_defaults: Vec<(String, Option)> = struct_name_for_lookup .as_ref() .map(|name| { - self.lookup_static_method_param_defaults_keyed( + self.lookup_static_method_param_defaults( name, &static_call.method, struct_key_for_lookup.as_ref(), @@ -2249,7 +2247,7 @@ impl Elaborator<'_, H> { let param_is_mut = struct_name_for_lookup .as_deref() .map(|name| { - self.lookup_static_method_param_is_mut_keyed( + self.lookup_static_method_param_is_mut( name, &static_call.method, struct_key_for_lookup.as_ref(), @@ -2779,7 +2777,7 @@ impl Elaborator<'_, H> { &mut self, struct_name: &str, method_name: &str, - static_key_hint: Option<&crate::elaborator::trait_env::ImplTargetKey>, + target_hint: Option<&crate::elaborator::trait_env::ImplTargetKey>, ) -> Vec { // O(1) lookup via pre-built static method index. The index is // keyed by the receiver's canonical decl key so two same-named @@ -2788,9 +2786,7 @@ impl Elaborator<'_, H> { // (it threads through the `TypeId`'s module source and so // distinguishes `CounterA::make` from `CounterB::make` even // though both alias the same bare name `"Counter"`). - let static_key = static_key_hint - .cloned() - .unwrap_or_else(|| self.impl_target(struct_name)); + let static_key = self.static_receiver_key(struct_name, target_hint); // Carry the impl's defining module out of the index alongside // the AST so the per-param elaborator can swap into its perspective — // a static method's signature references types the impl module @@ -2876,62 +2872,52 @@ impl Elaborator<'_, H> { /// the same order as [`Self::lookup_static_method_param_types_keyed`]. /// Returns `(param_name, default_expr)` pairs; `default_expr` is `None` for /// parameters without a declared default. - pub(super) fn lookup_static_method_param_defaults_keyed( + pub(super) fn lookup_static_method_param_defaults( &mut self, struct_name: &str, method_name: &str, - static_key_hint: Option<&crate::elaborator::trait_env::ImplTargetKey>, + target_hint: Option<&crate::elaborator::trait_env::ImplTargetKey>, ) -> Vec<(String, Option)> { - let static_key = static_key_hint - .cloned() - .unwrap_or_else(|| self.impl_target(struct_name)); + let static_key = self.static_receiver_key(struct_name, target_hint); // Names and defaults come out of the same record, so their order // matches the parameter types by construction. - let indexed = self - .unique_static_method_sig(&static_key, method_name) - .map(|sig| crate::elaborator::sig::Param::named_defaults(&sig.params)); - if let Some(defaults) = indexed { - return defaults; - } - - Vec::new() + self.unique_static_method_sig(&static_key, method_name) + .map(|sig| crate::elaborator::sig::Param::named_defaults(&sig.params)) + .unwrap_or_default() } - /// The receiver a static lookup written `struct_name` keys on: the - /// declaration, and its declared name. Neither side's spelling is that - /// name — a call site and an impl header each write whatever their own - /// module imported — so a header's head is un-aliased before comparing. - fn static_receiver_target( + /// The receiver a static lookup keys on: the key its caller already + /// resolved, else the written name over the walking module's frame. + fn static_receiver_key( &self, struct_name: &str, target_hint: Option<&ImplTargetKey>, - ) -> (ImplTargetKey, String) { - let target = target_hint + ) -> ImplTargetKey { + target_hint .cloned() - .unwrap_or_else(|| self.impl_target(struct_name)); - let declared = target - .type_name(self.tysys.resolutions.defs()) - .unwrap_or(struct_name) - .to_string(); - (target, declared) + .unwrap_or_else(|| self.impl_target(struct_name)) } - /// Keys of the *trait* impl blocks whose target head is `target`, - /// current-module-first. A receiver reaches two namespaces: usually a - /// declaration, but an impl binding its name as its own type parameter - /// (`impl Trait for V`) keys under that binder instead. Both are - /// searched in the current module, only the declaration namespace outside - /// it. Consumers re-check the impl's spelling against `declared_name`. - fn trait_impl_keys_current_first( + /// The *trait* impl blocks a receiver written `struct_name` reaches, + /// current-module-first, with the declared name their heads must spell. + /// + /// A receiver reaches two namespaces: its declaration, and an impl binding + /// the name as its own type parameter (`impl Trait for V`), which + /// keys under that binder. Both are searched in the current module, only + /// the declaration namespace outside it. + fn trait_impls_for_receiver( &self, - target: &ImplTargetKey, - declared_name: &str, - ) -> Vec { - let env = &self.tysys.trait_env; + struct_name: &str, + target_hint: Option<&ImplTargetKey>, + ) -> (Vec, String) { let defs = self.tysys.resolutions.defs(); + let target = self.static_receiver_key(struct_name, target_hint); + let declared_name = target.type_name(defs).unwrap_or(struct_name).to_string(); + + let env = &self.tysys.trait_env; let declared = env.entries_by_receiver_vec(&target.receiver(defs)); let binder = - env.entries_by_receiver_vec(&Receiver::Type(FqTypeName::binder(declared_name))); + env.entries_by_receiver_vec(&Receiver::Type(FqTypeName::binder(&declared_name))); let is_current = |k: &&crate::defs::DefId| *defs.module(**k) == self.current_module_source; let mut keys: Vec = declared .iter() @@ -2940,7 +2926,7 @@ impl Elaborator<'_, H> { .copied() .collect(); keys.extend(declared.iter().filter(|k| !is_current(k)).copied()); - keys + (keys, declared_name) } /// Canonical signatures of the methods named `method_name` declared on @@ -3030,7 +3016,7 @@ impl Elaborator<'_, H> { /// The declared type-param slots of a static method, keyed like /// [`Self::lookup_static_method_param_types_keyed`]. - pub(super) fn lookup_static_method_slots_keyed( + pub(super) fn lookup_static_method_slots( &self, method_name: &str, static_key: &crate::elaborator::trait_env::ImplTargetKey, @@ -3043,15 +3029,13 @@ impl Elaborator<'_, H> { /// Whether each parameter of a static method is `mut`, empty for an unknown /// method. The receiver key is pre-resolved where the caller holds one — a /// namespace member's bare spelling cannot reach it. - pub(super) fn lookup_static_method_param_is_mut_keyed( + pub(super) fn lookup_static_method_param_is_mut( &self, struct_name: &str, method_name: &str, - static_key_hint: Option<&crate::elaborator::trait_env::ImplTargetKey>, + target_hint: Option<&crate::elaborator::trait_env::ImplTargetKey>, ) -> Vec { - let type_target = static_key_hint - .cloned() - .unwrap_or_else(|| self.impl_target(struct_name)); + let type_target = self.static_receiver_key(struct_name, target_hint); self.impl_method_sigs(&type_target, method_name) .into_iter() .find(|sig| sig.self_kind == ast::SelfKind::None) @@ -3227,9 +3211,7 @@ impl Elaborator<'_, H> { method_name: &str, target_hint: Option<&ImplTargetKey>, ) -> bool { - let target = target_hint - .cloned() - .unwrap_or_else(|| self.impl_target(struct_name)); + let target = self.static_receiver_key(struct_name, target_hint); let keys = self.tysys.trait_env.inherent_impl_keys(&target); self.keys_declare_static_method(&keys, method_name) } @@ -3293,8 +3275,8 @@ impl Elaborator<'_, H> { .to_string(); let mut candidates: Vec = Vec::new(); let mut has_blanket = false; - let (target, declared_name) = self.static_receiver_target(struct_name, target_hint); - for impl_def in self.trait_impl_keys_current_first(&target, &declared_name) { + let (impl_defs, declared_name) = self.trait_impls_for_receiver(struct_name, target_hint); + for impl_def in impl_defs { let header = &self.tysys.trait_env.impl_headers[&impl_def]; let module = self.tysys.resolutions.defs().module(impl_def).clone(); let Some(trait_type) = header.trait_type.as_ref() else { @@ -3367,9 +3349,9 @@ impl Elaborator<'_, H> { ) } - /// An impl header's target head as a declaration name. The head is written - /// in the impl's own module, so its alias is un-aliased there — unless the - /// block's own type parameter binds the spelling, which shadows any import. + /// An impl header's target head as a declaration name, resolved through the + /// impl's own imports — unless its type parameters bind the spelling, which + /// shadows them. fn impl_head_decl_name( &self, header: &super::trait_env::ImplHeader, @@ -3397,7 +3379,7 @@ impl Elaborator<'_, H> { arg_type_name: Option<&str>, target_hint: Option<&ImplTargetKey>, ) -> Option { - let (target, declared_name) = self.static_receiver_target(struct_name, target_hint); + let (impl_defs, declared_name) = self.trait_impls_for_receiver(struct_name, target_hint); let from_trait_name = self .tysys .type_table @@ -3534,7 +3516,7 @@ impl Elaborator<'_, H> { None }; - for impl_def in self.trait_impl_keys_current_first(&target, &declared_name) { + for impl_def in impl_defs { let header = &self.tysys.trait_env.impl_headers[&impl_def]; let module_source = self.tysys.resolutions.defs().module(impl_def).clone(); if let Some((trait_name, method_id)) = check_impl(header, &module_source) { @@ -3891,13 +3873,13 @@ impl Elaborator<'_, H> { }) }; - let param_is_mut = self.lookup_static_method_param_is_mut_keyed( + let param_is_mut = self.lookup_static_method_param_is_mut( &actual_struct_name, method_name, receiver_key.as_ref(), ); - let param_defaults = self.lookup_static_method_param_defaults_keyed( + let param_defaults = self.lookup_static_method_param_defaults( &actual_struct_name, method_name, receiver_key.as_ref(), diff --git a/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado b/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado index 4f1e4fd77c1..65e5ef95600 100644 --- a/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado +++ b/wado-compiler/tests/fixtures/cross_module_same_name_conversion.wado @@ -1,5 +1,5 @@ -// The caller declares a `Meters` of its own, so a conversion surveyed by the -// written name reaches the wrong module's impl. +// An import alias beside a same-named local type: the receiver's declaration +// picks the impl, and neither module's spelling of it can. use { Meters as M } from "./sub/cross_module_same_name_conversion_a.wado";