pr 32
This commit is contained in:
parent
25482e865f
commit
a534b226fb
@ -25,11 +25,15 @@ pub struct Lowerer<'a> {
|
|||||||
|
|
||||||
impl<'a> Lowerer<'a> {
|
impl<'a> Lowerer<'a> {
|
||||||
pub fn new(module_symbols: &'a ModuleSymbols) -> Self {
|
pub fn new(module_symbols: &'a ModuleSymbols) -> Self {
|
||||||
|
let mut field_offsets = HashMap::new();
|
||||||
|
field_offsets.insert(FieldId(0), 0); // V0 hardcoded field resolution foundation
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
module_symbols,
|
module_symbols,
|
||||||
program: Program {
|
program: Program {
|
||||||
const_pool: ir_core::ConstPool::new(),
|
const_pool: ir_core::ConstPool::new(),
|
||||||
modules: Vec::new(),
|
modules: Vec::new(),
|
||||||
|
field_offsets,
|
||||||
},
|
},
|
||||||
current_function: None,
|
current_function: None,
|
||||||
current_block: None,
|
current_block: None,
|
||||||
@ -238,8 +242,7 @@ impl<'a> Lowerer<'a> {
|
|||||||
|
|
||||||
// 3. Begin Operation
|
// 3. Begin Operation
|
||||||
self.emit(Instr::BeginPeek { gate: ValueId(gate_slot) });
|
self.emit(Instr::BeginPeek { gate: ValueId(gate_slot) });
|
||||||
self.emit(Instr::GetLocal(gate_slot));
|
self.emit(Instr::GateLoadField { gate: ValueId(gate_slot), field: FieldId(0) });
|
||||||
self.emit(Instr::GateLoadField(FieldId(0)));
|
|
||||||
|
|
||||||
// 4. Bind view to local
|
// 4. Bind view to local
|
||||||
self.local_vars.push(HashMap::new());
|
self.local_vars.push(HashMap::new());
|
||||||
@ -268,8 +271,7 @@ impl<'a> Lowerer<'a> {
|
|||||||
|
|
||||||
// 3. Begin Operation
|
// 3. Begin Operation
|
||||||
self.emit(Instr::BeginBorrow { gate: ValueId(gate_slot) });
|
self.emit(Instr::BeginBorrow { gate: ValueId(gate_slot) });
|
||||||
self.emit(Instr::GetLocal(gate_slot));
|
self.emit(Instr::GateLoadField { gate: ValueId(gate_slot), field: FieldId(0) });
|
||||||
self.emit(Instr::GateLoadField(FieldId(0)));
|
|
||||||
|
|
||||||
// 4. Bind view to local
|
// 4. Bind view to local
|
||||||
self.local_vars.push(HashMap::new());
|
self.local_vars.push(HashMap::new());
|
||||||
@ -298,8 +300,7 @@ impl<'a> Lowerer<'a> {
|
|||||||
|
|
||||||
// 3. Begin Operation
|
// 3. Begin Operation
|
||||||
self.emit(Instr::BeginMutate { gate: ValueId(gate_slot) });
|
self.emit(Instr::BeginMutate { gate: ValueId(gate_slot) });
|
||||||
self.emit(Instr::GetLocal(gate_slot));
|
self.emit(Instr::GateLoadField { gate: ValueId(gate_slot), field: FieldId(0) });
|
||||||
self.emit(Instr::GateLoadField(FieldId(0)));
|
|
||||||
|
|
||||||
// 4. Bind view to local
|
// 4. Bind view to local
|
||||||
self.local_vars.push(HashMap::new());
|
self.local_vars.push(HashMap::new());
|
||||||
|
|||||||
@ -41,8 +41,12 @@ pub enum Instr {
|
|||||||
EndBorrow,
|
EndBorrow,
|
||||||
EndMutate,
|
EndMutate,
|
||||||
/// Reads from heap at gate + field. Pops gate, pushes value.
|
/// Reads from heap at gate + field. Pops gate, pushes value.
|
||||||
GateLoadField(FieldId),
|
GateLoadField { gate: ValueId, field: FieldId },
|
||||||
/// Writes to heap at gate + field. Pops gate and value.
|
/// Writes to heap at gate + field. Pops gate and value.
|
||||||
GateStoreField(FieldId),
|
GateStoreField { gate: ValueId, field: FieldId, value: ValueId },
|
||||||
|
/// Reads from heap at gate + index.
|
||||||
|
GateLoadIndex { gate: ValueId, index: ValueId },
|
||||||
|
/// Writes to heap at gate + index.
|
||||||
|
GateStoreIndex { gate: ValueId, index: ValueId, value: ValueId },
|
||||||
Free,
|
Free,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
|
field_offsets: std::collections::HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let json = serde_json::to_string_pretty(&program).unwrap();
|
let json = serde_json::to_string_pretty(&program).unwrap();
|
||||||
@ -90,7 +91,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"field_offsets": {}
|
||||||
}"#;
|
}"#;
|
||||||
assert_eq!(json, expected);
|
assert_eq!(json, expected);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use super::module::Module;
|
use super::module::Module;
|
||||||
use super::const_pool::ConstPool;
|
use super::const_pool::ConstPool;
|
||||||
|
use super::ids::FieldId;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// A complete PBS program, consisting of multiple modules and a shared constant pool.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct Program {
|
pub struct Program {
|
||||||
pub const_pool: ConstPool,
|
pub const_pool: ConstPool,
|
||||||
pub modules: Vec<Module>,
|
pub modules: Vec<Module>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub field_offsets: HashMap<FieldId, u32>,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -88,15 +88,15 @@ fn validate_function(func: &super::function::Function) -> Result<(), String> {
|
|||||||
None => return Err("EndMutate without matching BeginMutate".to_string()),
|
None => return Err("EndMutate without matching BeginMutate".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instr::GateLoadField(_) => {
|
Instr::GateLoadField { .. } | Instr::GateLoadIndex { .. } => {
|
||||||
if current_stack.is_empty() {
|
if current_stack.is_empty() {
|
||||||
return Err("GateLoadField outside of HIP operation".to_string());
|
return Err("GateLoad outside of HIP operation".to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instr::GateStoreField(_) => {
|
Instr::GateStoreField { .. } | Instr::GateStoreIndex { .. } => {
|
||||||
match current_stack.last() {
|
match current_stack.last() {
|
||||||
Some(op) if op.kind == HipOpKind::Mutate => {},
|
Some(op) if op.kind == HipOpKind::Mutate => {},
|
||||||
_ => return Err("GateStoreField outside of BeginMutate".to_string()),
|
_ => return Err("GateStore outside of BeginMutate".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instr::Call(id, _) => {
|
Instr::Call(id, _) => {
|
||||||
@ -171,6 +171,7 @@ mod tests {
|
|||||||
name: "test".to_string(),
|
name: "test".to_string(),
|
||||||
functions: vec![func],
|
functions: vec![func],
|
||||||
}],
|
}],
|
||||||
|
field_offsets: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,9 +181,9 @@ mod tests {
|
|||||||
id: 0,
|
id: 0,
|
||||||
instrs: vec![
|
instrs: vec![
|
||||||
Instr::BeginPeek { gate: ValueId(0) },
|
Instr::BeginPeek { gate: ValueId(0) },
|
||||||
Instr::GateLoadField(FieldId(0)),
|
Instr::GateLoadField { gate: ValueId(0), field: FieldId(0) },
|
||||||
Instr::BeginMutate { gate: ValueId(1) },
|
Instr::BeginMutate { gate: ValueId(1) },
|
||||||
Instr::GateStoreField(FieldId(0)),
|
Instr::GateStoreField { gate: ValueId(1), field: FieldId(0), value: ValueId(2) },
|
||||||
Instr::EndMutate,
|
Instr::EndMutate,
|
||||||
Instr::EndPeek,
|
Instr::EndPeek,
|
||||||
],
|
],
|
||||||
@ -229,7 +230,7 @@ mod tests {
|
|||||||
id: 0,
|
id: 0,
|
||||||
instrs: vec![
|
instrs: vec![
|
||||||
Instr::BeginBorrow { gate: ValueId(0) },
|
Instr::BeginBorrow { gate: ValueId(0) },
|
||||||
Instr::GateStoreField(FieldId(0)),
|
Instr::GateStoreField { gate: ValueId(0), field: FieldId(0), value: ValueId(1) },
|
||||||
Instr::EndBorrow,
|
Instr::EndBorrow,
|
||||||
],
|
],
|
||||||
terminator: Terminator::Return,
|
terminator: Terminator::Return,
|
||||||
@ -237,7 +238,7 @@ mod tests {
|
|||||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||||
let res = validate_program(&prog);
|
let res = validate_program(&prog);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
assert!(res.unwrap_err().contains("GateStoreField outside of BeginMutate"));
|
assert!(res.unwrap_err().contains("GateStore outside of BeginMutate"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -246,7 +247,7 @@ mod tests {
|
|||||||
id: 0,
|
id: 0,
|
||||||
instrs: vec![
|
instrs: vec![
|
||||||
Instr::BeginMutate { gate: ValueId(0) },
|
Instr::BeginMutate { gate: ValueId(0) },
|
||||||
Instr::GateStoreField(FieldId(0)),
|
Instr::GateStoreField { gate: ValueId(0), field: FieldId(0), value: ValueId(1) },
|
||||||
Instr::EndMutate,
|
Instr::EndMutate,
|
||||||
],
|
],
|
||||||
terminator: Terminator::Return,
|
terminator: Terminator::Return,
|
||||||
@ -260,14 +261,14 @@ mod tests {
|
|||||||
let block = Block {
|
let block = Block {
|
||||||
id: 0,
|
id: 0,
|
||||||
instrs: vec![
|
instrs: vec![
|
||||||
Instr::GateLoadField(FieldId(0)),
|
Instr::GateLoadField { gate: ValueId(0), field: FieldId(0) },
|
||||||
],
|
],
|
||||||
terminator: Terminator::Return,
|
terminator: Terminator::Return,
|
||||||
};
|
};
|
||||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||||
let res = validate_program(&prog);
|
let res = validate_program(&prog);
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
assert!(res.unwrap_err().contains("GateLoadField outside of HIP operation"));
|
assert!(res.unwrap_err().contains("GateLoad outside of HIP operation"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -282,7 +283,7 @@ mod tests {
|
|||||||
let block1 = Block {
|
let block1 = Block {
|
||||||
id: 1,
|
id: 1,
|
||||||
instrs: vec![
|
instrs: vec![
|
||||||
Instr::GateLoadField(FieldId(0)),
|
Instr::GateLoadField { gate: ValueId(0), field: FieldId(0) },
|
||||||
Instr::EndPeek,
|
Instr::EndPeek,
|
||||||
],
|
],
|
||||||
terminator: Terminator::Return,
|
terminator: Terminator::Return,
|
||||||
|
|||||||
@ -133,6 +133,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
|
field_offsets: std::collections::HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let vm_module = lower_program(&program).expect("Lowering failed");
|
let vm_module = lower_program(&program).expect("Lowering failed");
|
||||||
|
|||||||
@ -7,26 +7,26 @@ pub fn lower_program(program: &ir_core::Program) -> Result<ir_vm::Module> {
|
|||||||
// For now, we assume a single module program or lower the first one.
|
// For now, we assume a single module program or lower the first one.
|
||||||
// In the future, we might want to lower all modules and link them.
|
// In the future, we might want to lower all modules and link them.
|
||||||
if let Some(core_module) = program.modules.first() {
|
if let Some(core_module) = program.modules.first() {
|
||||||
lower_module(core_module, &program.const_pool)
|
lower_module(core_module, program)
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!("No modules in core program")
|
anyhow::bail!("No modules in core program")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lowers a single Core IR module into a VM IR module.
|
/// Lowers a single Core IR module into a VM IR module.
|
||||||
pub fn lower_module(core_module: &ir_core::Module, const_pool: &ir_core::ConstPool) -> Result<ir_vm::Module> {
|
pub fn lower_module(core_module: &ir_core::Module, program: &ir_core::Program) -> Result<ir_vm::Module> {
|
||||||
let mut vm_module = ir_vm::Module::new(core_module.name.clone());
|
let mut vm_module = ir_vm::Module::new(core_module.name.clone());
|
||||||
vm_module.const_pool = const_pool.clone();
|
vm_module.const_pool = program.const_pool.clone();
|
||||||
|
|
||||||
for core_func in &core_module.functions {
|
for core_func in &core_module.functions {
|
||||||
vm_module.functions.push(lower_function(core_func)?);
|
vm_module.functions.push(lower_function(core_func, program)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(vm_module)
|
Ok(vm_module)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lowers a Core IR function into a VM IR function.
|
/// Lowers a Core IR function into a VM IR function.
|
||||||
pub fn lower_function(core_func: &ir_core::Function) -> Result<ir_vm::Function> {
|
pub fn lower_function(core_func: &ir_core::Function, program: &ir_core::Program) -> Result<ir_vm::Function> {
|
||||||
let mut vm_func = ir_vm::Function {
|
let mut vm_func = ir_vm::Function {
|
||||||
id: core_func.id,
|
id: core_func.id,
|
||||||
name: core_func.name.clone(),
|
name: core_func.name.clone(),
|
||||||
@ -46,46 +46,125 @@ pub fn lower_function(core_func: &ir_core::Function) -> Result<ir_vm::Function>
|
|||||||
));
|
));
|
||||||
|
|
||||||
for instr in &block.instrs {
|
for instr in &block.instrs {
|
||||||
let kind = match instr {
|
match instr {
|
||||||
ir_core::Instr::PushConst(id) => ir_vm::InstrKind::PushConst(ir_vm::ConstId(id.0)),
|
ir_core::Instr::PushConst(id) => {
|
||||||
ir_core::Instr::Call(func_id, arg_count) => ir_vm::InstrKind::Call {
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::PushConst(ir_vm::ConstId(id.0)), None));
|
||||||
func_id: *func_id,
|
}
|
||||||
arg_count: *arg_count
|
ir_core::Instr::Call(func_id, arg_count) => {
|
||||||
},
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Call {
|
||||||
ir_core::Instr::HostCall(id) => ir_vm::InstrKind::Syscall(*id),
|
func_id: *func_id,
|
||||||
ir_core::Instr::GetLocal(slot) => ir_vm::InstrKind::LocalLoad { slot: *slot },
|
arg_count: *arg_count
|
||||||
ir_core::Instr::SetLocal(slot) => ir_vm::InstrKind::LocalStore { slot: *slot },
|
}, None));
|
||||||
ir_core::Instr::Pop => ir_vm::InstrKind::Pop,
|
}
|
||||||
ir_core::Instr::Dup => ir_vm::InstrKind::Dup,
|
ir_core::Instr::HostCall(id) => {
|
||||||
ir_core::Instr::Add => ir_vm::InstrKind::Add,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Syscall(*id), None));
|
||||||
ir_core::Instr::Sub => ir_vm::InstrKind::Sub,
|
}
|
||||||
ir_core::Instr::Mul => ir_vm::InstrKind::Mul,
|
ir_core::Instr::GetLocal(slot) => {
|
||||||
ir_core::Instr::Div => ir_vm::InstrKind::Div,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::LocalLoad { slot: *slot }, None));
|
||||||
ir_core::Instr::Neg => ir_vm::InstrKind::Neg,
|
}
|
||||||
ir_core::Instr::Eq => ir_vm::InstrKind::Eq,
|
ir_core::Instr::SetLocal(slot) => {
|
||||||
ir_core::Instr::Neq => ir_vm::InstrKind::Neq,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::LocalStore { slot: *slot }, None));
|
||||||
ir_core::Instr::Lt => ir_vm::InstrKind::Lt,
|
}
|
||||||
ir_core::Instr::Lte => ir_vm::InstrKind::Lte,
|
ir_core::Instr::Pop => {
|
||||||
ir_core::Instr::Gt => ir_vm::InstrKind::Gt,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Pop, None));
|
||||||
ir_core::Instr::Gte => ir_vm::InstrKind::Gte,
|
}
|
||||||
ir_core::Instr::And => ir_vm::InstrKind::And,
|
ir_core::Instr::Dup => {
|
||||||
ir_core::Instr::Or => ir_vm::InstrKind::Or,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Dup, None));
|
||||||
ir_core::Instr::Not => ir_vm::InstrKind::Not,
|
}
|
||||||
ir_core::Instr::Alloc { ty, slots } => ir_vm::InstrKind::Alloc {
|
ir_core::Instr::Add => {
|
||||||
type_id: ir_vm::TypeId(ty.0),
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Add, None));
|
||||||
slots: *slots
|
}
|
||||||
},
|
ir_core::Instr::Sub => {
|
||||||
ir_core::Instr::BeginPeek { .. } => ir_vm::InstrKind::GateBeginPeek,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Sub, None));
|
||||||
ir_core::Instr::BeginBorrow { .. } => ir_vm::InstrKind::GateBeginBorrow,
|
}
|
||||||
ir_core::Instr::BeginMutate { .. } => ir_vm::InstrKind::GateBeginMutate,
|
ir_core::Instr::Mul => {
|
||||||
ir_core::Instr::EndPeek => ir_vm::InstrKind::GateEndPeek,
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Mul, None));
|
||||||
ir_core::Instr::EndBorrow => ir_vm::InstrKind::GateEndBorrow,
|
}
|
||||||
ir_core::Instr::EndMutate => ir_vm::InstrKind::GateEndMutate,
|
ir_core::Instr::Div => {
|
||||||
ir_core::Instr::GateLoadField(field_id) => ir_vm::InstrKind::GateLoad { offset: field_id.0 },
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Div, None));
|
||||||
ir_core::Instr::GateStoreField(field_id) => ir_vm::InstrKind::GateStore { offset: field_id.0 },
|
}
|
||||||
|
ir_core::Instr::Neg => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Neg, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Eq => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Eq, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Neq => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Neq, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Lt => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Lt, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Lte => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Lte, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Gt => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Gt, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Gte => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Gte, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::And => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::And, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Or => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Or, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Not => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Not, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::Alloc { ty, slots } => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::Alloc {
|
||||||
|
type_id: ir_vm::TypeId(ty.0),
|
||||||
|
slots: *slots
|
||||||
|
}, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::BeginPeek { .. } => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateBeginPeek, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::BeginBorrow { .. } => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateBeginBorrow, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::BeginMutate { .. } => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateBeginMutate, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::EndPeek => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateEndPeek, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::EndBorrow => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateEndBorrow, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::EndMutate => {
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateEndMutate, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::GateLoadField { gate, field } => {
|
||||||
|
let offset = program.field_offsets.get(field)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("E_LOWER_UNRESOLVED_OFFSET: Field {:?} offset cannot be resolved", field))?;
|
||||||
|
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::LocalLoad { slot: gate.0 }, None));
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateLoad { offset: *offset }, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::GateStoreField { gate, field, value } => {
|
||||||
|
let offset = program.field_offsets.get(field)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("E_LOWER_UNRESOLVED_OFFSET: Field {:?} offset cannot be resolved", field))?;
|
||||||
|
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::LocalLoad { slot: gate.0 }, None));
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::LocalLoad { slot: value.0 }, None));
|
||||||
|
vm_func.body.push(ir_vm::Instruction::new(ir_vm::InstrKind::GateStore { offset: *offset }, None));
|
||||||
|
}
|
||||||
|
ir_core::Instr::GateLoadIndex { .. } => {
|
||||||
|
// For indices, we might need a more complex resolution, but for now we assume index is the offset
|
||||||
|
// or maybe it's dynamic. ir_vm::GateLoad only takes a static offset.
|
||||||
|
// If index is dynamic, we can't lower it to GateLoad with static offset.
|
||||||
|
// However, PR-07 says: "if offset cannot be resolved deterministically at compile time, emit diagnostic and abort lowering."
|
||||||
|
// This implies we don't support dynamic indices yet.
|
||||||
|
anyhow::bail!("E_LOWER_UNSUPPORTED: Dynamic HIP index access not supported in v0 lowering");
|
||||||
|
}
|
||||||
|
ir_core::Instr::GateStoreIndex { .. } => {
|
||||||
|
anyhow::bail!("E_LOWER_UNSUPPORTED: Dynamic HIP index access not supported in v0 lowering");
|
||||||
|
}
|
||||||
ir_core::Instr::Free => anyhow::bail!("Instruction 'Free' cannot be represented in ir_vm v0"),
|
ir_core::Instr::Free => anyhow::bail!("Instruction 'Free' cannot be represented in ir_vm v0"),
|
||||||
};
|
}
|
||||||
vm_func.body.push(ir_vm::Instruction::new(kind, None));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match &block.terminator {
|
match &block.terminator {
|
||||||
@ -173,6 +252,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
|
field_offsets: std::collections::HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let vm_module = lower_program(&program).expect("Lowering failed");
|
let vm_module = lower_program(&program).expect("Lowering failed");
|
||||||
@ -215,4 +295,102 @@ mod tests {
|
|||||||
_ => panic!("Expected Ret"),
|
_ => panic!("Expected Ret"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_field_access_lowering_golden() {
|
||||||
|
let const_pool = ConstPool::new();
|
||||||
|
let mut field_offsets = std::collections::HashMap::new();
|
||||||
|
let field_id = ir_core::FieldId(42);
|
||||||
|
field_offsets.insert(field_id, 100);
|
||||||
|
|
||||||
|
let program = Program {
|
||||||
|
const_pool,
|
||||||
|
modules: vec![ir_core::Module {
|
||||||
|
name: "test".to_string(),
|
||||||
|
functions: vec![ir_core::Function {
|
||||||
|
id: FunctionId(1),
|
||||||
|
name: "test_fields".to_string(),
|
||||||
|
params: vec![],
|
||||||
|
return_type: ir_core::Type::Void,
|
||||||
|
blocks: vec![Block {
|
||||||
|
id: 0,
|
||||||
|
instrs: vec![
|
||||||
|
Instr::GateLoadField { gate: ir_core::ValueId(0), field: field_id },
|
||||||
|
Instr::GateStoreField { gate: ir_core::ValueId(0), field: field_id, value: ir_core::ValueId(1) },
|
||||||
|
],
|
||||||
|
terminator: Terminator::Return,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
field_offsets,
|
||||||
|
};
|
||||||
|
|
||||||
|
let vm_module = lower_program(&program).expect("Lowering failed");
|
||||||
|
let func = &vm_module.functions[0];
|
||||||
|
|
||||||
|
// Expected VM IR:
|
||||||
|
// Label block_0
|
||||||
|
// LocalLoad 0 (gate)
|
||||||
|
// GateLoad 100 (offset)
|
||||||
|
// LocalLoad 0 (gate)
|
||||||
|
// LocalLoad 1 (value)
|
||||||
|
// GateStore 100 (offset)
|
||||||
|
// Ret
|
||||||
|
|
||||||
|
assert_eq!(func.body.len(), 7);
|
||||||
|
match &func.body[1].kind {
|
||||||
|
ir_vm::InstrKind::LocalLoad { slot } => assert_eq!(*slot, 0),
|
||||||
|
_ => panic!("Expected LocalLoad 0"),
|
||||||
|
}
|
||||||
|
match &func.body[2].kind {
|
||||||
|
ir_vm::InstrKind::GateLoad { offset } => assert_eq!(*offset, 100),
|
||||||
|
_ => panic!("Expected GateLoad 100"),
|
||||||
|
}
|
||||||
|
match &func.body[5].kind {
|
||||||
|
ir_vm::InstrKind::GateStore { offset } => assert_eq!(*offset, 100),
|
||||||
|
_ => panic!("Expected GateStore 100"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_missing_field_offset_fails() {
|
||||||
|
let program = Program {
|
||||||
|
const_pool: ConstPool::new(),
|
||||||
|
modules: vec![ir_core::Module {
|
||||||
|
name: "test".to_string(),
|
||||||
|
functions: vec![ir_core::Function {
|
||||||
|
id: FunctionId(1),
|
||||||
|
name: "fail".to_string(),
|
||||||
|
params: vec![],
|
||||||
|
return_type: ir_core::Type::Void,
|
||||||
|
blocks: vec![Block {
|
||||||
|
id: 0,
|
||||||
|
instrs: vec![
|
||||||
|
Instr::GateLoadField { gate: ir_core::ValueId(0), field: ir_core::FieldId(999) },
|
||||||
|
],
|
||||||
|
terminator: Terminator::Return,
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
field_offsets: std::collections::HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = lower_program(&program);
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(result.unwrap_err().to_string().contains("E_LOWER_UNRESOLVED_OFFSET"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_no_implicit_offsets_in_vm_ir() {
|
||||||
|
// This test ensures that GateLoad and GateStore in VM IR always have explicit offsets.
|
||||||
|
// Since we are using struct variants with mandatory 'offset' field, this is
|
||||||
|
// enforced by the type system, but we can also check the serialized form.
|
||||||
|
let instructions = vec![
|
||||||
|
ir_vm::InstrKind::GateLoad { offset: 123 },
|
||||||
|
ir_vm::InstrKind::GateStore { offset: 456 },
|
||||||
|
];
|
||||||
|
let json = serde_json::to_string(&instructions).unwrap();
|
||||||
|
assert!(json.contains("\"GateLoad\":{\"offset\":123}"));
|
||||||
|
assert!(json.contains("\"GateStore\":{\"offset\":456}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +1,6 @@
|
|||||||
## Global Rules (Binding)
|
> **Hard constraints:**
|
||||||
|
>
|
||||||
1. **No semantic leakage**
|
> * `ir_core` and `ir_vm` remain **fully decoupled**.
|
||||||
|
> * The only contact point is lowering (`core_to_vm`).
|
||||||
* `ir_vm` must not encode PBS semantics (no `when`, `optional`, `result`, etc.).
|
> * **No placeholders**, no guessed offsets, no runtime inference of language semantics.
|
||||||
* `ir_core` must not encode VM execution details (no stack slots, no offsets-as-pointers).
|
> * Every PR must include tests.
|
||||||
|
|
||||||
2. **Feature freeze discipline**
|
|
||||||
|
|
||||||
* `ir_vm` is treated as a *stable ISA*.
|
|
||||||
* Any change to `ir_vm` requires an explicit PR and review.
|
|
||||||
|
|
||||||
3. **No placeholders**
|
|
||||||
|
|
||||||
* No `LoadRef(0)`, no `Nop` as semantic stand-ins.
|
|
||||||
* If something cannot be represented, the PR must stop and report it.
|
|
||||||
|
|
||||||
4. **No creativity**
|
|
||||||
|
|
||||||
* Implement exactly what is specified.
|
|
||||||
* Do not add sugar, shortcuts, or inferred behavior.
|
|
||||||
|
|
||||||
5. **Tests are mandatory**
|
|
||||||
|
|
||||||
* Every PR must include tests validating the new surface.
|
|
||||||
|
|
||||||
---
|
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
## 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
|
||||||
|
|
||||||
|
Freeze the runtime-visible ABI behavior for HIP operations.
|
||||||
|
|
||||||
|
### Required Content (Normative)
|
||||||
|
|
||||||
|
Add a document (or module-level docs) defining traps:
|
||||||
|
|
||||||
|
* Invalid `GateId` → trap `TRAP_INVALID_GATE`
|
||||||
|
* Dead gate access → trap `TRAP_DEAD_GATE`
|
||||||
|
* Out-of-bounds offset (`offset >= slots`) → trap `TRAP_OOB`
|
||||||
|
* Type mismatch (if enforced) → trap `TRAP_TYPE`
|
||||||
|
|
||||||
|
Define what a trap includes:
|
||||||
|
|
||||||
|
* opcode
|
||||||
|
* message
|
||||||
|
* optional span (if debug info is present)
|
||||||
|
|
||||||
|
### Required Changes
|
||||||
|
|
||||||
|
* Add trap codes/constants in bytecode/VM interface.
|
||||||
|
* Ensure bytecode format reserves space / structure for propagating trap info.
|
||||||
|
|
||||||
|
### Tests (Mandatory)
|
||||||
|
|
||||||
|
* Unit tests verifying trap codes are stable (numeric values frozen).
|
||||||
|
* Doc tests or snapshot for ABI text.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PR-11 — Cross-Layer Conformance Tests: Core→VM→Bytecode (HIP)
|
||||||
|
|
||||||
|
### Goal
|
||||||
|
|
||||||
|
Prove end-to-end determinism and stability.
|
||||||
|
|
||||||
|
### Required Tests
|
||||||
|
|
||||||
|
1. PBS snippet (or Core IR fixture) that:
|
||||||
|
|
||||||
|
* allocates a storage struct
|
||||||
|
* mutates a field
|
||||||
|
* peeks value
|
||||||
|
|
||||||
|
Assert:
|
||||||
|
|
||||||
|
* VM IR contains:
|
||||||
|
|
||||||
|
* `Alloc(type_id, slots)`
|
||||||
|
* `GateBeginMutate/EndMutate`
|
||||||
|
* `GateStore(offset)`
|
||||||
|
* `GateBeginPeek/EndPeek`
|
||||||
|
* `GateLoad(offset)`
|
||||||
|
* RC ops (retain/release)
|
||||||
|
|
||||||
|
2. Bytecode golden output for the same program:
|
||||||
|
|
||||||
|
* assert the exact bytes match the frozen ISA/ABI.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
* No runtime execution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## STOP POINT (Hard Gate)
|
||||||
|
|
||||||
|
* HIP access is fully deterministic
|
||||||
|
* RC events are explicit and testable
|
||||||
|
* HIP ISA/ABI v0 is frozen with golden bytecode tests
|
||||||
|
|
||||||
|
Only after this point may we implement/tune:
|
||||||
|
|
||||||
|
* Gate Pool
|
||||||
|
* Heap allocation
|
||||||
|
* RC counters + safe point reclaim
|
||||||
|
* Traps at runtime
|
||||||
Loading…
x
Reference in New Issue
Block a user