use crate::common::diagnostics::DiagnosticBundle; use crate::common::files::FileManager; use crate::frontends::pbs::{collector::SymbolCollector, parser::Parser, Symbol, Visibility}; use crate::common::spans::FileId; use crate::manifest::{load_manifest, ManifestKind}; use prometeu_analysis::NameInterner; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProjectSources { pub main: Option, pub files: Vec, pub test_files: Vec, } #[derive(Debug)] pub enum SourceError { Io(std::io::Error), Manifest(crate::manifest::ManifestError), MissingMain(PathBuf), Diagnostics(DiagnosticBundle), } impl std::fmt::Display for SourceError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SourceError::Io(e) => write!(f, "IO error: {}", e), SourceError::Manifest(e) => write!(f, "Manifest error: {}", e), SourceError::MissingMain(path) => write!(f, "Missing entry point: {}", path.display()), SourceError::Diagnostics(d) => write!(f, "Source diagnostics: {:?}", d), } } } impl std::error::Error for SourceError {} impl From for SourceError { fn from(e: std::io::Error) -> Self { SourceError::Io(e) } } impl From for SourceError { fn from(e: crate::manifest::ManifestError) -> Self { SourceError::Manifest(e) } } impl From for SourceError { fn from(d: DiagnosticBundle) -> Self { SourceError::Diagnostics(d) } } #[derive(Debug, Clone)] pub struct ExportTable { pub symbols: HashMap, } pub fn discover(project_dir: &Path) -> Result { let project_dir = project_dir.canonicalize()?; let manifest = load_manifest(&project_dir)?; let main_modules_dir = project_dir.join("src/main/modules"); let test_modules_dir = project_dir.join("src/test/modules"); let mut production_files = Vec::new(); if main_modules_dir.exists() && main_modules_dir.is_dir() { discover_recursive(&main_modules_dir, &mut production_files)?; } let mut test_files = Vec::new(); if test_modules_dir.exists() && test_modules_dir.is_dir() { discover_recursive(&test_modules_dir, &mut test_files)?; } // Sort files for determinism production_files.sort(); test_files.sort(); // Recommended main: src/main/modules/main.pbs let main_path = main_modules_dir.join("main.pbs"); let has_main = production_files.iter().any(|p| p == &main_path); let main = if has_main { Some(main_path) } else { None }; if manifest.kind == ManifestKind::App && main.is_none() { return Err(SourceError::MissingMain(main_modules_dir.join("main.pbs"))); } Ok(ProjectSources { main, files: production_files, test_files, }) } fn discover_recursive(dir: &Path, files: &mut Vec) -> std::io::Result<()> { for entry in fs::read_dir(dir)? { let entry = entry?; let path = entry.path(); if path.is_dir() { discover_recursive(&path, files)?; } else if let Some(ext) = path.extension() { if ext == "pbs" { files.push(path); } } } Ok(()) } pub fn build_exports(module_dir: &Path, file_manager: &mut FileManager) -> Result { let mut symbols = HashMap::new(); let mut files = Vec::new(); let mut interner = NameInterner::new(); if module_dir.is_dir() { discover_recursive(module_dir, &mut files)?; } else if module_dir.extension().map_or(false, |ext| ext == "pbs") { files.push(module_dir.to_path_buf()); } for file_path in files { let source = fs::read_to_string(&file_path)?; let file_id = file_manager.add(file_path.clone(), source.clone()); let mut parser = Parser::new(&source, FileId(file_id as u32), &mut interner); let parsed = parser.parse_file()?; let mut collector = SymbolCollector::new(&interner); let (type_symbols, value_symbols) = collector.collect(&parsed.arena, parsed.root)?; // Merge only public symbols for symbol in type_symbols.symbols.into_values() { if symbol.visibility == Visibility::Pub { symbols.insert(interner.resolve(symbol.name).to_string(), symbol); } } for symbol in value_symbols.symbols.into_values() { if symbol.visibility == Visibility::Pub { symbols.insert(interner.resolve(symbol.name).to_string(), symbol); } } } Ok(ExportTable { symbols }) } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::tempdir; #[test] fn test_discover_app_with_main() { let dir = tempdir().unwrap(); let project_dir = dir.path().canonicalize().unwrap(); fs::write(project_dir.join("prometeu.json"), r#"{ "name": "app", "version": "0.1.0", "kind": "app" }"#).unwrap(); fs::create_dir_all(project_dir.join("src/main/modules")).unwrap(); let main_pbs = project_dir.join("src/main/modules/main.pbs"); fs::write(&main_pbs, "").unwrap(); let other_pbs = project_dir.join("src/main/modules/other.pbs"); fs::write(&other_pbs, "").unwrap(); let sources = discover(&project_dir).unwrap(); assert_eq!(sources.main, Some(main_pbs)); assert_eq!(sources.files.len(), 2); } #[test] fn test_discover_app_missing_main() { let dir = tempdir().unwrap(); let project_dir = dir.path().canonicalize().unwrap(); fs::write(project_dir.join("prometeu.json"), r#"{ "name": "app", "version": "0.1.0", "kind": "app" }"#).unwrap(); fs::create_dir_all(project_dir.join("src/main/modules")).unwrap(); fs::write(project_dir.join("src/main/modules/not_main.pbs"), "").unwrap(); let result = discover(&project_dir); assert!(matches!(result, Err(SourceError::MissingMain(_)))); } #[test] fn test_discover_lib_without_main() { let dir = tempdir().unwrap(); let project_dir = dir.path().canonicalize().unwrap(); fs::write(project_dir.join("prometeu.json"), r#"{ "name": "lib", "version": "0.1.0", "kind": "lib" }"#).unwrap(); fs::create_dir_all(project_dir.join("src/main/modules")).unwrap(); let lib_pbs = project_dir.join("src/main/modules/lib.pbs"); fs::write(&lib_pbs, "").unwrap(); let sources = discover(&project_dir).unwrap(); assert_eq!(sources.main, None); assert_eq!(sources.files, vec![lib_pbs]); } #[test] fn test_discover_recursive() { let dir = tempdir().unwrap(); let project_dir = dir.path().canonicalize().unwrap(); fs::write(project_dir.join("prometeu.json"), r#"{ "name": "lib", "version": "0.1.0", "kind": "lib" }"#).unwrap(); fs::create_dir_all(project_dir.join("src/main/modules/utils")).unwrap(); let main_pbs = project_dir.join("src/main/modules/main.pbs"); let util_pbs = project_dir.join("src/main/modules/utils/util.pbs"); fs::write(&main_pbs, "").unwrap(); fs::write(&util_pbs, "").unwrap(); let sources = discover(&project_dir).unwrap(); assert_eq!(sources.files.len(), 2); assert!(sources.files.contains(&main_pbs)); assert!(sources.files.contains(&util_pbs)); } #[test] fn test_build_exports() { let dir = tempdir().unwrap(); let module_dir = dir.path().join("math"); fs::create_dir_all(&module_dir).unwrap(); fs::write(module_dir.join("Vector.pbs"), "pub declare struct Vector {}").unwrap(); fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden {}").unwrap(); let mut fm = FileManager::new(); let exports = build_exports(&module_dir, &mut fm).unwrap(); assert!(exports.symbols.contains_key("Vector")); assert!(!exports.symbols.contains_key("Hidden")); } }