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 = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"frontend-api",
|
||||
"pathdiff",
|
||||
"prometeu-abi",
|
||||
"prometeu-analysis",
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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());
|
||||
|
||||
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 contracts;
|
||||
pub mod frontend;
|
||||
pub mod adapter;
|
||||
|
||||
pub use collector::SymbolCollector;
|
||||
pub use lexer::Lexer;
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user