From d2f9b30fcd1dee5a48c6354c514d47b16fff9c6e Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 11 Jun 2026 20:34:32 -0500 Subject: [PATCH] feat: add tests & combine interface registries --- crates/core/src/loader.rs | 1 + crates/core/src/loader/dependency.rs | 4 +- crates/core/src/loader/registry.rs | 316 ++++++++++++++++++ crates/core/src/resolve.rs | 309 ++++------------- crates/core/src/types.rs | 303 +++++++++-------- ...14_project_compilation_and_dependencies.md | 17 +- 6 files changed, 537 insertions(+), 413 deletions(-) create mode 100644 crates/core/src/loader/registry.rs diff --git a/crates/core/src/loader.rs b/crates/core/src/loader.rs index c81cea6..55dd644 100644 --- a/crates/core/src/loader.rs +++ b/crates/core/src/loader.rs @@ -1 +1,2 @@ pub mod dependency; +pub(crate) mod registry; diff --git a/crates/core/src/loader/dependency.rs b/crates/core/src/loader/dependency.rs index 9176a85..1a2265a 100644 --- a/crates/core/src/loader/dependency.rs +++ b/crates/core/src/loader/dependency.rs @@ -7,7 +7,7 @@ use crate::diagnostic::{Diagnostic, DiagnosticCode, Diagnostics}; use crate::project::{Dependency, DependencySource, DependencyToml}; use crate::source::{SourceFile, SourceFileId}; use crate::types::ModuleInterface; -use crate::{ast, parse, target, types}; +use crate::{ast, parse, target}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct DependencyPackage { @@ -108,7 +108,7 @@ fn load_pkg_interfaces( .join("/"); match parse_module(&path, source_id, compile_target) { Ok(module) => { - interfaces.insert(module_name, types::interface_from_ast(&module)); + interfaces.insert(module_name, ModuleInterface::from(&module)); } Err(mut errors) => diagnostics.append(&mut errors), } diff --git a/crates/core/src/loader/registry.rs b/crates/core/src/loader/registry.rs new file mode 100644 index 0000000..124a450 --- /dev/null +++ b/crates/core/src/loader/registry.rs @@ -0,0 +1,316 @@ +use std::collections::HashMap; + +use crate::ast::{self, Declaration}; +use crate::types::{ConstructorInfo, ModuleInterface, Type}; +use crate::{labels::FunctionLabelMap, resolve::Namespace}; +use crate::{source::Span, stdlib::StdlibRegistry}; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ResolveInterfaceRegistry { + modules: HashMap, + prelude: ResolveModuleInterface, +} + +impl ResolveInterfaceRegistry { + pub fn for_single_file() -> Self { + Self::default().with_prelude_interface().with_stdlib_interfaces() + } + + pub fn for_project<'a>( + dependency_interfaces: &HashMap, + modules: impl IntoIterator, + ) -> Self { + Self::default() + .with_prelude_interface() + .with_stdlib_interfaces() + .with_dependency_interfaces(dependency_interfaces) + .with_project_interfaces(modules) + } + + fn with_prelude_interface(mut self) -> Self { + self.prelude = ResolveModuleInterface::prelude(); + self + } + + fn with_stdlib_interfaces(mut self) -> Self { + self.modules.extend(stdlib_resolve_interfaces()); + self + } + + fn with_dependency_interfaces(mut self, interfaces: &HashMap) -> Self { + self.modules.extend(dependency_resolve_interfaces(interfaces)); + self + } + + fn with_project_interfaces<'a>(mut self, modules: impl IntoIterator) -> Self { + self.modules + .extend(modules.into_iter().map(|(name, module)| (name.clone(), module.into()))); + self + } + + pub fn get(&self, module: &str) -> Option<&ResolveModuleInterface> { + self.modules.get(module) + } + + pub fn member(&self, module: &str, namespace: Namespace, name: &str) -> Option<&ResolveModuleMember> { + self.get(module)?.members.get(&(namespace, name.to_string())) + } + + pub fn has_public_member(&self, namespace: Namespace, name: &str) -> bool { + self.modules + .values() + .chain(std::iter::once(&self.prelude)) + .any(|interface| { + interface + .members + .get(&(namespace, name.to_string())) + .is_some_and(|member| member.public) + }) + } + + pub fn prelude_members(&self) -> impl Iterator { + self.prelude.members.iter() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolveModuleMember { + pub public: bool, + pub span: Span, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ResolveModuleInterface { + pub members: HashMap<(Namespace, String), ResolveModuleMember>, +} + +impl ResolveModuleInterface { + fn prelude() -> Self { + let members = ModuleInterface::prelude_type_names() + .into_iter() + .map(|name| { + ( + (Namespace::Type, name.to_string()), + ResolveModuleMember { public: true, span: ModuleInterface::module_span() }, + ) + }) + .collect(); + Self { members } + } +} + +impl From<&ast::Module> for ResolveModuleInterface { + fn from(value: &ast::Module) -> Self { + // TODO: can this be constructed from an iterator? + let mut members = HashMap::new(); + for function in &value.functions { + members.insert( + (Namespace::Value, function.name.text.clone()), + ResolveModuleMember { public: function.public, span: function.name.span }, + ); + } + + for declaration in &value.declarations { + match declaration { + Declaration::Constant(constant) => { + members.insert( + (Namespace::Value, constant.name.text.clone()), + ResolveModuleMember { public: constant.public, span: constant.span }, + ); + } + Declaration::ExternalFunction(function) => { + members.insert( + (Namespace::Value, function.name.text.clone()), + ResolveModuleMember { public: function.public, span: function.span }, + ); + } + Declaration::ExternalType(type_) => { + members.insert( + (Namespace::Type, type_.name.text.clone()), + ResolveModuleMember { public: type_.public, span: type_.span }, + ); + } + Declaration::TypeDefinition(type_) => { + members.insert( + (Namespace::Type, type_.name.text.clone()), + ResolveModuleMember { public: type_.public, span: type_.span }, + ); + let exported_details = type_.public && !type_.opaque; + for constructor in &type_.constructors { + members.insert( + (Namespace::Constructor, constructor.name.text.clone()), + ResolveModuleMember { public: exported_details, span: constructor.span }, + ); + for argument in &constructor.arguments { + if let Some(label) = &argument.label { + members.insert( + (Namespace::Field, label.text.clone()), + ResolveModuleMember { public: exported_details, span: label.span }, + ); + } + } + } + } + Declaration::TypeAlias(alias) => { + members.insert( + (Namespace::Type, alias.name.text.clone()), + ResolveModuleMember { public: alias.public, span: alias.span }, + ); + } + Declaration::TargetGroup(group) => { + let nested = Self::from(&ast::Module { + span: group.span, + declarations: group.declarations.clone(), + imports: Vec::new(), + functions: Vec::new(), + }); + members.extend(nested.members); + } + _ => {} + } + } + + Self { members } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TypeInterfaceRegistry { + pub modules: HashMap, + prelude: ModuleInterface, +} + +impl TypeInterfaceRegistry { + pub fn for_single_file() -> Self { + Self::default().with_prelude_interface().with_stdlib_interfaces() + } + + pub fn for_project<'a>( + dependency_interfaces: &HashMap, + project_modules: impl IntoIterator, + ) -> Self { + Self::default() + .with_prelude_interface() + .with_stdlib_interfaces() + .with_dependency_interfaces(dependency_interfaces) + .with_project_interfaces(project_modules) + } + + fn with_prelude_interface(mut self) -> Self { + self.prelude = ModuleInterface::prelude(); + self + } + + fn with_stdlib_interfaces(mut self) -> Self { + self.modules.extend( + StdlibRegistry::new() + .modules() + .map(|module| (module.name.to_string(), module.interface.clone())), + ); + self + } + + fn with_dependency_interfaces(mut self, interfaces: &HashMap) -> Self { + self.modules.extend(interfaces.clone()); + self + } + + fn with_project_interfaces<'a>(mut self, modules: impl IntoIterator) -> Self { + self.modules.extend( + modules + .into_iter() + .map(|(name, module)| (name.to_string(), ModuleInterface::from(module))), + ); + self + } + + pub fn get(&self, module: &str) -> Option<&ModuleInterface> { + self.modules.get(module) + } + + pub fn constructors(&self) -> HashMap { + self.modules + .values() + .chain(std::iter::once(&self.prelude)) + .flat_map(|i| i.interface_constructors()) + .collect() + } + + pub fn values(&self) -> HashMap { + self.modules + .iter() + .flat_map(|(module, interface)| interface.qualified_values(module)) + .collect() + } + + pub fn function_labels(&self) -> FunctionLabelMap { + self.modules + .iter() + .flat_map(|(module, interface)| interface.qualified_function_labels(module)) + .collect() + } +} + +fn stdlib_resolve_interfaces() -> HashMap { + StdlibRegistry::new() + .modules() + .map(|module| { + let mut members = HashMap::new(); + for name in module.interface.functions.keys() { + members.insert( + (Namespace::Value, name.clone()), + ResolveModuleMember { public: true, span: ModuleInterface::module_span() }, + ); + } + for name in module.interface.types.keys() { + members.insert( + (Namespace::Type, name.clone()), + ResolveModuleMember { public: true, span: ModuleInterface::module_span() }, + ); + } + for name in module.interface.constructors.keys() { + members.insert( + (Namespace::Constructor, name.clone()), + ResolveModuleMember { public: true, span: ModuleInterface::module_span() }, + ); + } + (module.name.to_string(), ResolveModuleInterface { members }) + }) + .collect() +} + +fn dependency_resolve_interfaces( + interfaces: &HashMap, +) -> HashMap { + interfaces + .iter() + .map(|(module, interface)| { + let mut members = HashMap::new(); + for name in interface.functions.keys() { + members.insert( + (Namespace::Value, name.clone()), + ResolveModuleMember { public: true, span: ModuleInterface::module_span() }, + ); + } + for (name, declaration) in &interface.types { + members.insert( + (Namespace::Type, name.clone()), + ResolveModuleMember { public: true, span: declaration.span }, + ); + } + for (name, constructor) in &interface.constructors { + members.insert( + (Namespace::Constructor, name.clone()), + ResolveModuleMember { public: true, span: constructor.span }, + ); + for field in &constructor.fields { + members.insert( + (Namespace::Field, field.name.clone()), + ResolveModuleMember { public: true, span: constructor.span }, + ); + } + } + (module.clone(), ResolveModuleInterface { members }) + }) + .collect() +} diff --git a/crates/core/src/resolve.rs b/crates/core/src/resolve.rs index c3b3ae6..dc62a57 100644 --- a/crates/core/src/resolve.rs +++ b/crates/core/src/resolve.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use crate::ast::{self, Declaration, Expression, Pattern, Statement, UnqualifiedImportKind}; use crate::diagnostic::{Diagnostic, DiagnosticCode, Diagnostics, Label}; -use crate::{parse, project::Project, source::Span, stdlib::StdlibRegistry, target, types}; +use crate::loader::registry::ResolveInterfaceRegistry; +use crate::{parse, project::Project, source::Span, target}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SymbolId(pub u32); @@ -93,102 +94,6 @@ pub struct ResolvedProject { pub modules: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Default)] -struct ModuleInterfaceRegistry { - modules: HashMap, - prelude: ModuleInterface, -} - -impl ModuleInterfaceRegistry { - fn for_single_file() -> Self { - Self::default().with_prelude_interface().with_stdlib_interfaces() - } - - fn for_project<'a>( - dependency_interfaces: &HashMap, - modules: impl IntoIterator, - ) -> Self { - Self::default() - .with_prelude_interface() - .with_stdlib_interfaces() - .with_dependency_interfaces(dependency_interfaces) - .with_project_interfaces(modules) - } - - fn with_prelude_interface(mut self) -> Self { - self.prelude = ModuleInterface::prelude_resolve(); - self - } - - fn with_stdlib_interfaces(mut self) -> Self { - self.modules.extend(stdlib_resolve_interfaces()); - self - } - - fn with_dependency_interfaces(mut self, interfaces: &HashMap) -> Self { - self.modules.extend(dependency_resolve_interfaces(interfaces)); - self - } - - fn with_project_interfaces<'a>(mut self, modules: impl IntoIterator) -> Self { - self.modules - .extend(modules.into_iter().map(|(name, module)| (name.clone(), module.into()))); - self - } - - fn get(&self, module: &str) -> Option<&ModuleInterface> { - self.modules.get(module) - } - - fn member(&self, module: &str, namespace: Namespace, name: &str) -> Option<&ModuleMember> { - self.get(module)?.members.get(&(namespace, name.to_string())) - } - - fn has_public_member(&self, namespace: Namespace, name: &str) -> bool { - self.modules - .values() - .chain(std::iter::once(&self.prelude)) - .any(|interface| { - interface - .members - .get(&(namespace, name.to_string())) - .is_some_and(|member| member.public) - }) - } - - fn prelude_members(&self) -> impl Iterator { - self.prelude.members.iter() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -struct ModuleInterface { - members: HashMap<(Namespace, String), ModuleMember>, -} - -impl ModuleInterface { - fn prelude_resolve() -> ModuleInterface { - let members = [ - "Int", "Float", "String", "BitArray", "Bool", "Nil", "List", "Result", "Option", "Order", - ] - .into_iter() - .map(|name| { - ( - (Namespace::Type, name.to_string()), - ModuleMember { public: true, span: module_span() }, - ) - }) - .collect(); - ModuleInterface { members } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ModuleMember { - public: bool, - span: Span, -} - pub fn resolve(module: ast::Module) -> Result { Resolver::new(module).with_stdlib_interfaces().resolve() } @@ -212,7 +117,7 @@ pub fn resolve_project(project: &Project) -> Result Result, - interfaces: ModuleInterfaceRegistry, + interfaces: ResolveInterfaceRegistry, symbols: Vec, scopes: Vec, references: Vec, @@ -243,7 +148,7 @@ impl Resolver { Self { module, module_name: None, - interfaces: ModuleInterfaceRegistry::default(), + interfaces: ResolveInterfaceRegistry::default(), symbols: Vec::new(), scopes: Vec::new(), references: Vec::new(), @@ -251,12 +156,12 @@ impl Resolver { } } - fn with_project(module: ast::Module, module_name: String, interfaces: ModuleInterfaceRegistry) -> Self { + fn with_project(module: ast::Module, module_name: String, interfaces: ResolveInterfaceRegistry) -> Self { Self { module_name: Some(module_name), interfaces, ..Self::new(module) } } fn with_stdlib_interfaces(mut self) -> Self { - self.interfaces = ModuleInterfaceRegistry::for_single_file(); + self.interfaces = ResolveInterfaceRegistry::for_single_file(); self } @@ -1025,153 +930,6 @@ impl Resolver { } } -fn stdlib_resolve_interfaces() -> HashMap { - StdlibRegistry::new() - .modules() - .map(|module| { - let mut members = HashMap::new(); - for name in module.interface.functions.keys() { - members.insert( - (Namespace::Value, name.clone()), - ModuleMember { public: true, span: module_span() }, - ); - } - for name in module.interface.types.keys() { - members.insert( - (Namespace::Type, name.clone()), - ModuleMember { public: true, span: module_span() }, - ); - } - for name in module.interface.constructors.keys() { - members.insert( - (Namespace::Constructor, name.clone()), - ModuleMember { public: true, span: module_span() }, - ); - } - (module.name.to_string(), ModuleInterface { members }) - }) - .collect() -} - -fn dependency_resolve_interfaces( - interfaces: &HashMap, -) -> HashMap { - interfaces - .iter() - .map(|(module, interface)| { - let mut members = HashMap::new(); - for (name, type_) in &interface.functions { - members.insert( - (Namespace::Value, name.clone()), - ModuleMember { public: true, span: type_span(type_) }, - ); - } - for (name, declaration) in &interface.types { - members.insert( - (Namespace::Type, name.clone()), - ModuleMember { public: true, span: declaration.span }, - ); - } - for (name, constructor) in &interface.constructors { - members.insert( - (Namespace::Constructor, name.clone()), - ModuleMember { public: true, span: constructor.span }, - ); - for field in &constructor.fields { - members.insert( - (Namespace::Field, field.name.clone()), - ModuleMember { public: true, span: constructor.span }, - ); - } - } - (module.clone(), ModuleInterface { members }) - }) - .collect() -} - -fn type_span(_type_: &types::Type) -> Span { - module_span() -} - -fn module_span() -> Span { - Span { file_id: crate::source::SourceFileId(u32::MAX), start: 0, end: 0 } -} - -impl From<&ast::Module> for ModuleInterface { - fn from(value: &ast::Module) -> Self { - let mut members = HashMap::new(); - - for function in &value.functions { - members.insert( - (Namespace::Value, function.name.text.clone()), - ModuleMember { public: function.public, span: function.name.span }, - ); - } - - for declaration in &value.declarations { - match declaration { - Declaration::Constant(constant) => { - members.insert( - (Namespace::Value, constant.name.text.clone()), - ModuleMember { public: constant.public, span: constant.span }, - ); - } - Declaration::ExternalFunction(function) => { - members.insert( - (Namespace::Value, function.name.text.clone()), - ModuleMember { public: function.public, span: function.span }, - ); - } - Declaration::ExternalType(type_) => { - members.insert( - (Namespace::Type, type_.name.text.clone()), - ModuleMember { public: type_.public, span: type_.span }, - ); - } - Declaration::TypeDefinition(type_) => { - members.insert( - (Namespace::Type, type_.name.text.clone()), - ModuleMember { public: type_.public, span: type_.span }, - ); - let exported_details = type_.public && !type_.opaque; - for constructor in &type_.constructors { - members.insert( - (Namespace::Constructor, constructor.name.text.clone()), - ModuleMember { public: exported_details, span: constructor.span }, - ); - for argument in &constructor.arguments { - if let Some(label) = &argument.label { - members.insert( - (Namespace::Field, label.text.clone()), - ModuleMember { public: exported_details, span: label.span }, - ); - } - } - } - } - Declaration::TypeAlias(alias) => { - members.insert( - (Namespace::Type, alias.name.text.clone()), - ModuleMember { public: alias.public, span: alias.span }, - ); - } - Declaration::TargetGroup(group) => { - let nested = Self::from(&ast::Module { - span: group.span, - declarations: group.declarations.clone(), - imports: Vec::new(), - functions: Vec::new(), - }); - members.extend(nested.members); - } - _ => {} - } - } - - Self { members } - } -} - #[cfg(test)] mod tests { use super::*; @@ -1412,6 +1170,59 @@ fn main(person) { case person { Missing(age: value) -> value } } })); } + #[test] + fn resolves_prelude_names_from_interface_registry() { + let resolved = resolve_source("fn main(values: List(Int)) -> Int { 1 }").expect("resolve prelude types"); + + assert!( + resolved + .references + .iter() + .any(|reference| reference.name.text == "List") + ); + assert!(resolved.references.iter().any(|reference| reference.name.text == "Int")); + } + + #[test] + fn resolves_project_stdlib_dependency_and_unqualified_imports_through_registry() { + let dir = tempdir().expect("tempdir"); + write( + &dir.path().join("gleam.toml"), + r#"name = "app" +version = "1.0.0" + +[dependencies] +dep_pkg = { path = "dep_pkg" } +"#, + ); + write(&dir.path().join("src/app.gleam"), "pub fn id(x: Int) -> Int { x }\n"); + write( + &dir.path().join("src/main.gleam"), + r#"import app.{id} +import dep/foo.{dep_id} +import gleam/int.{to_string} + +pub fn main() -> String { + to_string(id(dep_id(1))) +} +"#, + ); + let dep = dir.path().join("dep_pkg"); + write(&dep.join("gleam.toml"), "name = \"dep_pkg\"\nversion = \"0.1.0\"\n"); + write(&dep.join("src/dep/foo.gleam"), "pub fn dep_id(x: Int) -> Int { x }\n"); + let project = project::load_project(dir.path()).expect("load project"); + + let resolved = resolve_project(&project).expect("resolve registry imports"); + + assert_eq!(resolved.modules.len(), 2); + assert!(resolved.modules.iter().any(|module| { + module + .references + .iter() + .any(|reference| reference.name.text == "to_string") + })); + } + #[test] fn resolves_project_modules_and_rejects_private_access() { let dir = tempdir().expect("tempdir"); diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 0eba605..4b24fe6 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -19,10 +19,10 @@ use crate::{ TypeTerm, UnificationError, Unifier, }, labels::{ArgumentLabelError, FunctionLabelMap, call_argument_order, function_label_map, use_callback_placement}, + loader::registry::TypeInterfaceRegistry, project::Project, resolve::{self, ResolvedModule}, - source::Span, - stdlib::StdlibRegistry, + source::{SourceFileId, Span}, }; /// A type known to the compiler. @@ -118,6 +118,78 @@ pub struct ModuleInterface { pub constructors: HashMap, } +impl From<&ast::Module> for ModuleInterface { + fn from(module: &ast::Module) -> Self { + let mut interface = ModuleInterface::default(); + collect_interface_declarations(&module.declarations, &mut interface); + interface + } +} + +impl ModuleInterface { + pub fn prelude_type_names() -> [&'static str; 10] { + [ + "Int", "Float", "String", "BitArray", "Bool", "Nil", "List", "Result", "Option", "Order", + ] + } + + pub fn module_span() -> Span { + Span { file_id: SourceFileId(u32::MAX), start: 0, end: 0 } + } + + pub fn prelude() -> ModuleInterface { + let mut interface = ModuleInterface::default(); + for name in Self::prelude_type_names() { + interface.types.insert( + name.to_string(), + TypeDeclaration { + name: name.to_string(), + parameters: Vec::new(), + opaque: false, + constructors: Vec::new(), + span: Self::module_span(), + }, + ); + } + interface + } + + pub fn interface_constructors(&self) -> Vec<(String, ConstructorInfo)> { + self.constructors + .iter() + .map(|(name, constructor)| (name.clone(), constructor.clone())) + .collect() + } + + pub fn qualified_values(&self, module: &str) -> Vec<(String, Type)> { + let short = module.rsplit('/').next().unwrap_or(module); + self.functions + .iter() + .flat_map(|(name, type_)| { + [ + (format!("{module}.{name}"), type_.clone()), + (format!("{short}.{name}"), type_.clone()), + (name.clone(), type_.clone()), + ] + }) + .collect() + } + + pub fn qualified_function_labels(&self, module: &str) -> Vec<(String, Vec>)> { + let short = module.rsplit('/').next().unwrap_or(module); + self.function_labels + .iter() + .flat_map(|(name, labels)| { + [ + (format!("{module}.{name}"), labels.clone()), + (format!("{short}.{name}"), labels.clone()), + (name.clone(), labels.clone()), + ] + }) + .collect() + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypedExpression { pub span: Span, @@ -149,12 +221,6 @@ pub struct TypedProject { pub interfaces: HashMap, } -pub fn interface_from_ast(module: &ast::Module) -> ModuleInterface { - let mut interface = ModuleInterface::default(); - collect_interface_declarations(&module.declarations, &mut interface); - interface -} - fn collect_interface_declarations(declarations: &[Declaration], interface: &mut ModuleInterface) { for declaration in declarations { match declaration { @@ -267,10 +333,12 @@ pub fn check(module: ResolvedModule) -> Result { } fn check_with_externals( - module: ResolvedModule, external_constructors: HashMap, - external_values: HashMap, external_function_labels: FunctionLabelMap, + module: ResolvedModule, module_interfaces: TypeInterfaceRegistry, + external_constructors: HashMap, external_values: HashMap, + external_function_labels: FunctionLabelMap, ) -> Result { TypeChecker::new(module) + .with_module_interfaces(module_interfaces) .with_external_constructors(external_constructors) .with_external_values(external_values) .with_external_function_labels(external_function_labels) @@ -283,41 +351,20 @@ pub fn check_project(project: &Project) -> Result { let mut interfaces = HashMap::new(); let mut diagnostics = Vec::new(); - let stdlib_interfaces = StdlibRegistry::new() - .modules() - .map(|module| (module.name.to_string(), module.interface.clone())) - .collect::>(); - let dependency_interfaces = project.graph.dependency_interfaces.clone(); - let mut external_interfaces = stdlib_interfaces.clone(); - external_interfaces.extend(dependency_interfaces.clone()); - interfaces.extend(external_interfaces.clone()); + let type_interfaces = TypeInterfaceRegistry::for_project( + &project.graph.dependency_interfaces, + project + .graph + .modules + .iter() + .zip(resolved.modules.iter()) + .map(|(module_info, module)| (module_info.name.as_str(), &module.ast)), + ); + interfaces.extend(type_interfaces.modules.clone()); - let external_constructors = resolved - .modules - .iter() - .flat_map(|module| constructors_from_ast(&module.ast)) - .chain(external_interfaces.values().flat_map(interface_constructors)) - .collect::>(); - let stdlib_values = external_interfaces - .iter() - .flat_map(|(module, interface)| qualified_values_from_interface(module, interface)) - .collect::>(); - let mut external_values = resolved - .modules - .iter() - .flat_map(|module| values_from_ast(&module.ast)) - .chain(stdlib_values) - .collect::>(); - let mut external_function_labels = resolved - .modules - .iter() - .flat_map(|module| function_label_map(&module.ast)) - .chain( - external_interfaces - .iter() - .flat_map(|(module, interface)| qualified_function_labels_from_interface(module, interface)), - ) - .collect::>(); + let external_constructors = type_interfaces.constructors(); + let mut external_values = type_interfaces.values(); + let mut external_function_labels = type_interfaces.function_labels(); let module_order = project_module_order(project, &resolved); let mut resolved_modules = resolved.modules.into_iter().map(Some).collect::>(); @@ -326,6 +373,7 @@ pub fn check_project(project: &Project) -> Result { let module = resolved_modules[index].take().expect("resolved module checked once"); match check_with_externals( module, + type_interfaces.clone(), external_constructors.clone(), external_values.clone(), external_function_labels.clone(), @@ -412,6 +460,7 @@ struct TypeChecker { external_values: HashMap, constructors: HashMap, function_labels: HashMap>>, + module_interfaces: TypeInterfaceRegistry, interface: ModuleInterface, functions: Vec, expressions: Vec, @@ -430,6 +479,7 @@ impl TypeChecker { external_values: HashMap::new(), constructors: HashMap::new(), function_labels, + module_interfaces: TypeInterfaceRegistry::for_single_file(), interface: ModuleInterface::default(), functions: Vec::new(), expressions: Vec::new(), @@ -440,6 +490,11 @@ impl TypeChecker { } } + fn with_module_interfaces(mut self, interfaces: TypeInterfaceRegistry) -> Self { + self.module_interfaces = interfaces; + self + } + fn with_external_constructors(mut self, constructors: HashMap) -> Self { self.constructors.extend(constructors); self @@ -457,7 +512,7 @@ impl TypeChecker { fn check(mut self) -> Result { self.collect_type_declarations(); - self.collect_imported_stdlib_interfaces(); + self.collect_imported_interfaces(); self.collect_annotated_function_types(); self.collect_external_function_types(); self.collect_constant_types(); @@ -524,10 +579,9 @@ impl TypeChecker { } } - fn collect_imported_stdlib_interfaces(&mut self) { - let registry = StdlibRegistry::new(); + fn collect_imported_interfaces(&mut self) { for import in &self.module.ast.imports { - let Some(interface) = registry.interface(&import.module.text) else { + let Some(interface) = self.module_interfaces.get(&import.module.text) else { continue; }; self.interface.types.extend(interface.types.clone()); @@ -551,12 +605,19 @@ impl TypeChecker { self.external_values .insert(format!("{module_name}.{name}"), type_.clone()); } + for (name, labels) in &interface.function_labels { + self.function_labels + .insert(format!("{module_name}.{name}"), labels.clone()); + } for imported in &import.unqualified { let local_name = imported.alias.as_ref().unwrap_or(&imported.name).text.clone(); match imported.kind { ast::UnqualifiedImportKind::Value => { if let Some(type_) = interface.functions.get(&imported.name.text) { - self.external_values.insert(local_name, type_.clone()); + self.external_values.insert(local_name.clone(), type_.clone()); + } + if let Some(labels) = interface.function_labels.get(&imported.name.text) { + self.function_labels.insert(local_name, labels.clone()); } } ast::UnqualifiedImportKind::TypeOrConstructor => { @@ -2424,104 +2485,6 @@ fn bit_string_pattern_bindings(raw: &ast::RawSyntax) -> Vec { .collect() } -fn values_from_ast(module: &ast::Module) -> Vec<(String, Type)> { - let mut checker = TypeChecker::new(ResolvedModule { - ast: module.clone(), - symbols: resolve::SymbolTable { symbols: Vec::new(), scopes: Vec::new() }, - references: Vec::new(), - }); - let mut values = Vec::new(); - for declaration in &module.declarations { - match declaration { - Declaration::Function(function) => { - if let Some(type_) = checker.function_type_from_annotations(function) { - values.push((function.name.text.clone(), type_.clone())); - values.push((format!("{}.{}", module_name(module), function.name.text), type_)); - } - } - Declaration::ExternalFunction(function) => { - if let Some(type_) = checker.external_function_type(function) { - values.push((function.name.text.clone(), type_.clone())); - values.push((format!("{}.{}", module_name(module), function.name.text), type_)); - } - } - Declaration::Constant(constant) => { - if let Some(type_) = checker.constant_type(constant) { - values.push((constant.name.text.clone(), type_.clone())); - values.push((format!("{}.{}", module_name(module), constant.name.text), type_)); - } - } - _ => {} - } - } - values -} - -fn module_name(module: &ast::Module) -> String { - module - .imports - .first() - .map(|import| import.module.text.clone()) - .unwrap_or_else(|| "module".into()) -} - -fn interface_constructors(interface: &ModuleInterface) -> Vec<(String, ConstructorInfo)> { - interface - .constructors - .iter() - .map(|(name, constructor)| (name.clone(), constructor.clone())) - .collect() -} - -fn qualified_values_from_interface(module: &str, interface: &ModuleInterface) -> Vec<(String, Type)> { - let short = module.rsplit('/').next().unwrap_or(module); - interface - .functions - .iter() - .flat_map(|(name, type_)| { - [ - (format!("{module}.{name}"), type_.clone()), - (format!("{short}.{name}"), type_.clone()), - (name.clone(), type_.clone()), - ] - }) - .collect() -} - -fn qualified_function_labels_from_interface( - module: &str, interface: &ModuleInterface, -) -> Vec<(String, Vec>)> { - let short = module.rsplit('/').next().unwrap_or(module); - interface - .function_labels - .iter() - .flat_map(|(name, labels)| { - [ - (format!("{module}.{name}"), labels.clone()), - (format!("{short}.{name}"), labels.clone()), - (name.clone(), labels.clone()), - ] - }) - .collect() -} - -fn constructors_from_ast(module: &ast::Module) -> Vec<(String, ConstructorInfo)> { - module - .declarations - .iter() - .filter_map(|declaration| match declaration { - Declaration::TypeDefinition(raw) => type_definition_from_ast(raw), - _ => None, - }) - .flat_map(|declaration| { - declaration - .constructors - .into_iter() - .map(|constructor| (constructor.name.clone(), constructor)) - }) - .collect() -} - fn empty_use_continuation_diagnostic(span: Span) -> Diagnostic { Diagnostic::new(DiagnosticCode::TypeError, "use has no continuation") .with_label(Label::primary(span, "nothing follows this use expression")) @@ -3012,6 +2975,52 @@ fn generic(result: Outcome(Int)) { case result { Ok(value) -> value Error(_) -> ); } + #[test] + fn type_checks_project_stdlib_dependency_and_unqualified_imports_from_registry() { + let dir = tempdir().expect("tempdir"); + write( + &dir.path().join("gleam.toml"), + r#"name = "app" +version = "1.0.0" + +[dependencies] +dep_pkg = { path = "dep_pkg" } +"#, + ); + write(&dir.path().join("src/app.gleam"), "pub fn id(x: Int) -> Int { x }\n"); + write( + &dir.path().join("src/main.gleam"), + r#"import app as numbers +import dep/foo.{Make, labelled} +import gleam/int as integer + +pub fn main() -> String { + integer.to_string(numbers.id(labelled(value: Make(1)))) +} +"#, + ); + let dep = dir.path().join("dep_pkg"); + write(&dep.join("gleam.toml"), "name = \"dep_pkg\"\nversion = \"0.1.0\"\n"); + write( + &dep.join("src/dep/foo.gleam"), + r#"pub type Thing { + Make(count: Int) +} + +pub fn labelled(value thing: Thing) -> Int { + 1 +} +"#, + ); + + let project = project::load_project(dir.path()).expect("load project"); + let typed = check_project(&project).expect("type check registry-backed imports"); + + assert!(typed.interfaces.contains_key("app")); + assert!(typed.interfaces.contains_key("dep/foo")); + assert!(typed.interfaces.contains_key("gleam/int")); + } + #[test] fn checks_dependency_interfaces_for_values_types_constructors_and_labels() { let dir = tempdir().expect("tempdir"); diff --git a/docs/internal/tasks/14_project_compilation_and_dependencies.md b/docs/internal/tasks/14_project_compilation_and_dependencies.md index 4904495..be10865 100644 --- a/docs/internal/tasks/14_project_compilation_and_dependencies.md +++ b/docs/internal/tasks/14_project_compilation_and_dependencies.md @@ -61,21 +61,8 @@ The goal here is to make linked project names deterministic and collision-free. - [x] Discover all selected project modules in deterministic order. - [x] Parse every module before cross-module resolution. - [x] Apply target-group filtering before name resolution. -- [ ] Resolve project imports, stdlib imports, dependency interfaces, and +- [x] Resolve project imports, stdlib imports, dependency interfaces, and prelude names through one module-interface path. - - [x] Add one resolver-facing module-interface registry used by both - single-file and project resolution. - - [x] Move project, stdlib, and dependency interface loading behind registry - source helpers instead of open-coded map assembly. - - [x] Validate imported modules and unqualified imported members through the - registry when imports are collected. - - [x] Represent prelude names as an implicit interface in the registry instead - of hard-coded resolver symbols. - - [ ] Make type checking consume the same interface registry data for - external values, constructors, function labels, and types. - - [ ] Add focused coverage for project, stdlib, dependency, prelude, - unqualified import, unknown module, unknown member, and visibility - resolution. - [x] Type-check modules in dependency order. - [x] Lower every typed project module to IR. - [x] Preserve source paths and spans through project diagnostics. @@ -119,7 +106,7 @@ The goal here is to make linked project names deterministic and collision-free. - [ ] Avoid writing partial final artifacts after failed compilation unless the user explicitly requested debug dumps. -## Done when +## Definition of Done A user can compile a `gleam.toml` project with multiple modules and receive one linked Wasm artifact or stable, source-rendered diagnostics. -- 2.51.2