Files
hakorune/docs/development/current/main/phases/phase-131/p1.5-option-b-analysis.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

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:

  1. ExitMeta created correctly: xValueId(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<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

  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 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.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.