Files
hakorune/src/mir/join_ir/lowering/carrier_info.rs
nyash-codex 33f03d9775 refactor(joinir): Phase 86 - Carrier init builder, debug migration, error tags
P1: Carrier Initialization Builder (HIGH) 
- New module: carrier_init_builder.rs (197 lines, 8 tests)
- Refactored loop_header_phi_builder.rs (-34 lines)
- Centralized CarrierInit value generation (SSOT)
- Eliminates scattered match patterns across header PHI, exit line
- Consistent debug output: [carrier_init_builder] format
- Net: -34 lines of duplicated logic

P2: Remaining DebugOutputBox Migration (QUICK) 
- Migrated carrier_info.rs::record_promoted_binding()
- Uses DebugOutputBox for JOINIR_DEBUG checks
- Maintains JOINIR_TEST_DEBUG override for test diagnostics
- Consistent log formatting: [context/category] message
- Net: +3 lines (SSOT migration)

P3: Error Message Centralization (LOW) 
- New module: error_tags.rs (136 lines, 5 tests)
- Migrated 3 error sites:
  * ownership/relay:runtime_unsupported (plan_validator.rs)
  * joinir/freeze (control_flow/mod.rs)
  * (ExitLine errors were debug messages, not returns)
- Centralized error tag generation (freeze, exit_line_contract, ownership_relay_unsupported, etc.)
- Net: +133 lines (SSOT module + tests)

Total changes:
- New files: carrier_init_builder.rs (197), error_tags.rs (136)
- Modified: 6 files
- Production code: +162 lines (SSOT investment)
- Tests: 987/987 PASS (982→987, +5 new tests)
- Phase 81 ExitLine: 2/2 PASS
- Zero compilation errors/warnings

Benefits:
 Single Responsibility: Each helper has one concern
 Testability: 13 new unit tests (8 carrier init, 5 error tags)
 Consistency: Uniform debug/error formatting
 SSOT: Centralized CarrierInit and error tag generation
 Discoverability: Easy to find all error types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 21:48:02 +09:00

1114 lines
40 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Carrier variable metadata for JoinIR loop lowering
//!
//! This module defines metadata structures for tracking carrier variables
//! in loop lowering. This enables dynamic generation of exit bindings
//! without hardcoded variable names or ValueIds.
//!
//! Phase 193-2: Enhanced builder methods for flexible construction
//!
//! # Phase 183-2: Primary CarrierInfo Construction
//!
//! This module is the single source of truth for CarrierInfo initialization.
//! Both MIR and JoinIR contexts use `CarrierInfo::from_variable_map()` as the
//! primary construction method.
//!
//! - MIR context: `common_init.rs` delegates to this module
//! - JoinIR context: Uses `from_variable_map()` directly
//!
//! # Phase 76: BindingId-Based Promotion Tracking
//!
//! Replaces name-based promotion hacks (`"digit_pos"` → `"is_digit_pos"`) with
//! type-safe BindingId mapping. This eliminates fragile string matching while
//! maintaining backward compatibility through dual-path lookup.
use crate::mir::ValueId;
use std::collections::BTreeMap; // Phase 222.5-D: HashMap → BTreeMap for determinism
#[cfg(feature = "normalized_dev")]
use crate::mir::BindingId; // Phase 76+78: BindingId for promoted carriers
/// Phase 227: CarrierRole - Distinguishes loop state carriers from condition-only carriers
///
/// When LoopBodyLocal variables are promoted to carriers, we need to know whether
/// they carry loop state (need exit PHI) or are only used in conditions (no exit PHI).
///
/// # Example
///
/// ```ignore
/// // LoopState carrier: sum needs exit PHI (value persists after loop)
/// loop(i < n) {
/// sum = sum + i; // sum updated in loop body
/// }
/// print(sum); // sum used after loop
///
/// // ConditionOnly carrier: is_digit_pos does NOT need exit PHI
/// loop(p < s.length()) {
/// local digit_pos = digits.indexOf(s.substring(p, p+1));
/// if digit_pos < 0 { break; } // Only used in condition
/// num_str = num_str + ch;
/// p = p + 1;
/// }
/// // digit_pos not used after loop
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierRole {
/// Value needed after loop (sum, result, count, p, num_str)
/// - Participates in header PHI (loop iteration)
/// - Participates in exit PHI (final value after loop)
LoopState,
/// Only used for loop condition (is_digit_pos, is_whitespace)
/// - Participates in header PHI (loop iteration)
/// - Does NOT participate in exit PHI (not needed after loop)
ConditionOnly,
}
/// Phase 228: Initialization policy for carrier variables
///
/// When carriers participate in header PHI, they need an initial value.
/// Most carriers use their host_id value (FromHost), but promoted LoopBodyLocal
/// carriers need explicit bool initialization (BoolConst).
///
/// # Example
///
/// ```ignore
/// // Regular carrier (sum): Use host_id value
/// CarrierVar { name: "sum", host_id: ValueId(10), init: FromHost, .. }
///
/// // ConditionOnly carrier (is_digit_pos): Initialize with false
/// CarrierVar { name: "is_digit_pos", host_id: ValueId(15), init: BoolConst(false), .. }
///
/// // Loop-local derived carrier (digit_value): Initialize with local zero (no host slot)
/// CarrierVar { name: "digit_value", host_id: ValueId(0), init: LoopLocalZero, .. }
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CarrierInit {
/// No explicit initialization (use host_id value)
FromHost,
/// Initialize with bool constant (for ConditionOnly carriers)
BoolConst(bool),
/// Initialize with loop-local zero (no host slot; used for derived carriers like digit_value)
LoopLocalZero,
}
// Phase 229: ConditionAlias removed - redundant with promoted_loopbodylocals
// The naming convention (old_name → "is_<old_name>" or "is_<old_name>_match")
// is sufficient to resolve promoted variables dynamically.
/// Information about a single carrier variable
#[derive(Debug, Clone)]
pub struct CarrierVar {
/// Variable name (e.g., "sum", "printed", "is_digit_pos")
pub name: String,
/// Host ValueId for this variable (MIR側)
pub host_id: ValueId,
/// Phase 177-STRUCT: JoinIR側でこのキャリアを表すValueId
///
/// ヘッダPHIのdstや、exitで使う値を記録する。
/// これにより、index ベースのマッチングを名前ベースに置き換えられる。
///
/// - `Some(vid)`: Header PHI生成後にセットされる
/// - `None`: まだPHI生成前、または該当なし
pub join_id: Option<ValueId>,
/// Phase 227: Role of this carrier (LoopState or ConditionOnly)
///
/// - `LoopState`: Value needed after loop (participates in exit PHI)
/// - `ConditionOnly`: Only used for loop condition (no exit PHI)
pub role: CarrierRole,
/// Phase 228: Initialization policy for header PHI
///
/// - `FromHost`: Use host_id value (regular carriers)
/// - `BoolConst(false)`: Initialize with false (promoted LoopBodyLocal carriers)
pub init: CarrierInit,
/// Phase 78: BindingId for this carrier (dev-only)
///
/// For promoted carriers (e.g., is_digit_pos), this is allocated separately
/// by CarrierBindingAssigner. For source-derived carriers, this comes from
/// builder.binding_map.
///
/// Enables type-safe lookup: BindingId → ValueId (join_id) in ConditionEnv.
///
/// # Example
///
/// ```ignore
/// // Source-derived carrier
/// CarrierVar {
/// name: "sum",
/// binding_id: Some(BindingId(5)), // from builder.binding_map["sum"]
/// ..
/// }
///
/// // Promoted carrier
/// CarrierVar {
/// name: "is_digit_pos",
/// binding_id: Some(BindingId(10)), // allocated by CarrierBindingAssigner
/// ..
/// }
/// ```
#[cfg(feature = "normalized_dev")]
pub binding_id: Option<BindingId>,
}
impl CarrierVar {
/// Create a new CarrierVar with default LoopState role
///
/// This is the primary constructor for CarrierVar. Use this instead of
/// struct literal syntax to ensure role defaults to LoopState.
pub fn new(name: String, host_id: ValueId) -> Self {
Self {
name,
host_id,
join_id: None,
role: CarrierRole::LoopState,
init: CarrierInit::FromHost, // Phase 228: Default to FromHost
#[cfg(feature = "normalized_dev")]
binding_id: None, // Phase 78: No BindingId by default
}
}
/// Create a CarrierVar with explicit role
pub fn with_role(name: String, host_id: ValueId, role: CarrierRole) -> Self {
Self {
name,
host_id,
join_id: None,
role,
init: CarrierInit::FromHost, // Phase 228: Default to FromHost
#[cfg(feature = "normalized_dev")]
binding_id: None, // Phase 78: No BindingId by default
}
}
/// Phase 228: Create a CarrierVar with explicit role and init policy
pub fn with_role_and_init(
name: String,
host_id: ValueId,
role: CarrierRole,
init: CarrierInit,
) -> Self {
Self {
name,
host_id,
join_id: None,
role,
init,
#[cfg(feature = "normalized_dev")]
binding_id: None, // Phase 78: No BindingId by default
}
}
}
/// Complete carrier information for a loop
#[derive(Debug, Clone)]
pub struct CarrierInfo {
/// Loop control variable name (e.g., "i")
pub loop_var_name: String,
/// Loop control variable ValueId in host
pub loop_var_id: ValueId,
/// Additional carrier variables (e.g., sum, printed)
pub carriers: Vec<CarrierVar>,
/// Phase 171-C-5: Trim pattern helper (if this CarrierInfo was created from Trim promotion)
pub trim_helper: Option<crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper>,
/// Phase 224: Promoted LoopBodyLocal variables (e.g., "digit_pos" promoted to "is_digit_pos")
///
/// These variables were originally LoopBodyLocal but have been promoted to carriers
/// during condition promotion (e.g., DigitPosPromoter). The lowerer should skip
/// LoopBodyLocal checks for these variables.
///
/// Phase 229: Naming convention for promoted carriers:
/// - DigitPos pattern: "var" → "is_var" (e.g., "digit_pos" → "is_digit_pos")
/// - Trim pattern: "var" → "is_var_match" (e.g., "ch" → "is_ch_match")
///
/// Condition variable resolution dynamically infers the carrier name from this list.
pub promoted_loopbodylocals: Vec<String>,
/// Phase 76: Type-safe promotion tracking (dev-only)
///
/// Maps original BindingId to promoted BindingId, eliminating name-based hacks.
///
/// # Example
///
/// DigitPos promotion:
/// - Original: BindingId(5) for `"digit_pos"`
/// - Promoted: BindingId(10) for `"is_digit_pos"`
/// - Map entry: `promoted_bindings[BindingId(5)] = BindingId(10)`
///
/// This enables type-safe resolution:
/// ```ignore
/// if let Some(promoted_bid) = carrier_info.promoted_bindings.get(&original_bid) {
/// // Lookup promoted carrier by BindingId (no string matching!)
/// }
/// ```
///
/// # Migration Strategy (Phase 76)
///
/// - **Dual Path**: BindingId lookup (NEW) OR name-based fallback (LEGACY)
/// - **Populated by**: DigitPosPromoter, TrimLoopHelper (Phase 76)
/// - **Used by**: ConditionEnv::resolve_var_with_binding (Phase 75+)
/// - **Phase 77**: Remove name-based fallback after full migration
///
/// # Design Notes
///
/// **Q: Why BindingId map instead of name map?**
/// - **Type Safety**: Compiler-checked binding identity (no typos)
/// - **Shadowing-Aware**: BindingId distinguishes inner/outer scope vars
/// - **No Name Collisions**: BindingId is unique even if names shadow
///
/// **Q: Why not remove `promoted_loopbodylocals` immediately?**
/// - **Legacy Compatibility**: Existing code uses name-based lookup
/// - **Gradual Migration**: Phase 76 adds BindingId, Phase 77 removes name-based
/// - **Fail-Safe**: Dual path ensures no regressions during transition
#[cfg(feature = "normalized_dev")]
pub promoted_bindings: BTreeMap<BindingId, BindingId>,
}
impl CarrierInfo {
/// Phase 193-2: Create CarrierInfo from a variable_map
///
/// Automatically extracts all non-loop-control variables from the host's
/// variable_map. This eliminates manual carrier listing for simple cases.
///
/// # Arguments
///
/// * `loop_var_name` - Name of the loop control variable (e.g., "i")
/// * `variable_map` - Host function's variable_map (String → ValueId)
///
/// # Returns
///
/// CarrierInfo with loop_var and all other variables as carriers
///
/// # Example
///
/// ```ignore
/// let carrier_info = CarrierInfo::from_variable_map(
/// "i".to_string(),
/// &variable_map // {"i": ValueId(5), "sum": ValueId(10), "count": ValueId(11)}
/// )?;
/// // Result: CarrierInfo with loop_var="i", carriers=[sum, count]
/// ```
pub fn from_variable_map(
loop_var_name: String,
variable_map: &BTreeMap<String, ValueId>, // Phase 222.5-D: HashMap → BTreeMap for determinism
) -> Result<Self, String> {
// Find loop variable
let loop_var_id = variable_map.get(&loop_var_name).copied().ok_or_else(|| {
format!(
"Loop variable '{}' not found in variable_map",
loop_var_name
)
})?;
// Collect all non-loop-var variables as carriers
let mut carriers: Vec<CarrierVar> = variable_map
.iter()
.filter(|(name, _)| *name != &loop_var_name)
.map(|(name, &id)| CarrierVar {
name: name.clone(),
host_id: id,
join_id: None, // Phase 177-STRUCT-1: Set by header PHI generation
role: CarrierRole::LoopState, // Phase 227: Default to LoopState
init: CarrierInit::FromHost, // Phase 228: Default to FromHost
#[cfg(feature = "normalized_dev")]
binding_id: None, // Phase 78: Set by CarrierBindingAssigner
})
.collect();
// Sort for determinism
carriers.sort_by(|a, b| a.name.cmp(&b.name));
Ok(CarrierInfo {
loop_var_name,
loop_var_id,
carriers,
trim_helper: None, // Phase 171-C-5: No Trim pattern by default
promoted_loopbodylocals: Vec::new(), // Phase 224: No promoted variables by default
#[cfg(feature = "normalized_dev")]
promoted_bindings: BTreeMap::new(), // Phase 76: No promoted bindings by default
})
}
/// Phase 193-2: Create CarrierInfo with explicit carrier list
///
/// Useful when you have specific carriers in mind and want explicit control
/// over which variables are treated as carriers.
///
/// # Arguments
///
/// * `loop_var_name` - Name of the loop control variable
/// * `loop_var_id` - ValueId of the loop variable
/// * `carrier_names` - Names of carrier variables (will look up in variable_map)
/// * `variable_map` - Host function's variable_map for lookups
///
/// # Returns
///
/// CarrierInfo with only the specified carriers
///
/// # Example
///
/// ```ignore
/// let carrier_info = CarrierInfo::with_explicit_carriers(
/// "i".to_string(),
/// ValueId(5),
/// vec!["sum".to_string(), "count".to_string()],
/// &variable_map
/// )?;
/// ```
pub fn with_explicit_carriers(
loop_var_name: String,
loop_var_id: ValueId,
carrier_names: Vec<String>,
variable_map: &BTreeMap<String, ValueId>, // Phase 222.5-D: HashMap → BTreeMap for determinism
) -> Result<Self, String> {
let mut carriers = Vec::new();
for name in carrier_names {
let host_id = variable_map
.get(&name)
.copied()
.ok_or_else(|| format!("Carrier variable '{}' not found in variable_map", name))?;
carriers.push(CarrierVar {
name,
host_id,
join_id: None, // Phase 177-STRUCT-1: Set by header PHI generation
role: CarrierRole::LoopState, // Phase 227: Default to LoopState
init: CarrierInit::FromHost, // Phase 228: Default to FromHost
#[cfg(feature = "normalized_dev")]
binding_id: None, // Phase 78: Set by CarrierBindingAssigner
});
}
// Sort for determinism
carriers.sort_by(|a, b| a.name.cmp(&b.name));
Ok(CarrierInfo {
loop_var_name,
loop_var_id,
carriers,
trim_helper: None, // Phase 171-C-5: No Trim pattern by default
promoted_loopbodylocals: Vec::new(), // Phase 224: No promoted variables by default
#[cfg(feature = "normalized_dev")]
promoted_bindings: BTreeMap::new(), // Phase 76: No promoted bindings by default
})
}
/// Phase 193-2: Create CarrierInfo with manual CarrierVar list
///
/// Most explicit construction method - you provide everything directly.
/// Useful when you already have CarrierVar structs built elsewhere.
///
/// # Arguments
///
/// * `loop_var_name` - Name of the loop control variable
/// * `loop_var_id` - ValueId of the loop variable
/// * `carriers` - Vec of already-constructed CarrierVar structs
pub fn with_carriers(
loop_var_name: String,
loop_var_id: ValueId,
mut carriers: Vec<CarrierVar>,
) -> Self {
// Sort for determinism
carriers.sort_by(|a, b| a.name.cmp(&b.name));
Self {
loop_var_name,
loop_var_id,
carriers,
trim_helper: None, // Phase 171-C-5: No Trim pattern by default
promoted_loopbodylocals: Vec::new(), // Phase 224: No promoted variables by default
#[cfg(feature = "normalized_dev")]
promoted_bindings: BTreeMap::new(), // Phase 76: No promoted bindings by default
}
}
/// Phase 193-2: Get carrier count
///
/// Convenience method for checking how many carriers this info has.
pub fn carrier_count(&self) -> usize {
self.carriers.len()
}
/// Phase 193-2: Check if this has multiple carriers
///
/// Useful for pattern matching: "is this a multi-carrier loop?"
pub fn is_multi_carrier(&self) -> bool {
self.carriers.len() > 1
}
/// Phase 193-2: Find a carrier by name
///
/// Lookup a specific carrier variable by name.
pub fn find_carrier(&self, name: &str) -> Option<&CarrierVar> {
self.carriers.iter().find(|c| c.name == name)
}
/// Phase 171-C-4: Merge carriers from another CarrierInfo
///
/// Deduplicates by carrier name. If a carrier with the same name already exists,
/// it will not be added again.
///
/// # Arguments
///
/// * `other` - Another CarrierInfo to merge from
///
/// # Example
///
/// ```ignore
/// let mut carrier_info = CarrierInfo::from_variable_map("i", &variable_map)?;
/// let promoted_carrier = TrimPatternInfo::to_carrier_info();
/// carrier_info.merge_from(&promoted_carrier);
/// ```
pub fn merge_from(&mut self, other: &CarrierInfo) {
for carrier in &other.carriers {
if !self.carriers.iter().any(|c| c.name == carrier.name) {
self.carriers.push(carrier.clone());
}
}
// Maintain sorted order for determinism
self.carriers.sort_by(|a, b| a.name.cmp(&b.name));
// Phase 171-C-5: Also merge trim_helper if present
if other.trim_helper.is_some() {
self.trim_helper = other.trim_helper.clone();
}
// Phase 224: Merge promoted_loopbodylocals (deduplicate)
for promoted_var in &other.promoted_loopbodylocals {
if !self.promoted_loopbodylocals.contains(promoted_var) {
self.promoted_loopbodylocals.push(promoted_var.clone());
}
}
// Phase 76: Merge promoted_bindings (dev-only)
#[cfg(feature = "normalized_dev")]
{
for (original, promoted) in &other.promoted_bindings {
self.promoted_bindings.insert(*original, *promoted);
}
}
}
/// Phase 171-C-5: Get Trim pattern helper
///
/// Returns the TrimLoopHelper if this CarrierInfo was created from Trim promotion.
///
/// # Returns
///
/// * `Some(&TrimLoopHelper)` - If this CarrierInfo contains Trim pattern information
/// * `None` - If this is a regular CarrierInfo (not from Trim promotion)
///
/// # Example
///
/// ```ignore
/// if let Some(helper) = carrier_info.trim_helper() {
/// eprintln!("Trim pattern detected: {}", helper.carrier_name);
/// eprintln!("Whitespace chars: {:?}", helper.whitespace_chars);
/// }
/// ```
pub fn trim_helper(
&self,
) -> Option<&crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper> {
self.trim_helper.as_ref()
}
/// Phase 229/231: Resolve promoted LoopBodyLocal name to carrier JoinIR ValueId
///
/// This helper centralizes the naming convention for promoted variables so that
/// ScopeManager 実装がそれぞれ命名規約を再実装しなくて済むようにするよ。
///
/// 命名規約:
/// - DigitPos パターン: `"var"` → `"is_var"`(例: "digit_pos" → "is_digit_pos"
/// - Trim パターン : `"var"` → `"is_var_match"`(例: "ch" → "is_ch_match"
///
/// # Arguments
///
/// * `original_name` - 元の LoopBodyLocal 名(例: "digit_pos"
///
/// # Returns
///
/// * `Some(ValueId)` - 対応する carrier の join_id が見つかった場合
/// * `None` - promoted_loopbodylocals に含まれない、または join_id 未設定の場合
///
/// # Phase 77: DEPRECATED
///
/// This method uses fragile naming conventions ("is_*", "is_*_match") and will
/// be removed in Phase 78+ when all call sites migrate to BindingId-based lookup.
/// Use `resolve_promoted_with_binding()` for type-safe BindingId lookup.
#[deprecated(
since = "phase77",
note = "Use resolve_promoted_with_binding() for type-safe BindingId lookup"
)]
pub fn resolve_promoted_join_id(&self, original_name: &str) -> Option<ValueId> {
#[cfg(feature = "normalized_dev")]
eprintln!(
"[phase77/legacy/carrier_info] WARNING: Using deprecated name-based promoted lookup for '{}'",
original_name
);
if !self
.promoted_loopbodylocals
.contains(&original_name.to_string())
{
return None;
}
let candidates = [
format!("is_{}", original_name), // DigitPos pattern
format!("is_{}_match", original_name), // Trim pattern
];
for carrier_name in &candidates {
// loop_var 自身が ConditionOnly carrier として扱われるケースは現状ほぼないが、
// 将来の拡張に備えて loop_var_name も一応チェックしておく。
if carrier_name == &self.loop_var_name {
if let Some(carrier) = self.carriers.iter().find(|c| c.name == self.loop_var_name) {
if let Some(join_id) = carrier.join_id {
return Some(join_id);
}
}
}
if let Some(carrier) = self.carriers.iter().find(|c| c.name == *carrier_name) {
if let Some(join_id) = carrier.join_id {
return Some(join_id);
}
}
}
None
}
/// Phase 76: Type-safe promoted binding resolution (dev-only)
///
/// Resolves a promoted LoopBodyLocal binding via BindingId map, eliminating
/// name-based hacks (`format!("is_{}", name)`). Falls back to legacy name-based
/// lookup for backward compatibility during Phase 76-77 migration.
///
/// # Arguments
///
/// * `original_binding` - Original LoopBodyLocal's BindingId (e.g., BindingId(5) for "digit_pos")
///
/// # Returns
///
/// * `Some(BindingId)` - Promoted carrier's BindingId (e.g., BindingId(10) for "is_digit_pos")
/// * `None` - No promotion mapping found
///
/// # Example
///
/// ```ignore
/// // DigitPos promotion: BindingId(5) "digit_pos" → BindingId(10) "is_digit_pos"
/// let original_bid = BindingId(5);
/// if let Some(promoted_bid) = carrier_info.resolve_promoted_with_binding(original_bid) {
/// // Lookup carrier by promoted BindingId (type-safe!)
/// let promoted_value = condition_env.get_by_binding(promoted_bid);
/// }
/// ```
///
/// # Migration Path (Phase 76-77)
///
/// - **Phase 76**: BindingId map populated by promoters, dual path (BindingId OR name)
/// - **Phase 77**: Remove name-based fallback, BindingId-only lookup
///
/// # Design Notes
///
/// **Why not merge with `resolve_promoted_join_id()`?**
/// - Different input type: BindingId vs String
/// - Different output: BindingId vs ValueId
/// - Different usage: ScopeManager (BindingId) vs legacy lowerers (name)
///
/// **Why BTreeMap instead of HashMap?**
/// - Deterministic iteration (Phase 222.5-D consistency)
/// - Debug-friendly sorted output
#[cfg(feature = "normalized_dev")]
pub fn resolve_promoted_with_binding(&self, original_binding: BindingId) -> Option<BindingId> {
self.promoted_bindings.get(&original_binding).copied()
}
/// Phase 76: Record a promoted binding (dev-only)
///
/// Helper method to populate the promoted_bindings map during promotion.
/// Called by wrapper functions that have access to both CarrierInfo and binding_map.
///
/// # Arguments
///
/// * `original_binding` - Original LoopBodyLocal's BindingId
/// * `promoted_binding` - Promoted carrier's BindingId
///
/// # Example
///
/// ```ignore
/// // After DigitPosPromoter creates CarrierInfo, record the binding mapping:
/// carrier_info.record_promoted_binding(
/// binding_map.get("digit_pos").copied().unwrap(), // BindingId(5)
/// binding_map.get("is_digit_pos").copied().unwrap() // BindingId(10)
/// );
/// ```
///
/// # Phase 76 Note
///
/// This method is currently UNUSED because promoters (DigitPosPromoter, TrimLoopHelper)
/// don't have access to binding_map. Actual population happens in a future phase when
/// we integrate BindingId tracking into the promotion pipeline.
#[cfg(feature = "normalized_dev")]
pub fn record_promoted_binding(&mut self, original_binding: BindingId, promoted_binding: BindingId) {
use super::debug_output_box::DebugOutputBox;
// Phase 86: Use DebugOutputBox for consistent debug output
// Allow JOINIR_TEST_DEBUG override for test-specific diagnostics
let test_debug = std::env::var("JOINIR_TEST_DEBUG").is_ok();
let debug = DebugOutputBox::new("binding_pilot/promoted_bindings");
if debug.is_enabled() || test_debug {
eprintln!(
"[binding_pilot/promoted_bindings] {}{}",
original_binding, promoted_binding
);
}
self.promoted_bindings.insert(original_binding, promoted_binding);
}
}
/// Exit metadata returned by lowerers
///
/// This structure captures the mapping from JoinIR exit values to
/// carrier variable names, enabling dynamic binding generation.
#[derive(Debug, Clone)]
pub struct ExitMeta {
/// Exit value bindings: (carrier_name, join_exit_value_id)
///
/// Example for Pattern 4:
/// ```
/// vec![("sum".to_string(), ValueId(15))]
/// ```
/// where ValueId(15) is the k_exit parameter in JoinIR-local space.
pub exit_values: Vec<(String, ValueId)>,
}
/// Phase 33-14: JoinFragmentMeta - Distinguishes expr result from carrier updates
///
/// ## Purpose
///
/// Separates two distinct use cases for JoinIR loops:
///
/// 1. **Expr Result Pattern** (joinir_min_loop.hako):
/// ```nyash
/// local result = loop(...) { ... } // Loop used as expression
/// return result
/// ```
/// Here, the k_exit return value is the "expr result" that should go to exit_phi_inputs.
///
/// 2. **Carrier Update Pattern** (trim pattern):
/// ```nyash
/// loop(...) { start = start + 1 } // Loop used for side effects
/// print(start) // Use carrier after loop
/// ```
/// Here, there's no "expr result" - only carrier variable updates.
///
/// ## SSA Correctness
///
/// Previously, exit_phi_inputs mixed expr results with carrier updates, causing:
/// - PHI inputs that referenced undefined remapped values
/// - SSA-undef errors in VM execution
///
/// With JoinFragmentMeta:
/// - `expr_result`: Only goes to exit_phi_inputs (generates PHI for expr value)
/// - `exit_meta`: Only goes to carrier_inputs (updates variable_map via carrier PHIs)
///
/// ## Example: Pattern 2 (joinir_min_loop.hako)
///
/// ```rust
/// JoinFragmentMeta {
/// expr_result: Some(i_exit), // k_exit returns i as expr value
/// exit_meta: ExitMeta::single("i".to_string(), i_exit), // Also a carrier
/// }
/// ```
///
/// ## Example: Pattern 3 (trim pattern)
///
/// ```rust
/// JoinFragmentMeta {
/// expr_result: None, // Loop doesn't return a value
/// exit_meta: ExitMeta::multiple(vec![
/// ("start".to_string(), start_exit),
/// ("end".to_string(), end_exit),
/// ]),
/// }
/// ```
#[derive(Debug, Clone)]
pub struct JoinFragmentMeta {
/// Expression result ValueId from k_exit (JoinIR-local)
///
/// - `Some(vid)`: Loop is used as expression, k_exit's return value → exit_phi_inputs
/// - `None`: Loop is used for side effects only, no PHI for expr value
pub expr_result: Option<ValueId>,
/// Carrier variable exit bindings (existing ExitMeta)
///
/// Maps carrier names to their JoinIR-local exit values.
/// These go to carrier_inputs for carrier PHI generation.
pub exit_meta: ExitMeta,
}
impl JoinFragmentMeta {
/// Create JoinFragmentMeta for expression result pattern
///
/// Use when the loop returns a value (like `return loop(...)`).
pub fn with_expr_result(expr_result: ValueId, exit_meta: ExitMeta) -> Self {
Self {
expr_result: Some(expr_result),
exit_meta,
}
}
/// Create JoinFragmentMeta for carrier-only pattern
///
/// Use when the loop only updates carriers (like trim pattern).
pub fn carrier_only(exit_meta: ExitMeta) -> Self {
Self {
expr_result: None,
exit_meta,
}
}
/// Create empty JoinFragmentMeta (no expr result, no carriers)
pub fn empty() -> Self {
Self {
expr_result: None,
exit_meta: ExitMeta::empty(),
}
}
/// Check if this fragment has an expression result
pub fn has_expr_result(&self) -> bool {
self.expr_result.is_some()
}
/// Phase 33-14: Backward compatibility - convert to ExitMeta
///
/// During migration, some code may still expect ExitMeta.
/// This extracts just the carrier bindings.
#[deprecated(since = "33-14", note = "Use exit_meta directly for carrier access")]
pub fn to_exit_meta(&self) -> ExitMeta {
self.exit_meta.clone()
}
}
impl ExitMeta {
/// Create new ExitMeta with no exit values
pub fn empty() -> Self {
Self {
exit_values: vec![],
}
}
/// Create ExitMeta with a single exit value
pub fn single(carrier_name: String, join_value: ValueId) -> Self {
Self {
exit_values: vec![(carrier_name, join_value)],
}
}
/// Create ExitMeta with multiple exit values
pub fn multiple(exit_values: Vec<(String, ValueId)>) -> Self {
Self { exit_values }
}
/// Phase 193-2: Get the count of exit bindings
///
/// Useful for checking if this ExitMeta has any exit values.
pub fn binding_count(&self) -> usize {
self.exit_values.len()
}
/// Phase 193-2: Check if this has any exit values
pub fn is_empty(&self) -> bool {
self.exit_values.is_empty()
}
/// Phase 193-2: Find a binding by carrier name
///
/// Lookup a specific exit value by carrier name.
pub fn find_binding(&self, carrier_name: &str) -> Option<ValueId> {
self.exit_values
.iter()
.find(|(name, _)| name == carrier_name)
.map(|(_, value_id)| *value_id)
}
/// Phase 193-2: Add a binding to ExitMeta
///
/// Convenient way to build ExitMeta incrementally.
pub fn with_binding(mut self, carrier_name: String, join_value: ValueId) -> Self {
self.exit_values.push((carrier_name, join_value));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
// Helper: Create a CarrierVar for testing
fn test_carrier(name: &str, id: u32) -> CarrierVar {
CarrierVar {
name: name.to_string(),
host_id: ValueId(id),
join_id: None, // Phase 177-STRUCT-1
role: CarrierRole::LoopState, // Phase 227: Default to LoopState
init: CarrierInit::FromHost, // Phase 228: Default to FromHost
#[cfg(feature = "normalized_dev")]
binding_id: None, // Phase 78: No BindingId by default
}
}
// Helper: Create a CarrierInfo for testing
fn test_carrier_info(loop_var: &str, loop_id: u32, carriers: Vec<CarrierVar>) -> CarrierInfo {
CarrierInfo::with_carriers(loop_var.to_string(), ValueId(loop_id), carriers)
}
#[test]
fn test_merge_from_empty() {
// Merge empty CarrierInfo should not change anything
let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
let other = test_carrier_info("j", 20, vec![]);
carrier_info.merge_from(&other);
assert_eq!(carrier_info.carrier_count(), 1);
assert_eq!(carrier_info.carriers[0].name, "sum");
}
#[test]
fn test_merge_from_new_carrier() {
// Merge a new carrier that doesn't exist yet
let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
let other = test_carrier_info("j", 20, vec![test_carrier("count", 15)]);
carrier_info.merge_from(&other);
assert_eq!(carrier_info.carrier_count(), 2);
// Should be sorted by name
assert_eq!(carrier_info.carriers[0].name, "count"); // 'c' < 's'
assert_eq!(carrier_info.carriers[1].name, "sum");
}
#[test]
fn test_merge_from_duplicate_carrier() {
// Merge a carrier with the same name should NOT duplicate
let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
let other = test_carrier_info(
"j",
20,
vec![test_carrier("sum", 999)], // Same name, different ID
);
carrier_info.merge_from(&other);
// Should still have only 1 carrier (no duplication)
assert_eq!(carrier_info.carrier_count(), 1);
assert_eq!(carrier_info.carriers[0].name, "sum");
// Original ID should be preserved
assert_eq!(carrier_info.carriers[0].host_id, ValueId(10));
}
#[test]
fn test_merge_from_multiple_carriers() {
// Merge multiple carriers
let mut carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
let other = test_carrier_info(
"j",
20,
vec![test_carrier("count", 15), test_carrier("product", 18)],
);
carrier_info.merge_from(&other);
assert_eq!(carrier_info.carrier_count(), 3);
// Should be sorted by name
assert_eq!(carrier_info.carriers[0].name, "count");
assert_eq!(carrier_info.carriers[1].name, "product");
assert_eq!(carrier_info.carriers[2].name, "sum");
}
#[test]
fn test_merge_from_preserves_determinism() {
// Test that merge maintains sorted order
let mut carrier_info = test_carrier_info(
"i",
5,
vec![test_carrier("zebra", 30), test_carrier("alpha", 10)],
);
let other = test_carrier_info(
"j",
20,
vec![test_carrier("beta", 15), test_carrier("gamma", 18)],
);
carrier_info.merge_from(&other);
assert_eq!(carrier_info.carrier_count(), 4);
// Should be sorted alphabetically
assert_eq!(carrier_info.carriers[0].name, "alpha");
assert_eq!(carrier_info.carriers[1].name, "beta");
assert_eq!(carrier_info.carriers[2].name, "gamma");
assert_eq!(carrier_info.carriers[3].name, "zebra");
}
#[test]
fn test_merge_from_with_trim_helper() {
// Test that trim_helper is merged
use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper;
let mut carrier_info = test_carrier_info("i", 5, vec![]);
let mut other = test_carrier_info("j", 20, vec![]);
other.trim_helper = Some(TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string(), "\t".to_string()],
});
carrier_info.merge_from(&other);
// trim_helper should be copied
assert!(carrier_info.trim_helper.is_some());
let helper = carrier_info.trim_helper.as_ref().unwrap();
assert_eq!(helper.original_var, "ch");
assert_eq!(helper.carrier_name, "is_whitespace");
assert_eq!(helper.whitespace_count(), 2);
}
#[test]
fn test_trim_helper_accessor() {
// Test the trim_helper() accessor method
use crate::mir::loop_pattern_detection::trim_loop_helper::TrimLoopHelper;
let mut carrier_info = test_carrier_info("i", 5, vec![]);
// Initially None
assert!(carrier_info.trim_helper().is_none());
// Add trim_helper
carrier_info.trim_helper = Some(TrimLoopHelper {
original_var: "ch".to_string(),
carrier_name: "is_whitespace".to_string(),
whitespace_chars: vec![" ".to_string()],
});
// Now Some
assert!(carrier_info.trim_helper().is_some());
let helper = carrier_info.trim_helper().unwrap();
assert_eq!(helper.original_var, "ch");
}
// ========== Phase 76: promoted_bindings tests ==========
#[test]
#[cfg(feature = "normalized_dev")]
fn test_promoted_bindings_record_and_resolve() {
use crate::mir::BindingId;
let mut carrier_info = test_carrier_info("i", 5, vec![]);
// Record a promotion: BindingId(5) → BindingId(10)
carrier_info.record_promoted_binding(BindingId(5), BindingId(10));
// Resolve should find the mapping
assert_eq!(
carrier_info.resolve_promoted_with_binding(BindingId(5)),
Some(BindingId(10))
);
// Unknown BindingId should return None
assert_eq!(
carrier_info.resolve_promoted_with_binding(BindingId(99)),
None
);
}
#[test]
#[cfg(feature = "normalized_dev")]
fn test_promoted_bindings_multiple_mappings() {
use crate::mir::BindingId;
let mut carrier_info = test_carrier_info("i", 5, vec![]);
// Record multiple promotions (e.g., DigitPos + Trim in same loop)
carrier_info.record_promoted_binding(BindingId(5), BindingId(10)); // digit_pos → is_digit_pos
carrier_info.record_promoted_binding(BindingId(6), BindingId(11)); // ch → is_ch_match
// Both should resolve independently
assert_eq!(
carrier_info.resolve_promoted_with_binding(BindingId(5)),
Some(BindingId(10))
);
assert_eq!(
carrier_info.resolve_promoted_with_binding(BindingId(6)),
Some(BindingId(11))
);
}
#[test]
#[cfg(feature = "normalized_dev")]
fn test_promoted_bindings_merge() {
use crate::mir::BindingId;
let mut carrier_info1 = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
carrier_info1.record_promoted_binding(BindingId(1), BindingId(2));
let mut carrier_info2 = test_carrier_info("j", 20, vec![test_carrier("count", 15)]);
carrier_info2.record_promoted_binding(BindingId(3), BindingId(4));
// Merge carrier_info2 into carrier_info1
carrier_info1.merge_from(&carrier_info2);
// Both promoted_bindings should be present
assert_eq!(
carrier_info1.resolve_promoted_with_binding(BindingId(1)),
Some(BindingId(2))
);
assert_eq!(
carrier_info1.resolve_promoted_with_binding(BindingId(3)),
Some(BindingId(4))
);
}
#[test]
#[cfg(feature = "normalized_dev")]
fn test_promoted_bindings_default_empty() {
use crate::mir::BindingId;
// Newly created CarrierInfo should have empty promoted_bindings
let carrier_info = test_carrier_info("i", 5, vec![test_carrier("sum", 10)]);
assert_eq!(
carrier_info.resolve_promoted_with_binding(BindingId(0)),
None
);
}
#[test]
#[cfg(feature = "normalized_dev")]
fn test_promoted_bindings_overwrite() {
use crate::mir::BindingId;
let mut carrier_info = test_carrier_info("i", 5, vec![]);
// Record initial mapping
carrier_info.record_promoted_binding(BindingId(5), BindingId(10));
// Overwrite with new mapping (should replace)
carrier_info.record_promoted_binding(BindingId(5), BindingId(20));
// Should return the new value
assert_eq!(
carrier_info.resolve_promoted_with_binding(BindingId(5)),
Some(BindingId(20))
);
}
}