From 39d1214f031458f91dc8d84c8051b7d7d6e80aa6 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Mon, 2 Mar 2026 15:50:35 +0000 Subject: [PATCH] implements remain of capabilities --- ARCHITECTURE.md | 12 +-- Cargo.lock | 1 + .../console/prometeu-bytecode/src/opcode.rs | 3 +- crates/console/prometeu-firmware/Cargo.toml | 5 +- .../src/firmware/firmware.rs | 41 ++++++- .../src/firmware/firmware_step_launch_hub.rs | 2 +- .../firmware/firmware_step_load_cartridge.rs | 14 ++- .../src/virtual_machine_runtime.rs | 102 +++++++++++++++++- .../prometeu-vm/src/virtual_machine.rs | 79 ++++++++++---- .../console/prometeu-vm/src/vm_init_error.rs | 4 + .../src/debugger.rs | 2 +- crates/tools/pbxgen-stress/src/lib.rs | 50 +++++++-- docs/bytecode/ISA_CORE.md | 3 +- docs/specs/hardware/topics/chapter-16.md | 26 +++-- test-cartridges/stress-console/manifest.json | 3 +- test-cartridges/stress-console/program.pbx | Bin 741 -> 825 bytes 16 files changed, 292 insertions(+), 55 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1b654896..add0ccec 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,7 +15,7 @@ This document is the concise, authoritative description of the current Prometeu - Cooperative coroutines - Deterministic, cooperative scheduling. Switching and GC occur only at explicit safepoints (`FRAME_SYNC`). - Unified syscall ABI - - Numeric ID dispatch with metadata (`SyscallMeta`). Verifier enforces arity/return‑slot counts; capability gating at runtime. Syscalls are not first‑class values. + - PBX pre-load artifacts declare canonical host bindings in `SYSC` and encode call sites as `HOSTCALL `. The loader resolves and patches them to numeric `SYSCALL ` before verification/execution. Capability gating is enforced at load and checked again defensively at runtime. Syscalls are not first‑class values. 2. Memory Model @@ -97,7 +97,7 @@ The verifier statically checks bytecode for structural safety and stack‑shape - Call/return shape - Direct calls and returns must match the declared argument counts and return slot counts. Mismatches are rejected. - Syscalls - - Syscall IDs must exist per `SyscallMeta`. Arity and declared return slot counts must match metadata. Capability checks are enforced at runtime (not by the verifier). + - The verifier runs only on the patched executable image. `HOSTCALL` is invalid at verification time. Final `SYSCALL` IDs must exist per `SyscallMeta`, and arity/declared return slot counts must match metadata. - Closures - `CALL_CLOSURE` is only allowed on closure values; the callee function must be known; argument counts for closure calls must match. - Coroutines @@ -106,7 +106,7 @@ The verifier statically checks bytecode for structural safety and stack‑shape 4.2 Runtime vs Verifier Guarantees - The verifier guarantees structural correctness and stack‑shape invariants. It does not perform full type checking of value contents; dynamic checks (e.g., numeric domain checks, polymorphic comparisons, concrete syscall argument validation) occur at runtime and may trap. -- Capability gating for syscalls is enforced at runtime by the VM/native interface. +- Capability gating for syscalls is enforced at load from cartridge capability flags and checked again at runtime by the VM/native interface. 5. Closures (Model B) — Calling Convention @@ -124,13 +124,13 @@ The verifier statically checks bytecode for structural safety and stack‑shape ----------------------- - Identification - - Syscalls are addressed by a numeric ID. They are not first‑class values. + - Host bindings are declared canonically as `(module, name, version)` in PBX `SYSC`, then executed as numeric IDs after loader patching. Syscalls are not first‑class values. - Metadata‑driven - - `SyscallMeta` defines expected arity and return slot counts. The verifier checks IDs/arity/return‑slot counts against this metadata. + - `SyscallMeta` defines expected arity and return slot counts. The loader resolves `HOSTCALL` against this metadata and rejects raw `SYSCALL` in PBX pre-load artifacts; the verifier checks final IDs/arity/return‑slot counts against the same metadata. - Arguments and returns - Arguments are taken from the operand stack in the order defined by the ABI. Returns use multi‑slot results via a host‑side return buffer (`HostReturn`) which the VM copies back onto the stack, or zero slots for “void”. A mismatch in result counts is a fault/panic per current hardening logic. - Capabilities - - Each VM instance has capability flags. Invoking a syscall without the required capability traps. + - Cartridge capability flags are applied before load-time host resolution. Missing required capability aborts load; invoking a syscall without the required capability also traps defensively at runtime. 7. Garbage Collection diff --git a/Cargo.lock b/Cargo.lock index f22bfef7..643bb2cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1630,6 +1630,7 @@ dependencies = [ name = "prometeu-firmware" version = "0.1.0" dependencies = [ + "prometeu-drivers", "prometeu-hal", "prometeu-system", "prometeu-vm", diff --git a/crates/console/prometeu-bytecode/src/opcode.rs b/crates/console/prometeu-bytecode/src/opcode.rs index e286f058..d619a46d 100644 --- a/crates/console/prometeu-bytecode/src/opcode.rs +++ b/crates/console/prometeu-bytecode/src/opcode.rs @@ -190,7 +190,8 @@ pub enum OpCode { /// This opcode is valid only in PBX artifact form and must be patched by the loader /// into a final numeric `SYSCALL ` before verification or execution. Hostcall = 0x71, - /// Invokes a system function (Firmware/OS). + /// Invokes a final numeric system function (Firmware/OS). + /// Raw `SYSCALL` is valid only after loader patching and is rejected in PBX pre-load artifacts. /// Operand: syscall_id (u32) /// Stack: [args...] -> [results...] (depends on syscall) Syscall = 0x70, diff --git a/crates/console/prometeu-firmware/Cargo.toml b/crates/console/prometeu-firmware/Cargo.toml index 57050b40..37292655 100644 --- a/crates/console/prometeu-firmware/Cargo.toml +++ b/crates/console/prometeu-firmware/Cargo.toml @@ -7,4 +7,7 @@ license.workspace = true [dependencies] prometeu-vm = { path = "../prometeu-vm" } prometeu-system = { path = "../prometeu-system" } -prometeu-hal = { path = "../prometeu-hal" } \ No newline at end of file +prometeu-hal = { path = "../prometeu-hal" } + +[dev-dependencies] +prometeu-drivers = { path = "../prometeu-drivers" } diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 248c64f2..8032e022 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -160,7 +160,46 @@ impl Firmware { } pub fn load_cartridge(&mut self, cartridge: Cartridge) { - self.state = FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge }); + self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)); self.state_initialized = false; } } + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_drivers::hardware::Hardware; + use prometeu_hal::cartridge::AppMode; + + fn invalid_game_cartridge() -> Cartridge { + Cartridge { + app_id: 7, + title: "Broken Cart".into(), + app_version: "1.0.0".into(), + app_mode: AppMode::Game, + entrypoint: "".into(), + capabilities: 0, + program: vec![0, 0, 0, 0], + assets: vec![], + asset_table: vec![], + preload: vec![], + } + } + + #[test] + fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() { + let mut firmware = Firmware::new(None); + let mut hardware = Hardware::new(); + let signals = InputSignals::default(); + + firmware.load_cartridge(invalid_game_cartridge()); + firmware.tick(&signals, &mut hardware); + + match &firmware.state { + FirmwareState::AppCrashes(step) => { + assert!(step.error.contains("InvalidFormat")); + } + other => panic!("expected AppCrashes state, got {:?}", other), + } + } +} diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs index 3301e1bb..eb20a8c4 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_launch_hub.rs @@ -19,7 +19,7 @@ impl LaunchHubStep { Ok(cartridge) => { // In the case of debug, we could pause here, but the requirement says // for the Runtime to open the socket and wait. - return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge })); + return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge))); } Err(e) => { ctx.os.log( diff --git a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs index fa406a17..c26c2692 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware_step_load_cartridge.rs @@ -1,4 +1,4 @@ -use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep}; +use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep}; use crate::firmware::prometeu_context::PrometeuContext; use prometeu_hal::cartridge::{AppMode, Cartridge}; use prometeu_hal::color::Color; @@ -8,9 +8,14 @@ use prometeu_hal::window::Rect; #[derive(Debug, Clone)] pub struct LoadCartridgeStep { pub cartridge: Cartridge, + init_error: Option, } impl LoadCartridgeStep { + pub fn new(cartridge: Cartridge) -> Self { + Self { cartridge, init_error: None } + } + pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { ctx.os.log( LogLevel::Info, @@ -26,10 +31,15 @@ impl LoadCartridgeStep { self.cartridge.assets.clone(), ); - ctx.os.initialize_vm(ctx.vm, &self.cartridge); + self.init_error = + ctx.os.initialize_vm(ctx.vm, &self.cartridge).err().map(|err| format!("{:?}", err)); } pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option { + if let Some(error) = self.init_error.take() { + return Some(FirmwareState::AppCrashes(AppCrashesStep { error })); + } + if self.cartridge.app_mode == AppMode::System { let id = ctx.hub.window_manager.add_window( self.cartridge.title.clone(), diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime.rs b/crates/console/prometeu-system/src/virtual_machine_runtime.rs index 9ec1f115..4f891a9a 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime.rs @@ -12,7 +12,7 @@ use prometeu_hal::tile::Tile; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{HardwareBridge, InputSignals}; use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; -use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine}; +use prometeu_vm::{LogicalFrameEndingReason, VirtualMachine, VmInitError}; use std::collections::HashMap; use std::time::Instant; @@ -171,7 +171,13 @@ impl VirtualMachineRuntime { } /// Loads a cartridge into the PVM and resets the execution state. - pub fn initialize_vm(&mut self, vm: &mut VirtualMachine, cartridge: &Cartridge) { + pub fn initialize_vm( + &mut self, + vm: &mut VirtualMachine, + cartridge: &Cartridge, + ) -> Result<(), VmInitError> { + vm.set_capabilities(cartridge.capabilities); + match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) { Ok(_) => { // Determines the numeric app_id @@ -180,6 +186,7 @@ impl VirtualMachineRuntime { self.current_cartridge_app_version = cartridge.app_version.clone(); self.current_cartridge_app_mode = cartridge.app_mode; self.current_entrypoint = cartridge.entrypoint.clone(); + Ok(()) } Err(e) => { self.log( @@ -190,6 +197,7 @@ impl VirtualMachineRuntime { ); // Fail fast: no program is installed, no app id is switched. // We don't update current_app_id or other fields. + Err(e) } } } @@ -1115,3 +1123,93 @@ impl NativeInterface for VirtualMachineRuntime { } } } + +#[cfg(test)] +mod tests { + use super::*; + use prometeu_bytecode::assembler::assemble; + use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; + use prometeu_hal::cartridge::Cartridge; + use prometeu_hal::syscalls::caps; + + 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, + entrypoint: "".into(), + capabilities, + program, + assets: vec![], + asset_table: vec![], + preload: vec![], + } + } + + fn serialized_single_function_module(code: Vec, syscalls: Vec) -> Vec { + BytecodeModule { + version: 0, + const_pool: vec![], + functions: vec![FunctionMeta { + code_offset: 0, + code_len: code.len() as u32, + ..Default::default() + }], + code, + debug_info: None, + exports: vec![], + syscalls, + } + .serialize() + } + + #[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!(res.is_err()); + 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()); + } +} diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index b207c8af..b5e76bf3 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -1,22 +1,22 @@ use crate::call_frame::CallFrame; use crate::heap::{CoroutineState, Heap}; use crate::object::ObjectKind; -use crate::roots::{RootVisitor, visit_value_for_roots}; +use crate::roots::{visit_value_for_roots, RootVisitor}; use crate::scheduler::Scheduler; use crate::verifier::Verifier; use crate::vm_init_error::{LoaderPatchError, VmInitError}; use crate::{HostContext, NativeInterface}; -use prometeu_bytecode::HeapRef; use prometeu_bytecode::decode_next; +use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::model::BytecodeModule; +use prometeu_bytecode::HeapRef; use prometeu_bytecode::ProgramImage; use prometeu_bytecode::Value; -use prometeu_bytecode::isa::core::CoreOpCode as OpCode; use prometeu_bytecode::{ - TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB, - TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, + TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, + TRAP_OOB, TRAP_STACK_UNDERFLOW, TRAP_TYPE, }; -use prometeu_hal::syscalls::caps::ALL; +use prometeu_hal::syscalls::caps::NONE; use prometeu_hal::vm_fault::VmFault; fn patch_module_hostcalls( @@ -35,19 +35,28 @@ fn patch_module_hostcalls( let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?; let next_pc = instr.next_pc; - if instr.opcode == OpCode::Hostcall { - let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?; - let Some(resolved_syscall) = resolved.get(sysc_index as usize) else { - return Err(LoaderPatchError::HostcallIndexOutOfBounds { - pc, - sysc_index, - syscalls_len: resolved.len(), - }); - }; + match instr.opcode { + OpCode::Hostcall => { + let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?; + let Some(resolved_syscall) = resolved.get(sysc_index as usize) else { + return Err(LoaderPatchError::HostcallIndexOutOfBounds { + pc, + sysc_index, + syscalls_len: resolved.len(), + }); + }; - used[sysc_index as usize] = true; - module.code[pc..pc + 2].copy_from_slice(&(OpCode::Syscall as u16).to_le_bytes()); - module.code[pc + 2..pc + 6].copy_from_slice(&resolved_syscall.id.to_le_bytes()); + used[sysc_index as usize] = true; + module.code[pc..pc + 2].copy_from_slice(&(OpCode::Syscall as u16).to_le_bytes()); + module.code[pc + 2..pc + 6].copy_from_slice(&resolved_syscall.id.to_le_bytes()); + } + OpCode::Syscall => { + return Err(LoaderPatchError::RawSyscallInPreloadArtifact { + pc, + syscall_id: instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?, + }); + } + _ => {} } pc = next_pc; @@ -240,7 +249,7 @@ impl VirtualMachine { breakpoints: std::collections::HashSet::new(), gc_alloc_threshold: 1024, // conservative default; tests may override last_gc_live_count: 0, - capabilities: ALL, + capabilities: NONE, yield_requested: false, sleep_requested_until: None, current_tick: 0, @@ -1643,7 +1652,7 @@ mod tests { use crate::HostReturn; use prometeu_bytecode::model::{BytecodeModule, SyscallDecl}; use prometeu_bytecode::{ - FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW, assemble, disassemble, + assemble, disassemble, FunctionMeta, TRAP_INVALID_LOCAL, TRAP_STACK_UNDERFLOW, }; use prometeu_hal::expect_int; @@ -2874,6 +2883,36 @@ mod tests { ); } + #[test] + fn test_loader_patching_rejects_raw_syscall_in_preload_artifact() { + let mut vm = VirtualMachine::default(); + vm.set_capabilities(prometeu_hal::syscalls::caps::GFX); + + let code = assemble("PUSH_I32 0\nSYSCALL 0x1001\nHALT").expect("assemble"); + let bytes = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "gfx".into(), + name: "clear".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }], + ); + + let res = vm.initialize(bytes, ""); + + assert_eq!( + res, + Err(VmInitError::LoaderPatchFailed( + crate::vm_init_error::LoaderPatchError::RawSyscallInPreloadArtifact { + pc: 6, + syscall_id: 0x1001, + }, + )) + ); + } + #[test] fn test_loader_patching_propagates_resolution_failure() { let mut vm = VirtualMachine::default(); diff --git a/crates/console/prometeu-vm/src/vm_init_error.rs b/crates/console/prometeu-vm/src/vm_init_error.rs index 0ed9951c..d871a6f2 100644 --- a/crates/console/prometeu-vm/src/vm_init_error.rs +++ b/crates/console/prometeu-vm/src/vm_init_error.rs @@ -2,6 +2,10 @@ pub enum LoaderPatchError { DecodeFailed(prometeu_bytecode::DecodeError), ResolveFailed(prometeu_hal::syscalls::DeclaredLoadError), + RawSyscallInPreloadArtifact { + pc: usize, + syscall_id: u32, + }, HostcallIndexOutOfBounds { pc: usize, sysc_index: u32, diff --git a/crates/host/prometeu-host-desktop-winit/src/debugger.rs b/crates/host/prometeu-host-desktop-winit/src/debugger.rs index 8f01508e..dec3cd04 100644 --- a/crates/host/prometeu-host-desktop-winit/src/debugger.rs +++ b/crates/host/prometeu-host-desktop-winit/src/debugger.rs @@ -45,7 +45,7 @@ impl HostDebugger { // Pre-load cartridge metadata so the Handshake message can contain // valid information about the App being debugged. if let Ok(cartridge) = CartridgeLoader::load(path) { - firmware.os.initialize_vm(&mut firmware.vm, &cartridge); + let _ = firmware.os.initialize_vm(&mut firmware.vm, &cartridge); } match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) { diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index 91c7a53c..4c3efd72 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -1,7 +1,7 @@ use anyhow::Result; use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{ - BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, + BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl, }; use std::fs; use std::path::PathBuf; @@ -12,6 +12,36 @@ fn asm(s: &str) -> Vec { pub fn generate() -> Result<()> { let mut rom: Vec = Vec::new(); + let syscalls = vec![ + SyscallDecl { + module: "gfx".into(), + name: "clear_565".into(), + version: 1, + arg_slots: 1, + ret_slots: 0, + }, + SyscallDecl { + module: "gfx".into(), + name: "draw_disc".into(), + version: 1, + arg_slots: 5, + ret_slots: 0, + }, + SyscallDecl { + module: "gfx".into(), + name: "draw_text".into(), + version: 1, + arg_slots: 4, + ret_slots: 0, + }, + SyscallDecl { + module: "log".into(), + name: "write".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }, + ]; heavy_load(&mut rom); // light_load(&mut rom); @@ -38,7 +68,7 @@ pub fn generate() -> Result<()> { function_names: vec![(0, "main".into())], }), exports: vec![Export { symbol: "main".into(), func_idx: 0 }], - syscalls: vec![], + syscalls, }; let bytes = module.serialize(); @@ -54,9 +84,7 @@ pub fn generate() -> Result<()> { if !out_dir.join("assets.pa").exists() { fs::write(out_dir.join("assets.pa"), &[] as &[u8])?; } - if !out_dir.join("manifest.json").exists() { - fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"entrypoint\": \"main\"\n}\n")?; - } + fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"entrypoint\": \"main\",\n \"capabilities\": [\"gfx\", \"log\"]\n}\n")?; Ok(()) } @@ -86,7 +114,7 @@ fn heavy_load(mut rom: &mut Vec) { rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); // --- clear screen --- - rom.extend(asm("PUSH_I32 0\nSYSCALL 0x1010")); + rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); // --- draw 500 discs --- rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); @@ -105,8 +133,8 @@ fn heavy_load(mut rom: &mut Vec) { rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND")); // fill color = (i * 5678 + t) & 0xFFFF rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND")); - // SYSCALL GfxDrawDisc (x, y, r, border, fill) - rom.extend(asm("SYSCALL 0x1005")); + // HOSTCALL gfx.draw_disc (x, y, r, border, fill) + rom.extend(asm("HOSTCALL 1")); // i++ rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); @@ -140,8 +168,8 @@ fn heavy_load(mut rom: &mut Vec) { // color = (t * 10 + i * 1000) & 0xFFFF rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND")); - // SYSCALL GfxDrawText (x, y, str, color) - rom.extend(asm("SYSCALL 0x1008")); + // HOSTCALL gfx.draw_text (x, y, str, color) + rom.extend(asm("HOSTCALL 2")); // i++ rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); @@ -153,7 +181,7 @@ fn heavy_load(mut rom: &mut Vec) { rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); let jif_log_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nSYSCALL 0x5001")); + rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 3")); let after_log = rom.len() as u32; // --- end of function --- diff --git a/docs/bytecode/ISA_CORE.md b/docs/bytecode/ISA_CORE.md index 08469e48..e2374c07 100644 --- a/docs/bytecode/ISA_CORE.md +++ b/docs/bytecode/ISA_CORE.md @@ -54,7 +54,8 @@ This document defines the minimal, stable Core ISA surface for the Prometeu Virt - `PUSH_SCOPE`, `POP_SCOPE` — begin/end lexical scope. - System/Timing: - - `SYSCALL u32` — platform call; arity/types are verified by the VM/firmware layer. + - `HOSTCALL u32` — PBX pre-load host binding call by `SYSC` table index; the loader must resolve and rewrite it before verification or execution. + - `SYSCALL u32` — final numeric platform call in the executable image; raw `SYSCALL` in PBX pre-load artifacts is rejected by the loader. - `FRAME_SYNC` — yield until the next frame boundary (e.g., vblank); explicit safepoint. For exact immediates and stack effects, see `CoreOpCode::spec()` which is the single source of truth used by the decoder, disassembler, and (later) verifier. diff --git a/docs/specs/hardware/topics/chapter-16.md b/docs/specs/hardware/topics/chapter-16.md index 2f3f8f1b..a39d1fac 100644 --- a/docs/specs/hardware/topics/chapter-16.md +++ b/docs/specs/hardware/topics/chapter-16.md @@ -88,9 +88,11 @@ Example: At cartridge load time: 1. The cartridge declares required syscalls using canonical identities. -2. The host verifies capabilities. -3. The host resolves each canonical identity to a numeric `syscall_id`. -4. The VM stores the resolved table for fast execution. +2. The cartridge bytecode encodes host-backed call sites as `HOSTCALL ` into that declaration table. +3. The host verifies capabilities from the cartridge manifest before execution begins. +4. The host resolves each canonical identity to a numeric `syscall_id`. +5. The loader rewrites every `HOSTCALL ` into `SYSCALL `. +6. The VM stores the patched executable image for fast execution. At runtime, the VM executes: @@ -99,12 +101,21 @@ SYSCALL ``` Only numeric IDs are used during execution. +Raw `SYSCALL ` is not valid in a PBX pre-load artifact and must be rejected by the loader. --- ## 4 Syscall Instruction Semantics -The VM provides a single instruction: +The VM provides two relevant instruction forms across the load pipeline: + +Pre-load PBX artifact: + +``` +HOSTCALL +``` + +Final executable image: ``` SYSCALL @@ -112,13 +123,14 @@ SYSCALL Where: +* `` is an index into the program-declared syscall table. * `` is a 32-bit integer identifying the syscall. Execution steps: 1. The VM looks up syscall metadata using ``. 2. The VM verifies that enough arguments exist on the stack. -3. The VM checks capability requirements. +3. The VM checks capability requirements defensively. 4. The syscall executes in the host environment. 5. The syscall leaves exactly `ret_slots` values on the stack. @@ -160,7 +172,7 @@ Fields: | `may_allocate` | Whether the syscall may allocate VM heap objects | | `cost_hint` | Expected cycle cost | -The verifier uses this table to validate stack effects. +The loader uses this table to resolve canonical identities, validate declared ABI shape, and enforce capability gating. The verifier uses the final numeric metadata to validate stack effects after patching. --- @@ -369,4 +381,4 @@ status, progress = asset.status(id) -< [Back](chapter-15.md) | [Summary](table-of-contents.md) > \ No newline at end of file +< [Back](chapter-15.md) | [Summary](table-of-contents.md) > diff --git a/test-cartridges/stress-console/manifest.json b/test-cartridges/stress-console/manifest.json index c28e4851..dbde1fc3 100644 --- a/test-cartridges/stress-console/manifest.json +++ b/test-cartridges/stress-console/manifest.json @@ -5,5 +5,6 @@ "title": "Stress Console", "app_version": "0.1.0", "app_mode": "Game", - "entrypoint": "main" + "entrypoint": "main", + "capabilities": ["gfx", "log"] } diff --git a/test-cartridges/stress-console/program.pbx b/test-cartridges/stress-console/program.pbx index ed965b6a68cfe2e34bf129521748f80c6c1bb54b..d427e77c8e4398a791785f5b1298b9fc0846f243 100644 GIT binary patch delta 135 zcmaFLx|3}}3S-2?)FhTd1_p+Sr!N3$Muy4r8P5P2ObnCrnOuMjW`@bznY1EUfJ&Gd z($gw98Ip5S6N}G&nY36KfB*oJ Cy$x0X