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
- 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/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
@ -97,7 +97,7 @@ The verifier statically checks bytecode for structural safety and stackshape
- 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 stackshape
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.
- 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 stackshape
-----------------------
- 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
- `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 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
- 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
View File

@ -1630,6 +1630,7 @@ dependencies = [
name = "prometeu-firmware"
version = "0.1.0"
dependencies = [
"prometeu-drivers",
"prometeu-hal",
"prometeu-system",
"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
/// 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,

View File

@ -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" }

View File

@ -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),
}
}
}

View File

@ -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(

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 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(),

View File

@ -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());
}
}

View File

@ -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();

View File

@ -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,

View File

@ -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)) {

View File

@ -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 ---

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.
- 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.

View File

@ -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) >

View File

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