diff --git a/docs/development/current/main/phases/phase-256/README.md b/docs/development/current/main/phases/phase-256/README.md new file mode 100644 index 00000000..17d806b1 --- /dev/null +++ b/docs/development/current/main/phases/phase-256/README.md @@ -0,0 +1,138 @@ +# Phase 256: StringUtils.split/2 Pattern Support + +Status: Planning +Scope: Loop pattern recognition for split/tokenization operations +Related: +- Phase 255 完了(loop_invariants 導入、Pattern 6 完成) +- Phase 254 完了(Pattern 6 index_of 実装) + +## 失敗詳細 + +**テスト**: json_lint_vm (quick profile) +**エラー**: `[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern` +**関数**: `StringUtils.split/2` + +### エラーメッセージ全体 + +``` +[trace:dev] loop_canonicalizer: Decision: FAIL_FAST +[trace:dev] loop_canonicalizer: Missing caps: [ConstStep] +[trace:dev] loop_canonicalizer: Reason: Phase 143-P2: Loop does not match read_digits(loop(true)), + skip_whitespace, parse_number, continue, parse_string, or parse_array pattern +[ERROR] ❌ MIR compilation error: [joinir/freeze] Loop lowering failed: JoinIR does not support this pattern, + and LoopBuilder has been removed. +Function: StringUtils.split/2 +Hint: This loop pattern is not supported. All loops must use JoinIR lowering. +``` + +## 期待される動作 + +`StringUtils.split(s, separator)` が正常にコンパイルされ、文字列分割が動作すること。 + +## 実際の動作 + +Loop canonicalizer が `ConstStep` を要求しているが、このループはステップが複雑で定数ではない。 + +## 最小再現コード + +```nyash +split(s, separator) { + local result = new ArrayBox() + + // Early return for empty separator + if separator.length() == 0 { + result.push(s) + return result + } + + local start = 0 + local i = 0 + + // Main scan loop + loop(i <= s.length() - separator.length()) { + if s.substring(i, i + separator.length()) == separator { + result.push(s.substring(start, i)) + start = i + separator.length() + i = start // Variable step - moves by separator.length() + } else { + i = i + 1 // Constant step - moves by 1 + } + } + + // Push remaining segment + if start <= s.length() { + result.push(s.substring(start, s.length())) + } + + return result +} +``` + +## 分析 + +### ループ構造 + +1. **条件**: `i <= s.length() - separator.length()` +2. **ボディ**: + - If branch: マッチング検出時 + - `result.push()` でセグメント追加 + - `start = i + separator.length()` で次の開始位置更新 + - `i = start` で大きくジャンプ(可変ステップ) + - Else branch: マッチなし + - `i = i + 1` で 1 進む(定数ステップ) +3. **特徴**: + - **可変ステップ**: マッチング時は `separator.length()` 分ジャンプ + - **複数キャリア**: `i`, `start`, `result` を更新 + - **MethodCall**: `substring()`, `push()`, `length()` を使用 + +### Canonicalizer の問題 + +``` +Missing caps: [ConstStep] +``` + +- 既存の Pattern 1-6 は定数ステップを想定 +- このループは条件分岐で異なるステップ幅を使う +- Pattern 2 (balanced_depth_scan) に近いが、可変ステップがネック + +## 実装計画 + +### Option A: Pattern 7 - Split/Tokenization Pattern + +**新しいパターン追加**: +- 可変ステップサポート +- 複数キャリア(i, start, accumulator) +- If-else での異なるステップ幅処理 + +**検出条件**: +1. Loop condition: `i <= expr - len` +2. Body has if statement: + - Then: `i = something_big` (可変ジャンプ) + - Else: `i = i + 1` (定数ステップ) +3. Accumulator への追加操作 (`push` など) + +### Option B: Pattern 2 拡張 + +**既存 Pattern 2 を拡張**: +- ConstStep 要件を緩和 +- If-else で異なるステップ幅を許可 +- balanced_depth_scan_policy を拡張 + +### Option C: Normalization 経路 + +**ループ正規化で対応**: +- 可変ステップを定数ステップに変換 +- Carrier 追加で状態管理 + +## 次のステップ + +1. **StepTree 詳細解析**: split ループの完全な AST 構造確認 +2. **類似パターン調査**: 他の可変ステップループ(indexOf, contains など) +3. **Option 選択**: Pattern 7 新設 vs Pattern 2 拡張 vs Normalization +4. **実装戦略策定**: 選択した Option の詳細設計 + +## 備考 + +- Phase 255 で loop_invariants が導入されたが、このケースは invariants 以前の問題(可変ステップ) +- Phase 254-256 の流れで Pattern 6 → Pattern 7 の自然な進化が期待される +- split/tokenization は一般的なパターンなので、汎用的な解決策が望ましい diff --git a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs index 268f325c..40001bcd 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -698,6 +698,30 @@ pub(super) fn merge_and_rewrite( ); } } + + // Phase 255 P2: Set latch incoming for loop invariants + // + // Loop invariants don't have corresponding tail call args because they + // are not modified by the loop. Their latch incoming is the PHI dst itself + // (same value across all iterations). + for (inv_name, _inv_host_id) in b.loop_invariants.iter() { + if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) { + // For invariants, latch incoming is the PHI dst (same value) + loop_header_phi_info.set_latch_incoming( + inv_name, + new_block_id, // latch block + phi_dst, // same as PHI dst (invariant value) + ); + + if debug { + log!( + true, + "[cf_loop/joinir] Phase 255 P2: Set latch incoming for loop invariant '{}': block={:?}, value={:?} (PHI dst itself)", + inv_name, new_block_id, phi_dst + ); + } + } + } } // Phase 33-16: Classify tail call to determine redirection behavior diff --git a/src/mir/builder/control_flow/joinir/merge/loop_header_phi_builder.rs b/src/mir/builder/control_flow/joinir/merge/loop_header_phi_builder.rs index a420097b..0d4a8c9b 100644 --- a/src/mir/builder/control_flow/joinir/merge/loop_header_phi_builder.rs +++ b/src/mir/builder/control_flow/joinir/merge/loop_header_phi_builder.rs @@ -59,6 +59,12 @@ impl LoopHeaderPhiBuilder { /// * `CarrierInit::FromHost` - Use host_id directly as PHI init value /// * `CarrierInit::BoolConst(val)` - Generate explicit bool constant for ConditionOnly carriers /// * `CarrierInit::LoopLocalZero` - Generate const 0 for loop-local derived carriers (no host slot) + /// + /// # Phase 255 P2 Update + /// + /// Added `loop_invariants` parameter for variables that are referenced inside + /// the loop body but do not change across iterations. These need header PHI nodes + /// (with same value from all incoming edges) but do NOT need exit PHI nodes. pub fn build( builder: &mut crate::mir::builder::MirBuilder, header_block: BasicBlockId, @@ -71,6 +77,7 @@ impl LoopHeaderPhiBuilder { crate::mir::join_ir::lowering::carrier_info::CarrierInit, crate::mir::join_ir::lowering::carrier_info::CarrierRole, )], // Phase 228: Added CarrierInit and CarrierRole + loop_invariants: &[(String, ValueId)], // Phase 255 P2: Loop invariant variables expr_result_is_loop_var: bool, debug: bool, ) -> Result { @@ -196,6 +203,58 @@ impl LoopHeaderPhiBuilder { } } + // Phase 255 P2: Generate PHI nodes for loop invariants + // Loop invariants need header PHI (same value from all edges) but NOT exit PHI + for (name, host_id) in loop_invariants { + let invariant_phi_dst = builder.next_value_id(); + + // Phase 72: Observe PHI dst allocation + #[cfg(debug_assertions)] + crate::mir::join_ir::verify_phi_reserved::observe_phi_dst(invariant_phi_dst); + + // Phase 131-11-H: Set PHI type from entry incoming (init value) only + if let Some(init_type) = builder.type_ctx.value_types.get(host_id).cloned() { + builder + .type_ctx + .value_types + .insert(invariant_phi_dst, init_type.clone()); + + trace.stderr_if( + &format!( + "[carrier/phi] Loop invariant '{}': dst=%{} entry_type={:?} (same value all iterations)", + name, + invariant_phi_dst.as_u32(), + init_type + ), + debug || std::env::var("NYASH_CARRIER_PHI_DEBUG").ok().as_deref() == Some("1"), + ); + } + + // Register as CarrierPhiEntry with LoopState role + // Note: latch_incoming will be set to the same PHI dst by instruction_rewriter + info.carrier_phis.insert( + name.clone(), + CarrierPhiEntry { + phi_dst: invariant_phi_dst, + entry_incoming: (entry_block, *host_id), + latch_incoming: None, // Will be set to phi_dst (same value) by instruction_rewriter + role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState, + }, + ); + // Phase 177-STRUCT-2: Record insertion order + info.carrier_order.push(name.clone()); + + if debug { + trace.stderr_if( + &format!( + "[cf_loop/joinir] Loop invariant '{}' PHI: {:?} = phi [(from {:?}, {:?}), (latch same value)]", + name, invariant_phi_dst, entry_block, host_id + ), + true, + ); + } + } + // Set expr_result if this pattern returns the loop var if expr_result_is_loop_var { info.expr_result_phi = Some(loop_var_phi_dst); diff --git a/src/mir/builder/control_flow/joinir/merge/mod.rs b/src/mir/builder/control_flow/joinir/merge/mod.rs index 1575db54..c50274b0 100644 --- a/src/mir/builder/control_flow/joinir/merge/mod.rs +++ b/src/mir/builder/control_flow/joinir/merge/mod.rs @@ -337,6 +337,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks( loop_var_name, loop_var_init, &other_carriers, + &boundary.loop_invariants, // Phase 255 P2: Add loop invariants boundary.expr_result.is_some(), debug, )? diff --git a/src/mir/builder/control_flow/joinir/patterns/common/ast_helpers.rs b/src/mir/builder/control_flow/joinir/patterns/common/ast_helpers.rs new file mode 100644 index 00000000..4ca8afdc --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/common/ast_helpers.rs @@ -0,0 +1,25 @@ +//! Phase 255 P2: Common AST helpers for pattern lowering + +use crate::ast::{ASTNode, Span}; + +/// Create Variable ASTNode with unknown span +/// +/// # Phase 255 P2 +/// +/// This helper function eliminates duplicate var() implementations across +/// pattern lowering code. Previously, there were 7 copies of this function +/// scattered across different files. +/// +/// # Usage +/// +/// ```ignore +/// use super::common::var; +/// +/// let i_var = var("i"); // ASTNode::Variable { name: "i", span: Span::unknown() } +/// ``` +pub(crate) fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: Span::unknown(), + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/common/mod.rs b/src/mir/builder/control_flow/joinir/patterns/common/mod.rs new file mode 100644 index 00000000..bf9da060 --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/common/mod.rs @@ -0,0 +1,8 @@ +//! Phase 255 P2: Common utilities for JoinIR pattern lowering +//! +//! This module provides shared helper functions used across different pattern +//! lowering implementations, eliminating code duplication and ensuring consistency. + +mod ast_helpers; + +pub(crate) use ast_helpers::var; diff --git a/src/mir/builder/control_flow/joinir/patterns/mod.rs b/src/mir/builder/control_flow/joinir/patterns/mod.rs index ff28ac17..da9339a6 100644 --- a/src/mir/builder/control_flow/joinir/patterns/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/mod.rs @@ -48,7 +48,11 @@ //! Phase 93/94: Pattern Policies //! - policies/: Pattern recognition and routing decision (future expansion) //! - Currently a placeholder directory for future policy box organization +//! +//! Phase 255 P2: Common Utilities +//! - common/: Shared helper functions (var() etc.) to eliminate code duplication +pub(in crate::mir::builder) mod common; // Phase 255 P2: Common AST helpers pub(in crate::mir::builder) mod ast_feature_extractor; pub(in crate::mir::builder) mod policies; // Phase 93/94: Pattern routing policies (future expansion) pub(in crate::mir::builder) mod body_local_policy; // Phase 92 P3: promotion vs slot routing diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs index 7fdc4ae0..cffeadff 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs @@ -34,6 +34,7 @@ //! - Need to hoist MethodCall to init phase use super::super::trace; +use super::common::var; // Phase 255 P2: Use shared var() helper use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; use crate::mir::builder::MirBuilder; use crate::mir::ValueId; @@ -413,37 +414,29 @@ impl MirBuilder { let mut join_value_space = JoinValueSpace::new(); let join_module = lower_scan_with_init_minimal(&mut join_value_space); - // Phase 255 P0: Build CarrierInfo for multi-param loop - // Step 1: Create CarrierInfo with 3 variables (s, ch, i) - use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar, CarrierRole}; - - let carriers = vec![ - // s: haystack (ConditionOnly - header PHI only, no exit PHI) - CarrierVar::with_role( - parts.haystack.clone(), - s_host, - CarrierRole::ConditionOnly, - ), - // ch: needle (ConditionOnly - header PHI only, no exit PHI) - CarrierVar::with_role( - parts.needle.clone(), - ch_host, - CarrierRole::ConditionOnly, - ), - ]; + // Phase 255 P2: Build CarrierInfo for loop variable only + // Step 1: Create CarrierInfo with loop variable (i) only + // s and ch are now loop_invariants (not carriers) + use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierRole}; let carrier_info = CarrierInfo::with_carriers( parts.loop_var.clone(), // loop_var_name: "i" i_host, // loop_var_id (LoopState - header PHI + exit PHI) - carriers, // s, ch only (i is handled as loop_var) + vec![], // Empty carriers - only loop_var ); + // Phase 255 P2: Create loop_invariants for s and ch + let loop_invariants = vec![ + (parts.haystack.clone(), s_host), // s: haystack + (parts.needle.clone(), ch_host), // ch: needle + ]; + if debug { trace.debug( "pattern6/lower", &format!( - "Phase 255 P0: CarrierInfo with {} carriers (s, ch: ConditionOnly, i: LoopState)", - carrier_info.carrier_count() + "Phase 255 P2: CarrierInfo with loop_var only (i: LoopState), {} loop_invariants (s, ch)", + loop_invariants.len() ), ); } @@ -460,8 +453,8 @@ impl MirBuilder { ]; // Step 3: Build exit_bindings manually - // CRITICAL: ALL carriers (ConditionOnly + LoopState) must be in exit_bindings - // for latch incoming collection, even though ConditionOnly don't participate in exit PHI + // Phase 255 P2: Only LoopState variables (i) need exit bindings + // Loop invariants (s, ch) do NOT need exit bindings use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; let k_exit_func = join_module.require_function("k_exit", "Pattern 6"); @@ -478,37 +471,21 @@ impl MirBuilder { role: CarrierRole::LoopState, }; - // Phase 255 P0: Add ConditionOnly carriers in alphabetical order [ch, s] - // They don't participate in exit PHI, but need latch incoming collection - let ch_exit_binding = LoopExitBinding { - carrier_name: parts.needle.clone(), - join_exit_value: ValueId(0), // Placeholder - not used for ConditionOnly - host_slot: ch_host, - role: CarrierRole::ConditionOnly, - }; - - let s_exit_binding = LoopExitBinding { - carrier_name: parts.haystack.clone(), - join_exit_value: ValueId(0), // Placeholder - not used for ConditionOnly - host_slot: s_host, - role: CarrierRole::ConditionOnly, - }; - - // CRITICAL: Order must match tail call args order: [i, ch, s] (alphabetical) - // Phase 255 P0: loop_step tail call: args = vec![i_plus_1, ch_step_param, s_step_param] - // The loop variable i is first (args[0]), then carriers in alphabetical order (args[1], args[2]) - let exit_bindings = vec![i_exit_binding, ch_exit_binding, s_exit_binding]; + // Phase 255 P2: Only i (LoopState) in exit_bindings + // s and ch are loop_invariants, not carriers + let exit_bindings = vec![i_exit_binding]; if debug { trace.debug( "pattern6/lower", - &format!("Phase 255 P0: Generated {} exit_bindings", exit_bindings.len()), + &format!("Phase 255 P2: Generated {} exit_bindings (i only)", exit_bindings.len()), ); } - // Step 4: Build boundary with carrier_info + // Step 4: Build boundary with carrier_info and loop_invariants let boundary = JoinInlineBoundaryBuilder::new() .with_inputs(join_inputs, host_inputs) + .with_loop_invariants(loop_invariants) // Phase 255 P2: Add loop invariants .with_exit_bindings(exit_bindings) .with_loop_var_name(Some(parts.loop_var.clone())) .with_carrier_info(carrier_info.clone()) // ✅ Key: carrier_info for multi-PHI @@ -581,13 +558,7 @@ impl MirBuilder { } } -/// Phase 255 P1: Helper function to create Variable ASTNode -fn var(name: &str) -> ASTNode { - ASTNode::Variable { - name: name.to_string(), - span: crate::ast::Span::unknown(), - } -} +// Phase 255 P2: var() function removed - now using super::common::var #[cfg(test)] mod tests { diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs b/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs index e07499a1..3506cfc0 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/balanced_depth_scan_policy.rs @@ -6,6 +6,7 @@ //! - Fail-fast with tagged reasons when the shape is close but unsupported use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; +use crate::mir::builder::control_flow::joinir::patterns::common::var; // Phase 255 P2: Use shared var() helper use crate::mir::join_ir::lowering::common::balanced_depth_scan_emitter::BalancedDepthScanRecipe; use crate::mir::join_ir::lowering::error_tags; use crate::mir::join_ir::lowering::loop_update_analyzer::{UpdateExpr, UpdateRhs}; @@ -449,12 +450,7 @@ fn is_var_plus_int(node: &ASTNode, var_name: &str, n: i64) -> bool { ) } -fn var(name: &str) -> ASTNode { - ASTNode::Variable { - name: name.to_string(), - span: Span::unknown(), - } -} +// Phase 255 P2: var() function removed - now using crate::mir::builder::control_flow::joinir::patterns::common::var fn eq_str(left: ASTNode, s: &str) -> ASTNode { ASTNode::BinaryOp { diff --git a/src/mir/builder/control_flow/joinir/patterns/policies/post_loop_early_return_plan.rs b/src/mir/builder/control_flow/joinir/patterns/policies/post_loop_early_return_plan.rs index a0b38e69..e6ed51d6 100644 --- a/src/mir/builder/control_flow/joinir/patterns/policies/post_loop_early_return_plan.rs +++ b/src/mir/builder/control_flow/joinir/patterns/policies/post_loop_early_return_plan.rs @@ -1,15 +1,67 @@ -//! Post-loop early return plan (policy-level SSOT) +//! Phase 255 P2: Post-loop early return plan (policy-level SSOT) +//! +//! # Responsibility //! -//! Responsibility: //! - Describe a post-loop guard that emulates an in-loop `return` without making //! Pattern2 lowering itself return-in-loop aware. //! - Keep the plan policy-agnostic so multiple Pattern2 families can reuse it. +//! +//! # Architecture +//! +//! Ensures exit PHI values are used (prevents DCE elimination). The post-loop +//! guard creates a conditional that references the exit PHI value, forcing it +//! to be live and preventing dead code elimination. +//! +//! # Usage Patterns +//! +//! ## Pattern 2: Less Than (balanced_depth_scan) +//! +//! Used in `json_cur.find_balanced_*` family functions. +//! +//! ```ignore +//! PostLoopEarlyReturnPlan { +//! cond: BinaryOp { Less, var("i"), var("n") }, +//! ret_expr: var("i"), +//! } +//! ``` +//! +//! Generated post-loop guard: +//! ```nyash +//! if i < n { +//! return i +//! } +//! ``` +//! +//! ## Pattern 6: Not Equal (index_of) +//! +//! Used in `StringUtils.index_of` and similar search functions. +//! +//! ```ignore +//! PostLoopEarlyReturnPlan { +//! cond: BinaryOp { NotEqual, var("i"), Literal(-1) }, +//! ret_expr: var("i"), +//! } +//! ``` +//! +//! Generated post-loop guard: +//! ```nyash +//! if i != -1 { +//! return i +//! } +//! ``` +//! +//! # Builder Decision +//! +//! Deferred to Phase 256+. Currently 2 patterns use this (Pattern 2 and Pattern 6). +//! Direct construction is acceptable. Will create builder when 4+ patterns use it. use crate::ast::ASTNode; #[derive(Debug, Clone)] pub(crate) struct PostLoopEarlyReturnPlan { + /// Condition for the post-loop guard (e.g., `i < n` or `i != -1`) pub cond: ASTNode, + /// Expression to return if condition is true (e.g., `var("i")`) pub ret_expr: ASTNode, } diff --git a/src/mir/join_ir/lowering/inline_boundary.rs b/src/mir/join_ir/lowering/inline_boundary.rs index e8315023..ff063ffa 100644 --- a/src/mir/join_ir/lowering/inline_boundary.rs +++ b/src/mir/join_ir/lowering/inline_boundary.rs @@ -158,6 +158,50 @@ pub struct JoinInlineBoundary { #[deprecated(since = "Phase 190", note = "Use exit_bindings instead")] pub host_outputs: Vec, + /// Phase 255 P2: Loop invariant variables + /// + /// Variables that are referenced inside the loop body but do not change + /// across iterations. These variables need header PHI nodes (with the same + /// value from all incoming edges) but do NOT need exit PHI nodes. + /// + /// # Example: index_of(s, ch) + /// + /// ```nyash + /// index_of(s, ch) { + /// local i = 0 + /// loop(i < s.length()) { + /// if s.substring(i, i + 1) == ch { return i } + /// i = i + 1 + /// } + /// return -1 + /// } + /// ``` + /// + /// Here: + /// - `s` (haystack): loop invariant - used in loop body but never modified + /// - `ch` (needle): loop invariant - used in loop body but never modified + /// - `i` (loop index): loop state - modified each iteration (goes in exit_bindings) + /// + /// # Format + /// + /// Each entry is `(variable_name, host_value_id)`: + /// ``` + /// loop_invariants: vec![ + /// ("s".to_string(), ValueId(10)), // HOST ID for "s" + /// ("ch".to_string(), ValueId(11)), // HOST ID for "ch" + /// ] + /// ``` + /// + /// # Header PHI Generation + /// + /// For each loop invariant, LoopHeaderPhiBuilder generates: + /// ```mir + /// %phi_dst = phi [%host_id, entry], [%phi_dst, latch] + /// ``` + /// + /// The latch incoming is the PHI destination itself (same value preserved). + pub loop_invariants: Vec<(String, ValueId)>, + /// Explicit exit bindings for loop carriers (Phase 190+) /// /// Each binding explicitly names which variable is being updated and @@ -323,6 +367,7 @@ impl JoinInlineBoundary { join_outputs: vec![], #[allow(deprecated)] host_outputs: vec![], + loop_invariants: vec![], // Phase 255 P2: Default to empty exit_bindings: vec![], #[allow(deprecated)] condition_inputs: vec![], // Phase 171: Default to empty (deprecated) @@ -368,6 +413,7 @@ impl JoinInlineBoundary { join_outputs: vec![], #[allow(deprecated)] host_outputs, + loop_invariants: vec![], // Phase 255 P2: Default to empty exit_bindings: vec![], #[allow(deprecated)] condition_inputs: vec![], // Phase 171: Default to empty (deprecated) @@ -430,6 +476,7 @@ impl JoinInlineBoundary { join_outputs: vec![], #[allow(deprecated)] host_outputs: vec![], + loop_invariants: vec![], // Phase 255 P2: Default to empty exit_bindings, #[allow(deprecated)] condition_inputs: vec![], // Phase 171: Default to empty (deprecated) @@ -478,6 +525,7 @@ impl JoinInlineBoundary { join_outputs: vec![], #[allow(deprecated)] host_outputs: vec![], + loop_invariants: vec![], // Phase 255 P2: Default to empty exit_bindings: vec![], #[allow(deprecated)] condition_inputs, @@ -530,6 +578,7 @@ impl JoinInlineBoundary { join_outputs: vec![], #[allow(deprecated)] host_outputs: vec![], + loop_invariants: vec![], // Phase 255 P2: Default to empty exit_bindings, #[allow(deprecated)] condition_inputs, @@ -589,6 +638,7 @@ impl JoinInlineBoundary { join_outputs: vec![], #[allow(deprecated)] host_outputs: vec![], + loop_invariants: vec![], // Phase 255 P2: Default to empty exit_bindings: vec![], #[allow(deprecated)] condition_inputs: vec![], // Deprecated, use condition_bindings instead diff --git a/src/mir/join_ir/lowering/inline_boundary_builder.rs b/src/mir/join_ir/lowering/inline_boundary_builder.rs index a28db06d..d3d5c18b 100644 --- a/src/mir/join_ir/lowering/inline_boundary_builder.rs +++ b/src/mir/join_ir/lowering/inline_boundary_builder.rs @@ -90,6 +90,7 @@ impl JoinInlineBoundaryBuilder { join_outputs: vec![], #[allow(deprecated)] host_outputs: vec![], + loop_invariants: vec![], // Phase 255 P2: Initialize as empty exit_bindings: vec![], #[allow(deprecated)] condition_inputs: vec![], @@ -155,6 +156,25 @@ impl JoinInlineBoundaryBuilder { self } + /// Phase 255 P2: Set loop invariants + /// + /// Variables that are referenced inside the loop body but do not change + /// across iterations. These need header PHI nodes (with same value from all + /// incoming edges) but do NOT need exit PHI nodes. + /// + /// # Example + /// + /// ```ignore + /// builder.with_loop_invariants(vec![ + /// ("s".to_string(), ValueId(10)), // haystack + /// ("ch".to_string(), ValueId(11)), // needle + /// ]) + /// ``` + pub fn with_loop_invariants(mut self, invariants: Vec<(String, ValueId)>) -> Self { + self.boundary.loop_invariants = invariants; + self + } + /// Set loop variable name (Phase 33-16) /// /// Used for LoopHeaderPhiBuilder to track which PHI corresponds to the loop variable.