diff --git a/docs/development/current/main/phases/phase-131/p1.5-implementation-guide.md b/docs/development/current/main/phases/phase-131/p1.5-implementation-guide.md new file mode 100644 index 00000000..709184b8 --- /dev/null +++ b/docs/development/current/main/phases/phase-131/p1.5-implementation-guide.md @@ -0,0 +1,322 @@ +# Phase 131 P1.5: Implementation Guide (Quick Reference) + +Status: Implementation Guide +Scope: Step-by-step implementation instructions for Option B +Related: +- Root Cause: `p1.5-root-cause-summary.md` +- Design: `p1.5-option-b-analysis.md` + +## Implementation Sequence + +### Step 1: Create ExitReconnectorBox (New File) + +**File**: `src/mir/control_tree/normalized_shadow/exit_reconnector.rs` + +**Responsibility**: Generate host variable bindings from k_exit env parameters (Normalized IR only, PHI-free) + +**API**: +```rust +pub struct ExitReconnectorBox; + +impl ExitReconnectorBox { + /// Reconnect k_exit env parameters to host variable_map + /// + /// For Normalized IR only: k_exit env params are SSOT for final values + pub fn reconnect( + builder: &mut MirBuilder, + exit_meta: &ExitMeta, + env_layout: &EnvLayout, + debug: bool, + ) -> Result<(), String> { + // For each exit_value (var_name, k_exit_param_vid) + // - k_exit_param_vid is already the final value (SSOT) + // - Update builder.variable_ctx.variable_map[var_name] = k_exit_param_vid + // + // Strict mode: verify all env_layout.writes are in exit_meta + // + // Returns: Ok(()) on success, Err(msg) on contract violation + } +} +``` + +**Algorithm**: +```rust +// 1. Validate (strict mode only) +if strict_enabled() { + for var in &env_layout.writes { + if !exit_meta.exit_values.iter().any(|(name, _)| name == var) { + return Err(freeze_with_hint( + "phase131/exit_reconnect/missing", + &format!("Variable '{}' in writes but not in k_exit env", var), + "Ensure env_layout.writes matches k_exit parameter list (SSOT)" + )); + } + } +} + +// 2. Update variable_map directly (no PHI) +for (var_name, k_exit_param_vid) in &exit_meta.exit_values { + // k_exit_param_vid is the final value (SSOT) + builder.variable_ctx.variable_map.insert( + var_name.clone(), + *k_exit_param_vid + ); + + if debug { + eprintln!( + "[phase131/exit_reconnect] {} → {:?}", + var_name, k_exit_param_vid + ); + } +} +``` + +**File Template**: +```rust +//! Phase 131 P1.5: ExitReconnectorBox - Normalized Exit Reconnection +//! +//! ## Responsibility +//! +//! - Wire k_exit env parameters to host variable_map (Normalized IR only) +//! - PHI-free: k_exit env params are SSOT for final values +//! - Fail-Fast: Strict mode catches contract violations +//! +//! ## Contract +//! +//! - Only used for Normalized shadow path +//! - k_exit env parameters represent final variable values (SSOT) +//! - No PHI generation (direct variable_map update) +//! +//! ## Design Philosophy +//! +//! Normalized IR uses continuation-based control flow: +//! - main(env) → loop_step(env) → loop_body(env) → k_exit(env) +//! - k_exit receives final env state as parameters +//! - No loop back edges, no PHI merging needed +//! - k_exit env params ARE the truth (SSOT) + +use crate::mir::builder::MirBuilder; +use crate::mir::control_tree::normalized_shadow::env_layout::EnvLayout; +use crate::mir::join_ir::lowering::carrier_info::ExitMeta; +use crate::mir::join_ir::lowering::error_tags; + +pub struct ExitReconnectorBox; + +impl ExitReconnectorBox { + // ... implementation here ... +} + +#[cfg(test)] +mod tests { + // ... tests here ... +} +``` + +### Step 2: Update routing.rs (Bypass Merge Pipeline) + +**File**: `src/mir/builder/control_flow/joinir/routing.rs` + +**Location**: Lines 441-532 (current Normalized shadow handling) + +**Changes**: + +1. **Add env_layout to JoinFragmentMeta** (if not already there) +2. **Detect Normalized shadow path** +3. **Use ExitReconnectorBox instead of merge pipeline** + +**Code Change**: +```rust +// Around line 441 +match StepTreeNormalizedShadowLowererBox::try_lower_if_only(&tree, &available_inputs) { + Ok(Some((join_module, join_meta))) => { + // ... existing trace ... + + // Phase 131 P1.5: Normalized-specific exit reconnection + // Bypass PHI-based merge pipeline, use direct env→host wiring + use crate::mir::control_tree::normalized_shadow::exit_reconnector::ExitReconnectorBox; + use crate::mir::control_tree::normalized_shadow::env_layout::EnvLayout; + + // Build env_layout from contract (or get from join_meta if available) + let env_layout = EnvLayout::from_contract(&tree.contract, &available_inputs); + + // Direct reconnection (no PHI) + ExitReconnectorBox::reconnect( + self, + &join_meta.exit_meta, + &env_layout, + debug, + )?; + + trace::trace().routing( + "router/normalized", + func_name, + "Normalized exit reconnection complete (PHI-free)", + ); + + // Return void constant (loop executed successfully) + // Note: If k_exit returns a value, use that instead + use crate::mir::{ConstValue, MirInstruction}; + let void_id = self.next_value_id(); + self.emit_instruction(MirInstruction::Const { + dst: void_id, + value: ConstValue::Void, + })?; + + Ok(Some(void_id)) + } + Ok(None) => { + // ... existing fallback ... + } + Err(e) => { + // ... existing error handling ... + } +} +``` + +**Note**: Remove the existing `ExitMetaCollector::collect()` and `JoinIRConversionPipeline::execute()` calls - those are for standard loops with PHI. + +### Step 3: Update mod.rs (Export ExitReconnectorBox) + +**File**: `src/mir/control_tree/normalized_shadow/mod.rs` + +**Add**: +```rust +pub mod exit_reconnector; +pub use exit_reconnector::ExitReconnectorBox; +``` + +### Step 4: Testing + +#### Unit Tests (in exit_reconnector.rs) + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty_writes() { + // When env_layout.writes is empty + // Should return Ok(()) without updating variable_map + } + + #[test] + fn test_single_write() { + // When env_layout.writes = ["x"] + // And exit_meta.exit_values = [("x", ValueId(6))] + // Should update variable_map["x"] = ValueId(6) + } + + #[test] + #[should_panic] + fn test_missing_variable_strict() { + // When strict mode enabled + // And env_layout.writes = ["x", "y"] + // But exit_meta.exit_values = [("x", ValueId(6))] + // Should panic with freeze_with_hint + } +} +``` + +#### Integration Tests + +1. **VM Smoke**: + ```bash + bash tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh + ``` + Expected: `[PASS] Output verified: 1 (exit code: 1)` + +2. **LLVM EXE Smoke**: + ```bash + bash tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_llvm_exe.sh + ``` + Expected: `[PASS] Output verified: 1` + +3. **Regression**: + ```bash + # If-only patterns (Phase 130) + bash tools/smokes/v2/profiles/integration/apps/phase130_if_only_post_if_add_vm.sh + + # Existing JoinIR patterns (Phase 97) + bash tools/smokes/v2/profiles/integration/apps/phase97_next_non_ws_llvm_exe.sh + ``` + Expected: All PASS (既定挙動不変) + +### Step 5: Verification Checklist + +Before committing: +- [ ] `cargo build --release` → 0 errors +- [ ] `cargo test --lib` → all tests pass +- [ ] Phase 131 VM smoke → PASS +- [ ] Phase 131 LLVM EXE smoke → PASS +- [ ] Phase 130 regression → PASS +- [ ] Phase 97 regression → PASS + +## Commit Sequence + +### Commit 1: Create ExitReconnectorBox +``` +feat(normalized): Phase 131 P1.5 ExitReconnectorBox for host variable propagation + +- Add exit_reconnector.rs with pure function design +- Wire k_exit env params to host variable_map (PHI-free) +- Strict mode verifier for contract violations +- Unit tests for empty/single/multiple writes +``` + +### Commit 2: Update Routing +``` +fix(joinir/dev): bypass PHI generation for Normalized path + +- Detect Normalized shadow in routing.rs +- Use ExitReconnectorBox instead of merge pipeline +- Direct env→host wiring (no PHI generation) +- Existing patterns (Pattern 1-4) unaffected +``` + +### Commit 3: Enable Tests +``` +test(joinir): Phase 131 P1.5 enforce return parity (VM + LLVM EXE) + +- Enable phase131 VM/LLVM EXE smokes +- Verify regression tests (phase130, phase97) +- Update test expectations +``` + +### Commit 4: Documentation +``` +docs: Phase 131 P1.5 DONE (Normalized exit reconnection via Option B) + +- Update 10-Now.md with completion status +- Update phase-131/README.md with P1.5 summary +- Add root cause analysis docs +``` + +## Common Pitfalls to Avoid + +1. **Don't touch merge pipeline**: Standard loops (Pattern 1-4) still use it +2. **Don't generate PHI**: Normalized IR is PHI-free by design +3. **Don't forget strict mode**: Contract violations should freeze +4. **Don't skip regression tests**: 既定挙動不変 is critical + +## Key Principles (SSOT) + +1. **PHI禁止維持**: Normalized IR never generates PHI nodes +2. **既定挙動不変**: Standard patterns use existing merge pipeline +3. **責務分離**: ExitReconnectorBox handles Normalized-specific wiring +4. **箱化モジュール化**: New logic in dedicated box +5. **Fail-Fast**: Strict mode catches violations immediately + +## Success Criteria + +- [ ] loop(true) { x=1; break }; return x → returns 1 (not 0) +- [ ] VM and LLVM EXE both produce same result +- [ ] No regression in existing patterns +- [ ] Clean trace output (no PHI errors) +- [ ] Strict mode catches contract violations + +## Reference + +- Root Cause: `p1.5-root-cause-summary.md` +- Design Rationale: `p1.5-option-b-analysis.md` +- Phase 131 Overview: `README.md` diff --git a/docs/development/current/main/phases/phase-131/p1.5-option-b-analysis.md b/docs/development/current/main/phases/phase-131/p1.5-option-b-analysis.md new file mode 100644 index 00000000..a925381a --- /dev/null +++ b/docs/development/current/main/phases/phase-131/p1.5-option-b-analysis.md @@ -0,0 +1,273 @@ +# Phase 131 P1.5: Option B Analysis (Normalized Exit Reconnection) + +Status: Analysis +Scope: Phase 131 P1.5 root cause analysis and Option B design +Related: +- Phase 131 README: `docs/development/current/main/phases/phase-131/README.md` +- 10-Now.md: `docs/development/current/main/10-Now.md` + +## Current Problem + +### Symptom + +``` +[ERROR] ❌ [rust-vm] VM error: Invalid instruction: phi pred mismatch at ValueId(10): +no input for predecessor BasicBlockId(42) +``` + +### Root Cause + +**The problem is NOT with ExitMeta creation or boundary wiring.** The trace shows: + +1. ✅ **ExitMeta created correctly**: `x` → `ValueId(6)` (k_exit parameter) +2. ✅ **ExitMetaCollector works**: Creates binding `x: join ValueId(6) → host ValueId(2)` +3. ✅ **variable_map updated**: `variable_map['x'] ValueId(2) → ValueId(10)` +4. ❌ **PHI has wrong predecessors**: `ValueId(10) = phi [(BasicBlockId(39), ValueId(4))]` + - Expected: `BasicBlockId(42)` + - Actual: Only has `BasicBlockId(39)` + +**The problem**: The existing merge pipeline **assumes a standard loop CFG structure** with multiple predecessors (loop header, exit branches). But Normalized IR uses **tail-call continuations** instead, so the CFG structure is different: + +``` +Standard Loop (Pattern 1-4): + header → body → latch → header (loop back edge) + ↓ + exit (break edge) + → Exit PHI needs inputs from both paths + +Normalized loop(true) break-once: + main → loop_step → loop_body → k_exit → (return) + All connections are tail-calls, no loop back edge! + → Exit PHI generation logic doesn't match this structure +``` + +## Why Option B? + +### Option A (Fix PHI generation) + +- **Problem**: Normalized IR's CFG structure is fundamentally different from standard loops +- **Risk**: Trying to generate correct PHI nodes requires deep understanding of: + - Which blocks are predecessors in the tail-call CFG + - How to map k_exit parameters to PHI inputs + - Edge cases with conditional jumps in Normalized IR +- **Maintenance**: Every change to Normalized IR structure might break PHI generation + +### Option B (Direct env→host wiring, PHI-free) + +- **Principle**: **Normalized IR's k_exit env parameters ARE the final values (SSOT)** +- **Implementation**: Simply copy k_exit env params to host variable_map, no PHI needed +- **Why it works**: + ``` + k_exit(env) receives the final environment state + env.x = final value of x + → Just write env.x to host variable_map["x"] + ``` +- **Advantages**: + - Respects Normalized IR's **PHI-free** design philosophy + - Simpler: No CFG predecessor analysis needed + - Maintainable: Works regardless of Normalized IR structure changes + - SSOT: k_exit parameters are the canonical source of truth + +## Option B Design + +### Contract + +**For Normalized IR only:** +- k_exit env parameters represent final variable values (SSOT) +- No PHI generation for exit points +- Direct Copy instructions: `host_var = k_exit_param` + +### Implementation Plan + +#### 1. New Box: `ExitReconnectorBox` + +**Location**: `src/mir/control_tree/normalized_shadow/exit_reconnector.rs` + +**Responsibility**: Generate direct Copy instructions from k_exit params to host variable_map + +**Input**: +- `env_layout.writes`: List of variables written in loop +- `k_exit_params`: Vec (k_exit function parameters) +- `exit_values`: Vec<(String, ValueId)> (from ExitMeta) + +**Output**: +- `HostExitBindings`: Vec<(String, ValueId)> or BTreeMap + +**Algorithm**: +```rust +for (var_name, k_exit_param_vid) in exit_values { + // k_exit_param_vid is the SSOT final value + host_exit_bindings.insert(var_name, k_exit_param_vid); +} +``` + +#### 2. Routing Change (`routing.rs`) + +**Current** (lines 452-480): +```rust +// Uses ExitMetaCollector → creates PHI → variable_map update +let exit_bindings = ExitMetaCollector::collect(...); +let boundary = JoinInlineBoundaryBuilder::new() + .with_exit_bindings(exit_bindings) + .build(); +let exit_phi_result = JoinIRConversionPipeline::execute(...); +``` + +**New** (Normalized path only): +```rust +// Normalized-specific path: bypass PHI generation +if is_normalized_shadow { + // Use ExitReconnectorBox to wire env→host directly + let host_bindings = ExitReconnectorBox::reconnect( + self, + &join_meta.exit_meta, + &env_layout, + )?; + + // Update variable_map directly (no PHI) + for (var_name, final_value) in host_bindings { + self.variable_ctx.variable_map.insert(var_name, final_value); + } + + // Return void (or final value if needed) + Ok(Some(void_value)) +} else { + // Standard path: use existing merge pipeline (with PHI) + let exit_bindings = ExitMetaCollector::collect(...); + // ... existing code ... +} +``` + +#### 3. Verifier (strict mode) + +**Check**: All variables in `env_layout.writes` exist in k_exit parameters + +```rust +if strict_enabled() { + for var in &env_layout.writes { + if !k_exit_env.contains_key(var) { + freeze_with_hint( + "phase131/exit_reconnect/missing", + &format!("Variable '{}' in writes but not in k_exit env", var), + "Ensure env_layout.writes matches k_exit parameter list (SSOT)" + ); + } + } +} +``` + +### Why This Bypasses PHI Generation + +The key insight: **Normalized IR uses continuation parameters, not PHI nodes, for control flow merge** + +``` +Standard loop (needs PHI): + x = 0 + loop_header: + x_phi = phi [x_init, x_next] ← merge from 2 paths + ... + +Normalized loop (PHI-free): + main(env) → loop_step(env) → loop_body(env) → k_exit(env) + k_exit receives env.x as parameter ← SSOT, no merge needed +``` + +## Testing Strategy + +### Unit Tests + +1. **ExitReconnectorBox::reconnect()** + - Empty writes → empty bindings + - Single write → single binding + - Multiple writes → multiple bindings (order preserved) + - Missing variable in k_exit → strict mode error + +### Integration Tests + +1. **phase131_loop_true_break_once_min.hako** + - Expected: RC=1 (return value) + - Verifies: x=0; loop { x=1; break }; return x → 1 + +2. **Regression** + - phase130_if_only_post_if_add_vm.sh (if-only patterns still work) + - phase97_next_non_ws_llvm_exe.sh (existing JoinIR patterns) + +### Smoke Test Updates + +**Current failure**: +```bash +$ bash tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh +[FAIL] phi pred mismatch at ValueId(10) +``` + +**Expected after fix**: +```bash +$ bash tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh +[PASS] Output verified: 1 (exit code: 1) +``` + +## Implementation Checklist + +- [ ] Create `exit_reconnector.rs` with ExitReconnectorBox +- [ ] Update `routing.rs` to detect Normalized shadow path +- [ ] Implement direct env→host wiring (bypass merge pipeline) +- [ ] Add strict mode verifier +- [ ] Update unit tests +- [ ] Verify phase131 VM smoke +- [ ] Verify phase131 LLVM EXE smoke +- [ ] Run regression smokes (phase130, phase97) + +## Commit Plan + +1. `feat(normalized): Phase 131 P1.5 ExitReconnectorBox for host variable propagation` + - Add exit_reconnector.rs + - Pure function design (env_layout + k_exit params → host bindings) + +2. `fix(joinir/dev): bypass PHI generation for Normalized path` + - Update routing.rs to detect Normalized shadow + - Wire ExitReconnectorBox instead of merge pipeline + +3. `test(joinir): Phase 131 P1.5 enforce return parity (VM + LLVM EXE)` + - Enable VM/LLVM smoke tests + - Verify regression tests + +4. `docs: Phase 131 P1.5 DONE` + - Update 10-Now.md + - Update phase-131/README.md + +## Key Principles (SSOT) + +1. **PHI禁止維持**: Normalized IR never generates PHI nodes (env parameters are SSOT) +2. **既定挙動不変**: Standard loop patterns (Pattern 1-4) use existing merge pipeline +3. **責務分離**: ExitReconnectorBox handles Normalized-specific wiring +4. **箱化モジュール化**: New reconnection logic in dedicated box, not scattered +5. **Fail-Fast**: Strict mode catches contract violations immediately + +## Alternative Considered (Rejected) + +### Option A: Fix PHI Generation for Normalized IR + +**Rejected because**: +- Violates Normalized IR's PHI-free design philosophy +- Requires complex CFG analysis for tail-call structure +- High maintenance burden (CFG structure changes break it) +- Not SSOT (k_exit parameters ARE the truth, why generate PHI?) + +### Hybrid: PHI for some cases, direct wiring for others + +**Rejected because**: +- Adds complexity (two code paths) +- Hard to reason about when each applies +- No clear benefit over pure Option B + +## Conclusion + +**Option B is the correct choice** because: + +1. **Respects Normalized IR design**: PHI-free continuations +2. **SSOT principle**: k_exit env parameters are the canonical final values +3. **Simplicity**: Direct wiring, no CFG predecessor analysis +4. **Maintainability**: Isolated in ExitReconnectorBox, doesn't touch merge pipeline +5. **Fail-Fast**: Strict mode verifier catches contract violations + +The root cause was **not** a missing ExitMeta or boundary creation issue. It was **using the wrong merge strategy** (PHI-based) for a control flow structure (continuation-based) that doesn't need it. diff --git a/docs/development/current/main/phases/phase-131/p1.5-root-cause-summary.md b/docs/development/current/main/phases/phase-131/p1.5-root-cause-summary.md new file mode 100644 index 00000000..c09a02a5 --- /dev/null +++ b/docs/development/current/main/phases/phase-131/p1.5-root-cause-summary.md @@ -0,0 +1,228 @@ +# Phase 131 P1.5: Root Cause Analysis Summary + +Status: Analysis Complete +Date: 2025-12-18 +Scope: Phase 131 P1.5 variable propagation failure diagnosis + +## TL;DR + +**Problem**: loop(true) { x=1; break }; return x → returns 0 instead of 1 + +**Root Cause**: Using PHI-based merge pipeline for Normalized IR's continuation-based control flow + +**Solution**: Option B - Direct env→host wiring via ExitReconnectorBox (PHI-free, matches Normalized design) + +## Investigation Timeline + +### Step 1: Verify ExitMeta Creation ✅ + +**Location**: `src/mir/control_tree/normalized_shadow/loop_true_break_once.rs:317-327` + +**Code**: +```rust +let mut exit_values = Vec::new(); +for (i, var_name) in env_layout.writes.iter().enumerate() { + let k_exit_param_vid = k_exit_func.params[i]; + exit_values.push((var_name.clone(), k_exit_param_vid)); +} +let exit_meta = ExitMeta { exit_values }; +``` + +**Trace Evidence**: +``` +[trace:exit-line] collector: Collecting 1 exit values +[trace:exit-line] collector: checking carrier 'x' in variable_ctx.variable_map +[trace:exit-line] collector: collected 'x' JoinIR ValueId(6) → HOST ValueId(2), role=LoopState +``` + +**Conclusion**: ✅ ExitMeta is created correctly. `x` → `ValueId(6)` (k_exit param) is captured. + +### Step 2: Verify ExitMetaCollector ✅ + +**Location**: `src/mir/builder/control_flow/joinir/merge/exit_line/meta_collector.rs:77-261` + +**Trace Evidence**: +``` +[trace:exit-line] collector: collected 1 bindings: [LoopExitBinding { + carrier_name: "x", + join_exit_value: ValueId(6), + host_slot: ValueId(2), + role: LoopState +}] +``` + +**Conclusion**: ✅ ExitMetaCollector works correctly. Boundary is created with proper mapping. + +### Step 3: Verify variable_map Update ✅ + +**Trace Evidence**: +``` +[joinir/exit-line] variable_ctx.variable_map['x'] ValueId(2) → ValueId(10) +``` + +**Conclusion**: ✅ variable_map is updated with exit PHI result. + +### Step 4: Identify PHI Mismatch ❌ + +**Trace Evidence**: +``` +[DEBUG-177] Exit block PHI (carrier 'x'): + ValueId(10) = phi [(BasicBlockId(39), ValueId(4))] + +[ERROR] ❌ [rust-vm] VM error: Invalid instruction: +phi pred mismatch at ValueId(10): no input for predecessor BasicBlockId(42) +``` + +**Conclusion**: ❌ **PHI node has wrong predecessors** +- Has input from: BasicBlockId(39) +- Expected input from: BasicBlockId(42) +- This is a **CFG structure mismatch** + +## Root Cause: CFG Structure Incompatibility + +### Standard Loop CFG (Pattern 1-4) + +``` +entry + ↓ +header ←─────┐ + ├─→ body │ + │ ↓ │ + │ latch ──┘ (loop back edge) + ↓ +exit + ↓ + PHI = merge(break_edge, fallthrough) +``` + +**PHI needs inputs from**: +- Break edge from loop body +- Fallthrough from header (condition false) + +### Normalized IR CFG (Continuation-based) + +``` +main(env) + ↓ TailCall +loop_step(env) + ├─→ (true) TailCall → loop_body(env) + │ ↓ TailCall + ↓ k_exit(env) ──→ return env.x + └─→ (false) TailCall → k_exit(env) ──→ return env.x +``` + +**No loop back edge!** All connections are tail-calls. + +**k_exit receives env as parameter** - this IS the final value (SSOT), no merging needed! + +## Why PHI Generation Fails + +The merge pipeline (`JoinIRConversionPipeline`) assumes: +1. Loop has a header with PHI nodes (for loop variables) +2. Exit block needs PHI to merge values from multiple loop exits +3. CFG has loop back edges and break edges + +**But Normalized IR**: +1. No header PHI (env parameters are passed via tail-calls) +2. k_exit env parameter IS the final value (no merge needed) +3. No loop back edges (only forward tail-calls) + +**The PHI generation logic doesn't match Normalized's control flow structure.** + +## Why Option B is Correct + +### Option B: Direct env→host Wiring (PHI-free) + +**Principle**: k_exit env parameters are SSOT for final values + +**Implementation**: +```rust +// For Normalized IR only +for (var_name, k_exit_param_vid) in exit_meta.exit_values { + // k_exit_param_vid is already the final value! + variable_map.insert(var_name, k_exit_param_vid); +} +// No PHI generation needed +``` + +**Why it works**: +1. **Matches Normalized design**: PHI-free continuations +2. **SSOT**: k_exit parameters are the canonical final values +3. **Simple**: No CFG analysis, no predecessor tracking +4. **Maintainable**: Works regardless of Normalized IR structure + +### Option A: Fix PHI Generation (Rejected) + +**Would require**: +1. Analyze Normalized IR's tail-call CFG structure +2. Map k_exit parameters to PHI inputs +3. Handle edge cases (conditional jumps, multiple exits) +4. Keep in sync with Normalized IR structure changes + +**Problems**: +1. **Violates PHI-free design**: Normalized IR shouldn't generate PHI +2. **Complex**: CFG analysis for continuation-based control flow +3. **Fragile**: Breaks when Normalized IR structure changes +4. **Wrong abstraction**: k_exit params ARE the truth, why create PHI? + +## Solution: ExitReconnectorBox + +### Responsibility + +Generate direct Copy instructions from k_exit env params to host variable_map (Normalized IR only) + +### Contract + +**Input**: +- `env_layout.writes`: Variables written in loop +- `k_exit_params`: k_exit function parameters (SSOT for final values) +- `exit_values`: Mapping from ExitMeta + +**Output**: +- Direct variable_map updates (no PHI) + +**Invariant**: +- Only used for Normalized shadow path +- Standard loops (Pattern 1-4) continue using existing merge pipeline + +### Location + +`src/mir/control_tree/normalized_shadow/exit_reconnector.rs` + +**Rationale**: Normalized-specific logic stays in normalized_shadow/ module + +## Verification Strategy + +### Unit Tests + +1. ExitReconnectorBox::reconnect() basic cases +2. Strict mode catches missing variables +3. Empty writes → empty bindings + +### Integration Tests + +1. phase131_loop_true_break_once_vm.sh → RC=1 (expected) +2. phase131_loop_true_break_once_llvm_exe.sh → RC=1 +3. Regression: phase130, phase97 (既定挙動不変) + +## Key Insights + +1. **ExitMeta creation was NOT the problem** - it was already working correctly +2. **Boundary creation was NOT the problem** - ExitMetaCollector worked correctly +3. **The problem was using PHI-based merge** for a continuation-based control flow +4. **k_exit env parameters are SSOT** - no merging/PHI needed +5. **Option B respects Normalized IR's PHI-free design** - the right abstraction + +## Next Steps + +See `p1.5-option-b-analysis.md` for: +- Detailed implementation plan +- Testing strategy +- Commit sequence +- Fail-Fast verifier design + +## Related Documents + +- Phase 131 README: `docs/development/current/main/phases/phase-131/README.md` +- Option B Analysis: `docs/development/current/main/phases/phase-131/p1.5-option-b-analysis.md` +- Now: `docs/development/current/main/10-Now.md` diff --git a/src/mir/control_tree/normalized_shadow/exit_reconnector.rs b/src/mir/control_tree/normalized_shadow/exit_reconnector.rs new file mode 100644 index 00000000..5709a0f1 --- /dev/null +++ b/src/mir/control_tree/normalized_shadow/exit_reconnector.rs @@ -0,0 +1,229 @@ +//! Phase 131 P1.5: ExitReconnectorBox +//! +//! Option B implementation: Direct variable_map reconnection for Normalized shadow +//! +//! ## Purpose +//! +//! Normalized IR uses k_exit env params as SSOT for exit values. +//! This box reconnects those exit values directly to host's variable_map, +//! bypassing the traditional PHI-based merge pipeline. +//! +//! ## Why Option B? +//! +//! **Problem**: Normalized IR's exit values are passed via k_exit function params, +//! while the traditional merge pipeline expects ExitMeta → exit_bindings → PHI. +//! The two approaches are incompatible because: +//! +//! 1. Normalized k_exit params are ALREADY the final exit values (no PHI needed) +//! 2. Traditional pipeline generates PHI nodes to merge exit values +//! 3. Mixing the two creates duplicate/incorrect PHI generation +//! +//! **Solution**: Direct reconnection for Normalized shadow only: +//! - Skip traditional merge pipeline's exit PHI generation +//! - Use ExitReconnectorBox to update variable_map directly +//! - Maintain separation between Normalized and traditional paths +//! +//! ## Contract +//! +//! **Input**: +//! - `exit_values`: Vec<(String, ValueId)> from jump args to k_exit (after merge/remap) +//! - Variable names are the carrier names (e.g., "i", "sum", "count") +//! - ValueIds are the actual computed values passed to k_exit (host ValueIds) +//! - `variable_map`: &mut BTreeMap from MirBuilder +//! +//! **Effect**: +//! - Updates variable_map entries for each carrier with the jump arg ValueId +//! - This makes the exit values available to post-loop code +//! +//! **Output**: +//! - None (side effect only: variable_map mutation) +//! +//! ## Example +//! +//! ```text +//! Before reconnection: +//! variable_map = { "i" => ValueId(10) } // pre-loop value +//! +//! After loop merge: +//! Jump to k_exit with args [ValueId(42)] // computed exit value +//! +//! After ExitReconnectorBox::reconnect(): +//! variable_map = { "i" => ValueId(42) } // jump arg is now SSOT +//! ``` +//! +//! ## Design Notes +//! +//! - **Pure function**: No complex logic, just map update +//! - **No PHI generation**: k_exit params ARE the exit values +//! - **Normalized-specific**: Only used for Normalized shadow path +//! - **Fail-Fast**: Panics if carrier not in variable_map (contract violation) + +use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; +use std::collections::BTreeMap; + +/// ExitReconnectorBox: Direct variable_map reconnection for Normalized shadow +pub struct ExitReconnectorBox; + +impl ExitReconnectorBox { + /// Phase 131 P1.5: DEPRECATED - No longer used + /// + /// This function was used to extract k_exit jump args before the boundary approach. + /// Now we use MergeResult.remapped_exit_values instead (SSOT: merge owns remapper). + #[allow(dead_code)] + #[deprecated(note = "Use MergeResult.remapped_exit_values instead")] + pub fn extract_k_exit_jump_args( + _func: &MirFunction, + _exit_block: BasicBlockId, + ) -> Option> { + // Deprecated - boundary approach with remapped_exit_values is used instead + None + } + + // ORIGINAL extract_k_exit_jump_args (commented out due to MIR structure changes) + /* + pub fn extract_k_exit_jump_args_old( + func: &MirFunction, + exit_block: BasicBlockId, + ) -> Option> { + let verbose = crate::config::env::joinir_dev_enabled(); + + // (Old implementation commented out - see MergeResult.remapped_exit_values instead) + None + } + */ +} + +impl ExitReconnectorBox { + /// Reconnect k_exit env params to host variable_map + /// + /// # Algorithm + /// + /// For each (carrier_name, k_exit_param_vid) in exit_values: + /// 1. Look up carrier_name in variable_map + /// 2. Update variable_map[carrier_name] = k_exit_param_vid + /// + /// # Panics + /// + /// Panics if carrier_name is not in variable_map, as this indicates + /// a contract violation (Normalized lowering should only emit carriers + /// that exist in host's variable scope). + /// + /// # Phase 131 P1.5: Normalized-specific design + /// + /// This is ONLY for Normalized shadow path. Traditional patterns use + /// the standard merge pipeline with PHI generation. + pub fn reconnect( + exit_values: &[(String, ValueId)], + variable_map: &mut BTreeMap, + ) { + let verbose = crate::config::env::joinir_dev_enabled(); + + if verbose { + eprintln!( + "[normalized/exit-reconnect] Reconnecting {} exit values to variable_map", + exit_values.len() + ); + } + + for (var_name, k_exit_param_vid) in exit_values { + if verbose { + eprintln!( + "[normalized/exit-reconnect] Checking '{}' in variable_map", + var_name + ); + } + + // Phase 131 P1.5: variable_map MUST contain the carrier + // (Normalized lowering guarantees this via AvailableInputsCollectorBox) + if !variable_map.contains_key(var_name) { + panic!( + "[ExitReconnectorBox] Carrier '{}' not in variable_map. \ + This is a contract violation: Normalized lowering should only \ + emit carriers that exist in host scope. \ + Available carriers: {:?}", + var_name, + variable_map.keys().collect::>() + ); + } + + // Update variable_map: old host ValueId → k_exit param ValueId + let old_vid = variable_map[var_name]; + variable_map.insert(var_name.clone(), *k_exit_param_vid); + + if verbose { + eprintln!( + "[normalized/exit-reconnect] Reconnected '{}': {:?} → {:?}", + var_name, old_vid, k_exit_param_vid + ); + } + } + + if verbose { + eprintln!( + "[normalized/exit-reconnect] Reconnection complete. Updated {} carriers", + exit_values.len() + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mir::ValueId; + use std::collections::BTreeMap; + + #[test] + fn test_reconnect_single_carrier() { + let mut variable_map = BTreeMap::new(); + variable_map.insert("i".to_string(), ValueId(10)); + + let exit_values = vec![("i".to_string(), ValueId(100))]; + + ExitReconnectorBox::reconnect(&exit_values, &mut variable_map); + + assert_eq!(variable_map.get("i"), Some(&ValueId(100))); + } + + #[test] + fn test_reconnect_multiple_carriers() { + let mut variable_map = BTreeMap::new(); + variable_map.insert("sum".to_string(), ValueId(20)); + variable_map.insert("count".to_string(), ValueId(30)); + + let exit_values = vec![ + ("sum".to_string(), ValueId(200)), + ("count".to_string(), ValueId(300)), + ]; + + ExitReconnectorBox::reconnect(&exit_values, &mut variable_map); + + assert_eq!(variable_map.get("sum"), Some(&ValueId(200))); + assert_eq!(variable_map.get("count"), Some(&ValueId(300))); + } + + #[test] + #[should_panic(expected = "Carrier 'x' not in variable_map")] + fn test_reconnect_missing_carrier_panics() { + let mut variable_map = BTreeMap::new(); + variable_map.insert("i".to_string(), ValueId(10)); + + let exit_values = vec![("x".to_string(), ValueId(999))]; + + // This should panic because "x" is not in variable_map + ExitReconnectorBox::reconnect(&exit_values, &mut variable_map); + } + + #[test] + fn test_reconnect_empty_exit_values() { + let mut variable_map = BTreeMap::new(); + variable_map.insert("i".to_string(), ValueId(10)); + + let exit_values = vec![]; + + ExitReconnectorBox::reconnect(&exit_values, &mut variable_map); + + // variable_map should be unchanged + assert_eq!(variable_map.get("i"), Some(&ValueId(10))); + } +}