dev/pbs #8
@ -41,7 +41,7 @@ pub fn disasm(rom: &[u8]) -> Result<Vec<Instr>, String> {
|
||||
match opcode {
|
||||
OpCode::PushConst | OpCode::PushI32 | OpCode::Jmp | OpCode::JmpIfFalse | OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal | OpCode::SetGlobal | OpCode::GetLocal | OpCode::SetLocal
|
||||
| OpCode::PopN | OpCode::Syscall | OpCode::LoadRef | OpCode::StoreRef => {
|
||||
| OpCode::PopN | OpCode::Syscall | OpCode::GateLoad | OpCode::GateStore => {
|
||||
let v = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::U32(v));
|
||||
}
|
||||
@ -58,11 +58,11 @@ pub fn disasm(rom: &[u8]) -> Result<Vec<Instr>, String> {
|
||||
cursor.read_exact(&mut b_buf).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::Bool(b_buf[0] != 0));
|
||||
}
|
||||
OpCode::Call => {
|
||||
let addr = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
let args = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::U32(addr));
|
||||
operands.push(DisasmOperand::U32(args));
|
||||
OpCode::Call | OpCode::Alloc => {
|
||||
let v1 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
let v2 = read_u32_le(&mut cursor).map_err(|e| e.to_string())?;
|
||||
operands.push(DisasmOperand::U32(v1));
|
||||
operands.push(DisasmOperand::U32(v2));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@ -153,19 +153,46 @@ pub enum OpCode {
|
||||
/// Ends the current local scope, discarding its local variables.
|
||||
PopScope = 0x53,
|
||||
|
||||
// --- 6.7 Heap ---
|
||||
// --- 6.7 HIP (Heap Interface Protocol) ---
|
||||
|
||||
/// Allocates `size` slots on the heap.
|
||||
/// Stack: [size] -> [reference]
|
||||
/// Allocates `slots` slots on the heap with the given `type_id`.
|
||||
/// Operands: type_id (u32), slots (u32)
|
||||
/// Stack: [] -> [gate]
|
||||
Alloc = 0x60,
|
||||
/// Reads a value from the heap at `reference + offset`.
|
||||
/// Reads a value from the heap at `gate + offset`.
|
||||
/// Operand: offset (u32)
|
||||
/// Stack: [reference] -> [value]
|
||||
LoadRef = 0x61,
|
||||
/// Writes a value to the heap at `reference + offset`.
|
||||
/// Stack: [gate] -> [value]
|
||||
GateLoad = 0x61,
|
||||
/// Writes a value to the heap at `gate + offset`.
|
||||
/// Operand: offset (u32)
|
||||
/// Stack: [reference, value] -> []
|
||||
StoreRef = 0x62,
|
||||
/// Stack: [gate, value] -> []
|
||||
GateStore = 0x62,
|
||||
|
||||
/// Marks the beginning of a Peek scope for a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateBeginPeek = 0x63,
|
||||
/// Marks the end of a Peek scope for a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateEndPeek = 0x64,
|
||||
/// Marks the beginning of a Borrow scope for a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateBeginBorrow = 0x65,
|
||||
/// Marks the end of a Borrow scope for a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateEndBorrow = 0x66,
|
||||
/// Marks the beginning of a Mutate scope for a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateBeginMutate = 0x67,
|
||||
/// Marks the end of a Mutate scope for a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateEndMutate = 0x68,
|
||||
|
||||
/// Increments the reference count of a gate.
|
||||
/// Stack: [gate] -> [gate]
|
||||
GateRetain = 0x69,
|
||||
/// Decrements the reference count of a gate.
|
||||
/// Stack: [gate] -> []
|
||||
GateRelease = 0x6A,
|
||||
|
||||
// --- 6.8 Peripherals and System ---
|
||||
|
||||
@ -226,8 +253,16 @@ impl TryFrom<u16> for OpCode {
|
||||
0x52 => Ok(OpCode::PushScope),
|
||||
0x53 => Ok(OpCode::PopScope),
|
||||
0x60 => Ok(OpCode::Alloc),
|
||||
0x61 => Ok(OpCode::LoadRef),
|
||||
0x62 => Ok(OpCode::StoreRef),
|
||||
0x61 => Ok(OpCode::GateLoad),
|
||||
0x62 => Ok(OpCode::GateStore),
|
||||
0x63 => Ok(OpCode::GateBeginPeek),
|
||||
0x64 => Ok(OpCode::GateEndPeek),
|
||||
0x65 => Ok(OpCode::GateBeginBorrow),
|
||||
0x66 => Ok(OpCode::GateEndBorrow),
|
||||
0x67 => Ok(OpCode::GateBeginMutate),
|
||||
0x68 => Ok(OpCode::GateEndMutate),
|
||||
0x69 => Ok(OpCode::GateRetain),
|
||||
0x6A => Ok(OpCode::GateRelease),
|
||||
0x70 => Ok(OpCode::Syscall),
|
||||
0x80 => Ok(OpCode::FrameSync),
|
||||
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
|
||||
@ -283,10 +318,76 @@ impl OpCode {
|
||||
OpCode::PushScope => 3,
|
||||
OpCode::PopScope => 3,
|
||||
OpCode::Alloc => 10,
|
||||
OpCode::LoadRef => 3,
|
||||
OpCode::StoreRef => 3,
|
||||
OpCode::GateLoad => 3,
|
||||
OpCode::GateStore => 3,
|
||||
OpCode::GateBeginPeek => 1,
|
||||
OpCode::GateEndPeek => 1,
|
||||
OpCode::GateBeginBorrow => 1,
|
||||
OpCode::GateEndBorrow => 1,
|
||||
OpCode::GateBeginMutate => 1,
|
||||
OpCode::GateEndMutate => 1,
|
||||
OpCode::GateRetain => 1,
|
||||
OpCode::GateRelease => 1,
|
||||
OpCode::Syscall => 1,
|
||||
OpCode::FrameSync => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::asm::{assemble, Asm, Operand};
|
||||
|
||||
#[test]
|
||||
fn test_opcode_stability() {
|
||||
// Normative test: ensures opcode numeric values are frozen.
|
||||
assert_eq!(OpCode::Nop as u16, 0x00);
|
||||
assert_eq!(OpCode::PushConst as u16, 0x10);
|
||||
assert_eq!(OpCode::Alloc as u16, 0x60);
|
||||
assert_eq!(OpCode::GateLoad as u16, 0x61);
|
||||
assert_eq!(OpCode::GateStore as u16, 0x62);
|
||||
assert_eq!(OpCode::GateBeginPeek as u16, 0x63);
|
||||
assert_eq!(OpCode::GateEndPeek as u16, 0x64);
|
||||
assert_eq!(OpCode::GateBeginBorrow as u16, 0x65);
|
||||
assert_eq!(OpCode::GateEndBorrow as u16, 0x66);
|
||||
assert_eq!(OpCode::GateBeginMutate as u16, 0x67);
|
||||
assert_eq!(OpCode::GateEndMutate as u16, 0x68);
|
||||
assert_eq!(OpCode::GateRetain as u16, 0x69);
|
||||
assert_eq!(OpCode::GateRelease as u16, 0x6A);
|
||||
assert_eq!(OpCode::FrameSync as u16, 0x80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hip_bytecode_golden() {
|
||||
// Golden test for HIP opcodes and their encodings.
|
||||
// Rule: All multi-byte operands are little-endian.
|
||||
|
||||
let instructions = vec![
|
||||
Asm::Op(OpCode::Alloc, vec![Operand::U32(0x11223344), Operand::U32(0x55667788)]),
|
||||
Asm::Op(OpCode::GateLoad, vec![Operand::U32(0xAABBCCDD)]),
|
||||
Asm::Op(OpCode::GateStore, vec![Operand::U32(0x11223344)]),
|
||||
Asm::Op(OpCode::GateBeginPeek, vec![]),
|
||||
Asm::Op(OpCode::GateRetain, vec![]),
|
||||
Asm::Op(OpCode::GateRelease, vec![]),
|
||||
];
|
||||
|
||||
let bytes = assemble(&instructions).unwrap();
|
||||
|
||||
let mut expected = Vec::new();
|
||||
// Alloc (0x60, 0x00) + type_id (44 33 22 11) + slots (88 77 66 55)
|
||||
expected.extend_from_slice(&[0x60, 0x00, 0x44, 0x33, 0x22, 0x11, 0x88, 0x77, 0x66, 0x55]);
|
||||
// GateLoad (0x61, 0x00) + offset (DD CC BB AA)
|
||||
expected.extend_from_slice(&[0x61, 0x00, 0xDD, 0xCC, 0xBB, 0xAA]);
|
||||
// GateStore (0x62, 0x00) + offset (44 33 22 11)
|
||||
expected.extend_from_slice(&[0x62, 0x00, 0x44, 0x33, 0x22, 0x11]);
|
||||
// GateBeginPeek (0x63, 0x00)
|
||||
expected.extend_from_slice(&[0x63, 0x00]);
|
||||
// GateRetain (0x69, 0x00)
|
||||
expected.extend_from_slice(&[0x69, 0x00]);
|
||||
// GateRelease (0x6A, 0x00)
|
||||
expected.extend_from_slice(&[0x6A, 0x00]);
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,19 +161,23 @@ impl<'a> BytecodeEmitter<'a> {
|
||||
asm_instrs.push(Asm::Op(OpCode::Syscall, vec![Operand::U32(*id)]));
|
||||
}
|
||||
InstrKind::FrameSync => asm_instrs.push(Asm::Op(OpCode::FrameSync, vec![])),
|
||||
InstrKind::Alloc { .. } => asm_instrs.push(Asm::Op(OpCode::Alloc, vec![])),
|
||||
InstrKind::Alloc { type_id, slots } => {
|
||||
asm_instrs.push(Asm::Op(OpCode::Alloc, vec![Operand::U32(type_id.0), Operand::U32(*slots)]));
|
||||
}
|
||||
InstrKind::GateLoad { offset } => {
|
||||
asm_instrs.push(Asm::Op(OpCode::LoadRef, vec![Operand::U32(*offset)]));
|
||||
asm_instrs.push(Asm::Op(OpCode::GateLoad, vec![Operand::U32(*offset)]));
|
||||
}
|
||||
InstrKind::GateStore { offset } => {
|
||||
asm_instrs.push(Asm::Op(OpCode::StoreRef, vec![Operand::U32(*offset)]));
|
||||
}
|
||||
InstrKind::GateBeginPeek | InstrKind::GateEndPeek |
|
||||
InstrKind::GateBeginBorrow | InstrKind::GateEndBorrow |
|
||||
InstrKind::GateBeginMutate | InstrKind::GateEndMutate |
|
||||
InstrKind::GateRetain | InstrKind::GateRelease => {
|
||||
asm_instrs.push(Asm::Op(OpCode::Nop, vec![]));
|
||||
asm_instrs.push(Asm::Op(OpCode::GateStore, vec![Operand::U32(*offset)]));
|
||||
}
|
||||
InstrKind::GateBeginPeek => asm_instrs.push(Asm::Op(OpCode::GateBeginPeek, vec![])),
|
||||
InstrKind::GateEndPeek => asm_instrs.push(Asm::Op(OpCode::GateEndPeek, vec![])),
|
||||
InstrKind::GateBeginBorrow => asm_instrs.push(Asm::Op(OpCode::GateBeginBorrow, vec![])),
|
||||
InstrKind::GateEndBorrow => asm_instrs.push(Asm::Op(OpCode::GateEndBorrow, vec![])),
|
||||
InstrKind::GateBeginMutate => asm_instrs.push(Asm::Op(OpCode::GateBeginMutate, vec![])),
|
||||
InstrKind::GateEndMutate => asm_instrs.push(Asm::Op(OpCode::GateEndMutate, vec![])),
|
||||
InstrKind::GateRetain => asm_instrs.push(Asm::Op(OpCode::GateRetain, vec![])),
|
||||
InstrKind::GateRelease => asm_instrs.push(Asm::Op(OpCode::GateRelease, vec![])),
|
||||
}
|
||||
|
||||
let end_idx = asm_instrs.len();
|
||||
|
||||
@ -158,10 +158,10 @@ mod tests {
|
||||
let opcodes: Vec<_> = instrs.iter().map(|i| i.opcode).collect();
|
||||
|
||||
assert!(opcodes.contains(&OpCode::Alloc));
|
||||
assert!(opcodes.contains(&OpCode::LoadRef));
|
||||
// After PR-05, BeginMutate/EndMutate map to GateLoad/Nop for now
|
||||
// because VM is feature-frozen. StoreRef is removed from lowering.
|
||||
assert!(opcodes.contains(&OpCode::Nop));
|
||||
assert!(opcodes.contains(&OpCode::GateLoad));
|
||||
// After PR-09, BeginMutate/EndMutate map to their respective opcodes
|
||||
assert!(opcodes.contains(&OpCode::GateBeginMutate));
|
||||
assert!(opcodes.contains(&OpCode::GateEndMutate));
|
||||
assert!(opcodes.contains(&OpCode::Add));
|
||||
assert!(opcodes.contains(&OpCode::Ret));
|
||||
}
|
||||
@ -237,25 +237,25 @@ mod tests {
|
||||
0052 SetLocal U32(1)
|
||||
0058 Jmp U32(100)
|
||||
005E Jmp U32(100)
|
||||
0064 Alloc
|
||||
0066 SetLocal U32(1)
|
||||
006C GetLocal U32(1)
|
||||
0072 Nop
|
||||
0074 SetLocal U32(2)
|
||||
007A Nop
|
||||
007C GetLocal U32(2)
|
||||
0082 LoadRef U32(0)
|
||||
0088 SetLocal U32(3)
|
||||
008E GetLocal U32(3)
|
||||
0094 PushConst U32(5)
|
||||
009A Add
|
||||
009C SetLocal U32(4)
|
||||
00A2 Nop
|
||||
00A4 GetLocal U32(1)
|
||||
00AA Nop
|
||||
00AC GetLocal U32(2)
|
||||
00B2 Nop
|
||||
00B4 Ret
|
||||
0064 Alloc U32(2) U32(1)
|
||||
006E SetLocal U32(1)
|
||||
0074 GetLocal U32(1)
|
||||
007A GateRetain
|
||||
007C SetLocal U32(2)
|
||||
0082 GateBeginMutate
|
||||
0084 GetLocal U32(2)
|
||||
008A GateLoad U32(0)
|
||||
0090 SetLocal U32(3)
|
||||
0096 GetLocal U32(3)
|
||||
009C PushConst U32(5)
|
||||
00A2 Add
|
||||
00A4 SetLocal U32(4)
|
||||
00AA GateEndMutate
|
||||
00AC GetLocal U32(1)
|
||||
00B2 GateRelease
|
||||
00B4 GetLocal U32(2)
|
||||
00BA GateRelease
|
||||
00BC Ret
|
||||
"#;
|
||||
|
||||
assert_eq!(disasm_text, expected_disasm);
|
||||
|
||||
@ -544,15 +544,16 @@ impl VirtualMachine {
|
||||
self.operand_stack.truncate(frame.scope_stack_base);
|
||||
}
|
||||
OpCode::Alloc => {
|
||||
// Allocates 'size' values on the heap and pushes a reference to the stack
|
||||
let size = self.read_u32()? as usize;
|
||||
// Allocates 'slots' values on the heap and pushes a reference to the stack
|
||||
let _type_id = self.read_u32()?;
|
||||
let slots = self.read_u32()? as usize;
|
||||
let ref_idx = self.heap.len();
|
||||
for _ in 0..size {
|
||||
for _ in 0..slots {
|
||||
self.heap.push(Value::Null);
|
||||
}
|
||||
self.push(Value::Ref(ref_idx));
|
||||
}
|
||||
OpCode::LoadRef => {
|
||||
OpCode::GateLoad => {
|
||||
// Reads a value from a heap reference at a specific offset
|
||||
let offset = self.read_u32()? as usize;
|
||||
let ref_val = self.pop()?;
|
||||
@ -560,10 +561,10 @@ impl VirtualMachine {
|
||||
let val = self.heap.get(base + offset).cloned().ok_or("Invalid heap access")?;
|
||||
self.push(val);
|
||||
} else {
|
||||
return Err("Expected reference for LOAD_REF".into());
|
||||
return Err("Expected reference for GATE_LOAD".into());
|
||||
}
|
||||
}
|
||||
OpCode::StoreRef => {
|
||||
OpCode::GateStore => {
|
||||
// Writes a value to a heap reference at a specific offset
|
||||
let offset = self.read_u32()? as usize;
|
||||
let val = self.pop()?;
|
||||
@ -574,9 +575,18 @@ impl VirtualMachine {
|
||||
}
|
||||
self.heap[base + offset] = val;
|
||||
} else {
|
||||
return Err("Expected reference for STORE_REF".into());
|
||||
return Err("Expected reference for GATE_STORE".into());
|
||||
}
|
||||
}
|
||||
OpCode::GateBeginPeek | OpCode::GateEndPeek |
|
||||
OpCode::GateBeginBorrow | OpCode::GateEndBorrow |
|
||||
OpCode::GateBeginMutate | OpCode::GateEndMutate |
|
||||
OpCode::GateRetain => {
|
||||
// These are no-ops in v0, but they preserve the gate on the stack.
|
||||
}
|
||||
OpCode::GateRelease => {
|
||||
self.pop()?;
|
||||
}
|
||||
OpCode::Syscall => {
|
||||
// Calls a native function implemented by the Firmware/OS.
|
||||
// ABI Rule: Arguments are pushed in call order (LIFO).
|
||||
|
||||
@ -1,53 +1,3 @@
|
||||
## PR-09 — HIP ISA Freeze v0: Opcode Table + Encoding Contract (Bytecode)
|
||||
|
||||
### Goal
|
||||
|
||||
Freeze the HIP-related opcode set and encoding so bytecode becomes stable.
|
||||
|
||||
### Required Changes
|
||||
|
||||
1. Update `prometeu-bytecode`:
|
||||
|
||||
* Define the canonical HIP opcode subset:
|
||||
|
||||
* `PUSH_CONST`
|
||||
* `ALLOC(type_id, slots)`
|
||||
* `GATE_BEGIN_PEEK`, `GATE_END_PEEK`
|
||||
* `GATE_BEGIN_BORROW`, `GATE_END_BORROW`
|
||||
* `GATE_BEGIN_MUTATE`, `GATE_END_MUTATE`
|
||||
* `GATE_LOAD(offset)`
|
||||
* `GATE_STORE(offset)`
|
||||
* `GATE_RETAIN`, `GATE_RELEASE`
|
||||
* `FRAME_SYNC` (if included)
|
||||
|
||||
2. Define canonical encodings (normative in comments/doc):
|
||||
|
||||
* `GateId` encoding: `u32` little-endian
|
||||
* `TypeId` encoding: `u32` little-endian
|
||||
* `ConstId` encoding: `u32` little-endian
|
||||
* `slots`: `u32` little-endian
|
||||
* `offset`: `u32` little-endian
|
||||
|
||||
3. Update bytecode emitter so it emits these exact opcodes with these exact payloads.
|
||||
|
||||
### Non-goals
|
||||
|
||||
* No runtime execution changes
|
||||
|
||||
### Tests (Mandatory)
|
||||
|
||||
1. **Golden bytecode tests**:
|
||||
|
||||
* Given a minimal VM IR program using each HIP opcode, assert the exact emitted bytes.
|
||||
|
||||
2. **Opcode stability test**:
|
||||
|
||||
* Snapshot test of the opcode enum ordering and numeric values.
|
||||
|
||||
> If opcode numeric values already exist, DO NOT renumber. If new opcodes are added, append them.
|
||||
|
||||
---
|
||||
|
||||
## PR-10 — HIP ABI Freeze v0: Trap Conditions + Debug Surface
|
||||
|
||||
### Goal
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user