bquarkz 565fc0e451 dev/pbs (#8)
Co-authored-by: Nilton Constantino <nilton.constantino@visma.com>
Reviewed-on: #8
2026-02-03 15:28:30 +00:00

491 lines
18 KiB
Rust

//! # Compiler Orchestration
//!
//! This module provides the high-level API for triggering the compilation process.
//! It handles the transition between different compiler phases: Frontend -> IR -> Backend.
use crate::backend;
use crate::common::config::ProjectConfig;
use crate::common::symbols::{DebugSymbol, RawSymbol, SymbolsFile, ProjectSymbols};
use crate::common::files::FileManager;
use crate::common::spans::Span;
use anyhow::Result;
use prometeu_bytecode::BytecodeModule;
use std::path::Path;
/// The result of a successful compilation process.
/// It contains the final binary and the metadata needed for debugging.
#[derive(Debug)]
pub struct CompilationUnit {
/// The raw binary data formatted as Prometeu ByteCode (PBC).
/// This is what gets written to a `.pbc` file.
pub rom: Vec<u8>,
/// The list of debug symbols discovered during compilation.
/// These are used to map bytecode offsets back to source code locations.
pub raw_symbols: Vec<RawSymbol>,
/// The file manager containing all source files used during compilation.
pub file_manager: FileManager,
/// The high-level project symbols for LSP and other tools.
pub project_symbols: Vec<ProjectSymbols>,
/// The name of the root project.
pub root_project: String,
}
impl CompilationUnit {
/// Writes the compilation results (PBC binary, disassembly, and symbols) to the disk.
///
/// # Arguments
/// * `out` - The base path for the output `.pbc` file.
/// * `emit_disasm` - If true, a `.disasm` file will be created next to the output.
/// * `emit_symbols` - If true, a `.json` symbols file will be created next to the output.
pub fn export(&self, out: &Path, emit_disasm: bool, emit_symbols: bool) -> Result<()> {
let mut debug_symbols = Vec::new();
for raw in &self.raw_symbols {
let path = self.file_manager.get_path(raw.span.file_id)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("file_{}", raw.span.file_id));
let (line, col) = self.file_manager.lookup_pos(raw.span.file_id, raw.span.start);
debug_symbols.push(DebugSymbol {
pc: raw.pc,
file: path,
line,
col,
});
}
let lsp_symbols = SymbolsFile {
schema_version: 0,
compiler_version: "0.1.0".to_string(), // TODO: use crate version
root_project: self.root_project.clone(),
projects: self.project_symbols.clone(),
};
let artifacts = backend::artifacts::Artifacts::new(
self.rom.clone(),
debug_symbols,
lsp_symbols,
);
artifacts.export(out, emit_disasm, emit_symbols)
}
}
pub fn compile(project_dir: &Path) -> Result<CompilationUnit> {
compile_ext(project_dir, false)
}
pub fn compile_ext(project_dir: &Path, explain_deps: bool) -> Result<CompilationUnit> {
let config = ProjectConfig::load(project_dir)?;
if config.script_fe == "pbs" {
let graph_res = crate::deps::resolver::resolve_graph(project_dir);
if explain_deps || graph_res.is_err() {
match &graph_res {
Ok(graph) => {
println!("{}", graph.explain());
}
Err(crate::deps::resolver::ResolveError::WithTrace { trace, source }) => {
// Create a dummy graph to use its explain logic for the trace
let mut dummy_graph = crate::deps::resolver::ResolvedGraph::default();
dummy_graph.trace = trace.clone();
println!("{}", dummy_graph.explain());
eprintln!("Dependency resolution failed: {}", source);
}
Err(e) => {
eprintln!("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)
.map_err(|e| anyhow::anyhow!("Build failed: {}", e))?;
let module = BytecodeModule::from(build_result.image.clone());
let rom = module.serialize();
let mut raw_symbols = Vec::new();
if let Some(debug) = &build_result.image.debug_info {
for (pc, span) in &debug.pc_to_span {
raw_symbols.push(RawSymbol {
pc: *pc,
span: Span {
file_id: span.file_id as usize,
start: span.start,
end: span.end,
},
});
}
}
Ok(CompilationUnit {
rom,
raw_symbols,
file_manager: build_result.file_manager,
project_symbols: build_result.symbols,
root_project: config.manifest.name.clone(),
})
} else {
anyhow::bail!("Invalid frontend: {}", config.script_fe)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir_vm;
use prometeu_bytecode::disasm::disasm;
use prometeu_bytecode::opcode::OpCode;
use prometeu_bytecode::BytecodeLoader;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_invalid_frontend() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("prometeu.json");
fs::write(
config_path,
r#"{
"name": "invalid_fe",
"version": "0.1.0",
"script_fe": "invalid",
"entry": "main.pbs"
}"#,
)
.unwrap();
let result = compile(dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Invalid frontend: invalid"));
}
#[test]
fn test_compile_hip_program() {
let dir = tempdir().unwrap();
let project_dir = dir.path();
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "hip_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main/modules/main.pbs"
}"#,
).unwrap();
let code = "
fn frame(): void {
let x = alloc int;
mutate x as v {
let y = v + 1;
}
}
";
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile");
let pbc = BytecodeLoader::load(&unit.rom).expect("Failed to parse PBC");
let instrs = disasm(&pbc.code).expect("Failed to disassemble");
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
assert!(opcodes.contains(&OpCode::Alloc));
assert!(opcodes.contains(&OpCode::GateLoad));
// After PR-09, BeginMutate/EndMutate map to their respective opcodes
assert!(opcodes.contains(&OpCode::GateBeginMutate));
assert!(opcodes.contains(&OpCode::GateEndMutate));
assert!(opcodes.contains(&OpCode::Add));
assert!(opcodes.contains(&OpCode::Ret));
}
#[test]
fn test_golden_bytecode_snapshot() {
let dir = tempdir().unwrap();
let project_dir = dir.path();
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "golden_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main/modules/main.pbs"
}"#,
).unwrap();
let code = r#"
declare contract Gfx host {}
fn helper(val: int): int {
return val * 2;
}
fn main() {
Gfx.clear(0);
let x = 10;
if (x > 5) {
let y = helper(x);
}
let buf = alloc int;
mutate buf as b {
let current = b + 1;
}
}
"#;
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile");
let pbc = BytecodeLoader::load(&unit.rom).expect("Failed to parse PBC");
let instrs = disasm(&pbc.code).expect("Failed to disassemble");
let mut disasm_text = String::new();
for instr in instrs {
let operands_str = instr.operands.iter()
.map(|o| format!("{:?}", o))
.collect::<Vec<_>>()
.join(" ");
let line = if operands_str.is_empty() {
format!("{:04X} {:?}\n", instr.pc, instr.opcode)
} else {
format!("{:04X} {:?} {}\n", instr.pc, instr.opcode, operands_str.trim())
};
disasm_text.push_str(&line);
}
let expected_disasm = r#"0000 GetLocal U32(0)
0006 PushConst U32(1)
000C Mul
000E Ret
0010 PushConst U32(2)
0016 Syscall U32(4112)
001C PushConst U32(3)
0022 SetLocal U32(0)
0028 GetLocal U32(0)
002E PushConst U32(4)
0034 Gt
0036 JmpIfFalse U32(74)
003C Jmp U32(50)
0042 GetLocal U32(0)
0048 Call U32(0)
004E SetLocal U32(1)
0054 Jmp U32(80)
005A Jmp U32(80)
0060 Alloc U32(2) U32(1)
006A SetLocal U32(1)
0070 GetLocal U32(1)
0076 GateRetain
0078 SetLocal U32(2)
007E GetLocal U32(2)
0084 GateRetain
0086 GateBeginMutate
0088 GetLocal U32(2)
008E GateRetain
0090 GateLoad U32(0)
0096 SetLocal U32(3)
009C GetLocal U32(3)
00A2 PushConst U32(5)
00A8 Add
00AA SetLocal U32(4)
00B0 GateEndMutate
00B2 GateRelease
00B4 GetLocal U32(1)
00BA GateRelease
00BC GetLocal U32(2)
00C2 GateRelease
00C4 Ret
"#;
assert_eq!(disasm_text, expected_disasm);
}
#[test]
fn test_hip_conformance_v0() {
use crate::ir_core::*;
use crate::lowering::lower_program;
use crate::backend;
use std::collections::HashMap;
// --- 1. SETUP CORE IR FIXTURE ---
let mut const_pool = ConstPool::new();
let val_42 = const_pool.add_int(42);
let mut field_offsets = HashMap::new();
let f1 = FieldId(0);
field_offsets.insert(f1, 0);
let mut local_types = HashMap::new();
local_types.insert(0, Type::Struct("Storage".to_string())); // slot 0: gate handle
local_types.insert(1, Type::Int); // slot 1: value 42
local_types.insert(2, Type::Int); // slot 2: result of peek
let program = Program {
const_pool,
modules: vec![Module {
name: "conformance".to_string(),
functions: vec![Function {
id: FunctionId(1),
name: "main".to_string(),
param_slots: 0,
local_slots: 0,
return_slots: 0,
params: vec![],
return_type: Type::Void,
blocks: vec![Block {
id: 0,
instrs: vec![
// 1. allocates a storage struct
Instr::from(InstrKind::Alloc { ty: TypeId(1), slots: 2 }),
Instr::from(InstrKind::SetLocal(0)),
// 2. mutates a field (offset 0)
Instr::from(InstrKind::BeginMutate { gate: ValueId(0) }),
Instr::from(InstrKind::PushConst(val_42)),
Instr::from(InstrKind::SetLocal(1)),
Instr::from(InstrKind::GateStoreField { gate: ValueId(0), field: f1, value: ValueId(1) }),
Instr::from(InstrKind::EndMutate),
// 3. peeks value (offset 0)
Instr::from(InstrKind::BeginPeek { gate: ValueId(0) }),
Instr::from(InstrKind::GateLoadField { gate: ValueId(0), field: f1 }),
Instr::from(InstrKind::SetLocal(2)),
Instr::from(InstrKind::EndPeek),
],
terminator: Terminator::Return,
}],
local_types,
}],
}],
field_offsets,
field_types: HashMap::new(),
};
// --- 2. LOWER TO VM IR ---
let vm_module = lower_program(&program).expect("Lowering failed");
let func = &vm_module.functions[0];
let kinds: Vec<_> = func.body.iter().map(|i| &i.kind).collect();
// Expected sequence of significant instructions:
// Alloc, LocalStore(0), GateBeginMutate, PushConst, LocalStore(1), LocalLoad(0), LocalLoad(1), GateStore(0), GateEndMutate...
assert!(kinds.iter().any(|k| matches!(k, ir_vm::InstrKind::Alloc { .. })), "Must contain Alloc");
assert!(kinds.iter().any(|k| matches!(k, ir_vm::InstrKind::GateBeginMutate)), "Must contain GateBeginMutate");
assert!(kinds.iter().any(|k| matches!(k, ir_vm::InstrKind::GateStore { offset: 0 })), "Must contain GateStore(0)");
assert!(kinds.iter().any(|k| matches!(k, ir_vm::InstrKind::GateBeginPeek)), "Must contain GateBeginPeek");
assert!(kinds.iter().any(|k| matches!(k, ir_vm::InstrKind::GateLoad { offset: 0 })), "Must contain GateLoad(0)");
// RC assertions:
assert!(kinds.contains(&&ir_vm::InstrKind::GateRetain), "Must contain GateRetain (on LocalLoad of gate)");
assert!(kinds.contains(&&ir_vm::InstrKind::GateRelease), "Must contain GateRelease (on cleanup or Pop)");
// --- 4. EMIT BYTECODE ---
let emit_result = backend::emit_module(&vm_module).expect("Emission failed");
let rom = emit_result.rom;
// --- 5. ASSERT INDUSTRIAL FORMAT ---
use prometeu_bytecode::BytecodeLoader;
let pbc = BytecodeLoader::load(&rom).expect("Failed to parse industrial PBC");
assert_eq!(&rom[0..4], b"PBS\0");
assert_eq!(pbc.const_pool.len(), 2); // Null, 42
// ROM Data contains HIP opcodes:
let code = pbc.code;
assert!(code.iter().any(|&b| b == 0x60), "Bytecode must contain Alloc (0x60)");
assert!(code.iter().any(|&b| b == 0x67), "Bytecode must contain GateBeginMutate (0x67)");
assert!(code.iter().any(|&b| b == 0x62), "Bytecode must contain GateStore (0x62)");
assert!(code.iter().any(|&b| b == 0x63), "Bytecode must contain GateBeginPeek (0x63)");
assert!(code.iter().any(|&b| b == 0x61), "Bytecode must contain GateLoad (0x61)");
assert!(code.iter().any(|&b| b == 0x69), "Bytecode must contain GateRetain (0x69)");
assert!(code.iter().any(|&b| b == 0x6A), "Bytecode must contain GateRelease (0x6A)");
}
#[test]
fn test_project_root_and_entry_resolution() {
let dir = tempdir().unwrap();
let project_dir = dir.path();
// Create prometeu.json
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "resolution_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main/modules/main.pbs"
}"#,
).unwrap();
// Create src directory and main.pbs
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
fs::write(project_dir.join("src/main/modules/main.pbs"), "").unwrap();
// Call compile
let result = compile(project_dir);
assert!(result.is_ok(), "Failed to compile: {:?}", result.err());
}
#[test]
fn test_symbols_emission_integration() {
let dir = tempdir().unwrap();
let project_dir = dir.path();
fs::write(
project_dir.join("prometeu.json"),
r#"{
"name": "symbols_test",
"version": "0.1.0",
"script_fe": "pbs",
"entry": "src/main/modules/main.pbs"
}"#,
).unwrap();
let code = r#"
fn frame(): void {
let x = 10;
}
"#;
fs::create_dir_all(project_dir.join("src/main/modules")).unwrap();
fs::write(project_dir.join("src/main/modules/main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile");
let out_pbc = project_dir.join("build/program.pbc");
fs::create_dir_all(out_pbc.parent().unwrap()).unwrap();
unit.export(&out_pbc, false, true).expect("Failed to export");
let symbols_path = project_dir.join("build/symbols.json");
assert!(symbols_path.exists(), "symbols.json should exist at {:?}", symbols_path);
let symbols_content = fs::read_to_string(symbols_path).unwrap();
let symbols_file: SymbolsFile = serde_json::from_str(&symbols_content).unwrap();
assert_eq!(symbols_file.schema_version, 0);
assert!(!symbols_file.projects.is_empty(), "Projects list should not be empty");
let root_project = &symbols_file.projects[0];
assert!(!root_project.symbols.is_empty(), "Symbols list should not be empty");
// Check for a symbol (v0 schema uses 0-based lines)
let main_sym = root_project.symbols.iter().find(|s| s.name == "frame");
assert!(main_sym.is_some(), "Should find 'frame' symbol");
let sym = main_sym.unwrap();
assert!(sym.decl_span.file.contains("main.pbs"), "Symbol file should point to main.pbs, got {}", sym.decl_span.file);
}
}