This commit is contained in:
bQUARKz 2026-02-10 17:54:49 +00:00
parent 6a4f7ea773
commit df8c0db6eb
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
12 changed files with 367 additions and 431 deletions

1
Cargo.lock generated
View File

@ -1920,6 +1920,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"frontend-api",
"pathdiff",
"prometeu-abi",
"prometeu-analysis",

View File

@ -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)]

View File

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

View File

@ -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 {}

View File

@ -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<LinkError> for BuildError {
}
}
pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget) -> Result<BuildResult, BuildError> {
pub fn build_from_graph(graph: &ResolvedGraph, target: BuildTarget, fe: &dyn CanonFrontend) -> Result<BuildResult, BuildError> {
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");

View File

@ -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<u32>,
pub is_host: bool,
pub ty: Option<PbsType>,
pub ty: Option<TypeRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
@ -105,204 +108,15 @@ impl From<crate::common::diagnostics::DiagnosticBundle> for CompileError {
}
}
struct ProjectModuleProvider {
modules: HashMap<String, ModuleSymbols>,
}
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<ProjectId, CompiledModule>,
file_manager: &mut FileManager,
fe: &dyn CanonFrontend,
_file_manager: &mut FileManager,
) -> Result<CompiledModule, CompileError> {
let mut interner = NameInterner::new();
// 1) Parse all files; collect+merge symbols by module_path
let mut module_symbols_map: HashMap<String, ModuleSymbols> = 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<String, ModuleSymbols> = 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<String, Vec<frontend_api::types::ExportItem>> = 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<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 {
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<crate::ir_vm::types::ConstId> = 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<id>"
// - Service method: "Service.method#sig<id>"
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"

View File

@ -105,7 +105,9 @@ pub fn compile_ext(project_dir: &Path, explain_deps: bool) -> Result<Compilation
let graph = graph_res.map_err(|e| anyhow::anyhow!("Dependency resolution failed: {}", e))?;
let build_result = crate::building::orchestrator::build_from_graph(&graph, crate::building::plan::BuildTarget::Main)
// Use PBS Frontend adapter implementing the canonical FE contract
let fe = crate::frontends::pbs::adapter::PbsFrontendAdapter;
let build_result = crate::building::orchestrator::build_from_graph(&graph, crate::building::plan::BuildTarget::Main, &fe)
.map_err(|e| anyhow::anyhow!("Build failed: {}", e))?;
let module = BytecodeModule::from(build_result.image.clone());

View File

@ -0,0 +1,137 @@
use crate::frontends::pbs::{parser::Parser, SymbolCollector, ModuleSymbols, Resolver, ModuleProvider, Lowerer};
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};
use prometeu_analysis::NameInterner;
use std::path::Path;
/// Adapter implementing the canonical Frontend contract for PBS.
pub struct PbsFrontendAdapter;
impl CanonFrontend for PbsFrontendAdapter {
fn parse_and_analyze(&self, entry_path: &str) -> FrontendUnit {
// Minimal translation: run existing PBS pipeline and package results into canonical unit.
let path = Path::new(entry_path);
let mut diags: Vec<CanonDiagnostic> = 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<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));
}
}
}
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),
}
}
}

View File

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

View File

@ -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<DiagnosticBundle> 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<String, Vec<Symbol>>,
}
// 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<ProjectSources, SourceError> {
let project_dir = project_dir.canonicalize()?;
@ -113,52 +105,7 @@ fn discover_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<(
Ok(())
}
pub fn build_exports(module_dir: &Path, file_manager: &mut FileManager) -> Result<ExportTable, SourceError> {
let mut symbols: HashMap<String, Vec<Symbol>> = 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<Symbol> 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.
}

View File

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

View File

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