diff --git a/Cargo.lock b/Cargo.lock index ad3fcebc..0167cbe3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1886,6 +1886,7 @@ dependencies = [ "clap", "prometeu-core", "prometeu-deps", + "prometeu-languages-registry", "serde", "serde_json", ] @@ -1923,7 +1924,11 @@ dependencies = [ "anyhow", "camino", "prometeu-core", + "prometeu-language-api", + "prometeu-languages-registry", "serde", + "serde_json", + "walkdir", ] [[package]] @@ -1980,9 +1985,20 @@ dependencies = [ [[package]] name = "prometeu-language-api" version = "0.1.0" + +[[package]] +name = "prometeu-language-pbs" +version = "0.1.0" dependencies = [ - "serde", - "thiserror", + "prometeu-language-api", +] + +[[package]] +name = "prometeu-languages-registry" +version = "0.1.0" +dependencies = [ + "prometeu-language-api", + "prometeu-language-pbs", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ea0f5f84..dba07052 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ [workspace] members = [ + "crates/compiler/languages/prometeu-languages-registry", + "crates/compiler/languages/prometeu-language-pbs", + "crates/compiler/prometeu-build-pipeline", "crates/compiler/prometeu-bytecode", "crates/compiler/prometeu-core", diff --git a/crates/compiler/languages/.gitkeep b/crates/compiler/languages/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/compiler/languages/prometeu-language-pbs/Cargo.toml b/crates/compiler/languages/prometeu-language-pbs/Cargo.toml new file mode 100644 index 00000000..d09a2da5 --- /dev/null +++ b/crates/compiler/languages/prometeu-language-pbs/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "prometeu-language-pbs" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "" + +[dependencies] +prometeu-language-api = { path = "../../prometeu-language-api" } \ No newline at end of file diff --git a/crates/compiler/languages/prometeu-language-pbs/src/language_spec.rs b/crates/compiler/languages/prometeu-language-pbs/src/language_spec.rs new file mode 100644 index 00000000..82b20ca0 --- /dev/null +++ b/crates/compiler/languages/prometeu-language-pbs/src/language_spec.rs @@ -0,0 +1,16 @@ +use std::sync::OnceLock; +use prometeu_language_api::{LanguageSpec, SourcePolicy}; + +pub static LANGUAGE_SPEC: OnceLock = OnceLock::new(); + +fn registry() -> &'static LanguageSpec { + LANGUAGE_SPEC.get_or_init(|| { + LanguageSpec { + id: "pbs", + source_policy: SourcePolicy { + extensions: vec!["pbs"], + case_sensitive: true, + }, + } + }) +} \ No newline at end of file diff --git a/crates/compiler/languages/prometeu-language-pbs/src/lib.rs b/crates/compiler/languages/prometeu-language-pbs/src/lib.rs new file mode 100644 index 00000000..046a7b10 --- /dev/null +++ b/crates/compiler/languages/prometeu-language-pbs/src/lib.rs @@ -0,0 +1,3 @@ +mod language_spec; + +pub use language_spec::LANGUAGE_SPEC; \ No newline at end of file diff --git a/crates/compiler/languages/prometeu-languages-registry/Cargo.toml b/crates/compiler/languages/prometeu-languages-registry/Cargo.toml new file mode 100644 index 00000000..8950c14d --- /dev/null +++ b/crates/compiler/languages/prometeu-languages-registry/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "prometeu-languages-registry" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "" + +[dependencies] +prometeu-language-api = { path = "../../prometeu-language-api" } + +prometeu-language-pbs = { path = "../prometeu-language-pbs" } \ No newline at end of file diff --git a/crates/compiler/languages/prometeu-languages-registry/src/language_spec_registry.rs b/crates/compiler/languages/prometeu-languages-registry/src/language_spec_registry.rs new file mode 100644 index 00000000..98e7569d --- /dev/null +++ b/crates/compiler/languages/prometeu-languages-registry/src/language_spec_registry.rs @@ -0,0 +1,20 @@ +use prometeu_language_api::LanguageSpec; +use std::collections::HashMap; +use std::sync::OnceLock; + +use prometeu_language_pbs::LANGUAGE_SPEC as PBS_LANGUAGE_SPEC; + +static REGISTRY: OnceLock> = OnceLock::new(); + +fn registry() -> &'static HashMap<&'static str, LanguageSpec> { + let pbs = PBS_LANGUAGE_SPEC.get().unwrap(); + REGISTRY.get_or_init(|| { + HashMap::from([ + (pbs.id, pbs.clone()), + ]) + }) +} + +pub fn get_language_spec(id: &str) -> Option<&LanguageSpec> { + registry().get(id) +} diff --git a/crates/compiler/languages/prometeu-languages-registry/src/lib.rs b/crates/compiler/languages/prometeu-languages-registry/src/lib.rs new file mode 100644 index 00000000..f9cca34d --- /dev/null +++ b/crates/compiler/languages/prometeu-languages-registry/src/lib.rs @@ -0,0 +1,3 @@ +mod language_spec_registry; + +pub use language_spec_registry::get_language_spec; \ No newline at end of file diff --git a/crates/compiler/prometeu-build-pipeline/Cargo.toml b/crates/compiler/prometeu-build-pipeline/Cargo.toml index 524b76ef..9b302b06 100644 --- a/crates/compiler/prometeu-build-pipeline/Cargo.toml +++ b/crates/compiler/prometeu-build-pipeline/Cargo.toml @@ -16,6 +16,7 @@ include = ["../../VERSION.txt"] [dependencies] prometeu-deps = { path = "../prometeu-deps" } prometeu-core = { path = "../prometeu-core" } +prometeu-languages-registry = { path = "../languages/prometeu-languages-registry" } clap = { version = "4.5.54", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" diff --git a/crates/compiler/prometeu-build-pipeline/src/cli.rs b/crates/compiler/prometeu-build-pipeline/src/cli.rs index 13031559..a5351ad9 100644 --- a/crates/compiler/prometeu-build-pipeline/src/cli.rs +++ b/crates/compiler/prometeu-build-pipeline/src/cli.rs @@ -2,7 +2,7 @@ use crate::pipeline::run_phases; use crate::{BuildMode, PipelineConfig, PipelineInput, PipelineOutput}; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use prometeu_deps::{load_sources, resolve_project, DepsConfig}; +use prometeu_deps::{load_sources, resolve_workspace, DepsConfig}; use std::path::{Path, PathBuf}; /// Command line interface for the Prometeu Compiler. @@ -128,7 +128,7 @@ fn run_pipeline(cfg: PipelineConfig, project_dir: &Path, explain_deps: bool) -> registry_dirs: vec![], }; - let resolved = resolve_project(&deps_cfg, project_dir) + let resolved = resolve_workspace(&deps_cfg, project_dir) .with_context(|| format!("deps: failed to resolve project at {:?}", project_dir))?; // resolved deve te dar pelo menos: diff --git a/crates/compiler/prometeu-build-pipeline/src/ctx.rs b/crates/compiler/prometeu-build-pipeline/src/ctx.rs index cf35fafe..7e4097ec 100644 --- a/crates/compiler/prometeu-build-pipeline/src/ctx.rs +++ b/crates/compiler/prometeu-build-pipeline/src/ctx.rs @@ -30,15 +30,11 @@ impl ProjectCtx { } } -/// Pipeline context (in-memory state). -/// Arena-friendly: uses Vec + IDs as the main storage. #[derive(Debug)] pub struct PipelineCtx { pub source_db: FileDB, pub interner: NameInterner, pub diagnostics: Vec, - - /// Projects in stack order (deps first). pub projects: Vec, } @@ -60,8 +56,8 @@ impl PipelineCtx { pub fn init_projects_from_stack(&mut self, stack: &BuildStack) { self.projects.clear(); self.projects.reserve(stack.projects.len()); - for p in &stack.projects { - self.projects.push(ProjectCtx::new(p.project_id)); + for project_id in &stack.projects { + self.projects.push(ProjectCtx::new(project_id.clone())); } } diff --git a/crates/compiler/prometeu-build-pipeline/src/lib.rs b/crates/compiler/prometeu-build-pipeline/src/lib.rs index 3f4a1b4d..cbe3a7eb 100644 --- a/crates/compiler/prometeu-build-pipeline/src/lib.rs +++ b/crates/compiler/prometeu-build-pipeline/src/lib.rs @@ -3,8 +3,8 @@ pub mod config; pub mod ctx; pub mod pipeline; pub mod phases; -pub use config::*; +pub use config::*; pub use ctx::*; pub use pipeline::*; pub use cli::run; diff --git a/crates/compiler/prometeu-build-pipeline/src/phases/frontend.rs b/crates/compiler/prometeu-build-pipeline/src/phases/language.rs similarity index 100% rename from crates/compiler/prometeu-build-pipeline/src/phases/frontend.rs rename to crates/compiler/prometeu-build-pipeline/src/phases/language.rs diff --git a/crates/compiler/prometeu-build-pipeline/src/phases/load_source.rs b/crates/compiler/prometeu-build-pipeline/src/phases/load_source.rs index b9990882..785abc07 100644 --- a/crates/compiler/prometeu-build-pipeline/src/phases/load_source.rs +++ b/crates/compiler/prometeu-build-pipeline/src/phases/load_source.rs @@ -13,14 +13,15 @@ pub fn run(_cfg: &PipelineConfig, input: &PipelineInput, ctx: &mut PipelineCtx) let is_empty = ctx.projects[i].files.is_empty(); if is_empty { - let proj = &input.stack.projects[i]; + let project_id = &input.stack.projects[i]; + let project_name = input.graph.project(project_id).unwrap().name.clone(); ctx.push_diagnostic(Diagnostic { severity: Severity::Warning, code: "PIPELINE_NO_SOURCES".into(), message: format!( "Project '{}' has no source files loaded.", - proj.name + project_name ), span: Span::none(), related: vec![], diff --git a/crates/compiler/prometeu-build-pipeline/src/phases/backend.rs b/crates/compiler/prometeu-build-pipeline/src/phases/lowering.rs similarity index 100% rename from crates/compiler/prometeu-build-pipeline/src/phases/backend.rs rename to crates/compiler/prometeu-build-pipeline/src/phases/lowering.rs diff --git a/crates/compiler/prometeu-build-pipeline/src/phases/mod.rs b/crates/compiler/prometeu-build-pipeline/src/phases/mod.rs index 61cb4d65..25bb69e6 100644 --- a/crates/compiler/prometeu-build-pipeline/src/phases/mod.rs +++ b/crates/compiler/prometeu-build-pipeline/src/phases/mod.rs @@ -1,5 +1,5 @@ pub mod boot; pub mod load_source; -pub mod frontend; -pub mod backend; +pub mod language; +pub mod lowering; pub mod emit; diff --git a/crates/compiler/prometeu-build-pipeline/src/pipeline.rs b/crates/compiler/prometeu-build-pipeline/src/pipeline.rs index 79c41b12..b8c5d73a 100644 --- a/crates/compiler/prometeu-build-pipeline/src/pipeline.rs +++ b/crates/compiler/prometeu-build-pipeline/src/pipeline.rs @@ -37,10 +37,10 @@ pub(crate) fn run_phases(cfg: PipelineConfig, input: PipelineInput) -> PipelineO phases::load_source::run(&cfg, &input, &mut ctx); // Frontend phase (stub / optional). - phases::frontend::run(&cfg, &input, &mut ctx); + phases::language::run(&cfg, &input, &mut ctx); // Backend phase (stub). - phases::backend::run(&cfg, &input, &mut ctx); + phases::lowering::run(&cfg, &input, &mut ctx); // Emit phase (stub). let artifacts = phases::emit::run(&cfg, &input, &mut ctx); diff --git a/crates/compiler/prometeu-deps/Cargo.toml b/crates/compiler/prometeu-deps/Cargo.toml index d76fcd17..9b923bd9 100644 --- a/crates/compiler/prometeu-deps/Cargo.toml +++ b/crates/compiler/prometeu-deps/Cargo.toml @@ -8,8 +8,12 @@ description = "" [dependencies] serde = { version = "1.0.228", features = ["derive"] } prometeu-core = { path = "../prometeu-core" } +prometeu-language-api = { path = "../prometeu-language-api" } +prometeu-languages-registry = { path = "../languages/prometeu-languages-registry" } anyhow = "1.0.101" camino = "1.2.2" +walkdir = "2.5.0" +serde_json = "1.0.149" [features] default = [] diff --git a/crates/compiler/prometeu-deps/src/lib.rs b/crates/compiler/prometeu-deps/src/lib.rs index 21831a41..970efa2e 100644 --- a/crates/compiler/prometeu-deps/src/lib.rs +++ b/crates/compiler/prometeu-deps/src/lib.rs @@ -1,11 +1,12 @@ mod model; -mod resolve_project; mod load_sources; +mod workspace; -pub use resolve_project::resolve_project; +pub use workspace::resolve_workspace; pub use load_sources::load_sources; -pub use model::resolved_project::ResolvedProject; +pub use model::manifest::*; +pub use model::resolved_project::ResolvedWorkspace; pub use model::resolved_explanation::ResolveExplanation; pub use model::deps_config::DepsConfig; pub use model::project_descriptor::ProjectDescriptor; diff --git a/crates/compiler/prometeu-deps/src/load_sources.rs b/crates/compiler/prometeu-deps/src/load_sources.rs index 7f7de59e..0a8c64b5 100644 --- a/crates/compiler/prometeu-deps/src/load_sources.rs +++ b/crates/compiler/prometeu-deps/src/load_sources.rs @@ -1,5 +1,97 @@ -use crate::{DepsConfig, LoadedSources, ResolvedProject}; +use anyhow::{Context, Result}; +use camino::Utf8PathBuf; +use walkdir::WalkDir; -pub fn load_sources(p0: &DepsConfig, p1: &ResolvedProject) -> anyhow::Result { - todo!() -} \ No newline at end of file +use crate::{ + DepsConfig, + LoadedFile, + LoadedSources, + ProjectSources, + ResolvedWorkspace, +}; + +pub fn load_sources(cfg: &DepsConfig, resolved: &ResolvedWorkspace) -> Result { + let mut per_project = Vec::with_capacity(resolved.stack.projects.len()); + + for project_id in &resolved.stack.projects { + let project = resolved + .graph + .project(project_id) + .with_context(|| format!("deps: unknown project_id {:?} in build stack", project_id))?; + + if cfg.explain { + eprintln!( + "[deps] load_sources: project {}@{} ({:?})", + project.name, project.version, project.project_dir + ); + } + + let mut files: Vec = Vec::new(); + + for root in &project.source_roots { + let abs_root = project.project_dir.join(root); + + if cfg.explain { + eprintln!("[deps] scanning {:?}", abs_root); + } + + if !abs_root.exists() { + anyhow::bail!( + "deps: source root does not exist for project {}@{}: {:?}", + project.name, + project.version, + abs_root + ); + } + + // Walk recursively. + for entry in WalkDir::new(&abs_root) + .follow_links(false) + .into_iter() + .filter_map(|e| e.ok()) + { + let ft = entry.file_type(); + if !ft.is_file() { + continue; + } + + let path = entry.path(); + + + // TODO: precisamos mexer no prometeu.json para configurar o frontend do projeto + // Filter extensions: start with PBS only. + if path.extension().and_then(|s| s.to_str()) != Some("pbs") { + continue; + } + + // Convert to Utf8Path (the best effort) and use a stable "uri". + let path_utf8: Utf8PathBuf = match Utf8PathBuf::from_path_buf(path.to_path_buf()) { + Ok(p) => p, + Err(_) => { + anyhow::bail!("deps: non-utf8 path found while scanning sources: {:?}", path); + } + }; + + let text = std::fs::read_to_string(&path_utf8) + .with_context(|| format!("deps: failed to read source file {:?}", path_utf8))?; + + // TODO: normalize newlines + + files.push(LoadedFile { + uri: path_utf8.to_string(), + text, + }); + } + } + + // Determinism: sort a file list by uri (important for stable builds). + files.sort_by(|a, b| a.uri.cmp(&b.uri)); + + per_project.push(ProjectSources { + project_id: project_id.clone(), + files, + }); + } + + Ok(LoadedSources { per_project }) +} diff --git a/crates/compiler/prometeu-deps/src/model/build_stack.rs b/crates/compiler/prometeu-deps/src/model/build_stack.rs index 0fc0dfec..e3e85727 100644 --- a/crates/compiler/prometeu-deps/src/model/build_stack.rs +++ b/crates/compiler/prometeu-deps/src/model/build_stack.rs @@ -1,7 +1,6 @@ -use crate::model::project_descriptor::ProjectDescriptor; +use prometeu_core::ProjectId; #[derive(Debug, Clone)] pub struct BuildStack { - /// deps-first order - pub projects: Vec, + pub projects: Vec, } diff --git a/crates/compiler/prometeu-deps/src/model/deps_config.rs b/crates/compiler/prometeu-deps/src/model/deps_config.rs index 3f31d5d6..e20e137e 100644 --- a/crates/compiler/prometeu-deps/src/model/deps_config.rs +++ b/crates/compiler/prometeu-deps/src/model/deps_config.rs @@ -2,7 +2,6 @@ use camino::Utf8PathBuf; pub struct DepsConfig { pub explain: bool, - // diretórios e política (só deps entende isso) pub cache_dir: Utf8PathBuf, - pub registry_dirs: Vec, // ou sources + pub registry_dirs: Vec, // or sources ? } \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/model/manifest.rs b/crates/compiler/prometeu-deps/src/model/manifest.rs new file mode 100644 index 00000000..2404d945 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/model/manifest.rs @@ -0,0 +1,75 @@ +use camino::Utf8PathBuf; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Manifest { + pub name: String, + pub version: String, + + #[serde(default)] + pub source_roots: Vec, + + pub language: LanguageDecl, + + #[serde(default)] + pub deps: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LanguageDecl { + pub id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DepDecl { + Local { + path: String, + }, + Git { + git: String, + rev: Option, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PrometeuLock { + pub schema: u32, + #[serde(default)] + pub mappings: Vec, +} + +impl PrometeuLock { + pub fn blank() -> Self { + Self { + schema: 0, + mappings: vec![], + } + } + + pub fn lookup_git_local_dir(&self, url: &str, rev: &str) -> Option<&String> { + self.mappings.iter().find_map(|m| match m { + LockMapping::Git { + git, rev: r, local_dir + } if git == url && r == rev => Some(local_dir), + _ => None, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "lowercase")] +pub enum LockMapping { + Git { + git: String, + rev: String, + local_dir: String, + }, + Registry { + registry: String, + version: String, + local_dir: String, + }, +} + + diff --git a/crates/compiler/prometeu-deps/src/model/mod.rs b/crates/compiler/prometeu-deps/src/model/mod.rs index 2db0b5ab..6bdf6d56 100644 --- a/crates/compiler/prometeu-deps/src/model/mod.rs +++ b/crates/compiler/prometeu-deps/src/model/mod.rs @@ -8,4 +8,5 @@ pub mod loaded_file; pub mod cache_blobs; pub mod resolved_project; pub mod resolved_explanation; -pub(crate) mod cache_plan; \ No newline at end of file +pub mod cache_plan; +pub mod manifest; \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/model/project_descriptor.rs b/crates/compiler/prometeu-deps/src/model/project_descriptor.rs index ce36e6fa..a1436312 100644 --- a/crates/compiler/prometeu-deps/src/model/project_descriptor.rs +++ b/crates/compiler/prometeu-deps/src/model/project_descriptor.rs @@ -1,8 +1,14 @@ +use camino::Utf8PathBuf; use prometeu_core::ProjectId; +use prometeu_language_api::SourcePolicy; #[derive(Debug, Clone)] pub struct ProjectDescriptor { pub project_id: ProjectId, pub name: String, pub version: String, + pub project_dir: Utf8PathBuf, + pub source_roots: Vec, + pub language_id: String, + pub source_policy: SourcePolicy, } diff --git a/crates/compiler/prometeu-deps/src/model/resolved_graph.rs b/crates/compiler/prometeu-deps/src/model/resolved_graph.rs index 468b24e2..357b088e 100644 --- a/crates/compiler/prometeu-deps/src/model/resolved_graph.rs +++ b/crates/compiler/prometeu-deps/src/model/resolved_graph.rs @@ -8,3 +8,9 @@ pub struct ResolvedGraph { // opcional: adjacency list para checks pub edges: Vec>, // edges[from] = vec[to] } + +impl ResolvedGraph { + pub fn project(&self, id: &ProjectId) -> Option<&ProjectDescriptor> { + self.projects.get(id.0 as usize) + } +} diff --git a/crates/compiler/prometeu-deps/src/model/resolved_project.rs b/crates/compiler/prometeu-deps/src/model/resolved_project.rs index 40df30aa..9c43016d 100644 --- a/crates/compiler/prometeu-deps/src/model/resolved_project.rs +++ b/crates/compiler/prometeu-deps/src/model/resolved_project.rs @@ -1,7 +1,7 @@ use crate::{BuildStack, ResolvedGraph, ResolveExplanation, CachePlan}; #[derive(Debug, Clone)] -pub struct ResolvedProject { +pub struct ResolvedWorkspace { pub graph: ResolvedGraph, pub stack: BuildStack, pub explain: Option diff --git a/crates/compiler/prometeu-deps/src/resolve_project.rs b/crates/compiler/prometeu-deps/src/resolve_project.rs deleted file mode 100644 index 42784a7e..00000000 --- a/crates/compiler/prometeu-deps/src/resolve_project.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::{DepsConfig, ResolvedProject}; -use camino::Utf8Path; -use std::path::Path; - -pub fn resolve_project(cfg: &DepsConfig, path: &Path) -> anyhow::Result { - let project_dir = Utf8Path::from_path(path).unwrap(); - todo!() -} \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/host.rs b/crates/compiler/prometeu-deps/src/workspace/host.rs new file mode 100644 index 00000000..19335c27 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/host.rs @@ -0,0 +1,32 @@ +use anyhow::{Context, Result}; +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::workspace::model::DepRef; + +pub trait DepsHost { + fn read_to_string(&self, path: &Utf8Path) -> Result; + + // fn ensure_project_local(&self, from_dir: &Utf8Path, dep: &DepRef) -> Result; +} + +pub struct FsHost; + +impl DepsHost for FsHost { + fn read_to_string(&self, path: &Utf8Path) -> Result { + std::fs::read_to_string(path) + .with_context(|| format!("failed to read {:?}", path)) + } + + // fn ensure_project_local(&self, from_dir: &Utf8Path, dep: &DepRef) -> Result { + // match dep { + // DepRef::Local { path } => { + // let joined = from_dir.join(path); + // let canon = joined.canonicalize() + // .with_context(|| format!("deps: dep path does not exist: {:?}", joined))?; + // Utf8PathBuf::from_path_buf(canon) + // .map_err(|p| anyhow::anyhow!("deps: non-utf8 dep dir: {:?}", p)) + // } + // _ => unimplemented!(), + // } + // } +} diff --git a/crates/compiler/prometeu-deps/src/workspace/mod.rs b/crates/compiler/prometeu-deps/src/workspace/mod.rs new file mode 100644 index 00000000..944786d8 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/mod.rs @@ -0,0 +1,6 @@ +mod resolve_workspace; +mod host; +mod model; +mod phases; + +pub use resolve_workspace::resolve_workspace; \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/model.rs b/crates/compiler/prometeu-deps/src/workspace/model.rs new file mode 100644 index 00000000..ddd6e65e --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/model.rs @@ -0,0 +1,31 @@ +use camino::Utf8PathBuf; +use prometeu_core::ProjectId; +use prometeu_language_api::SourcePolicy; + +use crate::Manifest; + +#[derive(Debug, Clone)] +pub struct RawProjectNode { + pub dir: Utf8PathBuf, + pub manifest_path: Utf8PathBuf, + pub manifest: Manifest, +} + +#[derive(Debug, Clone)] +pub enum DepRef { + Local { + path: Utf8PathBuf + }, +} + +#[derive(Debug, Clone)] +pub struct ProjectNode { + pub id: ProjectId, + pub dir: Utf8PathBuf, + pub name: String, + pub version: String, + pub source_roots: Vec, + pub language_id: String, + pub deps: Vec, + pub source_policy: SourcePolicy, +} diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/discover.rs b/crates/compiler/prometeu-deps/src/workspace/phases/discover.rs new file mode 100644 index 00000000..51184a08 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/discover.rs @@ -0,0 +1,131 @@ +use crate::model::manifest::DepDecl; +use crate::workspace::host::DepsHost; +use crate::workspace::model::RawProjectNode; +use crate::workspace::phases::state::ResolverState; +use crate::Manifest; +use anyhow::{anyhow, bail, Context, Result}; +use camino::Utf8PathBuf; +use serde_json; +use std::fs::canonicalize; + +/// Phase 1: Discover all projects in the workspace. +/// +/// - Reads `prometeu.json` from each pending project directory. +/// - Parses `Manifest`. +/// - Registers the raw node. +/// - Enqueues local-path deps for discovery (v0). +/// +/// Does NOT: +/// - assign ProjectId +/// - build edges +/// - validate versions +pub fn discover( + cfg: &crate::DepsConfig, + host: &dyn DepsHost, + state: &mut ResolverState, +) -> Result<()> { + while let Some(canon_dir) = state.pending.pop_front() { + // de-dup by directory + if state.raw_by_dir.contains_key(&canon_dir) { + continue; + } + + let manifest_path = canon_dir.join("prometeu.json"); + if !manifest_path.exists() || !manifest_path.is_file() { + bail!( + "deps: manifest not found: expected a file {:?} (project dir {:?})", + manifest_path, + canon_dir + ); + } + + if cfg.explain { + eprintln!("[deps][discover] reading {:?}", manifest_path); + } + + let text = host + .read_to_string(&manifest_path) + .with_context(|| format!("deps: failed to read manifest {:?}", manifest_path))?; + + let manifest: Manifest = serde_json::from_str(&text) + .with_context(|| format!("deps: invalid manifest JSON {:?}", manifest_path))?; + + // Register raw node + let raw_idx = state.raw.len(); + state.raw.push(RawProjectNode { + dir: canon_dir.clone(), + manifest_path: manifest_path.clone(), + manifest: manifest.clone(), + }); + state.raw_by_dir.insert(canon_dir.clone(), raw_idx); + + for dep in &manifest.deps { + match dep { + DepDecl::Local { path } => { + let dep_dir = canon_dir.join(path); + + let dep_dir_std = dep_dir.canonicalize().with_context(|| { + format!( + "deps: dep path does not exist: {:?} (from {:?})", + dep_dir, canon_dir + ) + })?; + + let dep_dir_canon = Utf8PathBuf::from_path_buf(dep_dir_std) + .map_err(|p| anyhow!("deps: non-utf8 dep dir: {:?}", p))?; + + if cfg.explain { + eprintln!("[deps][discover] local dep '{}' -> {:?}", path, dep_dir_canon); + } + state.pending.push_back(dep_dir_canon); + } + + DepDecl::Git { git, rev } => { + let Some(rev) = rev.as_deref() else { + bail!( + "deps: git dependency '{}' requires an explicit 'rev' (commit hash) for now", + git + ); + }; + + let Some(local_dir) = state.lock.lookup_git_local_dir(git, rev) else { + bail!( + "deps: git dependency requires prometeu.lock mapping, but entry not found: git='{}' rev='{}'", + git, + rev + ); + }; + + // canonicalize the lock-provided local dir to keep identity stable + let local_dir_std = canonicalize(local_dir) + .with_context(|| format!("deps: prometeu.lock local_dir does not exist: {:?}", local_dir))?; + + let local_dir_canon = Utf8PathBuf::from_path_buf(local_dir_std) + .map_err(|p| anyhow!("deps: non-utf8 lock local_dir: {:?}", p))?; + + // validate manifest exists at the mapped project root + // (this check should not belong here, but it is ok) + let mapped_manifest = local_dir_canon.join("prometeu.json"); + if !mapped_manifest.exists() || !mapped_manifest.is_file() { + bail!( + "deps: prometeu.lock maps git dep to {:?}, but manifest is missing: {:?}", + local_dir_canon, + mapped_manifest + ); + } + + if cfg.explain { + eprintln!( + "[deps][discover] git dep '{}' rev '{}' -> {:?}", + git, rev, local_dir_canon + ); + } + + state.pending.push_back(local_dir_canon); + } + } + } + } + + Ok(()) +} diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/localize.rs b/crates/compiler/prometeu-deps/src/workspace/phases/localize.rs new file mode 100644 index 00000000..aa900bc1 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/localize.rs @@ -0,0 +1,62 @@ +use anyhow::{Context, Result}; + +use prometeu_core::ProjectId; + +use crate::workspace::model::DepRef; +use crate::workspace::phases::state::ResolverState; + +/// Phase 3: Localize dependencies and build graph edges. +/// +/// For each project node: +/// - For each DepRef: +/// - host.ensure_project_local(from_dir, dep) -> dep_dir (local on disk) +/// - map dep_dir to ProjectId via st.by_dir +/// - st.edges[from].push(dep_id) +/// +/// v0 policy: +/// - Only DepRef::LocalPath is supported. +/// - Git/Registry cause a hard error (future extension point). +pub fn localize(cfg: &crate::DepsConfig, state: &mut ResolverState) -> Result<()> { + // Reset edges (allows re-run / deterministic behavior) + for e in &mut state.edges { + e.clear(); + } + + for from_idx in 0..state.nodes.len() { + let from_id: ProjectId = state.nodes[from_idx].id; + let from_dir = state.nodes[from_idx].dir.clone(); + + if cfg.explain { + eprintln!( + "[deps][localize] from id={:?} dir={:?}", + from_id, from_dir + ); + } + + // Clone deps to avoid borrow conflicts (simple + safe for now) + let deps = state.nodes[from_idx].deps.clone(); + + for dep in deps { + match &dep { + DepRef::Local { + path + } => { + let dep_id = state.by_dir.get(path).copied().with_context(|| { + format!( + "deps: localized dep dir {:?} was not discovered; \ + ensure the dep has a prometeu.json and is reachable via local paths", + path + ) + })?; + state.edges[from_id.0 as usize].push(dep_id); + } + } + } + + // Optional: keep edges deterministic + state.edges[from_id.0 as usize].sort_by_key(|id| id.0); + state.edges[from_id.0 as usize].dedup(); + } + + Ok(()) +} diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/materialize.rs b/crates/compiler/prometeu-deps/src/workspace/phases/materialize.rs new file mode 100644 index 00000000..7aca4ddd --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/materialize.rs @@ -0,0 +1,144 @@ +use crate::model::manifest::DepDecl; +use crate::workspace::model::{DepRef, ProjectNode}; +use crate::workspace::phases::state::ResolverState; +use anyhow::{anyhow, bail, Context, Result}; +use camino::Utf8PathBuf; +use prometeu_core::ProjectId; +use prometeu_languages_registry::get_language_spec; +use std::fs::canonicalize; + +/// Phase 2: Materialize projects (allocate ProjectId / arena nodes). +/// +/// Inputs: +/// - st.raw (RawProjectNode: dir + manifest) +/// +/// Outputs: +/// - st.nodes (ProjectNode arena) +/// - st.by_dir (dir -> ProjectId) +/// - st.edges (allocated adjacency lists, empty for now) +/// - st.root (ProjectId for root_dir) +/// +/// Does NOT: +/// - resolve deps to local dirs (that's phase localize) +/// - validate version conflicts/cycles +/// - resolve language/source policy +pub fn materialize(cfg: &crate::DepsConfig, state: &mut ResolverState) -> Result<()> { + // Reset materialized state (allows rerun in future refactors/tests) + state.nodes.clear(); + state.by_dir.clear(); + state.edges.clear(); + state.root = None; + + state.nodes.reserve(state.raw.len()); + state.edges.reserve(state.raw.len()); + + for (idx, raw) in state.raw.iter().enumerate() { + let id = ProjectId(idx as u32); + + // Default source roots if omitted + let source_roots: Vec = raw + .manifest + .source_roots + .iter() + .map(|root| Utf8PathBuf::from(root)) + .collect(); + if source_roots.is_empty() { + bail!( + "deps: no source roots specified for project {}", + raw.manifest.name + ) + } + + // Convert DepDecl -> DepRef (no localization yet) + let mut deps: Vec = Vec::with_capacity(raw.manifest.deps.len()); + for d in &raw.manifest.deps { + match d { + DepDecl::Local { path } => { + let joined = raw.dir.join(path); + let dir_std = joined.canonicalize() + .with_context(|| format!("deps: local dep path does not exist: {:?} (from {:?})", joined, raw.dir))?; + + let dir_canon = Utf8PathBuf::from_path_buf(dir_std) + .map_err(|p| anyhow!("deps: non-utf8 dep dir: {:?}", p))?; + deps.push(DepRef::Local { + path: dir_canon + }); + } + DepDecl::Git { git, rev } => { + let Some(rev) = rev.as_deref() else { + bail!( + "deps: git dependency '{}' requires an explicit 'rev' (commit hash) for now", + git + ); + }; + + let Some(local_dir) = state.lock.lookup_git_local_dir(git, rev) else { + bail!( + "deps: git dependency requires prometeu.lock mapping, but entry not found: git='{}' rev='{}'", + git, + rev + ); + }; + + // canonicalize the lock-provided local dir to keep identity stable + let path = canonicalize(local_dir).with_context(|| { + format!( + "deps: prometeu.lock local_dir does not exist: {:?}", + local_dir + ) + })?; + + let local_dir_canon = Utf8PathBuf::from_path_buf(path) + .map_err(|p| anyhow!("deps: non-utf8 lock local_dir: {:?}", p))?; + + deps.push(DepRef::Local { + path: local_dir_canon, + }); + } + } + } + + if cfg.explain { + eprintln!( + "[deps][materialize] id={:?} {}@{} dir={:?} language={}", + id, raw.manifest.name, raw.manifest.version, raw.dir, raw.manifest.language.id + ); + } + + let source_policy = get_language_spec(raw.manifest.language.id.as_str()) + .map(|spec| spec.source_policy.clone()) + .ok_or(anyhow!( + "deps: unknown language spec: {}", + raw.manifest.language.id + ))?; + + // Record node + state.nodes.push(ProjectNode { + id, + dir: raw.dir.clone(), + name: raw.manifest.name.clone(), + version: raw.manifest.version.clone(), + source_roots, + language_id: raw.manifest.language.id.clone(), + deps, + source_policy, + }); + + state.by_dir.insert(raw.dir.clone(), id); + state.edges.push(Vec::new()); + } + + // Determine root id + if let Some(root_id) = state.by_dir.get(&state.root_dir).copied() { + state.root = Some(root_id); + } else { + // This should never happen if seed/discover worked. + // Keep it as a hard failure (in a later validate phase you can convert to a nicer diagnostic). + anyhow::bail!( + "deps: root project dir {:?} was not discovered/materialized", + state.root_dir + ); + } + + Ok(()) +} diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/mod.rs b/crates/compiler/prometeu-deps/src/workspace/phases/mod.rs new file mode 100644 index 00000000..25fbb3e4 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/mod.rs @@ -0,0 +1,10 @@ +mod run_all; +mod state; +mod discover; +mod materialize; +mod localize; +mod validate; +mod policy; +mod stack; + +pub use run_all::run_all; \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/policy.rs b/crates/compiler/prometeu-deps/src/workspace/phases/policy.rs new file mode 100644 index 00000000..d73419f6 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/policy.rs @@ -0,0 +1,6 @@ +use crate::DepsConfig; +use crate::workspace::phases::state::ResolverState; + +pub(crate) fn policy(p0: &DepsConfig, p1: &mut ResolverState) -> _ { + todo!() +} \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/run_all.rs b/crates/compiler/prometeu-deps/src/workspace/phases/run_all.rs new file mode 100644 index 00000000..ae2e3781 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/run_all.rs @@ -0,0 +1,14 @@ +use camino::Utf8Path; +use crate::{DepsConfig, ResolvedWorkspace}; +use crate::workspace::host::FsHost; +use crate::workspace::phases::{discover, localize, materialize, policy, stack, state, validate}; + +pub fn run_all(cfg: &DepsConfig, fs_host: &FsHost, root_dir: &Utf8Path) -> Result { + let mut state = state::seed(cfg, root_dir)?; + discover::discover(cfg, fs_host, &mut state)?; + materialize::materialize(cfg, &mut state)?; + localize::localize(cfg, &mut state)?; + validate::validate(cfg, &mut state)?; + policy::policy(cfg, &mut state)?; + stack::stack(cfg, &mut state) +} \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/stack.rs b/crates/compiler/prometeu-deps/src/workspace/phases/stack.rs new file mode 100644 index 00000000..6436577f --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/stack.rs @@ -0,0 +1,6 @@ +use crate::{DepsConfig, ResolvedWorkspace}; +use crate::workspace::phases::state::ResolverState; + +pub(crate) fn stack(cfg: &DepsConfig, state: &mut ResolverState) -> Result { + todo!() +} \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/state.rs b/crates/compiler/prometeu-deps/src/workspace/phases/state.rs new file mode 100644 index 00000000..5eef4842 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/state.rs @@ -0,0 +1,58 @@ +use camino::{Utf8Path, Utf8PathBuf}; +use std::collections::{HashMap, VecDeque}; +use anyhow::Context; +use crate::workspace::model::{RawProjectNode, ProjectNode}; +use prometeu_core::ProjectId; +use crate::PrometeuLock; +use serde_json; + +pub struct ResolverState { + pub root_dir: Utf8PathBuf, + + // phase1 output + pub raw: Vec, + pub raw_by_dir: HashMap, + pub pending: VecDeque, + + // phase2+ + pub nodes: Vec, + pub by_dir: HashMap, + pub edges: Vec>, + + pub root: Option, + + pub lock: PrometeuLock, +} + +pub fn seed(_cfg: &crate::DepsConfig, root_dir: &Utf8Path) -> anyhow::Result { + let path_buf = root_dir.canonicalize()?; + let root_dir_canon = Utf8PathBuf::from_path_buf(path_buf) + .map_err(|p| anyhow::anyhow!("deps: non-utf8 root dir: {:?}", p))?; + + let lock_path = root_dir_canon.join("prometeu.lock"); + let lock = if lock_path.exists() { + let txt = std::fs::read_to_string(&lock_path)?; + serde_json::from_str::(&txt) + .with_context(|| format!("invalid prometeu.lock at {:?}", lock_path))? + } else { + PrometeuLock::blank() + }; + + let mut pending = VecDeque::new(); + pending.push_back(root_dir_canon.clone()); + + Ok(ResolverState { + root_dir: root_dir_canon.clone(), + raw: vec![], + raw_by_dir: HashMap::new(), + pending, + + nodes: vec![], + by_dir: HashMap::new(), + edges: vec![], + + root: None, + + lock, + }) +} diff --git a/crates/compiler/prometeu-deps/src/workspace/phases/validate.rs b/crates/compiler/prometeu-deps/src/workspace/phases/validate.rs new file mode 100644 index 00000000..e90da002 --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/phases/validate.rs @@ -0,0 +1,6 @@ +use crate::DepsConfig; +use crate::workspace::phases::state::ResolverState; + +pub(crate) fn validate(p0: &DepsConfig, p1: &mut ResolverState) -> _ { + todo!() +} \ No newline at end of file diff --git a/crates/compiler/prometeu-deps/src/workspace/resolve_workspace.rs b/crates/compiler/prometeu-deps/src/workspace/resolve_workspace.rs new file mode 100644 index 00000000..8e595efb --- /dev/null +++ b/crates/compiler/prometeu-deps/src/workspace/resolve_workspace.rs @@ -0,0 +1,10 @@ +use anyhow::Result; +use camino::Utf8Path; + +use crate::{DepsConfig, ResolvedWorkspace}; +use crate::workspace::host::FsHost; + +pub fn resolve_workspace(cfg: &DepsConfig, root_dir: &Utf8Path) -> Result { + let host = FsHost; + crate::workspace::phases::run_all(cfg, &host, root_dir) +} diff --git a/crates/compiler/prometeu-language-api/Cargo.toml b/crates/compiler/prometeu-language-api/Cargo.toml index 64020d67..880c247e 100644 --- a/crates/compiler/prometeu-language-api/Cargo.toml +++ b/crates/compiler/prometeu-language-api/Cargo.toml @@ -7,9 +7,4 @@ description = "Canonical language contract for Prometeu Backend: identifiers, re repository = "https://github.com/prometeu/runtime" [dependencies] -serde = { version = "1", features = ["derive"], optional = true } -thiserror = "1" -[features] -default = [] -serde = ["dep:serde"] diff --git a/crates/compiler/prometeu-language-api/src/language_spec.rs b/crates/compiler/prometeu-language-api/src/language_spec.rs new file mode 100644 index 00000000..ed1ff031 --- /dev/null +++ b/crates/compiler/prometeu-language-api/src/language_spec.rs @@ -0,0 +1,21 @@ +#[derive(Debug, Clone)] +pub struct SourcePolicy { + pub extensions: Vec<&'static str>, + pub case_sensitive: bool, +} + +impl SourcePolicy { + pub fn matches_ext(&self, ext: &str) -> bool { + if self.case_sensitive { + self.extensions.iter().any(|e| *e == ext) + } else { + self.extensions.iter().any(|e| e.eq_ignore_ascii_case(ext)) + } + } +} + +#[derive(Debug, Clone)] +pub struct LanguageSpec { + pub id: &'static str, + pub source_policy: SourcePolicy, +} \ No newline at end of file diff --git a/crates/compiler/prometeu-language-api/src/lib.rs b/crates/compiler/prometeu-language-api/src/lib.rs index 8b137891..522059e9 100644 --- a/crates/compiler/prometeu-language-api/src/lib.rs +++ b/crates/compiler/prometeu-language-api/src/lib.rs @@ -1 +1,3 @@ +mod language_spec; +pub use language_spec::*;