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:
@ -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)
|
||||
|
||||
|
||||
@ -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を揃える)
|
||||
|
||||
@ -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 専用パターンとして箱を追加する。
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(¤t) {
|
||||
for &successor in &block.successors {
|
||||
if reachable.insert(successor) {
|
||||
queue.push_back(successor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reachable
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user