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 module_name = entry.file_stem().unwrap().to_string_lossy();
|
||||||
let core_program = lowerer.lower_file(&ast, &module_name)?;
|
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
|
// Lower Core IR to VM IR
|
||||||
core_to_vm::lower_program(&core_program).map_err(|e| {
|
core_to_vm::lower_program(&core_program).map_err(|e| {
|
||||||
DiagnosticBundle::error(format!("Lowering error: {}", e), None)
|
DiagnosticBundle::error(format!("Lowering error: {}", e), None)
|
||||||
|
|||||||
@ -803,4 +803,20 @@ mod tests {
|
|||||||
let res = check_code(code);
|
let res = check_code(code);
|
||||||
assert!(res.is_ok());
|
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,
|
EndPeek,
|
||||||
EndBorrow,
|
EndBorrow,
|
||||||
EndMutate,
|
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,
|
Free,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ pub mod function;
|
|||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod instr;
|
pub mod instr;
|
||||||
pub mod terminator;
|
pub mod terminator;
|
||||||
|
pub mod validate;
|
||||||
|
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use const_pool::*;
|
pub use const_pool::*;
|
||||||
@ -17,6 +18,7 @@ pub use function::*;
|
|||||||
pub use block::*;
|
pub use block::*;
|
||||||
pub use instr::*;
|
pub use instr::*;
|
||||||
pub use terminator::*;
|
pub use terminator::*;
|
||||||
|
pub use validate::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::EndPeek |
|
||||||
ir_core::Instr::EndBorrow |
|
ir_core::Instr::EndBorrow |
|
||||||
ir_core::Instr::EndMutate => ir_vm::InstrKind::Nop,
|
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,
|
ir_core::Instr::Free => ir_vm::InstrKind::Nop,
|
||||||
};
|
};
|
||||||
vm_func.body.push(ir_vm::Instruction::new(kind, None));
|
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.**
|
**Only after this point may VM PRs begin.**
|
||||||
|
|
||||||
Any VM work before this is a hard rejection.
|
Any VM work before this is a hard rejection.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user