From 73ddc5f58dba1edc47232cf3ae44406d386349fa Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sat, 20 Dec 2025 23:30:27 +0900 Subject: [PATCH] feat(joinir): Phase 257 P1.1/P1.2/P1.3 - Pattern6 SSOT + LoopHeaderPhi CFG fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1.1: Pattern6 false positive fix (SSOT approach) - can_lower() now calls extract_scan_with_init_parts() for SSOT - index_of_string/2 no longer triggers false positive - Graceful fall-through with Ok(None) P1.2: LoopHeaderPhi CFG-based correction - Step 0: Manual successor update from terminators - CFG-based entry predecessor computation (header_preds - latch) - Multi-entry-pred support (bb0 host + bb10 JoinIR main) - Explicit host_entry_block addition (emit_jump runs after finalize) P1.3: Smoke script validation - phase254_p0_index_of_vm.sh: --verify + VM error detection - phase257 smokes updated Acceptance criteria (all PASS): ✅ phase254_p0_index_of_min.hako verify ✅ phase257_p0_last_index_of_min.hako verify ✅ ./tools/smokes/v2/run.sh --profile quick (no Pattern6 false positive) Technical discovery: - Host entry block (bb0) Jump set in Phase 6 (after finalize) - instruction_rewriter bypasses set_terminator(), skips successor update 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- docs/development/current/main/10-Now.md | 28 +++ .../phase-257-last-index-of-loop-shape.md | 19 ++ .../current/main/phases/phase-257/README.md | 60 +++++- .../joinir/merge/loop_header_phi_builder.rs | 190 +++++++++++++++++- .../patterns/pattern6_scan_with_init.rs | 78 +++---- src/mir/verification.rs | 8 +- src/mir/verification/cfg.rs | 108 ++++++++++ .../apps/phase254_p0_index_of_vm.sh | 52 +++-- .../phase257_p0_last_index_of_llvm_exe.sh | 10 +- .../apps/phase257_p0_last_index_of_vm.sh | 10 +- 10 files changed, 492 insertions(+), 71 deletions(-) diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index f85e727f..f6cd8932 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -6,6 +6,28 @@ - Phase 143-loopvocab P3+: 条件スコープ拡張(impure conditions 対応) - 詳細: `docs/development/current/main/30-Backlog.md` +## 2025-12-20:Phase 257 P1.1/P1.2/P1.3 完了 ✅ + +- **P1.1**: Pattern6 SSOT fix(false positive 根治) + - `can_lower()` が `extract_scan_with_init_parts()` を呼び出して SSOT を確立 + - `index_of_string/2` の誤検出が解消(`Ok(None)` で graceful fall-through) +- **P1.2**: LoopHeaderPhi CFG correction(entry predecessor 自動計算) + - Step 0: `update_successors_from_terminator()` を手動実行(instruction_rewriter が set_terminator() をバイパスするため) + - CFG から entry predecessors を計算(header_preds - latch_block) + - 複数 entry preds 対応(bb0 host + bb10 JoinIR main) + - host_entry_block を明示的に追加(emit_jump が finalize() 後に実行されるため) +- **P1.3**: Smoke script validation(既存実装確認) + - `phase254_p0_index_of_vm.sh`:`--verify` + VM error detection で false positive 回避 + +**検証結果**: +- ✅ `phase254_p0_index_of_min.hako` verify → PASS +- ✅ `phase257_p0_last_index_of_min.hako` verify → PASS +- ✅ `./tools/smokes/v2/run.sh --profile quick` → Pattern6 false positive なし(別の未サポートパターンで止まる) + +**技術的発見**: +- JoinIR merge では host entry block (bb0) の Jump が Phase 6 で設定されるため、Phase 4.5 の finalize() 時点で CFG に現れない +- 解決策:`builder.current_block` を捕捉し、CFG 計算後に明示的に entry_preds へ追加 + ## 2025-12-19:Phase 146/147 完了 ✅ - Phase 146 README: `docs/development/current/main/phases/phase-146/README.md` @@ -70,6 +92,12 @@ - Phase 257 README: `docs/development/current/main/phases/phase-257/README.md` - Goal: `StringUtils.last_index_of/2` を JoinIR で受理し、`--profile quick` を緑に戻す - Investigation(最小再現/論点): `docs/development/current/main/investigations/phase-257-last-index-of-loop-shape.md` + - Status: Pattern6 reverse scan + PHI/CFG 安定化は完了(最初の FAIL は次へ移動) + - Current first FAIL: `json_lint_vm / StringUtils.is_integer/1`(nested-if + loop pattern, unsupported) + +## 2025-12-20:Phase 258(is_integer nested-if + loop)🔜 + +- Phase 258 README: `docs/development/current/main/phases/phase-258/README.md` ## 2025-12-19:Phase 254(index_of loop pattern)✅ 完了(Blocked by Phase 255) diff --git a/docs/development/current/main/investigations/phase-257-last-index-of-loop-shape.md b/docs/development/current/main/investigations/phase-257-last-index-of-loop-shape.md index 2dae9878..bb282833 100644 --- a/docs/development/current/main/investigations/phase-257-last-index-of-loop-shape.md +++ b/docs/development/current/main/investigations/phase-257-last-index-of-loop-shape.md @@ -13,6 +13,13 @@ Related: - `json_lint_vm` が `StringUtils.last_index_of/2` で停止 - エラー: `[joinir/freeze] Loop lowering failed: JoinIR does not support this pattern, and LoopBuilder has been removed.` +更新(P0/P1後): + +- Pattern6 を reverse scan 対応した後、`phi pred mismatch` が露出したが、P1 で以下により根治した: + - MIR verifier で PHI predecessor を fail-fast 検証 + - loop header PHI の entry edge を CFG から復元(self pred 防止) + - smoke の false positive(exit=1衝突)を `--verify` + VM error 検出で抑止 + ## Minimal Fixture - `apps/tests/phase257_p0_last_index_of_min.hako` @@ -47,6 +54,13 @@ JoinIR パターンがこの形を受理できていないのが本体。 - Pattern7(SplitScan)は適用対象外。 - Pattern3/1 は `return` を含む loop を扱わない(or 目的が違う)。 +更新(P0/P1後): + +- Pattern6 の検出/生成はできているが、merge 側が “CFG(successors/preds)と terminator” の同期を前提にしていた。 + - joinir merge 中で `terminator` 直書きがあり、`successors` が未同期になることがある(CFG 解析が欠ける) + - finalize 時点では host entry jump が未設定なため、header preds だけでは entry pred を復元できない場合がある + - これらは P1 で補正済み(fail-fast + 復元ロジック) + ## Decision(Phase 257 の方針) Phase 257 では、以下で進める: @@ -54,6 +68,11 @@ Phase 257 では、以下で進める: - Pattern6(ScanWithInit)を “scan direction” 付きに一般化し、reverse scan + early return を受理する。 - Phase 256 で固めた Contract(`JumpArgsLayout`, pipeline contract checks)に従い、merge 側で推測しない。 +追加(P1): + +- MIR verifier に `InvalidPhi` チェックを追加し、`phi pred mismatch` を `--verify` で fail-fast にする +- loop header PHI の entry edge source を正す(必要なら preheader を生成) + ## Questions(将来に残す設計論点) 1. `LoopPatternKind` に Pattern6/7 を増やすべきか?(router 側での分類SSOTを揃える) diff --git a/docs/development/current/main/phases/phase-257/README.md b/docs/development/current/main/phases/phase-257/README.md index 2dbb4e0c..1d40190e 100644 --- a/docs/development/current/main/phases/phase-257/README.md +++ b/docs/development/current/main/phases/phase-257/README.md @@ -8,9 +8,9 @@ Related: ## Current Status (SSOT) -- Target first FAIL: `json_lint_vm / StringUtils.last_index_of/2` -- Pattern: Loop with early return (backward scan) -- Approach: Extend Pattern6(ScanWithInit)to support reverse scan + early return +- Former first FAIL: `json_lint_vm / StringUtils.last_index_of/2`(P0/P1で解消) +- Current first FAIL: `json_lint_vm / StringUtils.is_integer/1`(nested-if + loop, still unsupported) +- Approach (done): Pattern6(ScanWithInit)を reverse scan + early return に拡張し、PHI/CFG を fail-fast + 自動補正で安定化 --- @@ -120,6 +120,60 @@ Pattern6(ScanWithInit)を “scan direction” を持つ形に一般化す - Phase 256 系の契約(`JumpArgsLayout` / contract checks)に従い、推測しない - `expr_result` は `return i` の経路でのみ使用(not-found は `-1`) +--- + +## Progress + +### P0(完了) + +- Pattern6 を双方向 scan に拡張(forward/reverse) + - reverse scan 用 lowerer を追加(`scan_with_init_reverse.rs`) + - `apps/tests/phase257_p0_last_index_of_min.hako` を追加 +- ただし現状は「PHI predecessor mismatch」が先に露出しており、P1 でインフラ不変条件を固定する必要がある + +## Phase 257 P1(次の指示書 / SSOT) + +### Goal + +- Pattern6 の実行時 `phi pred mismatch` を根治し、`index_of` / `last_index_of` が VM で正常に走る +- `./tools/smokes/v2/run.sh --profile quick` の最初の FAIL を次へ進める + +### Tasks(順序) + +1) MIR verifier を強化して `InvalidPhi` を検出する(fail-fast) + - 期待: phi inputs が「ブロックの predecessor 全部」をカバーし、自己ブロック(self)を含まない +2) Pattern6 の loop header PHI の entry edge source を正す + - `entry_block == header_block` になっているケースを禁止し、必要なら preheader を作る or merge entry を main に寄せる +3) smoke の false positive を防ぐ + - `phase254_p0_index_of_vm.sh` は `--verify` を併用するか、VM error を検出して FAIL にする + +--- + +## Progress + +### P0(完了) + +- Pattern6 を双方向 scan に拡張(forward/reverse) + - reverse scan 用 lowerer を追加 + - fixture: `apps/tests/phase257_p0_last_index_of_min.hako` + +### P1(完了) + +- Pattern6 の誤検出を防止(detect/extract SSOT 化) + - `index_of_string/2` など “近いが別形” を `Ok(None)` で fall-through させる +- MIR verifier に PHI predecessor 検証を追加(fail-fast) + - unreachable pred は除外して現実的に運用 +- loop header PHI の entry edge を CFG から復元(self pred 根治) + - merge 内で `terminator` 直書きにより `successors` が同期されないケースを補正 + - finalize 時点で host entry jump が未設定なため、host entry predecessor を明示的に補う +- smoke の false positive を抑止(`--verify` + VM error 検出) + +## Next (Phase 258 proposal) + +- `StringUtils.is_integer/1` の loop を JoinIR で受理する(caps=If,Loop,NestedIf,Return) + - ループ形: `loop(i < s.length()) { if not is_digit(...) return false; i=i+1 } return true` + - 前処理に nested-if がある(`start` の決定) + **Option B: Pattern8_ReverseScanReturn 新設** Pattern6 を触らずに、reverse scan 専用パターンとして箱を追加する。 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 0d4a8c9b..51d27939 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 @@ -308,6 +308,20 @@ impl LoopHeaderPhiBuilder { } } + // Phase 257 P1.2-FIX: Capture builder's current_block BEFORE getting mutable reference + // This is the host entry block that will emit_jump to the loop header + let host_entry_block_opt = builder.current_block; + + if dev_debug { + trace.stderr_if( + &format!( + "[joinir/header-phi] Host entry block (will jump to header): {:?}", + host_entry_block_opt + ), + true, + ); + } + // Get the header block from current function let current_func = builder .scope_ctx @@ -315,6 +329,158 @@ impl LoopHeaderPhiBuilder { .as_mut() .ok_or("Phase 33-16: No current function when finalizing header PHIs")?; + // Phase 257 P1.2: Compute entry predecessor from CFG (correction, not just validation) + use crate::mir::verification::utils::compute_predecessors; + + // Step 0: Update successors from terminators (instruction_rewriter sets terminator directly, bypassing set_terminator()) + // This ensures the CFG is ready for compute_predecessors() + if dev_debug { + trace.stderr_if( + &format!( + "[joinir/header-phi] Step 0: Updating successors for {} blocks", + current_func.blocks.len() + ), + true, + ); + } + for (bid, block) in current_func.blocks.iter_mut() { + if block.terminator.is_some() { + block.successors.clear(); + if let Some(ref terminator) = block.terminator { + match terminator { + crate::mir::MirInstruction::Branch { then_bb, else_bb, .. } => { + block.successors.insert(*then_bb); + block.successors.insert(*else_bb); + if dev_debug { + trace.stderr_if( + &format!( + "[joinir/header-phi] Step 0: bb{} Branch → [{}, {}]", + bid.0, then_bb.0, else_bb.0 + ), + true, + ); + } + } + crate::mir::MirInstruction::Jump { target } => { + block.successors.insert(*target); + if dev_debug { + trace.stderr_if( + &format!( + "[joinir/header-phi] Step 0: bb{} Jump → bb{}", + bid.0, target.0 + ), + true, + ); + } + } + _ => { + if dev_debug { + trace.stderr_if( + &format!("[joinir/header-phi] Step 0: bb{} has other terminator", bid.0), + true, + ); + } + } + } + } + } else if dev_debug { + trace.stderr_if( + &format!("[joinir/header-phi] Step 0: bb{} has NO terminator", bid.0), + true, + ); + } + } + + // Step 1: Compute CFG predecessors + let preds = compute_predecessors(current_func); + let header_preds = preds.get(&info.header_block).ok_or_else(|| { + format!( + "[loop_header_phi_builder] Loop header bb{} has no predecessors in CFG", + info.header_block.0 + ) + })?; + + // Step 2: Identify latch block (all carriers must agree) + let latch_block = { + let mut latch_opt: Option = None; + for (name, entry) in &info.carrier_phis { + let (carrier_latch, _) = entry.latch_incoming.ok_or_else(|| { + format!("[loop_header_phi_builder] Carrier '{}' missing latch_incoming", name) + })?; + + if let Some(expected_latch) = latch_opt { + if carrier_latch != expected_latch { + return Err(format!( + "[loop_header_phi_builder] Latch block mismatch: carrier '{}' has bb{}, expected bb{}", + name, carrier_latch.0, expected_latch.0 + )); + } + } else { + latch_opt = Some(carrier_latch); + } + } + latch_opt.ok_or_else(|| { + "[loop_header_phi_builder] No carriers found (cannot determine latch)".to_string() + })? + }; + + // Step 3: Compute entry predecessors (header_preds - latch_block) + // Phase 257 P1.2-FIX: Multiple entry preds are OK (bb0 host + bb10 JoinIR main) + let mut entry_preds: Vec = header_preds + .iter() + .filter(|&&pred| pred != latch_block) + .copied() + .collect(); + + // Phase 257 P1.2-FIX: Add host_entry_block if not already in entry_preds + // The host block's terminator (Jump to loop header) is emitted AFTER finalize(), + // so it won't appear in the CFG yet. We need to manually add it. + if let Some(host_entry_block) = host_entry_block_opt { + if !entry_preds.contains(&host_entry_block) && host_entry_block != latch_block { + entry_preds.push(host_entry_block); + if dev_debug { + trace.stderr_if( + &format!( + "[joinir/header-phi] Added host_entry_block bb{} to entry_preds (terminator not set yet)", + host_entry_block.0 + ), + true, + ); + } + } + } + + // Step 4: Validate at least one entry predecessor + if entry_preds.is_empty() { + return Err(format!( + "[loop_header_phi_builder] No entry predecessor found for header bb{} \ + (all preds are latch bb{}). Hint: Check JoinIR merger - missing entry edge?", + info.header_block.0, latch_block.0 + )); + } + + if dev_debug { + let host_desc = host_entry_block_opt.map_or_else(|| "None".to_string(), |bb| format!("bb{}", bb.0)); + trace.stderr_if( + &format!( + "[joinir/header-phi] Entry predecessors: {:?} (latch=bb{}, host={}, total_preds={})", + entry_preds, latch_block.0, host_desc, header_preds.len() + ), + true, + ); + } + + // Step 5: Validate no entry_pred is the header itself (catch CFG bug) + for &entry_pred in &entry_preds { + if entry_pred == info.header_block { + return Err(format!( + "[loop_header_phi_builder] Entry predecessor bb{} is loop header itself (bb{}). \ + This indicates a CFG construction bug (self-loop without latch).", + entry_pred.0, info.header_block.0 + )); + } + } + let header_block = current_func .blocks .get_mut(&info.header_block) @@ -329,23 +495,37 @@ impl LoopHeaderPhiBuilder { // Sorted by carrier name for determinism let mut phi_instructions: Vec = Vec::new(); + // Step 6: Insert PHIs with inputs from ALL entry preds + latch + // Phase 257 P1.2-FIX: Handle multiple entry predecessors (bb0 host + bb10 JoinIR main) for (name, entry) in &info.carrier_phis { - let (entry_block, entry_val) = entry.entry_incoming; - let (latch_block, latch_val) = entry.latch_incoming.unwrap(); + let (_stored_entry_block, entry_val) = entry.entry_incoming; // Use value only + let (latch_block_stored, latch_val) = entry.latch_incoming.unwrap(); + + // Build PHI inputs: all entry preds use same init value, latch uses next value + let mut phi_inputs = Vec::new(); + for &entry_pred in &entry_preds { + phi_inputs.push((entry_pred, entry_val)); + } + phi_inputs.push((latch_block_stored, latch_val)); let phi = MirInstruction::Phi { dst: entry.phi_dst, - inputs: vec![(entry_block, entry_val), (latch_block, latch_val)], + inputs: phi_inputs.clone(), type_hint: None, }; phi_instructions.push(phi); if dev_debug { + let phi_desc = phi_inputs + .iter() + .map(|(bb, val)| format!("bb{} → {:?}", bb.0, val)) + .collect::>() + .join(", "); trace.stderr_if( &format!( - "[joinir/header-phi] Finalized carrier '{}' PHI: {:?} = phi [({:?}, {:?}), ({:?}, {:?})]", - name, entry.phi_dst, entry_block, entry_val, latch_block, latch_val + "[joinir/header-phi] Carrier '{}': phi {:?} = [{}]", + name, entry.phi_dst, phi_desc ), true, ); 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 5cf11753..99990675 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 @@ -79,57 +79,41 @@ struct ScanParts { /// /// Detection is structure-based only (no function name checks). pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool { - use crate::mir::loop_pattern_detection::LoopPatternKind; - - // Phase 254 P0: Accept Pattern2Break OR Pattern3IfPhi - // - Pattern2Break: loop with break statement - // - Pattern3IfPhi: loop with return statement (not counted as break) - // index_of has return (early exit), which is classified as Pattern3IfPhi - match ctx.pattern_kind { - LoopPatternKind::Pattern2Break | LoopPatternKind::Pattern3IfPhi => { - // Continue to structure checks + // Phase 257 P1.1: SSOT between detect and extract + // Call extract to check if pattern is actually extractable + // This prevents false positives where detection is too broad + match extract_scan_with_init_parts(ctx.condition, ctx.body, None) { + Ok(Some(_)) => { + // Pattern is extractable + if ctx.debug { + trace::trace().debug( + "pattern6/can_lower", + "accept: pattern extractable (SSOT verified)", + ); + } + true } - _ => return false, - } - - // Check for if statement with MethodCall in condition - let has_if_with_methodcall = ctx.body.iter().any(|stmt| { - matches!(stmt, ASTNode::If { condition, .. } if contains_methodcall(condition)) - }); - - if !has_if_with_methodcall { - if ctx.debug { - trace::trace().debug( - "pattern6/can_lower", - "reject: no if with MethodCall in condition", - ); + Ok(None) => { + // Not this pattern (fall through to other patterns) + if ctx.debug { + trace::trace().debug( + "pattern6/can_lower", + "reject: pattern not extractable (Ok(None))", + ); + } + false } - return false; - } - - // Check for ConstStep (i = i + 1) - let has_const_step = ctx.body.iter().any(|stmt| { - matches!(stmt, ASTNode::Assignment { value, .. } if is_const_step_pattern(value)) - }); - - if !has_const_step { - if ctx.debug { - trace::trace().debug( - "pattern6/can_lower", - "reject: no ConstStep pattern found", - ); + Err(e) => { + // Extraction error (log in debug mode, fall through) + if ctx.debug { + trace::trace().debug( + "pattern6/can_lower", + &format!("reject: extraction error: {}", e), + ); + } + false } - return false; } - - if ctx.debug { - trace::trace().debug( - "pattern6/can_lower", - "MATCHED: ScanWithInit pattern detected", - ); - } - - true } /// Check if AST node contains MethodCall diff --git a/src/mir/verification.rs b/src/mir/verification.rs index 5c533b9d..f38c2e74 100644 --- a/src/mir/verification.rs +++ b/src/mir/verification.rs @@ -13,7 +13,7 @@ mod cfg; mod dom; mod legacy; mod ssa; -mod utils; +pub(crate) mod utils; // Phase 257 P1-2: Made public for loop_header_phi_builder /// MIR verifier for SSA form and semantic correctness pub struct MirVerifier { @@ -164,6 +164,12 @@ impl MirVerifier { if let Err(mut cfg_errors) = self.verify_control_flow(function) { local_errors.append(&mut cfg_errors); } + + // Phase 257 P1-1: PHI predecessor validation + if let Err(mut phi_errors) = cfg::check_phi_predecessors(function) { + local_errors.append(&mut phi_errors); + } + // 4. Check merge-block value usage (ensure Phi is used) if let Err(mut merge_errors) = self.verify_merge_uses(function) { local_errors.append(&mut merge_errors); diff --git a/src/mir/verification/cfg.rs b/src/mir/verification/cfg.rs index 7429c4f9..2c70bdbd 100644 --- a/src/mir/verification/cfg.rs +++ b/src/mir/verification/cfg.rs @@ -83,3 +83,111 @@ pub fn check_merge_uses(function: &MirFunction) -> Result<(), Vec Result<(), Vec> { + use crate::mir::verification::utils::compute_predecessors; + use crate::mir::verification_types::VerificationError; + use crate::mir::MirInstruction; + use std::collections::HashSet; + + let mut errors = Vec::new(); + let preds = compute_predecessors(function); + + // Compute reachable blocks to filter out unreachable ones + // (Unreachable blocks may have incomplete PHIs, which is OK) + let reachable = compute_reachable_blocks(function); + + for (block_id, block) in &function.blocks { + // Skip unreachable blocks + if !reachable.contains(block_id) { + continue; + } + + for instr in &block.instructions { + if let MirInstruction::Phi { dst, inputs, .. } = instr { + let expected_preds = match preds.get(block_id) { + Some(p) => p, + None => { + errors.push(VerificationError::InvalidPhi { + phi_value: *dst, + block: *block_id, + reason: format!("Block bb{} has PHI but no predecessors", block_id.0), + }); + continue; + } + }; + + // Collect PHI input predecessor blocks + let phi_input_preds: HashSet<_> = inputs.iter().map(|(bb, _)| *bb).collect(); + + // Check 1: Each PHI input block is actually a predecessor (no phantom preds) + for (pred_block, _value) in inputs { + if !expected_preds.contains(pred_block) { + errors.push(VerificationError::InvalidPhi { + phi_value: *dst, + block: *block_id, + reason: format!( + "PHI dst={:?} has input from non-predecessor bb{} (actual preds: {:?})", + dst, pred_block.0, expected_preds + ), + }); + } + } + + // Check 2: All reachable predecessors have PHI inputs (no missing inputs) + // This is CRITICAL - catches the "no input for predecessor" runtime error + for &expected_pred in expected_preds { + // Only check reachable predecessors + if reachable.contains(&expected_pred) && !phi_input_preds.contains(&expected_pred) { + errors.push(VerificationError::InvalidPhi { + phi_value: *dst, + block: *block_id, + reason: format!( + "PHI dst={:?} missing input from reachable predecessor bb{} (has inputs from: {:?})", + dst, expected_pred.0, phi_input_preds + ), + }); + } + } + } + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } +} + +/// Compute reachable blocks from entry block +fn compute_reachable_blocks(function: &crate::mir::MirFunction) -> std::collections::HashSet { + use crate::mir::BasicBlockId; + use std::collections::{HashSet, VecDeque}; + + let mut reachable = HashSet::new(); + let mut queue = VecDeque::new(); + + let entry = BasicBlockId(0); // Entry block is always bb0 + queue.push_back(entry); + reachable.insert(entry); + + while let Some(current) = queue.pop_front() { + if let Some(block) = function.blocks.get(¤t) { + for &successor in &block.successors { + if reachable.insert(successor) { + queue.push_back(successor); + } + } + } + } + + reachable +} diff --git a/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh index 2970e6dd..aaeef984 100644 --- a/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh +++ b/tools/smokes/v2/profiles/integration/apps/phase254_p0_index_of_vm.sh @@ -1,22 +1,48 @@ -#!/bin/bash -# Phase 254 P0: index_of 形 loop (VM backend) +#!/usr/bin/env bash +# Phase 254 P0: index_of pattern (forward scan) - VM set -euo pipefail -HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" -HAKO_PATH="apps/tests/phase254_p0_index_of_min.hako" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd)" +HAKORUNE_BIN="${HAKORUNE_BIN:-$PROJECT_ROOT/target/release/hakorune}" +HAKO_PATH="$PROJECT_ROOT/apps/tests/phase254_p0_index_of_min.hako" -# Test: "abc".index_of("b") → 1 -EXPECTED_EXIT=1 +echo "[INFO] Environment check passed" +echo "[INFO] Plugin mode: dynamic" +echo "[INFO] Dynamic plugins check passed" +# Phase 257 P1-3: Step 1 - Add --verify flag (fail-fast on MIR errors) set +e -$HAKORUNE_BIN --backend vm "$HAKO_PATH" -actual_exit=$? +VERIFY_OUTPUT=$("$HAKORUNE_BIN" --backend vm --verify "$HAKO_PATH" 2>&1) +VERIFY_EXIT=$? set -e -if [[ $actual_exit -eq $EXPECTED_EXIT ]]; then - echo "✅ phase254_p0_index_of_vm: PASS (exit=$actual_exit)" - exit 0 -else - echo "❌ phase254_p0_index_of_vm: FAIL (expected=$EXPECTED_EXIT, got=$actual_exit)" +if [ "$VERIFY_EXIT" -ne 0 ]; then + echo "❌ phase254_p0_index_of_vm: FAIL (MIR verification failed)" + echo "$VERIFY_OUTPUT" + exit 1 +fi + +# Phase 257 P1-3: Step 2 - Run VM with error detection +set +e +OUTPUT=$("$HAKORUNE_BIN" --backend vm "$HAKO_PATH" 2>&1) +EXIT_CODE=$? +set -e + +# Check for VM errors in output (regardless of exit code) +if echo "$OUTPUT" | grep -Ei "error|panic|undefined|phi pred mismatch"; then + echo "❌ phase254_p0_index_of_vm: FAIL (VM runtime error detected)" + echo "$OUTPUT" + exit 1 +fi + +# Validate expected exit code (now safe - we've ruled out errors) +EXPECTED_EXIT=1 +if [ "$EXIT_CODE" -eq "$EXPECTED_EXIT" ]; then + echo "✅ phase254_p0_index_of_vm: PASS (exit=$EXIT_CODE, no errors)" + exit 0 +else + echo "❌ phase254_p0_index_of_vm: FAIL (exit=$EXIT_CODE, expected $EXPECTED_EXIT)" + echo "$OUTPUT" exit 1 fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_llvm_exe.sh index af448646..342140b9 100644 --- a/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_llvm_exe.sh +++ b/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_llvm_exe.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Phase 257 P0: last_index_of pattern (loop with early return) - LLVM EXE +# Phase 257 P0: last_index_of pattern (reverse scan) - LLVM EXE set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -9,6 +9,14 @@ HAKORUNE_BIN="${HAKORUNE_BIN:-$PROJECT_ROOT/target/release/hakorune}" echo "[INFO] Environment check passed" echo "[INFO] Plugin mode: dynamic" echo "[INFO] Dynamic plugins check passed" +echo "[DEBUG] PROJECT_ROOT=$PROJECT_ROOT" +echo "[DEBUG] Looking for: $PROJECT_ROOT/apps/tests/phase257_p0_last_index_of_min.hako" + +# Fail-fast: Check fixture exists +if [ ! -f "$PROJECT_ROOT/apps/tests/phase257_p0_last_index_of_min.hako" ]; then + echo "[FAIL] phase257_p0_last_index_of_llvm_exe: Fixture not found" + exit 1 +fi # Run LLVM with the Phase 257 P0 fixture set +e diff --git a/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_vm.sh index c226b247..5d6643c7 100644 --- a/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_vm.sh +++ b/tools/smokes/v2/profiles/integration/apps/phase257_p0_last_index_of_vm.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Phase 257 P0: last_index_of pattern (loop with early return) - VM +# Phase 257 P0: last_index_of pattern (reverse scan) - VM set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -9,6 +9,14 @@ HAKORUNE_BIN="${HAKORUNE_BIN:-$PROJECT_ROOT/target/release/hakorune}" echo "[INFO] Environment check passed" echo "[INFO] Plugin mode: dynamic" echo "[INFO] Dynamic plugins check passed" +echo "[DEBUG] PROJECT_ROOT=$PROJECT_ROOT" +echo "[DEBUG] Looking for: $PROJECT_ROOT/apps/tests/phase257_p0_last_index_of_min.hako" + +# Fail-fast: Check fixture exists +if [ ! -f "$PROJECT_ROOT/apps/tests/phase257_p0_last_index_of_min.hako" ]; then + echo "[FAIL] phase257_p0_last_index_of_vm: Fixture not found" + exit 1 +fi # Run VM with the Phase 257 P0 fixture set +e