introduce gate Id and heap index

This commit is contained in:
bQUARKz 2026-02-08 12:24:46 +00:00
parent 937e0d70b6
commit 0a087d51fb
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 195 additions and 61 deletions

View File

@ -2,7 +2,10 @@ use crate::call_frame::CallFrame;
use crate::scope_frame::ScopeFrame;
use crate::Value;
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;
/// Reason why the Virtual Machine stopped execution during a specific run.
@ -80,6 +83,8 @@ pub struct VirtualMachine {
pub program: ProgramImage,
/// Heap Memory: Dynamic allocation pool.
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.
pub cycles: u64,
/// Stop flag: true if a `HALT` opcode was encountered.
@ -88,6 +93,23 @@ pub struct VirtualMachine {
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 {
/// Creates a new VM instance with the provided bytecode and constants.
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
@ -99,6 +121,7 @@ impl VirtualMachine {
globals: Vec::new(),
program: ProgramImage::new(rom, constant_pool, vec![], None, std::collections::HashMap::new()),
heap: Vec::new(),
gate_pool: Vec::new(),
cycles: 0,
halted: false,
breakpoints: std::collections::HashSet::new(),
@ -117,6 +140,7 @@ impl VirtualMachine {
self.scope_stack.clear();
self.globals.clear();
self.heap.clear();
self.gate_pool.clear();
self.cycles = 0;
self.halted = true; // execution is impossible until successful load
@ -783,37 +807,73 @@ impl VirtualMachine {
self.operand_stack.truncate(frame.scope_stack_base);
}
OpCode::Alloc => {
let _type_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
let slots = u32::from_le_bytes(instr.imm[4..8].try_into().unwrap()) as usize;
let ref_idx = self.heap.len();
// Allocate a new gate with given type and number of slots.
let type_id = u32::from_le_bytes(instr.imm[0..4].try_into().unwrap());
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 {
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 => {
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))?;
if let Value::Gate(base) = ref_val {
let val = self.heap.get(base + offset).cloned().ok_or_else(|| {
self.trap(prometeu_bytecode::abi::TRAP_OOB, OpCode::GateLoad as u16, format!("Out-of-bounds heap access at offset {}", offset), start_pc as u32)
if let Value::Gate(gid_usize) = ref_val {
let gid = gid_usize as GateId;
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);
} 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 => {
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 ref_val = self.pop().map_err(|e| LogicalFrameEndingReason::Panic(e))?;
if let Value::Gate(base) = ref_val {
if base + offset >= self.heap.len() {
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));
if let Value::Gate(gid_usize) = ref_val {
let gid = gid_usize as GateId;
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 {
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 |
@ -870,6 +930,51 @@ impl VirtualMachine {
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 {
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]
fn test_entry_point_ret_with_prepare_call() {
// PushI32 0 (0x17), then Ret (0x51)

View File

@ -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 modules own code segment.
### Target
* In the linkers 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)
### Briefing