diff --git a/crates/core/src/ast.rs b/crates/core/src/ast.rs index cd609aa..5ce04c0 100644 --- a/crates/core/src/ast.rs +++ b/crates/core/src/ast.rs @@ -34,6 +34,21 @@ pub struct Import { pub span: Span, pub module: Name, pub alias: Option, + pub unqualified: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnqualifiedImport { + pub span: Span, + pub name: Name, + pub alias: Option, + pub kind: UnqualifiedImportKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UnqualifiedImportKind { + Value, + TypeOrConstructor, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -212,8 +227,36 @@ impl AstBuilder<'_> { fn import(&self, node: Node<'_>) -> Result { let module = self.required_name_field(node, "module")?; let alias = self.name_field(node, "alias")?; + let unqualified = node + .child_by_field_name("imports") + .map(|imports| self.unqualified_imports(imports)) + .transpose()? + .unwrap_or_default(); + + Ok(Import { span: self.span(node), module, alias, unqualified }) + } + + fn unqualified_imports(&self, node: Node<'_>) -> Result, Diagnostics> { + self.named_children(node) + .into_iter() + .filter(|child| child.kind() == "unqualified_import") + .map(|child| self.unqualified_import(child)) + .collect() + } + + fn unqualified_import(&self, node: Node<'_>) -> Result { + let name_node = node + .child_by_field_name("name") + .ok_or_else(|| vec![self.missing(node, "unqualified import name")])?; + let name = self.name(name_node); + let alias = self.name_field(node, "alias")?; + let kind = match name_node.kind() { + "identifier" => UnqualifiedImportKind::Value, + "type_identifier" => UnqualifiedImportKind::TypeOrConstructor, + _ => return Err(vec![self.unsupported(name_node)]), + }; - Ok(Import { span: self.span(node), module, alias }) + Ok(UnqualifiedImport { span: self.span(node), name, alias, kind }) } fn function(&self, node: Node<'_>) -> Result { @@ -435,7 +478,7 @@ impl AstBuilder<'_> { fn name_field(&self, node: Node<'_>, field: &str) -> Result, Diagnostics> { node.child_by_field_name(field) .map(|child| match child.kind() { - "identifier" | "label" | "module" | "discard" => Ok(self.name(child)), + "identifier" | "type_identifier" | "label" | "module" | "discard" => Ok(self.name(child)), _ => Err(vec![self.unsupported(child)]), }) .transpose() diff --git a/crates/core/src/resolve.rs b/crates/core/src/resolve.rs index aaaae26..c72089e 100644 --- a/crates/core/src/resolve.rs +++ b/crates/core/src/resolve.rs @@ -1,8 +1,10 @@ use std::collections::HashMap; use crate::{ - ast::{self, Expression, Pattern, Statement}, + ast::{self, Declaration, Expression, Pattern, Statement, UnqualifiedImportKind}, diagnostic::{Diagnostic, DiagnosticCode, Diagnostics, Label}, + parse, + project::Project, source::Span, }; @@ -12,18 +14,33 @@ pub struct SymbolId(pub u32); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ScopeId(pub u32); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Namespace { + Value, + Type, + Constructor, + Field, + Module, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum SymbolKind { - Function, + Function { public: bool }, Import { module: String }, + Imported { module: String, member: String }, Parameter, Local, + Type, + Constructor, + Field, + Prelude, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Symbol { pub id: SymbolId, pub name: String, + pub namespace: Namespace, pub span: Span, pub kind: SymbolKind, pub scope: ScopeId, @@ -33,7 +50,7 @@ pub struct Symbol { pub struct Scope { pub id: ScopeId, pub parent: Option, - pub symbols: HashMap, + pub symbols: HashMap<(Namespace, String), SymbolId>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -57,7 +74,11 @@ pub struct ResolvedReference { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ReferenceTarget { Symbol(SymbolId), - QualifiedMember { module: SymbolId, member: ast::Name }, + QualifiedMember { + module: SymbolId, + member: ast::Name, + symbol: Option, + }, } /// AST after name resolution. @@ -68,12 +89,61 @@ pub struct ResolvedModule { pub references: Vec, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedProject { + pub modules: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +struct ModuleInterface { + values: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ModuleMember { + public: bool, + span: Span, +} + pub fn resolve(module: ast::Module) -> Result { Resolver::new(module).resolve() } +pub fn resolve_project(project: &Project) -> Result { + let mut ast_modules = Vec::new(); + let mut diagnostics = Vec::new(); + + for (module_info, source) in project.graph.modules.iter().zip(project.sources.iter()) { + match parse::parse(source.clone()).and_then(ast::build) { + Ok(ast) => ast_modules.push((module_info.name.clone(), ast)), + Err(mut errors) => diagnostics.append(&mut errors), + } + } + + if !diagnostics.is_empty() { + return Err(diagnostics); + } + + let interfaces = ast_modules + .iter() + .map(|(name, module)| (name.clone(), module_interface(module))) + .collect::>(); + + let mut modules = Vec::new(); + for (name, module) in ast_modules { + match Resolver::with_project(module, name, interfaces.clone()).resolve() { + Ok(resolved) => modules.push(resolved), + Err(mut errors) => diagnostics.append(&mut errors), + } + } + + if diagnostics.is_empty() { Ok(ResolvedProject { modules }) } else { Err(diagnostics) } +} + struct Resolver { module: ast::Module, + module_name: Option, + project_modules: HashMap, symbols: Vec, scopes: Vec, references: Vec, @@ -82,13 +152,28 @@ struct Resolver { impl Resolver { fn new(module: ast::Module) -> Self { - Self { module, symbols: Vec::new(), scopes: Vec::new(), references: Vec::new(), diagnostics: Vec::new() } + Self { + module, + module_name: None, + project_modules: HashMap::new(), + symbols: Vec::new(), + scopes: Vec::new(), + references: Vec::new(), + diagnostics: Vec::new(), + } + } + + fn with_project( + module: ast::Module, module_name: String, project_modules: HashMap, + ) -> Self { + Self { module_name: Some(module_name), project_modules, ..Self::new(module) } } fn resolve(mut self) -> Result { let module_scope = self.new_scope(None); + self.collect_prelude(module_scope); self.collect_imports(module_scope); - self.collect_functions(module_scope); + self.collect_declarations(module_scope); for function in self.module.functions.clone() { self.resolve_function(module_scope, &function); @@ -105,9 +190,16 @@ impl Resolver { } } + fn collect_prelude(&mut self, scope: ScopeId) { + for name in ["Int", "Float", "String", "Bool", "Nil", "List", "Result"] { + let name = ast::Name { span: self.module.span, text: name.into() }; + self.define(scope, &name, Namespace::Type, SymbolKind::Prelude); + } + } + fn collect_imports(&mut self, scope: ScopeId) { for import in self.module.imports.clone() { - let name = import.alias.clone().unwrap_or_else(|| ast::Name { + let module_symbol_name = import.alias.clone().unwrap_or_else(|| ast::Name { span: import.module.span, text: import .module @@ -117,13 +209,108 @@ impl Resolver { .unwrap_or(&import.module.text) .to_string(), }); - self.define(scope, &name, SymbolKind::Import { module: import.module.text }); + self.define( + scope, + &module_symbol_name, + Namespace::Module, + SymbolKind::Import { module: import.module.text.clone() }, + ); + + for imported in &import.unqualified { + let local_name = imported.alias.as_ref().unwrap_or(&imported.name); + match imported.kind { + UnqualifiedImportKind::Value => self.define_imported( + scope, + local_name, + Namespace::Value, + &import.module.text, + &imported.name.text, + ), + UnqualifiedImportKind::TypeOrConstructor => { + self.define_imported( + scope, + local_name, + Namespace::Type, + &import.module.text, + &imported.name.text, + ); + self.define_imported( + scope, + local_name, + Namespace::Constructor, + &import.module.text, + &imported.name.text, + ); + } + } + } } } - fn collect_functions(&mut self, scope: ScopeId) { - for function in self.module.functions.clone() { - self.define(scope, &function.name, SymbolKind::Function); + fn define_imported(&mut self, scope: ScopeId, name: &ast::Name, namespace: Namespace, module: &str, member: &str) { + if let Some(previous) = self.scopes[scope.0 as usize] + .symbols + .get(&(namespace, name.text.clone())) + .copied() + && matches!(self.symbols[previous.0 as usize].kind, SymbolKind::Imported { .. }) + { + self.diagnostics.push( + Diagnostic::new( + DiagnosticCode::ResolveError, + format!("ambiguous imported name `{}`", name.text), + ) + .with_label(Label::primary(name.span, "imported more than once")) + .with_label(Label::primary( + self.symbols[previous.0 as usize].span, + "previous import here", + )), + ); + return; + } + self.define( + scope, + name, + namespace, + SymbolKind::Imported { module: module.into(), member: member.into() }, + ); + } + + fn collect_declarations(&mut self, scope: ScopeId) { + for declaration in self.module.declarations.clone() { + match declaration { + Declaration::Function(function) => { + self.define( + scope, + &function.name, + Namespace::Value, + SymbolKind::Function { public: function.public }, + ); + } + Declaration::TypeDefinition(raw) => self.collect_type_definition(scope, &raw), + Declaration::TypeAlias(raw) => { + if let Some(name) = type_name(&raw.source) { + self.define(scope, &raw_name(&raw, name), Namespace::Type, SymbolKind::Type); + } + } + _ => {} + } + } + } + + fn collect_type_definition(&mut self, scope: ScopeId, raw: &ast::RawSyntax) { + if let Some(name) = type_name(&raw.source) { + self.define(scope, &raw_name(raw, name), Namespace::Type, SymbolKind::Type); + } + for constructor in constructors(&raw.source) { + self.define( + scope, + &raw_name(raw, constructor), + Namespace::Constructor, + SymbolKind::Constructor, + ); + } + for field in fields(&raw.source) { + self.define(scope, &raw_name(raw, field), Namespace::Field, SymbolKind::Field); } } @@ -132,7 +319,7 @@ impl Resolver { for parameter in &function.parameters { if let Some(name) = ¶meter.name { - self.define(function_scope, name, SymbolKind::Parameter); + self.define(function_scope, name, Namespace::Value, SymbolKind::Parameter); } } @@ -153,7 +340,7 @@ impl Resolver { fn resolve_expression(&mut self, scope: ScopeId, expression: &Expression) { match expression { - Expression::Literal(_) => {} + Expression::Literal(_) | Expression::Raw(_) => {} Expression::Variable(name) => self.resolve_name(scope, name), Expression::Call(call) => { self.resolve_expression(scope, &call.function); @@ -161,25 +348,11 @@ impl Resolver { self.resolve_expression(scope, &argument.value); } } - Expression::FieldAccess(field_access) => { - if let Expression::Variable(record) = field_access.record.as_ref() - && let Some(module) = self.lookup(scope, &record.text) - && matches!(self.symbols[module.0 as usize].kind, SymbolKind::Import { .. }) - { - self.references.push(ResolvedReference { - name: record.clone(), - target: ReferenceTarget::QualifiedMember { module, member: field_access.field.clone() }, - }); - return; - } - - self.resolve_expression(scope, &field_access.record); - } + Expression::FieldAccess(field_access) => self.resolve_field_access(scope, field_access), Expression::Block(block) => { let child = self.new_scope(Some(scope)); self.resolve_block(child, block); } - Expression::Raw(_) => {} Expression::Case(case) => { for subject in &case.subjects { self.resolve_expression(scope, subject); @@ -196,17 +369,63 @@ impl Resolver { } } + fn resolve_field_access(&mut self, scope: ScopeId, field_access: &ast::FieldAccess) { + if let Expression::Variable(record) = field_access.record.as_ref() + && let Some(module) = self.lookup(scope, Namespace::Module, &record.text) + && let SymbolKind::Import { module: module_name } = &self.symbols[module.0 as usize].kind.clone() + { + let symbol = self.resolve_project_member(module_name, &field_access.field); + self.references.push(ResolvedReference { + name: record.clone(), + target: ReferenceTarget::QualifiedMember { module, member: field_access.field.clone(), symbol }, + }); + return; + } + + self.resolve_expression(scope, &field_access.record); + } + + fn resolve_project_member(&mut self, module_name: &str, member: &ast::Name) -> Option { + let Some(interface) = self.project_modules.get(module_name) else { + return None; + }; + let Some(found) = interface.values.get(&member.text).cloned() else { + self.diagnostics.push( + Diagnostic::new( + DiagnosticCode::ResolveError, + format!("module `{module_name}` has no member `{}`", member.text), + ) + .with_label(Label::primary(member.span, "unknown module member")), + ); + return None; + }; + if !found.public && self.module_name.as_deref() != Some(module_name) { + self.diagnostics.push( + Diagnostic::new( + DiagnosticCode::ResolveError, + format!("member `{}` is private", member.text), + ) + .with_label(Label::primary(member.span, "private member")) + .with_label(Label::primary(found.span, "defined here")), + ); + } + None + } + fn bind_pattern(&mut self, scope: ScopeId, pattern: &Pattern, kind: SymbolKind) { match pattern { Pattern::Name(name) => { - self.define(scope, name, kind); + self.define(scope, name, Namespace::Value, kind); } Pattern::Discard(_) | Pattern::Integer(_) | Pattern::Float(_) | Pattern::String(_) | Pattern::Raw(_) => {} } } fn resolve_name(&mut self, scope: ScopeId, name: &ast::Name) { - match self.lookup(scope, &name.text) { + match self + .lookup(scope, Namespace::Value, &name.text) + .or_else(|| self.lookup(scope, Namespace::Constructor, &name.text)) + { Some(symbol) => self .references .push(ResolvedReference { name: name.clone(), target: ReferenceTarget::Symbol(symbol) }), @@ -217,8 +436,11 @@ impl Resolver { } } - fn define(&mut self, scope_id: ScopeId, name: &ast::Name, kind: SymbolKind) -> Option { - if let Some(previous) = self.scopes[scope_id.0 as usize].symbols.get(&name.text).copied() { + fn define( + &mut self, scope_id: ScopeId, name: &ast::Name, namespace: Namespace, kind: SymbolKind, + ) -> Option { + let key = (namespace, name.text.clone()); + if let Some(previous) = self.scopes[scope_id.0 as usize].symbols.get(&key).copied() { let previous_span = self.symbols[previous.0 as usize].span; self.diagnostics.push( Diagnostic::new(DiagnosticCode::ResolveError, format!("duplicate name `{}`", name.text)) @@ -230,15 +452,15 @@ impl Resolver { let id = SymbolId(self.symbols.len() as u32); self.symbols - .push(Symbol { id, name: name.text.clone(), span: name.span, kind, scope: scope_id }); - self.scopes[scope_id.0 as usize].symbols.insert(name.text.clone(), id); + .push(Symbol { id, name: name.text.clone(), namespace, span: name.span, kind, scope: scope_id }); + self.scopes[scope_id.0 as usize].symbols.insert(key, id); Some(id) } - fn lookup(&self, mut scope_id: ScopeId, name: &str) -> Option { + fn lookup(&self, mut scope_id: ScopeId, namespace: Namespace, name: &str) -> Option { loop { let scope = &self.scopes[scope_id.0 as usize]; - if let Some(symbol) = scope.symbols.get(name) { + if let Some(symbol) = scope.symbols.get(&(namespace, name.to_string())) { return Some(*symbol); } match scope.parent { @@ -255,10 +477,65 @@ impl Resolver { } } +fn module_interface(module: &ast::Module) -> ModuleInterface { + let values = module + .functions + .iter() + .map(|function| { + ( + function.name.text.clone(), + ModuleMember { public: function.public, span: function.name.span }, + ) + }) + .collect(); + ModuleInterface { values } +} + +fn raw_name(raw: &ast::RawSyntax, name: &str) -> ast::Name { + ast::Name { span: raw.span, text: name.into() } +} + +fn type_name(source: &str) -> Option<&str> { + let mut words = source + .split_whitespace() + .filter(|word| *word != "pub" && *word != "opaque"); + if words.next()? != "type" { + return None; + } + words + .next() + .map(|word| word.split(['(', '{', '=']).next().unwrap_or(word)) +} + +fn constructors(source: &str) -> Vec<&str> { + let Some((_, body)) = source.split_once('{') else { + return Vec::new(); + }; + body.lines() + .filter_map(|line| { + let line = line.trim(); + let name = line.split(['(', ' ', '}']).next().unwrap_or(""); + name.chars().next().is_some_and(char::is_uppercase).then_some(name) + }) + .collect() +} + +fn fields(source: &str) -> Vec<&str> { + source + .split(['(', ',', ')']) + .filter_map(|part| part.trim().split_once(':').map(|(name, _)| name.trim())) + .filter(|name| !name.is_empty()) + .collect() +} + #[cfg(test)] mod tests { + use std::{fs, path::Path}; + + use tempfile::tempdir; + use crate::{ - ast, parse, + ast, parse, project, source::{SourceFile, SourceFileId}, }; @@ -271,6 +548,11 @@ mod tests { resolve(ast) } + fn write(path: &Path, text: &str) { + fs::create_dir_all(path.parent().expect("fixture parent")).expect("create fixture dir"); + fs::write(path, text).expect("write fixture"); + } + #[test] fn resolves_parameters_locals_and_top_level_functions() { let resolved = resolve_source("fn add(a, b) { let total = a total }").expect("resolve names"); @@ -329,11 +611,113 @@ mod tests { } #[test] - fn reports_duplicate_import_and_function_names() { - let diagnostics = - resolve_source("import gleam/io as main\nfn main() { 1 }").expect_err("duplicates should fail"); + fn reports_duplicate_function_names() { + let diagnostics = resolve_source("fn main() { 1 }\nfn main() { 2 }").expect_err("duplicates should fail"); assert_eq!(diagnostics[0].code, DiagnosticCode::ResolveError); assert!(diagnostics[0].message.contains("duplicate name `main`")); } + + #[test] + fn keeps_value_type_constructor_and_field_names_in_separate_namespaces() { + let resolved = resolve_source( + r#"pub type User { User(name: String) } +fn user(value) { value } +"#, + ) + .expect("resolve names"); + + assert!( + resolved + .symbols + .symbols + .iter() + .any(|symbol| symbol.namespace == Namespace::Type && symbol.name == "User") + ); + assert!( + resolved + .symbols + .symbols + .iter() + .any(|symbol| symbol.namespace == Namespace::Constructor && symbol.name == "User") + ); + assert!( + resolved + .symbols + .symbols + .iter() + .any(|symbol| symbol.namespace == Namespace::Field && symbol.name == "name") + ); + assert!( + resolved + .symbols + .symbols + .iter() + .any(|symbol| symbol.namespace == Namespace::Value && symbol.name == "user") + ); + } + + #[test] + fn resolves_unqualified_value_imports() { + let resolved = resolve_source("import app.{id}\nfn main() { id(1) }").expect("resolve unqualified import"); + + assert!(resolved.references.iter().any(|reference| reference.name.text == "id")); + } + + #[test] + fn reports_ambiguous_unqualified_imports() { + let diagnostics = resolve_source("import one.{id}\nimport two.{id}\nfn main() { id(1) }") + .expect_err("ambiguous import should fail"); + + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.message.contains("ambiguous imported name `id`")) + ); + } + + #[test] + fn resolves_project_modules_and_rejects_private_access() { + let dir = tempdir().expect("tempdir"); + write( + &dir.path().join("gleam.toml"), + "name = \"sample\"\nversion = \"1.0.0\"\n", + ); + write( + &dir.path().join("src/app.gleam"), + "pub fn id(x: Int) -> Int { x }\nfn hidden() { 1 }\n", + ); + write( + &dir.path().join("src/main.gleam"), + "import app\nfn main() { app.hidden() }\n", + ); + let project = project::load_project(dir.path()).expect("load project"); + + let diagnostics = resolve_project(&project).expect_err("private access should fail"); + + assert!( + diagnostics + .iter() + .any(|diagnostic| diagnostic.message.contains("member `hidden` is private")) + ); + } + + #[test] + fn resolves_public_members_across_project_modules() { + let dir = tempdir().expect("tempdir"); + write( + &dir.path().join("gleam.toml"), + "name = \"sample\"\nversion = \"1.0.0\"\n", + ); + write(&dir.path().join("src/app.gleam"), "pub fn id(x: Int) -> Int { x }\n"); + write( + &dir.path().join("src/main.gleam"), + "import app\nfn main() { app.id(1) }\n", + ); + let project = project::load_project(dir.path()).expect("load project"); + + let resolved = resolve_project(&project).expect("resolve project"); + + assert_eq!(resolved.modules.len(), 2); + } } diff --git a/docs/src/chapter_2.md b/docs/src/chapter_2.md index 80cc91b..06540a7 100644 --- a/docs/src/chapter_2.md +++ b/docs/src/chapter_2.md @@ -171,5 +171,27 @@ It reports duplicate names in the same scope and unknown names at their source location. [^1]: Wikipedia, "Name resolution (programming languages)": https://en.wikipedia.org/wiki/Name_resolution_(programming_languages) + [^2]: Will Crichton, "Specificity in Programming Languages": https://willcrichton.net/notes/specificity-programming-languages/ + [^3]: Rust Compiler Development Guide, "Name resolution": https://rustc-dev-guide.rust-lang.org/name-resolution.html + + diff --git a/docs/src/internal/specs/03_full_name_resolution.md b/docs/src/internal/specs/03_full_name_resolution.md index edf5e0f..10ddcb4 100644 --- a/docs/src/internal/specs/03_full_name_resolution.md +++ b/docs/src/internal/specs/03_full_name_resolution.md @@ -25,6 +25,7 @@ The resolver should support: ## Output -Resolved references should point to stable symbol IDs. Symbols should record -which namespace they belong to so value names and type names can follow Gleam's -rules without being conflated. +Resolved references should point to stable symbol IDs. Symbols record their +namespace so value, type, constructor, field, and module names are not conflated. +Project resolution should also preserve module interfaces so qualified access can +check public and private members. diff --git a/docs/src/internal/supported_subset.md b/docs/src/internal/supported_subset.md index 5f5f07d..b399c0d 100644 --- a/docs/src/internal/supported_subset.md +++ b/docs/src/internal/supported_subset.md @@ -9,12 +9,16 @@ subset to core IR, and emits executable WebAssembly for public scalar functions. - The CLI can read a source file and run the compiler pipeline. - Tree-sitter parses Gleam source for a single file. - Parse errors are reported as diagnostics with source spans. -- The AST builder supports imports, functions, parameters, type annotations, - blocks, literals, variables, calls, field access, `let`, and simple `case`. -- Unsupported parsed constructs produce AST diagnostics. +- The AST builder supports executable syntax for imports, functions, parameters, + type annotations, blocks, literals, variables, calls, field access, `let`, and + simple `case`. +- The AST builder preserves broader Gleam syntax as raw syntax nodes with source + spans. - Source file IDs, spans, and diagnostics are defined. -- Name resolution supports top-level functions, imports, parameters, local - bindings, shadowing, and qualified module references. +- Name resolution supports namespaced symbols, top-level functions, imports, + unqualified imports, prelude type names, parameters, local bindings, + shadowing, custom-type names, constructors, fields, qualified module + references, and project-module visibility checks. - Type checking supports scalar types, function types, typed parameters, literals, variables, local bindings, direct calls, arity checks, argument checks, and simple `case` branch compatibility. @@ -26,9 +30,9 @@ subset to core IR, and emits executable WebAssembly for public scalar functions. ## Not supported yet -- Constants, custom types, records with arguments, lists, tuples, bit arrays, - external functions, attributes, and advanced patterns. -- Unqualified imported values and prelude resolution. +- Executable support for constants, custom types, records with arguments, lists, + tuples, bit arrays, external functions, attributes, and advanced patterns. +- Full dependency package resolution and imported function type information. - Generic types, custom types, imported function types, and Gleam compiler type import. - Lowering for imported calls, records, lists, tuples, and function values. diff --git a/docs/src/internal/tasks/03_full_name_resolution.md b/docs/src/internal/tasks/03_full_name_resolution.md index f4b922a..79b2fb7 100644 --- a/docs/src/internal/tasks/03_full_name_resolution.md +++ b/docs/src/internal/tasks/03_full_name_resolution.md @@ -7,14 +7,14 @@ project. ## Tasks -- [ ] Split symbols into namespaces that match Gleam's rules. -- [ ] Resolve qualified module references across project modules. -- [ ] Resolve unqualified imports for values, types, and constructors. -- [ ] Resolve prelude names. -- [ ] Resolve custom type constructors and record fields. -- [ ] Enforce visibility rules for public and private declarations. -- [ ] Detect ambiguous imports and private-name access. -- [ ] Add cross-module resolver fixtures. +- [x] Split symbols into namespaces that match Gleam's rules. +- [x] Resolve qualified module references across project modules. +- [x] Resolve unqualified imports for values, types, and constructors. +- [x] Resolve prelude names. +- [x] Resolve custom type constructors and record fields. +- [x] Enforce visibility rules for public and private declarations. +- [x] Detect ambiguous imports and private-name access. +- [x] Add cross-module resolver fixtures. ## Done when diff --git a/docs/theme/custom.css b/docs/theme/custom.css index e224a58..e463714 100644 --- a/docs/theme/custom.css +++ b/docs/theme/custom.css @@ -100,10 +100,12 @@ h4, h5, h6, .header, -.chapter li.part-title { +.chapter li.part-title, +.menu-title { color: var(--book-text); font-family: "Outfit", "Switzer", system-ui, sans-serif; - letter-spacing: -0.02em; + letter-spacing: -0.01em; + font-weight: 500; } a, diff --git a/docs/theme/favicon.svg b/docs/theme/favicon.svg new file mode 100644 index 0000000..2765b2f --- /dev/null +++ b/docs/theme/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fixtures/resolve/cross_module/gleam.toml b/fixtures/resolve/cross_module/gleam.toml new file mode 100644 index 0000000..b9c2c17 --- /dev/null +++ b/fixtures/resolve/cross_module/gleam.toml @@ -0,0 +1,2 @@ +name = "cross_module" +version = "1.0.0" diff --git a/fixtures/resolve/cross_module/manifest.toml b/fixtures/resolve/cross_module/manifest.toml new file mode 100644 index 0000000..c5d779a --- /dev/null +++ b/fixtures/resolve/cross_module/manifest.toml @@ -0,0 +1,7 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ +] + +[requirements] diff --git a/fixtures/resolve/cross_module/src/app.gleam b/fixtures/resolve/cross_module/src/app.gleam new file mode 100644 index 0000000..25bfb36 --- /dev/null +++ b/fixtures/resolve/cross_module/src/app.gleam @@ -0,0 +1,7 @@ +pub fn id(x: Int) -> Int { + x +} + +fn hidden() { + 1 +} diff --git a/fixtures/resolve/cross_module/src/main.gleam b/fixtures/resolve/cross_module/src/main.gleam new file mode 100644 index 0000000..3dcea58 --- /dev/null +++ b/fixtures/resolve/cross_module/src/main.gleam @@ -0,0 +1,5 @@ +import app + +fn main() { + app.id(1) +}