pr 03.02
This commit is contained in:
parent
6a4f7ea773
commit
df8c0db6eb
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1920,6 +1920,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"frontend-api",
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"prometeu-abi",
|
"prometeu-abi",
|
||||||
"prometeu-analysis",
|
"prometeu-analysis",
|
||||||
|
|||||||
@ -178,6 +178,17 @@ impl CanonicalFnKey {
|
|||||||
pub fn new(import: ImportRef, arity: u16) -> Self { Self { import, arity } }
|
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.
|
/// Diagnostic severity.
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
|||||||
@ -17,6 +17,7 @@ include = ["../../VERSION.txt"]
|
|||||||
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
||||||
prometeu-abi = { path = "../prometeu-abi" }
|
prometeu-abi = { path = "../prometeu-abi" }
|
||||||
prometeu-analysis = { path = "../prometeu-analysis" }
|
prometeu-analysis = { path = "../prometeu-analysis" }
|
||||||
|
frontend-api = { path = "../frontend-api", features = ["serde"] }
|
||||||
clap = { version = "4.5.54", features = ["derive"] }
|
clap = { version = "4.5.54", features = ["derive"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
|||||||
@ -2,3 +2,12 @@ pub mod plan;
|
|||||||
pub mod output;
|
pub mod output;
|
||||||
pub mod linker;
|
pub mod linker;
|
||||||
pub mod orchestrator;
|
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 {}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
use crate::building::linker::{LinkError, Linker};
|
use crate::building::linker::{LinkError, Linker};
|
||||||
use crate::building::output::{compile_project, CompileError};
|
use crate::building::output::{compile_project, CompileError};
|
||||||
|
use frontend_api::traits::Frontend as CanonFrontend;
|
||||||
use crate::building::plan::{BuildPlan, BuildTarget};
|
use crate::building::plan::{BuildPlan, BuildTarget};
|
||||||
use crate::common::diagnostics::DiagnosticBundle;
|
use crate::common::diagnostics::DiagnosticBundle;
|
||||||
use crate::common::files::FileManager;
|
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 plan = BuildPlan::from_graph(graph, target);
|
||||||
let mut compiled_modules = HashMap::new();
|
let mut compiled_modules = HashMap::new();
|
||||||
let mut modules_in_order = Vec::new();
|
let mut modules_in_order = Vec::new();
|
||||||
let mut file_manager = FileManager::new();
|
let mut file_manager = FileManager::new();
|
||||||
|
|
||||||
for step in &plan.steps {
|
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());
|
compiled_modules.insert(step.project_id.clone(), compiled.clone());
|
||||||
modules_in_order.push(compiled);
|
modules_in_order.push(compiled);
|
||||||
}
|
}
|
||||||
@ -154,6 +155,7 @@ mod tests {
|
|||||||
use crate::sources::discover;
|
use crate::sources::discover;
|
||||||
use prometeu_analysis::ids::ProjectId;
|
use prometeu_analysis::ids::ProjectId;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use crate::frontends::pbs::adapter::PbsFrontendAdapter;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@ -201,7 +203,8 @@ mod tests {
|
|||||||
make_minimal_manifest(&project_dir);
|
make_minimal_manifest(&project_dir);
|
||||||
|
|
||||||
let graph = build_single_node_graph(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());
|
assert!(res.is_err());
|
||||||
let err = res.err().unwrap();
|
let err = res.err().unwrap();
|
||||||
match err {
|
match err {
|
||||||
@ -233,7 +236,8 @@ mod tests {
|
|||||||
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
|
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
|
||||||
|
|
||||||
let graph = build_single_node_graph(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());
|
assert!(res.is_err());
|
||||||
let err = res.err().unwrap();
|
let err = res.err().unwrap();
|
||||||
match err {
|
match err {
|
||||||
@ -262,7 +266,8 @@ mod tests {
|
|||||||
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
|
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
|
||||||
|
|
||||||
let graph = build_single_node_graph(project_dir);
|
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
|
// Locate function by name -> function index
|
||||||
let di = res.image.debug_info.as_ref().expect("debug info");
|
let di = res.image.debug_info.as_ref().expect("debug info");
|
||||||
|
|||||||
@ -2,22 +2,25 @@ use crate::backend::emit_fragments;
|
|||||||
use crate::building::plan::{BuildStep, BuildTarget};
|
use crate::building::plan::{BuildStep, BuildTarget};
|
||||||
use crate::common::diagnostics::DiagnosticBundle;
|
use crate::common::diagnostics::DiagnosticBundle;
|
||||||
use crate::common::files::FileManager;
|
use crate::common::files::FileManager;
|
||||||
use crate::common::spans::{FileId, Span};
|
use crate::common::spans::Span;
|
||||||
use crate::deps::resolver::ProjectKey;
|
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 crate::semantics::export_surface::ExportSurfaceKind;
|
||||||
use prometeu_analysis::ids::ProjectId;
|
use prometeu_analysis::ids::ProjectId;
|
||||||
use prometeu_analysis::NameInterner;
|
|
||||||
use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta};
|
use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use frontend_api::types::TypeRef;
|
||||||
|
use frontend_api::traits::Frontend as CanonFrontend;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct ExportKey {
|
pub struct ExportKey {
|
||||||
@ -30,7 +33,7 @@ pub struct ExportKey {
|
|||||||
pub struct ExportMetadata {
|
pub struct ExportMetadata {
|
||||||
pub func_idx: Option<u32>,
|
pub func_idx: Option<u32>,
|
||||||
pub is_host: bool,
|
pub is_host: bool,
|
||||||
pub ty: Option<PbsType>,
|
pub ty: Option<TypeRef>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
@ -105,204 +108,15 @@ impl From<crate::common::diagnostics::DiagnosticBundle> for CompileError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProjectModuleProvider {
|
// Note: PBS ModuleProvider/ModuleSymbols are no longer used at the backend boundary.
|
||||||
modules: HashMap<String, ModuleSymbols>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModuleProvider for ProjectModuleProvider {
|
|
||||||
fn get_module_symbols(&self, from_path: &str) -> Option<&ModuleSymbols> {
|
|
||||||
self.modules.get(from_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compile_project(
|
pub fn compile_project(
|
||||||
step: BuildStep,
|
step: BuildStep,
|
||||||
dep_modules: &HashMap<ProjectId, CompiledModule>,
|
dep_modules: &HashMap<ProjectId, CompiledModule>,
|
||||||
file_manager: &mut FileManager,
|
fe: &dyn CanonFrontend,
|
||||||
|
_file_manager: &mut FileManager,
|
||||||
) -> Result<CompiledModule, CompileError> {
|
) -> Result<CompiledModule, CompileError> {
|
||||||
let mut interner = NameInterner::new();
|
// 1) FE-driven analysis per source → gather VM IR modules
|
||||||
|
|
||||||
// 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)
|
|
||||||
let mut combined_vm = crate::ir_vm::Module::new(step.project_key.name.clone());
|
let mut combined_vm = crate::ir_vm::Module::new(step.project_key.name.clone());
|
||||||
combined_vm.const_pool = crate::ir_core::ConstPool::new();
|
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)
|
crate::ir_vm::types::ConstId(new_id.0)
|
||||||
};
|
};
|
||||||
|
|
||||||
for (module_path, parsed) in &parsed_files {
|
// Map: module_path → FE exports for that module
|
||||||
let ms = module_symbols_map.get(module_path).unwrap();
|
let mut fe_exports_per_module: HashMap<String, Vec<frontend_api::types::ExportItem>> = HashMap::new();
|
||||||
let imported = file_imported_symbols.get(module_path).unwrap();
|
|
||||||
|
|
||||||
let lowerer = Lowerer::new(&parsed.arena, ms, imported, &module_provider, &interner);
|
// Build dependency synthetic export keys and detect cross-dependency duplicates upfront
|
||||||
let program = lowerer.lower_file(parsed.root, module_path)?;
|
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)
|
for source_rel in &step.sources {
|
||||||
.map_err(|e| CompileError::Internal(format!("Lowering error ({}): {}", module_path, e)))?;
|
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
|
// 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());
|
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
|
// Append functions; remap PushConst ids safely
|
||||||
for mut f in vm_module.functions.into_iter() {
|
for mut f in vm_module.functions.into_iter() {
|
||||||
for instr in &mut f.body {
|
for instr in &mut f.body {
|
||||||
// safest: clone the kind and rewrite if needed
|
|
||||||
let kind_clone = instr.kind.clone();
|
let kind_clone = instr.kind.clone();
|
||||||
if let crate::ir_vm::instr::InstrKind::PushConst(old_id) = 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);
|
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();
|
let mut exports = BTreeMap::new();
|
||||||
|
|
||||||
for (module_path, ms) in &module_symbols_map {
|
for (module_path, items) in &fe_exports_per_module {
|
||||||
// Type exports (simple name)
|
for item in items {
|
||||||
for list in ms.type_symbols.symbols.values() {
|
let kind = match item.kind {
|
||||||
for sym in list {
|
frontend_api::types::ExportKind::Function => ExportSurfaceKind::Function,
|
||||||
if sym.visibility != Visibility::Pub {
|
frontend_api::types::ExportKind::Service => ExportSurfaceKind::Service,
|
||||||
continue;
|
frontend_api::types::ExportKind::Type => ExportSurfaceKind::DeclareType,
|
||||||
}
|
frontend_api::types::ExportKind::Const => continue,
|
||||||
let Some(surface_kind) = ExportSurfaceKind::from_symbol_kind(sym.kind) else {
|
|
||||||
continue;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if kind == ExportSurfaceKind::DeclareType || kind == ExportSurfaceKind::Service {
|
||||||
exports.insert(
|
exports.insert(
|
||||||
ExportKey {
|
ExportKey {
|
||||||
module_path: module_path.clone(),
|
module_path: module_path.clone(),
|
||||||
symbol_name: interner.resolve(sym.name).to_string(),
|
symbol_name: item.name.as_str().to_string(),
|
||||||
kind: surface_kind,
|
kind,
|
||||||
},
|
|
||||||
ExportMetadata {
|
|
||||||
func_idx: None,
|
|
||||||
is_host: sym.is_host,
|
|
||||||
ty: sym.ty.clone(),
|
|
||||||
},
|
},
|
||||||
|
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();
|
||||||
|
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)) },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
// Value exports: only by canonical signature "name#sigN"
|
// Map function export to VM function index by simple name (strip qualification)
|
||||||
for list in ms.value_symbols.symbols.values() {
|
let name_full = item.name.as_str();
|
||||||
for sym in list {
|
let expected_vm_name = name_full.split('.').last().unwrap_or(name_full);
|
||||||
if sym.visibility != Visibility::Pub {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
for (i, f) in combined_vm.functions.iter().enumerate() {
|
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()) {
|
if combined_func_origins.get(i).map(|s| s.as_str()) != Some(module_path.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
@ -413,54 +311,39 @@ pub fn compile_project(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Canonical export key name:
|
let sig_name = format!("{}#sig{}", name_full, f.sig.0);
|
||||||
// - 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
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
exports.insert(
|
exports.insert(
|
||||||
ExportKey {
|
ExportKey { module_path: module_path.clone(), symbol_name: sig_name, kind },
|
||||||
module_path: module_path.clone(),
|
ExportMetadata { func_idx: Some(i as u32), is_host: false, ty: Some(TypeRef(f.sig.0 as u32)) },
|
||||||
symbol_name: sig_name,
|
|
||||||
kind: surface_kind,
|
|
||||||
},
|
|
||||||
ExportMetadata {
|
|
||||||
func_idx: Some(i as u32),
|
|
||||||
is_host: sym.is_host,
|
|
||||||
ty: Some(ty),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6) Collect symbols for analysis (LSP, etc.)
|
// 3) Collect symbols for analysis (LSP, etc.) — minimal fallback from debug_info
|
||||||
let project_symbols = crate::common::symbols::collect_symbols(
|
let mut project_symbols = Vec::new();
|
||||||
&step.project_key.name,
|
if let Some(di) = &fragments.debug_info {
|
||||||
&module_symbols_map,
|
// Create at least a symbol for entry point or first function
|
||||||
file_manager,
|
if let Some((_, name)) = di.function_names.first() {
|
||||||
&interner,
|
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();
|
let mut debug_info = fragments.debug_info.clone();
|
||||||
if let Some(dbg) = debug_info.as_mut() {
|
if let Some(dbg) = debug_info.as_mut() {
|
||||||
// annotate function names with "@offset+len"
|
// 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();
|
let mut imports = Vec::new();
|
||||||
for (label, pcs) in fragments.unresolved_labels {
|
for (label, pcs) in fragments.unresolved_labels {
|
||||||
if !label.starts_with('@') {
|
if !label.starts_with('@') {
|
||||||
@ -533,6 +416,7 @@ mod tests {
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
use crate::frontends::pbs::adapter::PbsFrontendAdapter;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_compile_root_only_project() {
|
fn test_compile_root_only_project() {
|
||||||
@ -573,8 +457,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
let mut file_manager = FileManager::new();
|
||||||
|
let fe = PbsFrontendAdapter;
|
||||||
let compiled =
|
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.project_id, project_id);
|
||||||
assert_eq!(compiled.target, BuildTarget::Main);
|
assert_eq!(compiled.target, BuildTarget::Main);
|
||||||
@ -620,7 +505,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
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");
|
.expect("Failed to compile project");
|
||||||
|
|
||||||
// Find a function export with qualified method name prefix "Log.debug#sig"
|
// Find a function export with qualified method name prefix "Log.debug#sig"
|
||||||
|
|||||||
@ -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 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))?;
|
.map_err(|e| anyhow::anyhow!("Build failed: {}", e))?;
|
||||||
|
|
||||||
let module = BytecodeModule::from(build_result.image.clone());
|
let module = BytecodeModule::from(build_result.image.clone());
|
||||||
|
|||||||
137
crates/prometeu-compiler/src/frontends/pbs/adapter.rs
Normal file
137
crates/prometeu-compiler/src/frontends/pbs/adapter.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ pub mod typecheck;
|
|||||||
pub mod lowering;
|
pub mod lowering;
|
||||||
pub mod contracts;
|
pub mod contracts;
|
||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
|
pub mod adapter;
|
||||||
|
|
||||||
pub use collector::SymbolCollector;
|
pub use collector::SymbolCollector;
|
||||||
pub use lexer::Lexer;
|
pub use lexer::Lexer;
|
||||||
|
|||||||
@ -1,11 +1,7 @@
|
|||||||
use crate::common::diagnostics::DiagnosticBundle;
|
use crate::common::diagnostics::DiagnosticBundle;
|
||||||
use crate::common::files::FileManager;
|
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 crate::manifest::{load_manifest, ManifestKind};
|
||||||
use prometeu_analysis::NameInterner;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@ -55,12 +51,8 @@ impl From<DiagnosticBundle> for SourceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
// NOTE: Export surface discovery is a Frontend responsibility now.
|
||||||
pub struct ExportTable {
|
// This module is intentionally discovery-only (file listing + main rules).
|
||||||
/// Exported symbols keyed by simple name.
|
|
||||||
/// For overloads, multiple symbols may exist under the same key.
|
|
||||||
pub symbols: HashMap<String, Vec<Symbol>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn discover(project_dir: &Path) -> Result<ProjectSources, SourceError> {
|
pub fn discover(project_dir: &Path) -> Result<ProjectSources, SourceError> {
|
||||||
let project_dir = project_dir.canonicalize()?;
|
let project_dir = project_dir.canonicalize()?;
|
||||||
@ -113,52 +105,7 @@ fn discover_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> std::io::Result<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_exports(module_dir: &Path, file_manager: &mut FileManager) -> Result<ExportTable, SourceError> {
|
// build_exports removed: export collection belongs to Frontend (frontend-api contract).
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@ -266,19 +213,5 @@ mod tests {
|
|||||||
assert!(sources.files.contains(&util_pbs));
|
assert!(sources.files.contains(&util_pbs));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
// No export-surface tests here; handled by Frontend implementations.
|
||||||
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"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ use prometeu_compiler::building::plan::{BuildStep, BuildTarget};
|
|||||||
use prometeu_compiler::common::files::FileManager;
|
use prometeu_compiler::common::files::FileManager;
|
||||||
use prometeu_compiler::deps::resolver::ProjectKey;
|
use prometeu_compiler::deps::resolver::ProjectKey;
|
||||||
use prometeu_compiler::semantics::export_surface::ExportSurfaceKind;
|
use prometeu_compiler::semantics::export_surface::ExportSurfaceKind;
|
||||||
|
use prometeu_compiler::frontends::pbs::adapter::PbsFrontendAdapter;
|
||||||
use prometeu_analysis::ids::ProjectId;
|
use prometeu_analysis::ids::ProjectId;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@ -66,7 +67,8 @@ fn test_local_vs_dependency_conflict() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
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 {
|
match result {
|
||||||
Err(CompileError::DuplicateExport { symbol, .. }) => {
|
Err(CompileError::DuplicateExport { symbol, .. }) => {
|
||||||
@ -153,7 +155,8 @@ fn test_aliased_dependency_conflict() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
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 {
|
match result {
|
||||||
Err(CompileError::DuplicateExport { symbol, .. }) => {
|
Err(CompileError::DuplicateExport { symbol, .. }) => {
|
||||||
@ -189,7 +192,8 @@ fn test_mixed_main_test_modules() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
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
|
// Both should be in exports with normalized paths
|
||||||
assert!(compiled.exports.keys().any(|k| k.module_path == "math"));
|
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 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"
|
// 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 == "Gfx"));
|
||||||
@ -251,7 +256,8 @@ fn test_duplicate_symbol_in_same_module_different_files() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
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());
|
assert!(result.is_err());
|
||||||
// Should be a frontend error (duplicate symbol)
|
// Should be a frontend error (duplicate symbol)
|
||||||
}
|
}
|
||||||
@ -280,7 +286,8 @@ fn test_root_module_merging() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut file_manager = FileManager::new();
|
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 ""
|
// 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 == "Main"));
|
||||||
|
|||||||
@ -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)
|
## PR-03.02 — Ban PBS leakage at the BE boundary (dependency & import hygiene)
|
||||||
|
|
||||||
### Title
|
### Title
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user