introduce gate Id and heap index
This commit is contained in:
parent
937e0d70b6
commit
0a087d51fb
@ -2,7 +2,10 @@ use crate::call_frame::CallFrame;
|
|||||||
use crate::scope_frame::ScopeFrame;
|
use crate::scope_frame::ScopeFrame;
|
||||||
use crate::Value;
|
use crate::Value;
|
||||||
use crate::{HostContext, NativeInterface, ProgramImage, VmInitError};
|
use crate::{HostContext, NativeInterface, ProgramImage, VmInitError};
|
||||||
use prometeu_bytecode::abi::{TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_OOB, TRAP_TYPE};
|
use prometeu_bytecode::abi::{
|
||||||
|
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DEAD_GATE, TRAP_DIV_ZERO, TRAP_INVALID_FUNC, TRAP_INVALID_GATE, TRAP_OOB,
|
||||||
|
TRAP_TYPE,
|
||||||
|
};
|
||||||
use prometeu_bytecode::opcode::OpCode;
|
use prometeu_bytecode::opcode::OpCode;
|
||||||
|
|
||||||
/// Reason why the Virtual Machine stopped execution during a specific run.
|
/// Reason why the Virtual Machine stopped execution during a specific run.
|
||||||
@ -80,6 +83,8 @@ pub struct VirtualMachine {
|
|||||||
pub program: ProgramImage,
|
pub program: ProgramImage,
|
||||||
/// Heap Memory: Dynamic allocation pool.
|
/// Heap Memory: Dynamic allocation pool.
|
||||||
pub heap: Vec<Value>,
|
pub heap: Vec<Value>,
|
||||||
|
/// Gate Pool: indirection table for heap objects. Value::Gate carries an index into this pool.
|
||||||
|
pub gate_pool: Vec<GateEntry>,
|
||||||
/// Total virtual cycles consumed since the VM started.
|
/// Total virtual cycles consumed since the VM started.
|
||||||
pub cycles: u64,
|
pub cycles: u64,
|
||||||
/// Stop flag: true if a `HALT` opcode was encountered.
|
/// Stop flag: true if a `HALT` opcode was encountered.
|
||||||
@ -88,6 +93,23 @@ pub struct VirtualMachine {
|
|||||||
pub breakpoints: std::collections::HashSet<usize>,
|
pub breakpoints: std::collections::HashSet<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Identifier for a gate (index into `gate_pool`).
|
||||||
|
/// We keep using `Value::Gate(usize)` in the ABI, but inside the VM this represents a `GateId`.
|
||||||
|
pub type GateId = u32;
|
||||||
|
|
||||||
|
/// Metadata for a gate entry, which resolves a `GateId` into a slice of the heap.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GateEntry {
|
||||||
|
/// True if this entry is currently alive and usable.
|
||||||
|
pub alive: bool,
|
||||||
|
/// Base index into the heap vector where this gate's slots start.
|
||||||
|
pub base: u32,
|
||||||
|
/// Number of heap slots reserved for this gate.
|
||||||
|
pub slots: u32,
|
||||||
|
/// Type identifier for the gate's storage (kept for future checks; unused for now).
|
||||||
|
pub type_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
impl VirtualMachine {
|
impl VirtualMachine {
|
||||||
/// Creates a new VM instance with the provided bytecode and constants.
|
/// Creates a new VM instance with the provided bytecode and constants.
|
||||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
||||||
@ -99,6 +121,7 @@ impl VirtualMachine {
|
|||||||
globals: Vec::new(),
|
globals: Vec::new(),
|
||||||
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()),
|
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()),
|
||||||
heap: Vec::new(),
|
heap: Vec::new(),
|
||||||
|
gate_pool: Vec::new(),
|
||||||
cycles: 0,
|
cycles: 0,
|
||||||
halted: false,
|
halted: false,
|
||||||
breakpoints: std::collections::HashSet::new(),
|
breakpoints: std::collections::HashSet::new(),
|
||||||
@ -117,6 +140,7 @@ impl VirtualMachine {
|
|||||||
self.scope_stack.clear();
|
self.scope_stack.clear();
|
||||||
self.globals.clear();
|
self.globals.clear();
|
||||||
self.heap.clear();
|
self.heap.clear();
|
||||||
|
self.gate_pool.clear();
|
||||||
self.cycles = 0;
|
self.cycles = 0;
|
||||||
self.halted = true; // execution is impossible until successful load
|
self.halted = true; // execution is impossible until successful load
|
||||||
|
|
||||||
@ -783,37 +807,73 @@ impl VirtualMachine {
|
|||||||
self.operand_stack.truncate(frame.scope_stack_base);
|
self.operand_stack.truncate(frame.scope_stack_base);
|
||||||
}
|
}
|
||||||
OpCode::Alloc => {
|
OpCode::Alloc => {
|
||||||
let _type_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
// Allocate a new gate with given type and number of slots.
|
||||||
let slots = u32::from_le_bytes(instr.imm[4..8].try_into().unwrap()) as usize;
|
let type_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
|
||||||
let ref_idx = self.heap.len();
|
let slots_u32 = u32::from_le_bytes(instr.imm[4..8].try_into().unwrap());
|
||||||
|
let slots = slots_u32 as usize;
|
||||||
|
|
||||||
|
// Bump-allocate on the heap and zero-initialize with Null.
|
||||||
|
let base_idx = self.heap.len();
|
||||||
for _ in 0..slots {
|
for _ in 0..slots {
|
||||||
self.heap.push(Value::Null);
|
self.heap.push(Value::Null);
|
||||||
}
|
}
|
||||||
self.push(Value::Gate(ref_idx));
|
|
||||||
|
// Insert entry into gate pool; GateId is index in this pool.
|
||||||
|
let entry = GateEntry {
|
||||||
|
alive: true,
|
||||||
|
base: base_idx as u32,
|
||||||
|
slots: slots_u32,
|
||||||
|
type_id,
|
||||||
|
};
|
||||||
|
let gate_id = self.gate_pool.len() as u32;
|
||||||
|
self.gate_pool.push(entry);
|
||||||
|
|
||||||
|
// Push a Gate value that now carries the GateId instead of a heap base.
|
||||||
|
self.push(Value::Gate(gate_id as usize));
|
||||||
}
|
}
|
||||||
OpCode::GateLoad => {
|
OpCode::GateLoad => {
|
||||||
let offset = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
let offset = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
||||||
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
if let Value::Gate(base) = ref_val {
|
if let Value::Gate(gid_usize) = ref_val {
|
||||||
let val = self.heap.get(base + offset).cloned().ok_or_else(|| {
|
let gid = gid_usize as GateId;
|
||||||
self.trap(prometeu_bytecode::abi::TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)
|
let entry = self
|
||||||
|
.resolve_gate(gid, OpCode::GateLoad as u16, start_pc as u32)
|
||||||
|
.map_err(|t| t)?;
|
||||||
|
// bounds check against slots
|
||||||
|
let off_u32 = offset as u32;
|
||||||
|
if off_u32 >= entry.slots {
|
||||||
|
return Err(self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||||
|
}
|
||||||
|
let heap_idx = entry.base as usize + offset;
|
||||||
|
let val = self.heap.get(heap_idx).cloned().ok_or_else(|| {
|
||||||
|
// Should not happen if pool and heap are consistent, but keep defensive.
|
||||||
|
self.trap(TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)
|
||||||
})?;
|
})?;
|
||||||
self.push(val);
|
self.push(val);
|
||||||
} else {
|
} else {
|
||||||
return Err(self.trap(prometeu_bytecode::abi::TRAP_TYPE, OpCode::GateLoad as u16, "Expected gate handle for GATE_LOAD".to_string(), start_pc as u32));
|
return Err(self.trap(TRAP_TYPE, OpCode::GateLoad as u16, "Expected gate handle for GATE_LOAD".to_string(), start_pc as u32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::GateStore => {
|
OpCode::GateStore => {
|
||||||
let offset = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
let offset = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap()) as usize;
|
||||||
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
let ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
|
||||||
if let Value::Gate(base) = ref_val {
|
if let Value::Gate(gid_usize) = ref_val {
|
||||||
if base + offset >= self.heap.len() {
|
let gid = gid_usize as GateId;
|
||||||
return Err(self.trap(prometeu_bytecode::abi::TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
let entry = self
|
||||||
|
.resolve_gate(gid, OpCode::GateStore as u16, start_pc as u32)
|
||||||
|
.map_err(|t| t)?;
|
||||||
|
let off_u32 = offset as u32;
|
||||||
|
if off_u32 >= entry.slots {
|
||||||
|
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||||
}
|
}
|
||||||
self.heap[base + offset] = val;
|
let heap_idx = entry.base as usize + offset;
|
||||||
|
if heap_idx >= self.heap.len() {
|
||||||
|
return Err(self.trap(TRAP_OOB, OpCode::GateStore as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32));
|
||||||
|
}
|
||||||
|
self.heap[heap_idx] = val;
|
||||||
} else {
|
} else {
|
||||||
return Err(self.trap(prometeu_bytecode::abi::TRAP_TYPE, OpCode::GateStore as u16, "Expected gate handle for GATE_STORE".to_string(), start_pc as u32));
|
return Err(self.trap(TRAP_TYPE, OpCode::GateStore as u16, "Expected gate handle for GATE_STORE".to_string(), start_pc as u32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCode::GateBeginPeek | OpCode::GateEndPeek |
|
OpCode::GateBeginPeek | OpCode::GateEndPeek |
|
||||||
@ -870,6 +930,51 @@ impl VirtualMachine {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a `GateId` to an immutable reference to its `GateEntry` or returns a trap reason.
|
||||||
|
fn resolve_gate(&self, gate_id: GateId, opcode: u16, pc: u32) -> Result<&GateEntry, LogicalFrameEndingReason> {
|
||||||
|
let idx = gate_id as usize;
|
||||||
|
let entry = self.gate_pool.get(idx).ok_or_else(|| {
|
||||||
|
self.trap(
|
||||||
|
TRAP_INVALID_GATE,
|
||||||
|
opcode,
|
||||||
|
format!("Invalid gate id: {}", gate_id),
|
||||||
|
pc,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !entry.alive {
|
||||||
|
return Err(self.trap(
|
||||||
|
TRAP_DEAD_GATE,
|
||||||
|
opcode,
|
||||||
|
format!("Dead gate id: {}", gate_id),
|
||||||
|
pc,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a `GateId` to a mutable reference to its `GateEntry` or returns a trap reason.
|
||||||
|
fn resolve_gate_mut(&mut self, gate_id: GateId, opcode: u16, pc: u32) -> Result<&mut GateEntry, LogicalFrameEndingReason> {
|
||||||
|
let idx = gate_id as usize;
|
||||||
|
if idx >= self.gate_pool.len() {
|
||||||
|
return Err(self.trap(
|
||||||
|
TRAP_INVALID_GATE,
|
||||||
|
opcode,
|
||||||
|
format!("Invalid gate id: {}", gate_id),
|
||||||
|
pc,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !self.gate_pool[idx].alive {
|
||||||
|
return Err(self.trap(
|
||||||
|
TRAP_DEAD_GATE,
|
||||||
|
opcode,
|
||||||
|
format!("Dead gate id: {}", gate_id),
|
||||||
|
pc,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// This borrow is safe because we validated bounds and liveness without using closures.
|
||||||
|
Ok(&mut self.gate_pool[idx])
|
||||||
|
}
|
||||||
|
|
||||||
pub fn trap(&self, code: u32, opcode: u16, message: String, pc: u32) -> LogicalFrameEndingReason {
|
pub fn trap(&self, code: u32, opcode: u16, message: String, pc: u32) -> LogicalFrameEndingReason {
|
||||||
LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc))
|
LogicalFrameEndingReason::Trap(self.program.create_trap(code, opcode, message, pc))
|
||||||
}
|
}
|
||||||
@ -1626,6 +1731,82 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gate_ids_distinct_and_round_trip() {
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
// Program:
|
||||||
|
// ALLOC(type=0, slots=1) -> [g0]
|
||||||
|
// ALLOC(type=0, slots=1) -> [g0, g1]
|
||||||
|
// HALT
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
rom.extend_from_slice(&(OpCode::Alloc as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Alloc as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.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 = new_test_vm(rom.clone(), vec![]);
|
||||||
|
vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
|
||||||
|
let g1 = vm.pop().unwrap();
|
||||||
|
let g0 = vm.pop().unwrap();
|
||||||
|
assert_ne!(g0, g1, "GateIds must be distinct");
|
||||||
|
|
||||||
|
// Now test store/load round-trip using a new small program:
|
||||||
|
// ALLOC(type=0, slots=1) -> [g]
|
||||||
|
// DUP -> [g, g]
|
||||||
|
// PushI32 123 -> [g, g, 123]
|
||||||
|
// GateStore 0 -> []
|
||||||
|
// GateLoad 0 -> [123]
|
||||||
|
// HALT
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
rom.extend_from_slice(&(OpCode::Alloc as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Dup as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::PushI32 as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&123i32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::GateStore as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::GateLoad as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let mut vm = new_test_vm(rom.clone(), vec![]);
|
||||||
|
vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
assert_eq!(vm.pop().unwrap(), Value::Int32(123));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_gate_traps() {
|
||||||
|
let mut native = MockNative;
|
||||||
|
let mut ctx = HostContext::new(None);
|
||||||
|
|
||||||
|
// Program: GATE_LOAD 0; HALT
|
||||||
|
// We'll seed the operand stack with an invalid GateId before running.
|
||||||
|
let mut rom = Vec::new();
|
||||||
|
rom.extend_from_slice(&(OpCode::GateLoad as u16).to_le_bytes());
|
||||||
|
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||||
|
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||||
|
|
||||||
|
let mut vm = new_test_vm(rom.clone(), vec![]);
|
||||||
|
// Push an invalid GateId that is out of range (no allocations were made)
|
||||||
|
vm.operand_stack.push(Value::Gate(42usize));
|
||||||
|
|
||||||
|
let report = vm.run_budget(100, &mut native, &mut ctx).unwrap();
|
||||||
|
match report.reason {
|
||||||
|
LogicalFrameEndingReason::Trap(trap) => {
|
||||||
|
assert_eq!(trap.code, prometeu_bytecode::abi::TRAP_INVALID_GATE);
|
||||||
|
assert_eq!(trap.opcode, OpCode::GateLoad as u16);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Trap, got {:?}", report.reason),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_entry_point_ret_with_prepare_call() {
|
fn test_entry_point_ret_with_prepare_call() {
|
||||||
// PushI32 0 (0x17), then Ret (0x51)
|
// PushI32 0 (0x17), then Ret (0x51)
|
||||||
|
|||||||
@ -19,53 +19,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# PR-01 — Linker: Relocate control-flow jump targets when concatenating modules
|
|
||||||
|
|
||||||
### Briefing
|
|
||||||
|
|
||||||
Today the linker patches `CALL` and `PUSH_CONST` immediates, but **does not relocate jump targets** (`JMP`, `JMP_IF_*`) after concatenating module bytecode into a single code blob. This breaks cross-module correctness because label resolution in per-module assembly produces addresses relative to each module’s own code segment.
|
|
||||||
|
|
||||||
### Target
|
|
||||||
|
|
||||||
* In the linker’s relocation pass, patch immediates for:
|
|
||||||
|
|
||||||
* `OpCode::Jmp`
|
|
||||||
* `OpCode::JmpIfTrue`
|
|
||||||
* `OpCode::JmpIfFalse`
|
|
||||||
* Add `module_code_offsets[module_index]` to the jump target immediate.
|
|
||||||
|
|
||||||
### Non-goals
|
|
||||||
|
|
||||||
* No changes to opcode encoding.
|
|
||||||
* No changes to verifier.
|
|
||||||
* No changes to how labels are resolved in the assembler.
|
|
||||||
|
|
||||||
### Implementation notes
|
|
||||||
|
|
||||||
* Extend the existing “patch immediates” loop in `Linker::link`.
|
|
||||||
* Determine `module_index` from the current iterated module during relocation.
|
|
||||||
* Make sure **only jump targets** are adjusted, not fallthrough logic.
|
|
||||||
* Add a small helper function for patching immediates to reduce duplication.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
1. **Unit test** (preferred) in `prometeu-linker` (or wherever `Linker` tests live):
|
|
||||||
|
|
||||||
* Create 2 small modules where module #2 contains a local jump.
|
|
||||||
* Link them.
|
|
||||||
* Assert that the encoded jump immediate in the final program equals `original_target + module2_offset`.
|
|
||||||
2. **Integration test** (if a unit test is hard):
|
|
||||||
|
|
||||||
* Build two modules and execute in VM; ensure it reaches expected instruction sequence (e.g., sets a known local/global).
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
* Multi-module programs with jumps inside non-first modules execute correctly.
|
|
||||||
* Existing call/const relocation remains correct.
|
|
||||||
* Tests cover at least one `JMP` and one conditional jump.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PR-02 — VM: Introduce GateId vs heap index and a minimal GatePool (no RC yet)
|
## PR-02 — VM: Introduce GateId vs heap index and a minimal GatePool (no RC yet)
|
||||||
|
|
||||||
### Briefing
|
### Briefing
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user