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>
8.9 KiB
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:
- ✅ ExitMeta created correctly:
x→ValueId(6)(k_exit parameter) - ✅ ExitMetaCollector works: Creates binding
x: join ValueId(6) → host ValueId(2) - ✅ variable_map updated:
variable_map['x'] ValueId(2) → ValueId(10) - ❌ PHI has wrong predecessors:
ValueId(10) = phi [(BasicBlockId(39), ValueId(4))]- Expected:
BasicBlockId(42) - Actual: Only has
BasicBlockId(39)
- Expected:
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 loopk_exit_params: Vec (k_exit function parameters)exit_values: Vec<(String, ValueId)> (from ExitMeta)
Output:
HostExitBindings: Vec<(String, ValueId)> or BTreeMap<String, ValueId>
Algorithm:
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):
// 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):
// 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
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
- 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
-
phase131_loop_true_break_once_min.hako
- Expected: RC=1 (return value)
- Verifies: x=0; loop { x=1; break }; return x → 1
-
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 tools/smokes/v2/profiles/integration/apps/phase131_loop_true_break_once_vm.sh
[FAIL] phi pred mismatch at ValueId(10)
Expected after fix:
$ 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.rswith ExitReconnectorBox - Update
routing.rsto 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
-
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)
-
fix(joinir/dev): bypass PHI generation for Normalized path- Update routing.rs to detect Normalized shadow
- Wire ExitReconnectorBox instead of merge pipeline
-
test(joinir): Phase 131 P1.5 enforce return parity (VM + LLVM EXE)- Enable VM/LLVM smoke tests
- Verify regression tests
-
docs: Phase 131 P1.5 DONE- Update 10-Now.md
- Update phase-131/README.md
Key Principles (SSOT)
- PHI禁止維持: Normalized IR never generates PHI nodes (env parameters are SSOT)
- 既定挙動不変: Standard loop patterns (Pattern 1-4) use existing merge pipeline
- 責務分離: ExitReconnectorBox handles Normalized-specific wiring
- 箱化モジュール化: New reconnection logic in dedicated box, not scattered
- 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:
- Respects Normalized IR design: PHI-free continuations
- SSOT principle: k_exit env parameters are the canonical final values
- Simplicity: Direct wiring, no CFG predecessor analysis
- Maintainability: Isolated in ExitReconnectorBox, doesn't touch merge pipeline
- 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.