From b29ca1b1703e86ed0d4bb07a34eabdbe945ef129 Mon Sep 17 00:00:00 2001 From: Nilton Constantino Date: Thu, 29 Jan 2026 18:01:00 +0000 Subject: [PATCH] pr 25 --- .../src/frontends/pbs/mod.rs | 5 + .../src/frontends/pbs/typecheck.rs | 16 + crates/prometeu-compiler/src/ir_core/instr.rs | 4 + crates/prometeu-compiler/src/ir_core/mod.rs | 2 + .../prometeu-compiler/src/ir_core/validate.rs | 352 ++++++++++++++++++ .../src/lowering/core_to_vm.rs | 2 + docs/specs/pbs/files/PRs para Junie.md | 27 -- 7 files changed, 381 insertions(+), 27 deletions(-) create mode 100644 crates/prometeu-compiler/src/ir_core/validate.rs diff --git a/crates/prometeu-compiler/src/frontends/pbs/mod.rs b/crates/prometeu-compiler/src/frontends/pbs/mod.rs index a49d35cf..6f38af2b 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/mod.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/mod.rs @@ -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) diff --git a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs index 9d48ddf7..590ea253 100644 --- a/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs +++ b/crates/prometeu-compiler/src/frontends/pbs/typecheck.rs @@ -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")); + } } diff --git a/crates/prometeu-compiler/src/ir_core/instr.rs b/crates/prometeu-compiler/src/ir_core/instr.rs index 8e2713da..a95ead55 100644 --- a/crates/prometeu-compiler/src/ir_core/instr.rs +++ b/crates/prometeu-compiler/src/ir_core/instr.rs @@ -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, } diff --git a/crates/prometeu-compiler/src/ir_core/mod.rs b/crates/prometeu-compiler/src/ir_core/mod.rs index e95fa084..2eec4c04 100644 --- a/crates/prometeu-compiler/src/ir_core/mod.rs +++ b/crates/prometeu-compiler/src/ir_core/mod.rs @@ -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 { diff --git a/crates/prometeu-compiler/src/ir_core/validate.rs b/crates/prometeu-compiler/src/ir_core/validate.rs new file mode 100644 index 00000000..1100e21f --- /dev/null +++ b/crates/prometeu-compiler/src/ir_core/validate.rs @@ -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> = HashMap::new(); + let mut worklist: VecDeque = 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 = func.blocks.iter().map(|b| (b.id, b)).collect(); + let mut visited_with_stack: HashMap> = 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>, + worklist: &mut VecDeque, + target: u32, + stack: &Vec +) -> 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) -> 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()); + } +} diff --git a/crates/prometeu-compiler/src/lowering/core_to_vm.rs b/crates/prometeu-compiler/src/lowering/core_to_vm.rs index e4d0730a..0b9f4c93 100644 --- a/crates/prometeu-compiler/src/lowering/core_to_vm.rs +++ b/crates/prometeu-compiler/src/lowering/core_to_vm.rs @@ -78,6 +78,8 @@ pub fn lower_function(core_func: &ir_core::Function) -> Result 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)); diff --git a/docs/specs/pbs/files/PRs para Junie.md b/docs/specs/pbs/files/PRs para Junie.md index 7d93e547..ad872d9d 100644 --- a/docs/specs/pbs/files/PRs para Junie.md +++ b/docs/specs/pbs/files/PRs para Junie.md @@ -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.