//! # 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, /// The list of debug symbols discovered during compilation. /// These are used to map bytecode offsets back to source code locations. pub symbols: Vec, } 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 { let config = ProjectConfig::load(project_dir)?; // 1. Select Frontend // The _frontend is responsible for parsing source code and producing the IR. let _frontend: Box = 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::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#"{ "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::>() .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 U32(2) U32(1) 006E SetLocal U32(1) 0074 GetLocal U32(1) 007A GateRetain 007C SetLocal U32(2) 0082 GetLocal U32(2) 0088 GateRetain 008A GateBeginMutate 008C GetLocal U32(2) 0092 GateRetain 0094 GateLoad U32(0) 009A SetLocal U32(3) 00A0 GetLocal U32(3) 00A6 PushConst U32(5) 00AC Add 00AE SetLocal U32(4) 00B4 GateEndMutate 00B6 GateRelease 00B8 GetLocal U32(1) 00BE GateRelease 00C0 GetLocal U32(2) 00C6 GateRelease 00C8 PushConst U32(0) 00CE Ret "#; assert_eq!(disasm_text, expected_disasm); } #[test] fn test_hip_conformance_v0() { use crate::ir_core::*; use crate::ir_core::ids::*; 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(), params: vec![], return_type: Type::Void, blocks: vec![Block { id: 0, instrs: vec![ // 1. allocates a storage struct Instr::Alloc { ty: TypeId(1), slots: 2 }, Instr::SetLocal(0), // 2. mutates a field (offset 0) Instr::BeginMutate { gate: ValueId(0) }, Instr::PushConst(val_42), Instr::SetLocal(1), Instr::GateStoreField { gate: ValueId(0), field: f1, value: ValueId(1) }, Instr::EndMutate, // 3. peeks value (offset 0) Instr::BeginPeek { gate: ValueId(0) }, Instr::GateLoadField { gate: ValueId(0), field: f1 }, Instr::SetLocal(2), Instr::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"); // --- 3. ASSERT VM IR (Instructions + RC) --- 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 file_manager = crate::common::files::FileManager::new(); let emit_result = backend::emit_module(&vm_module, &file_manager).expect("Emission failed"); let rom = emit_result.rom; // --- 5. ASSERT GOLDEN BYTECODE (Exact Bytes) --- // Header: PPBC, Version: 0, Flags: 0 assert_eq!(&rom[0..4], b"PPBC"); assert_eq!(rom[4..6], [0, 0]); // Version 0 assert_eq!(rom[6..8], [0, 0]); // Flags 0 // CP Count: 2 (Null, 42) assert_eq!(rom[8..12], [2, 0, 0, 0]); // ROM Data contains HIP opcodes: assert!(rom.contains(&0x60), "Bytecode must contain Alloc (0x60)"); assert!(rom.contains(&0x67), "Bytecode must contain GateBeginMutate (0x67)"); assert!(rom.contains(&0x62), "Bytecode must contain GateStore (0x62)"); assert!(rom.contains(&0x63), "Bytecode must contain GateBeginPeek (0x63)"); assert!(rom.contains(&0x61), "Bytecode must contain GateLoad (0x61)"); assert!(rom.contains(&0x69), "Bytecode must contain GateRetain (0x69)"); assert!(rom.contains(&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#"{ "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()); } }