use super::*; use crate::fs::{FsBackend, FsEntry, FsError}; use prometeu_bytecode::TRAP_TYPE; use prometeu_bytecode::Value; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, ConstantPoolEntry, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; use prometeu_hal::AudioOpStatus; use prometeu_hal::GfxOpStatus; use prometeu_hal::InputSignals; use prometeu_hal::asset::{ AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus, }; use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge}; use prometeu_hal::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1; use prometeu_hal::syscalls::caps; use prometeu_vm::VmInitError; use std::collections::HashMap; use std::sync::atomic::Ordering; #[derive(Default)] struct MemFsBackend { files: HashMap>, } impl FsBackend for MemFsBackend { fn mount(&mut self) -> Result<(), FsError> { Ok(()) } fn unmount(&mut self) {} fn list_dir(&self, _path: &str) -> Result, FsError> { Ok(Vec::new()) } fn read_file(&self, path: &str) -> Result, FsError> { self.files.get(path).cloned().ok_or(FsError::NotFound) } fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> { self.files.insert(path.to_string(), data.to_vec()); Ok(()) } fn delete(&mut self, path: &str) -> Result<(), FsError> { self.files.remove(path).map(|_| ()).ok_or(FsError::NotFound) } fn exists(&self, path: &str) -> bool { self.files.contains_key(path) } fn is_healthy(&self) -> bool { true } } fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { Cartridge { app_id: 42, title: "Test Cart".into(), app_version: "1.0.0".into(), app_mode: AppMode::Game, capabilities, program, assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], } } fn serialized_single_function_module(code: Vec, syscalls: Vec) -> Vec { serialized_single_function_module_with_consts(code, vec![], syscalls) } fn serialized_single_function_module_with_consts( code: Vec, const_pool: Vec, syscalls: Vec, ) -> Vec { BytecodeModule { version: 0, const_pool, functions: vec![FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() }], code, debug_info: None, exports: vec![], syscalls, } .serialize() } fn test_glyph_payload_size(width: usize, height: usize) -> usize { (width * height).div_ceil(2) + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::()) } fn test_glyph_decoded_size(width: usize, height: usize) -> usize { width * height + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::()) } fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry { AssetEntry { asset_id: 7, asset_name: asset_name.to_string(), bank_type: BankType::GLYPH, offset: 0, size: data_len as u64, decoded_size: test_glyph_decoded_size(16, 16) as u64, codec: AssetCodec::None, metadata: serde_json::json!({ "tile_size": 16, "width": 16, "height": 16, "palette_count": GLYPH_BANK_PALETTE_COUNT_V1, "palette_authored": GLYPH_BANK_PALETTE_COUNT_V1 }), } } fn test_glyph_asset_data() -> Vec { let mut data = vec![0x11u8; test_glyph_payload_size(16, 16) - (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2)]; data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2]); data } #[test] fn initialize_vm_applies_cartridge_capabilities_before_loader_resolution() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }], ); let cartridge = cartridge_with_program(program, caps::NONE); let res = runtime.initialize_vm(&mut vm, &cartridge); assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); assert_eq!(runtime.current_app_id, 0); assert_eq!(vm.pc(), 0); assert_eq!(vm.operand_stack_top(1), Vec::::new()); } #[test] fn initialize_vm_succeeds_when_cartridge_capabilities_cover_hostcalls() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }], ); let cartridge = cartridge_with_program(program, caps::GFX); let res = runtime.initialize_vm(&mut vm, &cartridge); assert!(res.is_ok()); assert_eq!(runtime.current_app_id, 42); assert!(!vm.is_halted()); } #[test] fn tick_returns_error_when_vm_ends_slice_with_trap() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }], ); let cartridge = cartridge_with_program(program, caps::GFX); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware).expect("trap must surface as runtime error"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE); assert!(trap.message.contains("Expected integer at index 0")); } other => panic!("expected VmTrap crash report, got {:?}", other), } assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); } #[test] fn tick_returns_panic_report_distinct_from_trap() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::new(assemble("HOSTCALL 0\nHALT").expect("assemble"), vec![]); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let report = runtime .tick(&mut vm, &signals, &mut hardware) .expect("panic must surface as runtime error"); match report { CrashReport::VmPanic { message, pc } => { assert!(message.contains("HOSTCALL 0 reached execution without loader patching")); assert!(pc.is_some()); } other => panic!("expected VmPanic crash report, got {:?}", other), } assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmPanic { .. }))); } #[test] fn initialize_vm_success_clears_previous_crash_report() { let mut runtime = VirtualMachineRuntime::new(None); runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(1) }); let mut vm = VirtualMachine::default(); let code = assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }], ); let cartridge = cartridge_with_program(program, caps::GFX); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); assert!(runtime.last_crash_report.is_none()); } #[test] fn reset_clears_cartridge_scoped_runtime_state() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); runtime.tick_index = 77; runtime.logical_frame_index = 12; runtime.logical_frame_active = true; runtime.logical_frame_remaining_cycles = 345; runtime.last_frame_cpu_time_us = 678; runtime.fs_state = FsState::Mounted; runtime.open_files.insert(7, "/save.dat".into()); runtime.next_handle = 9; runtime.current_app_id = 42; runtime.current_cartridge_title = "Cart".into(); runtime.current_cartridge_app_version = "1.2.3".into(); runtime.current_cartridge_app_mode = AppMode::System; runtime.logs_written_this_frame.insert(42, 3); runtime.atomic_telemetry.logs_count.store(5, Ordering::Relaxed); runtime.atomic_telemetry.current_logs_count.store(8, Ordering::Relaxed); runtime.atomic_telemetry.frame_index.store(8, Ordering::Relaxed); runtime.atomic_telemetry.cycles_used.store(99, Ordering::Relaxed); runtime.atomic_telemetry.completed_logical_frames.store(2, Ordering::Relaxed); runtime.last_crash_report = Some(CrashReport::VmPanic { message: "stale".into(), pc: Some(55) }); runtime.paused = true; runtime.debug_step_request = true; runtime.needs_prepare_entry_call = true; runtime.reset(&mut vm); assert_eq!(runtime.tick_index, 77); assert_eq!(runtime.fs_state, FsState::Mounted); assert_eq!(runtime.logical_frame_index, 0); assert!(!runtime.logical_frame_active); assert_eq!(runtime.logical_frame_remaining_cycles, 0); assert_eq!(runtime.last_frame_cpu_time_us, 0); assert!(runtime.open_files.is_empty()); assert_eq!(runtime.next_handle, 1); assert_eq!(runtime.current_app_id, 0); assert!(runtime.current_cartridge_title.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty()); assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); assert!(runtime.logs_written_this_frame.is_empty()); assert_eq!(runtime.atomic_telemetry.logs_count.load(Ordering::Relaxed), 0); assert_eq!(runtime.atomic_telemetry.current_logs_count.load(Ordering::Relaxed), 0); assert_eq!(runtime.atomic_telemetry.frame_index.load(Ordering::Relaxed), 0); assert_eq!(runtime.atomic_telemetry.cycles_used.load(Ordering::Relaxed), 0); assert_eq!(runtime.atomic_telemetry.completed_logical_frames.load(Ordering::Relaxed), 0); assert!(runtime.last_crash_report.is_none()); assert!(!runtime.paused); assert!(!runtime.debug_step_request); assert!(!runtime.needs_prepare_entry_call); assert_eq!(vm.pc(), 0); } #[test] fn initialize_vm_failure_clears_previous_identity_and_handles() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let good_program = serialized_single_function_module( assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), vec![SyscallDecl { module: "gfx".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }], ); let good_cartridge = cartridge_with_program(good_program, caps::GFX); runtime.initialize_vm(&mut vm, &good_cartridge).expect("runtime must initialize"); runtime.open_files.insert(5, "/save.dat".into()); runtime.next_handle = 6; runtime.paused = true; runtime.debug_step_request = true; runtime.atomic_telemetry.cycles_used.store(123, Ordering::Relaxed); let bad_program = serialized_single_function_module( assemble("PUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"), vec![SyscallDecl { module: "gfx".into(), name: "clear".into(), version: 1, arg_slots: 1, ret_slots: 0, }], ); let bad_cartridge = cartridge_with_program(bad_program, caps::NONE); let res = runtime.initialize_vm(&mut vm, &bad_cartridge); assert!(matches!(res, Err(CrashReport::VmInit { error: VmInitError::LoaderPatchFailed(_) }))); assert_eq!(runtime.current_app_id, 0); assert!(runtime.current_cartridge_title.is_empty()); assert!(runtime.current_cartridge_app_version.is_empty()); assert_eq!(runtime.current_cartridge_app_mode, AppMode::Game); assert!(runtime.open_files.is_empty()); assert_eq!(runtime.next_handle, 1); assert!(!runtime.paused); assert!(!runtime.debug_step_request); assert_eq!(runtime.atomic_telemetry.cycles_used.load(Ordering::Relaxed), 0); assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmInit { .. }))); } #[test] fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "set_sprite".into(), version: 1, arg_slots: 10, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::GFX); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]); } #[test] fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 512\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "set_sprite".into(), version: 1, arg_slots: 10, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::GFX); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid sprite index must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]); } #[test] fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 64\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "set_sprite".into(), version: 1, arg_slots: 10, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::GFX); let asset_data = test_glyph_asset_data(); hardware.assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }], AssetsPayloadSource::from_bytes(asset_data), ); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid gfx parameter range must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]); } #[test] fn tick_audio_play_sample_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 -1\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "audio".into(), name: "play_sample".into(), version: 1, arg_slots: 5, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::AUDIO); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::ArgRangeInvalid as i64)]); } #[test] fn tick_audio_play_voice_invalid_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 16\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "audio".into(), name: "play".into(), version: 1, arg_slots: 7, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::AUDIO); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid voice must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::VoiceInvalid as i64)]); } #[test] fn tick_audio_play_missing_asset_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "audio".into(), name: "play".into(), version: 1, arg_slots: 7, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::AUDIO); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "missing audio asset must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AudioOpStatus::BankInvalid as i64)]); } #[test] fn tick_audio_play_type_mismatch_surfaces_trap_not_panic() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "audio".into(), name: "play".into(), version: 1, arg_slots: 7, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::AUDIO); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE); assert!(trap.message.contains("Expected integer at index 0")); } other => panic!("expected VmTrap crash report, got {:?}", other), } assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); } #[test] fn tick_asset_commit_operational_error_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "asset".into(), name: "commit".into(), version: 1, arg_slots: 1, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::ASSET); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "operational error must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); } #[test] fn tick_asset_load_missing_asset_returns_status_and_zero_handle() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "asset".into(), name: "load".into(), version: 1, arg_slots: 2, ret_slots: 2, }], ); let cartridge = cartridge_with_program(program, caps::ASSET); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "missing asset must not crash"); assert!(vm.is_halted()); assert_eq!( vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64)] ); } #[test] fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let asset_data = test_glyph_asset_data(); hardware.assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![], AssetsPayloadSource::from_bytes(asset_data), ); let code = assemble("PUSH_I32 7\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "asset".into(), name: "load".into(), version: 1, arg_slots: 2, ret_slots: 2, }], ); let cartridge = cartridge_with_program(program, caps::ASSET); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid slot must not crash"); assert!(vm.is_halted()); assert_eq!( vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(AssetLoadError::SlotIndexInvalid as i64)] ); } #[test] fn tick_asset_status_unknown_handle_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "asset".into(), name: "status".into(), version: 1, arg_slots: 1, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::ASSET); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "unknown asset handle must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(LoadStatus::UnknownHandle as i64)]); } #[test] fn tick_asset_commit_invalid_transition_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 1\nHOSTCALL 0\nPOP_N 1\nPUSH_I32 1\nHOSTCALL 1\nHALT") .expect("assemble"); let program = serialized_single_function_module( code, vec![ SyscallDecl { module: "asset".into(), name: "cancel".into(), version: 1, arg_slots: 1, ret_slots: 1, }, SyscallDecl { module: "asset".into(), name: "commit".into(), version: 1, arg_slots: 1, ret_slots: 1, }, ], ); let cartridge = cartridge_with_program(program, caps::ASSET); let asset_data = test_glyph_asset_data(); hardware.assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![], AssetsPayloadSource::from_bytes(asset_data), ); let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated"); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "invalid transition must not crash"); assert!(vm.is_halted()); assert_eq!(handle, 1); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]); } #[test] fn tick_asset_cancel_unknown_handle_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 999\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "asset".into(), name: "cancel".into(), version: 1, arg_slots: 1, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::ASSET); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "unknown handle cancel must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::UnknownHandle as i64)]); } #[test] fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("PUSH_I32 1\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "asset".into(), name: "cancel".into(), version: 1, arg_slots: 1, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::ASSET); let asset_data = test_glyph_asset_data(); hardware.assets.initialize_for_cartridge( vec![test_glyph_asset_entry("tile_asset", asset_data.len())], vec![], AssetsPayloadSource::from_bytes(asset_data), ); let handle = hardware.assets.load(7, 0).expect("asset handle must be allocated"); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); loop { match hardware.assets.status(handle) { LoadStatus::READY => break, LoadStatus::PENDING | LoadStatus::LOADING => { std::thread::sleep(std::time::Duration::from_millis(1)); } other => panic!("unexpected asset status before commit: {:?}", other), } } assert_eq!(hardware.assets.commit(handle), AssetOpStatus::Ok); hardware.assets.apply_commits(); assert_eq!(hardware.assets.status(handle), LoadStatus::COMMITTED); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "cancel after commit must not crash"); assert!(vm.is_halted()); assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(AssetOpStatus::InvalidState as i64)]); } #[test] fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\ PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\ PUSH_I32 999\nPUSH_I32 0\nHOSTCALL 2\n\ HALT" ) .expect("assemble"); let program = serialized_single_function_module( code, vec![ SyscallDecl { module: "gfx".into(), name: "set_sprite".into(), version: 1, arg_slots: 10, ret_slots: 1, }, SyscallDecl { module: "audio".into(), name: "play".into(), version: 1, arg_slots: 7, ret_slots: 1, }, SyscallDecl { module: "asset".into(), name: "load".into(), version: 1, arg_slots: 2, ret_slots: 2, }, ], ); let cartridge = cartridge_with_program(program, caps::GFX | caps::AUDIO | caps::ASSET); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "mixed status-first surface must not crash"); assert!(vm.is_halted()); assert_eq!( vm.operand_stack_top(4), vec![ Value::Int64(0), Value::Int64(AssetLoadError::AssetNotFound as i64), Value::Int64(AudioOpStatus::BankInvalid as i64), Value::Int64(GfxOpStatus::BankInvalid as i64), ] ); } #[test] fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", ) .expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "gfx".into(), name: "set_sprite".into(), version: 1, arg_slots: 10, ret_slots: 1, }], ); let cartridge = cartridge_with_program(program, caps::GFX); runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware).expect("type mismatch must surface as trap"); match report { CrashReport::VmTrap { trap } => { assert_eq!(trap.code, TRAP_TYPE); assert!(trap.message.contains("Expected integer at index 0")); } other => panic!("expected VmTrap crash report, got {:?}", other), } assert!(matches!(runtime.last_crash_report, Some(CrashReport::VmTrap { .. }))); } #[test] fn tick_memcard_slot_roundtrip_for_game_profile() { let mut runtime = VirtualMachineRuntime::new(None); runtime.mount_fs(Box::new(MemFsBackend::default())); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble( "PUSH_I32 0\nPUSH_I32 0\nPUSH_CONST 0\nHOSTCALL 0\nPOP_N 2\nPUSH_I32 0\nHOSTCALL 1\nPOP_N 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 10\nHOSTCALL 2\nHALT", ) .expect("assemble"); let program = serialized_single_function_module_with_consts( code, vec![ConstantPoolEntry::String("6869".into())], // "hi" in hex vec![ SyscallDecl { module: "mem".into(), name: "slot_write".into(), version: 1, arg_slots: 3, ret_slots: 2, }, SyscallDecl { module: "mem".into(), name: "slot_commit".into(), version: 1, arg_slots: 1, ret_slots: 1, }, SyscallDecl { module: "mem".into(), name: "slot_read".into(), version: 1, arg_slots: 3, ret_slots: 3, }, ], ); let cartridge = Cartridge { app_id: 42, title: "Memcard Game".into(), app_version: "1.0.0".into(), app_mode: AppMode::Game, capabilities: caps::FS, program, assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], }; runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "memcard roundtrip must not crash"); assert!(vm.is_halted()); assert_eq!( vm.operand_stack_top(3), vec![Value::Int64(2), Value::String("6869".into()), Value::Int64(0)] ); } #[test] fn tick_memcard_access_is_denied_for_non_game_profile() { let mut runtime = VirtualMachineRuntime::new(None); let mut vm = VirtualMachine::default(); let mut hardware = Hardware::new(); let signals = InputSignals::default(); let code = assemble("HOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module( code, vec![SyscallDecl { module: "mem".into(), name: "slot_count".into(), version: 1, arg_slots: 0, ret_slots: 2, }], ); let cartridge = Cartridge { app_id: 101, title: "System App".into(), app_version: "1.0.0".into(), app_mode: AppMode::System, capabilities: caps::FS, program, assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], }; runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize"); let report = runtime.tick(&mut vm, &signals, &mut hardware); assert!(report.is_none(), "non-game memcard call must return status"); assert!(vm.is_halted()); // top-first: count then status assert_eq!(vm.operand_stack_top(2), vec![Value::Int64(0), Value::Int64(4)]); }