feat(joinir): Phase 257 P1.1/P1.2/P1.3 - Pattern6 SSOT + LoopHeaderPhi CFG fix

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 <noreply@anthropic.com>
This commit is contained in:
2025-12-20 23:30:27 +09:00
parent 9ba89bada2
commit 73ddc5f58d
10 changed files with 492 additions and 71 deletions

View File

@ -6,6 +6,28 @@
- Phase 143-loopvocab P3+: 条件スコープ拡張impure conditions 対応)
- 詳細: `docs/development/current/main/30-Backlog.md`
## 2025-12-20Phase 257 P1.1/P1.2/P1.3 完了 ✅
- **P1.1**: Pattern6 SSOT fixfalse positive 根治)
- `can_lower()``extract_scan_with_init_parts()` を呼び出して SSOT を確立
- `index_of_string/2` の誤検出が解消(`Ok(None)` で graceful fall-through
- **P1.2**: LoopHeaderPhi CFG correctionentry 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-19Phase 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-20Phase 258is_integer nested-if + loop🔜
- Phase 258 README: `docs/development/current/main/phases/phase-258/README.md`
## 2025-12-19Phase 254index_of loop pattern✅ 完了Blocked by Phase 255

View File

@ -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 positiveexit=1衝突`--verify` + VM error 検出で抑止
## Minimal Fixture
- `apps/tests/phase257_p0_last_index_of_min.hako`
@ -47,6 +54,13 @@ JoinIR パターンがこの形を受理できていないのが本体。
- Pattern7SplitScanは適用対象外。
- Pattern3/1 は `return` を含む loop を扱わないor 目的が違う)。
更新P0/P1後:
- Pattern6 の検出/生成はできているが、merge 側が “CFGsuccessors/predsと terminator” の同期を前提にしていた。
- joinir merge 中で `terminator` 直書きがあり、`successors` が未同期になることがあるCFG 解析が欠ける)
- finalize 時点では host entry jump が未設定なため、header preds だけでは entry pred を復元できない場合がある
- これらは P1 で補正済みfail-fast + 復元ロジック)
## DecisionPhase 257 の方針)
Phase 257 では、以下で進める:
@ -54,6 +68,11 @@ Phase 257 では、以下で進める:
- Pattern6ScanWithInitを “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を揃える

View File

@ -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 Pattern6ScanWithInitto 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): Pattern6ScanWithInit reverse scan + early return に拡張し、PHI/CFG を fail-fast + 自動補正で安定化
---
@ -120,6 +120,60 @@ Pattern6ScanWithInitを “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 専用パターンとして箱を追加する。

View File

@ -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<BasicBlockId> = 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<BasicBlockId> = 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<MirInstruction> = 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::<Vec<_>>()
.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,
);

View File

@ -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

View File

@ -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);

View File

@ -83,3 +83,111 @@ pub fn check_merge_uses(function: &MirFunction) -> Result<(), Vec<VerificationEr
Err(errors)
}
}
/// Phase 257 P1-1: Verify PHI inputs reference actual CFG predecessors
///
/// Checks:
/// 1. Each PHI input references an actual CFG predecessor (no phantom predecessors)
/// 2. All reachable predecessors have corresponding PHI inputs (no missing inputs)
pub(super) fn check_phi_predecessors(
function: &crate::mir::MirFunction,
) -> Result<(), Vec<crate::mir::verification_types::VerificationError>> {
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<crate::mir::BasicBlockId> {
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(&current) {
for &successor in &block.successors {
if reachable.insert(successor) {
queue.push_back(successor);
}
}
}
}
reachable
}

View File

@ -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

View File

@ -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

View File

@ -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