diff --git a/crates/frontend-api/src/types.rs b/crates/frontend-api/src/types.rs index 9ecc994b..44fea3ef 100644 --- a/crates/frontend-api/src/types.rs +++ b/crates/frontend-api/src/types.rs @@ -164,16 +164,18 @@ pub enum ExportKind { Const, } -/// An export item description (opaque for BE logic, except name/kind). +/// Canonical export item (FE-owned, BE-agnostic, no string protocols). +/// +/// This surface is stable even if display formatting changes. #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub struct ExportItem { - pub name: ItemName, - pub kind: ExportKind, -} - -impl ExportItem { - pub fn new(name: ItemName, kind: ExportKind) -> Self { Self { name, kind } } +pub enum ExportItem { + /// A declared canonical type (struct/enum/etc.) + Type { name: ItemName }, + /// A declared service owner (method container) + Service { name: ItemName }, + /// A function export (free fn or method) identified by canonical key + Function { fn_key: CanonicalFnKey }, } /// A fully-qualified export reference: project + module + item + kind. diff --git a/crates/prometeu-compiler/src/building/linker.rs b/crates/prometeu-compiler/src/building/linker.rs index 9697de9c..e7772d5f 100644 --- a/crates/prometeu-compiler/src/building/linker.rs +++ b/crates/prometeu-compiler/src/building/linker.rs @@ -81,8 +81,11 @@ impl Linker { let mut combined_pc_to_span = Vec::new(); let mut combined_function_names = Vec::new(); - // 1. DebugSymbol resolution map: (ProjectKey, module_path, symbol_name) -> func_idx in combined_functions + // 1. DebugSymbol resolution maps: + // - canonical: (ProjectKey, module_path, ExportItem) -> func_idx + // - compatibility (Phase 03 transitional): (ProjectKey, module_path, short_name) -> func_idx let mut global_symbols = HashMap::new(); + let mut global_symbols_str = HashMap::new(); let mut module_code_offsets = Vec::with_capacity(modules.len()); let mut module_function_offsets = Vec::with_capacity(modules.len()); @@ -102,12 +105,25 @@ impl Linker { if let Some(local_func_idx) = meta.func_idx { let global_func_idx = function_offset + local_func_idx; // Note: Use a tuple as key for clarity - let symbol_id = (module.project_id.clone(), key.module_path.clone(), key.symbol_name.clone()); + let symbol_id = (module.project_id.clone(), key.module_path.clone(), key.item.clone()); if global_symbols.contains_key(&symbol_id) { - return Err(LinkError::DuplicateExport(format!("Project {:?} export {}:{} already defined", symbol_id.0, symbol_id.1, symbol_id.2))); + return Err(LinkError::DuplicateExport(format!( + "Project {:?} export {}:{:?} already defined", + symbol_id.0, symbol_id.1, symbol_id.2 + ))); } + // Canonical mapping global_symbols.insert(symbol_id, global_func_idx); + + // Compatibility string mapping (short name only) + let short_name = match &key.item { + frontend_api::types::ExportItem::Function { fn_key } => fn_key.name.as_str().to_string(), + frontend_api::types::ExportItem::Service { name } => name.as_str().to_string(), + frontend_api::types::ExportItem::Type { name } => name.as_str().to_string(), + }; + let symbol_id_str = (module.project_id.clone(), key.module_path.clone(), short_name); + global_symbols_str.insert(symbol_id_str, global_func_idx); } } @@ -171,7 +187,7 @@ impl Linker { for pid in candidate_projects { let pid_val: ProjectId = (*pid).clone(); let key = (pid_val, import.key.module_path.clone(), import.key.symbol_name.clone()); - if let Some(&idx) = global_symbols.get(&key) { + if let Some(&idx) = global_symbols_str.get(&key) { resolved_idx = Some(idx); break; } @@ -272,10 +288,12 @@ impl Linker { for (key, meta) in &root_module.exports { if let Some(local_func_idx) = meta.func_idx { let global_func_idx = module_function_offsets.last().unwrap() + local_func_idx; - final_exports.insert(format!("{}:{}", key.module_path, key.symbol_name), global_func_idx); - // Also provide short name for root module exports to facilitate entrypoint resolution - if !final_exports.contains_key(&key.symbol_name) { - final_exports.insert(key.symbol_name.clone(), global_func_idx); + final_exports.insert(format!("{}:{:?}", key.module_path, key.item), global_func_idx); + // Also provide short name for root module exports to facilitate entrypoint resolution. + // For canonical items, we fall back to the `Debug` representation without the module path. + let short = format!("{:?}", key.item); + if !final_exports.contains_key(&short) { + final_exports.insert(short, global_func_idx); } } } @@ -421,11 +439,9 @@ mod tests { lib_code.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes()); let mut lib_exports = BTreeMap::new(); - lib_exports.insert(ExportKey { - module_path: "math".into(), - symbol_name: "add".into(), - kind: ExportSurfaceKind::Service, - }, ExportMetadata { func_idx: Some(0), is_host: false, ty: None }); + use frontend_api::types::{ExportItem, ItemName, CanonicalFnKey, SignatureRef}; + let add_key = ExportItem::Function { fn_key: CanonicalFnKey::new(None, ItemName::new("add").unwrap(), SignatureRef(0)) }; + lib_exports.insert(ExportKey { module_path: "math".into(), item: add_key }, ExportMetadata { func_idx: Some(0), is_host: false, ty: None }); let lib_module = CompiledModule { project_id: lib_id, diff --git a/crates/prometeu-compiler/src/building/output.rs b/crates/prometeu-compiler/src/building/output.rs index ad7bb50b..832a940d 100644 --- a/crates/prometeu-compiler/src/building/output.rs +++ b/crates/prometeu-compiler/src/building/output.rs @@ -8,7 +8,7 @@ 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; +use frontend_api::types::{TypeRef, ExportItem}; use frontend_api::traits::Frontend as CanonFrontend; use std::collections::{BTreeMap, HashMap}; @@ -25,8 +25,7 @@ fn symbol_name_hash(name: &str) -> u32 { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct ExportKey { pub module_path: String, - pub symbol_name: String, - pub kind: ExportSurfaceKind, + pub item: ExportItem, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -135,23 +134,21 @@ pub fn compile_project( // 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 } } + 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 both legacy and canonical forms ONLY for collision detection between deps and local modules. - // Import resolution uses only '@alias:module' elsewhere. + // 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.symbol_name.clone(), kind_id(key.kind)); + let k = DepKey(sp.clone(), key.item.clone()); if !dep_seen.insert(k) { return Err(CompileError::DuplicateExport { - symbol: key.symbol_name.clone(), + symbol: format!("{:?}", key.item), first_dep: alias.clone(), second_dep: alias.clone(), }); @@ -190,36 +187,23 @@ pub fn compile_project( // 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)); + let dep_key = DepKey(module_path.clone(), it.clone()); if dep_seen.contains(&dep_key) { return Err(CompileError::DuplicateExport { - symbol: it.name.as_str().to_string(), + 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.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) - ) - }); + 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.name.as_str(), module_path), + format!("Duplicate symbol '{:?}' in module '{}'", it, module_path), crate::common::spans::Span::new(crate::common::spans::FileId::INVALID, 0, 0), ) )); @@ -266,59 +250,32 @@ pub fn compile_project( 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: item.name.as_str().to_string(), - kind, - }, - ExportMetadata { func_idx: None, is_host: false, ty: Some(TypeRef(symbol_name_hash(item.name.as_str()))) }, - ); - - // Heuristic: also export service methods as qualified functions "Service.method#sigN" - if kind == ExportSurfaceKind::Service { - let svc_name = item.name.as_str().to_string(); + 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; - } - // 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); + 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(), symbol_name: qualified, kind: ExportSurfaceKind::Function }, + 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)) }, ); } } - } 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; - } - if f.name != expected_vm_name { - continue; - } - - let sig_name = format!("{}#sig{}", name_full, f.sig.0); - exports.insert( - 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)) }, - ); - } } } } @@ -419,6 +376,7 @@ mod tests { 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() { @@ -466,11 +424,10 @@ mod tests { assert_eq!(compiled.project_id, project_id); assert_eq!(compiled.target, BuildTarget::Main); - // Vec2 should be exported + // Vec2 should be exported (canonical) let vec2_key = ExportKey { module_path: "".to_string(), - symbol_name: "Vec2".to_string(), - kind: ExportSurfaceKind::DeclareType, + item: ExportItem::Type { name: ItemName::new("Vec2").unwrap() }, }; assert!(compiled.exports.contains_key(&vec2_key)); } @@ -511,15 +468,17 @@ mod tests { 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" + // Find a function export with canonical fn key: owner=Log, name=debug let mut found = false; for (key, _meta) in &compiled.exports { - if key.kind == ExportSurfaceKind::Function && key.symbol_name.starts_with("Log.debug#sig") { - found = true; - break; + 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 qualified name 'Log.debug#sigX' but not found. Exports: {:?}", compiled.exports.keys().collect::>()); + assert!(found, "Expected an export with canonical fn key owner=Log, name=debug but not found. Exports: {:?}", compiled.exports.keys().collect::>()); } } diff --git a/crates/prometeu-compiler/src/frontends/pbs/adapter.rs b/crates/prometeu-compiler/src/frontends/pbs/adapter.rs index 0959b52d..37c1c830 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/adapter.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/adapter.rs @@ -2,7 +2,17 @@ use crate::frontends::pbs::{parser::Parser, SymbolCollector, ModuleSymbols, Reso use crate::lowering::core_to_vm; use crate::common::spans::FileId; use frontend_api::traits::{Frontend as CanonFrontend, FrontendUnit}; -use frontend_api::types::{Diagnostic as CanonDiagnostic, Severity as CanonSeverity, ExportItem, ExportKind, ItemName, LoweredIr, ImportRef, parse_pbs_from_string}; +use frontend_api::types::{ + Diagnostic as CanonDiagnostic, + Severity as CanonSeverity, + ExportItem, + ItemName, + LoweredIr, + ImportRef, + CanonicalFnKey, + SignatureRef, + parse_pbs_from_string, +}; use prometeu_analysis::NameInterner; use std::path::Path; @@ -90,38 +100,33 @@ impl CanonFrontend for PbsFrontendAdapter { } } - // Prepare canonical exports from symbol tables (names only, kind best-effort) + // Prepare canonical exports from symbol tables (canonical, no string protocols) 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)); + match sym.kind { + crate::frontends::pbs::symbols::SymbolKind::Service => exports.push(ExportItem::Service { name }), + _ => exports.push(ExportItem::Type { name }), + } } } } 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 + // Build canonical function key for free fns and methods 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() + if let crate::frontends::pbs::symbols::SymbolKind::Function = sym.kind { + // Attempt to derive owner from origin `svc:Owner` if present + let owner_name = sym.origin.as_deref().and_then(|o| o.strip_prefix("svc:")).and_then(|svc| ItemName::new(svc).ok()); + // We don't have a stable signature id from PBS yet; use 0 as placeholder until resolver exposes it. + let sig = SignatureRef(0); + if let Ok(name_item) = ItemName::new(raw_name) { + let fn_key = CanonicalFnKey::new(owner_name, name_item, sig); + exports.push(ExportItem::Function { fn_key }); } - } 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)); } } } diff --git a/crates/prometeu-compiler/tests/export_conflicts.rs b/crates/prometeu-compiler/tests/export_conflicts.rs index 72b1bf11..e5a2174f 100644 --- a/crates/prometeu-compiler/tests/export_conflicts.rs +++ b/crates/prometeu-compiler/tests/export_conflicts.rs @@ -3,7 +3,7 @@ use prometeu_compiler::building::output::{compile_project, CompileError, ExportK 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 frontend_api::types::{ExportItem, ItemName}; use prometeu_compiler::frontends::pbs::adapter::PbsFrontendAdapter; use prometeu_analysis::ids::ProjectId; use std::collections::{BTreeMap, HashMap}; @@ -23,8 +23,7 @@ fn test_local_vs_dependency_conflict() { let mut dep_exports = BTreeMap::new(); dep_exports.insert(ExportKey { module_path: "math".to_string(), // normalized path - symbol_name: "Vector".to_string(), - kind: ExportSurfaceKind::DeclareType, + item: ExportItem::Type { name: ItemName::new("Vector").unwrap() }, }, ExportMetadata { func_idx: None, is_host: false, @@ -89,8 +88,7 @@ fn test_aliased_dependency_conflict() { let mut dep1_exports = BTreeMap::new(); dep1_exports.insert(ExportKey { module_path: "b/c".to_string(), - symbol_name: "Vector".to_string(), - kind: ExportSurfaceKind::DeclareType, + item: ExportItem::Type { name: ItemName::new("Vector").unwrap() }, }, ExportMetadata { func_idx: None, is_host: false, @@ -115,8 +113,7 @@ fn test_aliased_dependency_conflict() { let mut dep2_exports = BTreeMap::new(); dep2_exports.insert(ExportKey { module_path: "c".to_string(), - symbol_name: "Vector".to_string(), - kind: ExportSurfaceKind::DeclareType, + item: ExportItem::Type { name: ItemName::new("Vector").unwrap() }, }, ExportMetadata { func_idx: None, is_host: false, @@ -228,8 +225,8 @@ fn test_module_merging_same_directory() { 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")); - assert!(compiled.exports.keys().any(|k| k.module_path == "gfx" && k.symbol_name == "Color")); + assert!(compiled.exports.keys().any(|k| k.module_path == "gfx" && matches!(&k.item, ExportItem::Service { name } if name.as_str() == "Gfx"))); + assert!(compiled.exports.keys().any(|k| k.module_path == "gfx" && matches!(&k.item, ExportItem::Type { name } if name.as_str() == "Color"))); } #[test] @@ -290,6 +287,6 @@ fn test_root_module_merging() { 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")); - assert!(compiled.exports.keys().any(|k| k.module_path == "" && k.symbol_name == "Utils")); + assert!(compiled.exports.keys().any(|k| k.module_path == "" && matches!(&k.item, ExportItem::Service { name } if name.as_str() == "Main"))); + assert!(compiled.exports.keys().any(|k| k.module_path == "" && matches!(&k.item, ExportItem::Service { name } if name.as_str() == "Utils"))); } diff --git a/test-cartridges/canonical/golden/program.pbc b/test-cartridges/canonical/golden/program.pbc index 5a70490d..3646ac85 100644 Binary files a/test-cartridges/canonical/golden/program.pbc and b/test-cartridges/canonical/golden/program.pbc differ