Co-authored-by: Nilton Constantino <nilton.constantino@visma.com> Reviewed-on: #8
491 lines
18 KiB
Rust
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);
|
|
}
|
|
}
|