Files
hakorune/docs/development/current/main/phases/phase-131/p1.5-root-cause-summary.md
nyash-codex bfac188732 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>
2025-12-18 17:47:45 +09:00

6.6 KiB

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:

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. xValueId(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:

// 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
  • 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