diff --git a/crates/prometeu-compiler/src/compiler.rs b/crates/prometeu-compiler/src/compiler.rs index f8718fc8..b4e20107 100644 --- a/crates/prometeu-compiler/src/compiler.rs +++ b/crates/prometeu-compiler/src/compiler.rs @@ -259,7 +259,8 @@ mod tests { 00BE GateRelease 00C0 GetLocal U32(2) 00C6 GateRelease -00C8 Ret +00C8 PushConst U32(0) +00CE Ret "#; assert_eq!(disasm_text, expected_disasm); diff --git a/crates/prometeu-compiler/src/ir_vm/mod.rs b/crates/prometeu-compiler/src/ir_vm/mod.rs index 3b062935..115b6c81 100644 --- a/crates/prometeu-compiler/src/ir_vm/mod.rs +++ b/crates/prometeu-compiler/src/ir_vm/mod.rs @@ -146,7 +146,7 @@ mod tests { assert_eq!(func.name, "start"); assert_eq!(func.id, FunctionId(10)); - assert_eq!(func.body.len(), 3); + assert_eq!(func.body.len(), 4); match &func.body[0].kind { InstrKind::Label(Label(l)) => assert!(l.contains("block_0")), _ => panic!("Expected label"), @@ -156,6 +156,10 @@ mod tests { _ => panic!("Expected PushConst"), } match &func.body[2].kind { + InstrKind::PushNull => (), + _ => panic!("Expected PushNull"), + } + match &func.body[3].kind { InstrKind::Ret => (), _ => panic!("Expected Ret"), } diff --git a/crates/prometeu-compiler/src/lowering/core_to_vm.rs b/crates/prometeu-compiler/src/lowering/core_to_vm.rs index c311295e..3f9ce990 100644 --- a/crates/prometeu-compiler/src/lowering/core_to_vm.rs +++ b/crates/prometeu-compiler/src/lowering/core_to_vm.rs @@ -313,6 +313,13 @@ pub fn lower_function( vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateRelease, None)); } } + + // If the function is Void, we must push a Null value to satisfy the VM's RET instruction. + // The VM always pops one value from the stack to be the return value. + if vm_func.return_type == ir_vm::Type::Void { + vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::PushNull, None)); + } + vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Ret, None)); } ir_core::Terminator::Jump(target) => { @@ -422,7 +429,7 @@ mod tests { let func = &vm_module.functions[0]; assert_eq!(func.name, "main"); - assert_eq!(func.body.len(), 7); + assert_eq!(func.body.len(), 8); match &func.body[0].kind { InstrKind::Label(Label(l)) => assert_eq!(l, "block_0"), @@ -452,6 +459,10 @@ mod tests { _ => panic!("Expected HostCall 42"), } match &func.body[6].kind { + InstrKind::PushNull => (), + _ => panic!("Expected PushNull"), + } + match &func.body[7].kind { InstrKind::Ret => (), _ => panic!("Expected Ret"), } @@ -500,7 +511,7 @@ mod tests { // GateStore 100 (offset) // Ret - assert_eq!(func.body.len(), 9); + assert_eq!(func.body.len(), 10); match &func.body[1].kind { ir_vm::InstrKind::LocalLoad { slot } => assert_eq!(*slot, 0), _ => panic!("Expected LocalLoad 0"), @@ -517,6 +528,14 @@ mod tests { ir_vm::InstrKind::GateStore { offset } => assert_eq!(*offset, 100), _ => panic!("Expected GateStore 100"), } + match &func.body[8].kind { + ir_vm::InstrKind::PushNull => (), + _ => panic!("Expected PushNull"), + } + match &func.body[9].kind { + ir_vm::InstrKind::Ret => (), + _ => panic!("Expected Ret"), + } } #[test] @@ -609,10 +628,10 @@ mod tests { assert!(found_overwrite, "Should have emitted release-then-store sequence for overwrite"); // Check Ret cleanup: - // LocalLoad 1, GateRelease, Ret + // LocalLoad 1, GateRelease, PushNull, Ret let mut found_cleanup = false; - for i in 0..kinds.len() - 2 { - if let (InstrKind::LocalLoad { slot: 1 }, InstrKind::GateRelease, InstrKind::Ret) = (kinds[i], kinds[i+1], kinds[i+2]) { + for i in 0..kinds.len() - 3 { + if let (InstrKind::LocalLoad { slot: 1 }, InstrKind::GateRelease, InstrKind::PushNull, InstrKind::Ret) = (kinds[i], kinds[i+1], kinds[i+2], kinds[i+3]) { found_cleanup = true; break; } diff --git a/crates/prometeu-compiler/tests/hip_conformance.rs b/crates/prometeu-compiler/tests/hip_conformance.rs index b468121d..9f5fd8df 100644 --- a/crates/prometeu-compiler/tests/hip_conformance.rs +++ b/crates/prometeu-compiler/tests/hip_conformance.rs @@ -92,7 +92,7 @@ fn test_hip_conformance_core_to_vm_to_bytecode() { 0x02, 0x00, 0x00, 0x00, // CP Count: 2 0x00, // CP[0]: Null 0x01, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // CP[1]: Int64(42) - 0x66, 0x00, 0x00, 0x00, // ROM Size: 102 + 0x6c, 0x00, 0x00, 0x00, // ROM Size: 108 // Instructions: 0x60, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, // Alloc { tid: 10, slots: 2 } 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, // SetLocal 0 @@ -118,6 +118,7 @@ fn test_hip_conformance_core_to_vm_to_bytecode() { 0x11, 0x00, // Pop 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, // GetLocal 0 (cleanup) 0x6a, 0x00, // GateRelease + 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, // PushConst 0 (Null return) 0x51, 0x00, // Ret ]; diff --git a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs index 6461ab3b..6da876e1 100644 --- a/crates/prometeu-core/src/prometeu_os/prometeu_os.rs +++ b/crates/prometeu-core/src/prometeu_os/prometeu_os.rs @@ -51,6 +51,7 @@ pub struct PrometeuOS { pub current_cartridge_title: String, pub current_cartridge_app_version: String, pub current_cartridge_app_mode: crate::model::AppMode, + pub current_entrypoint: String, /// Rate-limiter to prevent apps from flooding the log buffer and killing performance. pub logs_written_this_frame: HashMap, @@ -100,6 +101,7 @@ impl PrometeuOS { current_cartridge_title: String::new(), current_cartridge_app_version: String::new(), current_cartridge_app_mode: crate::model::AppMode::Game, + current_entrypoint: String::new(), logs_written_this_frame: HashMap::new(), telemetry_current: TelemetryFrame::default(), telemetry_last: TelemetryFrame::default(), @@ -167,6 +169,7 @@ impl PrometeuOS { self.current_cartridge_title = cartridge.title.clone(); self.current_cartridge_app_version = cartridge.app_version.clone(); self.current_cartridge_app_mode = cartridge.app_mode; + self.current_entrypoint = cartridge.entrypoint.clone(); } /// Executes a single VM instruction (Debug). @@ -204,6 +207,12 @@ impl PrometeuOS { self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME; self.begin_logical_frame(signals, hw); + // If the VM is not currently executing a function (e.g. at the start of the app + // or after the entrypoint function returned), we prepare a new call to the entrypoint. + if vm.call_stack.is_empty() { + vm.prepare_call(&self.current_entrypoint); + } + // Reset telemetry for the new logical frame self.telemetry_current = TelemetryFrame { frame_index: self.logical_frame_index, @@ -236,8 +245,16 @@ impl PrometeuOS { self.log(LogLevel::Info, LogSource::Vm, 0xDEB1, format!("Breakpoint hit at PC 0x{:X}", vm.pc)); } - // 4. Frame Finalization (FRAME_SYNC reached) - if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync { + // Handle Panics + if let crate::virtual_machine::LogicalFrameEndingReason::Panic(err) = run.reason { + let err_msg = format!("PVM Fault: \"{}\"", err); + self.log(LogLevel::Error, LogSource::Vm, 0, err_msg.clone()); + return Some(err_msg); + } + + // 4. Frame Finalization (FRAME_SYNC reached or Entrypoint returned) + if run.reason == crate::virtual_machine::LogicalFrameEndingReason::FrameSync || + run.reason == crate::virtual_machine::LogicalFrameEndingReason::EndOfRom { // All drawing commands for this frame are now complete. // Finalize the framebuffer. hw.gfx_mut().render_all(); @@ -626,6 +643,45 @@ mod tests { os.syscall(0x1001, &mut vm, &mut hw).unwrap(); // gfx.clear assert_eq!(vm.pop().unwrap(), Value::Null); } + + #[test] + fn test_entrypoint_called_every_frame() { + let mut os = PrometeuOS::new(None); + let mut vm = VirtualMachine::default(); + let mut hw = Hardware::new(); + let signals = InputSignals::default(); + + // PushI32 0 (0x17), then Ret (0x51) + let rom = vec![ + 0x17, 0x00, // PushI32 + 0x00, 0x00, 0x00, 0x00, // value 0 + 0x51, 0x00 // Ret + ]; + let cartridge = Cartridge { + app_id: 1234, + title: "test".to_string(), + app_version: "1.0.0".to_string(), + app_mode: AppMode::Game, + entrypoint: "0".to_string(), + program: rom, + assets: vec![], + asset_table: vec![], + preload: vec![], + }; + os.initialize_vm(&mut vm, &cartridge); + + // First frame + os.tick(&mut vm, &signals, &mut hw); + assert_eq!(os.logical_frame_index, 1); + assert!(!os.logical_frame_active); + assert!(vm.call_stack.is_empty()); + + // Second frame - Should call entrypoint again + os.tick(&mut vm, &signals, &mut hw); + assert_eq!(os.logical_frame_index, 2); + assert!(!os.logical_frame_active); + assert!(vm.call_stack.is_empty()); + } } impl NativeInterface for PrometeuOS { diff --git a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs index ed5bf582..026d8a30 100644 --- a/crates/prometeu-core/src/virtual_machine/virtual_machine.rs +++ b/crates/prometeu-core/src/virtual_machine/virtual_machine.rs @@ -137,6 +137,30 @@ impl VirtualMachine { self.cycles = 0; self.halted = false; } + + /// Prepares the VM to execute a specific entrypoint by setting the PC and + /// pushing an initial call frame. + pub fn prepare_call(&mut self, entrypoint: &str) { + let addr = if let Ok(addr) = entrypoint.parse::() { + addr + } else { + 0 + }; + + self.pc = addr; + self.halted = false; + + // Pushing a sentinel frame so RET works at the top level. + // The return address is set to the end of ROM, which will naturally + // cause the VM to stop after returning from the entrypoint. + self.operand_stack.clear(); + self.call_stack.clear(); + self.scope_stack.clear(); + self.call_stack.push(CallFrame { + return_pc: self.program.rom.len() as u32, + stack_base: 0, + }); + } } impl Default for VirtualMachine { @@ -1277,4 +1301,25 @@ mod tests { _ => panic!("Expected Trap, got {:?}", report.reason), } } + + #[test] + fn test_entry_point_ret_with_prepare_call() { + // PushI32 0 (0x17), then Ret (0x51) + let rom = vec![ + 0x17, 0x00, // PushI32 + 0x00, 0x00, 0x00, 0x00, // value 0 + 0x51, 0x00 // Ret + ]; + let mut vm = VirtualMachine::new(rom, vec![]); + let mut hw = crate::Hardware::new(); + struct TestNative; + impl NativeInterface for TestNative { + fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine, _hw: &mut dyn HardwareBridge) -> Result { Ok(0) } + } + let mut native = TestNative; + + vm.prepare_call("0"); + let result = vm.run_budget(100, &mut native, &mut hw).expect("VM run failed"); + assert_eq!(result.reason, LogicalFrameEndingReason::EndOfRom); + } } diff --git a/test-cartridges/test01/cartridge/program.disasm.txt b/test-cartridges/test01/cartridge/program.disasm.txt deleted file mode 100644 index 4a12b599..00000000 --- a/test-cartridges/test01/cartridge/program.disasm.txt +++ /dev/null @@ -1,12 +0,0 @@ -00000000 PushConst U32(1) -00000006 SetLocal U32(0) -0000000C GetLocal U32(0) -00000012 PushConst U32(1) -00000018 Eq -0000001A JmpIfFalse U32(56) -00000020 Jmp U32(38) -00000026 PushConst U32(2) -0000002C Syscall U32(4097) -00000032 Jmp U32(62) -00000038 Jmp U32(62) -0000003E Ret diff --git a/test-cartridges/test01/cartridge/program.pbc b/test-cartridges/test01/cartridge/program.pbc index 6ff155ac..008bb275 100644 Binary files a/test-cartridges/test01/cartridge/program.pbc and b/test-cartridges/test01/cartridge/program.pbc differ diff --git a/test-cartridges/test01/sdk b/test-cartridges/test01/sdk index 914946df..5cd18364 120000 --- a/test-cartridges/test01/sdk +++ b/test-cartridges/test01/sdk @@ -1 +1 @@ -../../target/debug \ No newline at end of file +../../dist-staging/stable/prometeu-aarch64-apple-darwin \ No newline at end of file diff --git a/test-cartridges/test01/src/main.pbs b/test-cartridges/test01/src/main.pbs index d492d539..b0b6d1b0 100644 --- a/test-cartridges/test01/src/main.pbs +++ b/test-cartridges/test01/src/main.pbs @@ -1,6 +1,2 @@ -declare contract Touch host { - fn f(); -} - fn frame(): void { } diff --git a/test-cartridges/test01/src/sdk.pbs b/test-cartridges/test01/src/sdk.pbs deleted file mode 100644 index d1619b74..00000000 --- a/test-cartridges/test01/src/sdk.pbs +++ /dev/null @@ -1,3 +0,0 @@ -pub declare contract Gfx host {} -pub declare contract Input host {} -pub declare contract Touch host {} diff --git a/test-cartridges/test01/test-cartridges/test01/src/main.pbs b/test-cartridges/test01/test-cartridges/test01/src/main.pbs deleted file mode 100644 index a474cbb9..00000000 --- a/test-cartridges/test01/test-cartridges/test01/src/main.pbs +++ /dev/null @@ -1,7 +0,0 @@ -import Gfx from "./sdk.pbs" - -fn frame(): void { - Gfx.clear(0); - Gfx.fillRect(110, 110, 20, 20, 2016); - Gfx.drawText(10, 10, "Test01 PBS - Minimal", 65535); -}