//! Phase 201: JoinValueSpace - Single source of truth for JoinIR ValueId allocation //! //! This module provides a unified ValueId allocator for JoinIR lowering to prevent //! collisions between different allocation contexts (param vs local vs PHI). //! //! ## Problem Solved //! //! Before Phase 201, Pattern 2 frontend used `alloc_join_value()` for env variables, //! while JoinIR lowering used a separate `alloc_value()` starting from 0. Both could //! produce the same ValueId for different purposes, causing PHI corruption after remapping. //! //! ## ValueId Space Layout //! //! ```text //! 0 100 1000 u32::MAX //! ├──────────┼──────────┼──────────────────────────┤ //! │ PHI │ Param │ Local │ //! │ Reserved│ Region │ Region │ //! └──────────┴──────────┴──────────────────────────┘ //! ``` //! //! - **PHI Reserved (0-99)**: Pre-reserved for LoopHeader PHI dst //! - **Param Region (100-999)**: For ConditionEnv, CarrierInfo.join_id, CapturedEnv //! - **Local Region (1000+)**: For Const, BinOp, etc. in pattern lowerers //! //! ## Usage //! //! ```ignore //! let mut space = JoinValueSpace::new(); //! //! // Pattern frontend allocates param IDs //! let i_param = space.alloc_param(); // ValueId(100) //! let v_param = space.alloc_param(); // ValueId(101) //! //! // PHI builder reserves PHI dst //! space.reserve_phi(ValueId(0)); // Mark as reserved //! //! // JoinIR lowerer allocates local IDs //! let const_100 = space.alloc_local(); // ValueId(1000) //! let binop_result = space.alloc_local(); // ValueId(1001) //! //! // No collision possible! //! ``` use crate::mir::ValueId; use std::collections::HashSet; /// Region boundaries (can be tuned based on actual usage) /// Phase 205: Explicit min/max constants for each region pub const PHI_RESERVED_MIN: u32 = 0; pub const PHI_RESERVED_MAX: u32 = 99; pub const PARAM_MIN: u32 = 100; pub const PARAM_MAX: u32 = 999; pub const LOCAL_MIN: u32 = 1000; pub const LOCAL_MAX: u32 = 100000; // Legacy aliases for backward compatibility const PHI_MAX: u32 = PHI_RESERVED_MAX; const PARAM_BASE: u32 = PARAM_MIN; const LOCAL_BASE: u32 = LOCAL_MIN; /// Single source of truth for JoinIR ValueId allocation /// /// All JoinIR ValueId allocation should go through this box to ensure /// disjoint regions for Param, Local, and PHI dst IDs. #[derive(Debug, Clone)] pub struct JoinValueSpace { /// Next available param ID (starts at PARAM_BASE) next_param: u32, /// Next available local ID (starts at LOCAL_BASE) next_local: u32, /// Reserved PHI dst IDs (debug verification only) reserved_phi: HashSet, /// Phase 205: Track all allocated IDs for collision detection (debug-only) #[cfg(debug_assertions)] allocated_ids: HashSet, } /// Region classification for ValueIds #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Region { /// PHI Reserved region (0-99) PhiReserved, /// Param region (100-999) Param, /// Local region (1000+) Local, /// Unknown/invalid region Unknown, } impl JoinValueSpace { /// Create a new JoinValueSpace with default regions pub fn new() -> Self { Self { next_param: PARAM_BASE, next_local: LOCAL_BASE, reserved_phi: HashSet::new(), #[cfg(debug_assertions)] allocated_ids: HashSet::new(), } } /// Phase 205: Check for ValueId collision (debug-only) /// /// Panics if the given ValueId has already been allocated. /// This is a fail-fast mechanism to detect bugs in JoinIR lowering. #[cfg(debug_assertions)] fn check_collision(&self, id: ValueId, role: &str) { if self.allocated_ids.contains(&id.0) { panic!( "[JoinValueSpace] ValueId collision detected!\n\ ID: {:?}\n\ Role: {}\n\ This indicates a bug in JoinIR lowering - contact maintainer", id, role ); } } /// Allocate a parameter ValueId (for ConditionEnv, CarrierInfo, etc.) /// /// Returns ValueId in Param Region (100-999). /// Panics in debug mode if param region overflows. pub fn alloc_param(&mut self) -> ValueId { let id = self.next_param; debug_assert!( id < LOCAL_BASE, "Param region overflow: {} >= {}", id, LOCAL_BASE ); // Phase 205: Collision detection (debug-only) #[cfg(debug_assertions)] self.check_collision(ValueId(id), "param"); #[cfg(debug_assertions)] self.allocated_ids.insert(id); self.next_param += 1; ValueId(id) } /// Allocate a local ValueId (for Const, BinOp, etc. in lowerers) /// /// Returns ValueId in Local Region (1000+). pub fn alloc_local(&mut self) -> ValueId { let id = self.next_local; // Phase 205: Collision detection (debug-only) #[cfg(debug_assertions)] self.check_collision(ValueId(id), "local"); #[cfg(debug_assertions)] self.allocated_ids.insert(id); self.next_local += 1; ValueId(id) } /// Reserve a PHI dst ValueId (called by PHI builder before allocation) /// /// No allocation - just marks the ID as reserved for PHI use. /// This is for debug verification only; the actual PHI dst comes from /// MirBuilder (host side), not JoinValueSpace. pub fn reserve_phi(&mut self, id: ValueId) { debug_assert!( id.0 <= PHI_MAX, "PHI reservation out of range: {} > {}", id.0, PHI_MAX ); self.reserved_phi.insert(id.0); } /// Check if a ValueId is reserved as PHI dst pub fn is_phi_reserved(&self, id: ValueId) -> bool { self.reserved_phi.contains(&id.0) } /// Determine which region a ValueId belongs to pub fn region_of(&self, id: ValueId) -> Region { if id.0 <= PHI_MAX { Region::PhiReserved } else if id.0 < LOCAL_BASE { Region::Param } else { Region::Local } } /// Phase 205: Verify that a ValueId is in the expected region (debug-only) /// /// Returns Ok(()) if the ValueId is in the expected region. /// Returns Err(message) if the region doesn't match. /// /// This is a fail-fast verification mechanism for debugging. #[cfg(debug_assertions)] pub fn verify_region(&self, id: ValueId, expected_region: Region) -> Result<(), String> { let actual = self.region_of(id); if actual != expected_region { return Err(format!( "ValueId {:?} is in {:?} region, expected {:?}\n\ Hint: Use alloc_param() for loop arguments, alloc_local() for JoinIR values", id, actual, expected_region )); } Ok(()) } /// Get the current param counter (for debugging) pub fn param_count(&self) -> u32 { self.next_param - PARAM_BASE } /// Get the current local counter (for debugging) pub fn local_count(&self) -> u32 { self.next_local - LOCAL_BASE } /// Get the number of reserved PHI IDs (for debugging) pub fn phi_reserved_count(&self) -> usize { self.reserved_phi.len() } /// Verify no overlap between regions (debug assertion) /// /// This checks that: /// 1. Param region hasn't overflowed into Local region /// 2. Reserved PHI IDs are within PHI region /// /// Returns Ok(()) if valid, Err(message) if invalid. #[cfg(debug_assertions)] pub fn verify_no_overlap(&self) -> Result<(), String> { // Check param region hasn't overflowed if self.next_param >= LOCAL_BASE { return Err(format!( "Param region overflow: next_param={} >= LOCAL_BASE={}", self.next_param, LOCAL_BASE )); } // Check all reserved PHI IDs are in PHI region for &phi_id in &self.reserved_phi { if phi_id > PHI_MAX { return Err(format!( "PHI ID {} is out of PHI region (max={})", phi_id, PHI_MAX )); } } Ok(()) } /// Create an allocator closure for local IDs /// /// This is a convenience method to create a closure compatible with /// existing lowerer signatures that expect `FnMut() -> ValueId`. /// /// # Example /// /// ```ignore /// let mut space = JoinValueSpace::new(); /// let mut alloc_local = space.local_allocator(); /// let id1 = alloc_local(); // ValueId(1000) /// let id2 = alloc_local(); // ValueId(1001) /// ``` pub fn local_allocator(&mut self) -> impl FnMut() -> ValueId + '_ { move || self.alloc_local() } /// Create an allocator closure for param IDs /// /// Similar to local_allocator(), but for param region. pub fn param_allocator(&mut self) -> impl FnMut() -> ValueId + '_ { move || self.alloc_param() } } impl Default for JoinValueSpace { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_new_space_has_correct_initial_values() { let space = JoinValueSpace::new(); assert_eq!(space.next_param, PARAM_BASE); assert_eq!(space.next_local, LOCAL_BASE); assert!(space.reserved_phi.is_empty()); } #[test] fn test_alloc_param_returns_correct_ids() { let mut space = JoinValueSpace::new(); let id1 = space.alloc_param(); let id2 = space.alloc_param(); let id3 = space.alloc_param(); assert_eq!(id1, ValueId(100)); assert_eq!(id2, ValueId(101)); assert_eq!(id3, ValueId(102)); } #[test] fn test_alloc_local_returns_correct_ids() { let mut space = JoinValueSpace::new(); let id1 = space.alloc_local(); let id2 = space.alloc_local(); let id3 = space.alloc_local(); assert_eq!(id1, ValueId(1000)); assert_eq!(id2, ValueId(1001)); assert_eq!(id3, ValueId(1002)); } #[test] fn test_param_and_local_do_not_overlap() { let mut space = JoinValueSpace::new(); // Allocate many params for _ in 0..100 { space.alloc_param(); } // Allocate many locals for _ in 0..100 { space.alloc_local(); } // Param should be in range [100, 200) assert_eq!(space.next_param, 200); // Local should be in range [1000, 1100) assert_eq!(space.next_local, 1100); // No overlap possible assert!(space.next_param < LOCAL_BASE); } #[test] fn test_reserve_phi() { let mut space = JoinValueSpace::new(); space.reserve_phi(ValueId(0)); space.reserve_phi(ValueId(5)); space.reserve_phi(ValueId(10)); assert!(space.is_phi_reserved(ValueId(0))); assert!(space.is_phi_reserved(ValueId(5))); assert!(space.is_phi_reserved(ValueId(10))); assert!(!space.is_phi_reserved(ValueId(1))); assert!(!space.is_phi_reserved(ValueId(100))); } #[test] fn test_region_of() { let space = JoinValueSpace::new(); // PHI region assert_eq!(space.region_of(ValueId(0)), Region::PhiReserved); assert_eq!(space.region_of(ValueId(50)), Region::PhiReserved); assert_eq!(space.region_of(ValueId(99)), Region::PhiReserved); // Param region assert_eq!(space.region_of(ValueId(100)), Region::Param); assert_eq!(space.region_of(ValueId(500)), Region::Param); assert_eq!(space.region_of(ValueId(999)), Region::Param); // Local region assert_eq!(space.region_of(ValueId(1000)), Region::Local); assert_eq!(space.region_of(ValueId(5000)), Region::Local); assert_eq!(space.region_of(ValueId(u32::MAX)), Region::Local); } #[test] fn test_counters() { let mut space = JoinValueSpace::new(); assert_eq!(space.param_count(), 0); assert_eq!(space.local_count(), 0); assert_eq!(space.phi_reserved_count(), 0); space.alloc_param(); space.alloc_param(); space.alloc_local(); space.reserve_phi(ValueId(0)); assert_eq!(space.param_count(), 2); assert_eq!(space.local_count(), 1); assert_eq!(space.phi_reserved_count(), 1); } #[cfg(debug_assertions)] #[test] fn test_verify_no_overlap_success() { let mut space = JoinValueSpace::new(); space.alloc_param(); space.alloc_local(); space.reserve_phi(ValueId(0)); assert!(space.verify_no_overlap().is_ok()); } #[test] fn test_local_allocator_closure() { let mut space = JoinValueSpace::new(); let id1; let id2; { let mut alloc = space.local_allocator(); id1 = alloc(); id2 = alloc(); } assert_eq!(id1, ValueId(1000)); assert_eq!(id2, ValueId(1001)); } #[test] fn test_param_allocator_closure() { let mut space = JoinValueSpace::new(); let id1; let id2; { let mut alloc = space.param_allocator(); id1 = alloc(); id2 = alloc(); } assert_eq!(id1, ValueId(100)); assert_eq!(id2, ValueId(101)); } /// Phase 201-A scenario: Verify that the bug case is impossible /// /// Previously: env['v'] = ValueId(7), const 100 dst = ValueId(7) -> collision /// Now: env['v'] = alloc_param() -> ValueId(100+), const 100 = alloc_local() -> ValueId(1000+) #[test] fn test_phase201a_scenario_no_collision() { let mut space = JoinValueSpace::new(); // Pattern 2 frontend allocates param for carrier 'v' let v_param = space.alloc_param(); // ValueId(100) // JoinIR lowering allocates local for const 100 let const_100 = space.alloc_local(); // ValueId(1000) // They are in different regions - no collision! assert_ne!(v_param, const_100); assert_eq!(space.region_of(v_param), Region::Param); assert_eq!(space.region_of(const_100), Region::Local); } }