pr 25
This commit is contained in:
parent
f56353ce9b
commit
b29ca1b170
@ -65,6 +65,11 @@ impl Frontend for PbsFrontend {
|
||||
let module_name = entry.file_stem().unwrap().to_string_lossy();
|
||||
let core_program = lowerer.lower_file(&ast, &module_name)?;
|
||||
|
||||
// Validate Core IR Invariants
|
||||
crate::ir_core::validate_program(&core_program).map_err(|e| {
|
||||
DiagnosticBundle::error(format!("Core IR Invariant Violation: {}", e), None)
|
||||
})?;
|
||||
|
||||
// Lower Core IR to VM IR
|
||||
core_to_vm::lower_program(&core_program).map_err(|e| {
|
||||
DiagnosticBundle::error(format!("Lowering error: {}", e), None)
|
||||
|
||||
@ -803,4 +803,20 @@ mod tests {
|
||||
let res = check_code(code);
|
||||
assert!(res.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hip_invariant_violation_return() {
|
||||
let code = "
|
||||
fn test_hip(g: int) {
|
||||
mutate g as x {
|
||||
return;
|
||||
}
|
||||
}
|
||||
";
|
||||
let res = check_code(code);
|
||||
assert!(res.is_err());
|
||||
let err = res.unwrap_err();
|
||||
assert!(err.contains("Core IR Invariant Violation"));
|
||||
assert!(err.contains("non-empty HIP stack"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,5 +40,9 @@ pub enum Instr {
|
||||
EndPeek,
|
||||
EndBorrow,
|
||||
EndMutate,
|
||||
/// Reads from heap at reference + offset. Pops reference, pushes value.
|
||||
LoadRef(u32),
|
||||
/// Writes to heap at reference + offset. Pops reference and value.
|
||||
StoreRef(u32),
|
||||
Free,
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ pub mod function;
|
||||
pub mod block;
|
||||
pub mod instr;
|
||||
pub mod terminator;
|
||||
pub mod validate;
|
||||
|
||||
pub use ids::*;
|
||||
pub use const_pool::*;
|
||||
@ -17,6 +18,7 @@ pub use function::*;
|
||||
pub use block::*;
|
||||
pub use instr::*;
|
||||
pub use terminator::*;
|
||||
pub use validate::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
352
crates/prometeu-compiler/src/ir_core/validate.rs
Normal file
352
crates/prometeu-compiler/src/ir_core/validate.rs
Normal file
@ -0,0 +1,352 @@
|
||||
use super::ids::ValueId;
|
||||
use super::instr::Instr;
|
||||
use super::program::Program;
|
||||
use super::terminator::Terminator;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum HipOpKind {
|
||||
Peek,
|
||||
Borrow,
|
||||
Mutate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct HipOp {
|
||||
pub kind: HipOpKind,
|
||||
pub gate: ValueId,
|
||||
}
|
||||
|
||||
pub fn validate_program(program: &Program) -> Result<(), String> {
|
||||
for module in &program.modules {
|
||||
for func in &module.functions {
|
||||
validate_function(func)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_function(func: &super::function::Function) -> Result<(), String> {
|
||||
let mut block_entry_stacks: HashMap<u32, Vec<HipOp>> = HashMap::new();
|
||||
let mut worklist: VecDeque<u32> = VecDeque::new();
|
||||
|
||||
if func.blocks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Assume the first block is the entry block (usually ID 0)
|
||||
let entry_block_id = func.blocks[0].id;
|
||||
block_entry_stacks.insert(entry_block_id, Vec::new());
|
||||
worklist.push_back(entry_block_id);
|
||||
|
||||
let blocks_by_id: HashMap<u32, &super::block::Block> = func.blocks.iter().map(|b| (b.id, b)).collect();
|
||||
let mut visited_with_stack: HashMap<u32, Vec<HipOp>> = HashMap::new();
|
||||
|
||||
while let Some(block_id) = worklist.pop_front() {
|
||||
let block = blocks_by_id.get(&block_id).ok_or_else(|| format!("Invalid block ID: {}", block_id))?;
|
||||
let mut current_stack = block_entry_stacks.get(&block_id).unwrap().clone();
|
||||
|
||||
// If we've already visited this block with the same stack, skip it to avoid infinite loops
|
||||
if let Some(prev_stack) = visited_with_stack.get(&block_id) {
|
||||
if prev_stack == ¤t_stack {
|
||||
continue;
|
||||
} else {
|
||||
return Err(format!("Block {} reached with inconsistent HIP stacks: {:?} vs {:?}", block_id, prev_stack, current_stack));
|
||||
}
|
||||
}
|
||||
visited_with_stack.insert(block_id, current_stack.clone());
|
||||
|
||||
for instr in &block.instrs {
|
||||
match instr {
|
||||
Instr::BeginPeek { gate } => {
|
||||
current_stack.push(HipOp { kind: HipOpKind::Peek, gate: *gate });
|
||||
}
|
||||
Instr::BeginBorrow { gate } => {
|
||||
current_stack.push(HipOp { kind: HipOpKind::Borrow, gate: *gate });
|
||||
}
|
||||
Instr::BeginMutate { gate } => {
|
||||
current_stack.push(HipOp { kind: HipOpKind::Mutate, gate: *gate });
|
||||
}
|
||||
Instr::EndPeek => {
|
||||
match current_stack.pop() {
|
||||
Some(op) if op.kind == HipOpKind::Peek => {},
|
||||
Some(op) => return Err(format!("EndPeek doesn't match current HIP op: {:?}", op)),
|
||||
None => return Err("EndPeek without matching BeginPeek".to_string()),
|
||||
}
|
||||
}
|
||||
Instr::EndBorrow => {
|
||||
match current_stack.pop() {
|
||||
Some(op) if op.kind == HipOpKind::Borrow => {},
|
||||
Some(op) => return Err(format!("EndBorrow doesn't match current HIP op: {:?}", op)),
|
||||
None => return Err("EndBorrow without matching BeginBorrow".to_string()),
|
||||
}
|
||||
}
|
||||
Instr::EndMutate => {
|
||||
match current_stack.pop() {
|
||||
Some(op) if op.kind == HipOpKind::Mutate => {},
|
||||
Some(op) => return Err(format!("EndMutate doesn't match current HIP op: {:?}", op)),
|
||||
None => return Err("EndMutate without matching BeginMutate".to_string()),
|
||||
}
|
||||
}
|
||||
Instr::LoadRef(_) => {
|
||||
if current_stack.is_empty() {
|
||||
return Err("LoadRef outside of HIP operation".to_string());
|
||||
}
|
||||
}
|
||||
Instr::StoreRef(_) => {
|
||||
match current_stack.last() {
|
||||
Some(op) if op.kind == HipOpKind::Mutate => {},
|
||||
_ => return Err("StoreRef outside of BeginMutate".to_string()),
|
||||
}
|
||||
}
|
||||
Instr::Call(id, _) => {
|
||||
if id.0 == 0 {
|
||||
return Err("Call to FunctionId(0)".to_string());
|
||||
}
|
||||
}
|
||||
Instr::Alloc { ty, .. } => {
|
||||
if ty.0 == 0 {
|
||||
return Err("Alloc with TypeId(0)".to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match &block.terminator {
|
||||
Terminator::Return => {
|
||||
if !current_stack.is_empty() {
|
||||
return Err(format!("Function returns with non-empty HIP stack: {:?}", current_stack));
|
||||
}
|
||||
}
|
||||
Terminator::Jump(target) => {
|
||||
propagate_stack(&mut block_entry_stacks, &mut worklist, *target, ¤t_stack)?;
|
||||
}
|
||||
Terminator::JumpIfFalse { target, else_target } => {
|
||||
propagate_stack(&mut block_entry_stacks, &mut worklist, *target, ¤t_stack)?;
|
||||
propagate_stack(&mut block_entry_stacks, &mut worklist, *else_target, ¤t_stack)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn propagate_stack(
|
||||
entry_stacks: &mut HashMap<u32, Vec<HipOp>>,
|
||||
worklist: &mut VecDeque<u32>,
|
||||
target: u32,
|
||||
stack: &Vec<HipOp>
|
||||
) -> Result<(), String> {
|
||||
if let Some(existing) = entry_stacks.get(&target) {
|
||||
if existing != stack {
|
||||
return Err(format!("Control flow merge at block {} with inconsistent HIP stacks: {:?} vs {:?}", target, existing, stack));
|
||||
}
|
||||
} else {
|
||||
entry_stacks.insert(target, stack.clone());
|
||||
worklist.push_back(target);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ir_core::*;
|
||||
|
||||
fn create_dummy_function(blocks: Vec<Block>) -> Function {
|
||||
Function {
|
||||
id: FunctionId(1),
|
||||
name: "test".to_string(),
|
||||
params: vec![],
|
||||
return_type: Type::Void,
|
||||
blocks,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_dummy_program(func: Function) -> Program {
|
||||
Program {
|
||||
const_pool: ConstPool::new(),
|
||||
modules: vec![Module {
|
||||
name: "test".to_string(),
|
||||
functions: vec![func],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_hip_nesting() {
|
||||
let block = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::BeginPeek { gate: ValueId(0) },
|
||||
Instr::LoadRef(0),
|
||||
Instr::BeginMutate { gate: ValueId(1) },
|
||||
Instr::StoreRef(0),
|
||||
Instr::EndMutate,
|
||||
Instr::EndPeek,
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||
assert!(validate_program(&prog).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_hip_unbalanced() {
|
||||
let block = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::BeginPeek { gate: ValueId(0) },
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||
let res = validate_program(&prog);
|
||||
assert!(res.is_err());
|
||||
assert!(res.unwrap_err().contains("non-empty HIP stack"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_hip_wrong_end() {
|
||||
let block = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::BeginPeek { gate: ValueId(0) },
|
||||
Instr::EndMutate,
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||
let res = validate_program(&prog);
|
||||
assert!(res.is_err());
|
||||
assert!(res.unwrap_err().contains("EndMutate doesn't match"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_store_outside_mutate() {
|
||||
let block = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::BeginBorrow { gate: ValueId(0) },
|
||||
Instr::StoreRef(0),
|
||||
Instr::EndBorrow,
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||
let res = validate_program(&prog);
|
||||
assert!(res.is_err());
|
||||
assert!(res.unwrap_err().contains("StoreRef outside of BeginMutate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_store_in_mutate() {
|
||||
let block = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::BeginMutate { gate: ValueId(0) },
|
||||
Instr::StoreRef(0),
|
||||
Instr::EndMutate,
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||
assert!(validate_program(&prog).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_load_outside_hip() {
|
||||
let block = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::LoadRef(0),
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block]));
|
||||
let res = validate_program(&prog);
|
||||
assert!(res.is_err());
|
||||
assert!(res.unwrap_err().contains("LoadRef outside of HIP operation"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_hip_across_blocks() {
|
||||
let block0 = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::BeginPeek { gate: ValueId(0) },
|
||||
],
|
||||
terminator: Terminator::Jump(1),
|
||||
};
|
||||
let block1 = Block {
|
||||
id: 1,
|
||||
instrs: vec![
|
||||
Instr::LoadRef(0),
|
||||
Instr::EndPeek,
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block0, block1]));
|
||||
assert!(validate_program(&prog).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_hip_across_blocks_inconsistent() {
|
||||
let block0 = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::PushConst(ConstId(0)), // cond
|
||||
],
|
||||
terminator: Terminator::JumpIfFalse { target: 2, else_target: 1 },
|
||||
};
|
||||
let block1 = Block {
|
||||
id: 1,
|
||||
instrs: vec![
|
||||
Instr::BeginPeek { gate: ValueId(0) },
|
||||
],
|
||||
terminator: Terminator::Jump(3),
|
||||
};
|
||||
let block2 = Block {
|
||||
id: 2,
|
||||
instrs: vec![
|
||||
// No BeginPeek here
|
||||
],
|
||||
terminator: Terminator::Jump(3),
|
||||
};
|
||||
let block3 = Block {
|
||||
id: 3,
|
||||
instrs: vec![
|
||||
Instr::EndPeek, // ERROR: block 2 reaches here with empty stack
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog = create_dummy_program(create_dummy_function(vec![block0, block1, block2, block3]));
|
||||
let res = validate_program(&prog);
|
||||
assert!(res.is_err());
|
||||
assert!(res.unwrap_err().contains("Control flow merge at block 3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_silent_fallback_checks() {
|
||||
let block_func0 = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::Call(FunctionId(0), 0),
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog_func0 = create_dummy_program(create_dummy_function(vec![block_func0]));
|
||||
assert!(validate_program(&prog_func0).is_err());
|
||||
|
||||
let block_ty0 = Block {
|
||||
id: 0,
|
||||
instrs: vec![
|
||||
Instr::Alloc { ty: TypeId(0), slots: 1 },
|
||||
],
|
||||
terminator: Terminator::Return,
|
||||
};
|
||||
let prog_ty0 = create_dummy_program(create_dummy_function(vec![block_ty0]));
|
||||
assert!(validate_program(&prog_ty0).is_err());
|
||||
}
|
||||
}
|
||||
@ -78,6 +78,8 @@ pub fn lower_function(core_func: &ir_core::Function) -> Result<ir_vm::Function>
|
||||
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::Free => ir_vm::InstrKind::Nop,
|
||||
};
|
||||
vm_func.body.push(ir_vm::Instruction::new(kind, None));
|
||||
|
||||
@ -1,30 +1,3 @@
|
||||
# PR-25 — Core IR Invariants Test Suite
|
||||
|
||||
### Goal
|
||||
|
||||
Lock in correct semantics before touching the VM.
|
||||
|
||||
### Required Invariants
|
||||
|
||||
* Every `Begin*` has a matching `End*`
|
||||
* Gate passed to `Begin*` is available at `End*`
|
||||
* No storage writes without `BeginMutate`
|
||||
* No silent fallbacks
|
||||
|
||||
### Tests
|
||||
|
||||
* Property-style tests or golden IR assertions
|
||||
|
||||
---
|
||||
|
||||
## STOP POINT
|
||||
|
||||
After PR-25:
|
||||
|
||||
* Core IR correctly represents PBS HIP semantics
|
||||
* Lowering is deterministic and safe
|
||||
* VM is still unchanged
|
||||
|
||||
**Only after this point may VM PRs begin.**
|
||||
|
||||
Any VM work before this is a hard rejection.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user