pr 44
This commit is contained in:
parent
e784dab34e
commit
13e58a8efd
@ -19,7 +19,7 @@ pub fn operand_size(opcode: OpCode) -> usize {
|
|||||||
OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => 4,
|
OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue => 4,
|
||||||
OpCode::GetGlobal | OpCode::SetGlobal => 4,
|
OpCode::GetGlobal | OpCode::SetGlobal => 4,
|
||||||
OpCode::GetLocal | OpCode::SetLocal => 4,
|
OpCode::GetLocal | OpCode::SetLocal => 4,
|
||||||
OpCode::Call => 8, // addr(u32) + args_count(u32)
|
OpCode::Call => 4, // func_id(u32)
|
||||||
OpCode::Syscall => 4,
|
OpCode::Syscall => 4,
|
||||||
OpCode::Alloc => 8, // type_id(u32) + slots(u32)
|
OpCode::Alloc => 8, // type_id(u32) + slots(u32)
|
||||||
OpCode::GateLoad | OpCode::GateStore => 4, // offset(u32)
|
OpCode::GateLoad | OpCode::GateStore => 4, // offset(u32)
|
||||||
@ -41,6 +41,8 @@ pub const TRAP_TYPE: u32 = 0x04;
|
|||||||
pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007;
|
pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007;
|
||||||
/// Not enough arguments on the stack for the requested syscall.
|
/// Not enough arguments on the stack for the requested syscall.
|
||||||
pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008;
|
pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008;
|
||||||
|
/// Attempted to access a local slot that is out of bounds for the current frame.
|
||||||
|
pub const TRAP_INVALID_LOCAL: u32 = 0x0000_0009;
|
||||||
|
|
||||||
/// Detailed information about a runtime trap.
|
/// Detailed information about a runtime trap.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -81,6 +83,7 @@ mod tests {
|
|||||||
assert_eq!(TRAP_TYPE, 0x04);
|
assert_eq!(TRAP_TYPE, 0x04);
|
||||||
assert_eq!(TRAP_INVALID_SYSCALL, 0x07);
|
assert_eq!(TRAP_INVALID_SYSCALL, 0x07);
|
||||||
assert_eq!(TRAP_STACK_UNDERFLOW, 0x08);
|
assert_eq!(TRAP_STACK_UNDERFLOW, 0x08);
|
||||||
|
assert_eq!(TRAP_INVALID_LOCAL, 0x09);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -96,6 +99,7 @@ HIP Traps:
|
|||||||
System Traps:
|
System Traps:
|
||||||
- INVALID_SYSCALL (0x07): Unknown syscall ID.
|
- INVALID_SYSCALL (0x07): Unknown syscall ID.
|
||||||
- STACK_UNDERFLOW (0x08): Missing syscall arguments.
|
- STACK_UNDERFLOW (0x08): Missing syscall arguments.
|
||||||
|
- INVALID_LOCAL (0x09): Local slot out of bounds.
|
||||||
|
|
||||||
Operand Sizes:
|
Operand Sizes:
|
||||||
- Alloc: 8 bytes (u32 type_id, u32 slots)
|
- Alloc: 8 bytes (u32 type_id, u32 slots)
|
||||||
@ -106,9 +110,9 @@ Operand Sizes:
|
|||||||
// This test serves as a "doc-lock".
|
// This test serves as a "doc-lock".
|
||||||
// If you change the ABI, you must update this string.
|
// If you change the ABI, you must update this string.
|
||||||
let current_info = format!(
|
let current_info = format!(
|
||||||
"\nHIP Traps:\n- INVALID_GATE (0x{:02X}): Non-existent gate handle.\n- DEAD_GATE (0x{:02X}): Gate handle with RC=0.\n- OOB (0x{:02X}): Access beyond allocated slots.\n- TYPE (0x{:02X}): Type mismatch during heap access.\n\nSystem Traps:\n- INVALID_SYSCALL (0x{:02X}): Unknown syscall ID.\n- STACK_UNDERFLOW (0x{:02X}): Missing syscall arguments.\n\nOperand Sizes:\n- Alloc: {} bytes (u32 type_id, u32 slots)\n- GateLoad: {} bytes (u32 offset)\n- GateStore: {} bytes (u32 offset)\n- PopN: {} bytes (u32 count)\n",
|
"\nHIP Traps:\n- INVALID_GATE (0x{:02X}): Non-existent gate handle.\n- DEAD_GATE (0x{:02X}): Gate handle with RC=0.\n- OOB (0x{:02X}): Access beyond allocated slots.\n- TYPE (0x{:02X}): Type mismatch during heap access.\n\nSystem Traps:\n- INVALID_SYSCALL (0x{:02X}): Unknown syscall ID.\n- STACK_UNDERFLOW (0x{:02X}): Missing syscall arguments.\n- INVALID_LOCAL (0x{:02X}): Local slot out of bounds.\n\nOperand Sizes:\n- Alloc: {} bytes (u32 type_id, u32 slots)\n- GateLoad: {} bytes (u32 offset)\n- GateStore: {} bytes (u32 offset)\n- PopN: {} bytes (u32 count)\n",
|
||||||
TRAP_INVALID_GATE, TRAP_DEAD_GATE, TRAP_OOB, TRAP_TYPE,
|
TRAP_INVALID_GATE, TRAP_DEAD_GATE, TRAP_OOB, TRAP_TYPE,
|
||||||
TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW,
|
TRAP_INVALID_SYSCALL, TRAP_STACK_UNDERFLOW, TRAP_INVALID_LOCAL,
|
||||||
operand_size(OpCode::Alloc),
|
operand_size(OpCode::Alloc),
|
||||||
operand_size(OpCode::GateLoad),
|
operand_size(OpCode::GateLoad),
|
||||||
operand_size(OpCode::GateStore),
|
operand_size(OpCode::GateStore),
|
||||||
|
|||||||
29
crates/prometeu-core/src/virtual_machine/local_addressing.rs
Normal file
29
crates/prometeu-core/src/virtual_machine/local_addressing.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
use crate::virtual_machine::call_frame::CallFrame;
|
||||||
|
use prometeu_bytecode::v0::FunctionMeta;
|
||||||
|
use prometeu_bytecode::abi::{TrapInfo, TRAP_INVALID_LOCAL};
|
||||||
|
|
||||||
|
/// Computes the absolute stack index for the start of the current frame's locals (including args).
|
||||||
|
pub fn local_base(frame: &CallFrame) -> usize {
|
||||||
|
frame.stack_base
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the absolute stack index for a given local slot.
|
||||||
|
pub fn local_index(frame: &CallFrame, slot: u32) -> usize {
|
||||||
|
frame.stack_base + slot as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a local slot index is within the valid range for the function.
|
||||||
|
/// Range: 0 <= slot < (param_slots + local_slots)
|
||||||
|
pub fn check_local_slot(meta: &FunctionMeta, slot: u32, opcode: u16, pc: u32) -> Result<(), TrapInfo> {
|
||||||
|
let limit = meta.param_slots as u32 + meta.local_slots as u32;
|
||||||
|
if slot < limit {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(TrapInfo {
|
||||||
|
code: TRAP_INVALID_LOCAL,
|
||||||
|
opcode,
|
||||||
|
message: format!("Local slot {} out of bounds for function (limit {})", slot, limit),
|
||||||
|
pc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ mod value;
|
|||||||
mod call_frame;
|
mod call_frame;
|
||||||
mod scope_frame;
|
mod scope_frame;
|
||||||
mod program;
|
mod program;
|
||||||
|
pub mod local_addressing;
|
||||||
pub mod opcode_spec;
|
pub mod opcode_spec;
|
||||||
pub mod bytecode;
|
pub mod bytecode;
|
||||||
pub mod verifier;
|
pub mod verifier;
|
||||||
|
|||||||
@ -28,6 +28,12 @@ pub enum LogicalFrameEndingReason {
|
|||||||
Panic(String),
|
Panic(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<TrapInfo> for LogicalFrameEndingReason {
|
||||||
|
fn from(info: TrapInfo) -> Self {
|
||||||
|
LogicalFrameEndingReason::Trap(info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A report detailing the results of an execution slice (run_budget).
|
/// A report detailing the results of an execution slice (run_budget).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct BudgetReport {
|
pub struct BudgetReport {
|
||||||
@ -201,6 +207,15 @@ impl VirtualMachine {
|
|||||||
self.operand_stack.clear();
|
self.operand_stack.clear();
|
||||||
self.call_stack.clear();
|
self.call_stack.clear();
|
||||||
self.scope_stack.clear();
|
self.scope_stack.clear();
|
||||||
|
|
||||||
|
// Entrypoint also needs locals allocated.
|
||||||
|
// For the sentinel frame, stack_base is always 0.
|
||||||
|
if let Some(func) = self.program.functions.get(func_idx) {
|
||||||
|
for _ in 0..func.local_slots {
|
||||||
|
self.operand_stack.push(Value::Null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.call_stack.push(CallFrame {
|
self.call_stack.push(CallFrame {
|
||||||
return_pc: self.program.rom.len() as u32,
|
return_pc: self.program.rom.len() as u32,
|
||||||
stack_base: 0,
|
stack_base: 0,
|
||||||
@ -606,26 +621,42 @@ impl VirtualMachine {
|
|||||||
self.globals[idx] = val;
|
self.globals[idx] = val;
|
||||||
}
|
}
|
||||||
OpCode::GetLocal => {
|
OpCode::GetLocal => {
|
||||||
let idx = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
let slot = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
||||||
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
||||||
let val = self.operand_stack.get(frame.stack_base + idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Invalid local index".into()))?;
|
let func = &self.program.functions[frame.func_idx];
|
||||||
|
|
||||||
|
crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)?;
|
||||||
|
|
||||||
|
let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot);
|
||||||
|
let val = self.operand_stack.get(stack_idx).cloned().ok_or_else(|| LogicalFrameEndingReason::Panic("Internal error: validated local slot not found in stack".into()))?;
|
||||||
self.push(val);
|
self.push(val);
|
||||||
}
|
}
|
||||||
OpCode::SetLocal => {
|
OpCode::SetLocal => {
|
||||||
let idx = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
let slot = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
||||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
let frame = self.call_stack.last().ok_or_else(|| LogicalFrameEndingReason::Panic("No active call frame".into()))?;
|
||||||
let stack_idx = frame.stack_base + idx;
|
let func = &self.program.functions[frame.func_idx];
|
||||||
if stack_idx >= self.operand_stack.len() {
|
|
||||||
return Err(LogicalFrameEndingReason::Panic("Local index out of bounds".into()));
|
crate::virtual_machine::local_addressing::check_local_slot(func, slot, opcode as u16, start_pc as u32)?;
|
||||||
}
|
|
||||||
|
let stack_idx = crate::virtual_machine::local_addressing::local_index(frame, slot);
|
||||||
self.operand_stack[stack_idx] = val;
|
self.operand_stack[stack_idx] = val;
|
||||||
}
|
}
|
||||||
OpCode::Call => {
|
OpCode::Call => {
|
||||||
let func_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
let func_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
||||||
let callee = self.program.functions.get(func_id).ok_or_else(|| LogicalFrameEndingReason::Panic(format!("Invalid func_id {}", func_id)))?;
|
let callee = self.program.functions.get(func_id).ok_or_else(|| LogicalFrameEndingReason::Panic(format!("Invalid func_id {}", func_id)))?;
|
||||||
|
|
||||||
|
if self.operand_stack.len() < callee.param_slots as usize {
|
||||||
|
return Err(LogicalFrameEndingReason::Panic("Stack underflow during CALL: not enough arguments".into()));
|
||||||
|
}
|
||||||
|
|
||||||
let stack_base = self.operand_stack.len() - callee.param_slots as usize;
|
let stack_base = self.operand_stack.len() - callee.param_slots as usize;
|
||||||
|
|
||||||
|
// Allocate and zero-init local_slots
|
||||||
|
for _ in 0..callee.local_slots {
|
||||||
|
self.operand_stack.push(Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
self.call_stack.push(CallFrame {
|
self.call_stack.push(CallFrame {
|
||||||
return_pc: self.pc as u32,
|
return_pc: self.pc as u32,
|
||||||
stack_base,
|
stack_base,
|
||||||
@ -1682,4 +1713,137 @@ mod tests {
|
|||||||
let res = vm.initialize(pbc, "");
|
let res = vm.initialize(pbc, "");
|
||||||
assert!(res.is_ok(), "Should NOT fail if Call pattern is in immediate: {:?}", res);
|
assert!(res.is_ok(), "Should NOT fail if Call pattern is in immediate: {:?}", res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_locals_round_trip() {
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut hw = MockHardware;
|
||||||
|
|
||||||
|
// PUSH_I32 42
|
||||||
|
// SET_LOCAL 0
|
||||||
|
// PUSH_I32 0 (garbage)
|
||||||
|
// GET_LOCAL 0
|
||||||
|
// RET (1 slot)
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&42i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let mut vm = VirtualMachine::new(rom, vec![]);
|
||||||
|
vm.program.functions = std::sync::Arc::from(vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: 18,
|
||||||
|
local_slots: 1,
|
||||||
|
return_slots: 1,
|
||||||
|
..Default::default()
|
||||||
|
}]);
|
||||||
|
|
||||||
|
vm.prepare_call("0");
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut hw).unwrap();
|
||||||
|
assert_eq!(report.reason, LogicalFrameEndingReason::EndOfRom);
|
||||||
|
// RET pops return values and pushes them back on the caller stack (which is the sentinel frame's stack here).
|
||||||
|
assert_eq!(vm.operand_stack, vec![Value::Int32(42)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_locals_per_call_isolation() {
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut hw = MockHardware;
|
||||||
|
|
||||||
|
// Function 0 (entry):
|
||||||
|
// CALL 1
|
||||||
|
// POP
|
||||||
|
// CALL 1
|
||||||
|
// HALT
|
||||||
|
// Function 1:
|
||||||
|
// GET_LOCAL 0 (should be Null initially)
|
||||||
|
// PUSH_I32 42
|
||||||
|
// SET_LOCAL 0
|
||||||
|
// RET (1 slot: the initial Null)
|
||||||
|
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
// F0
|
||||||
|
let f0_start = 0;
|
||||||
|
rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Pop as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Call as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||||
|
let f0_len = rom.len() - f0_start;
|
||||||
|
|
||||||
|
// F1
|
||||||
|
let f1_start = rom.len() as u32;
|
||||||
|
rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&42i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::SetLocal as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Ret as u16).to_le_bytes());
|
||||||
|
let f1_len = rom.len() as u32 - f1_start;
|
||||||
|
|
||||||
|
let mut vm = VirtualMachine::new(rom, vec![]);
|
||||||
|
vm.program.functions = std::sync::Arc::from(vec![
|
||||||
|
FunctionMeta {
|
||||||
|
code_offset: f0_start as u32,
|
||||||
|
code_len: f0_len as u32,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
FunctionMeta {
|
||||||
|
code_offset: f1_start,
|
||||||
|
code_len: f1_len,
|
||||||
|
local_slots: 1,
|
||||||
|
return_slots: 1,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
vm.prepare_call("0");
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut hw).unwrap();
|
||||||
|
assert_eq!(report.reason, LogicalFrameEndingReason::Halted);
|
||||||
|
|
||||||
|
// The last value on stack is the return of the second CALL 1,
|
||||||
|
// which should be Value::Null because locals are zero-initialized on each call.
|
||||||
|
assert_eq!(vm.operand_stack.last().unwrap(), &Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_local_index_traps() {
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut hw = MockHardware;
|
||||||
|
|
||||||
|
// Function with 0 params, 1 local.
|
||||||
|
// GET_LOCAL 1 (OOB)
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
rom.extend_from_slice(&(OpCode::GetLocal as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let mut vm = VirtualMachine::new(rom, vec![]);
|
||||||
|
vm.program.functions = std::sync::Arc::from(vec![FunctionMeta {
|
||||||
|
code_offset: 0,
|
||||||
|
code_len: 8,
|
||||||
|
local_slots: 1,
|
||||||
|
..Default::default()
|
||||||
|
}]);
|
||||||
|
|
||||||
|
vm.prepare_call("0");
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut hw).unwrap();
|
||||||
|
|
||||||
|
match report.reason {
|
||||||
|
LogicalFrameEndingReason::Trap(trap) => {
|
||||||
|
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_INVALID_LOCAL);
|
||||||
|
assert_eq!(trap.opcode, OpCode::GetLocal as u16);
|
||||||
|
assert!(trap.message.contains("out of bounds"));
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Trap, got {:?}", report.reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,3 @@
|
|||||||
## PR-04 — Locals opcodes: GET_LOCAL / SET_LOCAL / INIT_LOCAL
|
|
||||||
|
|
||||||
**Why:** PBS `let` and parameters need first-class support.
|
|
||||||
|
|
||||||
### Scope
|
|
||||||
|
|
||||||
* Implement opcodes:
|
|
||||||
|
|
||||||
* `GET_LOCAL <u16 slot>` pushes value slots
|
|
||||||
* `SET_LOCAL <u16 slot>` pops value slots and writes
|
|
||||||
* `INIT_LOCAL <u16 slot>` (optional) for explicit initialization semantics
|
|
||||||
* Enforce bounds: local slot index must be within `[0..param+local_slots)`
|
|
||||||
* Enforce slot width: if types are multi-slot, compiler emits multiple GET/SET or uses `*_N` variants.
|
|
||||||
|
|
||||||
### Deliverables
|
|
||||||
|
|
||||||
* `LocalAddressing` utilities
|
|
||||||
* Deterministic trap codes:
|
|
||||||
|
|
||||||
* `TRAP_INVALID_LOCAL`
|
|
||||||
* `TRAP_LOCAL_WIDTH_MISMATCH` (if enforced)
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
* `let x: int = 1; return x;` works
|
|
||||||
* invalid local index traps
|
|
||||||
|
|
||||||
### Acceptance
|
|
||||||
|
|
||||||
* `let` works reliably; no stack side effects beyond specified pops/pushes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR-05 — Core arithmetic + comparisons in VM (int/bounded/bool)
|
## PR-05 — Core arithmetic + comparisons in VM (int/bounded/bool)
|
||||||
|
|
||||||
**Why:** The minimal executable PBS needs arithmetic that doesn’t corrupt stack.
|
**Why:** The minimal executable PBS needs arithmetic that doesn’t corrupt stack.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user