Files
hakorune/docs/development/current/main/design/edgecfg-fragments.md
tomoaki 757193891f feat(llvm/phi): Phase 277 P1 - fail-fast validation for PHI strict mode
## Summary
Implemented fail-fast validation for PHI ordering and value resolution in strict mode.

## Changes

### P1-1: Strict mode for "PHI after terminator"
- File: `src/llvm_py/phi_wiring/wiring.py::ensure_phi`
- Behavior: `NYASH_LLVM_PHI_STRICT=1` → RuntimeError if PHI created after terminator
- Default: Warning only (no regression)

### P1-2: Strict mode for "fallback 0"
- File: `src/llvm_py/phi_wiring/wiring.py::wire_incomings`
- Behavior: Strict mode forbids silent fallback to 0 (2 locations)
  - Location 1: Unresolvable incoming value
  - Location 2: Type coercion failure
- Error messages point to next debug file: `llvm_builder.py::_value_at_end_i64`

### P1-3: Connect verify_phi_ordering() to execution path
- File: `src/llvm_py/builders/function_lower.py`
- Behavior: Verify PHI ordering after all instructions emitted
- Debug mode: Shows " All N blocks have correct PHI ordering"
- Strict mode: Raises RuntimeError with block list if violations found

## Testing
 Test 1: strict=OFF - passes without errors
 Test 2: strict=ON - passes without errors (no violations in test fixtures)
 Test 3: debug mode - verify_phi_ordering() connected and running

## Scope
- LLVM harness (Python) changes only
- No new environment variables (uses existing 3 from Phase 277 P2)
- No JoinIR/Rust changes (root fix is Phase 279)
- Default behavior unchanged (strict mode opt-in)

## Next Steps
- Phase 278: Remove deprecated env var support
- Phase 279: Root fix - unify "2本のコンパイラ" pipelines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 14:48:37 +09:00

18 KiB
Raw Blame History

EdgeCFG Flow FragmentsFrag / ExitKind— Structured→CFG lowering SSOT

Status: Draftdesign SSOT candidate
Last updated: 2025-12-21

Related:

  • North starCFG/ABI: docs/development/current/main/design/join-explicit-cfg-construction.md
  • Catch/Cleanup/Async: docs/development/current/main/design/exception-cleanup-async.md

目的(なぜ必要?)

EdgeCFGblock-parameterized CFG / edge-args SSOTが固まると、次に残る “泥沼” はここだけになる:

  • 構造化制御if/loop + catch/cleanup→ CFG の lowering で起きる exit 配線問題
  • 「pattern番号で推測分岐」が増殖しやすい領域長期的には消したい

この文書は「pattern番号の列挙」を設計の中心にしないために、Structured→CFG の lowering を **合成代数fragment composition**として SSOT 化する。

結論(本書の北極星):

  • “分岐の中心” は pattern番号ではなく ExitKindFragfragment に置く
  • 値の合流は EdgeCFG の block params + edge-args で表し、PHI/推測/メタに逃げない
  • pattern は「Extractor形の認識/ Plan最小要件の抽出」までに縮退し、merge/配線層へ逆流させない

“フロー” は 2 層ある(混ぜると崩れる)

  1. CFG層EdgeCFG / plumbing

    • terminator 語彙: Jump/Branch/Return/Invoke
    • edge-args: terminator operand が SSOT
    • out_edges の参照点が SSOT複数 edge 前提)
  2. Structured→CFG lowering 層flow composition

    • if/loop/catch/cleanup/seq を “Frag の合成” として書く
    • 難しさの本体は exit脱出の種類ネスト合流

コア概念(最小の強い箱)

ExitKind脱出の種類を一次概念にする

最低限の ExitKind:

  • Normalfallthrough
  • Break(loop_id) / Continue(loop_id)
  • Return
  • UnwindInvoke.err / catch へ)
  • Cancelasync の drop/cancel 用。今は予約)

EdgeStub未配線の脱出エッジ

“どこへ飛ぶべきか未確定” な edge を表す。最終的に EdgeCFG の terminator edge に落ちる。

例(概念):

  • from: BlockId
  • kind: ExitKind
  • args: EdgeArgs(ターゲット params に対応する値。target が未確定でも “役割” はここで決める)

Fragfragment

Frag = { entry_block, exits: Map<ExitKind, Vec<EdgeStub>> }
  • entry_block: 断片の入口
  • exits: 断片から外へ出る未配線 edge の集合

合成則pattern列挙を写像へ落とす

seq(a, b)

  • a.exits[Normal]b.entry へ接続するedge-args を必要に応じて写像)
  • それ以外の exit は上位へ伝搬する

if(cond, t, e)

  • header に Branch(cond, t.entry, e.entry) を置く
  • t.Normale.Normal は join へ集める(必要なら join block params を作る)
  • Break/Continue/Return/Unwind は上位へ伝搬

loop(body)

  • header / latch / after を組み、Continue を header に戻す
  • Break を after へ出す
  • Return/Unwind は上位へ伝搬

cleanup(body, cleanup_block)finally の後継)

狙い: “脱出 edge 正規化”

  • body の全 exitNormal/Break/Continue/Return/Unwind/Cancelを cleanup 経由へリライトする
  • cleanup 後に “元の exit” を再発射するExitTag + payload を block params で運ぶ)

重要: 例外 edgeInvoke.errも漏れなく cleanup に寄せる。

pattern は最終的に消える?(設計としての答え)

消える(実装の中心概念から降格する)。

  • pattern番号は 回帰テスト名/症状ラベルとしては残して良い
  • 実装の中心は Frag/ExitKind/join(block params) の合成則になる
  • 各 pattern 実装は “Extractor形の認識→ Frag 合成呼び出し” の薄い層へ縮退する

実装の入口SSOT API を先に作る)

目的: “どこを触ればいいか” を 1 箇所に固定し、推測・部分続行・場当たり分岐を減らす。

推奨の入口:

  • EdgeCFG の plumbing API既存: BasicBlock::out_edges()
  • Structured→CFG の入口 API新規: Frag / ExitKind / compose::{seq, if_, loop_ , cleanup}

物理配置(案):

  • src/mir/builder/control_flow/edgecfg/api/(または .../joinir/api/ に併設してもよい)
    • frag.rs / exit_kind.rs / compose.rs / patch.rs

verifyFail-Fast の置き場所)

  • NormalizeBox 直後: terminator 語彙固定・edge-args 長さ一致・cond付きJump禁止など “意味SSOT” を確定
  • merge直前: boundary/ABI/edge-args の矛盾を即死させ “配線SSOT” を確定
  • --verify: PHI predecessor / CFG cache 整合 / edge-args の長さ一致を常設

直近の導入ステップ(最小で始める)

  1. Frag/ExitKind/EdgeStub の型を追加docs+code 入口 SSOT
  2. seq/if/loop の合成だけ実装cleanup/Invoke は後段)
  3. 既存 pattern のうち 1 本だけ Frag 合成に寄せるPattern8 推奨)
  4. 2 本目で再利用が見えたら "pattern番号での枝刈り" を削って合成側へ寄せる

Loop に関する注意JoinIR-only

  • cf_loop は JoinIR-onlyHard Freeze。EdgeCFG の “loop 直適用” を急いで別経路に生やすと SSOT が割れる。
  • loop の EdgeCFG 化は、まず **BasicBlockId 層で持っている箇所Phase 268 の if_form のような場所)**から適用を進める。
  • JoinIR 側の loop は Phase 270 で fixture/smoke による SSOT 固定を先に行い、壊れたら最小差分で直す。

補足Phase 270:

  • Pattern1simple_while_minimalは test-only stub のため、一般ループの “基準” には使えない。
  • Phase 270 では “最小の固定形” を Pattern9AccumConstLoopとして追加し、後で Frag 合成側へ吸収される前提で橋渡しにする。

Bridge patterns撤去条件SSOT

ここで言う “bridge pattern” は、既存の JoinIR ルートを壊さずに 最小の固定形を先に通すための一時パターン。 (例: Phase 270 の Pattern9_AccumConstLoop

  • 原則:
    • bridge pattern は 汎用化しない固定形SSOT + fixture/smoke で仕様を固定するだけ)。
    • 将来は Frag/ExitKind 合成側へ 吸収して削除する前提で追加する。

Bridge contractテンプレ / SSOT

bridge pattern を追加する場合は、最低限この “撤去条件” を先に書く(書けないなら追加しない)。

  • 固定する fixture/smokeSSOT
    • fixture最小と smokeintegrationを必ず紐づける
    • 「何が通れば撤去できるか」を machine-checkable にする
  • 置換先(吸収先)の SSOT がある
    • Pattern番号列挙の反対側に、必ず “吸収先” を書く(例: Frag/ExitKind 合成、もしくは emission 入口)
    • 吸収先が未確定な場合でも “層” は確定させるpattern層にロジックを増やさない
  • 撤去条件(最低限)
    1. 置換先(吸収先)で同じ fixture/smoke が PASS する
    2. bridge pattern 依存の分岐が router から消せる(最小差分で削除できる)
    3. quick/integration の FAIL 位置が悪化しない既知Failは増やさない
  • 撤去手順(最小)
    • router から bridge pattern を外す
    • fixture/smoke+ quickで PASS 維持
    • ファイル削除(または historical へ隔離し、SSOT から参照を外す

Phase 271: Pattern9_AccumConstLoop 撤去条件SSOT

Phase 270 の “JoinIR-only minimal loop” を通すための橋渡し。将来は Frag 合成側へ吸収して削除する。

  • 固定 fixture/smoke
    • fixture: apps/tests/phase270_p0_loop_min_const.hakoexit=3
    • smoke: tools/smokes/v2/profiles/integration/apps/phase270_p0_loop_min_const_vm.sh
  • 吸収先(層)
    • Structured→CFG lowering 層(Frag/ExitKind 合成)またはその emission 入口pattern層は extractor に縮退)
  • 撤去条件
    1. 上記 fixture/smoke が、bridge pattern を使わない経路で PASS するFrag/emit_frag 側で loop を構築できる)
    2. Pattern9 が router から削除されても coverage が落ちない(同 fixture が同じルートで通る)
    3. tools/smokes/v2/run.sh --profile quick が悪化しない
  • 撤去手順
    • Pattern9 の router 分岐を削除 → smoke PASS → Pattern9 実装を削除(または historical 化)

実装入口(コード SSOT

Phase 264 で入口API を作成完了

  • 物理配置: src/mir/builder/control_flow/edgecfg/api/
  • コア型: ExitKind, EdgeStub, Frag
  • 合成関数: seq, if_, loop_, cleanupシグネチャのみ、中身TODO
  • 検証: verify_frag_invariants(空実装)

Phase 265 P0 で最小実装完了

  • compose::loop_(): exit集合の分類実装配線なし、P1以降
  • verify_frag_invariants(): 最小検証追加(デバッグガード付き)
  • Pattern8適用: P0ではやらない偽Frag回避、P1から実戦投入

Phase 265 P1: 配線ロジック実装完了

目的: Frag/ExitKind が BasicBlockId 層で配線できることを証明

実装完了内容:

  • EdgeStub に target: Option<BasicBlockId> 追加
  • compose::loop_() が Continue → header, Break → after への配線を実行
  • verify_frag_invariants() が配線契約を検証(デバッグモード)
  • test-only PoC で配線の実証完了5個のテスト

配線契約:

  • Continue(loop_id) の EdgeStub.target = Some(header)
  • Break(loop_id) の EdgeStub.target = Some(after)
  • Normal/Return/Unwind の EdgeStub.target = None上位へ伝搬

Phase 265 P1 のスコープ:

  • Frag 層での配線ロジック
  • BasicBlockId 層でのテスト証明
  • MIR 命令生成Phase 266+
  • NormalizedShadow/JoinIR層への適用Phase 266+、JoinIR-VM Bridge 改修後)

Phase 265 P2 完了2025-12-21

実装完了内容:

  • Frag に wires: Vec<EdgeStub> フィールド追加
  • wires/exits 分離設計確立
    • exits: target = None のみ(未配線、外へ出る exit
    • wires: target = Some(...) のみ(配線済み、内部配線)
  • loop_() を wires 対応に更新Break/Continue → wires
  • seq(a, b) 実装完了a.Normal → wires
  • if_(header, cond, t, e, join_frag) 実装完了t/e.Normal → wires
  • verify_frag_invariants() に wires/exits 分離契約追加(警告のみ)
  • 全テスト PASS13個: frag 3個 + compose 9個 + verify 1個

設計判断の記録:

  1. なぜ wires/exits を分離するか?

    • 問題: 解決済み配線と未解決 exit を混ぜると、次の合成で内部配線が再度配線対象になる
    • 決定: wires/exits を分離し、不変条件を強化
    • 理由: 合成の意味が素直になり、Phase 266 で wires を MIR terminator に落とすだけ
  2. なぜ if_ は join_frag を受け取るか?

    • 問題: join: BasicBlockId だと、if の Normal exit が「join block」か「join 以降」か曖昧
    • 決定: join_frag: Frag を受け取る
    • 理由: if の Normal exit = join 以降join_frag.exitsが明確、PHI 生成の柔軟性確保
  3. なぜ verify は警告のみか?

    • P2 の役割: wires/exits 分離の証明に集中MIR 命令生成なし)
    • Phase 266 で MIR 生成時に verify を厳格化target 違反 → Err

次フェーズへの橋渡し:

次フェーズPhase 266: wires → MIR terminator 生成test-only PoC

  • wires を MIR terminator に落とす SSOT を追加(emit_wires
  • verify の strict 版を追加(verify_frag_invariants_strict、段階導入)

次フェーズPhase 267: JoinIR/NormalizedShadow への適用 + Branch 生成

  • NormalizedShadow/JoinIR で Frag/wires を実戦投入(層境界を守って段階的に)
  • Branch の terminator 生成wires → MIRを追加

Phase 267: Pattern6/7/8 への展開

  • Pattern6 (ScanWithInit) を Frag 化
  • Pattern7 (SplitScan) を Frag 化
  • Pattern8 (BoolPredicateScan) を Frag 化
  • 再利用性の確認pattern番号分岐削減

現時点では既存 pattern6/7/8 や merge/EdgeCFG は未改変(合成能力の証明のみ)。

Phase 266 P0-P2 完了2025-12-21

実装完了内容:

  • emit.rs 作成wires → MIR terminator 変換の SSOT
    • emit_wires() 実装from グループ化 + Return の target=None 許可)
    • unit test 4個jump/return/unwired/multiple_from_same_block
  • verify_frag_invariants_strict() 追加(段階導入を壊さない)
    • 既存の verify_frag_invariants() は変更なし(警告のまま)
    • wires/exits 分離契約を Err 化Return の target=None は許可)
  • mod.rs 更新emit module エクスポート)
  • 全テスト PASS1392 passed: 既存 1388 + 新規 4個

実装の核心原則:

  1. from ごとにグループ化して1本だけ許可

    • BTreeMap で from ごとにグループ化
    • 1 block = 1 terminator 制約を厳格に強制
  2. Return は target=None を許可

    • Return は target が意味を持たないemit_wires で無視される)
    • Fail-Fast 対象は Normal/Break/Continue/Unwind の target=None のみ
  3. verify_frag_invariants_strict() 別名で用意

    • 既存の verify_frag_invariants() は警告のまま維持
    • 新規 verify_frag_invariants_strict() で Err 化
    • PoC/emit 側だけ strict を使用(段階導入を壊さない)
  4. Phase 260 terminator 語彙ルールを厳守

    • Jump: set_jump_with_edge_args() を使用
    • Return: set_terminator() + set_return_env() を使用

設計判断の記録:

  1. なぜ from グループ化が必要か?

    • 問題: 同じ block に複数 terminator を設定すると上書きになる
    • 決定: from ごとにグループ化し、1本だけ許可Fail-Fast
    • 理由: 1 block = 1 terminator は MIR の不変条件
  2. なぜ Return は target=None を許可するか?

    • 問題: Return は呼び出し元に戻るので、target が意味を持たない
    • 決定: Return のみ target=None を許可
    • 理由: Normal/Break/Continue/Unwind は明確な target が必要
  3. なぜ verify_frag_invariants_strict() を別名にしたか?

    • 問題: 既存の verify_frag_invariants() を Err 化すると、既存コードが壊れる
    • 決定: 新規に strict 版を追加し、段階導入
    • 理由: Phase 267+ で既存コードを段階的に strict へ移行

次フェーズへの橋渡し:

Phase 267P0完了: Branch の第一級化BranchStub + emit_frag

  • 目的: Frag に Branch を第一級で追加し、wiresJump/Returnと同様に MIR terminator へ落とす入口を作る。
  • 追加:
    • BranchStubheader→then/else の分岐を表現)
    • Frag.branches: Vec<BranchStub>Branch 専用、wires と分離)
    • emit_frag(function, frag)SSOT: emit_wires + set_branch_with_edge_args、1 block=1 terminator を Fail-Fast
  • スコープ:
    • BasicBlockId 層で unit test により PoC 証明
    • NormalizedShadow/JoinIR への実適用は Phase 268 に繰り越し(層境界維持)

詳細: docs/development/current/main/phases/phase-267/README.md

Phase 268完了: if_form.rs への Frag 適用 + compose::if_ Entry Edge-args SSOT化

P0: 最小適用emission 層経由)

  • 目的: EdgeCFG Fragment を "層を跨がずに" 実戦投入する
  • 戦略: if_form.rs に直接 Frag 構築コードを書かず、emission/branch.rs に薄い入口関数 emit_conditional_edgecfg() を追加
  • 理由:
    1. 層が綺麗: Frag 構築ロジックを emission 層に閉じ込める
    2. 差分が小さい: if_form.rs は API 呼び出し差し替えのみ3箇所削除 + 1箇所追加
    3. デバッグ容易: 層境界が明確で問題切り分けが簡単
  • 実装:
    • emission/branch.rs に emit_conditional_edgecfg() 追加
    • if_form.rs の emit_conditional() + emit_jump() 2箇所を削除し、新規 API 呼び出しに置換
  • テスト結果:
    • cargo build --release: 成功
    • cargo test --lib --release: 1444/1444 PASS
    • quick smoke: 45/46 PASS

P1: compose::if_() Entry Edge-args SSOT化

  • 目的: compose::if_() の then/else entry edge-args を呼び出し側 SSOT にし、TODO 削除Phase 267 P2+ からの継続)
  • 核心原則: compose::if_() 内部で then/else entry edge-args を "勝手に空 Vec で生成" しない → 呼び出し側が明示的に渡す
  • 実装:
    • compose::if_() シグネチャ変更: if_(header, cond, t, then_entry_args, e, else_entry_args, join_frag)
    • emission/branch.rs::emit_conditional_edgecfg() から空 EdgeArgs を then/else 両方に渡す
    • EdgeCFG テスト更新compose.rs 2箇所、emit.rs 1箇所
    • TODO コメント削除完了
  • テスト結果:
    • cargo build --release: 成功
    • cargo test --lib --release: 1444/1444 PASS
    • quick smoke: 45/46 PASS

アーキテクチャ

if_form.rs (MirBuilder 層)
  ↓ 呼び出し
emission/branch.rs::emit_conditional_edgecfg() (emission 層: 薄ラッパー)
  ↓ 内部で使用
Frag 構築 + compose::if_() + emit_frag() (EdgeCFG Fragment API)
  ↓ 最終的に呼び出し
set_branch_with_edge_args() / set_jump_with_edge_args() (Phase 260 SSOT)

詳細: docs/development/current/main/phases/phase-268/README.md

Phase 267: JoinIR Pattern への適用

  • NormalizedShadow への Frag 適用
  • Pattern6/7/8 を Frag 化
  • Branch 生成 + pattern番号分岐削減
  • fixture + smoke test