2026-03-24 13:40:28 +00:00

264 lines
8.4 KiB
Rust

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<PathBuf>,
pub files: Vec<PathBuf>,
pub test_files: Vec<PathBuf>,
}
#[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<std::io::Error> for SourceError {
fn from(e: std::io::Error) -> Self {
SourceError::Io(e)
}
}
impl From<crate::manifest::ManifestError> for SourceError {
fn from(e: crate::manifest::ManifestError) -> Self {
SourceError::Manifest(e)
}
}
impl From<DiagnosticBundle> for SourceError {
fn from(d: DiagnosticBundle) -> Self {
SourceError::Diagnostics(d)
}
}
#[derive(Debug, Clone)]
pub struct ExportTable {
pub symbols: HashMap<String, Symbol>,
}
pub fn discover(project_dir: &Path) -> Result<ProjectSources, SourceError> {
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<PathBuf>) -> 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<ExportTable, SourceError> {
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"));
}
}