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