This commit is contained in:
Nilton Constantino 2026-01-29 18:01:00 +00:00
parent f56353ce9b
commit b29ca1b170
No known key found for this signature in database
7 changed files with 381 additions and 27 deletions

View File

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

View File

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

View File

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

View File

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

View 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 == &current_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, &current_stack)?;
}
Terminator::JumpIfFalse { target, else_target } => {
propagate_stack(&mut block_entry_stacks, &mut worklist, *target, &current_stack)?;
propagate_stack(&mut block_entry_stacks, &mut worklist, *else_target, &current_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());
}
}

View File

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

View File

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