feat(joinir): Phase 75 - BindingId pilot lookup (dev-only)

Phase 75 pilots BindingId-based variable lookup in the ScopeManager and
ConditionEnv components. Demonstrates "BindingId priority → name fallback"
strategy with comprehensive testing and zero production impact.

Changes:
- scope_manager.rs: Added lookup_with_binding() trait method
- condition_env.rs: Implemented resolve_var_with_binding() with 3-tier fallback
- expr_lowerer.rs: Integrated pilot lookup paths
- condition_lowering_box.rs: Updated with BindingId support

Tests: 3/3 new pilot tests PASS (priority/fallback/legacy)
Tests: lib 958/958 PASS, normalized_dev 54/54 PASS (no regressions)

Design: Feature-gated with normalized_dev, enables Phase 76 expansion.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-13 05:35:04 +09:00
parent e1574af741
commit c18dde238a
6 changed files with 760 additions and 0 deletions

View File

@ -23,6 +23,8 @@ use super::carrier_info::CarrierInfo;
use super::condition_env::ConditionEnv;
use super::loop_body_local_env::LoopBodyLocalEnv;
use crate::mir::loop_pattern_detection::function_scope_capture::CapturedEnv;
#[cfg(feature = "normalized_dev")]
use crate::mir::BindingId; // Phase 75: BindingId-based lookup pilot
use crate::mir::ValueId;
/// Phase 231: Scope kind for variables
@ -68,6 +70,37 @@ pub trait ScopeManager {
/// 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 75: BindingId-based lookup (dev-only, pilot integration)
///
/// Look up variable by BindingId first, falling back to name-based lookup.
/// This supports gradual migration from name-based to BindingId-based
/// variable resolution.
///
/// # Arguments
///
/// * `binding_id` - Optional BindingId for the variable
/// * `name` - Variable name (used as fallback if BindingId lookup fails)
///
/// # Returns
///
/// `Some(ValueId)` if found via BindingId or name, `None` otherwise.
///
/// # Example
///
/// ```ignore
/// // BindingId available - priority lookup
/// let value_id = scope.lookup_with_binding(Some(BindingId(5)), "x");
///
/// // BindingId not available - legacy name-based fallback
/// let value_id = scope.lookup_with_binding(None, "x");
/// ```
#[cfg(feature = "normalized_dev")]
fn lookup_with_binding(&self, binding_id: Option<BindingId>, name: &str) -> Option<ValueId> {
// Default implementation: BindingId not supported, use name lookup
let _ = binding_id; // Suppress unused warning
self.lookup(name)
}
}
/// Phase 231: Pattern2-specific scope manager (pilot implementation)
@ -176,6 +209,98 @@ impl<'a> ScopeManager for Pattern2ScopeManager<'a> {
None
}
/// Phase 76: BindingId-based lookup with promoted binding support (dev-only)
///
/// Extends Phase 75's BindingId priority lookup to check promoted_bindings map.
/// This eliminates name-based hacks (`format!("is_{}", name)`) by using type-safe
/// BindingId → BindingId mapping from CarrierInfo.
///
/// ## Lookup Order
///
/// 1. Direct BindingId lookup in ConditionEnv (if BindingId provided)
/// 2. **NEW (Phase 76)**: Promoted BindingId lookup in CarrierInfo.promoted_bindings
/// 3. Fallback to legacy name-based lookup (Phase 75 behavior)
///
/// # Arguments
///
/// * `binding_id` - Optional BindingId from MirBuilder's binding_map
/// * `name` - Variable name (fallback for legacy paths)
///
/// # Returns
///
/// `Some(ValueId)` if found via BindingId/promoted/name, `None` otherwise.
///
/// # Example (Phase 76 Promotion Path)
///
/// ```ignore
/// // Given:
/// // - "digit_pos" has BindingId(5)
/// // - "is_digit_pos" has BindingId(10)
/// // - CarrierInfo.promoted_bindings[BindingId(5)] = BindingId(10)
/// // - ConditionEnv.binding_id_map[BindingId(10)] = ValueId(102)
///
/// let scope = Pattern2ScopeManager { ... };
///
/// // Phase 76: BindingId-based promoted resolution (NEW!)
/// let result = scope.lookup_with_binding(Some(BindingId(5)), "digit_pos");
/// // Step 1: ConditionEnv[BindingId(5)] → None (not a carrier)
/// // Step 2: CarrierInfo.promoted_bindings[BindingId(5)] → BindingId(10) ✓
/// // Step 3: ConditionEnv[BindingId(10)] → ValueId(102) ✓
/// assert_eq!(result, Some(ValueId(102)));
///
/// // Phase 75: Legacy name-based fallback still works
/// let result = scope.lookup_with_binding(None, "digit_pos");
/// // → Falls back to format!("is_digit_pos") lookup
/// assert_eq!(result, Some(ValueId(102)));
/// ```
///
/// # Phase 77 Migration Note
///
/// The legacy name-based path (step 3) will be removed in Phase 77 after all
/// promoters populate promoted_bindings map and all call sites provide BindingId.
#[cfg(feature = "normalized_dev")]
fn lookup_with_binding(&self, binding_id: Option<BindingId>, name: &str) -> Option<ValueId> {
use crate::mir::BindingId;
if let Some(bid) = binding_id {
// Step 1: Try direct BindingId lookup in ConditionEnv (Phase 75)
if let Some(value_id) = self.condition_env.resolve_var_with_binding(Some(bid), name) {
if std::env::var("NYASH_JOINIR_DEBUG").is_ok() {
eprintln!(
"[phase76/direct] BindingId({}) -> ValueId({}) for '{}'",
bid.0, value_id.0, name
);
}
return Some(value_id);
}
// Step 2: **NEW (Phase 76)**: Check promoted_bindings map
if let Some(promoted_bid) = self.carrier_info.resolve_promoted_with_binding(bid) {
// Promoted BindingId found, lookup in ConditionEnv
if let Some(value_id) = self.condition_env.resolve_var_with_binding(Some(promoted_bid), name) {
if std::env::var("NYASH_JOINIR_DEBUG").is_ok() {
eprintln!(
"[phase76/promoted] BindingId({}) promoted to BindingId({}) -> ValueId({}) for '{}'",
bid.0, promoted_bid.0, value_id.0, name
);
}
return Some(value_id);
}
}
// Step 3: Fallback to legacy name-based lookup (Phase 75 fallback path)
if std::env::var("NYASH_JOINIR_DEBUG").is_ok() {
eprintln!(
"[phase76/fallback] BindingId({}) miss, falling back to name '{}' lookup",
bid.0, name
);
}
}
// Step 4: Legacy name-based lookup (Phase 75 behavior, will be removed in Phase 77)
self.lookup(name)
}
}
#[cfg(test)]
@ -195,6 +320,8 @@ mod tests {
carriers: vec![],
trim_helper: None,
promoted_loopbodylocals: vec![],
#[cfg(feature = "normalized_dev")]
promoted_bindings: std::collections::BTreeMap::new(),
};
let scope = Pattern2ScopeManager {
@ -226,6 +353,8 @@ mod tests {
}],
trim_helper: None,
promoted_loopbodylocals: vec![],
#[cfg(feature = "normalized_dev")]
promoted_bindings: std::collections::BTreeMap::new(),
};
let scope = Pattern2ScopeManager {
@ -256,6 +385,8 @@ mod tests {
}],
trim_helper: None,
promoted_loopbodylocals: vec!["digit_pos".to_string()],
#[cfg(feature = "normalized_dev")]
promoted_bindings: std::collections::BTreeMap::new(),
};
let scope = Pattern2ScopeManager {
@ -283,6 +414,8 @@ mod tests {
carriers: vec![],
trim_helper: None,
promoted_loopbodylocals: vec![],
#[cfg(feature = "normalized_dev")]
promoted_bindings: std::collections::BTreeMap::new(),
};
let scope = Pattern2ScopeManager {
@ -316,6 +449,8 @@ mod tests {
carriers: vec![],
trim_helper: None,
promoted_loopbodylocals: vec![],
#[cfg(feature = "normalized_dev")]
promoted_bindings: std::collections::BTreeMap::new(),
};
let scope = Pattern2ScopeManager {