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:
@ -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`
|
||||||
@ -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.
|
||||||
@ -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`
|
||||||
229
src/mir/control_tree/normalized_shadow/exit_reconnector.rs
Normal file
229
src/mir/control_tree/normalized_shadow/exit_reconnector.rs
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user