diff --git a/crates/prometeu-compiler/src/building/mod.rs b/crates/prometeu-compiler/src/building/mod.rs new file mode 100644 index 00000000..7764a5c3 --- /dev/null +++ b/crates/prometeu-compiler/src/building/mod.rs @@ -0,0 +1 @@ +pub mod plan; diff --git a/crates/prometeu-compiler/src/building/plan.rs b/crates/prometeu-compiler/src/building/plan.rs new file mode 100644 index 00000000..065e7402 --- /dev/null +++ b/crates/prometeu-compiler/src/building/plan.rs @@ -0,0 +1,247 @@ +use std::collections::{BTreeMap, HashMap}; +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; +use crate::deps::resolver::{ProjectId, ResolvedGraph}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum BuildTarget { + Main, + Test, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BuildStep { + pub project_id: ProjectId, + pub project_dir: PathBuf, + pub target: BuildTarget, + pub sources: Vec, + pub deps: BTreeMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BuildPlan { + pub steps: Vec, +} + +impl BuildPlan { + pub fn from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Self { + let mut steps = Vec::new(); + let sorted_ids = topological_sort(graph); + + for id in sorted_ids { + if let Some(node) = graph.nodes.get(&id) { + let sources_list: Vec = match target { + BuildTarget::Main => node.sources.files.clone(), + BuildTarget::Test => node.sources.test_files.clone(), + }; + + // Normalize to relative paths and sort lexicographically + let mut sources: Vec = sources_list + .into_iter() + .map(|p| { + p.strip_prefix(&node.path) + .map(|rp| rp.to_path_buf()) + .unwrap_or(p) + }) + .collect(); + sources.sort(); + + let mut deps = BTreeMap::new(); + if let Some(edges) = graph.edges.get(&id) { + for edge in edges { + deps.insert(edge.alias.clone(), edge.to.clone()); + } + } + + steps.push(BuildStep { + project_id: id.clone(), + project_dir: node.path.clone(), + target, + sources, + deps, + }); + } + } + + Self { steps } + } +} + +fn topological_sort(graph: &ResolvedGraph) -> Vec { + let mut in_degree = HashMap::new(); + let mut adj = HashMap::new(); + + for id in graph.nodes.keys() { + in_degree.insert(id.clone(), 0); + adj.insert(id.clone(), Vec::new()); + } + + for (from, edges) in &graph.edges { + for edge in edges { + // from depends on edge.to + // so edge.to must be built BEFORE from + // edge.to -> from + adj.get_mut(&edge.to).unwrap().push(from.clone()); + *in_degree.get_mut(from).unwrap() += 1; + } + } + + let mut ready: std::collections::BinaryHeap = graph.nodes.keys() + .filter(|id| *in_degree.get(id).unwrap() == 0) + .map(|id| ReverseProjectId(id.clone())) + .collect(); + + let mut result = Vec::new(); + while let Some(ReverseProjectId(u)) = ready.pop() { + result.push(u.clone()); + + if let Some(neighbors) = adj.get(&u) { + for v in neighbors { + let degree = in_degree.get_mut(v).unwrap(); + *degree -= 1; + if *degree == 0 { + ready.push(ReverseProjectId(v.clone())); + } + } + } + } + + result +} + +#[derive(Eq, PartialEq)] +struct ReverseProjectId(ProjectId); + +impl Ord for ReverseProjectId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + // BinaryHeap is a max-heap. We want min-heap for lexicographic order. + // So we reverse the comparison. + other.0.name.cmp(&self.0.name) + .then(other.0.version.cmp(&self.0.version)) + } +} + +impl PartialOrd for ReverseProjectId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::deps::resolver::{ProjectId, ResolvedNode, ResolvedEdge, ResolvedGraph}; + use crate::sources::ProjectSources; + use crate::manifest::Manifest; + use std::collections::HashMap; + + fn mock_node(name: &str, version: &str) -> ResolvedNode { + ResolvedNode { + id: ProjectId { name: name.to_string(), version: version.to_string() }, + path: PathBuf::from(format!("/{}", name)), + manifest: Manifest { + name: name.to_string(), + version: version.to_string(), + kind: crate::manifest::ManifestKind::Lib, + dependencies: HashMap::new(), + }, + sources: ProjectSources { + main: None, + files: vec![PathBuf::from("b.pbs"), PathBuf::from("a.pbs")], + test_files: vec![PathBuf::from("test_b.pbs"), PathBuf::from("test_a.pbs")], + }, + } + } + + #[test] + fn test_topo_sort_stability() { + let mut graph = ResolvedGraph::default(); + + let a = mock_node("a", "1.0.0"); + let b = mock_node("b", "1.0.0"); + let c = mock_node("c", "1.0.0"); + + graph.nodes.insert(a.id.clone(), a); + graph.nodes.insert(b.id.clone(), b); + graph.nodes.insert(c.id.clone(), c); + + // No edges, should be alphabetical: a, b, c + let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); + assert_eq!(plan.steps[0].project_id.name, "a"); + assert_eq!(plan.steps[1].project_id.name, "b"); + assert_eq!(plan.steps[2].project_id.name, "c"); + } + + #[test] + fn test_topo_sort_dependencies() { + let mut graph = ResolvedGraph::default(); + + let a = mock_node("a", "1.0.0"); + let b = mock_node("b", "1.0.0"); + let c = mock_node("c", "1.0.0"); + + graph.nodes.insert(a.id.clone(), a.clone()); + graph.nodes.insert(b.id.clone(), b.clone()); + graph.nodes.insert(c.id.clone(), c.clone()); + + // c depends on b, b depends on a + // Sort should be: a, b, c + graph.edges.insert(c.id.clone(), vec![ResolvedEdge { alias: "b_alias".to_string(), to: b.id.clone() }]); + graph.edges.insert(b.id.clone(), vec![ResolvedEdge { alias: "a_alias".to_string(), to: a.id.clone() }]); + + let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); + assert_eq!(plan.steps.len(), 3); + assert_eq!(plan.steps[0].project_id.name, "a"); + assert_eq!(plan.steps[1].project_id.name, "b"); + assert_eq!(plan.steps[2].project_id.name, "c"); + + assert_eq!(plan.steps[2].deps.get("b_alias").unwrap(), &b.id); + } + + #[test] + fn test_topo_sort_complex() { + let mut graph = ResolvedGraph::default(); + + // d -> b, c + // b -> a + // c -> a + // a + // Valid sorts: a, b, c, d OR a, c, b, d + // Lexicographic rule says b before c. So a, b, c, d. + + let a = mock_node("a", "1.0.0"); + let b = mock_node("b", "1.0.0"); + let c = mock_node("c", "1.0.0"); + let d = mock_node("d", "1.0.0"); + + graph.nodes.insert(a.id.clone(), a.clone()); + graph.nodes.insert(b.id.clone(), b.clone()); + graph.nodes.insert(c.id.clone(), c.clone()); + graph.nodes.insert(d.id.clone(), d.clone()); + + graph.edges.insert(d.id.clone(), vec![ + ResolvedEdge { alias: "b".to_string(), to: b.id.clone() }, + ResolvedEdge { alias: "c".to_string(), to: c.id.clone() }, + ]); + graph.edges.insert(b.id.clone(), vec![ResolvedEdge { alias: "a".to_string(), to: a.id.clone() }]); + graph.edges.insert(c.id.clone(), vec![ResolvedEdge { alias: "a".to_string(), to: a.id.clone() }]); + + let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); + let names: Vec<_> = plan.steps.iter().map(|s| s.project_id.name.as_str()).collect(); + assert_eq!(names, vec!["a", "b", "c", "d"]); + } + + #[test] + fn test_sources_sorting() { + let mut graph = ResolvedGraph::default(); + let a = mock_node("a", "1.0.0"); + graph.nodes.insert(a.id.clone(), a); + + let plan = BuildPlan::from_graph(&graph, BuildTarget::Main); + assert_eq!(plan.steps[0].sources, vec![PathBuf::from("a.pbs"), PathBuf::from("b.pbs")]); + + let plan_test = BuildPlan::from_graph(&graph, BuildTarget::Test); + assert_eq!(plan_test.steps[0].sources, vec![PathBuf::from("test_a.pbs"), PathBuf::from("test_b.pbs")]); + } +} diff --git a/crates/prometeu-compiler/src/deps/resolver.rs b/crates/prometeu-compiler/src/deps/resolver.rs index 842d46fb..40c84104 100644 --- a/crates/prometeu-compiler/src/deps/resolver.rs +++ b/crates/prometeu-compiler/src/deps/resolver.rs @@ -1,16 +1,17 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; use crate::manifest::{Manifest, load_manifest}; use crate::deps::fetch::{fetch_dependency, FetchError}; use crate::sources::{ProjectSources, discover, SourceError}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ProjectId { pub name: String, pub version: String, } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolvedNode { pub id: ProjectId, pub path: PathBuf, @@ -18,7 +19,7 @@ pub struct ResolvedNode { pub sources: ProjectSources, } -#[derive(Debug)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResolvedEdge { pub alias: String, pub to: ProjectId, diff --git a/crates/prometeu-compiler/src/lib.rs b/crates/prometeu-compiler/src/lib.rs index eb03fc1c..8ca91e4f 100644 --- a/crates/prometeu-compiler/src/lib.rs +++ b/crates/prometeu-compiler/src/lib.rs @@ -47,6 +47,7 @@ pub mod compiler; pub mod manifest; pub mod deps; pub mod sources; +pub mod building; use anyhow::Result; use clap::{Parser, Subcommand}; diff --git a/crates/prometeu-compiler/src/sources.rs b/crates/prometeu-compiler/src/sources.rs index 995d9da9..36a9fd5b 100644 --- a/crates/prometeu-compiler/src/sources.rs +++ b/crates/prometeu-compiler/src/sources.rs @@ -1,12 +1,13 @@ use std::path::{Path, PathBuf}; use std::fs; use std::collections::HashMap; +use serde::{Deserialize, Serialize}; use crate::manifest::{load_manifest, ManifestKind}; use crate::frontends::pbs::{Symbol, Visibility, parser::Parser, collector::SymbolCollector}; use crate::common::files::FileManager; use crate::common::diagnostics::DiagnosticBundle; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ProjectSources { pub main: Option, pub files: Vec,