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:
nyash-codex
2025-12-18 17:47:45 +09:00
parent bd39e09c5d
commit bfac188732
4 changed files with 1052 additions and 0 deletions

View 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)));
}
}