485 lines
18 KiB
Rust
485 lines
18 KiB
Rust
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::Span;
|
|
use crate::deps::resolver::ProjectKey;
|
|
use crate::semantics::export_surface::ExportSurfaceKind;
|
|
use prometeu_analysis::ids::ProjectId;
|
|
use prometeu_bytecode::{ConstantPoolEntry, DebugInfo, FunctionMeta};
|
|
use serde::{Deserialize, Serialize};
|
|
use frontend_api::types::{TypeRef, ExportItem};
|
|
use frontend_api::traits::Frontend as CanonFrontend;
|
|
use std::collections::{BTreeMap, HashMap};
|
|
|
|
// 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 {
|
|
pub module_path: String,
|
|
pub item: ExportItem,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExportMetadata {
|
|
pub func_idx: Option<u32>,
|
|
pub is_host: bool,
|
|
pub ty: Option<TypeRef>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
|
pub struct ImportKey {
|
|
pub dep_alias: String,
|
|
pub module_path: String,
|
|
pub symbol_name: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ImportMetadata {
|
|
pub key: ImportKey,
|
|
pub relocation_pcs: Vec<u32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CompiledModule {
|
|
pub project_id: ProjectId,
|
|
pub project_key: ProjectKey,
|
|
pub target: BuildTarget,
|
|
pub exports: BTreeMap<ExportKey, ExportMetadata>,
|
|
pub imports: Vec<ImportMetadata>,
|
|
pub const_pool: Vec<ConstantPoolEntry>,
|
|
pub code: Vec<u8>,
|
|
pub function_metas: Vec<FunctionMeta>,
|
|
pub debug_info: Option<DebugInfo>,
|
|
pub symbols: Vec<crate::common::symbols::Symbol>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum CompileError {
|
|
Frontend(DiagnosticBundle),
|
|
DuplicateExport {
|
|
symbol: String,
|
|
first_dep: String,
|
|
second_dep: String,
|
|
},
|
|
Io(std::io::Error),
|
|
Internal(String),
|
|
}
|
|
|
|
impl std::fmt::Display for CompileError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
CompileError::Frontend(d) => write!(f, "Frontend error: {:?}", d),
|
|
CompileError::DuplicateExport {
|
|
symbol,
|
|
first_dep,
|
|
second_dep,
|
|
} => write!(
|
|
f,
|
|
"duplicate export: symbol `{}`\n first defined in dependency `{}`\n again defined in dependency `{}`",
|
|
symbol, first_dep, second_dep
|
|
),
|
|
CompileError::Io(e) => write!(f, "IO error: {}", e),
|
|
CompileError::Internal(s) => write!(f, "Internal error: {}", s),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for CompileError {}
|
|
|
|
impl From<std::io::Error> for CompileError {
|
|
fn from(e: std::io::Error) -> Self {
|
|
CompileError::Io(e)
|
|
}
|
|
}
|
|
|
|
impl From<crate::common::diagnostics::DiagnosticBundle> for CompileError {
|
|
fn from(d: crate::common::diagnostics::DiagnosticBundle) -> Self {
|
|
CompileError::Frontend(d)
|
|
}
|
|
}
|
|
|
|
// Note: PBS ModuleProvider/ModuleSymbols are no longer used at the backend boundary.
|
|
|
|
pub fn compile_project(
|
|
step: BuildStep,
|
|
dep_modules: &HashMap<ProjectId, CompiledModule>,
|
|
fe: &dyn CanonFrontend,
|
|
_file_manager: &mut FileManager,
|
|
) -> Result<CompiledModule, CompileError> {
|
|
// 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();
|
|
|
|
// Origin module_path per appended function
|
|
let mut combined_func_origins: Vec<String> = Vec::new();
|
|
|
|
let insert_const =
|
|
|pool: &mut crate::ir_core::ConstPool, val: &crate::ir_core::ConstantValue| -> crate::ir_vm::types::ConstId {
|
|
let new_id = pool.insert(val.clone());
|
|
crate::ir_vm::types::ConstId(new_id.0)
|
|
};
|
|
|
|
// Map: module_path → FE exports for that module
|
|
let mut fe_exports_per_module: HashMap<String, Vec<frontend_api::types::ExportItem>> = HashMap::new();
|
|
|
|
// Build dependency synthetic export keys and detect cross-dependency duplicates upfront
|
|
use std::collections::HashSet;
|
|
#[derive(Hash, Eq, PartialEq)]
|
|
struct DepKey(String, ExportItem); // (module_path, item)
|
|
let mut dep_seen: HashSet<DepKey> = HashSet::new();
|
|
for (alias, project_id) in &step.deps {
|
|
if let Some(compiled) = dep_modules.get(project_id) {
|
|
for (key, _meta) in &compiled.exports {
|
|
// Track using canonical item keyed by module path; alias variations only for display/conflict reporting.
|
|
let synthetic_paths = [
|
|
format!("{}/{}", alias, key.module_path),
|
|
format!("@{}:{}", alias, key.module_path),
|
|
];
|
|
for sp in synthetic_paths {
|
|
let k = DepKey(sp.clone(), key.item.clone());
|
|
if !dep_seen.insert(k) {
|
|
return Err(CompileError::DuplicateExport {
|
|
symbol: format!("{:?}", key.item),
|
|
first_dep: alias.clone(),
|
|
second_dep: alias.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
// Conflict with dependency synthetic exports?
|
|
let dep_key = DepKey(module_path.clone(), it.clone());
|
|
if dep_seen.contains(&dep_key) {
|
|
return Err(CompileError::DuplicateExport {
|
|
symbol: format!("{:?}", it),
|
|
first_dep: "dependency".to_string(),
|
|
second_dep: "local".to_string(),
|
|
});
|
|
}
|
|
|
|
// Local duplicate within same module?
|
|
let already = entry.iter().any(|e| e == &it);
|
|
if already {
|
|
return Err(CompileError::Frontend(
|
|
DiagnosticBundle::error(
|
|
"E_RESOLVE_DUPLICATE_SYMBOL",
|
|
format!("Duplicate symbol '{:?}' in module '{}'", it, 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());
|
|
for c in &vm_module.const_pool.constants {
|
|
const_map.push(insert_const(&mut combined_vm.const_pool, c));
|
|
}
|
|
|
|
// Append functions; remap PushConst ids safely
|
|
for mut f in vm_module.functions.into_iter() {
|
|
for instr in &mut f.body {
|
|
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);
|
|
instr.kind = crate::ir_vm::instr::InstrKind::PushConst(mapped);
|
|
}
|
|
}
|
|
|
|
combined_func_origins.push(module_path.clone());
|
|
combined_vm.functions.push(f);
|
|
}
|
|
}
|
|
|
|
let fragments = emit_fragments(&combined_vm)
|
|
.map_err(|e| CompileError::Internal(format!("Emission error: {}", e)))?;
|
|
|
|
// Ensure function metas reflect final slots info
|
|
let mut fixed_function_metas = fragments.functions.clone();
|
|
for (i, fm) in fixed_function_metas.iter_mut().enumerate() {
|
|
if let Some(vm_func) = combined_vm.functions.get(i) {
|
|
fm.param_slots = vm_func.param_slots;
|
|
fm.local_slots = vm_func.local_slots;
|
|
fm.return_slots = vm_func.return_slots;
|
|
}
|
|
}
|
|
|
|
// 2) Collect exports from FE contract, map to VM function indices
|
|
let mut exports = BTreeMap::new();
|
|
|
|
for (module_path, items) in &fe_exports_per_module {
|
|
for item in items {
|
|
match item {
|
|
ExportItem::Type { name } => {
|
|
exports.insert(
|
|
ExportKey { module_path: module_path.clone(), item: item.clone() },
|
|
ExportMetadata { func_idx: None, is_host: false, ty: Some(TypeRef(symbol_name_hash(name.as_str()))) },
|
|
);
|
|
}
|
|
ExportItem::Service { name: _ } => {
|
|
// Service owner export (no functions synthesized here)
|
|
exports.insert(
|
|
ExportKey { module_path: module_path.clone(), item: item.clone() },
|
|
ExportMetadata { func_idx: None, is_host: false, ty: None },
|
|
);
|
|
}
|
|
ExportItem::Function { fn_key } => {
|
|
// Map function to VM function index by name + signature id
|
|
for (i, f) in combined_vm.functions.iter().enumerate() {
|
|
if combined_func_origins.get(i).map(|s| s.as_str()) != Some(module_path.as_str()) { continue; }
|
|
if f.name != fn_key.name.as_str() { continue; }
|
|
if f.sig.0 != fn_key.sig.0 { continue; }
|
|
exports.insert(
|
|
ExportKey { module_path: module_path.clone(), item: item.clone() },
|
|
ExportMetadata { func_idx: Some(i as u32), is_host: false, ty: Some(TypeRef(f.sig.0 as u32)) },
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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"
|
|
// NOTE: assumes dbg.function_names aligns with functions order.
|
|
let mut enriched = Vec::new();
|
|
for (i, (fid, name)) in dbg.function_names.clone().into_iter().enumerate() {
|
|
if let Some(meta) = fixed_function_metas.get(i) {
|
|
enriched.push((fid, format!("{}@{}+{}", name, meta.code_offset, meta.code_len)));
|
|
} else {
|
|
enriched.push((fid, name));
|
|
}
|
|
}
|
|
if !enriched.is_empty() {
|
|
dbg.function_names = enriched;
|
|
}
|
|
}
|
|
|
|
// 5) Collect imports from unresolved labels
|
|
let mut imports = Vec::new();
|
|
for (label, pcs) in fragments.unresolved_labels {
|
|
if !label.starts_with('@') {
|
|
continue;
|
|
}
|
|
|
|
// Format: @dep_alias::module_path:symbol_name
|
|
let parts: Vec<&str> = label[1..].splitn(2, "::").collect();
|
|
if parts.len() != 2 {
|
|
continue;
|
|
}
|
|
|
|
let dep_alias = parts[0].to_string();
|
|
let rest = parts[1];
|
|
|
|
// Split from the right once: "...:<symbol_name>"
|
|
let sub_parts: Vec<&str> = rest.rsplitn(2, ':').collect();
|
|
if sub_parts.len() != 2 {
|
|
continue;
|
|
}
|
|
|
|
let symbol_name = sub_parts[0].to_string();
|
|
let module_path = sub_parts[1].to_string();
|
|
|
|
imports.push(ImportMetadata {
|
|
key: ImportKey {
|
|
dep_alias,
|
|
module_path,
|
|
symbol_name,
|
|
},
|
|
relocation_pcs: pcs,
|
|
});
|
|
}
|
|
|
|
Ok(CompiledModule {
|
|
project_id: step.project_id,
|
|
project_key: step.project_key,
|
|
target: step.target,
|
|
exports,
|
|
imports,
|
|
const_pool: fragments.const_pool,
|
|
code: fragments.code,
|
|
function_metas: fixed_function_metas,
|
|
debug_info,
|
|
symbols: project_symbols,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tempfile::tempdir;
|
|
use crate::frontends::pbs::adapter::PbsFrontendAdapter;
|
|
use frontend_api::types::{ItemName, ExportItem, CanonicalFnKey};
|
|
|
|
#[test]
|
|
fn test_compile_root_only_project() {
|
|
let dir = tempdir().unwrap();
|
|
let project_dir = dir.path().to_path_buf();
|
|
|
|
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
|
|
|
|
// NOTE: ajuste de sintaxe: seu PBS entrypoint atual é `fn frame(): void` dentro de main.pbs
|
|
// e "mod fn frame" pode não ser válido. Mantive o essencial.
|
|
let main_code = r#"
|
|
pub declare struct Vec2(x: int, y: int)
|
|
|
|
fn add(a: int, b: int): int {
|
|
return a + b;
|
|
}
|
|
|
|
fn frame(): void {
|
|
let x = add(1, 2);
|
|
}
|
|
"#;
|
|
|
|
fs::write(project_dir.join("src/main/modules/main.pbs"), main_code).unwrap();
|
|
|
|
let project_key = ProjectKey {
|
|
name: "root".to_string(),
|
|
version: "0.1.0".to_string(),
|
|
};
|
|
let project_id = ProjectId(0);
|
|
|
|
let step = BuildStep {
|
|
project_id,
|
|
project_key: project_key.clone(),
|
|
project_dir: project_dir.clone(),
|
|
target: BuildTarget::Main,
|
|
sources: vec![PathBuf::from("src/main/modules/main.pbs")],
|
|
deps: BTreeMap::new(),
|
|
};
|
|
|
|
let mut file_manager = FileManager::new();
|
|
let fe = PbsFrontendAdapter;
|
|
let compiled =
|
|
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);
|
|
|
|
// Vec2 should be exported (canonical)
|
|
let vec2_key = ExportKey {
|
|
module_path: "".to_string(),
|
|
item: ExportItem::Type { name: ItemName::new("Vec2").unwrap() },
|
|
};
|
|
assert!(compiled.exports.contains_key(&vec2_key));
|
|
}
|
|
|
|
#[test]
|
|
fn test_service_method_export_qualified() {
|
|
let dir = tempdir().unwrap();
|
|
let project_dir = dir.path().to_path_buf();
|
|
|
|
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
|
|
|
|
let main_code = r#"
|
|
pub service Log {
|
|
fn debug(msg: string): void {
|
|
}
|
|
}
|
|
"#;
|
|
|
|
fs::write(project_dir.join("src/main/modules/main.pbs"), main_code).unwrap();
|
|
|
|
let project_key = ProjectKey {
|
|
name: "root".to_string(),
|
|
version: "0.1.0".to_string(),
|
|
};
|
|
let project_id = ProjectId(0);
|
|
|
|
let step = BuildStep {
|
|
project_id,
|
|
project_key: project_key.clone(),
|
|
project_dir: project_dir.clone(),
|
|
target: BuildTarget::Main,
|
|
sources: vec![PathBuf::from("src/main/modules/main.pbs")],
|
|
deps: BTreeMap::new(),
|
|
};
|
|
|
|
let mut file_manager = FileManager::new();
|
|
let fe = PbsFrontendAdapter;
|
|
let compiled = compile_project(step, &HashMap::new(), &fe, &mut file_manager)
|
|
.expect("Failed to compile project");
|
|
|
|
// Find a function export with canonical fn key: owner=Log, name=debug
|
|
let mut found = false;
|
|
for (key, _meta) in &compiled.exports {
|
|
if let ExportItem::Function { fn_key } = &key.item {
|
|
if fn_key.owner.as_ref().map(|n| n.as_str()) == Some("Log") && fn_key.name.as_str() == "debug" {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(found, "Expected an export with canonical fn key owner=Log, name=debug but not found. Exports: {:?}", compiled.exports.keys().collect::<Vec<_>>());
|
|
}
|
|
}
|