# Phase 77: Expansion - Pattern2→3→4 BindingId Migration Complete **Status**: ANALYSIS COMPLETE - Ready for Implementation **Date**: 2025-12-13 **Estimated Duration**: 2-3 hours **Build Baseline**: 958/958 tests PASS (without normalized_dev), lib tests stable --- ## Executive Summary Phase 77 completes the BindingId migration by: 1. **Populating promoted_bindings** in DigitPos/Trim promoters 2. **Extending BindingId lookup** to Pattern3/4 3. **Deleting legacy name-based code** (~40 lines of string hacks) 4. **Achieving type-safe promotion** across all JoinIR patterns ### Dependencies - **Phase 74**: BindingId infrastructure (MirBuilder.binding_map) ✅ - **Phase 75**: BindingId priority lookup (ConditionEnv) ✅ - **Phase 76**: promoted_bindings data structure ✅ ### Success Metrics - ✅ DigitPos/Trim promoters populate promoted_bindings - ✅ Pattern3/4 use BindingId priority lookup - ✅ Legacy name-based code deleted (~40 lines) - ✅ No new by-name dependencies introduced - ✅ 958/958 tests PASS (regression-free) --- ## Architecture Overview ### Phase 74-77 Evolution ``` Phase 74: Infrastructure → binding_map added to MirBuilder Phase 75: Pilot → BindingId priority lookup in ConditionEnv Phase 76: Promotion Infra → promoted_bindings map in CarrierInfo Phase 77: Expansion (THIS) → Populate map + Delete legacy code ``` ### Current State (After Phase 76) **Infrastructure Complete**: - ✅ `MirBuilder.binding_map: BTreeMap` - ✅ `CarrierInfo.promoted_bindings: BTreeMap` - ✅ `ConditionEnv.resolve_var_with_binding()` (3-tier lookup) - ✅ `Pattern2ScopeManager.lookup_with_binding()` (BindingId priority) **Missing Pieces** (Phase 77 scope): - ❌ Promoters don't call `record_promoted_binding()` yet - ❌ Pattern3/4 don't use BindingId lookup yet - ❌ Legacy name-based code still active --- ## Implementation Plan ### Task 1: DigitPosPromoter Integration (45 min) **Goal**: Populate promoted_bindings when DigitPos promotion succeeds **File**: `src/mir/loop_pattern_detection/loop_body_digitpos_promoter.rs` **Current Code** (lines 225-239): ```rust // Phase 229: Record promoted variable (no need for condition_aliases) // Dynamic resolution uses promoted_loopbodylocals + naming convention carrier_info .promoted_loopbodylocals .push(var_in_cond.clone()); eprintln!( "[digitpos_promoter] Phase 247-EX: A-4 DigitPos pattern promoted: {} → {} (bool) + {} (i64)", var_in_cond, bool_carrier_name, int_carrier_name ); eprintln!( "[digitpos_promoter] Phase 229: Recorded promoted variable '{}' (carriers: '{}', '{}')", var_in_cond, bool_carrier_name, int_carrier_name ); return DigitPosPromotionResult::Promoted { carrier_info, promoted_var: var_in_cond, carrier_name: bool_carrier_name, }; ``` **Problem**: - No access to `binding_map` from MirBuilder - `carrier_info` is created locally, but `binding_map` lives in MirBuilder - Need to pass `binding_map` reference through call chain **Solution Approach**: **Option A** (Recommended): Add optional binding_map parameter to DigitPosPromotionRequest ```rust pub struct DigitPosPromotionRequest<'a> { // ... existing fields ... /// Phase 77: Optional BindingId map for type-safe promotion tracking (dev-only) #[cfg(feature = "normalized_dev")] pub binding_map: Option<&'a BTreeMap>, } ``` **Changes Required**: 1. Add `binding_map` field to `DigitPosPromotionRequest` (dev-only) 2. In `try_promote()`, after successful promotion: ```rust #[cfg(feature = "normalized_dev")] if let Some(binding_map) = req.binding_map { if let (Some(original_bid), Some(promoted_bool_bid)) = ( binding_map.get(var_in_cond), binding_map.get(&bool_carrier_name), ) { carrier_info.record_promoted_binding(*original_bid, *promoted_bool_bid); eprintln!( "[digitpos_promoter/phase77] Recorded promoted binding: {} (BindingId({:?})) → {} (BindingId({:?}))", var_in_cond, original_bid, bool_carrier_name, promoted_bool_bid ); } } ``` **Call Site Updates**: - `src/mir/loop_pattern_detection/loop_body_cond_promoter.rs:203-210` - Add `binding_map: Some(&req.binding_map)` to `DigitPosPromotionRequest` construction - Requires `binding_map` in `ConditionPromotionRequest` **Dependency Chain**: ``` MirBuilder.binding_map ↓ (needs to flow through) ConditionPromotionRequest.binding_map (NEW) ↓ DigitPosPromotionRequest.binding_map (NEW) ↓ try_promote() → record_promoted_binding() ``` ### Task 2: TrimLoopHelper Integration (30 min) **Goal**: Populate promoted_bindings when Trim promotion succeeds **File**: `src/mir/loop_pattern_detection/trim_loop_helper.rs` **Similar Approach**: 1. Add optional `binding_map` to `TrimPromotionRequest` 2. In `TrimLoopHelper::try_promote()`, record promoted binding 3. Update call sites to pass `binding_map` **Implementation**: ```rust #[cfg(feature = "normalized_dev")] if let Some(binding_map) = req.binding_map { if let (Some(original_bid), Some(promoted_bid)) = ( binding_map.get(&req.var_name), binding_map.get(&carrier_name), ) { carrier_info.record_promoted_binding(*original_bid, *promoted_bid); eprintln!( "[trim_lowerer/phase77] Recorded promoted binding: {} → {}", req.var_name, carrier_name ); } } ``` ### Task 3: Pattern3/4 BindingId Lookup Integration (45 min) **Goal**: Use BindingId priority lookup in Pattern3/4 condition resolution **Files**: - `src/mir/builder/control_flow/joinir/patterns/pattern3_with_if_phi.rs` - `src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs` **Current State**: Pattern3/4 use name-based lookup only **Target State**: Use `ConditionEnv.resolve_var_with_binding()` like Pattern2 does **Changes**: 1. **Pattern3 ConditionEnv Builder** (dev-only parameter): ```rust #[cfg(feature = "normalized_dev")] fn build_condition_env_with_binding_map( &mut self, binding_map: &BTreeMap, ) -> ConditionEnv { // Use BindingId priority resolution } ``` 2. **Pattern4 Similar Integration**: - Add `with_binding_map()` variant for dev-only BindingId lookup - Fallback to name-based for production (dual-path) **Note**: Full production integration deferred to Phase 78+ (requires broader refactoring) ### Task 4: Legacy Code Deletion (~40 lines) **Goal**: Remove fragile name-based promotion hacks #### 4.1 CarrierInfo::resolve_promoted_join_id (carrier_info.rs:440-505) **Current Code**: ```rust let candidates = [ format!("is_{}", original_name), // DigitPos pattern format!("is_{}_match", original_name), // Trim pattern ]; for carrier_name in &candidates { if let Some(carrier) = self.carriers.iter().find(|c| c.name == *carrier_name) { return carrier.join_id; } } ``` **Deletion Strategy**: ```rust /// Phase 77: Type-safe promoted binding resolution /// /// DEPRECATED: Use `resolve_promoted_with_binding()` for BindingId-based lookup. /// This method remains for legacy fallback only (dev-only name guard). #[cfg(feature = "normalized_dev")] #[deprecated(since = "phase77", note = "Use resolve_promoted_with_binding() instead")] pub fn resolve_promoted_join_id(&self, original_name: &str) -> Option { // Try BindingId-based lookup first (requires binding context) // Fallback to name-based (legacy, for non-migrated call sites) eprintln!( "[phase77/legacy/carrier_info] WARNING: Using deprecated name-based promoted lookup for '{}'", original_name ); let candidates = [ format!("is_{}", original_name), format!("is_{}_match", original_name), ]; // ... rest of legacy code ... } ``` **Full Deletion Criteria** (Phase 78+): - ✅ All call sites migrated to BindingId - ✅ No `None` binding_map in production paths - ✅ Dev-only name guard deprecated warnings addressed #### 4.2 Pattern2ScopeManager Fallback Path (scope_manager.rs) **Current Code** (lines 140-160): ```rust // Step 3: Legacy name-based fallback self.lookup(name) ``` **Phase 77 Update**: ```rust // Step 3: Legacy name-based fallback (dev-only warning) #[cfg(feature = "normalized_dev")] if binding_id.is_some() { eprintln!( "[phase77/fallback/scope_manager] BindingId provided but not resolved: '{}' (BindingId({:?}))", name, binding_id ); } self.lookup(name) ``` **Full Deletion** (Phase 78+): Remove fallback entirely, make BindingId required ### Task 5: E2E Verification Tests (30 min) **Goal**: Verify end-to-end BindingId promotion flow **File**: `tests/normalized_joinir_min.rs` (or new `tests/phase77_binding_promotion.rs`) #### Test 1: DigitPos End-to-End ```rust #[cfg(feature = "normalized_dev")] #[test] fn test_phase77_digitpos_end_to_end_binding_resolution() { // Fixture with digit_pos pattern // Verify: // 1. DigitPosPromoter records promoted_bindings // 2. ScopeManager resolves via BindingId (not name) // 3. No fallback warnings in debug output } ``` #### Test 2: Trim End-to-End ```rust #[cfg(feature = "normalized_dev")] #[test] fn test_phase77_trim_end_to_end_binding_resolution() { // Fixture with trim pattern (ch → is_ch_match) // Verify promoted_bindings population } ``` #### Test 3: Pattern3 BindingId Lookup ```rust #[cfg(feature = "normalized_dev")] #[test] fn test_phase77_pattern3_binding_lookup() { // P3 if-sum with promoted carrier // Verify BindingId priority lookup works } ``` #### Test 4: Pattern4 BindingId Lookup ```rust #[cfg(feature = "normalized_dev")] #[test] fn test_phase77_pattern4_binding_lookup() { // P4 continue with promoted carrier // Verify BindingId resolution } ``` --- ## Migration Path Analysis ### Phase 77 Scope (THIS PHASE) **WILL DO**: - ✅ Populate promoted_bindings in promoters (DigitPos/Trim) - ✅ Add binding_map parameter to promotion requests (dev-only) - ✅ Extend Pattern3/4 with BindingId lookup (dev-only variants) - ✅ Deprecate legacy name-based code (not delete yet) - ✅ Add E2E verification tests **WILL NOT DO** (deferred to Phase 78+): - ❌ Remove legacy code entirely (keep dual-path for safety) - ❌ Make BindingId required in production paths - ❌ Remove `promoted_loopbodylocals` field (still used as fallback) ### Phase 78+ Future Work **Deletion Candidates** (~40 lines): 1. `CarrierInfo::resolve_promoted_join_id()` body (25 lines) 2. `Pattern2ScopeManager` name-based fallback (10 lines) 3. `promoted_loopbodylocals` field + related code (5 lines) **Preconditions for Deletion**: - All promoters populate promoted_bindings ✅ (Phase 77) - All patterns use BindingId lookup ✅ (Phase 77 dev-only, Phase 78 production) - No `None` binding_map in call chain (Phase 78 refactoring) --- ## Implementation Sequence ### Step-by-Step (2-3 hours total) 1. **DigitPosPromoter Integration** (45 min) - Add `binding_map` parameter to request struct - Implement promoted_bindings recording - Update call sites - Test: verify BindingId recorded correctly 2. **TrimLoopHelper Integration** (30 min) - Similar changes to DigitPos - Update trim-specific call sites - Test: verify trim pattern promotion 3. **Pattern3/4 Lookup Integration** (45 min) - Add dev-only BindingId lookup variants - Wire up binding_map from MirBuilder - Test: verify P3/P4 resolve via BindingId 4. **Legacy Code Deprecation** (15 min) - Add deprecation warnings - Add fallback logging - Document deletion criteria 5. **E2E Tests** (30 min) - Write 4 verification tests - Verify no regressions in existing tests - Document test coverage 6. **Documentation** (15 min) - This document - Update CURRENT_TASK.md - Phase 77 completion summary --- ## Acceptance Criteria ### Build & Test - ✅ `cargo build --lib --features normalized_dev` succeeds - ✅ `cargo test --release --lib` passes (958/958) - ✅ New Phase 77 tests pass (4/4) - ✅ No regressions in existing tests ### Code Quality - ✅ DigitPos/Trim promoters populate promoted_bindings - ✅ Pattern3/4 use BindingId lookup (dev-only) - ✅ Legacy code deprecated (not deleted) - ✅ Debug logging for fallback paths ### Documentation - ✅ This document complete - ✅ Migration path clearly defined - ✅ Phase 78+ deletion criteria documented - ✅ CURRENT_TASK.md updated ### Design Principles - ✅ Fail-Fast: No silent fallbacks in dev mode - ✅ Dual-path: Legacy code kept for safety - ✅ Dev-only: All changes feature-gated - ✅ Type-safe: BindingId replaces string matching --- ## Design Notes ### Q: Why not delete legacy code immediately? **Answer**: Gradual migration reduces risk - Phase 77: Deprecate + Log (observe fallback usage) - Phase 78: Require BindingId in production paths - Phase 79: Delete legacy code entirely This 3-phase deletion ensures: - No "big bang" removals - Fallback paths for debugging - Easy rollback if issues arise ### Q: Why dev-only feature gate? **Answer**: Zero production impact during migration - `#[cfg(feature = "normalized_dev")]` guards all new code - Production builds use existing name-based paths - Enables safe experimentation ### Q: Where is binding_map created? **Answer**: MirBuilder owns the SSOT - `MirBuilder.binding_map` populated during AST lowering (Phase 68-69) - Each `local` declaration allocates a new BindingId - Shadowing tracked via LexicalScopeFrame ### Q: How to pass binding_map to promoters? **Answer**: Thread through call chain (Option A) ``` MirBuilder.binding_map ↓ [NEW] ConditionPromotionRequest.binding_map ↓ [NEW] DigitPosPromotionRequest.binding_map ↓ try_promote() → record_promoted_binding() ``` Alternative (Option B): Return promoted names from promoter, caller records - Pro: Simpler promoter API - Con: Caller needs both binding_map and carrier_info - Verdict: Option A preferred (promoter owns CarrierInfo, natural fit) --- ## Related Phases - **Phase 73**: ScopeManager BindingId design (SSOT) - **Phase 74**: BindingId infrastructure (MirBuilder.binding_map) - **Phase 75**: BindingId priority lookup (ConditionEnv) - **Phase 76**: Promoted BindingId mapping (promoted_bindings) - **Phase 77**: Population + Expansion (this phase) - **Phase 78+**: Legacy code deletion (future) --- ## Conclusion Phase 77 completes the **BindingId migration foundation** by: 1. **Populating** promoted_bindings in all promoters 2. **Extending** BindingId lookup to Pattern3/4 3. **Deprecating** legacy name-based code (deletion in Phase 78+) 4. **Verifying** end-to-end type-safe promotion **Impact**: Eliminates ~40 lines of fragile string matching and achieves **type-safe, shadowing-aware promotion tracking** across all JoinIR patterns. **Next Steps** (Phase 78+): - Remove deprecated legacy code - Make BindingId required in production paths - Remove `promoted_loopbodylocals` field entirely **Risk Level**: LOW - Dev-only feature-gated changes - Dual-path design provides fallback - Comprehensive test coverage - Gradual migration strategy