feat(joinir): Phase 201 JoinValueSpace - unified ValueId allocation
Phase 201 introduces JoinValueSpace to prevent ValueId collisions between Pattern 2 frontend (alloc_join_value) and JoinIR lowering (alloc_value). ValueId Space Layout: - PHI Reserved (0-99): For LoopHeader PHI dst - Param Region (100-999): For ConditionEnv, CarrierInfo, CapturedEnv - Local Region (1000+): For Const, BinOp, etc. in pattern lowerers Changes: - Add join_value_space.rs with JoinValueSpace struct (10 tests) - Add ConditionEnvBuilder v2 API using JoinValueSpace - Wire Pattern 2 frontend to use JoinValueSpace for param allocation Note: E2E tests fail until Task 201-5 wires lowerers to alloc_local() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
393
src/mir/join_ir/lowering/join_value_space.rs
Normal file
393
src/mir/join_ir/lowering/join_value_space.rs
Normal file
@ -0,0 +1,393 @@
|
||||
//! 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)
|
||||
const PHI_MAX: u32 = 99; // PHI dst range: 0-99
|
||||
const PARAM_BASE: u32 = 100; // Param range: 100-999
|
||||
const LOCAL_BASE: u32 = 1000; // Local range: 1000+
|
||||
|
||||
/// 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<u32>,
|
||||
}
|
||||
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
);
|
||||
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;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user