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::Span; use crate::deps::resolver::ProjectKey; use crate::semantics::export_surface::ExportSurfaceKind; use prometeu_analysis::ids::ProjectId; use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta}; use serde::{Deserialize, Serialize}; use frontend_api::types::{TypeRef, ExportItem}; use frontend_api::traits::Frontend as CanonFrontend; use std::collections::{BTreeMap, HashMap}; // 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 { pub module_path: String, pub item: ExportItem, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExportMetadata { pub func_idx: Option, pub is_host: bool, pub ty: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ImportKey { pub dep_alias: String, pub module_path: String, pub symbol_name: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImportMetadata { pub key: ImportKey, pub relocation_pcs: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CompiledModule { pub project_id: ProjectId, pub project_key: ProjectKey, pub target: BuildTarget, pub exports: BTreeMap, pub imports: Vec, pub const_pool: Vec, pub code: Vec, pub function_metas: Vec, pub debug_info: Option, pub symbols: Vec, } #[derive(Debug)] pub enum CompileError { Frontend(DiagnosticBundle), DuplicateExport { symbol: String, first_dep: String, second_dep: String, }, Io(std::io::Error), Internal(String), } impl std::fmt::Display for CompileError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { CompileError::Frontend(d) => write!(f, "Frontend error: {:?}", d), CompileError::DuplicateExport { symbol, first_dep, second_dep, } => write!( f, "duplicate export: symbol `{}`\n first defined in dependency `{}`\n again defined in dependency `{}`", symbol, first_dep, second_dep ), CompileError::Io(e) => write!(f, "IO error: {}", e), CompileError::Internal(s) => write!(f, "Internal error: {}", s), } } } impl std::error::Error for CompileError {} impl From for CompileError { fn from(e: std::io::Error) -> Self { CompileError::Io(e) } } impl From for CompileError { fn from(d: crate::common::diagnostics::DiagnosticBundle) -> Self { CompileError::Frontend(d) } } // Note: PBS ModuleProvider/ModuleSymbols are no longer used at the backend boundary. pub fn compile_project( step: BuildStep, dep_modules: &HashMap, fe: &dyn CanonFrontend, _file_manager: &mut FileManager, ) -> Result { // 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(); // Origin module_path per appended function let mut combined_func_origins: Vec = Vec::new(); let insert_const = |pool: &mut crate::ir_core::ConstPool, val: &crate::ir_core::ConstantValue| -> crate::ir_vm::types::ConstId { let new_id = pool.insert(val.clone()); crate::ir_vm::types::ConstId(new_id.0) }; // Map: module_path → FE exports for that module let mut fe_exports_per_module: HashMap> = HashMap::new(); // Build dependency synthetic export keys and detect cross-dependency duplicates upfront use std::collections::HashSet; #[derive(Hash, Eq, PartialEq)] struct DepKey(String, ExportItem); // (module_path, item) 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 { // Track using canonical item keyed by module path; alias variations only for display/conflict reporting. let synthetic_paths = [ format!("{}/{}", alias, key.module_path), format!("@{}:{}", alias, key.module_path), ]; for sp in synthetic_paths { let k = DepKey(sp.clone(), key.item.clone()); if !dep_seen.insert(k) { return Err(CompileError::DuplicateExport { symbol: format!("{:?}", key.item), first_dep: alias.clone(), second_dep: alias.clone(), }); } } } } } 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 { // Conflict with dependency synthetic exports? let dep_key = DepKey(module_path.clone(), it.clone()); if dep_seen.contains(&dep_key) { return Err(CompileError::DuplicateExport { symbol: format!("{:?}", it), first_dep: "dependency".to_string(), second_dep: "local".to_string(), }); } // Local duplicate within same module? let already = entry.iter().any(|e| e == &it); if already { return Err(CompileError::Frontend( DiagnosticBundle::error( "E_RESOLVE_DUPLICATE_SYMBOL", format!("Duplicate symbol '{:?}' in module '{}'", it, 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()); for c in &vm_module.const_pool.constants { const_map.push(insert_const(&mut combined_vm.const_pool, c)); } // Append functions; remap PushConst ids safely for mut f in vm_module.functions.into_iter() { for instr in &mut f.body { 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); instr.kind = crate::ir_vm::instr::InstrKind::PushConst(mapped); } } combined_func_origins.push(module_path.clone()); combined_vm.functions.push(f); } } let fragments = emit_fragments(&combined_vm) .map_err(|e| CompileError::Internal(format!("Emission error: {}", e)))?; // Ensure function metas reflect final slots info let mut fixed_function_metas = fragments.functions.clone(); for (i, fm) in fixed_function_metas.iter_mut().enumerate() { if let Some(vm_func) = combined_vm.functions.get(i) { fm.param_slots = vm_func.param_slots; fm.local_slots = vm_func.local_slots; fm.return_slots = vm_func.return_slots; } } // 2) Collect exports from FE contract, map to VM function indices let mut exports = BTreeMap::new(); for (module_path, items) in &fe_exports_per_module { for item in items { match item { ExportItem::Type { name } => { exports.insert( ExportKey { module_path: module_path.clone(), item: item.clone() }, ExportMetadata { func_idx: None, is_host: false, ty: Some(TypeRef(symbol_name_hash(name.as_str()))) }, ); } ExportItem::Service { name: _ } => { // Service owner export (no functions synthesized here) exports.insert( ExportKey { module_path: module_path.clone(), item: item.clone() }, ExportMetadata { func_idx: None, is_host: false, ty: None }, ); } ExportItem::Function { fn_key } => { // Map function to VM function index by name + signature id 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; } if f.name != fn_key.name.as_str() { continue; } if f.sig.0 != fn_key.sig.0 { continue; } exports.insert( ExportKey { module_path: module_path.clone(), item: item.clone() }, ExportMetadata { func_idx: Some(i as u32), is_host: false, ty: Some(TypeRef(f.sig.0 as u32)) }, ); } } } } } // 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, }); } } // 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" // NOTE: assumes dbg.function_names aligns with functions order. let mut enriched = Vec::new(); for (i, (fid, name)) in dbg.function_names.clone().into_iter().enumerate() { if let Some(meta) = fixed_function_metas.get(i) { enriched.push((fid, format!("{}@{}+{}", name, meta.code_offset, meta.code_len))); } else { enriched.push((fid, name)); } } if !enriched.is_empty() { dbg.function_names = enriched; } } // 5) Collect imports from unresolved labels let mut imports = Vec::new(); for (label, pcs) in fragments.unresolved_labels { if !label.starts_with('@') { continue; } // Format: @dep_alias::module_path:symbol_name let parts: Vec<&str> = label[1..].splitn(2, "::").collect(); if parts.len() != 2 { continue; } let dep_alias = parts[0].to_string(); let rest = parts[1]; // Split from the right once: "...:" let sub_parts: Vec<&str> = rest.rsplitn(2, ':').collect(); if sub_parts.len() != 2 { continue; } let symbol_name = sub_parts[0].to_string(); let module_path = sub_parts[1].to_string(); imports.push(ImportMetadata { key: ImportKey { dep_alias, module_path, symbol_name, }, relocation_pcs: pcs, }); } Ok(CompiledModule { project_id: step.project_id, project_key: step.project_key, target: step.target, exports, imports, const_pool: fragments.const_pool, code: fragments.code, function_metas: fixed_function_metas, debug_info, symbols: project_symbols, }) } #[cfg(test)] mod tests { use super::*; use std::fs; use std::path::PathBuf; use tempfile::tempdir; use crate::frontends::pbs::adapter::PbsFrontendAdapter; use frontend_api::types::{ItemName, ExportItem, CanonicalFnKey}; #[test] fn test_compile_root_only_project() { let dir = tempdir().unwrap(); let project_dir = dir.path().to_path_buf(); fs::create_dir_all(project_dir.join("src/main/modules")).unwrap(); // NOTE: ajuste de sintaxe: seu PBS entrypoint atual é `fn frame(): void` dentro de main.pbs // e "mod fn frame" pode não ser válido. Mantive o essencial. let main_code = r#" pub declare struct Vec2(x: int, y: int) fn add(a: int, b: int): int { return a + b; } fn frame(): void { let x = add(1, 2); } "#; fs::write(project_dir.join("src/main/modules/main.pbs"), main_code).unwrap(); let project_key = ProjectKey { name: "root".to_string(), version: "0.1.0".to_string(), }; let project_id = ProjectId(0); let step = BuildStep { project_id, project_key: project_key.clone(), project_dir: project_dir.clone(), target: BuildTarget::Main, sources: vec![PathBuf::from("src/main/modules/main.pbs")], deps: BTreeMap::new(), }; let mut file_manager = FileManager::new(); let fe = PbsFrontendAdapter; let compiled = 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); // Vec2 should be exported (canonical) let vec2_key = ExportKey { module_path: "".to_string(), item: ExportItem::Type { name: ItemName::new("Vec2").unwrap() }, }; assert!(compiled.exports.contains_key(&vec2_key)); } #[test] fn test_service_method_export_qualified() { let dir = tempdir().unwrap(); let project_dir = dir.path().to_path_buf(); fs::create_dir_all(project_dir.join("src/main/modules")).unwrap(); let main_code = r#" pub service Log { fn debug(msg: string): void { } } "#; fs::write(project_dir.join("src/main/modules/main.pbs"), main_code).unwrap(); let project_key = ProjectKey { name: "root".to_string(), version: "0.1.0".to_string(), }; let project_id = ProjectId(0); let step = BuildStep { project_id, project_key: project_key.clone(), project_dir: project_dir.clone(), target: BuildTarget::Main, sources: vec![PathBuf::from("src/main/modules/main.pbs")], deps: BTreeMap::new(), }; let mut file_manager = FileManager::new(); let fe = PbsFrontendAdapter; let compiled = compile_project(step, &HashMap::new(), &fe, &mut file_manager) .expect("Failed to compile project"); // Find a function export with canonical fn key: owner=Log, name=debug let mut found = false; for (key, _meta) in &compiled.exports { if let ExportItem::Function { fn_key } = &key.item { if fn_key.owner.as_ref().map(|n| n.as_str()) == Some("Log") && fn_key.name.as_str() == "debug" { found = true; break; } } } assert!(found, "Expected an export with canonical fn key owner=Log, name=debug but not found. Exports: {:?}", compiled.exports.keys().collect::>()); } }