This commit is contained in:
Nilton Constantino 2026-01-31 18:21:16 +00:00
parent 33908aa828
commit be1c244b5a
No known key found for this signature in database
5 changed files with 274 additions and 23 deletions

View File

@ -212,4 +212,49 @@ impl Syscall {
_ => 1, _ => 1,
} }
} }
pub fn name(&self) -> &'static str {
match self {
Self::SystemHasCart => "SystemHasCart",
Self::SystemRunCart => "SystemRunCart",
Self::GfxClear => "GfxClear",
Self::GfxFillRect => "GfxFillRect",
Self::GfxDrawLine => "GfxDrawLine",
Self::GfxDrawCircle => "GfxDrawCircle",
Self::GfxDrawDisc => "GfxDrawDisc",
Self::GfxDrawSquare => "GfxDrawSquare",
Self::GfxSetSprite => "GfxSetSprite",
Self::GfxDrawText => "GfxDrawText",
Self::GfxClear565 => "GfxClear565",
Self::InputGetPad => "InputGetPad",
Self::InputGetPadPressed => "InputGetPadPressed",
Self::InputGetPadReleased => "InputGetPadReleased",
Self::InputGetPadHold => "InputGetPadHold",
Self::InputPadSnapshot => "InputPadSnapshot",
Self::InputTouchSnapshot => "InputTouchSnapshot",
Self::TouchGetX => "TouchGetX",
Self::TouchGetY => "TouchGetY",
Self::TouchIsDown => "TouchIsDown",
Self::TouchIsPressed => "TouchIsPressed",
Self::TouchIsReleased => "TouchIsReleased",
Self::TouchGetHold => "TouchGetHold",
Self::AudioPlaySample => "AudioPlaySample",
Self::AudioPlay => "AudioPlay",
Self::FsOpen => "FsOpen",
Self::FsRead => "FsRead",
Self::FsWrite => "FsWrite",
Self::FsClose => "FsClose",
Self::FsListDir => "FsListDir",
Self::FsExists => "FsExists",
Self::FsDelete => "FsDelete",
Self::LogWrite => "LogWrite",
Self::LogWriteTag => "LogWriteTag",
Self::AssetLoad => "AssetLoad",
Self::AssetStatus => "AssetStatus",
Self::AssetCommit => "AssetCommit",
Self::AssetCancel => "AssetCancel",
Self::BankInfo => "BankInfo",
Self::BankSlotInfo => "BankSlotInfo",
}
}
} }

View File

@ -311,4 +311,19 @@ mod tests {
let res = Verifier::verify(&code, &functions).unwrap(); let res = Verifier::verify(&code, &functions).unwrap();
assert_eq!(res[0], 2); assert_eq!(res[0], 2);
} }
#[test]
fn test_verifier_invalid_syscall_id() {
let mut code = Vec::new();
code.push(OpCode::Syscall as u8); code.push(0x00);
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: 6,
..Default::default()
}];
let res = Verifier::verify(&code, &functions);
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
}
} }

View File

@ -999,6 +999,7 @@ impl VirtualMachine {
} }
args.reverse(); args.reverse();
let stack_height_before = self.operand_stack.len();
let mut ret = crate::virtual_machine::HostReturn::new(&mut self.operand_stack); let mut ret = crate::virtual_machine::HostReturn::new(&mut self.operand_stack);
native.syscall(id, &args, &mut ret, hw).map_err(|fault| match fault { native.syscall(id, &args, &mut ret, hw).map_err(|fault| match fault {
crate::virtual_machine::VmFault::Trap(code, msg) => LogicalFrameEndingReason::Trap(TrapInfo { crate::virtual_machine::VmFault::Trap(code, msg) => LogicalFrameEndingReason::Trap(TrapInfo {
@ -1009,6 +1010,15 @@ impl VirtualMachine {
}), }),
crate::virtual_machine::VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg), crate::virtual_machine::VmFault::Panic(msg) => LogicalFrameEndingReason::Panic(msg),
})?; })?;
let stack_height_after = self.operand_stack.len();
let results_pushed = stack_height_after - stack_height_before;
if results_pushed != syscall.results_count() {
return Err(LogicalFrameEndingReason::Panic(format!(
"Syscall {} (0x{:08X}) results mismatch: expected {}, got {}",
syscall.name(), id, syscall.results_count(), results_pushed
)));
}
} }
OpCode::FrameSync => { OpCode::FrameSync => {
return Ok(()); return Ok(());
@ -1929,6 +1939,37 @@ mod tests {
} }
} }
#[test]
fn test_syscall_results_count_mismatch_panic() {
// GfxClear565 (0x1010) expects 0 results
let rom = vec![
0x17, 0x00, // PushI32
0x00, 0x00, 0x00, 0x00, // value 0
0x70, 0x00, // Syscall + Reserved
0x10, 0x10, 0x00, 0x00, // Syscall ID 0x1010
];
struct BadNative;
impl NativeInterface for BadNative {
fn syscall(&mut self, _id: u32, _args: &[Value], ret: &mut HostReturn, _hw: &mut dyn HardwareBridge) -> Result<(), VmFault> {
// Wrong: GfxClear565 is void but we push something
ret.push_int(42);
Ok(())
}
}
let mut vm = VirtualMachine::new(rom, vec![]);
let mut native = BadNative;
let mut hw = MockHardware;
vm.prepare_call("0");
let report = vm.run_budget(100, &mut native, &mut hw).unwrap();
match report.reason {
LogicalFrameEndingReason::Panic(msg) => assert!(msg.contains("results mismatch")),
_ => panic!("Expected Panic, got {:?}", report.reason),
}
}
#[test] #[test]
fn test_host_return_bounded_overflow_trap() { fn test_host_return_bounded_overflow_trap() {
let mut stack = Vec::new(); let mut stack = Vec::new();

View File

@ -0,0 +1,173 @@
# Runtime Traps v0 — Prometeu VM Specification
> **Status:** Proposed (requires explicit owner approval)
>
> **Scope:** Prometeu VM / PBS v0 execution model
---
## 1. Motivation
Prometeu aims to be a **deterministic, industrial-grade virtual machine**.
To achieve this, execution errors that are:
* caused by **user programs**,
* predictable by the execution model,
* and recoverable at the tooling / host level,
must be **explicitly represented** and **ABI-stable**.
This specification introduces **Runtime Traps** as a *formal concept*, consolidating behavior that already existed implicitly in the VM.
---
## 2. Definition
A **Runtime Trap** is a **controlled interruption of program execution** caused by a semantic violation detected at runtime.
A trap:
* **terminates the current execution frame** (or program, depending on host policy)
* **does not corrupt VM state**
* **returns structured diagnostic information** (`TrapInfo`)
* **is deterministic** for a given bytecode + state
A trap is **not**:
* a debugger breakpoint
* undefined behavior
* a VM panic
* a verifier/load-time error
---
## 3. Trap vs Other Failure Modes
| Category | When | Recoverable | ABI-stable | Example |
| ------------------ | ---------------------- | ----------- | ---------- | -------------------------------- |
| **Verifier error** | Load-time | ❌ | ❌ | Stack underflow, bad CFG join |
| **Runtime trap** | Execution | ✅ | ✅ | OOB access, invalid local |
| **VM panic** | VM invariant violation | ❌ | ❌ | Handler returns wrong slot count |
| **Breakpoint** | Debug only | ✅ | ❌ | Developer inspection |
---
## 4. Trap Information (`TrapInfo`)
All runtime traps must produce a `TrapInfo` structure with the following fields:
```text
TrapInfo {
code: u32, // ABI-stable trap code
opcode: u16, // opcode that triggered the trap
pc: u32, // program counter (relative to module)
message: String, // human-readable explanation (non-ABI)
}
```
### ABI Guarantees
* `code`, `opcode`, and `pc` are ABI-relevant and stable
* `message` is diagnostic only and may change
---
## 5. Standard Trap Codes (v0)
### 5.1 Memory & Bounds
| Code | Name | Meaning |
| -------------------- | ------------- | ------------------------------ |
| `TRAP_OOB` | Out of bounds | Access beyond allowed bounds |
| `TRAP_INVALID_LOCAL` | Invalid local | Local slot index out of bounds |
### 5.2 Heap / Gate
| Code | Name | Meaning |
| ------------------- | -------------- | -------------------------- |
| `TRAP_INVALID_GATE` | Invalid gate | Non-existent gate handle |
| `TRAP_DEAD_GATE` | Dead gate | Gate with refcount = 0 |
| `TRAP_TYPE` | Type violation | Heap or gate type mismatch |
### 5.3 System
| Code | Name | Meaning |
| ---------------------- | --------------- | --------------------------------------- |
| `TRAP_INVALID_SYSCALL` | Invalid syscall | Unknown syscall ID |
| `TRAP_STACK_UNDERFLOW` | Stack underflow | Missing arguments for syscall or opcode |
> This list is **closed for PBS v0** unless explicitly extended.
---
## 6. Trap Semantics
### 6.1 Execution
When a trap occurs:
1. The current instruction **does not complete**
2. No partial side effects are committed
3. Execution stops and returns `TrapInfo` to the host
### 6.2 Stack & Frames
* Operand stack is left in a **valid but unspecified** state
* Call frames above the trapping frame are not resumed
### 6.3 Host Policy
The host decides:
* whether the trap terminates the whole program
* whether execution may be restarted
* how the trap is surfaced to the user (error, log, UI, etc.)
---
## 7. Verifier Interaction
The verifier **must prevent** traps that are statically provable, including:
* stack underflow
* invalid control-flow joins
* invalid syscall IDs
* incorrect return slot counts
If a verifier rejects a module, **no runtime traps should occur for those causes**.
---
## 8. What Is *Not* a Trap
The following are **VM bugs or tooling errors**, not traps:
* handler returns wrong number of slots
* opcode implementation violates `OpcodeSpec`
* verifier and runtime disagree on stack effects
These must result in **VM panic**, not a trap.
---
## 9. Versioning Policy
* Trap codes are **ABI-stable within a major version** (v0)
* New trap codes may only be added in a **new major ABI version** (v1)
* Removing or reinterpreting trap codes is forbidden
---
## 10. Summary
Runtime traps are:
* an explicit part of the Prometeu execution model
* deterministic and ABI-stable
* reserved for **user-program semantic errors**
They are **not** debugging tools and **not** VM panics.
This spec formalizes existing behavior and freezes it for PBS v0.
---

View File

@ -1,26 +1,3 @@
## PR-09 — Debug info v0: spans, symbols, and traceable traps
**Why:** Industrial debugging requires actionable failures.
### Scope
* Add optional debug section:
* per-instruction span table (`pc -> (file_id, start, end)`)
* function names
* Enhance trap payload with debug span (if present)
### Tests
* trap includes span when debug present
* trap still works without debug
### Acceptance
* You can pinpoint “where” a trap happened reliably.
---
## PR-10 — Program image + linker: imports/exports resolved before VM run ## PR-10 — Program image + linker: imports/exports resolved before VM run
**Why:** Imports are compile-time, but we need an industrial linking model for multi-module PBS. **Why:** Imports are compile-time, but we need an industrial linking model for multi-module PBS.