pr 61
This commit is contained in:
parent
b4deaa243e
commit
3b0d9435e2
@ -38,11 +38,34 @@ impl CompilationUnit {
|
||||
|
||||
|
||||
pub fn compile(project_dir: &Path) -> Result<CompilationUnit> {
|
||||
compile_ext(project_dir, false)
|
||||
}
|
||||
|
||||
pub fn compile_ext(project_dir: &Path, explain_deps: bool) -> Result<CompilationUnit> {
|
||||
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))?;
|
||||
|
||||
@ -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<ResolutionStep>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct ResolvedGraph {
|
||||
pub nodes: HashMap<ProjectId, ResolvedNode>,
|
||||
pub edges: HashMap<ProjectId, Vec<ResolvedEdge>>,
|
||||
pub root_id: Option<ProjectId>,
|
||||
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<ProjectId>) {
|
||||
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<ResolveError>,
|
||||
},
|
||||
}
|
||||
|
||||
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<ResolvedGraph, ResolveError> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 } => {
|
||||
|
||||
@ -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.
|
||||
Loading…
x
Reference in New Issue
Block a user