From 3b0d9435e2ead0147e28451e7b527836ab73b967 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Mon, 2 Feb 2026 20:07:49 +0000 Subject: [PATCH] pr 61 --- crates/prometeu-compiler/src/compiler.rs | 27 +- crates/prometeu-compiler/src/deps/resolver.rs | 282 +++++++++++++++++- crates/prometeu-compiler/src/lib.rs | 7 +- docs/specs/pbs/files/PRs para Junie.md | 45 --- 4 files changed, 298 insertions(+), 63 deletions(-) diff --git a/crates/prometeu-compiler/src/compiler.rs b/crates/prometeu-compiler/src/compiler.rs index 2e0165d9..0aad62ac 100644 --- a/crates/prometeu-compiler/src/compiler.rs +++ b/crates/prometeu-compiler/src/compiler.rs @@ -38,11 +38,34 @@ impl CompilationUnit { pub fn compile(project_dir: &Path) -> Result { + compile_ext(project_dir, false) +} + +pub fn compile_ext(project_dir: &Path, explain_deps: bool) -> Result { let config = ProjectConfig::load(project_dir)?; if config.script_fe == "pbs" { - let graph = crate::deps::resolver::resolve_graph(project_dir) - .map_err(|e| anyhow::anyhow!("Dependency resolution failed: {}", e))?; + let graph_res = crate::deps::resolver::resolve_graph(project_dir); + + if explain_deps || graph_res.is_err() { + match &graph_res { + Ok(graph) => { + println!("{}", graph.explain()); + } + Err(crate::deps::resolver::ResolveError::WithTrace { trace, source }) => { + // Create a dummy graph to use its explain logic for the trace + let mut dummy_graph = crate::deps::resolver::ResolvedGraph::default(); + dummy_graph.trace = trace.clone(); + println!("{}", dummy_graph.explain()); + eprintln!("Dependency resolution failed: {}", source); + } + Err(e) => { + eprintln!("Dependency resolution failed: {}", e); + } + } + } + + let graph = graph_res.map_err(|e| anyhow::anyhow!("Dependency resolution failed: {}", e))?; let program_image = crate::building::orchestrator::build_from_graph(&graph, crate::building::plan::BuildTarget::Main) .map_err(|e| anyhow::anyhow!("Build failed: {}", e))?; diff --git a/crates/prometeu-compiler/src/deps/resolver.rs b/crates/prometeu-compiler/src/deps/resolver.rs index 9228fb50..edde0874 100644 --- a/crates/prometeu-compiler/src/deps/resolver.rs +++ b/crates/prometeu-compiler/src/deps/resolver.rs @@ -25,11 +25,40 @@ pub struct ResolvedEdge { pub to: ProjectId, } -#[derive(Debug, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ResolutionStep { + TryResolve { + alias: String, + spec: String, + }, + Resolved { + project_id: ProjectId, + path: PathBuf, + }, + UsingCached { + project_id: ProjectId, + }, + Conflict { + name: String, + existing_version: String, + new_version: String, + }, + Error { + message: String, + }, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +pub struct ResolutionTrace { + pub steps: Vec, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct ResolvedGraph { pub nodes: HashMap, pub edges: HashMap>, pub root_id: Option, + pub trace: ResolutionTrace, } impl ResolvedGraph { @@ -59,6 +88,52 @@ impl ResolvedGraph { } None } + + pub fn explain(&self) -> String { + let mut out = String::new(); + out.push_str("--- Dependency Resolution Trace ---\n"); + for step in &self.trace.steps { + match step { + ResolutionStep::TryResolve { alias, spec } => { + out.push_str(&format!(" [?] Resolving '{}' (spec: {})\n", alias, spec)); + } + ResolutionStep::Resolved { project_id, path } => { + out.push_str(&format!(" [✓] Resolved '{}' v{} at {:?}\n", project_id.name, project_id.version, path)); + } + ResolutionStep::UsingCached { project_id } => { + out.push_str(&format!(" [.] Using cached '{}' v{}\n", project_id.name, project_id.version)); + } + ResolutionStep::Conflict { name, existing_version, new_version } => { + out.push_str(&format!(" [!] CONFLICT for '{}': {} vs {}\n", name, existing_version, new_version)); + } + ResolutionStep::Error { message } => { + out.push_str(&format!(" [X] ERROR: {}\n", message)); + } + } + } + + if let Some(root_id) = &self.root_id { + out.push_str("\n--- Resolved Dependency Graph ---\n"); + let mut visited = HashSet::new(); + out.push_str(&format!("{} v{}\n", root_id.name, root_id.version)); + self.print_node(root_id, 0, &mut out, &mut visited); + } + + out + } + + fn print_node(&self, id: &ProjectId, indent: usize, out: &mut String, visited: &mut HashSet) { + if let Some(edges) = self.edges.get(id) { + for edge in edges { + let prefix = " ".repeat(indent); + out.push_str(&format!("{}└── {}: {} v{}\n", prefix, edge.alias, edge.to.name, edge.to.version)); + if !visited.contains(&edge.to) { + visited.insert(edge.to.clone()); + self.print_node(&edge.to, indent + 1, out, visited); + } + } + } + } } #[derive(Debug)] @@ -82,6 +157,10 @@ pub enum ResolveError { path: PathBuf, source: std::io::Error, }, + WithTrace { + trace: ResolutionTrace, + source: Box, + }, } impl std::fmt::Display for ResolveError { @@ -99,6 +178,7 @@ impl std::fmt::Display for ResolveError { ResolveError::FetchError(e) => write!(f, "Fetch error: {}", e), ResolveError::SourceError(e) => write!(f, "Source error: {}", e), ResolveError::IoError { path, source } => write!(f, "IO error at {}: {}", path.display(), source), + ResolveError::WithTrace { source, .. } => write!(f, "{}", source), } } } @@ -140,7 +220,13 @@ pub fn resolve_graph(root_dir: &Path) -> Result { source: e, })?; - let root_id = resolve_recursive(&root_path, &root_path, &mut graph, &mut visited, &mut stack)?; + let root_id = match resolve_recursive(&root_path, &root_path, &mut graph, &mut visited, &mut stack) { + Ok(id) => id, + Err(e) => return Err(ResolveError::WithTrace { + trace: graph.trace, + source: Box::new(e), + }), + }; graph.root_id = Some(root_id); Ok(graph) @@ -172,6 +258,11 @@ fn resolve_recursive( for node in graph.nodes.values() { if node.id.name == project_id.name { if node.id.version != project_id.version { + graph.trace.steps.push(ResolutionStep::Conflict { + name: project_id.name.clone(), + existing_version: node.id.version.clone(), + new_version: project_id.version.clone(), + }); return Err(ResolveError::VersionConflict { name: project_id.name.clone(), v1: node.id.version.clone(), @@ -191,15 +282,45 @@ fn resolve_recursive( // If already fully visited, return the ID if visited.contains(&project_id) { + graph.trace.steps.push(ResolutionStep::UsingCached { + project_id: project_id.clone(), + }); return Ok(project_id); } + graph.trace.steps.push(ResolutionStep::Resolved { + project_id: project_id.clone(), + path: project_path.to_path_buf(), + }); + + visited.insert(project_id.clone()); stack.push(project_id.clone()); let mut edges = Vec::new(); for (alias, spec) in &manifest.dependencies { - let dep_path = fetch_dependency(alias, spec, project_path, root_project_dir)?; - let dep_id = resolve_recursive(&dep_path, root_project_dir, graph, visited, stack)?; + graph.trace.steps.push(ResolutionStep::TryResolve { + alias: alias.clone(), + spec: format!("{:?}", spec), + }); + + let dep_path = match fetch_dependency(alias, spec, project_path, root_project_dir) { + Ok(p) => p, + Err(e) => { + graph.trace.steps.push(ResolutionStep::Error { + message: format!("Fetch error for '{}': {}", alias, e), + }); + return Err(e.into()); + } + }; + + let dep_id = match resolve_recursive(&dep_path, root_project_dir, graph, visited, stack) { + Ok(id) => id, + Err(e) => { + // If it's a version conflict, we already pushed it inside the recursive call + // but let's make sure we catch other errors too. + return Err(e); + } + }; edges.push(ResolvedEdge { alias: alias.clone(), @@ -208,8 +329,6 @@ fn resolve_recursive( } stack.pop(); - visited.insert(project_id.clone()); - graph.nodes.insert(project_id.clone(), ResolvedNode { id: project_id.clone(), path: project_path.to_path_buf(), @@ -283,10 +402,14 @@ mod tests { let err = resolve_graph(&a).unwrap_err(); match err { - ResolveError::CycleDetected(chain) => { - assert_eq!(chain, vec!["a", "b", "a"]); + ResolveError::WithTrace { source, .. } => { + if let ResolveError::CycleDetected(chain) = *source { + assert_eq!(chain, vec!["a", "b", "a"]); + } else { + panic!("Expected CycleDetected error, got {:?}", source); + } } - _ => panic!("Expected CycleDetected error, got {:?}", err), + _ => panic!("Expected WithTrace containing CycleDetected error, got {:?}", err), } } @@ -368,10 +491,14 @@ mod tests { let err = resolve_graph(&root).unwrap_err(); match err { - ResolveError::VersionConflict { name, .. } => { - assert_eq!(name, "shared"); + ResolveError::WithTrace { source, .. } => { + if let ResolveError::VersionConflict { name, .. } = *source { + assert_eq!(name, "shared"); + } else { + panic!("Expected VersionConflict error, got {:?}", source); + } } - _ => panic!("Expected VersionConflict error, got {:?}", err), + _ => panic!("Expected WithTrace containing VersionConflict error, got {:?}", err), } } @@ -425,10 +552,14 @@ mod tests { let err = resolve_graph(&root).unwrap_err(); match err { - ResolveError::NameCollision { name, .. } => { - assert_eq!(name, "collision"); + ResolveError::WithTrace { source, .. } => { + if let ResolveError::NameCollision { name, .. } = *source { + assert_eq!(name, "collision"); + } else { + panic!("Expected NameCollision error, got {:?}", source); + } } - _ => panic!("Expected NameCollision error, got {:?}", err), + _ => panic!("Expected WithTrace containing NameCollision error, got {:?}", err), } } @@ -509,4 +640,125 @@ mod tests { let expected = root.join("src/main/modules/local_mod"); assert_eq!(path, expected); } + + #[test] + fn test_resolution_trace_and_explain() { + let dir = tempdir().unwrap(); + let root_dir = dir.path().join("root"); + fs::create_dir_all(&root_dir).unwrap(); + let root_dir = root_dir.canonicalize().unwrap(); + + // Root project + fs::write(root_dir.join("prometeu.json"), r#"{ + "name": "root", + "version": "1.0.0", + "dependencies": { + "dep1": { "path": "../dep1" } + } + }"#).unwrap(); + fs::create_dir_all(root_dir.join("src/main/modules")).unwrap(); + fs::write(root_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + // Dep 1 + let dep1_dir = dir.path().join("dep1"); + fs::create_dir_all(&dep1_dir).unwrap(); + let dep1_dir = dep1_dir.canonicalize().unwrap(); + fs::write(dep1_dir.join("prometeu.json"), r#"{ + "name": "dep1", + "version": "1.1.0" + }"#).unwrap(); + fs::create_dir_all(dep1_dir.join("src/main/modules")).unwrap(); + fs::write(dep1_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + let graph = resolve_graph(&root_dir).unwrap(); + let explanation = graph.explain(); + + assert!(explanation.contains("--- Dependency Resolution Trace ---")); + assert!(explanation.contains("[✓] Resolved 'root' v1.0.0")); + assert!(explanation.contains("[?] Resolving 'dep1'")); + assert!(explanation.contains("[✓] Resolved 'dep1' v1.1.0")); + + assert!(explanation.contains("--- Resolved Dependency Graph ---")); + assert!(explanation.contains("root v1.0.0")); + assert!(explanation.contains("└── dep1: dep1 v1.1.0")); + } + + #[test] + fn test_conflict_explanation() { + let dir = tempdir().unwrap(); + let root_dir = dir.path().join("root"); + fs::create_dir_all(&root_dir).unwrap(); + let root_dir = root_dir.canonicalize().unwrap(); + + // Root -> A, B + // A -> C v1 + // B -> C v2 + + fs::write(root_dir.join("prometeu.json"), r#"{ + "name": "root", + "version": "1.0.0", + "dependencies": { + "a": { "path": "../a" }, + "b": { "path": "../b" } + } + }"#).unwrap(); + fs::create_dir_all(root_dir.join("src/main/modules")).unwrap(); + fs::write(root_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + let a_dir = dir.path().join("a"); + fs::create_dir_all(&a_dir).unwrap(); + let a_dir = a_dir.canonicalize().unwrap(); + fs::write(a_dir.join("prometeu.json"), r#"{ + "name": "a", + "version": "1.0.0", + "dependencies": { "c": { "path": "../c1" } } + }"#).unwrap(); + fs::create_dir_all(a_dir.join("src/main/modules")).unwrap(); + fs::write(a_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + let b_dir = dir.path().join("b"); + fs::create_dir_all(&b_dir).unwrap(); + let b_dir = b_dir.canonicalize().unwrap(); + fs::write(b_dir.join("prometeu.json"), r#"{ + "name": "b", + "version": "1.0.0", + "dependencies": { "c": { "path": "../c2" } } + }"#).unwrap(); + fs::create_dir_all(b_dir.join("src/main/modules")).unwrap(); + fs::write(b_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + let c1_dir = dir.path().join("c1"); + fs::create_dir_all(&c1_dir).unwrap(); + let c1_dir = c1_dir.canonicalize().unwrap(); + fs::write(c1_dir.join("prometeu.json"), r#"{ + "name": "c", + "version": "1.0.0" + }"#).unwrap(); + fs::create_dir_all(c1_dir.join("src/main/modules")).unwrap(); + fs::write(c1_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + let c2_dir = dir.path().join("c2"); + fs::create_dir_all(&c2_dir).unwrap(); + let c2_dir = c2_dir.canonicalize().unwrap(); + fs::write(c2_dir.join("prometeu.json"), r#"{ + "name": "c", + "version": "2.0.0" + }"#).unwrap(); + fs::create_dir_all(c2_dir.join("src/main/modules")).unwrap(); + fs::write(c2_dir.join("src/main/modules/main.pbs"), "").unwrap(); + + let res = resolve_graph(&root_dir); + assert!(res.is_err()); + + if let Err(ResolveError::WithTrace { trace, source }) = res { + let mut dummy = ResolvedGraph::default(); + dummy.trace = trace; + let explanation = dummy.explain(); + + assert!(explanation.contains("[!] CONFLICT for 'c': 1.0.0 vs 2.0.0")); + assert!(source.to_string().contains("Version conflict for project 'c': 1.0.0 vs 2.0.0")); + } else { + panic!("Expected WithTrace error"); + } + } } diff --git a/crates/prometeu-compiler/src/lib.rs b/crates/prometeu-compiler/src/lib.rs index 8ca91e4f..8ab901ef 100644 --- a/crates/prometeu-compiler/src/lib.rs +++ b/crates/prometeu-compiler/src/lib.rs @@ -86,6 +86,10 @@ pub enum Commands { /// Whether to generate a .json symbols file for source mapping. #[arg(long, default_value_t = true)] emit_symbols: bool, + + /// Whether to explain the dependency resolution process. + #[arg(long)] + explain_deps: bool, }, /// Verifies if a Prometeu project is syntactically and semantically valid without emitting code. Verify { @@ -105,6 +109,7 @@ pub fn run() -> Result<()> { out, emit_disasm, emit_symbols, + explain_deps, .. } => { let build_dir = project_dir.join("build"); @@ -117,7 +122,7 @@ pub fn run() -> Result<()> { println!("Building project at {:?}", project_dir); println!("Output: {:?}", out); - let compilation_unit = compiler::compile(&project_dir)?; + let compilation_unit = compiler::compile_ext(&project_dir, explain_deps)?; compilation_unit.export(&out, emit_disasm, emit_symbols)?; } Commands::Verify { project_dir } => { diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index cf0be47a..e69de29b 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -1,45 +0,0 @@ -## PR-17 — Diagnostics UX: dependency graph and resolution trace - -**Why:** Dependency failures must be explainable. - -### Scope - -* Add compiler diagnostics output: - - * resolved dependency graph - * alias → project mapping - * explanation of conflicts or failures - -* Add CLI/API flag: - - * `--explain-deps` - -### Deliverables - -* human-readable resolution trace - -### Tests - -* snapshot tests for diagnostics output (best-effort) - -### Acceptance - -* Users can debug dependency and linking issues without guesswork. - ---- - -## Suggested Execution Order - -1. PR-09 → PR-10 → PR-11 -2. PR-12 → PR-13 -3. PR-14 → PR-15 -4. PR-16 → PR-17 - ---- - -## Notes for Junie - -* Keep all v0 decisions simple and deterministic. -* Prefer explicit errors over silent fallback. -* Treat `archive-pbs/test01` as the north-star integration scenario. -* No background work: every PR must include tests proving behavior.