This commit is contained in:
Nilton Constantino 2026-01-30 15:15:38 +00:00
parent 25482e865f
commit a534b226fb
No known key found for this signature in database
9 changed files with 392 additions and 93 deletions

View File

@ -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());

View File

@ -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,
} }

View File

@ -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);
} }

View File

@ -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>,
} }

View File

@ -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,

View File

@ -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");

View File

@ -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}"));
}
} }

View File

@ -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.
---

View File

@ -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