From ba61458a781c6146a020a702e844b48704848ccd Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Mon, 2 Feb 2026 11:53:18 +0000 Subject: [PATCH] pr 53 --- crates/prometeu-compiler/src/deps/mod.rs | 1 + crates/prometeu-compiler/src/deps/resolver.rs | 371 ++++++++++++++++++ crates/prometeu-compiler/src/lib.rs | 1 + docs/specs/pbs/files/PRs para Junie.md | 49 --- 4 files changed, 373 insertions(+), 49 deletions(-) create mode 100644 crates/prometeu-compiler/src/deps/mod.rs create mode 100644 crates/prometeu-compiler/src/deps/resolver.rs diff --git a/crates/prometeu-compiler/src/deps/mod.rs b/crates/prometeu-compiler/src/deps/mod.rs new file mode 100644 index 00000000..e7558041 --- /dev/null +++ b/crates/prometeu-compiler/src/deps/mod.rs @@ -0,0 +1 @@ +pub mod resolver; diff --git a/crates/prometeu-compiler/src/deps/resolver.rs b/crates/prometeu-compiler/src/deps/resolver.rs new file mode 100644 index 00000000..be36c47d --- /dev/null +++ b/crates/prometeu-compiler/src/deps/resolver.rs @@ -0,0 +1,371 @@ +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use crate::manifest::{Manifest, DependencySpec, load_manifest}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ProjectId { + pub name: String, + pub version: String, +} + +#[derive(Debug)] +pub struct ResolvedNode { + pub id: ProjectId, + pub path: PathBuf, + pub manifest: Manifest, +} + +#[derive(Debug)] +pub struct ResolvedEdge { + pub alias: String, + pub to: ProjectId, +} + +#[derive(Debug, Default)] +pub struct ResolvedGraph { + pub nodes: HashMap, + pub edges: HashMap>, + pub root_id: Option, +} + +#[derive(Debug)] +pub enum ResolveError { + CycleDetected(Vec), + MissingDependency(PathBuf), + VersionConflict { + name: String, + v1: String, + v2: String, + }, + NameCollision { + name: String, + p1: PathBuf, + p2: PathBuf, + }, + ManifestError(crate::manifest::ManifestError), + IoError { + path: PathBuf, + source: std::io::Error, + }, +} + +impl std::fmt::Display for ResolveError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResolveError::CycleDetected(chain) => write!(f, "Cycle detected: {}", chain.join(" -> ")), + ResolveError::MissingDependency(path) => write!(f, "Missing dependency at: {}", path.display()), + ResolveError::VersionConflict { name, v1, v2 } => { + write!(f, "Version conflict for project '{}': {} vs {}", name, v1, v2) + } + ResolveError::NameCollision { name, p1, p2 } => { + write!(f, "Name collision: two distinct projects claiming same name '{}' at {} and {}", name, p1.display(), p2.display()) + } + ResolveError::ManifestError(e) => write!(f, "Manifest error: {}", e), + ResolveError::IoError { path, source } => write!(f, "IO error at {}: {}", path.display(), source), + } + } +} + +impl std::error::Error for ResolveError {} + +impl From for ResolveError { + fn from(e: crate::manifest::ManifestError) -> Self { + ResolveError::ManifestError(e) + } +} + +pub fn resolve_graph(root_dir: &Path) -> Result { + let mut graph = ResolvedGraph::default(); + let mut visited = HashSet::new(); + let mut stack = Vec::new(); + + let root_path = root_dir.canonicalize().map_err(|e| ResolveError::IoError { + path: root_dir.to_path_buf(), + source: e, + })?; + + let root_id = resolve_recursive(&root_path, &mut graph, &mut visited, &mut stack)?; + graph.root_id = Some(root_id); + + Ok(graph) +} + +fn resolve_recursive( + project_path: &Path, + graph: &mut ResolvedGraph, + visited: &mut HashSet, + stack: &mut Vec, +) -> Result { + let manifest = load_manifest(project_path)?; + let project_id = ProjectId { + name: manifest.name.clone(), + version: manifest.version.clone(), + }; + + // Cycle detection + if let Some(pos) = stack.iter().position(|id| id == &project_id) { + let mut chain: Vec = stack[pos..].iter().map(|id| id.name.clone()).collect(); + chain.push(project_id.name.clone()); + return Err(ResolveError::CycleDetected(chain)); + } + + // Collision handling: Name collision + // If we find a project with the same name but different path/version, we might have a collision or version conflict. + for node in graph.nodes.values() { + if node.id.name == project_id.name { + if node.id.version != project_id.version { + return Err(ResolveError::VersionConflict { + name: project_id.name.clone(), + v1: node.id.version.clone(), + v2: project_id.version.clone(), + }); + } + // Same name, same version, but different path? + if node.path != project_path { + return Err(ResolveError::NameCollision { + name: project_id.name.clone(), + p1: node.path.clone(), + p2: project_path.to_path_buf(), + }); + } + } + } + + // If already fully visited, return the ID + if visited.contains(&project_id) { + return Ok(project_id); + } + + stack.push(project_id.clone()); + + let mut edges = Vec::new(); + for (alias, spec) in &manifest.dependencies { + let dep_path = match spec { + DependencySpec::Path(p) => project_path.join(p), + DependencySpec::Full(full) => { + if let Some(p) = &full.path { + project_path.join(p) + } else { + // Git dependencies not supported in v0 (PR-11 will add fetching) + return Err(ResolveError::MissingDependency(PathBuf::from("git-dependency-unsupported-in-v0"))); + } + } + }; + + let dep_path = dep_path.canonicalize().map_err(|_| ResolveError::MissingDependency(dep_path))?; + let dep_id = resolve_recursive(&dep_path, graph, visited, stack)?; + + edges.push(ResolvedEdge { + alias: alias.clone(), + to: dep_id, + }); + } + + stack.pop(); + visited.insert(project_id.clone()); + + graph.nodes.insert(project_id.clone(), ResolvedNode { + id: project_id.clone(), + path: project_path.to_path_buf(), + manifest, + }); + graph.edges.insert(project_id.clone(), edges); + + Ok(project_id) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + #[test] + fn test_simple_graph() { + let dir = tempdir().unwrap(); + let root = dir.path().join("root"); + let dep = dir.path().join("dep"); + fs::create_dir_all(&root).unwrap(); + fs::create_dir_all(&dep).unwrap(); + + fs::write(root.join("prometeu.json"), r#"{ + "name": "root", + "version": "0.1.0", + "dependencies": { "d": "../dep" } + }"#).unwrap(); + + fs::write(dep.join("prometeu.json"), r#"{ + "name": "dep", + "version": "1.0.0" + }"#).unwrap(); + + let graph = resolve_graph(&root).unwrap(); + assert_eq!(graph.nodes.len(), 2); + let root_id = graph.root_id.as_ref().unwrap(); + assert_eq!(root_id.name, "root"); + + let edges = graph.edges.get(root_id).unwrap(); + assert_eq!(edges.len(), 1); + assert_eq!(edges[0].alias, "d"); + assert_eq!(edges[0].to.name, "dep"); + } + + #[test] + fn test_cycle_detection() { + let dir = tempdir().unwrap(); + let a = dir.path().join("a"); + let b = dir.path().join("b"); + fs::create_dir_all(&a).unwrap(); + fs::create_dir_all(&b).unwrap(); + + fs::write(a.join("prometeu.json"), r#"{ + "name": "a", + "version": "0.1.0", + "dependencies": { "b": "../b" } + }"#).unwrap(); + + fs::write(b.join("prometeu.json"), r#"{ + "name": "b", + "version": "0.1.0", + "dependencies": { "a": "../a" } + }"#).unwrap(); + + let err = resolve_graph(&a).unwrap_err(); + match err { + ResolveError::CycleDetected(chain) => { + assert_eq!(chain, vec!["a", "b", "a"]); + } + _ => panic!("Expected CycleDetected error, got {:?}", err), + } + } + + #[test] + fn test_alias_does_not_change_identity() { + let dir = tempdir().unwrap(); + let root = dir.path().join("root"); + let dep = dir.path().join("dep"); + fs::create_dir_all(&root).unwrap(); + fs::create_dir_all(&dep).unwrap(); + + fs::write(root.join("prometeu.json"), r#"{ + "name": "root", + "version": "0.1.0", + "dependencies": { "my_alias": "../dep" } + }"#).unwrap(); + + fs::write(dep.join("prometeu.json"), r#"{ + "name": "actual_name", + "version": "1.0.0" + }"#).unwrap(); + + let graph = resolve_graph(&root).unwrap(); + let root_id = graph.root_id.as_ref().unwrap(); + let edges = graph.edges.get(root_id).unwrap(); + assert_eq!(edges[0].alias, "my_alias"); + assert_eq!(edges[0].to.name, "actual_name"); + assert!(graph.nodes.contains_key(&edges[0].to)); + } + + #[test] + fn test_version_conflict() { + let dir = tempdir().unwrap(); + let root = dir.path().join("root"); + let dep1 = dir.path().join("dep1"); + let dep2 = dir.path().join("dep2"); + let shared = dir.path().join("shared1"); + let shared2 = dir.path().join("shared2"); + fs::create_dir_all(&root).unwrap(); + fs::create_dir_all(&dep1).unwrap(); + fs::create_dir_all(&dep2).unwrap(); + fs::create_dir_all(&shared).unwrap(); + fs::create_dir_all(&shared2).unwrap(); + + fs::write(root.join("prometeu.json"), r#"{ + "name": "root", + "version": "0.1.0", + "dependencies": { "d1": "../dep1", "d2": "../dep2" } + }"#).unwrap(); + + fs::write(dep1.join("prometeu.json"), r#"{ + "name": "dep1", + "version": "0.1.0", + "dependencies": { "s": "../shared1" } + }"#).unwrap(); + + fs::write(dep2.join("prometeu.json"), r#"{ + "name": "dep2", + "version": "0.1.0", + "dependencies": { "s": "../shared2" } + }"#).unwrap(); + + fs::write(shared.join("prometeu.json"), r#"{ + "name": "shared", + "version": "1.0.0" + }"#).unwrap(); + + fs::write(shared2.join("prometeu.json"), r#"{ + "name": "shared", + "version": "2.0.0" + }"#).unwrap(); + + let err = resolve_graph(&root).unwrap_err(); + match err { + ResolveError::VersionConflict { name, .. } => { + assert_eq!(name, "shared"); + } + _ => panic!("Expected VersionConflict error, got {:?}", err), + } + } + + #[test] + fn test_name_collision() { + let dir = tempdir().unwrap(); + let root = dir.path().join("root"); + let dep1 = dir.path().join("dep1"); + let dep2 = dir.path().join("dep2"); + let p1 = dir.path().join("p1"); + let p2 = dir.path().join("p2"); + fs::create_dir_all(&root).unwrap(); + fs::create_dir_all(&dep1).unwrap(); + fs::create_dir_all(&dep2).unwrap(); + fs::create_dir_all(&p1).unwrap(); + fs::create_dir_all(&p2).unwrap(); + + fs::write(root.join("prometeu.json"), r#"{ + "name": "root", + "version": "0.1.0", + "dependencies": { "d1": "../dep1", "d2": "../dep2" } + }"#).unwrap(); + + fs::write(dep1.join("prometeu.json"), r#"{ + "name": "dep1", + "version": "0.1.0", + "dependencies": { "p": "../p1" } + }"#).unwrap(); + + fs::write(dep2.join("prometeu.json"), r#"{ + "name": "dep2", + "version": "0.1.0", + "dependencies": { "p": "../p2" } + }"#).unwrap(); + + // Both p1 and p2 claim to be "collision" version 1.0.0 + fs::write(p1.join("prometeu.json"), r#"{ + "name": "collision", + "version": "1.0.0" + }"#).unwrap(); + + fs::write(p2.join("prometeu.json"), r#"{ + "name": "collision", + "version": "1.0.0" + }"#).unwrap(); + + let err = resolve_graph(&root).unwrap_err(); + match err { + ResolveError::NameCollision { name, .. } => { + assert_eq!(name, "collision"); + } + _ => panic!("Expected NameCollision error, got {:?}", err), + } + } +} diff --git a/crates/prometeu-compiler/src/lib.rs b/crates/prometeu-compiler/src/lib.rs index c94396e3..6943df64 100644 --- a/crates/prometeu-compiler/src/lib.rs +++ b/crates/prometeu-compiler/src/lib.rs @@ -45,6 +45,7 @@ pub mod backend; pub mod frontends; pub mod compiler; pub mod manifest; +pub mod deps; use anyhow::Result; use clap::{Parser, Subcommand}; diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index c535f633..7bcb5d3e 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -1,52 +1,3 @@ -## PR-10 — Dependency Resolver v0: build a resolved project graph - -**Why:** We need a deterministic **module graph** from manifest(s) before compiling. - -### Scope - -* Implement `prometeu_compiler::deps::resolver`: - - * Input: root project dir - * Output: `ResolvedGraph` -* Graph nodes: - - * project identity: `{name, version}` - * local alias name (the key used by the parent) - * root path in filesystem (after fetch/resolve) - * manifest loaded for each node -* Resolution rules (v0): - - * DFS/stack walk from root - * cycle detection - * collision handling: - - * If the same (project name) appears with incompatible versions, error - * aliasing: - - * alias is local to the edge, but graph also stores the underlying project identity - -### Deliverables - -* `resolve_graph(root_dir) -> Result` -* `ResolveError` variants: - - * cycle detected (show chain) - * missing dependency (path not found / git not fetchable) - * version conflict (same project name, incompatible constraints) - * name collision (two distinct projects claiming same name) - -### Tests - -* simple root -> dep path graph -* cycle detection -* alias rename does not change project identity - -### Acceptance - -* Compiler can produce a stable, deterministic dependency graph. - ---- - ## PR-11 — Dependency Fetching v0: local cache layout + git/path fetch **Why:** Graph resolution needs a concrete directory for each dependency.