From 0a087d51fbf9351ee1e8c67a1870b75f79e8468e Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Sun, 8 Feb 2026 12:24:46 +0000 Subject: [PATCH] introduce gate Id and heap index --- crates/prometeu-vm/src/virtual_machine.rs | 209 ++++++++++++++++++++-- files/TODO.md | 47 ----- 2 files changed, 195 insertions(+), 61 deletions(-) diff --git a/crates/prometeu-vm/src/virtual_machine.rs b/crates/prometeu-vm/src/virtual_machine.rs index 62a60cee..045896ef 100644 --- a/crates/prometeu-vm/src/virtual_machine.rs +++ b/crates/prometeu-vm/src/virtual_machine.rs @@ -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, + /// Gate Pool: indirection table for heap objects. Value::Gate carries an index into this pool. + pub gate_pool: Vec, /// 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, } +/// 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, constant_pool: Vec) -> 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) diff --git a/files/TODO.md b/files/TODO.md index 8323572b..9200f3ea 100644 --- a/files/TODO.md +++ b/files/TODO.md @@ -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) ### Briefing