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:
@ -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)**
|
||||
Pattern6(1-level nested loop)の JoinIR→bridge→merge 経路で発生していた `undefined ValueId` と `vm step budget exceeded`(無限ループ)を解消。`apps/tests/phase1883_nested_minimal.hako` が RC=9 を返し、quick 154 PASS を維持。
|
||||
|
||||
- SSOT(mergeの契約):
|
||||
- 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 維持。
|
||||
|
||||
|
||||
@ -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 完了** ✅
|
||||
- Pattern6(NestedLoopMinimal): `apps/tests/phase1883_nested_minimal.hako` が RC=9
|
||||
- Merge SSOT(latch/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)
|
||||
|
||||
@ -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 の機能追加、既定挙動変更、フォールバック追加
|
||||
|
||||
---
|
||||
|
||||
## 目的
|
||||
|
||||
Pattern6(NestedLoopMinimal)で露出した 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)
|
||||
|
||||
@ -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 contract(SSOT):
|
||||
- `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 が PASS(JoinIR 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-back(outer carrier reconnection)と “再帰 lowering の一般化(depthを増やしても壊れない)” を docs-first で設計してから実装
|
||||
|
||||
### Post-completion (refactoring window)
|
||||
|
||||
実装完了後のリファクタ(意味論不変)を挟む場合は、`P2-REFACTORING-INSTRUCTIONS.md` を入口にする。
|
||||
|
||||
### Planned cleanup (after Phase 188.3)
|
||||
|
||||
Pattern6 を通す過程で露出しやすい “暗黙ルール” を SSOT 化して、今後の nested/generalization を楽にする:
|
||||
|
||||
@ -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,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ✅
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user