From 538b1d01ccf049c496f2e562f8ed249a29ff7db9 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 23 Oct 2026 14:10:48 +0300 Subject: [PATCH 1/3] feat: add poisoned names to module scope --- src/ast/analyze/call.rs | 10 ++++------ src/ast/analyze/mod.rs | 2 +- src/ast/scope/mod.rs | 12 ++++++++++-- src/ast/scope/modules.rs | 21 +++++++++++++++------ src/str.rs | 7 +++++++ 5 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/ast/analyze/call.rs b/src/ast/analyze/call.rs index 3073e98a..24a07ffb 100644 --- a/src/ast/analyze/call.rs +++ b/src/ast/analyze/call.rs @@ -345,11 +345,9 @@ impl CallName { parse::CallName::TypeCast(target) => { scope.resolve(target).map(Self::TypeCast).with_span(from) } - parse::CallName::Custom(name) => { - scope.get_function(name).map(Self::Custom).with_span(from) - } + parse::CallName::Custom(name) => Ok(Self::Custom(scope.get_function(name, from)?)), parse::CallName::ArrayFold(name, size) => { - let function = scope.get_function(name).with_span(from)?; + let function = scope.get_function(name, from)?; // A function that is used in a array fold has the signature: // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 @@ -361,7 +359,7 @@ impl CallName { } } parse::CallName::Fold(name, bound) => { - let function = scope.get_function(name).with_span(from)?; + let function = scope.get_function(name, from)?; // A function that is used in a list fold has the signature: // fn f(element: E, accumulator: A) -> A if function.params().len() != 2 @@ -373,7 +371,7 @@ impl CallName { } } parse::CallName::ForWhile(name) => { - let function = scope.get_function(name).with_span(from)?; + let function = scope.get_function(name, from)?; // A function that is used in a for-while loop has the signature: // fn f(accumulator: A, readonly_context: C, counter: u{N}) -> Either // where diff --git a/src/ast/analyze/mod.rs b/src/ast/analyze/mod.rs index 533a6b1a..df562d29 100644 --- a/src/ast/analyze/mod.rs +++ b/src/ast/analyze/mod.rs @@ -41,7 +41,7 @@ mod span_tests { Function::analyze(&parsed, &ResolvedType::unit(), &mut scope).expect("function analyzes"); let function = scope - .get_function(parsed.name()) + .get_function(parsed.name(), &parsed) .expect("function is registered in scope"); assert_eq!(function.span().to_slice(source), Some(source)); diff --git a/src/ast/scope/mod.rs b/src/ast/scope/mod.rs index 7d43e1eb..4c092c2c 100644 --- a/src/ast/scope/mod.rs +++ b/src/ast/scope/mod.rs @@ -288,8 +288,16 @@ impl Scope { self.modules.insert_function(name, visibility, function) } - pub(super) fn get_function(&self, name: &FunctionName) -> Result { - self.modules.get_function(name) + pub(super) fn get_function( + &self, + name: &FunctionName, + span: impl Into, + ) -> Result { + match self.modules.get_function(name) { + Ok(function) => Ok(function), + Err(_) if self.modules.is_poisoned(name.as_str()) => Err(Failure::Reported), + Err(error) => Err(error.with_span(span.into()).into()), + } } /// Insert a variable into the current block. diff --git a/src/ast/scope/modules.rs b/src/ast/scope/modules.rs index 7ab5eb2f..dd4cc17e 100644 --- a/src/ast/scope/modules.rs +++ b/src/ast/scope/modules.rs @@ -1,5 +1,5 @@ use std::collections::hash_map::Entry; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use crate::ast::CustomFunction; @@ -16,6 +16,8 @@ struct ModuleScope { functions: HashMap, /// Nested inling `mod` blocks, each becoming a child scope. submodules: HashMap, + /// Names of failed imports, whose uses are not reported again. + poisoned: HashSet, } /// The modules of the program and the current position inside them. @@ -87,11 +89,18 @@ impl ModuleTree { /// /// * [`Error::UndefinedAlias`]: The alias is not defined in the current scope. pub(super) fn get_alias(&self, name: &AliasName) -> Result { - self.current() - .aliases - .get(name) - .map(|(ty, _)| ty.clone()) - .ok_or_else(|| Error::UndefinedAlias { name: name.clone() }) + let current = self.current(); + + match current.aliases.get(name) { + Some((ty, _)) => Ok(ty.clone()), + None if current.poisoned.contains(name.as_str()) => Ok(ResolvedType::never()), + None => Err(Error::UndefinedAlias { name: name.clone() }), + } + } + + /// Check whether `name` comes from a failed import in the current module. + pub(super) fn is_poisoned(&self, name: &str) -> bool { + self.current().poisoned.contains(name) } /// Resolve a type with aliases to a type without aliases. diff --git a/src/str.rs b/src/str.rs index 5bf52534..fd84df37 100644 --- a/src/str.rs +++ b/src/str.rs @@ -390,6 +390,13 @@ pub struct SymbolName(Arc); wrapped_string!(SymbolName, "unresolved symbol name"); impl_arbitrary_lowercase_alpha!(SymbolName); +/// Lets a set of symbols be searched by an item name without allocating. +impl std::borrow::Borrow for SymbolName { + fn borrow(&self) -> &str { + self.as_str() + } +} + #[cfg(test)] mod tests { use super::*; From f5c65862216833c1b1c3eabacff282a09421c7a4 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 23 Oct 2026 14:10:48 +0300 Subject: [PATCH 2/3] feat: keep analyzing after a failed use --- src/ast/analyze/program.rs | 2 +- src/ast/scope/mod.rs | 8 +- src/ast/scope/modules.rs | 271 +++++++++++++++++++++++++------------ 3 files changed, 190 insertions(+), 91 deletions(-) diff --git a/src/ast/analyze/program.rs b/src/ast/analyze/program.rs index 9325dd4f..33eedf80 100644 --- a/src/ast/analyze/program.rs +++ b/src/ast/analyze/program.rs @@ -118,7 +118,7 @@ impl AbstractSyntaxTree for Item { Function::analyze(function, ty, scope).map(Self::Function) } parse::Item::Use(use_decl) => { - scope.resolve_use(use_decl).with_span(use_decl)?; + scope.resolve_use(use_decl); Ok(Self::Use) } parse::Item::EnumDeclaration(decl) => { diff --git a/src/ast/scope/mod.rs b/src/ast/scope/mod.rs index 4c092c2c..824d3fe1 100644 --- a/src/ast/scope/mod.rs +++ b/src/ast/scope/mod.rs @@ -258,8 +258,12 @@ impl Scope { self.modules.path() } - pub(super) fn resolve_use(&mut self, use_decl: &UseDecl) -> Result<(), Error> { - self.modules.resolve_use(use_decl) + pub(super) fn resolve_use(&mut self, use_decl: &UseDecl) { + let span = Span::from(use_decl); + + for error in self.modules.resolve_use(use_decl) { + self.report(error.with_span(span)); + } } pub(super) fn get_alias(&self, name: &AliasName) -> Result { diff --git a/src/ast/scope/modules.rs b/src/ast/scope/modules.rs index dd4cc17e..79a2b312 100644 --- a/src/ast/scope/modules.rs +++ b/src/ast/scope/modules.rs @@ -6,7 +6,7 @@ use crate::ast::CustomFunction; use crate::driver::{CRATE_STR, MAIN_STR}; use crate::error::Error; use crate::parse::{self, UseDecl, Visibility}; -use crate::str::{AliasName, FunctionName, ModuleName, SymbolName}; +use crate::str::{AliasName, FunctionName, Identifier, ModuleName, SymbolName}; use crate::types::{AliasedType, EnumInfo, EnumVariantInfo, ResolvedType}; /// A single module namespace. Handles arbitrary nesting via `submodules`. @@ -203,6 +203,8 @@ impl ModuleTree { /// Resolves a `use` declaration by navigating the module tree, checking visibility, /// and importing matching items into the current scope. /// + /// Returns every error instead of stopping at the first one. + /// /// ## Errors /// /// * [`Error::MissingCrateKeyword`] The import path does not start with the `crate` keyword. @@ -210,87 +212,143 @@ impl ModuleTree { /// * [`Error::ModuleIsPrivate`] Attempted to navigate into a private module from an unauthorized scope. /// * [`Error::MainCannotBeAlias`] Attempted to alias an imported item to the reserved `main` identifier. /// * May also return errors propagated from item collection and insertion, such as [`Error::PrivateItem`] or [`Error::RedefinedItem`]. - pub(super) fn resolve_use(&mut self, use_decl: &UseDecl) -> Result<(), Error> { - let path = use_decl.path(); - if path.first().map(|id| id.as_str()) != Some(CRATE_STR) { - return Err(Error::MissingCrateKeyword); - } - + pub(super) fn resolve_use(&mut self, use_decl: &UseDecl) -> Vec { let use_vis = use_decl.visibility().clone(); let use_decl_items = match use_decl.items() { parse::UseItems::Single(elem) => std::slice::from_ref(elem), parse::UseItems::List(elems) => elems.as_slice(), }; - // Phase 1: navigate to target and collect items. Immutable borrow, dropped at end of block - // Vec<(ProcessedAlias, ProcessedFunction, ProcessedModule)> - // where each is Result<(Key, (Value, Visibility)), Error> - let collected: Vec<_> = { - // TODO: Part, that can be optimized - // How many segments do the caller's path and the target's path have in common? - let shared_prefix_len = self - .path + let mut errors = Vec::new(); + // Phase 1: navigate to the target module. If it cannot be reached, every imported name is poisoned. + let Some(target_scope) = self.find_use_target(use_decl.path(), &mut errors) else { + let local_names = use_decl_items .iter() - .zip(&path[1..]) - .take_while(|(curr, nav)| curr.as_str() == nav.as_str()) - .count(); + .map(|(name, aliased)| aliased.as_ref().unwrap_or(name).clone()); + self.current_mut().poisoned.extend(local_names); + return errors; + }; - let mut target_scope = &self.root; + // Phase 2: stage each item. A failed item is reported, then poisoned or imported anyway. + let current = self.current(); + let mut pending = ModuleScope::default(); + for (name, aliased) in use_decl_items { + if aliased.as_ref().is_some_and(|a| a == MAIN_STR) { + errors.push(Error::MainCannotBeAlias); + continue; + } - for (ind, segment) in path[1..].iter().enumerate() { - let name = ModuleName::from_ident(segment); + let local_name = aliased.as_ref().unwrap_or(name); + let stage = |pending: &mut ModuleScope, ignore_privacy: bool| { + Self::resolve_processing_use_items_error(&[ + Self::stage_collected( + Self::try_collect_item( + name, + local_name, + &target_scope.aliases, + &use_vis, + ignore_privacy, + ), + ¤t.aliases, + &mut pending.aliases, + ), + Self::stage_collected( + Self::try_collect_item( + name, + local_name, + &target_scope.functions, + &use_vis, + ignore_privacy, + ), + ¤t.functions, + &mut pending.functions, + ), + Self::stage_collected( + Self::try_collect_item( + name, + local_name, + &target_scope.submodules, + &use_vis, + ignore_privacy, + ), + ¤t.submodules, + &mut pending.submodules, + ), + ]) + }; + + let Err(error) = stage(&mut pending, false) else { + continue; + }; + + match error { + Error::UnresolvedItem { .. } => { + pending.poisoned.insert(local_name.clone()); + if target_scope.poisoned.contains(name) { + continue; + } + } + Error::PrivateItem { .. } => { + let _ = stage(&mut pending, true); + } + _ => {} + } + errors.push(error); + } - let (inner, visibility) = target_scope - .submodules - .get(&name) - .ok_or_else(|| Error::ModuleNotFound { name: name.clone() })?; + // Phase 3: commit everything that was staged, including the poisoned names. + let current = self.current_mut(); + current.aliases.extend(pending.aliases); + current.functions.extend(pending.functions); + current.submodules.extend(pending.submodules); + current.poisoned.extend(pending.poisoned); - if matches!(visibility, Visibility::Private) && shared_prefix_len < ind { - return Err(Error::ModuleIsPrivate { name }); - } + errors + } - target_scope = inner; - } + /// Navigate to the module that a `use` path names. + /// + /// A private module is reported but still entered, because its items are known. + /// Returns `None` if the module does not exist. + fn find_use_target( + &self, + path: &[Identifier], + errors: &mut Vec, + ) -> Option<&ModuleScope> { + if path.first().map(|id| id.as_str()) != Some(CRATE_STR) { + errors.push(Error::MissingCrateKeyword); + return None; + } - let mut collected = Vec::with_capacity(use_decl_items.len()); - for (name, aliased) in use_decl_items { - if aliased.as_ref().is_some_and(|a| a == MAIN_STR) { - return Err(Error::MainCannotBeAlias); - } + // TODO: Part, that can be optimized + // How many segments do the caller's path and the target's path have in common? + let shared_prefix_len = self + .path + .iter() + .zip(&path[1..]) + .take_while(|(curr, nav)| curr.as_str() == nav.as_str()) + .count(); + + let mut target_scope = &self.root; - let local_name = aliased.as_ref().unwrap_or(name); + for (ind, segment) in path[1..].iter().enumerate() { + let name = ModuleName::from_ident(segment); - let alias_res = - Self::try_collect_item(name, local_name, &target_scope.aliases, &use_vis); - let func_res = - Self::try_collect_item(name, local_name, &target_scope.functions, &use_vis); - let mod_res = - Self::try_collect_item(name, local_name, &target_scope.submodules, &use_vis); + let Some((inner, visibility)) = target_scope.submodules.get(&name) else { + if !target_scope.poisoned.contains(name.as_str()) { + errors.push(Error::ModuleNotFound { name }); + } + return None; + }; - collected.push((alias_res, func_res, mod_res)); + if matches!(visibility, Visibility::Private) && shared_prefix_len < ind { + errors.push(Error::ModuleIsPrivate { name }); } - collected - }; - // Phase 2: validate against existing names and stage the complete import. - // Failed declarations discard these maps without changing the module tree. - let current = self.current(); - let mut pending = ModuleScope::default(); - for (alias_res, func_res, mod_res) in collected { - Self::resolve_processing_use_items_error(&[ - Self::stage_collected(alias_res, ¤t.aliases, &mut pending.aliases), - Self::stage_collected(func_res, ¤t.functions, &mut pending.functions), - Self::stage_collected(mod_res, ¤t.submodules, &mut pending.submodules), - ])?; + target_scope = inner; } - // Phase 3: commit because every item was resolved successfully - let current = self.current_mut(); - current.aliases.extend(pending.aliases); - current.functions.extend(pending.functions); - current.submodules.extend(pending.submodules); - - Ok(()) + Some(target_scope) } /// Attempts to find `name` in `target_map` and prepare it for import into another scope. @@ -304,6 +362,7 @@ impl ModuleTree { local_name: &SymbolName, target_map: &HashMap, use_vis: &Visibility, + ignore_privacy: bool, ) -> Result<(K, (V, Visibility)), Error> where K: Eq + std::hash::Hash + From + Clone, @@ -316,7 +375,7 @@ impl ModuleTree { name: name.to_string(), })?; - if matches!(vis, Visibility::Private) { + if matches!(vis, Visibility::Private) && !ignore_privacy { return Err(Error::PrivateItem { name: name.to_string(), }); @@ -391,19 +450,28 @@ impl ModuleTree { } #[cfg(test)] -mod transactional_use_tests { +mod use_tests { use crate::parse::ParseFromStr; use crate::str::AliasName; use crate::types::{ResolvedType, TypeConstructible}; use super::*; - fn resolve(scope: &mut ModuleTree, source: &str) -> Result<(), Error> { + fn resolve(scope: &mut ModuleTree, source: &str) -> Vec { let program = parse::Program::parse_from_str(source).unwrap(); let parse::Item::Use(decl) = &program.items()[0] else { panic!("expected use declaration"); }; - scope.resolve_use(decl) + + scope + .resolve_use(decl) + .iter() + .map(ToString::to_string) + .collect() + } + + fn alias(scope: &ModuleTree, name: &str) -> Option { + scope.get_alias(&AliasName::from_str_unchecked(name)).ok() } fn scope() -> ModuleTree { @@ -427,35 +495,62 @@ mod transactional_use_tests { } #[test] - fn failed_imports_preserve_the_complete_module_tree() { - for (source, expected) in [ + fn failed_imports_keep_the_rest_of_the_declaration() { + let missing = Error::UnresolvedItem { + name: "Missing".into(), + }; + let secret = Error::PrivateItem { + name: "Secret".into(), + }; + let same = Error::RedefinedItem { + name: "Same".into(), + }; + let taken = Error::RedefinedItem { + name: "Taken".into(), + }; + let module = Error::ModuleNotFound { + name: ModuleName::from_str_unchecked("nowhere"), + }; + + for (source, expected, bound) in [ ( "use crate::source::{Good, Missing};", - Error::UnresolvedItem { - name: "Missing".into(), - }, + vec![missing], + vec![ + ("Good", ResolvedType::u32()), + ("Missing", ResolvedType::never()), + ], ), ( "use crate::source::{Good, Secret};", - Error::PrivateItem { - name: "Secret".into(), - }, + vec![secret], + vec![ + ("Good", ResolvedType::u32()), + ("Secret", ResolvedType::u16()), + ], ), ( "use crate::source::{Good as Same, Good as Same};", - Error::RedefinedItem { - name: "Same".into(), - }, + vec![same], + vec![("Same", ResolvedType::u32())], ), ( "use crate::source::{Good, Good as main};", - Error::MainCannotBeAlias, + vec![Error::MainCannotBeAlias], + vec![("Good", ResolvedType::u32())], ), ( "use crate::source::{Good, Good as Taken};", - Error::RedefinedItem { - name: "Taken".into(), - }, + vec![taken], + vec![("Good", ResolvedType::u32()), ("Taken", ResolvedType::u8())], + ), + ( + "use crate::nowhere::{Good, Other};", + vec![module], + vec![ + ("Good", ResolvedType::never()), + ("Other", ResolvedType::never()), + ], ), ] { for nested in [false, true] { @@ -474,15 +569,15 @@ mod transactional_use_tests { (ResolvedType::u8(), Visibility::Private), ); - let before = scope.root.clone(); let path = scope.path.clone(); - let error = resolve(&mut scope, source).expect_err(source); + let errors = resolve(&mut scope, source); - assert_eq!(error.to_string(), expected.to_string(), "{source}"); - assert_eq!(scope.root, before, "failed import mutated scope: {source}"); + let expected: Vec<_> = expected.iter().map(ToString::to_string).collect(); + assert_eq!(errors, expected, "{source}"); + for (name, ty) in &bound { + assert_eq!(alias(&scope, name).as_ref(), Some(ty), "{name} in {source}"); + } assert_eq!(scope.path, path, "{source}"); - - resolve(&mut scope, "use crate::source::Good;").unwrap(); } } } @@ -500,7 +595,7 @@ mod transactional_use_tests { ModuleName::from_str_unchecked("Good"), (ModuleScope::default(), Visibility::Public), ); - resolve(&mut scope, "pub use crate::source::Good as Renamed;").unwrap(); + assert!(resolve(&mut scope, "pub use crate::source::Good as Renamed;").is_empty()); assert_eq!( scope From 379420243e58dbed229d5d6b5954d5c99fedf63e Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 23 Oct 2026 14:10:48 +0300 Subject: [PATCH 3/3] tests: use recovery --- src/ast/scope/modules.rs | 31 ++++++++++++++++++++ src/ast/tests/multi_error.rs | 56 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/ast/scope/modules.rs b/src/ast/scope/modules.rs index 79a2b312..2d48b61e 100644 --- a/src/ast/scope/modules.rs +++ b/src/ast/scope/modules.rs @@ -582,6 +582,37 @@ mod use_tests { } } + #[test] + fn uses_of_poisoned_names_are_not_reported() { + let mut scope = scope(); + scope + .enter( + ModuleName::from_str_unchecked("reexport"), + Visibility::Public, + ) + .unwrap(); + assert_eq!( + resolve(&mut scope, "pub use crate::source::Missing;").len(), + 1 + ); + scope.exit(); + + for source in [ + "use crate::reexport::Missing;", + "use crate::reexport::Missing::Inner;", + ] { + let mut scope = ModuleTree { + root: scope.root.clone(), + path: Vec::new(), + }; + assert_eq!( + resolve(&mut scope, source), + Vec::::new(), + "{source}" + ); + } + } + #[test] fn successful_import_commits_all_namespaces_and_visibility() { let mut scope = scope(); diff --git a/src/ast/tests/multi_error.rs b/src/ast/tests/multi_error.rs index 863549ae..553c2176 100644 --- a/src/ast/tests/multi_error.rs +++ b/src/ast/tests/multi_error.rs @@ -491,3 +491,59 @@ fn broken_let_still_defines_its_variables() { ], ); } + +#[test] +fn broken_use_does_not_stop_the_analysis() { + assert_errors( + "mod m { + pub fn f() -> u32 { 1 } + } + + use crate::m::{f, Missing}; + use crate::nowhere::g; + + fn h() -> u32 { x } + + fn main() { + let a: u32 = f(); + let b: Missing = 1; + let c: u32 = g(); + let d: bool = a; + }", + &[ + "Item `Missing` could not be found", + "Module `nowhere` not found", + "Variable `x` is not defined", + "Expected expression of type `bool`, found type `u32`", + ], + ); +} + +#[test] +fn private_import_is_still_checked() { + assert_errors( + "mod m { + type Secret = u16; + + mod inner { + pub type Word = u8; + } + } + + use crate::m::Secret; + use crate::m::inner::Word; + + fn main() { + let s: Secret = 1; + let w: Word = 2; + let t: bool = s; + let u: bool = w; + }", + &[ + "Item `Secret` is private", + "Module `inner` is private", + "Expected expression of type `bool`, found type `u16`", + "Expected expression of type `bool`, found type `u8`", + ], + ); +}