fix(joinir): Phase 287 P2 - Pattern6 nested loop latch overwrite fix

Fix infinite loop in Pattern6 (nested loop minimal) caused by main→loop_step
overwriting k_inner_exit→loop_step latch values.

Root cause: JoinIR main entry block was incorrectly treated as BackEdge,
causing it to overwrite the correct latch incoming values set by the true
back edge (k_inner_exit → loop_step).

Solution:
- Restrict latch recording to TailCallKind::BackEdge only
- Treat only MAIN's entry block as entry-like (not loop_step's entry block)
- Add debug_assert! to detect double latch set in future

Refactoring:
- Extract latch recording to latch_incoming_recorder module (SSOT)
- Add boundary.loop_header_func_name for explicit header identification
- Strengthen tail_call_classifier with is_source_entry_like parameter

Tests: apps/tests/phase1883_nested_minimal.hako → RC:9 (was infinite loop)
Smoke: 154/154 PASS, no regressions

🤖 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-27 09:39:29 +09:00
parent bbfc3c1d94
commit a04b48416e
20 changed files with 401 additions and 171 deletions

View File

@ -12,6 +12,23 @@ Scope: Repo root の旧リンク互換。現行の入口は `docs/development/cu
### 状況SSOT
**2025-12-27: Phase 188.3 / Phase 287 P2 COMPLETE (Pattern6 nested loop: merge/latch fixes)**
Pattern61-level nested loopの JoinIR→bridge→merge 経路で発生していた `undefined ValueId``vm step budget exceeded`(無限ループ)を解消。`apps/tests/phase1883_nested_minimal.hako` が RC=9 を返し、quick 154 PASS を維持。
- SSOTmergeの契約:
- latch_incoming を記録してよいのは `TailCallKind::BackEdge` のみLoopEntry は上書き禁止)
- `LoopEntry` 判定は “JoinIR main の entry block のみ” を entry-like とする(誤分類で latch が壊れるのを防止)
- latch 二重設定は `debug_assert!` で fail-fast回帰検知
- 変更箇所:
- `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs`
- `src/mir/builder/control_flow/joinir/merge/loop_header_phi_info.rs`
- 検証DONE:
- [x] 再現とPHI確認Pattern6
- [x] latch記録条件を修正BackEdgeのみ
- [x] デバッグ出力を撤去(恒常出力なし)
- [x] quick/fixtureで検証quick 154 PASS / fixture RC=9
- [x] docsを締めて次の指示書を用意refactor挟み込み用
**2025-12-26: Phase 286 P2.2 COMPLETE (Hygiene: extractor重複排除 + router小整理)**
Pattern1/Pattern4 の Plan/Frag PoC 完了後、extractor の `extract_loop_increment_plan``common_helpers.rs` に統一、router の 3行パターンnormalize→verify→lower`lower_via_plan()` ヘルパーで共通化。~65行削減、quick 154 PASS 維持。

View File

@ -1,13 +1,19 @@
# Self Current Task — Now (main)
## Current Focus: Phase 188.3 (Nested Loop Lowering)
## Current Focus: Post Phase 188.3 (Refactoring Window)
**2025-12-27: Phase 188.3 完了**
- Pattern6NestedLoopMinimal: `apps/tests/phase1883_nested_minimal.hako` が RC=9
- Merge SSOTlatch/entry-like/double latchを固定BackEdgeのみlatch記録、main entry blockのみentry-like、二重latchはdebug_assert
- `./tools/smokes/v2/run.sh --profile quick` 154/154 PASS 維持
- 入口: `docs/development/current/main/phases/phase-188.3/README.md`
- 次の指示書refactor挟み込み: `docs/development/current/main/phases/phase-188.3/P2-REFACTORING-INSTRUCTIONS.md`
**2025-12-27: Phase 188.2 完了**
- StepTreeの `max_loop_depth` を SSOT に採用Option A
- strict mode で depth > 2 を明示エラー化Fail-Fast
- quick 154/154 PASS、integration selfhost FAIL=0 維持
- 次: `docs/development/current/main/phases/phase-188.3/README.md`depth=2 を JoinIR lowering で通す / “最小write-back”は carrier として明示
- 実装導線(手順書): `docs/development/current/main/phases/phase-188.3/P1-INSTRUCTIONS.md`merge/rewriter の “undef ValueId” 典型罠もここに固定)
- 次: `docs/development/current/main/phases/phase-188.3/P2-REFACTORING-INSTRUCTIONS.md`(意味論不変での整理を優先
**2025-12-27: Phase S0.1 完了**
- integration selfhost を「落ちない状態」に収束FAIL=0

View File

@ -0,0 +1,74 @@
# Phase 188.3 P2: Pattern6 merge/latch 周りのリファクタ指示書(意味論不変)
**Date**: 2025-12-27
**Scope**: JoinIR merge の tail-call 分類・latch 記録の可読性/SSOT化
**Non-goals**: Pattern6 の選定/Lowering の機能追加、既定挙動変更、フォールバック追加
---
## 目的
Pattern6NestedLoopMinimalで露出した merge/rewriter の暗黙ルールを「構造」で固定し、次の拡張Phase 188.4+)や回帰検知を楽にする。
---
## SSOT固定する契約
- `latch_incoming` を記録してよいのは `TailCallKind::BackEdge` のみLoopEntry は上書き禁止)
- entry-like は “JoinIR main の entry block のみ”(`loop_step` の entry block を entry-like と誤認しない)
- latch 二重設定は `debug_assert!` で fail-fast回帰検知
実装の現行入口:
- `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs`
- `src/mir/builder/control_flow/joinir/merge/loop_header_phi_info.rs`
---
## リファクタ方針(構造で解決)
### A) tail-call 分類を「1箇所」で作って再利用する
現状は plan stage の中で `classify_tail_call(...)` を複数回呼び出している。
`TailCallFacts`structを作って、各 block の tail-call に対して以下を 1 回だけ確定し、後続が参照する形にする:
- `target_func_name`
- `is_target_continuation`
- `is_target_loop_entry`
- `is_entry_like_block`MAIN + entry block
- `tail_call_kind`
### B) latch 記録を “専用の小箱” に隔離する
`instruction_rewriter.rs` の末尾にある “latch 記録” を以下の形で隔離し、責務を固定する:
- `LatchIncomingRecorder`new moduleを追加し、`record_if_backedge(...)` だけを公開
- 引数: `tail_call_kind`, `boundary`, `new_block_id`, `args`, `loop_header_phi_info`
- 返り値: `()`(失敗は `debug_assert!` / 既存の `Result` に委譲)
これで「BackEdge 以外で記録しない」契約が局所化され、将来の変更点が明確になる。
### C) “entry-like” 判定を SSOT helper に寄せる
`func_name == MAIN && old_block_id == entry_block` の判定を helper 化する:
- `is_entry_like_block(func_name, old_block_id, func_entry_block) -> bool`
判定の重複と誤用loop_step の entry を entry-like と見なす)を構造で防ぐ。
---
## テスト(仕様固定)
最小で良いので、契約をテストで固定する(既存のユニットテストがある範囲で)。
- `LoopHeaderPhiInfo::set_latch_incoming()` の二重セットが debug で fail-fast すること(`#[should_panic]`
- Pattern6 fixture は既にあるので、スモークで RC を固定するquick の回帰検知)
---
## 検証手順
1. `cargo build --release`
2. `./target/release/hakorune --backend vm apps/tests/phase1883_nested_minimal.hako`RC=9
3. `./tools/smokes/v2/run.sh --profile quick`154/154 PASS

View File

@ -1,7 +1,7 @@
# Phase 188.3: Nested loop lowering (1-level) — make Pattern 6 real
**Date**: 2025-12-27
**Status**: In progress (Phase 2 done: selection / Phase 3: lowering)
**Status**: Complete (Pattern6 lowering + merge/latch fix)
**Prereq**: Phase 188.2 Option A is complete (StepTree depth SSOT + strict Fail-Fast)
---
@ -44,15 +44,19 @@ Phase 188.2 の strict ガードは「depth > 2 を明示エラー」にして
## Current Code Status (reality)
Phase 188.3 は “選択ロジックPattern6選定” まで実装済みで、lowering が未実装stubな状態
Phase 188.3 は “選択ロジックPattern6選定→ lowering → merge/rewriter 安定化” まで完了している
- Selection SSOT: `src/mir/builder/control_flow/joinir/routing.rs``choose_pattern_kind()`
- cheap check → StepTree → AST validation
- `max_loop_depth == 2` かつ “Pattern6 lowerable” のときだけ `Pattern6NestedLoopMinimal` を返す
- Lowering stub: `src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs`
- 現在は `Err("[Pattern6] ... not yet implemented")` で Fail-Fast
この README のゴールは、stub を “実装済み” にして fixture を通すこと。
- Lowering: `src/mir/builder/control_flow/joinir/patterns/pattern6_nested_minimal.rs`
- JoinIR pipeline で `inner_step/k_inner_exit/k_exit` を含む関数群を生成して merge する
- Fixture: `apps/tests/phase1883_nested_minimal.hako`RC=9
- Merge/Rewrite contractSSOT:
- `latch_incoming` を記録してよいのは `TailCallKind::BackEdge` のみLoopEntry は上書き禁止)
- entry-like は “JoinIR main の entry block のみ”
- 二重 latch は `debug_assert!` で fail-fast
- 実装: `src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs`, `src/mir/builder/control_flow/joinir/merge/loop_header_phi_info.rs`
---
@ -167,6 +171,7 @@ JoinIR merge は「JoinIR の param ValueId は SSA 命令で定義されない
- `./tools/smokes/v2/run.sh --profile quick` が常にグリーン維持
- integration selfhost が FAIL=0 を維持
- 追加した nested loop fixture が PASSJoinIR lowering が使われたことをログ/タグで確認可能)
- 実測: `./target/release/hakorune --backend vm apps/tests/phase1883_nested_minimal.hako` → RC=9
---
@ -181,6 +186,10 @@ JoinIR merge は「JoinIR の param ValueId は SSA 命令で定義されない
- **Phase 188.3**: depth=2 の最小形を “確実に通す” + PoC fixture を smoke 固定
- **Phase 188.4+**: write-backouter carrier reconnectionと “再帰 lowering の一般化depthを増やしても壊れない” を docs-first で設計してから実装
### Post-completion (refactoring window)
実装完了後のリファクタ(意味論不変)を挟む場合は、`P2-REFACTORING-INSTRUCTIONS.md` を入口にする。
### Planned cleanup (after Phase 188.3)
Pattern6 を通す過程で露出しやすい “暗黙ルール” を SSOT 化して、今後の nested/generalization を楽にする:

View File

@ -754,21 +754,19 @@ mod tests {
// Phase 286C-4.2: Test helper for JoinInlineBoundary construction
#[cfg(test)]
fn make_boundary(exit_bindings: Vec<crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding>) -> JoinInlineBoundary {
fn make_boundary(
exit_bindings: Vec<crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding>,
) -> JoinInlineBoundary {
use crate::mir::join_ir::lowering::carrier_info::ExitReconnectMode;
use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder;
JoinInlineBoundary {
join_inputs: vec![],
host_inputs: vec![],
join_outputs: vec![],
#[allow(deprecated)]
host_outputs: vec![],
loop_invariants: vec![],
exit_bindings,
loop_var_name: None,
continuation_function_ids: vec![],
exit_reconnect_mode: ExitReconnectMode::DirectValue,
}
let mut boundary = JoinInlineBoundaryBuilder::new()
.with_inputs(vec![], vec![])
.with_exit_bindings(exit_bindings)
.build();
boundary.exit_reconnect_mode = ExitReconnectMode::DirectValue;
boundary
}
#[cfg(test)]
@ -783,9 +781,9 @@ mod carrier_inputs_tests {
let boundary = make_boundary(vec![
LoopExitBinding {
carrier_name: "sum".to_string(),
role: CarrierRole::Accumulator,
host_slot: ValueId(10),
join_exit_value: ValueId(100),
role: CarrierRole::LoopState,
},
]);
@ -822,13 +820,13 @@ mod carrier_inputs_tests {
#[test]
fn test_verify_carrier_inputs_complete_valid() {
// Setup: Accumulator carrier with inputs
// Setup: LoopState carrier with inputs
let boundary = make_boundary(vec![
LoopExitBinding {
carrier_name: "count".to_string(),
role: CarrierRole::Accumulator,
host_slot: ValueId(30),
join_exit_value: ValueId(102),
role: CarrierRole::LoopState,
},
]);

View File

@ -143,7 +143,9 @@ impl ExitArgsCollectorBox {
if strict_exit {
return Err(msg);
} else {
eprintln!("[DEBUG-177] {}", msg);
if crate::config::env::is_joinir_debug() {
eprintln!("[DEBUG-177] {}", msg);
}
}
}
}
@ -179,7 +181,9 @@ impl ExitArgsCollectorBox {
if strict_exit {
Err(msg)
} else {
eprintln!("[DEBUG-177] {}", msg);
if crate::config::env::is_joinir_debug() {
eprintln!("[DEBUG-177] {}", msg);
}
Ok(0) // Best effort: try direct mapping
}
} else {
@ -214,7 +218,9 @@ impl ExitArgsCollectorBox {
if strict_exit {
Err(msg)
} else {
eprintln!("[DEBUG-177] {}", msg);
if crate::config::env::is_joinir_debug() {
eprintln!("[DEBUG-177] {}", msg);
}
Ok(0)
}
}

View File

@ -97,7 +97,7 @@ impl ExitLineOrchestrator {
"[joinir/exit-line] orchestrator start: {} carrier PHIs",
carrier_phis.len()
),
true,
verbose,
);
}
@ -105,7 +105,7 @@ impl ExitLineOrchestrator {
ExitLineReconnector::reconnect(builder, boundary, carrier_phis, remapped_exit_values, debug)?;
if verbose {
trace.stderr_if("[joinir/exit-line] orchestrator complete", true);
trace.stderr_if("[joinir/exit-line] orchestrator complete", verbose);
}
Ok(())

View File

@ -98,7 +98,7 @@ impl ExitLineReconnector {
boundary.exit_bindings.len(),
carrier_phis.len()
),
true,
verbose,
);
if !boundary.exit_bindings.is_empty() {
trace.stderr_if(
@ -110,7 +110,7 @@ impl ExitLineReconnector {
.map(|b| (&b.carrier_name, b.role, b.join_exit_value))
.collect::<Vec<_>>()
),
true,
verbose,
);
}
}
@ -118,7 +118,7 @@ impl ExitLineReconnector {
// Early return for empty exit_bindings
if boundary.exit_bindings.is_empty() {
if verbose {
trace.stderr_if("[joinir/exit-line] reconnect: no exit bindings, skip", true);
trace.stderr_if("[joinir/exit-line] reconnect: no exit bindings, skip", verbose);
}
return Ok(());
}
@ -130,7 +130,7 @@ impl ExitLineReconnector {
boundary.exit_bindings.len(),
carrier_phis.len()
),
true,
verbose,
);
}

View File

@ -66,6 +66,7 @@ use std::collections::BTreeSet;
// Phase 260 P0.1 Step 3: Import from helpers module
use super::rewriter::helpers::is_skippable_continuation;
use super::rewriter::latch_incoming_recorder; // Phase 287 P2: latch recording SSOT
// Phase 286C-2 Step 2: Import from instruction_filter_box module
use super::rewriter::instruction_filter_box::InstructionFilterBox;
// Phase 286C-2 Step 2: Import from parameter_binding_box module
@ -283,15 +284,39 @@ fn plan_rewrites(
functions_merge.sort_by_key(|(name, _)| name.as_str());
// Determine entry function (loop header)
let entry_func_name = functions_merge
.iter()
.find(|(name, _)| {
let name_str = name.as_str();
let is_continuation = continuation_candidates.contains(*name);
let is_main = name_str == crate::mir::join_ir::lowering::canonical_names::MAIN;
!is_continuation && !is_main
})
.map(|(name, _)| name.as_str());
//
// Phase 287 P2: Prefer boundary SSOT (loop_header_func_name) over heuristic.
let entry_func_name = boundary
.and_then(|b| b.loop_header_func_name.as_deref())
.or_else(|| {
functions_merge
.iter()
.find(|(name, _)| {
let name_str = name.as_str();
let is_continuation = continuation_candidates.contains(*name);
let is_main = name_str == crate::mir::join_ir::lowering::canonical_names::MAIN;
!is_continuation && !is_main
})
.map(|(name, _)| name.as_str())
});
fn resolve_target_func_name<'a>(
function_entry_map: &'a BTreeMap<String, BasicBlockId>,
target_block: BasicBlockId,
) -> Option<&'a str> {
function_entry_map
.iter()
.find_map(|(fname, &entry_block)| (entry_block == target_block).then(|| fname.as_str()))
}
fn is_joinir_main_entry_block(
func_name: &str,
func: &crate::mir::MirFunction,
old_block_id: BasicBlockId,
) -> bool {
func_name == crate::mir::join_ir::lowering::canonical_names::MAIN
&& old_block_id == func.entry_block
}
// Process each function
for (func_name, func) in functions_merge {
@ -328,13 +353,13 @@ fn plan_rewrites(
let mut blocks_merge: Vec<_> = func.blocks.iter().collect();
blocks_merge.sort_by_key(|(id, _)| id.0);
// Determine if this is the loop entry point
let is_loop_entry_point =
entry_func_name == Some(func_name.as_str()) && blocks_merge.first().map(|(id, _)| **id) == Some(func.entry_block);
// Determine if this is the loop header entry block (loop_step entry).
let is_loop_header_entry_block = entry_func_name == Some(func_name.as_str())
&& blocks_merge.first().map(|(id, _)| **id) == Some(func.entry_block);
// Check if loop header has PHIs
let is_loop_header_with_phi =
is_loop_entry_point && !loop_header_phi_info.carrier_phis.is_empty();
is_loop_header_entry_block && !loop_header_phi_info.carrier_phis.is_empty();
// Collect PHI dst IDs for this block (if loop header)
let phi_dst_ids_for_block: std::collections::HashSet<ValueId> =
@ -395,7 +420,11 @@ fn plan_rewrites(
// Skip boundary input Const instructions
let boundary_inputs: Vec<ValueId> = boundary_input_set.iter().cloned().collect();
if InstructionFilterBox::should_skip_boundary_input_const(*dst, &boundary_inputs, is_loop_entry_point) {
if InstructionFilterBox::should_skip_boundary_input_const(
*dst,
&boundary_inputs,
is_loop_header_entry_block,
) {
log!(verbose, "[plan_rewrites] Skipping boundary input const: {:?}", inst);
continue;
}
@ -478,33 +507,34 @@ fn plan_rewrites(
// Second pass: Insert parameter bindings for tail calls (if any)
if let Some((target_block, ref args)) = tail_call_target {
// Find the target function name from the target_block
let mut target_func_name: Option<String> = None;
for (fname, &entry_block) in &ctx.function_entry_map {
if entry_block == target_block {
target_func_name = Some(fname.clone());
break;
}
}
let target_func_name = resolve_target_func_name(&ctx.function_entry_map, target_block);
// Check if target is continuation/recursive/loop entry
let is_target_continuation = target_func_name
.as_ref()
.map(|name| continuation_candidates.contains(name))
.unwrap_or(false);
let is_recursive_call = target_func_name
.as_ref()
.map(|name| name == func_name)
.unwrap_or(false);
let is_recursive_call = target_func_name.map(|name| name == func_name).unwrap_or(false);
// Phase 188.3: Define is_target_loop_entry early for latch incoming logic
let is_target_loop_entry = target_func_name
.as_ref()
.map(|name| entry_func_name == Some(name.as_str()))
.map(|name| entry_func_name == Some(name))
.unwrap_or(false);
if let Some(ref target_func_name) = target_func_name {
// Phase 287 P2: Calculate tail_call_kind early for latch incoming logic
// Only treat MAIN's entry block as entry-like (not loop_step's entry block)
let is_entry_like_block_for_latch =
is_joinir_main_entry_block(func_name, func, *old_block_id);
let tail_call_kind = classify_tail_call(
is_entry_like_block_for_latch,
!loop_header_phi_info.carrier_phis.is_empty(),
boundary.is_some(),
is_target_continuation,
is_target_loop_entry,
);
if let Some(target_func_name) = target_func_name {
if let Some(target_params) = function_params.get(target_func_name) {
log!(
@ -519,7 +549,7 @@ fn plan_rewrites(
// 3. Continuation call (handled separately below)
// Phase 287 P1: Skip ONLY when target is loop header
// (not when source is entry func but target is non-entry like inner_step)
if is_loop_entry_point && is_target_loop_entry {
if is_loop_header_entry_block && is_target_loop_entry {
log!(
verbose,
"[plan_rewrites] Skip param bindings in header block (PHIs define carriers)"
@ -586,59 +616,15 @@ fn plan_rewrites(
}
}
// Record latch incoming for loop header PHI (only for calls to loop entry func)
// Phase 287 P2: Restrict to is_target_loop_entry only (not is_recursive_call)
// This prevents inner_step recursion from overwriting outer loop's latch values
if is_target_loop_entry {
if let Some(b) = boundary {
if let Some(loop_var_name) = &b.loop_var_name {
if !args.is_empty() {
let latch_value = args[0];
loop_header_phi_info.set_latch_incoming(
loop_var_name,
new_block_id,
latch_value,
);
log!(
verbose,
"[plan_rewrites] Set latch incoming for '{}': block={:?}, value={:?}",
loop_var_name, new_block_id, latch_value
);
}
}
// Set latch incoming for other carriers
let mut carrier_arg_idx = if b.loop_var_name.is_some() { 1 } else { 0 };
for binding in b.exit_bindings.iter() {
if let Some(ref loop_var) = b.loop_var_name {
if &binding.carrier_name == loop_var {
continue;
}
}
if carrier_arg_idx < args.len() {
let latch_value = args[carrier_arg_idx];
loop_header_phi_info.set_latch_incoming(
&binding.carrier_name,
new_block_id,
latch_value,
);
carrier_arg_idx += 1;
}
}
// Set latch incoming for loop invariants
for (inv_name, _inv_host_id) in b.loop_invariants.iter() {
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) {
loop_header_phi_info.set_latch_incoming(
inv_name,
new_block_id,
phi_dst,
);
}
}
}
}
// Record latch incoming for loop header PHI (SSOT)
// Phase 287 P2: BackEdge のみ latch 記録LoopEntry main → loop_step を除外)
latch_incoming_recorder::record_if_backedge(
tail_call_kind,
boundary,
new_block_id,
args,
loop_header_phi_info,
);
}
// Synchronize spans
@ -815,30 +801,23 @@ fn plan_rewrites(
} else if let Some((target_block, args)) = tail_call_target {
// Tail call: Set Jump terminator
// Classify tail call and determine actual target
let target_func_name = {
let mut name: Option<String> = None;
for (fname, &entry_block) in &ctx.function_entry_map {
if entry_block == target_block {
name = Some(fname.clone());
break;
}
}
name
};
let target_func_name = resolve_target_func_name(&ctx.function_entry_map, target_block);
let is_target_continuation = target_func_name
.as_ref()
.map(|name| continuation_candidates.contains(name))
.unwrap_or(false);
// Phase 287 P2: Compute is_target_loop_entry for classify_tail_call
let is_target_loop_entry = target_func_name
.as_ref()
.map(|name| entry_func_name == Some(name.as_str()))
.map(|name| entry_func_name == Some(name))
.unwrap_or(false);
// Phase 287 P2: main の entry block からの呼び出しを LoopEntry 扱いにする
// Only treat MAIN's entry block as entry-like (not loop_step's entry block)
let is_entry_like_block = is_joinir_main_entry_block(func_name, func, *old_block_id);
let tail_call_kind = classify_tail_call(
is_loop_entry_point,
is_entry_like_block,
!loop_header_phi_info.carrier_phis.is_empty(),
boundary.is_some(),
is_target_continuation,
@ -864,15 +843,7 @@ fn plan_rewrites(
}
TailCallKind::ExitJump => {
// Check if target is skippable continuation
let mut target_func_name: Option<String> = None;
for (fname, &entry_block) in &ctx.function_entry_map {
if entry_block == target_block {
target_func_name = Some(fname.clone());
break;
}
}
let is_target_skippable = target_func_name
.as_ref()
let is_target_skippable = resolve_target_func_name(&ctx.function_entry_map, target_block)
.map(|name| skippable_continuation_func_names.contains(name))
.unwrap_or(false);

View File

@ -97,6 +97,15 @@ impl LoopHeaderPhiInfo {
/// Called from instruction_rewriter after processing tail call Copy instructions.
pub fn set_latch_incoming(&mut self, name: &str, from_block: BasicBlockId, value: ValueId) {
if let Some(entry) = self.carrier_phis.get_mut(name) {
// Phase 287 P2 Fail-Fast: デバッグビルドのみ二重セット検知
debug_assert!(
entry.latch_incoming.is_none(),
"Phase 287 P2 Fail-Fast: Double latch set for '{}'. Existing: {:?}, New: ({:?}, {:?})",
name,
entry.latch_incoming,
from_block,
value
);
entry.latch_incoming = Some((from_block, value));
}
}
@ -170,4 +179,22 @@ mod tests {
info.set_latch_incoming("i", BasicBlockId(20), ValueId(50));
assert!(info.all_latch_set());
}
#[test]
#[should_panic(expected = "Double latch set")]
fn set_latch_incoming_double_set_panics_in_debug() {
let mut info = LoopHeaderPhiInfo::empty(BasicBlockId(9));
info.carrier_phis.insert(
"i".to_string(),
CarrierPhiEntry {
phi_dst: ValueId(100),
entry_incoming: (BasicBlockId(1), ValueId(1)),
latch_incoming: None,
role: CarrierRole::LoopState,
},
);
info.set_latch_incoming("i", BasicBlockId(2), ValueId(2));
info.set_latch_incoming("i", BasicBlockId(3), ValueId(3));
}
}

View File

@ -217,7 +217,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
"[cf_loop/joinir] Boundary join_inputs={:?} host_inputs={:?}",
boundary.join_inputs, boundary.host_inputs
),
true,
verbose,
);
trace.stderr_if(
&format!(
@ -225,7 +225,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
boundary.exit_bindings.len(),
exit_summary.join(", ")
),
true,
verbose,
);
if !cond_summary.is_empty() {
trace.stderr_if(
@ -234,7 +234,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
cond_summary.len(),
cond_summary.join(", ")
),
true,
verbose,
);
}
if let Some(ci) = &boundary.carrier_info {
@ -244,11 +244,11 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
"[cf_loop/joinir] Boundary carrier_info: loop_var='{}', carriers={:?}",
ci.loop_var_name, carriers
),
true,
verbose,
);
}
} else {
trace.stderr_if("[cf_loop/joinir] No boundary provided", true);
trace.stderr_if("[cf_loop/joinir] No boundary provided", verbose);
}
}
@ -364,18 +364,34 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
let (mut loop_header_phi_info, merge_entry_block) = if let Some(boundary) = boundary {
if let Some(loop_var_name) = &boundary.loop_var_name {
// Get loop_step function for PHI placement (the actual loop header)
let (loop_step_func_name, loop_step_func) = mir_module
.functions
.iter()
.find(|(name, _)| {
let is_continuation = boundary.continuation_func_ids.contains(*name);
let is_main = *name == crate::mir::join_ir::lowering::canonical_names::MAIN;
!is_continuation && !is_main
let loop_step_func_name = boundary
.loop_header_func_name
.as_deref()
.or_else(|| {
mir_module
.functions
.iter()
.find(|(name, _)| {
let is_continuation = boundary.continuation_func_ids.contains(*name);
let is_main =
*name == crate::mir::join_ir::lowering::canonical_names::MAIN;
!is_continuation && !is_main
})
.map(|(name, _)| name.as_str())
})
.or_else(|| mir_module.functions.iter().next())
.map(|(name, func)| (name.as_str(), func))
.or_else(|| mir_module.functions.keys().next().map(|s| s.as_str()))
.ok_or("JoinIR module has no functions (Phase 201-A)")?;
let loop_step_func = mir_module
.functions
.get(loop_step_func_name)
.ok_or_else(|| {
format!(
"loop_header_func_name '{}' not found in JoinIR module (Phase 287 P2)",
loop_step_func_name
)
})?;
// Phase 256.7-fix: Determine merge_entry_block
// When main has condition_bindings as params, we enter through main first
// (for boundary Copies), then main's tail call jumps to loop_step.
@ -479,7 +495,7 @@ pub(in crate::mir::builder) fn merge_joinir_mir_blocks(
"[cf_loop/joinir] Phase 256.7-fix: merge_entry_func='{}', merge_entry_block={:?}, loop_header_block={:?}",
entry_func_name, entry_block_remapped, loop_header_block
),
true, // Always log for debugging
debug,
);
trace.stderr_if(
&format!(

View File

@ -70,7 +70,9 @@ pub(in crate::mir::builder::control_flow::joinir::merge) fn collect_exit_values_
if strict_exit {
return Err(msg);
} else {
eprintln!("[DEBUG-177] {}", msg);
if crate::config::env::is_joinir_debug() {
eprintln!("[DEBUG-177] {}", msg);
}
}
}
// The jump_args are in JoinIR value space, remap them to HOST

View File

@ -0,0 +1,55 @@
//! Latch incoming recorder (SSOT)
//!
//! Phase 287 P2: Centralize "when is it legal to record latch_incoming" policy.
//!
//! Contract:
//! - Only record latch_incoming for `TailCallKind::BackEdge`
//! - Never record for LoopEntry (main → loop_step), to avoid overwriting the true latch
use crate::mir::builder::control_flow::joinir::merge::loop_header_phi_info::LoopHeaderPhiInfo;
use crate::mir::builder::control_flow::joinir::merge::tail_call_classifier::TailCallKind;
use crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary;
use crate::mir::{BasicBlockId, ValueId};
pub(in crate::mir::builder::control_flow::joinir::merge) fn record_if_backedge(
tail_call_kind: TailCallKind,
boundary: Option<&JoinInlineBoundary>,
new_block_id: BasicBlockId,
args: &[ValueId],
loop_header_phi_info: &mut LoopHeaderPhiInfo,
) {
if tail_call_kind != TailCallKind::BackEdge {
return;
}
let Some(boundary) = boundary else { return };
if let Some(loop_var_name) = &boundary.loop_var_name {
if let Some(&latch_value) = args.first() {
loop_header_phi_info.set_latch_incoming(loop_var_name, new_block_id, latch_value);
}
}
// Other carriers (excluding loop_var)
let mut carrier_arg_idx = if boundary.loop_var_name.is_some() { 1 } else { 0 };
for binding in boundary.exit_bindings.iter() {
if let Some(ref loop_var) = boundary.loop_var_name {
if &binding.carrier_name == loop_var {
continue;
}
}
if let Some(&latch_value) = args.get(carrier_arg_idx) {
loop_header_phi_info.set_latch_incoming(&binding.carrier_name, new_block_id, latch_value);
carrier_arg_idx += 1;
}
}
// Loop invariants: latch incoming is the PHI destination itself (preserve value).
for (inv_name, _inv_host_id) in boundary.loop_invariants.iter() {
if let Some(phi_dst) = loop_header_phi_info.get_carrier_phi(inv_name) {
loop_header_phi_info.set_latch_incoming(inv_name, new_block_id, phi_dst);
}
}
}

View File

@ -36,6 +36,7 @@ pub(super) mod carrier_inputs_collector; // Phase 286C-5 Step 1: DRY carrier_inp
pub(super) mod exit_collection; // Phase 260 P0.1 Step 6: Exit collection extracted ✅
pub(super) mod helpers; // Phase 260 P0.1 Step 3: Helpers extracted ✅
pub(super) mod instruction_filter_box; // Phase 286C-2 Step 2: Skip judgment logic extracted ✅
pub(super) mod latch_incoming_recorder; // Phase 287 P2: latch recording SSOT ✅
pub(super) mod logging; // Phase 260 P0.1 Step 2: Logging extracted ✅
pub(super) mod parameter_binding_box; // Phase 286C-2 Step 2: Parameter binding helpers ✅
pub(super) mod return_converter_box; // Phase 286C-2 Step 2: Return → Jump conversion helpers ✅

View File

@ -34,7 +34,7 @@ pub enum TailCallKind {
/// Classifies a tail call based on context
///
/// # Arguments
/// * `is_entry_func_entry_block` - True if this is the first function's first block (loop entry point)
/// * `is_source_entry_like` - True if this tail call originates from an entry-like block
/// * `has_loop_header_phis` - True if loop header PHI nodes exist
/// * `has_boundary` - True if JoinInlineBoundary exists (indicates loop context)
/// * `is_target_continuation` - True if the tail call target is a continuation function (k_exit)
@ -43,7 +43,7 @@ pub enum TailCallKind {
/// # Returns
/// The classification of this tail call
pub fn classify_tail_call(
is_entry_func_entry_block: bool,
is_source_entry_like: bool,
has_loop_header_phis: bool,
has_boundary: bool,
is_target_continuation: bool,
@ -56,9 +56,9 @@ pub fn classify_tail_call(
return TailCallKind::ExitJump;
}
// Entry function's entry block is the loop entry point
// It already IS at the header, so no redirection needed
if is_entry_func_entry_block {
// Entry-like block jumping into the loop header is a LoopEntry (main → loop_step).
// It should NOT be redirected to the header block, otherwise we create a self-loop.
if is_source_entry_like && is_target_loop_entry {
return TailCallKind::LoopEntry;
}
@ -66,7 +66,7 @@ pub fn classify_tail_call(
// This prevents inner_step→inner_step from being classified as BackEdge,
// which would incorrectly redirect it to the outer header (loop_step).
// inner_step→inner_step should jump to inner_step's entry block, not outer header.
if is_target_loop_entry && has_boundary && has_loop_header_phis && !is_entry_func_entry_block {
if is_target_loop_entry && has_boundary && has_loop_header_phis {
return TailCallKind::BackEdge;
}
@ -81,7 +81,7 @@ mod tests {
#[test]
fn test_classify_loop_entry() {
let result = classify_tail_call(
true, // is_entry_func_entry_block
true, // is_source_entry_like
true, // has_loop_header_phis
true, // has_boundary
false, // is_target_continuation
@ -93,7 +93,7 @@ mod tests {
#[test]
fn test_classify_back_edge() {
let result = classify_tail_call(
false, // is_entry_func_entry_block (not entry block)
false, // is_source_entry_like
true, // has_loop_header_phis
true, // has_boundary
false, // is_target_continuation
@ -105,7 +105,7 @@ mod tests {
#[test]
fn test_classify_exit_jump() {
let result = classify_tail_call(
false, // is_entry_func_entry_block
false, // is_source_entry_like
false, // has_loop_header_phis (no header PHIs)
true, // has_boundary
false, // is_target_continuation
@ -117,7 +117,7 @@ mod tests {
#[test]
fn test_classify_no_boundary() {
let result = classify_tail_call(
false, // is_entry_func_entry_block
false, // is_source_entry_like
true, // has_loop_header_phis
false, // has_boundary (no boundary → exit)
false, // is_target_continuation
@ -131,7 +131,7 @@ mod tests {
// Phase 256 P1.10: Continuation calls (k_exit) are always ExitJump
// even when they would otherwise be classified as BackEdge
let result = classify_tail_call(
false, // is_entry_func_entry_block
false, // is_source_entry_like
true, // has_loop_header_phis
true, // has_boundary
true, // is_target_continuation ← this makes it ExitJump
@ -145,7 +145,7 @@ mod tests {
// Phase 287 P2: inner_step→inner_step should NOT be BackEdge
// even with boundary and header PHIs, because target is not loop entry func
let result = classify_tail_call(
false, // is_entry_func_entry_block (inner_step body block)
false, // is_source_entry_like
true, // has_loop_header_phis
true, // has_boundary
false, // is_target_continuation
@ -153,4 +153,17 @@ mod tests {
);
assert_eq!(result, TailCallKind::ExitJump);
}
#[test]
fn test_classify_entry_like_but_not_loop_entry_target() {
// Entry-like source does not imply LoopEntry unless the target is loop_step.
let result = classify_tail_call(
true, // is_source_entry_like
true, // has_loop_header_phis
true, // has_boundary
false, // is_target_continuation
false, // is_target_loop_entry
);
assert_eq!(result, TailCallKind::ExitJump);
}
}

View File

@ -52,13 +52,13 @@ pub(super) fn collect_values(
// Phase 188-Impl-3: Collect function parameters for tail call conversion
function_params.insert(func_name.clone(), func.params.clone());
// Phase 256 P1.10 DEBUG: Always log function params for debugging
// Phase 256 P1.10 DEBUG: function params log (guarded)
trace.stderr_if(
&format!(
"[cf_loop/joinir] Phase 256 P1.10 DEBUG: Function '{}' params: {:?}",
func_name, func.params
),
true,
debug,
);
for block in func.blocks.values() {

View File

@ -383,6 +383,7 @@ mod tests {
expr_result: None, // Phase 33-14: Add missing field
jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly,
loop_var_name: None, // Phase 33-16: Add missing field
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228: Add missing field
loop_invariants: vec![], // Phase 255 P2: Add missing field
continuation_func_ids: std::collections::BTreeSet::from([

View File

@ -137,6 +137,7 @@ mod tests {
expr_result: None, // Phase 33-14: Add missing field
jump_args_layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly,
loop_var_name: None, // Phase 33-16: Add missing field
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228: Add missing field
loop_invariants: vec![], // Phase 255 P2: Add missing field
continuation_func_ids: std::collections::BTreeSet::from([

View File

@ -308,6 +308,15 @@ pub struct JoinInlineBoundary {
/// Used to track which PHI corresponds to the loop variable.
pub loop_var_name: Option<String>,
/// Phase 287 P2: Loop header function name (SSOT)
///
/// Merge must not guess the loop header function from "first non-main non-continuation".
/// For loop patterns, set this explicitly (typically `"loop_step"`).
///
/// - `Some(name)`: Merge uses this as the loop header function.
/// - `None`: Legacy heuristic remains (for backwards compatibility).
pub loop_header_func_name: Option<String>,
/// Phase 228: Carrier metadata (for header PHI generation)
///
/// Contains full carrier information including initialization policies.
@ -435,6 +444,7 @@ impl JoinInlineBoundary {
expr_result: None, // Phase 33-14: Default to carrier-only pattern
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228: Default to None
continuation_func_ids: Self::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
@ -482,6 +492,7 @@ impl JoinInlineBoundary {
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
@ -546,6 +557,7 @@ impl JoinInlineBoundary {
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
@ -596,6 +608,7 @@ impl JoinInlineBoundary {
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
@ -650,6 +663,7 @@ impl JoinInlineBoundary {
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
@ -711,6 +725,7 @@ impl JoinInlineBoundary {
expr_result: None, // Phase 33-14
jump_args_layout: JumpArgsLayout::CarriersOnly, // Phase 256 P1.12
loop_var_name: None, // Phase 33-16
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228
continuation_func_ids: Self::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5: Default to Phi
@ -767,6 +782,7 @@ mod tests {
expr_result: Some(ValueId(10)),
jump_args_layout: JumpArgsLayout::ExprResultPlusCarriers,
loop_var_name: None,
loop_header_func_name: None,
carrier_info: None,
continuation_func_ids: JoinInlineBoundary::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(),

View File

@ -98,6 +98,7 @@ impl JoinInlineBoundaryBuilder {
expr_result: None,
jump_args_layout: JumpArgsLayout::CarriersOnly,
loop_var_name: None,
loop_header_func_name: None, // Phase 287 P2
carrier_info: None, // Phase 228: Initialize as None
continuation_func_ids: JoinInlineBoundary::default_continuations(),
exit_reconnect_mode: ExitReconnectMode::default(), // Phase 131 P1.5
@ -184,6 +185,14 @@ impl JoinInlineBoundaryBuilder {
self
}
/// Phase 287 P2: Set loop header function name (SSOT)
///
/// If omitted for loop patterns, `build()` defaults it to `"loop_step"` when `loop_var_name` is set.
pub fn with_loop_header_func_name(mut self, name: Option<String>) -> Self {
self.boundary.loop_header_func_name = name;
self
}
/// Set expression result (Phase 33-14)
///
/// If the loop is used as an expression, this is the JoinIR-local ValueId
@ -200,6 +209,14 @@ impl JoinInlineBoundaryBuilder {
boundary.expr_result,
boundary.exit_bindings.as_slice(),
);
// Phase 287 P2: Default loop header function name for loop patterns.
// If a pattern sets loop_var_name, it must have a loop header function.
if boundary.loop_var_name.is_some() && boundary.loop_header_func_name.is_none() {
boundary.loop_header_func_name =
Some(crate::mir::join_ir::lowering::canonical_names::LOOP_STEP.to_string());
}
boundary
}