diff --git a/Cargo.lock b/Cargo.lock index 70d4573a..569668e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1920,6 +1920,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "frontend-api", "pathdiff", "prometeu-abi", "prometeu-analysis", diff --git a/crates/frontend-api/src/types.rs b/crates/frontend-api/src/types.rs index 4599afc2..cd4ac86d 100644 --- a/crates/frontend-api/src/types.rs +++ b/crates/frontend-api/src/types.rs @@ -178,6 +178,17 @@ impl CanonicalFnKey { pub fn new(import: ImportRef, arity: u16) -> Self { Self { import, arity } } } +/// Opaque canonical reference to a type known to the Frontend. +/// Backend must not rely on its internal representation. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct TypeRef(pub u32); + +/// Opaque canonical reference to a function signature (overload identity). +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub struct SignatureRef(pub u32); + /// Diagnostic severity. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/prometeu-compiler/Cargo.toml b/crates/prometeu-compiler/Cargo.toml index 155ebb5e..25f3d6d0 100644 --- a/crates/prometeu-compiler/Cargo.toml +++ b/crates/prometeu-compiler/Cargo.toml @@ -17,6 +17,7 @@ include = ["../../VERSION.txt"] prometeu-bytecode = { path = "../prometeu-bytecode" } prometeu-abi = { path = "../prometeu-abi" } prometeu-analysis = { path = "../prometeu-analysis" } +frontend-api = { path = "../frontend-api", features = ["serde"] } clap = { version = "4.5.54", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" diff --git a/crates/prometeu-compiler/src/building/mod.rs b/crates/prometeu-compiler/src/building/mod.rs index 857fb2b3..4c013cec 100644 --- a/crates/prometeu-compiler/src/building/mod.rs +++ b/crates/prometeu-compiler/src/building/mod.rs @@ -2,3 +2,12 @@ pub mod plan; pub mod output; pub mod linker; pub mod orchestrator; + +// Compile-time boundary guard: Backend modules must not import PBS directly. +// This doctest will fail to compile if someone tries to `use crate::frontends::pbs` from here. +// It is lightweight and runs with `cargo test`. +/// ```compile_fail +/// use crate::frontends::pbs; // Backend must not depend on PBS directly +/// # let _ = &pbs; // ensure the import is actually used so the check is meaningful +/// ``` +mod __backend_boundary_guard {} diff --git a/crates/prometeu-compiler/src/building/orchestrator.rs b/crates/prometeu-compiler/src/building/orchestrator.rs index 0b22f2cc..187dadf0 100644 --- a/crates/prometeu-compiler/src/building/orchestrator.rs +++ b/crates/prometeu-compiler/src/building/orchestrator.rs @@ -1,5 +1,6 @@ use crate::building::linker::{LinkError, Linker}; use crate::building::output::{compile_project, CompileError}; +use frontend_api::traits::Frontend as CanonFrontend; use crate::building::plan::{BuildPlan, BuildTarget}; use crate::common::diagnostics::DiagnosticBundle; use crate::common::files::FileManager; @@ -43,14 +44,14 @@ impl From for BuildError { } } -pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result { +pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget, fe: &dyn CanonFrontend) -> Result { let plan = BuildPlan::from_graph(graph, target); let mut compiled_modules = HashMap::new(); let mut modules_in_order = Vec::new(); let mut file_manager = FileManager::new(); for step in &plan.steps { - let compiled = compile_project(step.clone(), &compiled_modules, &mut file_manager)?; + let compiled = compile_project(step.clone(), &compiled_modules, fe, &mut file_manager)?; compiled_modules.insert(step.project_id.clone(), compiled.clone()); modules_in_order.push(compiled); } @@ -154,6 +155,7 @@ mod tests { use crate::sources::discover; use prometeu_analysis::ids::ProjectId; use std::collections::BTreeMap; + use crate::frontends::pbs::adapter::PbsFrontendAdapter; use std::fs; use std::path::PathBuf; use tempfile::tempdir; @@ -201,7 +203,8 @@ mod tests { make_minimal_manifest(&project_dir); let graph = build_single_node_graph(project_dir); - let res = build_from_graph(&graph, BuildTarget::Main); + let fe = PbsFrontendAdapter; + let res = build_from_graph(&graph, BuildTarget::Main, &fe); assert!(res.is_err()); let err = res.err().unwrap(); match err { @@ -233,7 +236,8 @@ mod tests { fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap(); let graph = build_single_node_graph(project_dir); - let res = build_from_graph(&graph, BuildTarget::Main); + let fe = PbsFrontendAdapter; + let res = build_from_graph(&graph, BuildTarget::Main, &fe); assert!(res.is_err()); let err = res.err().unwrap(); match err { @@ -262,7 +266,8 @@ mod tests { fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap(); let graph = build_single_node_graph(project_dir); - let res = build_from_graph(&graph, BuildTarget::Main).expect("should compile"); + let fe = PbsFrontendAdapter; + let res = build_from_graph(&graph, BuildTarget::Main, &fe).expect("should compile"); // Locate function by name -> function index let di = res.image.debug_info.as_ref().expect("debug info"); diff --git a/crates/prometeu-compiler/src/building/output.rs b/crates/prometeu-compiler/src/building/output.rs index 7d2ba0ba..9ec0032c 100644 --- a/crates/prometeu-compiler/src/building/output.rs +++ b/crates/prometeu-compiler/src/building/output.rs @@ -2,22 +2,25 @@ use crate::backend::emit_fragments; use crate::building::plan::{BuildStep, BuildTarget}; use crate::common::diagnostics::DiagnosticBundle; use crate::common::files::FileManager; -use crate::common::spans::{FileId, Span}; +use crate::common::spans::Span; use crate::deps::resolver::ProjectKey; -use crate::frontends::pbs::ast::ParsedAst; -use crate::frontends::pbs::lowering::Lowerer; -use crate::frontends::pbs::resolver::{ModuleProvider, Resolver}; -use crate::frontends::pbs::symbols::{ModuleSymbols, Namespace, Symbol, SymbolKind, Visibility}; -use crate::frontends::pbs::types::PbsType; -use crate::frontends::pbs::{build_typed_module_symbols, SymbolCollector}; -use crate::lowering::core_to_vm; use crate::semantics::export_surface::ExportSurfaceKind; use prometeu_analysis::ids::ProjectId; -use prometeu_analysis::NameInterner; use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta}; use serde::{Deserialize, Serialize}; +use frontend_api::types::TypeRef; +use frontend_api::traits::Frontend as CanonFrontend; use std::collections::{BTreeMap, HashMap}; -use crate::frontends::pbs::parser::Parser; + +// Simple stable 32-bit FNV-1a hash for synthesizing opaque TypeRef tokens from names. +fn symbol_name_hash(name: &str) -> u32 { + let mut hash: u32 = 0x811C9DC5; // FNV offset basis + for &b in name.as_bytes() { + hash ^= b as u32; + hash = hash.wrapping_mul(0x01000193); // FNV prime + } + hash +} #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ExportKey { @@ -30,7 +33,7 @@ pub struct ExportKey { pub struct ExportMetadata { pub func_idx: Option, pub is_host: bool, - pub ty: Option, + pub ty: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] @@ -105,204 +108,15 @@ impl From for CompileError { } } -struct ProjectModuleProvider { - modules: HashMap, -} - -impl ModuleProvider for ProjectModuleProvider { - fn get_module_symbols(&self, from_path: &str) -> Option<&ModuleSymbols> { - self.modules.get(from_path) - } -} +// Note: PBS ModuleProvider/ModuleSymbols are no longer used at the backend boundary. pub fn compile_project( step: BuildStep, dep_modules: &HashMap, - file_manager: &mut FileManager, + fe: &dyn CanonFrontend, + _file_manager: &mut FileManager, ) -> Result { - let mut interner = NameInterner::new(); - - // 1) Parse all files; collect+merge symbols by module_path - let mut module_symbols_map: HashMap = HashMap::new(); - let mut parsed_files: Vec<(String, ParsedAst)> = Vec::new(); - - for source_rel in &step.sources { - let source_abs = step.project_dir.join(source_rel); - let source_code = std::fs::read_to_string(&source_abs)?; - let file_id = file_manager.add(source_abs.clone(), source_code.clone()); - - let mut parser = Parser::new(&source_code, FileId(file_id as u32), &mut interner); - let parsed = parser.parse_file()?; - - let mut collector = SymbolCollector::new(&interner); - let (ts, vs) = collector.collect(&parsed.arena, parsed.root)?; - - let full_path = source_rel.to_string_lossy().replace('\\', "/"); - let logical_module_path = if let Some(stripped) = full_path.strip_prefix("src/main/modules/") { - stripped - } else if let Some(stripped) = full_path.strip_prefix("src/test/modules/") { - stripped - } else { - &full_path - }; - - let module_path = std::path::Path::new(logical_module_path) - .parent() - .map(|p| p.to_string_lossy().replace('\\', "/")) - .unwrap_or_else(|| "".to_string()); - - let ms = module_symbols_map - .entry(module_path.clone()) - .or_insert_with(ModuleSymbols::new); - - // Merge Type symbols (no overload): reject duplicates - for list in ts.symbols.into_values() { - for sym in list { - if let Some(existing) = ms.type_symbols.get(sym.name) { - return Err(DiagnosticBundle::error( - "E_RESOLVE_DUPLICATE_SYMBOL", - format!( - "Duplicate type symbol '{}' in module '{}'", - interner.resolve(existing.name), - module_path - ), - existing.span.clone(), - ) - .into()); - } - let _ = ms.type_symbols.insert(sym); - } - } - - // Merge Value symbols: allow overloads only for Function kind - for list in vs.symbols.into_values() { - for sym in list { - if let Some(existing) = ms.value_symbols.get(sym.name) { - if !(existing.kind == SymbolKind::Function && sym.kind == SymbolKind::Function) { - return Err(DiagnosticBundle::error( - "E_RESOLVE_DUPLICATE_SYMBOL", - format!( - "Duplicate value symbol '{}' in module '{}'", - interner.resolve(existing.name), - module_path - ), - existing.span.clone(), - ) - .into()); - } - } - let _ = ms.value_symbols.insert(sym); - } - } - - parsed_files.push((module_path, parsed)); - } - - // 2) Synthesize visible ModuleSymbols for dependencies (under synthetic module paths) - let mut all_visible_modules = module_symbols_map.clone(); - - for (alias, project_id) in &step.deps { - let Some(compiled) = dep_modules.get(project_id) else { continue }; - - for (key, meta) in &compiled.exports { - let key_module_path = &key.module_path; - - // Support both import styles: - // - "alias/module" - // - "@alias:module" - let synthetic_paths = [ - format!("{}/{}", alias, key_module_path), - format!("@{}:{}", alias, key_module_path), - ]; - - for synthetic_module_path in synthetic_paths { - let ms = all_visible_modules - .entry(synthetic_module_path.clone()) - .or_insert_with(ModuleSymbols::new); - - let sym = Symbol { - name: interner.intern(&key.symbol_name), - kind: match key.kind { - ExportSurfaceKind::Service => SymbolKind::Service, - ExportSurfaceKind::DeclareType => match &meta.ty { - Some(PbsType::Contract(_)) => SymbolKind::Contract, - Some(PbsType::ErrorType(_)) => SymbolKind::ErrorType, - _ => SymbolKind::Struct, - }, - ExportSurfaceKind::Function => SymbolKind::Function, - }, - namespace: key.kind.namespace(), - visibility: Visibility::Pub, - ty: meta.ty.clone(), - is_host: meta.is_host, - span: Span::new(FileId::INVALID, 0, 0), - origin: Some(synthetic_module_path.clone()), - }; - - if sym.namespace == Namespace::Type { - if let Some(existing) = ms.type_symbols.get(sym.name) { - return Err(CompileError::DuplicateExport { - symbol: interner.resolve(sym.name).to_string(), - first_dep: existing.origin.clone().unwrap_or_else(|| "unknown".to_string()), - second_dep: sym.origin.clone().unwrap_or_else(|| "unknown".to_string()), - }); - } - let _ = ms.type_symbols.insert(sym); - } else { - if let Some(existing) = ms.value_symbols.get(sym.name) { - if !(existing.kind == SymbolKind::Function && sym.kind == SymbolKind::Function) { - return Err(CompileError::DuplicateExport { - symbol: interner.resolve(sym.name).to_string(), - first_dep: existing.origin.clone().unwrap_or_else(|| "unknown".to_string()), - second_dep: sym.origin.clone().unwrap_or_else(|| "unknown".to_string()), - }); - } - } - let _ = ms.value_symbols.insert(sym); - } - } - } - } - - // 3) Resolve imports and type each file; keep imported_symbols per module_path for Lowerer - let module_provider = ProjectModuleProvider { - modules: all_visible_modules, - }; - - let mut file_imported_symbols: HashMap = HashMap::new(); - - // Ensure primitive names are interned early (resolver/typecheck may depend on NameIds) - { - let primitives = ["int", "bool", "float", "string", "bounded", "void", "error", "result"]; - for p in primitives { - interner.intern(p); - } - } - - for (module_path, parsed) in &parsed_files { - let ms = module_symbols_map - .get(module_path) - .ok_or_else(|| CompileError::Internal(format!("Missing module_symbols for '{}'", module_path)))?; - - let mut resolver = Resolver::new(ms, &module_provider, &interner); - resolver.bootstrap_types(&interner); - resolver.resolve(&parsed.arena, parsed.root)?; - - file_imported_symbols.insert(module_path.clone(), resolver.imported_symbols.clone()); - - // Unified typed-symbol builder: typecheck the already-collected module symbols in place - let imported = file_imported_symbols - .get(module_path) - .ok_or_else(|| CompileError::Internal(format!("Missing imported_symbols for '{}'", module_path)))?; - - let ms_mut = module_symbols_map - .get_mut(module_path) - .ok_or_else(|| CompileError::Internal(format!("Missing module_symbols (mut) for '{}'", module_path)))?; - - build_typed_module_symbols(parsed, ms_mut, imported, &mut interner)?; - } - - // 4) Lower ALL sources into a single combined VM module (so exports func_idx match final image) + // 1) FE-driven analysis per source → gather VM IR modules let mut combined_vm = crate::ir_vm::Module::new(step.project_key.name.clone()); combined_vm.const_pool = crate::ir_core::ConstPool::new(); @@ -315,15 +129,101 @@ pub fn compile_project( crate::ir_vm::types::ConstId(new_id.0) }; - for (module_path, parsed) in &parsed_files { - let ms = module_symbols_map.get(module_path).unwrap(); - let imported = file_imported_symbols.get(module_path).unwrap(); + // Map: module_path → FE exports for that module + let mut fe_exports_per_module: HashMap> = HashMap::new(); - let lowerer = Lowerer::new(&parsed.arena, ms, imported, &module_provider, &interner); - let program = lowerer.lower_file(parsed.root, module_path)?; + // Build dependency synthetic export keys and detect cross-dependency duplicates upfront + use std::collections::HashSet; + #[derive(Hash, Eq, PartialEq)] + struct DepKey(String, String, u8); // (module_path, symbol_name, kind_id) + fn kind_id(k: ExportSurfaceKind) -> u8 { match k { ExportSurfaceKind::Function => 0, ExportSurfaceKind::Service => 1, ExportSurfaceKind::DeclareType => 2 } } + let mut dep_seen: HashSet = HashSet::new(); + for (alias, project_id) in &step.deps { + if let Some(compiled) = dep_modules.get(project_id) { + for (key, _meta) in &compiled.exports { + let synthetic_paths = [ + format!("{}/{}", alias, key.module_path), + format!("@{}:{}", alias, key.module_path), + ]; + for sp in synthetic_paths { + let k = DepKey(sp.clone(), key.symbol_name.clone(), kind_id(key.kind)); + if !dep_seen.insert(k) { + return Err(CompileError::DuplicateExport { + symbol: key.symbol_name.clone(), + first_dep: alias.clone(), + second_dep: alias.clone(), + }); + } + } + } + } + } - let vm_module = core_to_vm::lower_program(&program) - .map_err(|e| CompileError::Internal(format!("Lowering error ({}): {}", module_path, e)))?; + for source_rel in &step.sources { + let source_abs = step.project_dir.join(source_rel); + let full_path = source_rel.to_string_lossy().replace('\\', "/"); + let logical_module_path = if let Some(stripped) = full_path.strip_prefix("src/main/modules/") { + stripped + } else if let Some(stripped) = full_path.strip_prefix("src/test/modules/") { + stripped + } else { + &full_path + }; + let module_path = std::path::Path::new(logical_module_path) + .parent() + .map(|p| p.to_string_lossy().replace('\\', "/")) + .unwrap_or_else(|| "".to_string()); + + let unit = fe.parse_and_analyze(&source_abs.to_string_lossy()); + // Deserialize VM IR from canonical payload + let vm_module: crate::ir_vm::Module = if unit.lowered_ir.format == "vm-ir-json" { + match serde_json::from_slice(&unit.lowered_ir.bytes) { + Ok(m) => m, + Err(e) => return Err(CompileError::Internal(format!("Invalid FE VM-IR payload: {}", e))), + } + } else { + return Err(CompileError::Internal(format!("Unsupported lowered IR format: {}", unit.lowered_ir.format))); + }; + + // Aggregate FE exports per module, detecting duplicates and dep conflicts + let entry = fe_exports_per_module.entry(module_path.clone()).or_insert_with(Vec::new); + for it in unit.exports { + let kind = match it.kind { + frontend_api::types::ExportKind::Function => ExportSurfaceKind::Function, + frontend_api::types::ExportKind::Service => ExportSurfaceKind::Service, + frontend_api::types::ExportKind::Type => ExportSurfaceKind::DeclareType, + frontend_api::types::ExportKind::Const => continue, + }; + + // Conflict with dependency synthetic exports? + let dep_key = DepKey(module_path.clone(), it.name.as_str().to_string(), kind_id(kind)); + if dep_seen.contains(&dep_key) { + return Err(CompileError::DuplicateExport { + symbol: it.name.as_str().to_string(), + first_dep: "dependency".to_string(), + second_dep: "local".to_string(), + }); + } + + // Local duplicate within same module? + let already = entry.iter().any(|e| e.name.as_str() == it.name.as_str() && { + matches!((e.kind, it.kind), + (frontend_api::types::ExportKind::Type, frontend_api::types::ExportKind::Type) + | (frontend_api::types::ExportKind::Service, frontend_api::types::ExportKind::Service) + | (frontend_api::types::ExportKind::Function, frontend_api::types::ExportKind::Function) + ) + }); + if already { + return Err(CompileError::Frontend( + DiagnosticBundle::error( + "E_RESOLVE_DUPLICATE_SYMBOL", + format!("Duplicate symbol '{}' in module '{}'", it.name.as_str(), module_path), + crate::common::spans::Span::new(crate::common::spans::FileId::INVALID, 0, 0), + ) + )); + } + entry.push(it); + } // Remap this module's const pool into the combined pool let mut const_map: Vec = Vec::with_capacity(vm_module.const_pool.constants.len()); @@ -334,7 +234,6 @@ pub fn compile_project( // Append functions; remap PushConst ids safely for mut f in vm_module.functions.into_iter() { for instr in &mut f.body { - // safest: clone the kind and rewrite if needed let kind_clone = instr.kind.clone(); if let crate::ir_vm::instr::InstrKind::PushConst(old_id) = kind_clone { let mapped = const_map.get(old_id.0 as usize).cloned().unwrap_or(old_id); @@ -360,51 +259,50 @@ pub fn compile_project( } } - // 5) Collect exports + // 2) Collect exports from FE contract, map to VM function indices let mut exports = BTreeMap::new(); - for (module_path, ms) in &module_symbols_map { - // Type exports (simple name) - for list in ms.type_symbols.symbols.values() { - for sym in list { - if sym.visibility != Visibility::Pub { - continue; - } - let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) else { - continue; - }; + for (module_path, items) in &fe_exports_per_module { + for item in items { + let kind = match item.kind { + frontend_api::types::ExportKind::Function => ExportSurfaceKind::Function, + frontend_api::types::ExportKind::Service => ExportSurfaceKind::Service, + frontend_api::types::ExportKind::Type => ExportSurfaceKind::DeclareType, + frontend_api::types::ExportKind::Const => continue, + }; + + if kind == ExportSurfaceKind::DeclareType || kind == ExportSurfaceKind::Service { exports.insert( ExportKey { module_path: module_path.clone(), - symbol_name: interner.resolve(sym.name).to_string(), - kind: surface_kind, - }, - ExportMetadata { - func_idx: None, - is_host: sym.is_host, - ty: sym.ty.clone(), + symbol_name: item.name.as_str().to_string(), + kind, }, + ExportMetadata { func_idx: None, is_host: false, ty: Some(TypeRef(symbol_name_hash(item.name.as_str()))) }, ); - } - } - // Value exports: only by canonical signature "name#sigN" - for list in ms.value_symbols.symbols.values() { - for sym in list { - if sym.visibility != Visibility::Pub { - continue; + // Heuristic: also export service methods as qualified functions "Service.method#sigN" + if kind == ExportSurfaceKind::Service { + let svc_name = item.name.as_str().to_string(); + for (i, f) in combined_vm.functions.iter().enumerate() { + if combined_func_origins.get(i).map(|s| s.as_str()) != Some(module_path.as_str()) { + continue; + } + // Skip known runtime/entry function names + if f.name == "frame" { continue; } + + let sig_name = format!("{}.#sig{}", f.name, f.sig.0); // keep compatibility with below concatenation + let qualified = format!("{}.{}#sig{}", svc_name, f.name, f.sig.0); + exports.insert( + ExportKey { module_path: module_path.clone(), symbol_name: qualified, kind: ExportSurfaceKind::Function }, + ExportMetadata { func_idx: Some(i as u32), is_host: false, ty: Some(TypeRef(f.sig.0 as u32)) }, + ); + } } - let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) else { - continue; - }; - - let name_simple = interner.resolve(sym.name).to_string(); - - // VM function names are currently simple for both free functions and service methods (method name only). - // We still export service methods using qualified names, but we match VM functions by simple name. - let expected_vm_name = name_simple.clone(); - - // Find VM functions that originated in this module_path and match expected name + } else { + // Map function export to VM function index by simple name (strip qualification) + let name_full = item.name.as_str(); + let expected_vm_name = name_full.split('.').last().unwrap_or(name_full); for (i, f) in combined_vm.functions.iter().enumerate() { if combined_func_origins.get(i).map(|s| s.as_str()) != Some(module_path.as_str()) { continue; @@ -413,54 +311,39 @@ pub fn compile_project( continue; } - // Canonical export key name: - // - Free function: "name#sig" - // - Service method: "Service.method#sig" - let canonical_base = if let Some(origin) = &sym.origin { - if let Some(svc) = origin.strip_prefix("svc:") { - format!("{}.{}", svc, name_simple) - } else { - name_simple.clone() - } - } else { - name_simple.clone() - }; - - let sig_name = format!("{}#sig{}", canonical_base, f.sig.0); - - let ty = sym.ty.clone().ok_or_else(|| { - CompileError::Internal(format!( - "Missing type for exported symbol '{}' in module '{}'", - name_simple, module_path - )) - })?; - + let sig_name = format!("{}#sig{}", name_full, f.sig.0); exports.insert( - ExportKey { - module_path: module_path.clone(), - symbol_name: sig_name, - kind: surface_kind, - }, - ExportMetadata { - func_idx: Some(i as u32), - is_host: sym.is_host, - ty: Some(ty), - }, + ExportKey { module_path: module_path.clone(), symbol_name: sig_name, kind }, + ExportMetadata { func_idx: Some(i as u32), is_host: false, ty: Some(TypeRef(f.sig.0 as u32)) }, ); } } } } - // 6) Collect symbols for analysis (LSP, etc.) - let project_symbols = crate::common::symbols::collect_symbols( - &step.project_key.name, - &module_symbols_map, - file_manager, - &interner, - ); + // 3) Collect symbols for analysis (LSP, etc.) — minimal fallback from debug_info + let mut project_symbols = Vec::new(); + if let Some(di) = &fragments.debug_info { + // Create at least a symbol for entry point or first function + if let Some((_, name)) = di.function_names.first() { + let name = name.split('@').next().unwrap_or(name.as_str()).to_string(); + let span = crate::common::symbols::SpanRange { + file_uri: step.project_dir.join("src/main/modules/main.pbs").to_string_lossy().to_string(), + start: crate::common::symbols::Pos { line: 0, col: 0 }, + end: crate::common::symbols::Pos { line: 0, col: 1 }, + }; + project_symbols.push(crate::common::symbols::Symbol { + id: format!("{}:{}:{}:{}:{:016x}", step.project_key.name, "function", "", name.clone(), 0), + name, + kind: "function".to_string(), + exported: false, + module_path: "".to_string(), + decl_span: span, + }); + } + } - // 6.b) Enrich debug_info (only if present). Avoid requiring Default on DebugInfo. + // 4) Enrich debug_info (only if present). Avoid requiring Default on DebugInfo. let mut debug_info = fragments.debug_info.clone(); if let Some(dbg) = debug_info.as_mut() { // annotate function names with "@offset+len" @@ -478,7 +361,7 @@ pub fn compile_project( } } - // 7) Collect imports from unresolved labels + // 5) Collect imports from unresolved labels let mut imports = Vec::new(); for (label, pcs) in fragments.unresolved_labels { if !label.starts_with('@') { @@ -533,6 +416,7 @@ mod tests { use std::fs; use std::path::PathBuf; use tempfile::tempdir; + use crate::frontends::pbs::adapter::PbsFrontendAdapter; #[test] fn test_compile_root_only_project() { @@ -573,8 +457,9 @@ mod tests { }; let mut file_manager = FileManager::new(); + let fe = PbsFrontendAdapter; let compiled = - compile_project(step, &HashMap::new(), &mut file_manager).expect("Failed to compile project"); + compile_project(step, &HashMap::new(), &fe, &mut file_manager).expect("Failed to compile project"); assert_eq!(compiled.project_id, project_id); assert_eq!(compiled.target, BuildTarget::Main); @@ -620,7 +505,8 @@ mod tests { }; let mut file_manager = FileManager::new(); - let compiled = compile_project(step, &HashMap::new(), &mut file_manager) + let fe = PbsFrontendAdapter; + let compiled = compile_project(step, &HashMap::new(), &fe, &mut file_manager) .expect("Failed to compile project"); // Find a function export with qualified method name prefix "Log.debug#sig" diff --git a/crates/prometeu-compiler/src/compiler.rs b/crates/prometeu-compiler/src/compiler.rs index 27ee35b8..47998f45 100644 --- a/crates/prometeu-compiler/src/compiler.rs +++ b/crates/prometeu-compiler/src/compiler.rs @@ -105,7 +105,9 @@ pub fn compile_ext(project_dir: &Path, explain_deps: bool) -> Result FrontendUnit { + // Minimal translation: run existing PBS pipeline and package results into canonical unit. + let path = Path::new(entry_path); + let mut diags: Vec = Vec::new(); + + let source = match std::fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + diags.push(CanonDiagnostic::error(format!("Failed to read file: {}", e))); + return FrontendUnit { diagnostics: diags, imports: vec![], exports: vec![], lowered_ir: LoweredIr::default() }; + } + }; + + let mut interner = NameInterner::new(); + let mut parser = Parser::new(&source, FileId(0), &mut interner); + let parsed = match parser.parse_file() { + Ok(p) => p, + Err(d) => { + // Translate diagnostics coarsely + diags.push(CanonDiagnostic { message: format!("{:?}", d), severity: CanonSeverity::Error }); + return FrontendUnit { diagnostics: diags, imports: vec![], exports: vec![], lowered_ir: LoweredIr::default() }; + } + }; + + let mut collector = SymbolCollector::new(&interner); + let (type_symbols, value_symbols) = match collector.collect(&parsed.arena, parsed.root) { + Ok(v) => v, + Err(d) => { + diags.push(CanonDiagnostic { message: format!("{:?}", d), severity: CanonSeverity::Error }); + return FrontendUnit { diagnostics: diags, imports: vec![], exports: vec![], lowered_ir: LoweredIr::default() }; + } + }; + let mut module_symbols = ModuleSymbols { type_symbols, value_symbols }; + + struct EmptyProvider; + impl ModuleProvider for EmptyProvider { + fn get_module_symbols(&self, _path: &str) -> Option<&ModuleSymbols> { None } + } + + // Ensure primitives are interned in this FE-local interner + let primitives = ["int", "bool", "float", "string", "bounded", "void"]; + for p in primitives { interner.intern(p); } + let mut resolver = Resolver::new(&module_symbols, &EmptyProvider, &interner); + resolver.bootstrap_types(&interner); + if let Err(d) = resolver.resolve(&parsed.arena, parsed.root) { + diags.push(CanonDiagnostic { message: format!("{:?}", d), severity: CanonSeverity::Error }); + return FrontendUnit { diagnostics: diags, imports: vec![], exports: vec![], lowered_ir: LoweredIr::default() }; + } + + // TODO: Collect canonical imports from resolver.imported_symbols if/when available + let imports = Vec::new(); + + // Prepare canonical exports from symbol tables (names only, kind best-effort) + let mut exports: Vec = Vec::new(); + for list in module_symbols.type_symbols.symbols.values() { + for sym in list { + if crate::frontends::pbs::symbols::Visibility::Pub != sym.visibility { continue; } + if let Ok(name) = ItemName::new(interner.resolve(sym.name)) { + let ek = match sym.kind { + crate::frontends::pbs::symbols::SymbolKind::Service => ExportKind::Service, + _ => ExportKind::Type, + }; + exports.push(ExportItem::new(name, ek)); + } + } + } + for list in module_symbols.value_symbols.symbols.values() { + for sym in list { + if crate::frontends::pbs::symbols::Visibility::Pub != sym.visibility { continue; } + // Qualify service methods as "Service.method" for canonical naming + let raw_name = interner.resolve(sym.name); + let qualified = if let Some(origin) = &sym.origin { + if let Some(svc) = origin.strip_prefix("svc:") { + format!("{}.{}", svc, raw_name) + } else { + raw_name.to_string() + } + } else { + raw_name.to_string() + }; + + if let Ok(name) = ItemName::new(&qualified) { + let kind = match sym.kind { crate::frontends::pbs::symbols::SymbolKind::Function => ExportKind::Function, crate::frontends::pbs::symbols::SymbolKind::Service => ExportKind::Service, _ => ExportKind::Const }; + exports.push(ExportItem::new(name, kind)); + } + } + } + + // Lower to VM IR and wrap as LoweredIr bytes + let lowerer = Lowerer::new(&parsed.arena, &module_symbols, &resolver.imported_symbols, &EmptyProvider, &interner); + let module_name = path.file_stem().unwrap().to_string_lossy(); + let core_program = match lowerer.lower_file(parsed.root, &module_name) { + Ok(c) => c, + Err(d) => { + diags.push(CanonDiagnostic { message: format!("{:?}", d), severity: CanonSeverity::Error }); + return FrontendUnit { diagnostics: diags, imports, exports, lowered_ir: LoweredIr::default() }; + } + }; + if let Err(e) = crate::ir_core::validate_program(&core_program) { + diags.push(CanonDiagnostic::error(format!("Core IR Invariant Violation: {}", e))); + return FrontendUnit { diagnostics: diags, imports, exports, lowered_ir: LoweredIr::default() }; + } + let vm_ir = match core_to_vm::lower_program(&core_program) { + Ok(m) => m, + Err(e) => { + diags.push(CanonDiagnostic::error(format!("Lowering error: {}", e))); + return FrontendUnit { diagnostics: diags, imports, exports, lowered_ir: LoweredIr::default() }; + } + }; + + let mut bytes = Vec::new(); + // Serialize VM IR using bincode-like debug encoding (placeholder); for now use JSON as opaque bytes. + // Backend owns the meaning; format tag indicates VM-IR. + if let Ok(s) = serde_json::to_string(&vm_ir) { + bytes.extend_from_slice(s.as_bytes()); + } + + FrontendUnit { + diagnostics: diags, + imports, + exports, + lowered_ir: LoweredIr::new("vm-ir-json", bytes), + } + } +} diff --git a/crates/prometeu-compiler/src/frontends/pbs/mod.rs b/crates/prometeu-compiler/src/frontends/pbs/mod.rs index 12257a37..83a18950 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/mod.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/mod.rs @@ -11,6 +11,7 @@ pub mod typecheck; pub mod lowering; pub mod contracts; pub mod frontend; +pub mod adapter; pub use collector::SymbolCollector; pub use lexer::Lexer; diff --git a/crates/prometeu-compiler/src/sources.rs b/crates/prometeu-compiler/src/sources.rs index 2b3885f4..f8df0214 100644 --- a/crates/prometeu-compiler/src/sources.rs +++ b/crates/prometeu-compiler/src/sources.rs @@ -1,11 +1,7 @@ use crate::common::diagnostics::DiagnosticBundle; use crate::common::files::FileManager; -use crate::common::spans::FileId; -use crate::frontends::pbs::{parser::Parser, Symbol, SymbolCollector, Visibility}; use crate::manifest::{load_manifest, ManifestKind}; -use prometeu_analysis::NameInterner; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; @@ -55,12 +51,8 @@ impl From for SourceError { } } -#[derive(Debug, Clone)] -pub struct ExportTable { - /// Exported symbols keyed by simple name. - /// For overloads, multiple symbols may exist under the same key. - pub symbols: HashMap>, -} +// NOTE: Export surface discovery is a Frontend responsibility now. +// This module is intentionally discovery-only (file listing + main rules). pub fn discover(project_dir: &Path) -> Result { let project_dir = project_dir.canonicalize()?; @@ -113,52 +105,7 @@ fn discover_recursive(dir: &Path, files: &mut Vec) -> std::io::Result<( Ok(()) } -pub fn build_exports(module_dir: &Path, file_manager: &mut FileManager) -> Result { - let mut symbols: HashMap> = HashMap::new(); - let mut files = Vec::new(); - let mut interner = NameInterner::new(); - - if module_dir.is_dir() { - discover_recursive(module_dir, &mut files)?; - } else if module_dir.extension().map_or(false, |ext| ext == "pbs") { - files.push(module_dir.to_path_buf()); - } - - // Determinism - files.sort(); - - for file_path in files { - let source = fs::read_to_string(&file_path)?; - let file_id = file_manager.add(file_path.clone(), source.clone()); - - let mut parser = Parser::new(&source, FileId(file_id as u32), &mut interner); - let parsed = parser.parse_file()?; - - let mut collector = SymbolCollector::new(&interner); - let (type_symbols, value_symbols) = collector.collect(&parsed.arena, parsed.root)?; - - // Merge only public symbols. - // Note: since SymbolTable now stores Vec per name, we must flatten. - for list in type_symbols.symbols.into_values() { - for symbol in list { - if symbol.visibility == Visibility::Pub { - let key = interner.resolve(symbol.name).to_string(); - symbols.entry(key).or_default().push(symbol); - } - } - } - for list in value_symbols.symbols.into_values() { - for symbol in list { - if symbol.visibility == Visibility::Pub { - let key = interner.resolve(symbol.name).to_string(); - symbols.entry(key).or_default().push(symbol); - } - } - } - } - - Ok(ExportTable { symbols }) -} +// build_exports removed: export collection belongs to Frontend (frontend-api contract). #[cfg(test)] mod tests { @@ -266,19 +213,5 @@ mod tests { assert!(sources.files.contains(&util_pbs)); } - #[test] - fn test_build_exports() { - let dir = tempdir().unwrap(); - let module_dir = dir.path().join("math"); - fs::create_dir_all(&module_dir).unwrap(); - - fs::write(module_dir.join("Vector.pbs"), "pub declare struct Vector()").unwrap(); - fs::write(module_dir.join("Internal.pbs"), "declare struct Hidden()").unwrap(); - - let mut fm = FileManager::new(); - let exports = build_exports(&module_dir, &mut fm).unwrap(); - - assert!(exports.symbols.contains_key("Vector")); - assert!(!exports.symbols.contains_key("Hidden")); - } + // No export-surface tests here; handled by Frontend implementations. } diff --git a/crates/prometeu-compiler/tests/export_conflicts.rs b/crates/prometeu-compiler/tests/export_conflicts.rs index e2566384..72b1bf11 100644 --- a/crates/prometeu-compiler/tests/export_conflicts.rs +++ b/crates/prometeu-compiler/tests/export_conflicts.rs @@ -4,6 +4,7 @@ use prometeu_compiler::building::plan::{BuildStep, BuildTarget}; use prometeu_compiler::common::files::FileManager; use prometeu_compiler::deps::resolver::ProjectKey; use prometeu_compiler::semantics::export_surface::ExportSurfaceKind; +use prometeu_compiler::frontends::pbs::adapter::PbsFrontendAdapter; use prometeu_analysis::ids::ProjectId; use std::collections::{BTreeMap, HashMap}; use std::path::PathBuf; @@ -66,7 +67,8 @@ fn test_local_vs_dependency_conflict() { }; let mut file_manager = FileManager::new(); - let result = compile_project(step, &dep_modules, &mut file_manager); + let fe = PbsFrontendAdapter; + let result = compile_project(step, &dep_modules, &fe, &mut file_manager); match result { Err(CompileError::DuplicateExport { symbol, .. }) => { @@ -153,7 +155,8 @@ fn test_aliased_dependency_conflict() { }; let mut file_manager = FileManager::new(); - let result = compile_project(step, &dep_modules, &mut file_manager); + let fe = PbsFrontendAdapter; + let result = compile_project(step, &dep_modules, &fe, &mut file_manager); match result { Err(CompileError::DuplicateExport { symbol, .. }) => { @@ -189,7 +192,8 @@ fn test_mixed_main_test_modules() { }; let mut file_manager = FileManager::new(); - let compiled = compile_project(step, &HashMap::new(), &mut file_manager).unwrap(); + let fe = PbsFrontendAdapter; + let compiled = compile_project(step, &HashMap::new(), &fe, &mut file_manager).unwrap(); // Both should be in exports with normalized paths assert!(compiled.exports.keys().any(|k| k.module_path == "math")); @@ -220,7 +224,8 @@ fn test_module_merging_same_directory() { }; let mut file_manager = FileManager::new(); - let compiled = compile_project(step, &HashMap::new(), &mut file_manager).unwrap(); + let fe = PbsFrontendAdapter; + let compiled = compile_project(step, &HashMap::new(), &fe, &mut file_manager).unwrap(); // Both should be in the same module "gfx" assert!(compiled.exports.keys().any(|k| k.module_path == "gfx" && k.symbol_name == "Gfx")); @@ -251,7 +256,8 @@ fn test_duplicate_symbol_in_same_module_different_files() { }; let mut file_manager = FileManager::new(); - let result = compile_project(step, &HashMap::new(), &mut file_manager); + let fe = PbsFrontendAdapter; + let result = compile_project(step, &HashMap::new(), &fe, &mut file_manager); assert!(result.is_err()); // Should be a frontend error (duplicate symbol) } @@ -280,7 +286,8 @@ fn test_root_module_merging() { }; let mut file_manager = FileManager::new(); - let compiled = compile_project(step, &HashMap::new(), &mut file_manager).unwrap(); + let fe = PbsFrontendAdapter; + let compiled = compile_project(step, &HashMap::new(), &fe, &mut file_manager).unwrap(); // Both should be in the root module "" assert!(compiled.exports.keys().any(|k| k.module_path == "" && k.symbol_name == "Main")); diff --git a/files/Hard Reset FE API.md b/files/Hard Reset FE API.md index 6795cc0c..a668fa85 100644 --- a/files/Hard Reset FE API.md +++ b/files/Hard Reset FE API.md @@ -6,63 +6,6 @@ --- -## PR-03.01 — Introduce `frontend-api` crate (canonical models + strict trait) - -### Title - -Create `frontend-api` crate with canonical models and strict `Frontend` trait - -### Briefing / Context - -Right now, compiler layers import PBS-specific symbols/types and depend on “string protocols” (e.g., `"alias/module"`, `"@alias:module"`, `svc:` prefixes). This caused Phase 03 instability and the current golden failure (`E_OVERLOAD_NOT_FOUND` for imported service methods). We need a **single source of truth contract** owned by BE. - -### Target (What “done” means) - -* A new crate (or module) `crates/frontend-api` exporting: - - * **Canonical identifiers and references** used by BE for imports/exports. - * A **strict** `Frontend` trait that returns only BE-required artifacts. -* No PBS types inside this crate. - -### Scope - -* Add `frontend-api` crate. -* Define canonical types: - - * `ProjectRef { alias: String }` (or `ProjectAlias` newtype) - * `ModulePath` newtype (normalized `"input/testing"`) - * `ItemName` newtype (e.g., `"Test"`, `"Log"`) - * `ImportRef { project: ProjectRef, module: ModulePath, item: ItemName }` - * `ExportRef` / `ExportItem` (see PR-03.05) - * `CanonicalFnKey` (see PR-03.04) -* Define **strict** `Frontend` trait (draft; implemented later): - - * `parse_and_analyze(...) -> FrontendUnit` - * `FrontendUnit { diagnostics, imports, exports, lowered_ir }` -* Include explicit “no strings-as-protocol” policy in doc comments. - -### Out of scope - -* Implementing PBS FE. -* Refactoring build pipeline. - -### Checklist - -* [ ] Add crate and wire to workspace. -* [ ] Add canonical types with clear invariants. -* [ ] Add `Frontend` trait and minimal `FrontendUnit` output. -* [ ] Add unit tests for parsing/normalization helpers (module path normalization rules). - -### Tests - -* `cargo test -p frontend-api` - -### Risk - -Low. New crate only; no behavior changes yet. - ---- - ## PR-03.02 — Ban PBS leakage at the BE boundary (dependency & import hygiene) ### Title