implements remain of capabilities

This commit is contained in:
bQUARKz 2026-03-02 15:50:35 +00:00
parent c6e3199821
commit 39d1214f03
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
16 changed files with 292 additions and 55 deletions

View File

@ -15,7 +15,7 @@ This document is the concise, authoritative description of the current Prometeu
- Cooperative coroutines - Cooperative coroutines
- Deterministic, cooperative scheduling. Switching and GC occur only at explicit safepoints (`FRAME_SYNC`). - Deterministic, cooperative scheduling. Switching and GC occur only at explicit safepoints (`FRAME_SYNC`).
- Unified syscall ABI - Unified syscall ABI
- Numeric ID dispatch with metadata (`SyscallMeta`). Verifier enforces arity/returnslot counts; capability gating at runtime. Syscalls are not firstclass values. - PBX pre-load artifacts declare canonical host bindings in `SYSC` and encode call sites as `HOSTCALL <sysc_index>`. The loader resolves and patches them to numeric `SYSCALL <id>` before verification/execution. Capability gating is enforced at load and checked again defensively at runtime. Syscalls are not firstclass values.
2. Memory Model 2. Memory Model
@ -97,7 +97,7 @@ The verifier statically checks bytecode for structural safety and stackshape
- Call/return shape - Call/return shape
- Direct calls and returns must match the declared argument counts and return slot counts. Mismatches are rejected. - Direct calls and returns must match the declared argument counts and return slot counts. Mismatches are rejected.
- Syscalls - 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 - Closures
- `CALL_CLOSURE` is only allowed on closure values; the callee function must be known; argument counts for closure calls must match. - `CALL_CLOSURE` is only allowed on closure values; the callee function must be known; argument counts for closure calls must match.
- Coroutines - Coroutines
@ -106,7 +106,7 @@ The verifier statically checks bytecode for structural safety and stackshape
4.2 Runtime vs Verifier Guarantees 4.2 Runtime vs Verifier Guarantees
- The verifier guarantees structural correctness and stackshape 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. - The verifier guarantees structural correctness and stackshape 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 5. Closures (Model B) — Calling Convention
@ -124,13 +124,13 @@ The verifier statically checks bytecode for structural safety and stackshape
----------------------- -----------------------
- Identification - Identification
- Syscalls are addressed by a numeric ID. They are not firstclass values. - Host bindings are declared canonically as `(module, name, version)` in PBX `SYSC`, then executed as numeric IDs after loader patching. Syscalls are not firstclass values.
- Metadatadriven - Metadatadriven
- `SyscallMeta` defines expected arity and return slot counts. The verifier checks IDs/arity/returnslot 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/returnslot counts against the same metadata.
- Arguments and returns - Arguments and returns
- Arguments are taken from the operand stack in the order defined by the ABI. Returns use multislot results via a hostside 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. - Arguments are taken from the operand stack in the order defined by the ABI. Returns use multislot results via a hostside 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 - 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 7. Garbage Collection

1
Cargo.lock generated
View File

@ -1630,6 +1630,7 @@ dependencies = [
name = "prometeu-firmware" name = "prometeu-firmware"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"prometeu-drivers",
"prometeu-hal", "prometeu-hal",
"prometeu-system", "prometeu-system",
"prometeu-vm", "prometeu-vm",

View File

@ -190,7 +190,8 @@ pub enum OpCode {
/// This opcode is valid only in PBX artifact form and must be patched by the loader /// This opcode is valid only in PBX artifact form and must be patched by the loader
/// into a final numeric `SYSCALL <id>` before verification or execution. /// into a final numeric `SYSCALL <id>` before verification or execution.
Hostcall = 0x71, 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) /// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall) /// Stack: [args...] -> [results...] (depends on syscall)
Syscall = 0x70, Syscall = 0x70,

View File

@ -7,4 +7,7 @@ license.workspace = true
[dependencies] [dependencies]
prometeu-vm = { path = "../prometeu-vm" } prometeu-vm = { path = "../prometeu-vm" }
prometeu-system = { path = "../prometeu-system" } prometeu-system = { path = "../prometeu-system" }
prometeu-hal = { path = "../prometeu-hal" } prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]
prometeu-drivers = { path = "../prometeu-drivers" }

View File

@ -160,7 +160,46 @@ impl Firmware {
} }
pub fn load_cartridge(&mut self, cartridge: Cartridge) { 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; 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),
}
}
}

View File

@ -19,7 +19,7 @@ impl LaunchHubStep {
Ok(cartridge) => { Ok(cartridge) => {
// In the case of debug, we could pause here, but the requirement says // In the case of debug, we could pause here, but the requirement says
// for the Runtime to open the socket and wait. // for the Runtime to open the socket and wait.
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge })); return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
} }
Err(e) => { Err(e) => {
ctx.os.log( ctx.os.log(

View File

@ -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 crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::cartridge::{AppMode, Cartridge}; use prometeu_hal::cartridge::{AppMode, Cartridge};
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
@ -8,9 +8,14 @@ use prometeu_hal::window::Rect;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct LoadCartridgeStep { pub struct LoadCartridgeStep {
pub cartridge: Cartridge, pub cartridge: Cartridge,
init_error: Option<String>,
} }
impl LoadCartridgeStep { impl LoadCartridgeStep {
pub fn new(cartridge: Cartridge) -> Self {
Self { cartridge, init_error: None }
}
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) { pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log( ctx.os.log(
LogLevel::Info, LogLevel::Info,
@ -26,10 +31,15 @@ impl LoadCartridgeStep {
self.cartridge.assets.clone(), 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<FirmwareState> { pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
if let Some(error) = self.init_error.take() {
return Some(FirmwareState::AppCrashes(AppCrashesStep { error }));
}
if self.cartridge.app_mode == AppMode::System { if self.cartridge.app_mode == AppMode::System {
let id = ctx.hub.window_manager.add_window( let id = ctx.hub.window_manager.add_window(
self.cartridge.title.clone(), self.cartridge.title.clone(),

View File

@ -12,7 +12,7 @@ use prometeu_hal::tile::Tile;
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{HardwareBridge, InputSignals}; use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, expect_int}; 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::collections::HashMap;
use std::time::Instant; use std::time::Instant;
@ -171,7 +171,13 @@ impl VirtualMachineRuntime {
} }
/// Loads a cartridge into the PVM and resets the execution state. /// 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) { match vm.initialize(cartridge.program.clone(), &cartridge.entrypoint) {
Ok(_) => { Ok(_) => {
// Determines the numeric app_id // Determines the numeric app_id
@ -180,6 +186,7 @@ impl VirtualMachineRuntime {
self.current_cartridge_app_version = cartridge.app_version.clone(); self.current_cartridge_app_version = cartridge.app_version.clone();
self.current_cartridge_app_mode = cartridge.app_mode; self.current_cartridge_app_mode = cartridge.app_mode;
self.current_entrypoint = cartridge.entrypoint.clone(); self.current_entrypoint = cartridge.entrypoint.clone();
Ok(())
} }
Err(e) => { Err(e) => {
self.log( self.log(
@ -190,6 +197,7 @@ impl VirtualMachineRuntime {
); );
// Fail fast: no program is installed, no app id is switched. // Fail fast: no program is installed, no app id is switched.
// We don't update current_app_id or other fields. // 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<u8>, 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<u8>, syscalls: Vec<SyscallDecl>) -> Vec<u8> {
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::<Value>::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());
}
}

View File

@ -1,22 +1,22 @@
use crate::call_frame::CallFrame; use crate::call_frame::CallFrame;
use crate::heap::{CoroutineState, Heap}; use crate::heap::{CoroutineState, Heap};
use crate::object::ObjectKind; 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::scheduler::Scheduler;
use crate::verifier::Verifier; use crate::verifier::Verifier;
use crate::vm_init_error::{LoaderPatchError, VmInitError}; use crate::vm_init_error::{LoaderPatchError, VmInitError};
use crate::{HostContext, NativeInterface}; use crate::{HostContext, NativeInterface};
use prometeu_bytecode::HeapRef;
use prometeu_bytecode::decode_next; use prometeu_bytecode::decode_next;
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
use prometeu_bytecode::model::BytecodeModule; use prometeu_bytecode::model::BytecodeModule;
use prometeu_bytecode::HeapRef;
use prometeu_bytecode::ProgramImage; use prometeu_bytecode::ProgramImage;
use prometeu_bytecode::Value; use prometeu_bytecode::Value;
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
use prometeu_bytecode::{ use prometeu_bytecode::{
TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL, TRAP_OOB, TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_SYSCALL,
TRAP_STACK_UNDERFLOW, TRAP_TYPE, TrapInfo, 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; use prometeu_hal::vm_fault::VmFault;
fn patch_module_hostcalls( fn patch_module_hostcalls(
@ -35,19 +35,28 @@ fn patch_module_hostcalls(
let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?; let instr = decode_next(pc, &module.code).map_err(LoaderPatchError::DecodeFailed)?;
let next_pc = instr.next_pc; let next_pc = instr.next_pc;
if instr.opcode == OpCode::Hostcall { match instr.opcode {
let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?; OpCode::Hostcall => {
let Some(resolved_syscall) = resolved.get(sysc_index as usize) else { let sysc_index = instr.imm_u32().map_err(LoaderPatchError::DecodeFailed)?;
return Err(LoaderPatchError::HostcallIndexOutOfBounds { let Some(resolved_syscall) = resolved.get(sysc_index as usize) else {
pc, return Err(LoaderPatchError::HostcallIndexOutOfBounds {
sysc_index, pc,
syscalls_len: resolved.len(), sysc_index,
}); syscalls_len: resolved.len(),
}; });
};
used[sysc_index as usize] = true; used[sysc_index as usize] = true;
module.code[pc..pc + 2].copy_from_slice(&(OpCode::Syscall as u16).to_le_bytes()); 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()); 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; pc = next_pc;
@ -240,7 +249,7 @@ impl VirtualMachine {
breakpoints: std::collections::HashSet::new(), breakpoints: std::collections::HashSet::new(),
gc_alloc_threshold: 1024, // conservative default; tests may override gc_alloc_threshold: 1024, // conservative default; tests may override
last_gc_live_count: 0, last_gc_live_count: 0,
capabilities: ALL, capabilities: NONE,
yield_requested: false, yield_requested: false,
sleep_requested_until: None, sleep_requested_until: None,
current_tick: 0, current_tick: 0,
@ -1643,7 +1652,7 @@ mod tests {
use crate::HostReturn; use crate::HostReturn;
use prometeu_bytecode::model::{BytecodeModule, SyscallDecl}; use prometeu_bytecode::model::{BytecodeModule, SyscallDecl};
use prometeu_bytecode::{ 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; 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] #[test]
fn test_loader_patching_propagates_resolution_failure() { fn test_loader_patching_propagates_resolution_failure() {
let mut vm = VirtualMachine::default(); let mut vm = VirtualMachine::default();

View File

@ -2,6 +2,10 @@
pub enum LoaderPatchError { pub enum LoaderPatchError {
DecodeFailed(prometeu_bytecode::DecodeError), DecodeFailed(prometeu_bytecode::DecodeError),
ResolveFailed(prometeu_hal::syscalls::DeclaredLoadError), ResolveFailed(prometeu_hal::syscalls::DeclaredLoadError),
RawSyscallInPreloadArtifact {
pc: usize,
syscall_id: u32,
},
HostcallIndexOutOfBounds { HostcallIndexOutOfBounds {
pc: usize, pc: usize,
sysc_index: u32, sysc_index: u32,

View File

@ -45,7 +45,7 @@ impl HostDebugger {
// Pre-load cartridge metadata so the Handshake message can contain // Pre-load cartridge metadata so the Handshake message can contain
// valid information about the App being debugged. // valid information about the App being debugged.
if let Ok(cartridge) = CartridgeLoader::load(path) { 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)) { match TcpListener::bind(format!("127.0.0.1:{}", debug_port)) {

View File

@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::Result;
use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{ use prometeu_bytecode::model::{
BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl,
}; };
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@ -12,6 +12,36 @@ fn asm(s: &str) -> Vec<u8> {
pub fn generate() -> Result<()> { pub fn generate() -> Result<()> {
let mut rom: Vec<u8> = Vec::new(); let mut rom: Vec<u8> = 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); heavy_load(&mut rom);
// light_load(&mut rom); // light_load(&mut rom);
@ -38,7 +68,7 @@ pub fn generate() -> Result<()> {
function_names: vec![(0, "main".into())], function_names: vec![(0, "main".into())],
}), }),
exports: vec![Export { symbol: "main".into(), func_idx: 0 }], exports: vec![Export { symbol: "main".into(), func_idx: 0 }],
syscalls: vec![], syscalls,
}; };
let bytes = module.serialize(); let bytes = module.serialize();
@ -54,9 +84,7 @@ pub fn generate() -> Result<()> {
if !out_dir.join("assets.pa").exists() { if !out_dir.join("assets.pa").exists() {
fs::write(out_dir.join("assets.pa"), &[] as &[u8])?; 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 \"capabilities\": [\"gfx\", \"log\"]\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}\n")?;
}
Ok(()) Ok(())
} }
@ -86,7 +114,7 @@ fn heavy_load(mut rom: &mut Vec<u8>) {
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0"));
// --- clear screen --- // --- clear screen ---
rom.extend(asm("PUSH_I32 0\nSYSCALL 0x1010")); rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
// --- draw 500 discs --- // --- draw 500 discs ---
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
@ -105,8 +133,8 @@ fn heavy_load(mut rom: &mut Vec<u8>) {
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND")); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND"));
// fill color = (i * 5678 + t) & 0xFFFF // 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")); 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) // HOSTCALL gfx.draw_disc (x, y, r, border, fill)
rom.extend(asm("SYSCALL 0x1005")); rom.extend(asm("HOSTCALL 1"));
// i++ // i++
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
@ -140,8 +168,8 @@ fn heavy_load(mut rom: &mut Vec<u8>) {
// color = (t * 10 + i * 1000) & 0xFFFF // 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")); 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) // HOSTCALL gfx.draw_text (x, y, str, color)
rom.extend(asm("SYSCALL 0x1008")); rom.extend(asm("HOSTCALL 2"));
// i++ // i++
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
@ -153,7 +181,7 @@ fn heavy_load(mut rom: &mut Vec<u8>) {
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
let jif_log_offset = rom.len() + 2; let jif_log_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0")); 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; let after_log = rom.len() as u32;
// --- end of function --- // --- end of function ---

View File

@ -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. - `PUSH_SCOPE`, `POP_SCOPE` — begin/end lexical scope.
- System/Timing: - 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. - `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. For exact immediates and stack effects, see `CoreOpCode::spec()` which is the single source of truth used by the decoder, disassembler, and (later) verifier.

View File

@ -88,9 +88,11 @@ Example:
At cartridge load time: At cartridge load time:
1. The cartridge declares required syscalls using canonical identities. 1. The cartridge declares required syscalls using canonical identities.
2. The host verifies capabilities. 2. The cartridge bytecode encodes host-backed call sites as `HOSTCALL <sysc_index>` into that declaration table.
3. The host resolves each canonical identity to a numeric `syscall_id`. 3. The host verifies capabilities from the cartridge manifest before execution begins.
4. The VM stores the resolved table for fast execution. 4. The host resolves each canonical identity to a numeric `syscall_id`.
5. The loader rewrites every `HOSTCALL <sysc_index>` into `SYSCALL <id>`.
6. The VM stores the patched executable image for fast execution.
At runtime, the VM executes: At runtime, the VM executes:
@ -99,12 +101,21 @@ SYSCALL <id>
``` ```
Only numeric IDs are used during execution. Only numeric IDs are used during execution.
Raw `SYSCALL <id>` is not valid in a PBX pre-load artifact and must be rejected by the loader.
--- ---
## 4 Syscall Instruction Semantics ## 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 <sysc_index>
```
Final executable image:
``` ```
SYSCALL <id> SYSCALL <id>
@ -112,13 +123,14 @@ SYSCALL <id>
Where: Where:
* `<sysc_index>` is an index into the program-declared syscall table.
* `<id>` is a 32-bit integer identifying the syscall. * `<id>` is a 32-bit integer identifying the syscall.
Execution steps: Execution steps:
1. The VM looks up syscall metadata using `<id>`. 1. The VM looks up syscall metadata using `<id>`.
2. The VM verifies that enough arguments exist on the stack. 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. 4. The syscall executes in the host environment.
5. The syscall leaves exactly `ret_slots` values on the stack. 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 | | `may_allocate` | Whether the syscall may allocate VM heap objects |
| `cost_hint` | Expected cycle cost | | `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) > < [Back](chapter-15.md) | [Summary](table-of-contents.md) >

View File

@ -5,5 +5,6 @@
"title": "Stress Console", "title": "Stress Console",
"app_version": "0.1.0", "app_version": "0.1.0",
"app_mode": "Game", "app_mode": "Game",
"entrypoint": "main" "entrypoint": "main",
"capabilities": ["gfx", "log"]
} }