docs: Phase 131 P1.5 DirectValue exit reconnection design

Add design documents for Phase 131 P1.5 DirectValue mode:
- Root cause analysis of PHI-based exit merge assumptions
- Option B (DirectValue) analysis and trade-offs
- Implementation guide for exit value reconnection

Also add exit_reconnector.rs module stub for future extraction.

Related:
- Phase 131: loop(true) break-once Normalized support
- Normalized shadow path uses continuations, not PHI
- Exit values reconnect directly to host variable_map

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-18 17:47:45 +09:00
parent bd39e09c5d
commit bfac188732
4 changed files with 1052 additions and 0 deletions

View File

@ -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`

View File

@ -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<ValueId> (k_exit function parameters)
- `exit_values`: Vec<(String, ValueId)> (from ExitMeta)
**Output**:
- `HostExitBindings`: Vec<(String, ValueId)> or BTreeMap<String, ValueId>
**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.

View File

@ -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`

View File

@ -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<String, ValueId> 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<Vec<ValueId>> {
// 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<Vec<ValueId>> {
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<String, ValueId>,
) {
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::<Vec<_>>()
);
}
// 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)));
}
}