diff --git a/src/import_resolver.rs b/src/import_resolver.rs new file mode 100644 index 0000000..7852dc3 --- /dev/null +++ b/src/import_resolver.rs @@ -0,0 +1,410 @@ +use crate::ast::{ASTNode, ASTNodeKind, Span}; +use crate::lexer::Token; +use crate::parser::Parser; +use logos::Logos; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::path::Path; + +const EXTENSION: &str = ".sui"; + +/// Error types for import resolution +#[derive(Debug, Clone)] +pub enum ImportError { + DuplicateSymbol { + symbol: String, + locations: Vec, + }, + FileNotFound { + path: String, + reason: String, + }, + LexError { + file: String, + position: usize, + }, + ParseError { + file: String, + message: String, + span: (usize, usize), + }, + CircularDependency { + files: Vec, + }, +} + +impl std::fmt::Display for ImportError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ImportError::DuplicateSymbol { symbol, locations } => { + write!(f, "Duplicate global symbol '{}' defined in:\n", symbol)?; + for (i, loc) in locations.iter().enumerate() { + write!(f, " [{}] {}", i + 1, loc)?; + if i < locations.len() - 1 { + writeln!(f)?; + } + } + Ok(()) + } + ImportError::FileNotFound { path, reason } => { + write!(f, "Module not found '{}': {}", path, reason) + } + ImportError::LexError { file, position } => { + write!(f, "Lexer error at position {} in {}", position, file) + } + ImportError::ParseError { + file, + message, + span, + } => { + write!( + f, + "Parse error in {}: {} (at {}..{})", + file, message, span.0, span.1 + ) + } + ImportError::CircularDependency { files } => { + write!(f, "Circular dependency detected: {}", files.join(" -> ")) + } + } + } +} + +impl std::error::Error for ImportError {} + +/// Cached parse output for a file +#[derive(Clone, Debug)] +struct ParsedFile { + /// AST nodes from this file (before import collection) + nodes: Vec, + /// Hash of the file content at parse time + content_hash: u64, +} + +/// Tracks global symbols and their definitions +#[derive(Debug, Default)] +struct GlobalSymbolRegistry { + /// Maps symbol name to (file, span) where it was defined + symbols: HashMap>, +} + +impl GlobalSymbolRegistry { + fn register(&mut self, symbol: String, file: String, span: Span) -> Result<(), ImportError> { + self.symbols.entry(symbol.clone()).or_insert_with(Vec::new); + let locations = self.symbols.get_mut(&symbol).unwrap(); + + locations.push((file.clone(), span)); + + // Check for duplicates + if locations.len() > 1 { + let location_strs = locations + .iter() + .map(|(f, s)| format!("{}:{}..{}", f, s.start, s.end)) + .collect(); + + return Err(ImportError::DuplicateSymbol { + symbol, + locations: location_strs, + }); + } + + Ok(()) + } +} + +/// Manages file dependency resolution and caching +pub struct ImportResolver { + /// Cache of parsed files keyed by path + parse_cache: HashMap, + /// Global symbol registry for duplicate detection + symbol_registry: GlobalSymbolRegistry, + /// Set of files currently being processed (for cycle detection) + processing_stack: HashSet, + /// Dependency graph: file -> list of files it depends on + dependency_graph: HashMap>, +} + +impl ImportResolver { + pub fn new() -> Self { + ImportResolver { + parse_cache: HashMap::new(), + symbol_registry: GlobalSymbolRegistry::default(), + processing_stack: HashSet::new(), + dependency_graph: HashMap::new(), + } + } + + /// Compute a simple hash of file content + fn hash_content(content: &str) -> u64 { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + content.hash(&mut hasher); + hasher.finish() + } + + /// Lex source code into tokens + fn lex_source(source: &str, filename: &str) -> Result, ImportError> { + let mut tokens = Vec::new(); + let mut lexer = Token::lexer(source); + + while let Some(token_result) = lexer.next() { + match token_result { + Ok(token) => { + tokens.push((token, lexer.span())); + } + Err(()) => { + return Err(ImportError::LexError { + file: filename.to_string(), + position: lexer.span().start, + }); + } + } + } + + Ok(tokens) + } + + /// Parse tokens into AST + fn parse_tokens( + tokens: Vec<(Token, logos::Span)>, + filename: String, + ) -> Result, ImportError> { + let mut parser = Parser::new(filename.clone(), tokens); + parser.parse().map_err(|e| ImportError::ParseError { + file: filename, + message: e.message.clone(), + span: (e.span.start, e.span.end), + }) + } + + /// Parse a file (using cache if content hash matches) + fn parse_file(&mut self, filename: &str) -> Result, ImportError> { + let source = fs::read_to_string(filename).map_err(|e| ImportError::FileNotFound { + path: filename.to_string(), + reason: e.to_string(), + })?; + + let content_hash = Self::hash_content(&source); + + // Check if we have a valid cached version + if let Some(cached) = self.parse_cache.get(filename) { + if cached.content_hash == content_hash { + println!(" [cache hit] {}", filename); + return Ok(cached.nodes.clone()); + } + } + + // Parse the file + println!(" [parsing] {}", filename); + let tokens = Self::lex_source(&source, filename)?; + let nodes = Self::parse_tokens(tokens, filename.to_string())?; + + // Cache the result + self.parse_cache.insert( + filename.to_string(), + ParsedFile { + nodes: nodes.clone(), + content_hash, + }, + ); + + Ok(nodes) + } + + /// Collect all dependencies from a file (via Use/Load statements) + fn collect_dependencies(&self, filename: &str, nodes: &[ASTNode]) -> Vec { + let mut deps = Vec::new(); + let mut seen = HashSet::new(); + + for node in nodes { + if let ASTNodeKind::Use(use_stmt) = &node.kind { + let path = &use_stmt.path; + if !seen.contains(path) { + seen.insert(path.clone()); + deps.push(path.clone()); + } + } + } + + // Resolve relative paths + let importing_dir = Path::new(filename).parent().unwrap_or(Path::new("")); + + deps.iter() + .filter_map(|path| { + let resolved = if path.starts_with("std/") { + Some(format!("src/{}{}", path, EXTENSION)) + } else if path.starts_with("@") { + // Package manager support - TODO + None + } else if path.starts_with(".") || path.starts_with("~") { + let resolved_path = importing_dir.join(path); + Some(resolved_path.to_string_lossy().to_string() + EXTENSION) + } else { + None + }; + resolved + }) + .collect() + } + + /// Check for circular dependencies + fn detect_cycles(&self, file: &str, target: &str) -> Result<(), ImportError> { + if file == target { + return Err(ImportError::CircularDependency { + files: vec![file.to_string()], + }); + } + + // Simple cycle detection: if target depends on file, we have a cycle + if let Some(target_deps) = self.dependency_graph.get(target) { + if target_deps.contains(file) { + return Err(ImportError::CircularDependency { + files: vec![file.to_string(), target.to_string()], + }); + } + } + + Ok(()) + } + + /// Recursively resolve imports for a file + fn resolve_imports_recursive(&mut self, filename: &str) -> Result, ImportError> { + // Check for cycles in processing + if self.processing_stack.contains(filename) { + return Err(ImportError::CircularDependency { + files: self + .processing_stack + .iter() + .cloned() + .collect::>() + .into_iter() + .chain(std::iter::once(filename.to_string())) + .collect(), + }); + } + + self.processing_stack.insert(filename.to_string()); + + // Parse the file + let nodes = self.parse_file(filename)?; + let mut result = Vec::new(); + + // Collect dependencies + let deps = self.collect_dependencies(filename, &nodes); + self.dependency_graph + .insert(filename.to_string(), deps.iter().cloned().collect()); + + // Process each node + for node in nodes { + match &node.kind { + ASTNodeKind::Use(use_stmt) => { + let path = &use_stmt.path; + + // Resolve path + let dep_file = if path.starts_with("std/") { + format!("src/{}{}", path, EXTENSION) + } else if path.starts_with("@") { + // Package manager support - TODO + todo!("Package manager imports not yet supported") + } else if path.starts_with(".") || path.starts_with("~") { + let importing_dir = Path::new(filename).parent().unwrap_or(Path::new("")); + let resolved_path = importing_dir.join(path); + resolved_path.to_string_lossy().to_string() + EXTENSION + } else { + return Err(ImportError::FileNotFound { + path: path.clone(), + reason: "Invalid import path format".to_string(), + }); + }; + + // Check for cycles + self.detect_cycles(filename, &dep_file)?; + + // Recursively resolve the imported file + let imported_nodes = self.resolve_imports_recursive(&dep_file)?; + result.extend(imported_nodes); + } + ASTNodeKind::Struct(s) => { + // Register the symbol + self.symbol_registry.register( + s.name.clone(), + filename.to_string(), + node.span.clone(), + )?; + result.push(node); + } + ASTNodeKind::Enum(e) => { + self.symbol_registry.register( + e.name.clone(), + filename.to_string(), + node.span.clone(), + )?; + result.push(node); + } + ASTNodeKind::Function(f) => { + self.symbol_registry.register( + f.name.clone(), + filename.to_string(), + node.span.clone(), + )?; + result.push(node); + } + ASTNodeKind::Impl(_) => { + // Impl blocks don't have names but are still tracked + result.push(node); + } + ASTNodeKind::Extern(e) => { + self.symbol_registry.register( + e.name.clone(), + filename.to_string(), + node.span.clone(), + )?; + result.push(node); + } + ASTNodeKind::Load(_) => { + result.push(node); + } + ASTNodeKind::Trait(t) => { + self.symbol_registry.register( + t.name.clone(), + filename.to_string(), + node.span.clone(), + )?; + result.push(node); + } + } + } + + self.processing_stack.remove(filename); + Ok(result) + } + + /// Resolve all imports starting from the given file + pub fn resolve(&mut self, filename: &str) -> Result, ImportError> { + println!("Starting import resolution..."); + self.resolve_imports_recursive(filename) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_content() { + let content1 = "fn main() do end"; + let content2 = "fn main() do end"; + let content3 = "fn main() { x }"; + + assert_eq!( + ImportResolver::hash_content(content1), + ImportResolver::hash_content(content2) + ); + assert_ne!( + ImportResolver::hash_content(content1), + ImportResolver::hash_content(content3) + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6da9abc..a9fac3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ pub mod ast; pub mod c_ir; pub mod c_lowerer; pub mod codegen; +pub mod import_resolver; pub mod lambda_lower; pub mod lexer; pub mod monomorphize; diff --git a/src/main.rs b/src/main.rs index 587e7c0..f4df3d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ -use logos::Logos; use std::fs; use suicmez::{ codegen::transpiler::Transpiler, + import_resolver::ImportResolver, lambda_lower::LambdaLowerer, - lexer::Token, monomorphize::{Monomorphizer, check_no_typevars}, - parser::Parser, typechecker::TypeChecker, }; @@ -164,34 +162,19 @@ fn format_type_error(source: &str, error: &suicmez::typechecker::TypeError) -> S } fn run_file(filename: &str) -> Result<(), String> { - // Read the source file + // ========== IMPORT RESOLUTION PHASE ========== + println!("\n=== Import Resolution Phase ==="); + let mut resolver = ImportResolver::new(); + let ast_nodes = resolver + .resolve(filename) + .map_err(|e| format!("Import resolution error: {}", e))?; + + println!("Import resolution complete! {} total nodes loaded", ast_nodes.len()); + + // Read the source file for error reporting let source = fs::read_to_string(filename) .map_err(|e| format!("Error reading file {}: {}", filename, e))?; - // First, we need to parse the source code - let mut tokens = Vec::new(); - let mut lexer = Token::lexer(&source); - - loop { - match lexer.next() { - Some(Ok(token)) => { - let span = lexer.span(); - tokens.push((token, span)); - } - Some(Err(_)) => { - return Err("Lexing error".to_string()); - } - None => break, - } - } - - let mut parser = Parser::new(filename.to_string(), tokens); - let ast_nodes = parser - .parse() - .map_err(|e| format_parse_error(&source, &e))?; - - println!("Parsed {} AST nodes successfully", ast_nodes.len()); - // Lower lambdas to generated functions let lowerer = LambdaLowerer::new(); let lowered_nodes = lowerer diff --git a/tests/import_tests/duplicate_main.sui b/tests/import_tests/duplicate_main.sui new file mode 100644 index 0000000..ad4a6ab --- /dev/null +++ b/tests/import_tests/duplicate_main.sui @@ -0,0 +1,6 @@ +use "./module_a" +use "./module_b" + +fn main() -> int do + 0 +end diff --git a/tests/import_tests/good_import.c b/tests/import_tests/good_import.c new file mode 100644 index 0000000..556be3d --- /dev/null +++ b/tests/import_tests/good_import.c @@ -0,0 +1,20 @@ +#include +#include +#include +#include +struct Point { + int x; + int y; +}; +int helper_func(void); +int main(void); + + +int helper_func(void) { + return 42; +} + +int main(void) { + return helper_func(); +} + diff --git a/tests/import_tests/good_import.sui b/tests/import_tests/good_import.sui new file mode 100644 index 0000000..74a48ad --- /dev/null +++ b/tests/import_tests/good_import.sui @@ -0,0 +1,5 @@ +use "./util" + +fn main() -> int do + helper_func() +end diff --git a/tests/import_tests/module_a.sui b/tests/import_tests/module_a.sui new file mode 100644 index 0000000..9ee28c9 --- /dev/null +++ b/tests/import_tests/module_a.sui @@ -0,0 +1,3 @@ +fn test_func() -> int do + 42 +end diff --git a/tests/import_tests/module_b.sui b/tests/import_tests/module_b.sui new file mode 100644 index 0000000..850a737 --- /dev/null +++ b/tests/import_tests/module_b.sui @@ -0,0 +1,3 @@ +fn test_func() -> int do + 99 +end diff --git a/tests/import_tests/util.sui b/tests/import_tests/util.sui new file mode 100644 index 0000000..47bd1bd --- /dev/null +++ b/tests/import_tests/util.sui @@ -0,0 +1,8 @@ +fn helper_func() -> int do + 42 +end + +struct Point + x: int, + y: int, +end