//! 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_" or "is__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, /// 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, } 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, /// Phase 171-C-5: Trim pattern helper (if this CarrierInfo was created from Trim promotion) pub trim_helper: Option, /// 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, /// 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, } 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, // Phase 222.5-D: HashMap → BTreeMap for determinism ) -> Result { // 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 = 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, variable_map: &BTreeMap, // Phase 222.5-D: HashMap → BTreeMap for determinism ) -> Result { 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, ) -> 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 { #[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 { 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, /// 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 { 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) -> 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)) ); } }