2026-01-29 16:45:02 +00:00

279 lines
9.0 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::files::FileManager;
use crate::common::symbols::Symbol;
use crate::frontends::Frontend;
use crate::ir_vm;
use anyhow::Result;
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 symbols: Vec<Symbol>,
}
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 artifacts = backend::artifacts::Artifacts::new(self.rom.clone(), self.symbols.clone());
artifacts.export(out, emit_disasm, emit_symbols)
}
}
/// Orchestrates the compilation of a Prometeu project starting from an entry file.
///
/// This function executes the full compiler pipeline:
/// 1. **Frontend**: Loads and parses the entry file (and its dependencies).
/// Currently, it uses the `TypescriptFrontend`.
/// 2. **IR Generation**: The frontend produces a high-level Intermediate Representation (IR).
/// 3. **Validation**: Checks the IR for consistency and VM compatibility.
/// 4. **Backend**: Lowers the IR into final Prometeu ByteCode.
///
/// # Errors
/// Returns an error if parsing fails, validation finds issues, or code generation fails.
///
/// # Example
/// ```no_run
/// use std::path::Path;
/// let project_dir = Path::new(".");
/// let unit = prometeu_compiler::compiler::compile(project_dir).expect("Failed to compile");
/// unit.export(Path::new("build/program.pbc"), true, true).unwrap();
/// ```
pub fn compile(project_dir: &Path) -> Result<CompilationUnit> {
let config = ProjectConfig::load(project_dir)?;
// 1. Select Frontend
// The _frontend is responsible for parsing source code and producing the IR.
let _frontend: Box<dyn Frontend> = match config.script_fe.as_str() {
"pbs" => Box::new(crate::frontends::pbs::PbsFrontend),
_ => anyhow::bail!("Invalid frontend: {}", config.script_fe),
};
#[allow(unreachable_code, unused_variables, unused_mut)]
{
let entry = project_dir.join(&config.entry);
let mut file_manager = FileManager::new();
// 2. Compile to IR (Intermediate Representation)
// This step abstracts away source-specific syntax (like TypeScript) into a
// generic set of instructions that the backend can understand.
let ir_module = _frontend.compile_to_ir(&entry, &mut file_manager)
.map_err(|bundle| {
if let Some(diag) = bundle.diagnostics.first() {
anyhow::anyhow!("{}", diag.message)
} else {
anyhow::anyhow!("Compilation failed with {} errors", bundle.diagnostics.len())
}
})?;
// 3. IR Validation
// Ensures the generated IR is sound and doesn't violate any VM constraints
// before we spend time generating bytecode.
ir_vm::validate::validate_module(&ir_module)
.map_err(|bundle| anyhow::anyhow!("IR Validation failed: {:?}", bundle))?;
// 4. Emit Bytecode
// The backend takes the validated IR and produces the final binary executable.
let result = backend::emit_module(&ir_module, &file_manager)?;
Ok(CompilationUnit {
rom: result.rom,
symbols: result.symbols,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
use prometeu_bytecode::pbc::parse_pbc;
use prometeu_bytecode::disasm::disasm;
use prometeu_bytecode::opcode::OpCode;
#[test]
fn test_invalid_frontend() {
let dir = tempdir().unwrap();
let config_path = dir.path().join("prometeu.json");
fs::write(
config_path,
r#"{
"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#"{
"script_fe": "pbs",
"entry": "main.pbs"
}"#,
).unwrap();
let code = "
fn frame(): void {
let x = alloc int;
mutate x as v {
let y = v + 1;
}
}
";
fs::write(project_dir.join("main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile");
let pbc = parse_pbc(&unit.rom).expect("Failed to parse PBC");
let instrs = disasm(&pbc.rom).expect("Failed to disassemble");
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
assert!(opcodes.contains(&OpCode::Alloc));
assert!(opcodes.contains(&OpCode::LoadRef));
assert!(opcodes.contains(&OpCode::StoreRef));
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#"{
"script_fe": "pbs",
"entry": "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::write(project_dir.join("main.pbs"), code).unwrap();
let unit = compile(project_dir).expect("Failed to compile");
let pbc = parse_pbc(&unit.rom).expect("Failed to parse PBC");
let instrs = disasm(&pbc.rom).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(4097)
001C PushConst U32(3)
0022 SetLocal U32(0)
0028 GetLocal U32(0)
002E PushConst U32(4)
0034 Gt
0036 JmpIfFalse U32(94)
003C Jmp U32(66)
0042 GetLocal U32(0)
0048 Call U32(0) U32(1)
0052 SetLocal U32(1)
0058 Jmp U32(100)
005E Jmp U32(100)
0064 Alloc
0066 SetLocal U32(1)
006C GetLocal U32(1)
0072 LoadRef U32(0)
0078 SetLocal U32(2)
007E GetLocal U32(2)
0084 PushConst U32(5)
008A Add
008C SetLocal U32(3)
0092 GetLocal U32(2)
0098 StoreRef U32(0)
009E Ret
"#;
assert_eq!(disasm_text, expected_disasm);
}
#[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#"{
"script_fe": "pbs",
"entry": "src/main.pbs"
}"#,
).unwrap();
// Create src directory and main.pbs
fs::create_dir(project_dir.join("src")).unwrap();
fs::write(project_dir.join("src/main.pbs"), "").unwrap();
// Call compile
let result = compile(project_dir);
assert!(result.is_ok(), "Failed to compile: {:?}", result.err());
}
}