Phase 77 design finalizes BindingId infrastructure migration with comprehensive implementation roadmap. Three design documents outline expansion of promoted_bindings tracking to Pattern3/4 and legacy code deprecation strategy. Documents: - phase77-expansion-completion.md: Architecture overview + 5 implementation tasks - PHASE_77_IMPLEMENTATION_GUIDE.md: Step-by-step code changes with exact locations - PHASE_77_EXECUTIVE_SUMMARY.md: Impact analysis and success metrics Key decisions: - DigitPosPromoter/TrimLoopHelper populate promoted_bindings - Pattern3/4 use BindingId priority in variable resolution - 3-phase deletion strategy (Deprecate→Require→Delete) - Dev-only feature gate preserves zero production impact Phase 77 implementation: ~2-3 hours (5 tasks: promoters + patterns + legacy + tests) 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
15 KiB
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:
- Populating promoted_bindings in DigitPos/Trim promoters
- Extending BindingId lookup to Pattern3/4
- Deleting legacy name-based code (~40 lines of string hacks)
- 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<String, BindingId> - ✅
CarrierInfo.promoted_bindings: BTreeMap<BindingId, BindingId> - ✅
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):
// 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_mapfrom MirBuilder carrier_infois created locally, butbinding_maplives in MirBuilder- Need to pass
binding_mapreference through call chain
Solution Approach:
Option A (Recommended): Add optional binding_map parameter to DigitPosPromotionRequest
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<String, BindingId>>,
}
Changes Required:
- Add
binding_mapfield toDigitPosPromotionRequest(dev-only) - In
try_promote(), after successful promotion:#[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)toDigitPosPromotionRequestconstruction - Requires
binding_mapinConditionPromotionRequest
- Add
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:
- Add optional
binding_maptoTrimPromotionRequest - In
TrimLoopHelper::try_promote(), record promoted binding - Update call sites to pass
binding_map
Implementation:
#[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.rssrc/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:
-
Pattern3 ConditionEnv Builder (dev-only parameter):
#[cfg(feature = "normalized_dev")] fn build_condition_env_with_binding_map( &mut self, binding_map: &BTreeMap<String, BindingId>, ) -> ConditionEnv { // Use BindingId priority resolution } -
Pattern4 Similar Integration:
- Add
with_binding_map()variant for dev-only BindingId lookup - Fallback to name-based for production (dual-path)
- Add
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:
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:
/// 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<ValueId> {
// 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
Nonebinding_map in production paths - ✅ Dev-only name guard deprecated warnings addressed
4.2 Pattern2ScopeManager Fallback Path (scope_manager.rs)
Current Code (lines 140-160):
// Step 3: Legacy name-based fallback
self.lookup(name)
Phase 77 Update:
// 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
#[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
#[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
#[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
#[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_loopbodylocalsfield (still used as fallback)
Phase 78+ Future Work
Deletion Candidates (~40 lines):
CarrierInfo::resolve_promoted_join_id()body (25 lines)Pattern2ScopeManagername-based fallback (10 lines)promoted_loopbodylocalsfield + 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
Nonebinding_map in call chain (Phase 78 refactoring)
Implementation Sequence
Step-by-Step (2-3 hours total)
-
DigitPosPromoter Integration (45 min)
- Add
binding_mapparameter to request struct - Implement promoted_bindings recording
- Update call sites
- Test: verify BindingId recorded correctly
- Add
-
TrimLoopHelper Integration (30 min)
- Similar changes to DigitPos
- Update trim-specific call sites
- Test: verify trim pattern promotion
-
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
-
Legacy Code Deprecation (15 min)
- Add deprecation warnings
- Add fallback logging
- Document deletion criteria
-
E2E Tests (30 min)
- Write 4 verification tests
- Verify no regressions in existing tests
- Document test coverage
-
Documentation (15 min)
- This document
- Update CURRENT_TASK.md
- Phase 77 completion summary
Acceptance Criteria
Build & Test
- ✅
cargo build --lib --features normalized_devsucceeds - ✅
cargo test --release --libpasses (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_mappopulated during AST lowering (Phase 68-69)- Each
localdeclaration 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:
- Populating promoted_bindings in all promoters
- Extending BindingId lookup to Pattern3/4
- Deprecating legacy name-based code (deletion in Phase 78+)
- 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_loopbodylocalsfield entirely
Risk Level: LOW
- Dev-only feature-gated changes
- Dual-path design provides fallback
- Comprehensive test coverage
- Gradual migration strategy