dev/pbs #8
@ -161,13 +161,18 @@ 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::LoadRef(offset) => {
|
||||
InstrKind::Alloc { .. } => asm_instrs.push(Asm::Op(OpCode::Alloc, vec![])),
|
||||
InstrKind::GateLoad { offset } => {
|
||||
asm_instrs.push(Asm::Op(OpCode::LoadRef, vec![Operand::U32(*offset)]));
|
||||
}
|
||||
InstrKind::StoreRef(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 => {
|
||||
asm_instrs.push(Asm::Op(OpCode::Nop, vec![]));
|
||||
}
|
||||
}
|
||||
|
||||
let end_idx = asm_instrs.len();
|
||||
@ -253,8 +258,8 @@ mod tests {
|
||||
params: vec![],
|
||||
return_type: Type::Void,
|
||||
body: vec![
|
||||
Instruction::new(InstrKind::PushConst(id_int), None),
|
||||
Instruction::new(InstrKind::PushConst(id_str), None),
|
||||
Instruction::new(InstrKind::PushConst(ir_vm::ConstId(id_int.0)), None),
|
||||
Instruction::new(InstrKind::PushConst(ir_vm::ConstId(id_str.0)), None),
|
||||
Instruction::new(InstrKind::Ret, None),
|
||||
],
|
||||
};
|
||||
|
||||
@ -241,14 +241,15 @@ mod tests {
|
||||
0066 SetLocal U32(1)
|
||||
006C GetLocal U32(1)
|
||||
0072 SetLocal U32(2)
|
||||
0078 LoadRef U32(0)
|
||||
007E SetLocal U32(3)
|
||||
0084 GetLocal U32(3)
|
||||
008A PushConst U32(5)
|
||||
0090 Add
|
||||
0092 SetLocal U32(4)
|
||||
0098 Nop
|
||||
009A Ret
|
||||
0078 Nop
|
||||
007A LoadRef U32(0)
|
||||
0080 SetLocal U32(3)
|
||||
0086 GetLocal U32(3)
|
||||
008C PushConst U32(5)
|
||||
0092 Add
|
||||
0094 SetLocal U32(4)
|
||||
009A Nop
|
||||
009C Ret
|
||||
"#;
|
||||
|
||||
assert_eq!(disasm_text, expected_disasm);
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
//! easy to lower into VM-specific bytecode.
|
||||
|
||||
use crate::common::spans::Span;
|
||||
use crate::ir_core::ids::{ConstId, FunctionId};
|
||||
use crate::ir_vm::types::{ConstId, TypeId};
|
||||
use crate::ir_core::ids::FunctionId;
|
||||
|
||||
/// An `Instruction` combines an instruction's behavior (`kind`) with its
|
||||
/// source code location (`span`) for debugging and error reporting.
|
||||
@ -139,10 +140,186 @@ pub enum InstrKind {
|
||||
|
||||
// --- HIP / Memory ---
|
||||
|
||||
/// Allocates memory on the heap. Pops size from stack.
|
||||
Alloc,
|
||||
/// Reads from heap at reference + offset. Pops reference, pushes value.
|
||||
LoadRef(u32),
|
||||
/// Writes to heap at reference + offset. Pops reference and value.
|
||||
StoreRef(u32),
|
||||
/// Allocates memory on the heap.
|
||||
Alloc { type_id: TypeId, slots: u32 },
|
||||
/// Reads from heap at gate + offset. Pops gate, pushes value.
|
||||
GateLoad { offset: u32 },
|
||||
/// Writes to heap at gate + offset. Pops gate and value.
|
||||
GateStore { offset: u32 },
|
||||
|
||||
// --- Scope Markers ---
|
||||
GateBeginPeek,
|
||||
GateEndPeek,
|
||||
GateBeginBorrow,
|
||||
GateEndBorrow,
|
||||
GateBeginMutate,
|
||||
GateEndMutate,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ir_vm::types::{ConstId, TypeId};
|
||||
|
||||
#[test]
|
||||
fn test_instr_kind_is_cloneable() {
|
||||
let instr = InstrKind::Alloc { type_id: TypeId(1), slots: 2 };
|
||||
let cloned = instr.clone();
|
||||
match cloned {
|
||||
InstrKind::Alloc { type_id, slots } => {
|
||||
assert_eq!(type_id, TypeId(1));
|
||||
assert_eq!(slots, 2);
|
||||
}
|
||||
_ => panic!("Clone failed"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_isa_surface_snapshot() {
|
||||
// This test ensures that the instruction set surface remains stable.
|
||||
// If you add/remove/change instructions, this test will fail,
|
||||
// prompting an explicit review of the ISA change.
|
||||
let instructions = vec![
|
||||
InstrKind::Nop,
|
||||
InstrKind::Halt,
|
||||
InstrKind::PushConst(ConstId(0)),
|
||||
InstrKind::PushBool(true),
|
||||
InstrKind::PushNull,
|
||||
InstrKind::Pop,
|
||||
InstrKind::Dup,
|
||||
InstrKind::Swap,
|
||||
InstrKind::Add,
|
||||
InstrKind::Sub,
|
||||
InstrKind::Mul,
|
||||
InstrKind::Div,
|
||||
InstrKind::Neg,
|
||||
InstrKind::Eq,
|
||||
InstrKind::Neq,
|
||||
InstrKind::Lt,
|
||||
InstrKind::Gt,
|
||||
InstrKind::Lte,
|
||||
InstrKind::Gte,
|
||||
InstrKind::And,
|
||||
InstrKind::Or,
|
||||
InstrKind::Not,
|
||||
InstrKind::BitAnd,
|
||||
InstrKind::BitOr,
|
||||
InstrKind::BitXor,
|
||||
InstrKind::Shl,
|
||||
InstrKind::Shr,
|
||||
InstrKind::GetLocal(0),
|
||||
InstrKind::SetLocal(0),
|
||||
InstrKind::GetGlobal(0),
|
||||
InstrKind::SetGlobal(0),
|
||||
InstrKind::Jmp(Label("target".to_string())),
|
||||
InstrKind::JmpIfFalse(Label("target".to_string())),
|
||||
InstrKind::Label(Label("target".to_string())),
|
||||
InstrKind::Call { func_id: FunctionId(0), arg_count: 0 },
|
||||
InstrKind::Ret,
|
||||
InstrKind::Syscall(0),
|
||||
InstrKind::FrameSync,
|
||||
InstrKind::Alloc { type_id: TypeId(0), slots: 0 },
|
||||
InstrKind::GateLoad { offset: 0 },
|
||||
InstrKind::GateStore { offset: 0 },
|
||||
InstrKind::GateBeginPeek,
|
||||
InstrKind::GateEndPeek,
|
||||
InstrKind::GateBeginBorrow,
|
||||
InstrKind::GateEndBorrow,
|
||||
InstrKind::GateBeginMutate,
|
||||
InstrKind::GateEndMutate,
|
||||
];
|
||||
|
||||
let serialized = serde_json::to_string_pretty(&instructions).unwrap();
|
||||
|
||||
// This is a "lock" on the ISA surface.
|
||||
// If the structure of InstrKind changes, the serialization will change.
|
||||
let expected_json = r#"[
|
||||
"Nop",
|
||||
"Halt",
|
||||
{
|
||||
"PushConst": 0
|
||||
},
|
||||
{
|
||||
"PushBool": true
|
||||
},
|
||||
"PushNull",
|
||||
"Pop",
|
||||
"Dup",
|
||||
"Swap",
|
||||
"Add",
|
||||
"Sub",
|
||||
"Mul",
|
||||
"Div",
|
||||
"Neg",
|
||||
"Eq",
|
||||
"Neq",
|
||||
"Lt",
|
||||
"Gt",
|
||||
"Lte",
|
||||
"Gte",
|
||||
"And",
|
||||
"Or",
|
||||
"Not",
|
||||
"BitAnd",
|
||||
"BitOr",
|
||||
"BitXor",
|
||||
"Shl",
|
||||
"Shr",
|
||||
{
|
||||
"GetLocal": 0
|
||||
},
|
||||
{
|
||||
"SetLocal": 0
|
||||
},
|
||||
{
|
||||
"GetGlobal": 0
|
||||
},
|
||||
{
|
||||
"SetGlobal": 0
|
||||
},
|
||||
{
|
||||
"Jmp": "target"
|
||||
},
|
||||
{
|
||||
"JmpIfFalse": "target"
|
||||
},
|
||||
{
|
||||
"Label": "target"
|
||||
},
|
||||
{
|
||||
"Call": {
|
||||
"func_id": 0,
|
||||
"arg_count": 0
|
||||
}
|
||||
},
|
||||
"Ret",
|
||||
{
|
||||
"Syscall": 0
|
||||
},
|
||||
"FrameSync",
|
||||
{
|
||||
"Alloc": {
|
||||
"type_id": 0,
|
||||
"slots": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"GateLoad": {
|
||||
"offset": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"GateStore": {
|
||||
"offset": 0
|
||||
}
|
||||
},
|
||||
"GateBeginPeek",
|
||||
"GateEndPeek",
|
||||
"GateBeginBorrow",
|
||||
"GateEndBorrow",
|
||||
"GateBeginMutate",
|
||||
"GateEndMutate"
|
||||
]"#;
|
||||
assert_eq!(serialized, expected_json);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,9 +15,7 @@ pub mod validate;
|
||||
|
||||
pub use instr::{Instruction, InstrKind, Label};
|
||||
pub use module::{Module, Function, Global, Param};
|
||||
pub use types::{Type, Value, GateId};
|
||||
// Note: ConstId and TypeId are not exported here to avoid conflict with ir_core::ids
|
||||
// until the crates are fully decoupled.
|
||||
pub use types::{Type, Value, GateId, ConstId, TypeId};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@ -40,7 +38,7 @@ mod tests {
|
||||
params: vec![],
|
||||
return_type: Type::Null,
|
||||
body: vec![
|
||||
Instruction::new(InstrKind::PushConst(ConstId(0)), None),
|
||||
Instruction::new(InstrKind::PushConst(crate::ir_vm::types::ConstId(0)), None),
|
||||
Instruction::new(InstrKind::Call { func_id: FunctionId(2), arg_count: 1 }, None),
|
||||
Instruction::new(InstrKind::Ret, None),
|
||||
],
|
||||
|
||||
@ -47,7 +47,7 @@ pub fn lower_function(core_func: &ir_core::Function) -> Result<ir_vm::Function>
|
||||
|
||||
for instr in &block.instrs {
|
||||
let kind = match instr {
|
||||
ir_core::Instr::PushConst(id) => ir_vm::InstrKind::PushConst(*id),
|
||||
ir_core::Instr::PushConst(id) => ir_vm::InstrKind::PushConst(ir_vm::ConstId(id.0)),
|
||||
ir_core::Instr::Call(func_id, arg_count) => ir_vm::InstrKind::Call {
|
||||
func_id: *func_id,
|
||||
arg_count: *arg_count
|
||||
@ -71,15 +71,27 @@ pub fn lower_function(core_func: &ir_core::Function) -> Result<ir_vm::Function>
|
||||
ir_core::Instr::And => ir_vm::InstrKind::And,
|
||||
ir_core::Instr::Or => ir_vm::InstrKind::Or,
|
||||
ir_core::Instr::Not => ir_vm::InstrKind::Not,
|
||||
ir_core::Instr::Alloc { .. } => ir_vm::InstrKind::Alloc,
|
||||
ir_core::Instr::BeginPeek { .. } |
|
||||
ir_core::Instr::BeginBorrow { .. } |
|
||||
ir_core::Instr::BeginMutate { .. } => ir_vm::InstrKind::LoadRef(0),
|
||||
ir_core::Instr::EndPeek |
|
||||
ir_core::Instr::EndBorrow |
|
||||
ir_core::Instr::EndMutate => ir_vm::InstrKind::Nop,
|
||||
ir_core::Instr::LoadRef(offset) => ir_vm::InstrKind::LoadRef(*offset),
|
||||
ir_core::Instr::StoreRef(offset) => ir_vm::InstrKind::StoreRef(*offset),
|
||||
ir_core::Instr::Alloc { ty, slots } => ir_vm::InstrKind::Alloc {
|
||||
type_id: ir_vm::TypeId(ty.0),
|
||||
slots: *slots
|
||||
},
|
||||
ir_core::Instr::BeginPeek { .. } => {
|
||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateBeginPeek, None));
|
||||
ir_vm::InstrKind::GateLoad { offset: 0 }
|
||||
}
|
||||
ir_core::Instr::BeginBorrow { .. } => {
|
||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateBeginBorrow, None));
|
||||
ir_vm::InstrKind::GateLoad { offset: 0 }
|
||||
}
|
||||
ir_core::Instr::BeginMutate { .. } => {
|
||||
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateBeginMutate, None));
|
||||
ir_vm::InstrKind::GateLoad { offset: 0 }
|
||||
}
|
||||
ir_core::Instr::EndPeek => ir_vm::InstrKind::GateEndPeek,
|
||||
ir_core::Instr::EndBorrow => ir_vm::InstrKind::GateEndBorrow,
|
||||
ir_core::Instr::EndMutate => ir_vm::InstrKind::GateEndMutate,
|
||||
ir_core::Instr::LoadRef(offset) => ir_vm::InstrKind::GateLoad { offset: *offset },
|
||||
ir_core::Instr::StoreRef(offset) => ir_vm::InstrKind::GateStore { offset: *offset },
|
||||
ir_core::Instr::Free => ir_vm::InstrKind::Nop,
|
||||
};
|
||||
vm_func.body.push(ir_vm::Instruction::new(kind, None));
|
||||
@ -133,7 +145,8 @@ fn lower_type(ty: &ir_core::Type) -> ir_vm::Type {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ir_core;
|
||||
use crate::ir_core::*;
|
||||
use crate::ir_core::{Block, Instr, Terminator, ConstantValue, Program, ConstPool};
|
||||
use crate::ir_core::ids::{FunctionId, ConstId as CoreConstId};
|
||||
use crate::ir_vm::*;
|
||||
|
||||
#[test]
|
||||
@ -154,7 +167,7 @@ mod tests {
|
||||
Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::PushConst(ConstId(0)),
|
||||
Instr::PushConst(CoreConstId(0)),
|
||||
Instr::Call(FunctionId(2), 1),
|
||||
],
|
||||
terminator: Terminator::Jump(1),
|
||||
|
||||
@ -1,72 +1,3 @@
|
||||
# PR-02 — Define `ir_vm` ISA v0 (Memory & Gates Only)
|
||||
|
||||
### Goal
|
||||
|
||||
Define a minimal, PBS-compatible but PBS-agnostic VM instruction set.
|
||||
|
||||
### Required Instructions
|
||||
|
||||
#### Constant Pool
|
||||
|
||||
* `PushConst(ConstId)`
|
||||
|
||||
#### HIP Allocation (Deterministic)
|
||||
|
||||
* `Alloc { type_id: TypeId, slots: u32 }`
|
||||
|
||||
#### Gate-Based Heap Access
|
||||
|
||||
* `GateLoad { offset: u32 }`
|
||||
* `GateStore { offset: u32 }`
|
||||
|
||||
> All heap access must follow: gate validation → base+slots resolution → bounds check → read/write.
|
||||
|
||||
#### Scope Markers (Semantic Preservation)
|
||||
|
||||
* `GateBeginPeek` / `GateEndPeek`
|
||||
* `GateBeginBorrow` / `GateEndBorrow`
|
||||
* `GateBeginMutate` / `GateEndMutate`
|
||||
|
||||
> These may be runtime no-ops in v0 but must exist to preserve semantics and debug invariants.
|
||||
|
||||
#### Safe Point Hook
|
||||
|
||||
* `FrameSync` (optional but recommended)
|
||||
|
||||
### Non-goals
|
||||
|
||||
* No RC implementation
|
||||
* No VM execution logic
|
||||
|
||||
### Tests
|
||||
|
||||
* Unit tests ensuring the instruction enum is stable and cloneable
|
||||
* Snapshot or debug-format test to lock the ISA surface
|
||||
|
||||
---
|
||||
|
||||
# PR-03 — Remove “Ref” Leakage from `ir_vm`
|
||||
|
||||
### Goal
|
||||
|
||||
Eliminate pointer-based mental models from the VM IR.
|
||||
|
||||
### Required Changes
|
||||
|
||||
* Rename any existing `LoadRef` / `StoreRef` to:
|
||||
|
||||
* `LocalLoad { slot: u32 }`
|
||||
* `LocalStore { slot: u32 }`
|
||||
* Remove or rename any type named `Ref` that refers to HIP
|
||||
|
||||
**Hard rule:** the word `Ref` must never refer to HIP memory in `ir_vm`.
|
||||
|
||||
### Tests
|
||||
|
||||
* Grep-style or unit test ensuring no `Ref`-named HIP ops exist in `ir_vm`
|
||||
|
||||
---
|
||||
|
||||
# PR-04 — Update `core_to_vm` Lowering (Kill Placeholders)
|
||||
|
||||
### Goal
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user