implements remain of capabilities
This commit is contained in:
parent
c6e3199821
commit
39d1214f03
@ -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 <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 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
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1630,6 +1630,7 @@ dependencies = [
|
||||
name = "prometeu-firmware"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-drivers",
|
||||
"prometeu-hal",
|
||||
"prometeu-system",
|
||||
"prometeu-vm",
|
||||
|
||||
@ -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 <id>` 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,
|
||||
|
||||
@ -7,4 +7,7 @@ license.workspace = true
|
||||
[dependencies]
|
||||
prometeu-vm = { path = "../prometeu-vm" }
|
||||
prometeu-system = { path = "../prometeu-system" }
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
|
||||
[dev-dependencies]
|
||||
prometeu-drivers = { path = "../prometeu-drivers" }
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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<String>,
|
||||
}
|
||||
|
||||
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<FirmwareState> {
|
||||
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(),
|
||||
|
||||
@ -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<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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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<u8> {
|
||||
|
||||
pub fn generate() -> Result<()> {
|
||||
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);
|
||||
// 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<u8>) {
|
||||
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<u8>) {
|
||||
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<u8>) {
|
||||
|
||||
// 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<u8>) {
|
||||
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 ---
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 <sysc_index>` 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 <sysc_index>` into `SYSCALL <id>`.
|
||||
6. The VM stores the patched executable image for fast execution.
|
||||
|
||||
At runtime, the VM executes:
|
||||
|
||||
@ -99,12 +101,21 @@ SYSCALL <id>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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>
|
||||
@ -112,13 +123,14 @@ SYSCALL <id>
|
||||
|
||||
Where:
|
||||
|
||||
* `<sysc_index>` is an index into the program-declared syscall table.
|
||||
* `<id>` is a 32-bit integer identifying the syscall.
|
||||
|
||||
Execution steps:
|
||||
|
||||
1. The VM looks up syscall metadata using `<id>`.
|
||||
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) >
|
||||
< [Back](chapter-15.md) | [Summary](table-of-contents.md) >
|
||||
|
||||
@ -5,5 +5,6 @@
|
||||
"title": "Stress Console",
|
||||
"app_version": "0.1.0",
|
||||
"app_mode": "Game",
|
||||
"entrypoint": "main"
|
||||
"entrypoint": "main",
|
||||
"capabilities": ["gfx", "log"]
|
||||
}
|
||||
|
||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user