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:
@ -28,6 +28,7 @@ use crate::ast::ASTNode;
|
|||||||
use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv};
|
use crate::mir::join_ir::lowering::condition_env::{ConditionBinding, ConditionEnv};
|
||||||
use crate::mir::join_ir::lowering::condition_to_joinir::extract_condition_variables;
|
use crate::mir::join_ir::lowering::condition_to_joinir::extract_condition_variables;
|
||||||
use crate::mir::join_ir::lowering::inline_boundary_builder::JoinInlineBoundaryBuilder;
|
use crate::mir::join_ir::lowering::inline_boundary_builder::JoinInlineBoundaryBuilder;
|
||||||
|
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||||
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
|
||||||
use crate::mir::ValueId;
|
use crate::mir::ValueId;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
@ -125,6 +126,82 @@ impl ConditionEnvBuilder {
|
|||||||
env
|
env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 201: Build ConditionEnv using JoinValueSpace (disjoint ValueId regions)
|
||||||
|
///
|
||||||
|
/// This method uses JoinValueSpace to allocate ValueIds, ensuring that
|
||||||
|
/// param IDs (100+) never collide with local IDs (1000+) used by JoinIR lowering.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `break_condition` - AST node for the break condition
|
||||||
|
/// * `loop_var_name` - Loop parameter name (excluded from condition-only variables)
|
||||||
|
/// * `variable_map` - HOST function's variable_map (for looking up HOST ValueIds)
|
||||||
|
/// * `loop_var_id` - HOST ValueId for the loop parameter
|
||||||
|
/// * `space` - JoinValueSpace for unified ValueId allocation
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Tuple of:
|
||||||
|
/// - ConditionEnv: Variable name → JoinIR ValueId mapping
|
||||||
|
/// - Vec<ConditionBinding>: HOST↔JoinIR value mappings for merge
|
||||||
|
/// - loop_var_join_id: The JoinIR ValueId allocated for the loop parameter
|
||||||
|
pub fn build_for_break_condition_v2(
|
||||||
|
break_condition: &ASTNode,
|
||||||
|
loop_var_name: &str,
|
||||||
|
variable_map: &BTreeMap<String, ValueId>,
|
||||||
|
_loop_var_id: ValueId,
|
||||||
|
space: &mut JoinValueSpace,
|
||||||
|
) -> Result<(ConditionEnv, Vec<ConditionBinding>, ValueId), String> {
|
||||||
|
// Extract all variables used in the condition (excluding loop parameter)
|
||||||
|
let condition_var_names = extract_condition_variables(
|
||||||
|
break_condition,
|
||||||
|
&[loop_var_name.to_string()],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut env = ConditionEnv::new();
|
||||||
|
let mut bindings = Vec::new();
|
||||||
|
|
||||||
|
// Phase 201: Allocate loop parameter ValueId from JoinValueSpace (Param region)
|
||||||
|
let loop_var_join_id = space.alloc_param();
|
||||||
|
env.insert(loop_var_name.to_string(), loop_var_join_id);
|
||||||
|
|
||||||
|
// For each condition variable, allocate JoinIR-local ValueId and build binding
|
||||||
|
for var_name in &condition_var_names {
|
||||||
|
let host_id = variable_map
|
||||||
|
.get(var_name)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Condition variable '{}' not found in variable_map. \
|
||||||
|
Loop condition references undefined variable.",
|
||||||
|
var_name
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Phase 201: Allocate from Param region to avoid collision with locals
|
||||||
|
let join_id = space.alloc_param();
|
||||||
|
|
||||||
|
env.insert(var_name.clone(), join_id);
|
||||||
|
bindings.push(ConditionBinding {
|
||||||
|
name: var_name.clone(),
|
||||||
|
host_value: host_id,
|
||||||
|
join_value: join_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((env, bindings, loop_var_join_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Phase 201: Build ConditionEnv with loop parameter only using JoinValueSpace
|
||||||
|
///
|
||||||
|
/// Uses JoinValueSpace to allocate the loop parameter ValueId.
|
||||||
|
pub fn build_loop_param_only_v2(loop_var_name: &str, space: &mut JoinValueSpace) -> (ConditionEnv, ValueId) {
|
||||||
|
let mut env = ConditionEnv::new();
|
||||||
|
let loop_var_join_id = space.alloc_param();
|
||||||
|
env.insert(loop_var_name.to_string(), loop_var_join_id);
|
||||||
|
(env, loop_var_join_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Build ConditionEnv with optional captured variables (Phase 200-B implementation)
|
/// Build ConditionEnv with optional captured variables (Phase 200-B implementation)
|
||||||
///
|
///
|
||||||
/// # Phase 200-B Implementation
|
/// # Phase 200-B Implementation
|
||||||
@ -351,4 +428,65 @@ mod tests {
|
|||||||
.unwrap_err()
|
.unwrap_err()
|
||||||
.contains("undefined_var"));
|
.contains("undefined_var"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 201: Test that v2 API uses JoinValueSpace correctly
|
||||||
|
#[test]
|
||||||
|
fn test_build_for_break_condition_v2_uses_param_region() {
|
||||||
|
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||||
|
|
||||||
|
// Condition: i < max
|
||||||
|
let condition = ASTNode::BinaryOp {
|
||||||
|
operator: crate::ast::BinaryOperator::Less,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "i".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Variable {
|
||||||
|
name: "max".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut variable_map = BTreeMap::new();
|
||||||
|
variable_map.insert("i".to_string(), ValueId(100));
|
||||||
|
variable_map.insert("max".to_string(), ValueId(200));
|
||||||
|
|
||||||
|
let mut space = JoinValueSpace::new();
|
||||||
|
let (env, bindings, loop_var_join_id) = ConditionEnvBuilder::build_for_break_condition_v2(
|
||||||
|
&condition,
|
||||||
|
"i",
|
||||||
|
&variable_map,
|
||||||
|
ValueId(100),
|
||||||
|
&mut space,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Phase 201: Loop param should be in Param region (100+)
|
||||||
|
assert_eq!(loop_var_join_id, ValueId(100)); // First param allocation
|
||||||
|
assert_eq!(env.get("i"), Some(ValueId(100)));
|
||||||
|
|
||||||
|
// Phase 201: Condition variable should also be in Param region
|
||||||
|
assert_eq!(env.get("max"), Some(ValueId(101))); // Second param allocation
|
||||||
|
assert_eq!(bindings.len(), 1);
|
||||||
|
assert_eq!(bindings[0].join_value, ValueId(101));
|
||||||
|
|
||||||
|
// Phase 201: Verify no collision with Local region (1000+)
|
||||||
|
let local_id = space.alloc_local();
|
||||||
|
assert_eq!(local_id, ValueId(1000)); // Locals start at 1000
|
||||||
|
assert_ne!(local_id, loop_var_join_id);
|
||||||
|
assert_ne!(local_id, bindings[0].join_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_build_loop_param_only_v2() {
|
||||||
|
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||||
|
|
||||||
|
let mut space = JoinValueSpace::new();
|
||||||
|
let (env, loop_var_join_id) = ConditionEnvBuilder::build_loop_param_only_v2("i", &mut space);
|
||||||
|
|
||||||
|
// Phase 201: Should use Param region
|
||||||
|
assert_eq!(loop_var_join_id, ValueId(100));
|
||||||
|
assert_eq!(env.get("i"), Some(ValueId(100)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -144,6 +144,11 @@ impl MirBuilder {
|
|||||||
// Phase 195: Use unified trace
|
// Phase 195: Use unified trace
|
||||||
trace::trace().varmap("pattern2_start", &self.variable_map);
|
trace::trace().varmap("pattern2_start", &self.variable_map);
|
||||||
|
|
||||||
|
// Phase 201: Create JoinValueSpace for unified ValueId allocation
|
||||||
|
// This ensures Param IDs (100+) never collide with Local IDs (1000+)
|
||||||
|
use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace;
|
||||||
|
let mut join_value_space = JoinValueSpace::new();
|
||||||
|
|
||||||
// Phase 200-C: Integrate capture analysis
|
// Phase 200-C: Integrate capture analysis
|
||||||
use crate::mir::loop_pattern_detection::function_scope_capture::{analyze_captured_vars_v2, CapturedEnv};
|
use crate::mir::loop_pattern_detection::function_scope_capture::{analyze_captured_vars_v2, CapturedEnv};
|
||||||
use super::condition_env_builder::ConditionEnvBuilder;
|
use super::condition_env_builder::ConditionEnvBuilder;
|
||||||
@ -170,21 +175,24 @@ impl MirBuilder {
|
|||||||
var.name, var.host_id, var.is_immutable);
|
var.name, var.host_id, var.is_immutable);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 200-C: Use existing path and manually add captured variables
|
// Phase 201: Use v2 API with JoinValueSpace
|
||||||
// TODO Phase 200-D: Refactor to use build_with_captures with boundary builder
|
// This allocates loop param and condition vars from Param region (100+)
|
||||||
let (mut env, mut condition_bindings) = ConditionEnvBuilder::build_for_break_condition(
|
let (mut env, mut condition_bindings, _loop_var_join_id) = ConditionEnvBuilder::build_for_break_condition_v2(
|
||||||
condition,
|
condition,
|
||||||
&loop_var_name,
|
&loop_var_name,
|
||||||
&self.variable_map,
|
&self.variable_map,
|
||||||
loop_var_id,
|
loop_var_id,
|
||||||
|
&mut join_value_space,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Phase 200-C: Manually add captured variables to env for E2E testing
|
eprintln!("[pattern2/phase201] Using JoinValueSpace: loop_var '{}' → {:?}",
|
||||||
// This is a temporary approach until Phase 200-D refactors the boundary creation
|
loop_var_name, env.get(&loop_var_name));
|
||||||
|
|
||||||
|
// Phase 201: Add captured variables using JoinValueSpace
|
||||||
for var in &captured_env.vars {
|
for var in &captured_env.vars {
|
||||||
if let Some(&host_id) = self.variable_map.get(&var.name) {
|
if let Some(&host_id) = self.variable_map.get(&var.name) {
|
||||||
// Allocate a JoinIR ValueId for this captured variable
|
// Phase 201: Allocate from Param region to avoid collision with locals
|
||||||
let join_id = crate::mir::ValueId(env.len() as u32);
|
let join_id = join_value_space.alloc_param();
|
||||||
env.insert(var.name.clone(), join_id);
|
env.insert(var.name.clone(), join_id);
|
||||||
|
|
||||||
// Add to condition_bindings for boundary processing
|
// Add to condition_bindings for boundary processing
|
||||||
@ -194,46 +202,34 @@ impl MirBuilder {
|
|||||||
join_value: join_id,
|
join_value: join_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
eprintln!("[pattern2/capture] Manually added captured '{}' to env: host={:?}, join={:?}",
|
eprintln!("[pattern2/capture] Phase 201: Added captured '{}': host={:?}, join={:?}",
|
||||||
var.name, host_id, join_id);
|
var.name, host_id, join_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 190-impl-D: Calculate ValueId offset for body-local variables
|
// Phase 201: Allocate carrier ValueIds from Param region
|
||||||
// JoinIR main() params are: [ValueId(0), ValueId(1), ...] for (loop_var, carrier1, carrier2, ...)
|
// This ensures carriers are also in the safe Param region
|
||||||
// Body-local variables must start AFTER all carrier params to avoid collision.
|
for carrier in &carrier_info.carriers {
|
||||||
// At this point carrier_info.carriers contains all potential carriers (before filtering).
|
let _carrier_join_id = join_value_space.alloc_param();
|
||||||
// We reserve space for: env.len() (condition vars) + carrier_info.carriers.len() (carriers)
|
eprintln!("[pattern2/phase201] Allocated carrier '{}' param ID: {:?}",
|
||||||
let body_local_start_offset = (env.len() + carrier_info.carriers.len()) as u32;
|
carrier.name, _carrier_join_id);
|
||||||
|
}
|
||||||
// Create allocator for body-local variables (starts after reserved param space)
|
|
||||||
let mut body_local_counter = body_local_start_offset;
|
|
||||||
let mut alloc_body_local_value = || {
|
|
||||||
let id = crate::mir::ValueId(body_local_counter);
|
|
||||||
body_local_counter += 1;
|
|
||||||
id
|
|
||||||
};
|
|
||||||
|
|
||||||
// Phase 191: Create empty body-local environment
|
// Phase 191: Create empty body-local environment
|
||||||
// LoopBodyLocalInitLowerer will populate it during init lowering
|
// LoopBodyLocalInitLowerer will populate it during init lowering
|
||||||
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
|
||||||
let mut body_local_env = LoopBodyLocalEnv::new();
|
let mut body_local_env = LoopBodyLocalEnv::new();
|
||||||
|
|
||||||
eprintln!("[pattern2/body-local] Phase 191: Created empty body-local environment (offset={})",
|
eprintln!("[pattern2/body-local] Phase 201: Created empty body-local environment (param_count={})",
|
||||||
body_local_start_offset);
|
join_value_space.param_count());
|
||||||
|
|
||||||
// Create allocator for other JoinIR-local ValueIds (Trim pattern, etc.)
|
// Phase 201: Create alloc_join_value closure using JoinValueSpace
|
||||||
// Continues from where body_local_counter left off
|
// This ensures all param allocations go through the unified space
|
||||||
let mut join_value_counter = body_local_counter;
|
let mut alloc_join_value = || join_value_space.alloc_param();
|
||||||
let mut alloc_join_value = || {
|
|
||||||
let id = crate::mir::ValueId(join_value_counter);
|
|
||||||
join_value_counter += 1;
|
|
||||||
id
|
|
||||||
};
|
|
||||||
|
|
||||||
// Debug: Log condition bindings
|
// Debug: Log condition bindings
|
||||||
eprintln!("[cf_loop/pattern2] Phase 171-172: ConditionEnv contains {} variables:", env.len());
|
eprintln!("[cf_loop/pattern2] Phase 201: ConditionEnv contains {} variables:", env.len());
|
||||||
eprintln!(" Loop param '{}' → JoinIR ValueId(0)", loop_var_name);
|
eprintln!(" Loop param '{}' → JoinIR {:?}", loop_var_name, env.get(&loop_var_name));
|
||||||
if !condition_bindings.is_empty() {
|
if !condition_bindings.is_empty() {
|
||||||
eprintln!(" {} condition-only bindings:", condition_bindings.len());
|
eprintln!(" {} condition-only bindings:", condition_bindings.len());
|
||||||
for binding in &condition_bindings {
|
for binding in &condition_bindings {
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
use crate::mir::ValueId;
|
use crate::mir::ValueId;
|
||||||
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
|
use super::inline_boundary::{JoinInlineBoundary, LoopExitBinding};
|
||||||
use super::condition_to_joinir::ConditionBinding;
|
use super::condition_to_joinir::ConditionBinding;
|
||||||
|
use super::join_value_space::JoinValueSpace;
|
||||||
|
|
||||||
/// Role of a parameter in JoinIR lowering (Phase 200-A)
|
/// Role of a parameter in JoinIR lowering (Phase 200-A)
|
||||||
///
|
///
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,6 +47,7 @@ pub mod if_phi_spec; // Phase 61-2
|
|||||||
pub(crate) mod if_select; // Phase 33: Internal If/Select lowering
|
pub(crate) mod if_select; // Phase 33: Internal If/Select lowering
|
||||||
pub mod inline_boundary; // Phase 188-Impl-3: JoinIR→Host boundary
|
pub mod inline_boundary; // Phase 188-Impl-3: JoinIR→Host boundary
|
||||||
pub mod inline_boundary_builder; // Phase 200-2: Builder pattern for JoinInlineBoundary
|
pub mod inline_boundary_builder; // Phase 200-2: Builder pattern for JoinInlineBoundary
|
||||||
|
pub mod join_value_space; // Phase 201: Unified JoinIR ValueId allocation
|
||||||
pub(crate) mod loop_form_intake; // Internal loop form intake
|
pub(crate) mod loop_form_intake; // Internal loop form intake
|
||||||
pub(crate) mod loop_pattern_router; // Phase 33-12: Loop pattern routing (re-exported)
|
pub(crate) mod loop_pattern_router; // Phase 33-12: Loop pattern routing (re-exported)
|
||||||
pub(crate) mod loop_pattern_validator; // Phase 33-23: Loop structure validation
|
pub(crate) mod loop_pattern_validator; // Phase 33-23: Loop structure validation
|
||||||
|
|||||||
Reference in New Issue
Block a user