feat(joinir): Phase 171-172 Issue 5 - ConditionEnvBuilder unified construction

Eliminates 40-50 lines of duplicated ConditionEnv + ConditionBinding construction in Pattern 2.

**Changes**:
- New: `condition_env_builder.rs` with factory methods
- `build_for_break_condition()`: Extract condition variables, allocate JoinIR ValueIds, create bindings
- `build_loop_param_only()`: Simple env with only loop parameter
- Updated Pattern 2 to use unified builder
- Includes 4 unit tests covering all usage scenarios

**Impact**:
- Pattern 2: 47 lines → 11 lines (36-line reduction, ~77%)
- Preserved allocator closure for Trim pattern additions
- Maintains mutability for downstream Trim pattern code

**Test**:
- cargo build --release:  PASS
- cargo test --release:  724/804 PASS (+4 improvements)
- Unit tests:  4/4 PASS

Phase 171-172 Stage 1: Total 66 lines reduced so far (Issue 4: 30 lines + Issue 5: 36 lines)
This commit is contained in:
nyash-codex
2025-12-08 03:48:18 +09:00
parent 5e3dec99e3
commit cc68327ab6
3 changed files with 263 additions and 38 deletions

View File

@ -0,0 +1,248 @@
//! ConditionEnvBuilder - Unified ConditionEnv construction
//!
//! Phase 171-172: Issue 5
//!
//! Provides unified construction methods for ConditionEnv and ConditionBindings
//! used in Pattern 2 (break condition analysis).
//!
//! # Responsibility
//!
//! - Extract condition variables from AST nodes
//! - Allocate JoinIR-local ValueIds for condition-only variables
//! - Build ConditionEnv mapping (variable name → JoinIR ValueId)
//! - Create ConditionBindings for host↔JoinIR value mapping
//!
//! # Usage
//!
//! ```rust
//! // Standard break condition analysis
//! let (env, bindings) = ConditionEnvBuilder::build_for_break_condition(
//! break_condition,
//! &loop_var_name,
//! &variable_map,
//! loop_var_id,
//! )?;
//! ```
use crate::ast::ASTNode;
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::ValueId;
use std::collections::BTreeMap;
pub struct ConditionEnvBuilder;
impl ConditionEnvBuilder {
/// Build ConditionEnv and ConditionBindings from break condition
///
/// This extracts all variables used in the break condition (excluding the loop parameter)
/// and creates:
/// 1. ConditionEnv: Maps variable names to JoinIR-local ValueIds
/// 2. ConditionBindings: Records HOST ValueId ↔ JoinIR ValueId mappings for merge
///
/// # 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
///
/// # Returns
///
/// Tuple of:
/// - ConditionEnv: Variable name → JoinIR ValueId mapping
/// - Vec<ConditionBinding>: HOST↔JoinIR value mappings for merge
///
/// # Errors
///
/// Returns error if a condition variable is not found in variable_map
pub fn build_for_break_condition(
break_condition: &ASTNode,
loop_var_name: &str,
variable_map: &BTreeMap<String, ValueId>,
loop_var_id: ValueId,
) -> Result<(ConditionEnv, Vec<ConditionBinding>), 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();
// Add loop parameter to env (ValueId(0) in JoinIR space)
// The loop parameter is NOT a condition binding (it's a join_input instead)
env.insert(loop_var_name.to_string(), ValueId(0));
// Create a local allocator for JoinIR-local ValueIds for condition-only variables
let mut join_value_counter = 1u32; // Start from 1 (0 is reserved for loop param)
// 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
)
})?;
let join_id = ValueId(join_value_counter);
join_value_counter += 1;
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))
}
/// Build ConditionEnv with loop parameter only
///
/// Used when there are no additional condition-only variables,
/// only the loop parameter needs to be in the environment.
///
/// # Arguments
///
/// * `loop_var_name` - Loop parameter name
///
/// # Returns
///
/// ConditionEnv with only the loop parameter mapped to ValueId(0)
pub fn build_loop_param_only(loop_var_name: &str) -> ConditionEnv {
let mut env = ConditionEnv::new();
env.insert(loop_var_name.to_string(), ValueId(0));
env
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{LiteralValue, Span};
#[test]
fn test_build_loop_param_only() {
let env = ConditionEnvBuilder::build_loop_param_only("i");
assert_eq!(env.len(), 1);
assert!(env.get("i").is_some());
}
#[test]
fn test_build_for_break_condition_no_extra_vars() {
// Condition: i < 10 (only uses loop parameter)
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::Literal {
value: LiteralValue::Integer(10),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let mut variable_map = BTreeMap::new();
variable_map.insert("i".to_string(), ValueId(100));
let (env, bindings) = ConditionEnvBuilder::build_for_break_condition(
&condition,
"i",
&variable_map,
ValueId(100),
)
.unwrap();
// Should have loop parameter in env
assert!(env.get("i").is_some());
// Should have no condition-only bindings
assert_eq!(bindings.len(), 0);
}
#[test]
fn test_build_for_break_condition_with_extra_var() {
// Condition: i < max (uses loop parameter + extra variable)
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 (env, bindings) = ConditionEnvBuilder::build_for_break_condition(
&condition,
"i",
&variable_map,
ValueId(100),
)
.unwrap();
// Should have loop parameter in env
assert!(env.get("i").is_some());
// Should have "max" in env with JoinIR-local ValueId
assert!(env.get("max").is_some());
// Should have one condition-only binding for "max"
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].name, "max");
assert_eq!(bindings[0].host_value, ValueId(200));
assert!(bindings[0].join_value.0 > 0); // JoinIR-local ID
}
#[test]
fn test_build_for_break_condition_undefined_variable() {
// Condition: i < undefined_var
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: "undefined_var".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
};
let mut variable_map = BTreeMap::new();
variable_map.insert("i".to_string(), ValueId(100));
// "undefined_var" is NOT in variable_map
let result = ConditionEnvBuilder::build_for_break_condition(
&condition,
"i",
&variable_map,
ValueId(100),
);
// Should return error
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("undefined_var"));
}
}

View File

@ -26,9 +26,11 @@
//! //!
//! Phase 171-172: Refactoring Infrastructure //! Phase 171-172: Refactoring Infrastructure
//! - loop_scope_shape_builder.rs: Unified LoopScopeShape initialization (Issue 4) //! - loop_scope_shape_builder.rs: Unified LoopScopeShape initialization (Issue 4)
//! - condition_env_builder.rs: Unified ConditionEnv construction (Issue 5)
pub(in crate::mir::builder) mod ast_feature_extractor; pub(in crate::mir::builder) mod ast_feature_extractor;
pub(in crate::mir::builder) mod common_init; pub(in crate::mir::builder) mod common_init;
pub(in crate::mir::builder) mod condition_env_builder;
pub(in crate::mir::builder) mod conversion_pipeline; pub(in crate::mir::builder) mod conversion_pipeline;
pub(in crate::mir::builder) mod exit_binding; pub(in crate::mir::builder) mod exit_binding;
pub(in crate::mir::builder) mod loop_scope_shape_builder; pub(in crate::mir::builder) mod loop_scope_shape_builder;

View File

@ -160,51 +160,26 @@ 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 171-fix: Build ConditionEnv and ConditionBindings // Phase 171-172: Use ConditionEnvBuilder for unified construction (Issue 5)
use crate::mir::join_ir::lowering::condition_to_joinir::{ use super::condition_env_builder::ConditionEnvBuilder;
extract_condition_variables, ConditionEnv, ConditionBinding, use crate::mir::join_ir::lowering::condition_env::ConditionBinding;
}; let (mut env, mut condition_bindings) = ConditionEnvBuilder::build_for_break_condition(
condition,
&loop_var_name,
&self.variable_map,
loop_var_id,
)?;
let condition_var_names = extract_condition_variables(condition, &[loop_var_name.clone()]); // Create allocator for additional JoinIR-local ValueIds (needed for Trim pattern)
let mut env = ConditionEnv::new(); let mut join_value_counter = env.len() as u32;
let mut condition_bindings = Vec::new();
// Phase 171-fix: Add loop parameter to env (ValueId(0) in JoinIR space)
// The loop parameter is NOT a condition binding (it's a join_input instead)
env.insert(loop_var_name.clone(), crate::mir::ValueId(0));
// Create a local allocator for JoinIR-local ValueIds for condition-only variables
let mut join_value_counter = 1u32; // Start from 1 (0 is reserved for loop param)
let mut alloc_join_value = || { let mut alloc_join_value = || {
let id = crate::mir::ValueId(join_value_counter); let id = crate::mir::ValueId(join_value_counter);
join_value_counter += 1; join_value_counter += 1;
id id
}; };
// For each condition variable, allocate JoinIR-local ValueId and build binding // Debug: Log condition bindings
for var_name in &condition_var_names { eprintln!("[cf_loop/pattern2] Phase 171-172: ConditionEnv contains {} variables:", env.len());
let host_id = self.variable_map.get(var_name)
.copied()
.ok_or_else(|| {
format!(
"[cf_loop/pattern2] Condition variable '{}' not found in variable_map. \
Loop condition references undefined variable.",
var_name
)
})?;
let join_id = alloc_join_value(); // Allocate JoinIR-local ValueId
env.insert(var_name.clone(), join_id);
condition_bindings.push(ConditionBinding {
name: var_name.clone(),
host_value: host_id,
join_value: join_id,
});
}
// Phase 171-fix Debug: Log condition bindings
eprintln!("[cf_loop/pattern2] Phase 171-fix: ConditionEnv contains {} variables:", env.len());
eprintln!(" Loop param '{}' → JoinIR ValueId(0)", loop_var_name); eprintln!(" Loop param '{}' → JoinIR ValueId(0)", 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());