Extended PatternPipelineContext and CarrierUpdateInfo for Pattern 3 AST-based generalization. Changes: 1. PatternPipelineContext: - Added loop_condition: Option<ASTNode> - Added loop_body: Option<Vec<ASTNode>> - Added loop_update_summary: Option<LoopUpdateSummary> - Updated build_pattern_context() for Pattern 3 2. CarrierUpdateInfo: - Added then_expr: Option<ASTNode> - Added else_expr: Option<ASTNode> - Updated analyze_loop_updates() with None defaults Status: Phase 213-2 Steps 2-2 & 2-3 complete Next: Create Pattern3IfAnalyzer to extract if statement and populate update summary
460 lines
14 KiB
Rust
460 lines
14 KiB
Rust
//! 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<u32>,
|
|
/// Phase 205: Track all allocated IDs for collision detection (debug-only)
|
|
#[cfg(debug_assertions)]
|
|
allocated_ids: 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(),
|
|
#[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);
|
|
}
|
|
}
|