feat(joinir): Phase 231 - ExprLowerer/ScopeManager pilot implementation

Pilot implementation of unified expression lowering for Pattern2 break conditions:

New files:
- scope_manager.rs (280 lines) - ScopeManager trait + Pattern2ScopeManager
- expr_lowerer.rs (455 lines) - ExprLowerer with Condition context support

Features:
- Unified variable lookup across ConditionEnv/LoopBodyLocalEnv/CapturedEnv/CarrierInfo
- Pre-validation of condition AST before lowering
- Fail-safe design with fallback to legacy path
- 8 new unit tests (all pass)

Integration:
- Pattern2 break condition uses ExprLowerer for pre-validation
- Existing proven lowering path preserved
- Zero impact on existing functionality (890/897 tests pass)

🤖 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-10 22:48:45 +09:00
parent b07329b37f
commit 13a676d406
10 changed files with 1427 additions and 75 deletions

View File

@ -0,0 +1,326 @@
//! Phase 231: Scope Manager for Unified Variable Lookup
//!
//! This module provides a unified interface for variable lookup across different
//! scopes in JoinIR lowering. It abstracts over the complexity of multiple
//! environments (ConditionEnv, LoopBodyLocalEnv, CapturedEnv, CarrierInfo).
//!
//! ## Design Philosophy
//!
//! **Box-First**: ScopeManager is a trait-based "box" that encapsulates variable
//! lookup logic, making it easy to swap implementations or test in isolation.
//!
//! **Single Responsibility**: Variable resolution only. Does NOT:
//! - Lower AST to JoinIR (that's ExprLowerer)
//! - Manage ValueId allocation (that's JoinValueSpace)
//! - Handle HOST ↔ JoinIR bindings (that's InlineBoundary)
//!
//! ## Pattern2 Pilot Implementation
//!
//! Phase 231 starts with Pattern2-specific implementation to validate the design.
//! Future phases will generalize to Pattern1, Pattern3, etc.
use crate::mir::ValueId;
use super::condition_env::ConditionEnv;
use super::loop_body_local_env::LoopBodyLocalEnv;
use super::carrier_info::CarrierInfo;
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
use std::collections::BTreeMap;
/// Phase 231: Scope kind for variables
///
/// Helps distinguish where a variable comes from, which affects how it's
/// treated during lowering (e.g., PHI generation, exit handling).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VarScopeKind {
/// Loop control variable (i, p)
LoopVar,
/// Carrier variable (sum, count, is_digit_pos)
Carrier,
/// Loop body-local variable (ch, digit_pos before promotion)
LoopBodyLocal,
/// Captured from outer function scope (digits, s, len)
Captured,
}
/// Phase 231: Scope manager trait for unified variable lookup
///
/// This trait provides a unified interface for looking up variables across
/// multiple environments. Implementations can aggregate different environment
/// types (ConditionEnv, LoopBodyLocalEnv, etc.) and provide consistent lookup.
///
/// # Example
///
/// ```ignore
/// let scope: &dyn ScopeManager = &Pattern2ScopeManager { ... };
/// if let Some(value_id) = scope.lookup("sum") {
/// // Use value_id in expression lowering
/// }
/// ```
pub trait ScopeManager {
/// Look up variable by name, return ValueId if found
///
/// This method searches across all available scopes and returns the first
/// match. The search order is implementation-defined but should be
/// documented in the implementing struct.
fn lookup(&self, name: &str) -> Option<ValueId>;
/// Get the scope kind of a variable
///
/// This helps the caller understand where the variable comes from, which
/// can affect code generation (e.g., PHI node generation, exit handling).
fn scope_of(&self, name: &str) -> Option<VarScopeKind>;
}
/// Phase 231: Pattern2-specific scope manager (pilot implementation)
///
/// This implementation aggregates all the environments used in Pattern2 loop
/// lowering and provides unified variable lookup.
///
/// ## Lookup Order
///
/// 1. ConditionEnv (includes loop var, carriers, condition-only vars)
/// 2. LoopBodyLocalEnv (body-local variables before promotion)
/// 3. CapturedEnv (function-scoped captured variables)
/// 4. Promoted LoopBodyLocal → Carrier (using naming convention)
///
/// ## Naming Convention for Promoted Variables
///
/// - DigitPos pattern: `"digit_pos"` → `"is_digit_pos"`
/// - Trim pattern: `"ch"` → `"is_ch_match"`
///
/// # Example
///
/// ```ignore
/// let scope = Pattern2ScopeManager {
/// condition_env: &env,
/// loop_body_local_env: Some(&body_local_env),
/// captured_env: Some(&captured_env),
/// carrier_info: &carrier_info,
/// };
///
/// // Lookup loop variable
/// assert_eq!(scope.lookup("i"), Some(ValueId(100)));
///
/// // Lookup carrier
/// assert_eq!(scope.lookup("sum"), Some(ValueId(101)));
///
/// // Lookup promoted variable (uses naming convention)
/// assert_eq!(scope.lookup("digit_pos"), Some(ValueId(102))); // Resolves to "is_digit_pos"
/// ```
pub struct Pattern2ScopeManager<'a> {
/// Condition environment (loop var + carriers + condition-only vars)
pub condition_env: &'a ConditionEnv,
/// Loop body-local environment (optional, may be empty)
pub loop_body_local_env: Option<&'a LoopBodyLocalEnv>,
/// Captured environment (optional, may be empty)
pub captured_env: Option<&'a CapturedEnv>,
/// Carrier information (includes promoted_loopbodylocals list)
pub carrier_info: &'a CarrierInfo,
}
impl<'a> ScopeManager for Pattern2ScopeManager<'a> {
fn lookup(&self, name: &str) -> Option<ValueId> {
// 1. ConditionEnv (highest priority: loop var, carriers, condition-only)
if let Some(id) = self.condition_env.get(name) {
return Some(id);
}
// 2. LoopBodyLocalEnv (body-local variables)
if let Some(env) = self.loop_body_local_env {
if let Some(id) = env.get(name) {
return Some(id);
}
}
// 3. CapturedEnv (function-scoped captured variables)
if let Some(env) = self.captured_env {
for var in &env.vars {
if var.name == name {
// Captured variables are already in condition_env, so this
// should have been caught in step 1. But check here for safety.
return self.condition_env.get(name);
}
}
}
// 4. Promoted LoopBodyLocal → Carrier lookup
// If this variable was promoted, try to find its carrier equivalent
if self.carrier_info.promoted_loopbodylocals.contains(&name.to_string()) {
// Try naming conventions
for carrier_name in &[
format!("is_{}", name), // DigitPos pattern
format!("is_{}_match", name), // Trim pattern
] {
// Check if it's the loop variable (unlikely but possible)
if carrier_name == &self.carrier_info.loop_var_name {
if let Some(id) = self.condition_env.get(&self.carrier_info.loop_var_name) {
return Some(id);
}
}
// Otherwise check carriers
if let Some(carrier) = self.carrier_info.carriers.iter().find(|c| c.name == *carrier_name) {
if let Some(join_id) = carrier.join_id {
return Some(join_id);
}
}
}
}
None
}
fn scope_of(&self, name: &str) -> Option<VarScopeKind> {
// Check loop variable first
if name == self.carrier_info.loop_var_name {
return Some(VarScopeKind::LoopVar);
}
// Check carriers
if self.carrier_info.carriers.iter().any(|c| c.name == name) {
return Some(VarScopeKind::Carrier);
}
// Check body-local
if let Some(env) = self.loop_body_local_env {
if env.contains(name) {
return Some(VarScopeKind::LoopBodyLocal);
}
}
// Check captured
if let Some(env) = self.captured_env {
if env.vars.iter().any(|v| v.name == name) {
return Some(VarScopeKind::Captured);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mir::join_ir::lowering::carrier_info::{CarrierVar, CarrierRole, CarrierInit};
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedVar;
#[test]
fn test_pattern2_scope_manager_loop_var() {
let mut condition_env = ConditionEnv::new();
condition_env.insert("i".to_string(), ValueId(100));
let carrier_info = CarrierInfo {
loop_var_name: "i".to_string(),
loop_var_id: ValueId(1),
carriers: vec![],
trim_helper: None,
promoted_loopbodylocals: vec![],
};
let scope = Pattern2ScopeManager {
condition_env: &condition_env,
loop_body_local_env: None,
captured_env: None,
carrier_info: &carrier_info,
};
assert_eq!(scope.lookup("i"), Some(ValueId(100)));
assert_eq!(scope.scope_of("i"), Some(VarScopeKind::LoopVar));
}
#[test]
fn test_pattern2_scope_manager_carrier() {
let mut condition_env = ConditionEnv::new();
condition_env.insert("i".to_string(), ValueId(100));
condition_env.insert("sum".to_string(), ValueId(101));
let carrier_info = CarrierInfo {
loop_var_name: "i".to_string(),
loop_var_id: ValueId(1),
carriers: vec![
CarrierVar {
name: "sum".to_string(),
host_id: ValueId(2),
join_id: Some(ValueId(101)),
role: CarrierRole::LoopState,
init: CarrierInit::FromHost,
},
],
trim_helper: None,
promoted_loopbodylocals: vec![],
};
let scope = Pattern2ScopeManager {
condition_env: &condition_env,
loop_body_local_env: None,
captured_env: None,
carrier_info: &carrier_info,
};
assert_eq!(scope.lookup("sum"), Some(ValueId(101)));
assert_eq!(scope.scope_of("sum"), Some(VarScopeKind::Carrier));
}
#[test]
fn test_pattern2_scope_manager_promoted_variable() {
let mut condition_env = ConditionEnv::new();
condition_env.insert("i".to_string(), ValueId(100));
let carrier_info = CarrierInfo {
loop_var_name: "i".to_string(),
loop_var_id: ValueId(1),
carriers: vec![
CarrierVar {
name: "is_digit_pos".to_string(),
host_id: ValueId(2),
join_id: Some(ValueId(102)),
role: CarrierRole::ConditionOnly,
init: CarrierInit::BoolConst(false),
},
],
trim_helper: None,
promoted_loopbodylocals: vec!["digit_pos".to_string()],
};
let scope = Pattern2ScopeManager {
condition_env: &condition_env,
loop_body_local_env: None,
captured_env: None,
carrier_info: &carrier_info,
};
// Lookup "digit_pos" should resolve to "is_digit_pos" carrier
assert_eq!(scope.lookup("digit_pos"), Some(ValueId(102)));
}
#[test]
fn test_pattern2_scope_manager_body_local() {
let mut condition_env = ConditionEnv::new();
condition_env.insert("i".to_string(), ValueId(100));
let mut body_local_env = LoopBodyLocalEnv::new();
body_local_env.insert("temp".to_string(), ValueId(200));
let carrier_info = CarrierInfo {
loop_var_name: "i".to_string(),
loop_var_id: ValueId(1),
carriers: vec![],
trim_helper: None,
promoted_loopbodylocals: vec![],
};
let scope = Pattern2ScopeManager {
condition_env: &condition_env,
loop_body_local_env: Some(&body_local_env),
captured_env: None,
carrier_info: &carrier_info,
};
assert_eq!(scope.lookup("temp"), Some(ValueId(200)));
assert_eq!(scope.scope_of("temp"), Some(VarScopeKind::LoopBodyLocal));
}
}