274 lines
8.9 KiB
Markdown
274 lines
8.9 KiB
Markdown
|
|
# 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.
|