This commit is contained in:
bQUARKz 2026-02-10 20:38:25 +00:00
parent a4cc1487c4
commit f994a566d8
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 112 additions and 133 deletions

View File

@ -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.

View File

@ -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,

View File

@ -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<DepKey> = 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::<Vec<_>>());
assert!(found, "Expected an export with canonical fn key owner=Log, name=debug but not found. Exports: {:?}", compiled.exports.keys().collect::<Vec<_>>());
}
}

View File

@ -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<ExportItem> = 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));
}
}
}

View File

@ -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")));
}