feat(joinir): Phase 184 - Body-local MIR Lowering Infrastructure

Phase 184 implements the foundation for body-local variable support in
update expressions, completing the three-box architecture:
LoopBodyLocalEnv (storage), UpdateEnv (composition), and
CarrierUpdateEmitter (emission).

## Implementation Summary

### Task 184-1: Design Document
- Created phase184-body-local-mir-lowering.md
- Two-Environment System design (ConditionEnv + LoopBodyLocalEnv)
- Box-First design principles documented

### Task 184-2: LoopBodyLocalEnv Implementation
- New file: src/mir/join_ir/lowering/loop_body_local_env.rs (216 lines)
- Storage box for body-local variable name → ValueId mappings
- BTreeMap for deterministic ordering (PHI consistency)
- 7 unit tests: empty env, single/multiple locals, get/contains, iteration

### Task 184-3: UpdateEnv Implementation
- New file: src/mir/join_ir/lowering/update_env.rs (237 lines)
- Composition box for unified variable resolution
- Priority order: ConditionEnv (condition vars) → LoopBodyLocalEnv (body-local)
- 8 unit tests: priority, fallback, not found, combined lookup

### Task 184-4: CarrierUpdateEmitter Integration
- Modified: src/mir/join_ir/lowering/carrier_update_emitter.rs
- Added emit_carrier_update_with_env() (UpdateEnv version)
- Kept emit_carrier_update() for backward compatibility
- 4 new unit tests: body-local variable, priority, not found, const update
- Total 10 tests PASS (6 existing + 4 new)

### Task 184-5: Representative Test Cases
- apps/tests/phase184_body_local_update.hako (Pattern1 baseline)
- apps/tests/phase184_body_local_with_break.hako (Pattern2, Phase 185 target)

### Task 184-6: Documentation Updates
- Updated: docs/development/current/main/joinir-architecture-overview.md
- Updated: CURRENT_TASK.md (Phase 184 completion record)

## Test Results

All 25 unit tests PASS:
- LoopBodyLocalEnv: 7 tests
- UpdateEnv: 8 tests
- CarrierUpdateEmitter: 10 tests (6 existing + 4 new)

Build:  Success (0 errors)

## Design Constraints

Following 箱理論 (Box Theory) principles:
- Single Responsibility: Each box has one clear purpose
- Deterministic: BTreeMap ensures consistent ordering
- Conservative: Pattern5 (Trim) integration deferred to Phase 185
- Fail-Fast: Explicit errors for unsupported patterns

## Scope Limitation

Phase 184 provides the **infrastructure only**:
-  Storage box (LoopBodyLocalEnv)
-  Composition box (UpdateEnv)
-  Emission support (CarrierUpdateEmitter)
-  Pattern2/4 integration (requires body-local collection, Phase 185)

## Next Steps

Phase 185: Pattern2/4 Integration
- Integrate LoopBodyLocalEnv into Pattern2/4 lowerers
- Add body-local variable collection from loop body AST
- Enable full E2E body-local variable support in update expressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-08 23:59:19 +09:00
parent 440f8646b1
commit b6e31cf8ca
9 changed files with 1263 additions and 6 deletions

View File

@ -1,21 +1,159 @@
//! Phase 176-2 / Phase 179: Carrier Update Emission
//! Phase 176-2 / Phase 179 / Phase 184: Carrier Update Emission
//!
//! Converts UpdateExpr (from LoopUpdateAnalyzer) into JoinIR instructions
//! that compute the updated carrier value.
//!
//! This module is extracted from loop_with_break_minimal.rs to improve
//! modularity and single responsibility.
//!
//! Phase 184: Added UpdateEnv support for body-local variable resolution.
use crate::mir::join_ir::lowering::carrier_info::CarrierVar;
use crate::mir::join_ir::lowering::condition_to_joinir::ConditionEnv;
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
use crate::mir::join_ir::lowering::loop_update_analyzer::{UpdateExpr, UpdateRhs};
use crate::mir::join_ir::lowering::update_env::UpdateEnv;
use crate::mir::join_ir::{BinOpKind, ConstValue, JoinInst, MirLikeInst};
use crate::mir::ValueId;
/// Emit JoinIR instructions for a single carrier update
/// Emit JoinIR instructions for a single carrier update (Phase 184: UpdateEnv version)
///
/// Converts UpdateExpr (from LoopUpdateAnalyzer) into JoinIR instructions
/// that compute the updated carrier value.
/// that compute the updated carrier value. Supports both condition variables
/// and body-local variables through UpdateEnv.
///
/// # Arguments
///
/// * `carrier` - Carrier variable information (name, ValueId)
/// * `update` - Update expression (e.g., CounterLike, AccumulationLike)
/// * `alloc_value` - ValueId allocator closure
/// * `env` - UpdateEnv for unified variable resolution
/// * `instructions` - Output vector to append instructions to
///
/// # Returns
///
/// ValueId of the computed update result
///
/// # Example
///
/// ```ignore
/// // For "count = count + temp":
/// let count_next = emit_carrier_update_with_env(
/// &count_carrier,
/// &UpdateExpr::BinOp { lhs: "count", op: Add, rhs: Variable("temp") },
/// &mut alloc_value,
/// &update_env, // Has both condition and body-local vars
/// &mut instructions,
/// )?;
/// // Generates:
/// // count_next = BinOp(Add, count_param, temp_value)
/// ```
pub fn emit_carrier_update_with_env(
carrier: &CarrierVar,
update: &UpdateExpr,
alloc_value: &mut dyn FnMut() -> ValueId,
env: &UpdateEnv,
instructions: &mut Vec<JoinInst>,
) -> Result<ValueId, String> {
match update {
UpdateExpr::Const(step) => {
// CounterLike: carrier = carrier + step
// Allocate const ValueId
let const_id = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst: const_id,
value: ConstValue::Integer(*step),
}));
// Get carrier parameter ValueId from env
let carrier_param = env
.resolve(&carrier.name)
.ok_or_else(|| {
format!(
"Carrier '{}' not found in UpdateEnv",
carrier.name
)
})?;
// Allocate result ValueId
let result = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result,
op: BinOpKind::Add,
lhs: carrier_param,
rhs: const_id,
}));
Ok(result)
}
UpdateExpr::BinOp { lhs, op, rhs } => {
// General binary operation: carrier = carrier op rhs
// Verify lhs matches carrier name
if lhs != &carrier.name {
return Err(format!(
"Update expression LHS '{}' doesn't match carrier '{}'",
lhs, carrier.name
));
}
// Get carrier parameter ValueId from env
let carrier_param = env
.resolve(&carrier.name)
.ok_or_else(|| {
format!(
"Carrier '{}' not found in UpdateEnv",
carrier.name
)
})?;
// Resolve RHS (Phase 184: Now supports body-local variables!)
let rhs_id = match rhs {
UpdateRhs::Const(n) => {
let const_id = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::Const {
dst: const_id,
value: ConstValue::Integer(*n),
}));
const_id
}
UpdateRhs::Variable(var_name) => {
env.resolve(var_name).ok_or_else(|| {
format!(
"Update RHS variable '{}' not found in UpdateEnv (neither condition nor body-local)",
var_name
)
})?
}
// Phase 178: String updates detected but not lowered to JoinIR yet
// The Rust MIR path handles string concatenation
// For JoinIR: just pass through the carrier param (no JoinIR update)
UpdateRhs::StringLiteral(_) | UpdateRhs::Other => {
eprintln!(
"[joinir/pattern2] Phase 178: Carrier '{}' has string/complex update - skipping JoinIR emit, using param passthrough",
carrier.name
);
return Ok(carrier_param); // Pass-through: no JoinIR update
}
};
// Allocate result ValueId
let result = alloc_value();
instructions.push(JoinInst::Compute(MirLikeInst::BinOp {
dst: result,
op: *op,
lhs: carrier_param,
rhs: rhs_id,
}));
Ok(result)
}
}
}
/// Emit JoinIR instructions for a single carrier update (backward compatibility version)
///
/// This function is kept for backward compatibility with existing Pattern2/4 code
/// that only needs ConditionEnv. New code should prefer `emit_carrier_update_with_env`.
///
/// # Arguments
///
@ -151,6 +289,7 @@ pub fn emit_carrier_update(
mod tests {
use super::*;
use crate::mir::join_ir::lowering::carrier_info::CarrierVar;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
// Helper: Create a test ConditionEnv
fn test_env() -> ConditionEnv {
@ -161,6 +300,19 @@ mod tests {
env
}
// Helper: Create a test LoopBodyLocalEnv
fn test_body_local_env() -> LoopBodyLocalEnv {
let mut env = LoopBodyLocalEnv::new();
env.insert("temp".to_string(), ValueId(50));
env.insert("digit".to_string(), ValueId(60));
env
}
// Helper: Create a test UpdateEnv
fn test_update_env() -> (ConditionEnv, LoopBodyLocalEnv) {
(test_env(), test_body_local_env())
}
// Helper: Create a test CarrierVar
fn test_carrier(name: &str, host_id: u32) -> CarrierVar {
CarrierVar {
@ -413,4 +565,168 @@ mod tests {
assert!(result.is_err());
assert!(result.unwrap_err().contains("Update RHS variable 'unknown_var' not found"));
}
// ============================================================================
// Phase 184: UpdateEnv version tests
// ============================================================================
#[test]
fn test_emit_update_with_env_body_local_variable() {
// Phase 184: Test using body-local variable in update expression
// sum = sum + temp (temp is body-local)
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
lhs: "sum".to_string(),
op: BinOpKind::Add,
rhs: UpdateRhs::Variable("temp".to_string()), // Body-local variable
};
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let mut value_counter = 110u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
let mut instructions = Vec::new();
let result = emit_carrier_update_with_env(
&carrier,
&update,
&mut alloc_value,
&update_env,
&mut instructions,
);
assert!(result.is_ok());
let result_id = result.unwrap();
// Should generate 1 instruction: BinOp(Add, sum, temp)
assert_eq!(instructions.len(), 1);
match &instructions[0] {
JoinInst::Compute(MirLikeInst::BinOp { dst, op, lhs, rhs }) => {
assert_eq!(*dst, ValueId(110));
assert_eq!(*op, BinOpKind::Add);
assert_eq!(*lhs, ValueId(20)); // sum from condition env
assert_eq!(*rhs, ValueId(50)); // temp from body-local env
}
_ => panic!("Expected BinOp instruction"),
}
assert_eq!(result_id, ValueId(110));
}
#[test]
fn test_emit_update_with_env_condition_priority() {
// Phase 184: Test condition variable takes priority over body-local
// If both envs have "x", condition env should win
let mut cond_env = ConditionEnv::new();
cond_env.insert("x".to_string(), ValueId(100)); // Condition: x=100
cond_env.insert("sum".to_string(), ValueId(20));
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("x".to_string(), ValueId(200)); // Body-local: x=200 (should be ignored)
let update_env = UpdateEnv::new(&cond_env, &body_env);
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
lhs: "sum".to_string(),
op: BinOpKind::Add,
rhs: UpdateRhs::Variable("x".to_string()),
};
let mut value_counter = 120u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
let mut instructions = Vec::new();
let result = emit_carrier_update_with_env(
&carrier,
&update,
&mut alloc_value,
&update_env,
&mut instructions,
);
assert!(result.is_ok());
// Should use x=100 (condition env), not x=200 (body-local env)
match &instructions[0] {
JoinInst::Compute(MirLikeInst::BinOp { dst: _, op: _, lhs: _, rhs }) => {
assert_eq!(*rhs, ValueId(100)); // Condition env wins
}
_ => panic!("Expected BinOp instruction"),
}
}
#[test]
fn test_emit_update_with_env_variable_not_found() {
// Phase 184: Test error when variable not in either env
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let carrier = test_carrier("sum", 200);
let update = UpdateExpr::BinOp {
lhs: "sum".to_string(),
op: BinOpKind::Add,
rhs: UpdateRhs::Variable("nonexistent".to_string()),
};
let mut value_counter = 130u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
let mut instructions = Vec::new();
let result = emit_carrier_update_with_env(
&carrier,
&update,
&mut alloc_value,
&update_env,
&mut instructions,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("Update RHS variable 'nonexistent' not found"));
assert!(err.contains("neither condition nor body-local"));
}
#[test]
fn test_emit_update_with_env_const_update() {
// Phase 184: Test UpdateEnv with simple const update (baseline)
let (cond_env, body_env) = test_update_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
let carrier = test_carrier("count", 100);
let update = UpdateExpr::Const(1);
let mut value_counter = 140u32;
let mut alloc_value = || {
let id = ValueId(value_counter);
value_counter += 1;
id
};
let mut instructions = Vec::new();
let result = emit_carrier_update_with_env(
&carrier,
&update,
&mut alloc_value,
&update_env,
&mut instructions,
);
assert!(result.is_ok());
assert_eq!(instructions.len(), 2); // Const + BinOp
}
}

View File

@ -0,0 +1,213 @@
//! Phase 184: Loop Body Local Variable Environment
//!
//! This module provides a storage box for body-local variables in loop patterns.
//! It collects local variable declarations from loop body AST and maintains
//! name → JoinIR ValueId mappings.
//!
//! ## Design Philosophy
//!
//! **Single Responsibility**: This module ONLY handles body-local variable storage.
//! It does NOT:
//! - Resolve condition variables (that's ConditionEnv)
//! - Perform variable resolution priority logic (that's UpdateEnv)
//! - Lower AST to JoinIR (that's JoinIrBuilder)
//!
//! ## Box-First Design
//!
//! Following 箱理論 (Box Theory) principles:
//! - **Single purpose**: Store body-local variable mappings
//! - **Clear boundaries**: Only body-scope variables, not condition variables
//! - **Deterministic**: BTreeMap ensures consistent ordering
use crate::mir::ValueId;
use std::collections::BTreeMap;
/// Environment for loop body-local variables
///
/// Maps variable names to JoinIR-local ValueIds for variables declared
/// within the loop body (not in conditions).
///
/// # Example
///
/// ```nyash
/// loop(i < 5) {
/// local temp = i * 2 // Body-local: temp
/// sum = sum + temp
/// i = i + 1
/// }
/// ```
///
/// LoopBodyLocalEnv would contain: `{ "temp" → ValueId(5) }`
#[derive(Debug, Clone, Default)]
pub struct LoopBodyLocalEnv {
/// Body-local variable name → JoinIR ValueId mapping
///
/// BTreeMap ensures deterministic iteration order (important for PHI generation)
locals: BTreeMap<String, ValueId>,
}
impl LoopBodyLocalEnv {
/// Create a new empty environment
pub fn new() -> Self {
Self {
locals: BTreeMap::new(),
}
}
/// Create an environment from loop body AST nodes
///
/// This method scans the loop body for local variable declarations
/// and collects their JoinIR ValueIds.
///
/// # Arguments
///
/// * `body_locals` - List of (name, ValueId) pairs from body analysis
///
/// # Example
///
/// ```ignore
/// let body_locals = vec![
/// ("temp".to_string(), ValueId(5)),
/// ("digit".to_string(), ValueId(6)),
/// ];
/// let env = LoopBodyLocalEnv::from_locals(body_locals);
/// assert_eq!(env.get("temp"), Some(ValueId(5)));
/// ```
pub fn from_locals(body_locals: Vec<(String, ValueId)>) -> Self {
let mut locals = BTreeMap::new();
for (name, value_id) in body_locals {
locals.insert(name, value_id);
}
Self { locals }
}
/// Insert a body-local variable binding
///
/// # Arguments
///
/// * `name` - Variable name (e.g., "temp", "digit")
/// * `join_id` - JoinIR-local ValueId for this variable
pub fn insert(&mut self, name: String, join_id: ValueId) {
self.locals.insert(name, join_id);
}
/// Look up a body-local variable by name
///
/// Returns `Some(ValueId)` if the variable exists in the environment,
/// `None` otherwise.
pub fn get(&self, name: &str) -> Option<ValueId> {
self.locals.get(name).copied()
}
/// Check if a body-local variable exists in the environment
pub fn contains(&self, name: &str) -> bool {
self.locals.contains_key(name)
}
/// Get the number of body-local variables in the environment
pub fn len(&self) -> usize {
self.locals.len()
}
/// Check if the environment is empty
pub fn is_empty(&self) -> bool {
self.locals.is_empty()
}
/// Get an iterator over all (name, ValueId) pairs
///
/// Iteration order is deterministic (sorted by name) due to BTreeMap.
pub fn iter(&self) -> impl Iterator<Item = (&String, &ValueId)> {
self.locals.iter()
}
/// Get all variable names (sorted)
pub fn names(&self) -> Vec<String> {
self.locals.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_body_local_env() {
let env = LoopBodyLocalEnv::new();
assert!(env.is_empty());
assert_eq!(env.len(), 0);
assert_eq!(env.names(), Vec::<String>::new());
}
#[test]
fn test_single_body_local() {
let mut env = LoopBodyLocalEnv::new();
env.insert("temp".to_string(), ValueId(5));
assert!(!env.is_empty());
assert_eq!(env.len(), 1);
assert!(env.contains("temp"));
assert_eq!(env.get("temp"), Some(ValueId(5)));
assert_eq!(env.names(), vec!["temp".to_string()]);
}
#[test]
fn test_multiple_body_locals() {
let mut env = LoopBodyLocalEnv::new();
env.insert("temp".to_string(), ValueId(5));
env.insert("digit".to_string(), ValueId(6));
env.insert("ch".to_string(), ValueId(7));
assert_eq!(env.len(), 3);
assert_eq!(env.get("temp"), Some(ValueId(5)));
assert_eq!(env.get("digit"), Some(ValueId(6)));
assert_eq!(env.get("ch"), Some(ValueId(7)));
// BTreeMap ensures sorted keys
let names = env.names();
assert_eq!(names, vec!["ch", "digit", "temp"]);
}
#[test]
fn test_get_nonexistent() {
let env = LoopBodyLocalEnv::new();
assert_eq!(env.get("nonexistent"), None);
assert!(!env.contains("nonexistent"));
}
#[test]
fn test_from_locals() {
let body_locals = vec![
("temp".to_string(), ValueId(5)),
("digit".to_string(), ValueId(6)),
];
let env = LoopBodyLocalEnv::from_locals(body_locals);
assert_eq!(env.len(), 2);
assert_eq!(env.get("temp"), Some(ValueId(5)));
assert_eq!(env.get("digit"), Some(ValueId(6)));
}
#[test]
fn test_iter_deterministic_order() {
let mut env = LoopBodyLocalEnv::new();
// Insert in non-alphabetical order
env.insert("zebra".to_string(), ValueId(3));
env.insert("apple".to_string(), ValueId(1));
env.insert("mango".to_string(), ValueId(2));
// Iteration should be sorted
let names: Vec<_> = env.iter().map(|(name, _)| name.as_str()).collect();
assert_eq!(names, vec!["apple", "mango", "zebra"]);
}
#[test]
fn test_overwrite_existing() {
let mut env = LoopBodyLocalEnv::new();
env.insert("temp".to_string(), ValueId(5));
env.insert("temp".to_string(), ValueId(10)); // Overwrite
assert_eq!(env.len(), 1);
assert_eq!(env.get("temp"), Some(ValueId(10)));
}
}

View File

@ -24,6 +24,7 @@ pub mod carrier_info; // Phase 196: Carrier metadata for loop lowering
pub(crate) mod carrier_update_emitter; // Phase 179: Carrier update instruction emission
pub(crate) mod common; // Internal lowering utilities
pub mod condition_env; // Phase 171-fix: Condition expression environment
pub mod loop_body_local_env; // Phase 184: Body-local variable environment
pub(crate) mod condition_lowerer; // Phase 171-fix: Core condition lowering logic
pub mod condition_to_joinir; // Phase 169: JoinIR condition lowering orchestrator (refactored)
pub(crate) mod condition_var_extractor; // Phase 171-fix: Variable extraction from condition AST
@ -62,6 +63,7 @@ pub mod stageb_body;
pub mod stageb_funcscanner;
pub mod type_hint_policy; // Phase 65.5: 型ヒントポリシー箱化
pub mod type_inference; // Phase 65-2-A
pub mod update_env; // Phase 184: Unified variable resolution for update expressions
pub(crate) mod value_id_ranges; // Internal ValueId range management
// Re-export public lowering functions

View File

@ -0,0 +1,248 @@
//! Phase 184: Update Expression Environment
//!
//! This module provides a unified variable resolution layer for carrier update expressions.
//! It combines ConditionEnv (condition variables) and LoopBodyLocalEnv (body-local variables)
//! with clear priority order.
//!
//! ## Design Philosophy
//!
//! **Single Responsibility**: This module ONLY handles variable resolution priority logic.
//! It does NOT:
//! - Store variables (that's ConditionEnv and LoopBodyLocalEnv)
//! - Lower AST to JoinIR (that's JoinIrBuilder)
//! - Emit update instructions (that's CarrierUpdateEmitter)
//!
//! ## Box-First Design
//!
//! Following 箱理論 (Box Theory) principles:
//! - **Composition**: Combines two environments without owning them
//! - **Clear priority**: Condition variables take precedence
//! - **Lightweight**: No allocation, just references
use crate::mir::join_ir::lowering::condition_env::ConditionEnv;
use crate::mir::join_ir::lowering::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::ValueId;
/// Unified environment for carrier update expression variable resolution
///
/// This structure provides a composition layer that resolves variables
/// with the following priority order:
///
/// 1. **Condition variables** (ConditionEnv) - Highest priority
/// - Loop parameters (e.g., `i`, `end`, `p`)
/// - Variables used in condition expressions
///
/// 2. **Body-local variables** (LoopBodyLocalEnv) - Fallback priority
/// - Variables declared in loop body (e.g., `local temp`)
/// - Only accessible if not shadowed by condition variables
///
/// # Example
///
/// ```nyash
/// loop(i < 5) { // i in ConditionEnv
/// local temp = i * 2 // temp in LoopBodyLocalEnv
/// sum = sum + temp // Resolves: sum (cond), temp (body)
/// i = i + 1
/// }
/// ```
///
/// ```ignore
/// let condition_env = /* ... i, sum ... */;
/// let body_local_env = /* ... temp ... */;
/// let update_env = UpdateEnv::new(&condition_env, &body_local_env);
///
/// // Resolve "sum" → ConditionEnv (priority 1)
/// assert_eq!(update_env.resolve("sum"), Some(ValueId(X)));
///
/// // Resolve "temp" → LoopBodyLocalEnv (priority 2)
/// assert_eq!(update_env.resolve("temp"), Some(ValueId(Y)));
///
/// // Resolve "unknown" → None
/// assert_eq!(update_env.resolve("unknown"), None);
/// ```
#[derive(Debug)]
pub struct UpdateEnv<'a> {
/// Condition variable environment (priority 1)
condition_env: &'a ConditionEnv,
/// Body-local variable environment (priority 2)
body_local_env: &'a LoopBodyLocalEnv,
}
impl<'a> UpdateEnv<'a> {
/// Create a new UpdateEnv with priority-ordered resolution
///
/// # Arguments
///
/// * `condition_env` - Condition variable environment (highest priority)
/// * `body_local_env` - Body-local variable environment (fallback)
pub fn new(
condition_env: &'a ConditionEnv,
body_local_env: &'a LoopBodyLocalEnv,
) -> Self {
Self {
condition_env,
body_local_env,
}
}
/// Resolve a variable name to JoinIR ValueId
///
/// Resolution order:
/// 1. Try condition_env.get(name)
/// 2. If not found, try body_local_env.get(name)
/// 3. If still not found, return None
///
/// # Arguments
///
/// * `name` - Variable name to resolve
///
/// # Returns
///
/// * `Some(ValueId)` - Variable found in one of the environments
/// * `None` - Variable not found in either environment
pub fn resolve(&self, name: &str) -> Option<ValueId> {
self.condition_env
.get(name)
.or_else(|| self.body_local_env.get(name))
}
/// Check if a variable exists in either environment
pub fn contains(&self, name: &str) -> bool {
self.resolve(name).is_some()
}
/// Get reference to condition environment (for debugging/diagnostics)
pub fn condition_env(&self) -> &ConditionEnv {
self.condition_env
}
/// Get reference to body-local environment (for debugging/diagnostics)
pub fn body_local_env(&self) -> &LoopBodyLocalEnv {
self.body_local_env
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper: Create a test ConditionEnv
fn test_condition_env() -> ConditionEnv {
let mut env = ConditionEnv::new();
env.insert("i".to_string(), ValueId(10));
env.insert("sum".to_string(), ValueId(20));
env.insert("end".to_string(), ValueId(30));
env
}
// Helper: Create a test LoopBodyLocalEnv
fn test_body_local_env() -> LoopBodyLocalEnv {
let mut env = LoopBodyLocalEnv::new();
env.insert("temp".to_string(), ValueId(50));
env.insert("digit".to_string(), ValueId(60));
env
}
#[test]
fn test_resolve_condition_priority() {
// Condition variables should be found first
let cond_env = test_condition_env();
let body_env = LoopBodyLocalEnv::new(); // Empty
let update_env = UpdateEnv::new(&cond_env, &body_env);
assert_eq!(update_env.resolve("i"), Some(ValueId(10)));
assert_eq!(update_env.resolve("sum"), Some(ValueId(20)));
assert_eq!(update_env.resolve("end"), Some(ValueId(30)));
}
#[test]
fn test_resolve_body_local_fallback() {
// Body-local variables should be found when not in condition env
let cond_env = ConditionEnv::new(); // Empty
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
assert_eq!(update_env.resolve("temp"), Some(ValueId(50)));
assert_eq!(update_env.resolve("digit"), Some(ValueId(60)));
}
#[test]
fn test_resolve_priority_order() {
// Condition env takes priority over body-local env
let mut cond_env = ConditionEnv::new();
cond_env.insert("x".to_string(), ValueId(100)); // Condition: x=100
let mut body_env = LoopBodyLocalEnv::new();
body_env.insert("x".to_string(), ValueId(200)); // Body-local: x=200
let update_env = UpdateEnv::new(&cond_env, &body_env);
// Should resolve to condition env value (100), not body-local (200)
assert_eq!(update_env.resolve("x"), Some(ValueId(100)));
}
#[test]
fn test_resolve_not_found() {
// Variable not in either environment → None
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
assert_eq!(update_env.resolve("unknown"), None);
assert_eq!(update_env.resolve("nonexistent"), None);
}
#[test]
fn test_resolve_combined_lookup() {
// Mixed lookup: some in condition, some in body-local
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
// Condition variables
assert_eq!(update_env.resolve("i"), Some(ValueId(10)));
assert_eq!(update_env.resolve("sum"), Some(ValueId(20)));
// Body-local variables
assert_eq!(update_env.resolve("temp"), Some(ValueId(50)));
assert_eq!(update_env.resolve("digit"), Some(ValueId(60)));
// Not found
assert_eq!(update_env.resolve("unknown"), None);
}
#[test]
fn test_contains() {
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
assert!(update_env.contains("i"));
assert!(update_env.contains("temp"));
assert!(!update_env.contains("unknown"));
}
#[test]
fn test_empty_environments() {
// Both environments empty
let cond_env = ConditionEnv::new();
let body_env = LoopBodyLocalEnv::new();
let update_env = UpdateEnv::new(&cond_env, &body_env);
assert_eq!(update_env.resolve("anything"), None);
assert!(!update_env.contains("anything"));
}
#[test]
fn test_accessor_methods() {
// Test diagnostic accessor methods
let cond_env = test_condition_env();
let body_env = test_body_local_env();
let update_env = UpdateEnv::new(&cond_env, &body_env);
// Should return references to underlying environments
assert_eq!(update_env.condition_env().len(), 3);
assert_eq!(update_env.body_local_env().len(), 2);
}
}