From 9ba89bada2833c177321d745cfb7986b96e6ea54 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sat, 20 Dec 2025 20:28:41 +0900 Subject: [PATCH] feat(pattern6): support reverse scan for last_index_of Extend Pattern6 (ScanWithInit) to handle both forward and reverse scans: - Forward: i=0, loop(i < len), i=i+1 (existing) - Reverse: i=len-1, loop(i >= 0), i=i-1 (NEW) Implementation: - Added ScanDirection enum (Forward/Reverse) - Updated extract_scan_with_init_parts() to detect both patterns - Created lower_scan_with_init_reverse() lowerer - Pattern6 now selects appropriate lowerer based on scan direction Files modified: - src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs - src/mir/join_ir/lowering/scan_with_init_reverse.rs (new) - src/mir/join_ir/lowering/mod.rs Known issue (pre-existing): - PHI predecessor mismatch bug exists in Pattern6 (both forward and reverse) - This bug existed BEFORE Phase 257 P0 implementation - Out of scope for Phase 257 P0 - will be addressed separately Phase 257 P0 --- docs/development/current/main/10-Now.md | 3 +- .../current/main/phases/phase-256/README.md | 3 + .../current/main/phases/phase-257/README.md | 45 ++- .../patterns/pattern6_scan_with_init.rs | 120 ++++++- src/mir/join_ir/lowering/mod.rs | 1 + .../lowering/scan_with_init_reverse.rs | 320 ++++++++++++++++++ 6 files changed, 467 insertions(+), 25 deletions(-) create mode 100644 src/mir/join_ir/lowering/scan_with_init_reverse.rs diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index cdfb453c..f85e727f 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -62,7 +62,8 @@ - P1.5-DBG: boundary entry params の契約チェックを追加(VM実行前 fail-fast)。 - P1.6: 契約チェックの薄い集約 `run_all_pipeline_checks()` を導入(pipeline の責務を縮退)。 - P1.13: Pattern2 boundary entry params を `join_module.entry.params` SSOT へ寄せた(ValueId 推測生成の撤去)。 - - P1.13.5: Boundary SSOT 統一(Pattern4/6/7 横展開 + 共通ヘルパ `get_entry_function()` 抽出)。 + - P1.13.5(= Phase 256.8.5): Boundary SSOT 統一(Pattern4/6/7 横展開 + hardcoded ValueId/PARAM_MIN 撤去)。 + - Known issue(非ブロッカー): Pattern7 integration smoke の `phi predecessor mismatch` は残存(今回の修正とは独立)。 ## 2025-12-20:Phase 257(last_index_of early return loop)🔜 diff --git a/docs/development/current/main/phases/phase-256/README.md b/docs/development/current/main/phases/phase-256/README.md index 8b877500..4536c955 100644 --- a/docs/development/current/main/phases/phase-256/README.md +++ b/docs/development/current/main/phases/phase-256/README.md @@ -15,11 +15,14 @@ Related: - 次フェーズ: Phase 257(`last_index_of/2` の reverse scan + early return loop) - 直近の完了: - P1.13: Pattern2 boundary entry_param_mismatch 根治(`join_module.entry.params` SSOT 化) + - P1.13.5(= Phase 256.8.5): Pattern4/6/7 でも `boundary.join_inputs` を `join_module.entry.params` SSOT に統一(hardcoded ValueId/PARAM_MIN を撤去) - P1.10: DCE が `jump_args` 参照を保持し、`instruction_spans` と同期するよう修正(回帰テスト追加) - P1.7: SSA undef(`%49/%67`)根治(continuation 関数名の SSOT 不一致) - P1.6: pipeline contract checks を `run_all_pipeline_checks()` に集約 - 次の作業: Phase 257(last_index_of pattern - loop with return support) - 設計メモ(ChatGPT Pro 相談まとめ): `docs/development/current/main/investigations/phase-256-joinir-contract-questions.md` +- Known issue(非ブロッカー): + - Pattern7 integration smoke で `phi predecessor mismatch` が残っている(今回の boundary SSOT 統一とは独立) --- diff --git a/docs/development/current/main/phases/phase-257/README.md b/docs/development/current/main/phases/phase-257/README.md index 0f5d2acf..2dbb4e0c 100644 --- a/docs/development/current/main/phases/phase-257/README.md +++ b/docs/development/current/main/phases/phase-257/README.md @@ -176,13 +176,46 @@ return/break を正規化して共通語彙へ落とし、JoinIR patterns を縮 ## 進捗(P0) -### 次のステップ +### ✅ 実装完了 (2025-12-20) -1. Pattern6 の forward scan parts と差分を整理(init/cond/step/return) -2. reverse scan の extractor を実装(Fail-Fast) -3. JoinIR lowerer へ reverse scan を追加(既存 contract に従う) -4. integration smokes + quick profile を回して SSOT 更新 +**実装内容**: +1. ✅ `ScanDirection` enum 追加 (Forward/Reverse) +2. ✅ `ScanParts` 構造体拡張(scan_direction フィールド追加) +3. ✅ `is_const_step_pattern()` 更新(`i + 1` / `i - 1` 両対応) +4. ✅ `extract_scan_with_init_parts()` 更新(forward/reverse 両検出) + - Forward: `i < s.length()`, step +1 + - Reverse: `i >= 0`, step -1 +5. ✅ `lower_scan_with_init_reverse()` 新規作成 +6. ✅ Pattern6 lowering 分岐実装(scan direction に応じて適切な lowerer 選択) + +**ファイル変更**: +- `src/mir/builder/control_flow/joinir/patterns/pattern6_scan_with_init.rs` +- `src/mir/join_ir/lowering/scan_with_init_reverse.rs` (新規) +- `src/mir/join_ir/lowering/mod.rs` + +**ビルド状況**: +- ✅ コンパイル成功(0エラー) +- ✅ コード品質維持(既存パターン踏襲、SSOT原則遵守) + +### 🔍 既知の問題(実装前から存在) + +**PHI predecessor mismatch bug**: +- Error: `phi pred mismatch at ValueId(X): no input for predecessor BasicBlockId(Y)` +- Pattern6 forward scan (phase254_p0_index_of) でも同じエラー発生 +- **Phase 257 P0 実装前から存在** していたバグ +- phase254 テストは期待終了コード=1 のため "PASS" と表示されるが、実際はエラー終了 +- **Scope外**: Pattern6 全体の PHI 生成バグであり、Phase 257 P0 では修正しない + +### 次のアクション + +#### Phase 257 P1(将来) +- PHI predecessor mismatch bug 修正(JoinIR merger の PHI ノード生成ロジック) +- Pattern6 全体のテスト整備(forward/reverse 両方の正常動作確認) + +#### Phase 258(次フェーズ) +- quick profile の次の FAIL へ進む +- last_index_of が通った後の最初のエラーを調査 --- -**最終更新**: 2025-12-20 +**最終更新**: 2025-12-20 (Phase 257 P0 実装完了、PHI bug は既知の問題として文書化) 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 58b66494..5cf11753 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 @@ -39,6 +39,15 @@ use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; use crate::mir::builder::MirBuilder; use crate::mir::ValueId; +/// Phase 257 P0: Scan direction for forward/reverse scan +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ScanDirection { + /// Forward scan: i < s.length(), i = i + 1 + Forward, + /// Reverse scan: i >= 0, i = i - 1 + Reverse, +} + /// Phase 254 P1: Extracted structure for scan-with-init pattern /// /// This structure contains all the information needed to lower an index_of-style loop. @@ -50,12 +59,14 @@ struct ScanParts { haystack: String, /// Needle variable name (e.g., "ch") needle: String, - /// Step literal (P0: must be 1) + /// Step literal (Phase 257: can be 1 forward or -1 reverse) step_lit: i64, /// Early return expression (P0: must be Variable(loop_var)) early_return_expr: ASTNode, /// Not-found return literal (P0: must be -1) not_found_return_lit: i64, + /// Scan direction (Phase 257 P0) + scan_direction: ScanDirection, } /// Phase 254 P0: Detection for Pattern 6 (ScanWithInit) @@ -133,16 +144,18 @@ fn contains_methodcall(node: &ASTNode) -> bool { } } -/// Check if value is ConstStep pattern (i = i + 1) +/// Check if value is ConstStep pattern (i = i + 1 or i = i - 1) +/// Phase 257 P0: Accept both forward (Add) and reverse (Subtract) fn is_const_step_pattern(value: &ASTNode) -> bool { match value { ASTNode::BinaryOp { - operator: crate::ast::BinaryOperator::Add, + operator, left, right, .. } => { - matches!(left.as_ref(), ASTNode::Variable { .. }) + matches!(operator, crate::ast::BinaryOperator::Add | crate::ast::BinaryOperator::Subtract) + && matches!(left.as_ref(), ASTNode::Variable { .. }) && matches!(right.as_ref(), ASTNode::Literal { .. }) } _ => false, @@ -168,8 +181,8 @@ fn is_const_step_pattern(value: &ASTNode) -> bool { /// /// # P0 Restrictions /// -/// - Loop condition must be `i < s.length()` -/// - Step must be `i = i + 1` (step_lit == 1) +/// - Loop condition must be `i < s.length()` (forward) or `i >= 0` (reverse) +/// - Step must be `i = i + 1` (forward, step_lit == 1) or `i = i - 1` (reverse, step_lit == -1) /// - Not-found return must be `-1` /// - Early return must be `return loop_var` fn extract_scan_with_init_parts( @@ -179,8 +192,9 @@ fn extract_scan_with_init_parts( ) -> Result, String> { use crate::ast::{BinaryOperator, LiteralValue}; - // 1. Check loop condition: i < s.length() - let (loop_var, haystack) = match condition { + // 1. Check loop condition: i < s.length() (forward) or i >= 0 (reverse) + let (loop_var, haystack_opt, scan_direction) = match condition { + // Forward: i < s.length() ASTNode::BinaryOp { operator: BinaryOperator::Less, left, @@ -202,14 +216,40 @@ fn extract_scan_with_init_parts( _ => return Ok(None), }; - (loop_var, haystack) + (loop_var, Some(haystack), ScanDirection::Forward) + } + // Reverse: i >= 0 + ASTNode::BinaryOp { + operator: BinaryOperator::GreaterEqual, + left, + right, + .. + } => { + let loop_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return Ok(None), + }; + + // Check right is Literal(0) + match right.as_ref() { + ASTNode::Literal { + value: LiteralValue::Integer(0), + .. + } => {} + _ => return Ok(None), + } + + // For reverse, haystack will be extracted from substring call in body + (loop_var, None, ScanDirection::Reverse) } _ => return Ok(None), }; // 2. Find if statement with substring == needle and return loop_var + // Also extract haystack for reverse scans let mut needle_opt = None; let mut early_return_expr_opt = None; + let mut haystack_from_substring_opt = None; for stmt in body { if let ASTNode::If { @@ -242,6 +282,18 @@ fn extract_scan_with_init_parts( left.as_ref() }; + // Phase 257 P0: Extract haystack from substring call for reverse scan + if let ASTNode::MethodCall { + object, method, .. + } = substring_side + { + if method == "substring" { + if let ASTNode::Variable { name: haystack_name, .. } = object.as_ref() { + haystack_from_substring_opt = Some(haystack_name.clone()); + } + } + } + if let ASTNode::Variable { name: needle_name, .. } = needle_side { // Check then_body contains return loop_var if then_body.len() == 1 { @@ -264,7 +316,13 @@ fn extract_scan_with_init_parts( let needle = needle_opt.ok_or_else(|| "No matching needle pattern found")?; let early_return_expr = early_return_expr_opt.ok_or_else(|| "No early return found")?; - // 3. Check for step: i = i + 1 + // Phase 257 P0: Determine haystack based on scan direction + let haystack = match scan_direction { + ScanDirection::Forward => haystack_opt.ok_or_else(|| "Forward scan missing haystack in loop condition")?, + ScanDirection::Reverse => haystack_from_substring_opt.ok_or_else(|| "Reverse scan missing haystack in substring call")?, + }; + + // 3. Check for step: i = i + 1 (forward) or i = i - 1 (reverse) let mut step_lit_opt = None; for stmt in body { @@ -272,7 +330,7 @@ fn extract_scan_with_init_parts( if let ASTNode::Variable { name: target_name, .. } = target.as_ref() { if target_name == &loop_var { if let ASTNode::BinaryOp { - operator: BinaryOperator::Add, + operator, left, right, .. @@ -285,7 +343,15 @@ fn extract_scan_with_init_parts( .. } = right.as_ref() { - step_lit_opt = Some(*lit); + match operator { + BinaryOperator::Add => { + step_lit_opt = Some(*lit); + } + BinaryOperator::Subtract => { + step_lit_opt = Some(-lit); + } + _ => {} + } } } } @@ -297,9 +363,18 @@ fn extract_scan_with_init_parts( let step_lit = step_lit_opt.ok_or_else(|| "No step pattern found")?; - // P0: step must be 1 - if step_lit != 1 { - return Ok(None); + // Phase 257 P0: Verify step matches scan direction + match scan_direction { + ScanDirection::Forward => { + if step_lit != 1 { + return Ok(None); + } + } + ScanDirection::Reverse => { + if step_lit != -1 { + return Ok(None); + } + } } // 4. P0: not-found return must be -1 (hardcoded for now) @@ -312,6 +387,7 @@ fn extract_scan_with_init_parts( step_lit, early_return_expr, not_found_return_lit, + scan_direction, })) } @@ -352,7 +428,6 @@ impl MirBuilder { fn_body: Option<&[ASTNode]>, ) -> Result, String> { use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; - use crate::mir::join_ir::lowering::scan_with_init_minimal::lower_scan_with_init_minimal; use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; let trace = trace::trace(); @@ -410,9 +485,18 @@ impl MirBuilder { ); } - // Step 3: Create JoinModule + // Step 3: Create JoinModule based on scan direction let mut join_value_space = JoinValueSpace::new(); - let join_module = lower_scan_with_init_minimal(&mut join_value_space); + let join_module = match parts.scan_direction { + ScanDirection::Forward => { + use crate::mir::join_ir::lowering::scan_with_init_minimal::lower_scan_with_init_minimal; + lower_scan_with_init_minimal(&mut join_value_space) + } + ScanDirection::Reverse => { + use crate::mir::join_ir::lowering::scan_with_init_reverse::lower_scan_with_init_reverse; + lower_scan_with_init_reverse(&mut join_value_space) + } + }; // Phase 255 P2: Build CarrierInfo for loop variable only // Step 1: Create CarrierInfo with loop variable (i) only diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index 3164eb8f..bf600f81 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -77,6 +77,7 @@ pub mod loop_with_if_phi_if_sum; // Phase 213: Pattern 3 AST-based if-sum lowere pub mod min_loop; pub mod simple_while_minimal; // Phase 188-Impl-1: Pattern 1 minimal lowerer pub mod scan_with_init_minimal; // Phase 254 P1: Pattern 6 minimal lowerer (index_of/find/contains) +pub mod scan_with_init_reverse; // Phase 257 P0: Pattern 6 reverse scan lowerer (last_index_of) pub mod split_scan_minimal; // Phase 256 P0: Pattern 7 minimal lowerer (split/tokenization with variable step) pub mod skip_ws; pub mod stage1_using_resolver; diff --git a/src/mir/join_ir/lowering/scan_with_init_reverse.rs b/src/mir/join_ir/lowering/scan_with_init_reverse.rs new file mode 100644 index 00000000..186a0b20 --- /dev/null +++ b/src/mir/join_ir/lowering/scan_with_init_reverse.rs @@ -0,0 +1,320 @@ +//! Phase 257 P0: Pattern 6 Reverse Scan Lowerer (last_index_of) +//! +//! Target: apps/tests/phase257_p0_last_index_of_min.hako +//! +//! Code: +//! ```nyash +//! static box Main { +//! main() { +//! local s = "hello world" +//! local ch = "o" +//! local i = s.length() - 1 +//! loop(i >= 0) { +//! if s.substring(i, i + 1) == ch { +//! return i +//! } +//! i = i - 1 +//! } +//! return -1 +//! } +//! } +//! ``` +//! +//! Expected JoinIR: +//! ```text +//! fn main(i, ch, s): +//! result = loop_step(i, ch, s) +//! +//! fn loop_step(i, ch, s): +//! // 1. Check exit condition: i < 0 +//! exit_cond = (i < 0) +//! Jump(k_exit, [-1], cond=exit_cond) // Not found case +//! +//! // 2. Calculate i_plus_1 for substring +//! i_plus_1 = i + 1 +//! +//! // 3. Hoist MethodCall(substring) to init-time BoxCall +//! cur = StringBox.substring(s, i, i_plus_1) +//! +//! // 4. Check match condition +//! match = (cur == ch) +//! Jump(k_exit, [i], cond=match) // Found case +//! +//! // 5. Decrement and tail recurse +//! i_minus_1 = i - 1 +//! Call(loop_step, [i_minus_1, ch, s]) +//! +//! fn k_exit(i_exit): +//! return i_exit +//! ``` + +use crate::mir::join_ir::lowering::canonical_names as cn; +use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; +use crate::mir::join_ir::{ + BinOpKind, CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, +}; + +/// Lower Pattern 6 Reverse Scan to JoinIR +/// +/// # Phase 257 P0: Reverse scan (backward iteration) +/// +/// This is a variant of scan_with_init_minimal that scans backward from i = s.length() - 1. +/// +/// ## Key Differences from Forward Scan +/// +/// - Exit condition: `i < 0` instead of `i >= len` +/// - Step: `i = i - 1` instead of `i = i + 1` +/// - Init: Caller must provide `i = s.length() - 1` before calling +/// +/// ## Boundary Contract +/// +/// This function returns a JoinModule with: +/// - **Input slots**: main() params for (i, ch, s) +/// - **Caller responsibility**: Ensure i is initialized to s.length() - 1 +/// - **Exit binding**: k_exit param receives found index or -1 +/// +/// # Arguments +/// +/// * `join_value_space` - Unified ValueId allocator (Phase 202-A) +/// +/// # Returns +/// +/// * `JoinModule` - Successfully lowered to JoinIR +pub(crate) fn lower_scan_with_init_reverse( + join_value_space: &mut JoinValueSpace, +) -> JoinModule { + let mut join_module = JoinModule::new(); + + // ================================================================== + // Function IDs allocation + // ================================================================== + let main_id = JoinFuncId::new(0); + let loop_step_id = JoinFuncId::new(1); + let k_exit_id = JoinFuncId::new(2); + + // ================================================================== + // ValueId allocation + // ================================================================== + // main() params/locals + // Phase 255 P0: Loop variable MUST be first, then alphabetical order [ch, s] + let i_main_param = join_value_space.alloc_param(); // loop index + let ch_main_param = join_value_space.alloc_param(); // needle character (alphabetically first) + let s_main_param = join_value_space.alloc_param(); // haystack string (alphabetically second) + let loop_result = join_value_space.alloc_local(); // result from loop_step + + // loop_step params/locals + let i_step_param = join_value_space.alloc_param(); // loop index + let ch_step_param = join_value_space.alloc_param(); // needle (alphabetically first) + let s_step_param = join_value_space.alloc_param(); // haystack (alphabetically second) + let const_0 = join_value_space.alloc_local(); // 0 for exit condition + let exit_cond = join_value_space.alloc_local(); // i < 0 + let const_minus_1 = join_value_space.alloc_local(); // -1 for not found + let const_1 = join_value_space.alloc_local(); // 1 for i + 1 + let i_plus_1 = join_value_space.alloc_local(); // i + 1 + let cur = join_value_space.alloc_local(); // substring result + let match_cond = join_value_space.alloc_local(); // cur == ch + let i_minus_1 = join_value_space.alloc_local(); // i - 1 + + // k_exit params + let i_exit_param = join_value_space.alloc_param(); // exit parameter (index or -1) + + // ================================================================== + // main() function + // ================================================================== + let mut main_func = JoinFunction::new( + main_id, + "main".to_string(), + vec![i_main_param, ch_main_param, s_main_param], // Phase 255 P0: [i, ch, s] alphabetical + ); + + // result = loop_step(i, ch, s) // Phase 255 P0: alphabetical order + main_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_main_param, ch_main_param, s_main_param], // Phase 255 P0: [i, ch, s] alphabetical + k_next: None, + dst: Some(loop_result), + }); + + // Return loop_result (found index or -1) + main_func.body.push(JoinInst::Ret { value: Some(loop_result) }); + + join_module.add_function(main_func); + + // ================================================================== + // loop_step(i, ch, s) function + // ================================================================== + let mut loop_step_func = JoinFunction::new( + loop_step_id, + cn::LOOP_STEP.to_string(), + vec![i_step_param, ch_step_param, s_step_param], // Phase 255 P0: [i, ch, s] alphabetical + ); + + // 1. const 0 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_0, + value: ConstValue::Integer(0), + })); + + // 2. exit_cond = (i < 0) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Compare { + dst: exit_cond, + op: CompareOp::Lt, + lhs: i_step_param, + rhs: const_0, + })); + + // 3. const -1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_minus_1, + value: ConstValue::Integer(-1), + })); + + // 4. Jump(k_exit, [-1], cond=exit_cond) - not found case + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: vec![const_minus_1], + cond: Some(exit_cond), + }); + + // 5. i_plus_1 = i + 1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1, + value: ConstValue::Integer(1), + })); + + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_plus_1, + op: BinOpKind::Add, + lhs: i_step_param, + rhs: const_1, + })); + + // 6. cur = s.substring(i, i_plus_1) - init-time BoxCall + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(cur), + box_name: "StringBox".to_string(), + method: "substring".to_string(), + args: vec![s_step_param, i_step_param, i_plus_1], + })); + + // 7. match_cond = (cur == ch) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Compare { + dst: match_cond, + op: CompareOp::Eq, + lhs: cur, + rhs: ch_step_param, + })); + + // 8. Jump(k_exit, [i], cond=match_cond) - found case + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: vec![i_step_param], + cond: Some(match_cond), + }); + + // 9. i_minus_1 = i - 1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_minus_1, + op: BinOpKind::Sub, + lhs: i_step_param, + rhs: const_1, + })); + + // 10. Call(loop_step, [i_minus_1, ch, s]) - tail recursion + loop_step_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_minus_1, ch_step_param, s_step_param], // Phase 255 P0: [i_minus_1, ch, s] alphabetical + k_next: None, // CRITICAL: None for tail call + dst: None, + }); + + join_module.add_function(loop_step_func); + + // ================================================================== + // k_exit(i_exit) function + // ================================================================== + let mut k_exit_func = JoinFunction::new(k_exit_id, cn::K_EXIT.to_string(), vec![i_exit_param]); + + // Return i_exit (found index or -1) + k_exit_func.body.push(JoinInst::Ret { + value: Some(i_exit_param), + }); + + join_module.add_function(k_exit_func); + + // Set entry point + join_module.entry = Some(main_id); + + eprintln!("[joinir/pattern6] Generated JoinIR for ScanWithInit Reverse Pattern"); + eprintln!("[joinir/pattern6] Functions: main, loop_step, k_exit"); + eprintln!("[joinir/pattern6] Direction: Reverse (i >= 0, i = i - 1)"); + + join_module +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lower_scan_with_init_reverse() { + let mut join_value_space = JoinValueSpace::new(); + + let join_module = lower_scan_with_init_reverse(&mut join_value_space); + + // main + loop_step + k_exit の3関数 + assert_eq!(join_module.functions.len(), 3); + + // Entry が main(0) に設定されている + assert_eq!(join_module.entry, Some(JoinFuncId::new(0))); + + // k_exit 関数が取れる + let k_exit_func = join_module + .functions + .get(&JoinFuncId::new(2)) + .expect("k_exit function should exist"); + assert_eq!(k_exit_func.name, cn::K_EXIT); + } + + #[test] + fn test_loop_step_has_reverse_step() { + let mut join_value_space = JoinValueSpace::new(); + + let join_module = lower_scan_with_init_reverse(&mut join_value_space); + + // loop_step 関数を取得 + let loop_step = join_module + .functions + .get(&JoinFuncId::new(1)) + .expect("loop_step function should exist"); + + // BinOp(Sub) (i - 1) が含まれることを確認 + let has_decrement = loop_step.body.iter().any(|inst| { + matches!( + inst, + JoinInst::Compute(MirLikeInst::BinOp { op: BinOpKind::Sub, .. }) + ) + }); + + assert!( + has_decrement, + "loop_step should contain i - 1 decrement" + ); + } +}