279 lines
9.0 KiB
Rust
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());
|
|
}
|
|
}
|