diff --git a/AGENTS.md b/AGENTS.md index 9e810ef3..6cc63a9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -241,6 +241,34 @@ fn check_layer_boundary() { - CURRENT_TASK.md に理由/範囲/フラグ名/戻し手順を記録 - ロールバック容易(小さな差分、ガード除去で原状回復) +## JoinIR / 関数正規化 IR 方針(Phase 26-H 以降) + +- 目的: Hakorune の制御構造(if / loop / break / continue / return)を、内部 IR 層で **関数と継続だけ**に正規化することで、LoopForm v2 / PHI / ExitLiveness の負担を構造的に軽くする。 +- 原則: + - 言語仕様(.hako 構文)はそのまま維持し、`AST → MIR/LoopForm → JoinIR → VM/LLVM` という層を追加して段階移行する。 + - JoinIR 以降では「関数呼び出し+引数+継続」だけで制御を表現する(`loop_step(i, k_exit)`, `join_after_if(x, k_exit)` など)。 + - 既存の LoopForm v2 / PhiBuilderBox / ExitPhiBuilder / LoopExitLiveness は、JoinIR 生成のための「構造箱・判定箱」として徐々に責務を縮退させる。 +- 箱の層分け: + - 構造箱: ControlForm / LoopForm / JoinIR 変換(どの関数・継続を作るかを決める)。 + - 判定箱: LoopVarClassBox / LoopExitLivenessBox / IfBodyLocalMergeBox(どの変数がどこまで生きるかを決める)。 + - 生成箱: PhiBuilderBox / ExitPhiBuilder / JoinIREmitter(MIR/JoinIR を実際に吐く)。 + - 検証箱: PhiInvariantsBox / MirVerifier / 将来の JoinIRVerifier(不変条件チェックのみ)。 +- 運用ルール: + - 新しい箱を追加する前に、「この箱は上のどの層か」を必ず決める(構造+判定+生成が 1 箱に混ざらないようにする)。 + - 1つの責務(例: Exit PHI, ExitLiveness)に対して箱が 3〜4 個を超えないようにし、増え過ぎたら SSOT 箱(PhiBuilderBox 等)に統合する。 + +## 開発時の箱の粒度・増減ポリシー + +- 1箱 = 1つの質問だけに答える: + - 例: 「この変数はどのクラスか?」「この変数は exit 後で live か?」など。 + - 1箱の中で「分類+生成」や「判定+検証」を混ぜない。 +- 新しい箱を増やす前に: + - 既存箱に複数の質問が混ざっていないかを確認し、必要なら分解を優先する。 + - 似た箱が既にないかを確認し、重複する場合は統合/移設の設計を docs(architecture/*.md, roadmap/phase-XX/README.md)に先に書く。 +- フェーズ単位: + - 「このフェーズで触る箱はここまで」と明示し、箱の追加はフェーズごとに小さく・戻しやすくする。 + - 大きな箱追加(JoinIR など)は必ず roadmap/phase-X の README と architecture/*.md にセットで設計を書いてから実装に入る。 + **Cranelift 開発メモ(このブランチの主目的)** - ここは Nyash の Cranelift JIT/AOT 開発用ブランチだよ。JIT 経路の実装・検証・計測が主対象だよ。 - ビルド(JIT有効): `cargo build --release --features cranelift-jit` diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 40f01cdd..77b24fa7 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,4 +1,4 @@ -# Current Task — Phase 21.8 / 25 / 25.1 / 25.2 / 25.4 Snapshot(2025-11-18 時点) +# Current Task — Phase 21.8 / 25 / 25.1 / 25.2 / 25.4 / 26-F / 26-G Snapshot(2025-11-22 時点) > このファイルは「今どこまで終わっていて、次に何をやるか」を 1000 行以内でざっくり把握するためのスナップショットだよ。 > 詳細な履歴やログは `git log CURRENT_TASK.md` からいつでも参照できるようにしておくね。 @@ -11,6 +11,8 @@ - **21.8**: Numeric Core / Core-15 まわりの安定化(既に日常的には安定運用)。 - **25.x**: Stage0/Stage1/Stage‑B / Selfhost ラインのブートストラップと LoopForm v2 / LoopSSA v2 まわりの整備。 - **25.1 系**: Stage‑B / Stage‑1 / selfhost 向けに、Rust MIR / LoopForm v2 / LoopSSA v2 を段階的に整える長期ライン。 + - **26-F / 26-G**: Exit PHI / ExitLiveness 用の 4箱構成(LoopVarClassBox / LoopExitLivenessBox / BodyLocalPhiBuilder / PhiInvariantsBox)と MirScanExitLiveness の準備。 + - **26-H(New)**: JoinIR 設計+ミニ実験フェーズ(制御構造を関数呼び出しに正規化する IR を小さく試す)。 - Rust 側: - LoopForm v2 + ControlForm + Conservative PHI は、代表テスト(Stage‑1 UsingResolver / Stage‑B 最小ループ)ではほぼ安定。 - 静的メソッド呼び出し規約と `continue` 絡みの PHI は 25.1m までで根治済み。 @@ -178,7 +180,57 @@ - チームレビュー - **長期**: NamingBox/UnifiedCallEmitter/VM の 3 点で「名前と arity の SSOT」完全統一。 -### 1-0. Phase 25.3 — FuncScanner / Stage‑B defs 安定化(完了) +### 1-02. Phase 26-F — Loop Exit Liveness / BodyLocal PHI Guard(箱とガードの整備、2025-11-22 時点) + +**目的** +- LoopForm v2 / Exit PHI まわりで、BodyLocal 変数の未定義利用を「箱」と「Fail-Fast」で確実に検知・抑制できるようにする。 +- MIR スキャン(本物の Exit Liveness)は次フェーズに送りつつ、構造と受け口と環境変数ガードだけ先に整える。 + +**やったこと(26-F 初期完了分)** +- 4箱構成の整理と実装(Exit PHI 専用レイヤ): + - `LoopVarClassBox`(`loop_var_classifier.rs`): + - 変数のスコープ分類専用(Pinned / Carrier / BodyLocalExit / BodyLocalInternal)。 + - ここでは「どこで定義されているか」だけを見て、PHI 発行は行わない。 + - `LoopExitLivenessBox`(`loop_exit_liveness.rs` 新設): + - ループ exit 後で「生きている可能性のある変数」の集合を返す箱。 + - Phase 26-F 時点では中身は保守的近似+ダミー、実運用は環境変数ガードで OFF。 + - 環境変数: + - `NYASH_EXIT_LIVE_ENABLE=1` で将来の MIR スキャン実装を opt-in で有効化(既定は 0/未設定)。 + - `NYASH_EXIT_LIVENESS_TRACE=1` でトレース出力。 + - `BodyLocalPhiBuilder`(`body_local_phi_builder.rs`): + - `LoopVarClassBox` の分類結果と `LoopExitLivenessBox` の `live_at_exit` を統合し、「どの BodyLocal に exit PHI が必要か」を決める箱。 + - 既定挙動(ガード OFF): + - これまで通り `class.needs_exit_phi()` のみを見る(Pinned / Carrier / BodyLocalExit)。 + - ガード ON の挙動(まだ実験段階): + - `BodyLocalInternal` かつ `live_at_exit` に含まれ、かつ `LocalScopeInspector::is_available_in_all` が true なものだけ、追加で exit PHI 候補に昇格できる OR ロジックを持つ。 + - `PhiInvariantsBox`(`phi_invariants.rs`): + - Exit/If PHI 最後の Fail-Fast 検証箱。 + - 「全 pred で定義されているか」「不正な incoming が無いか」をチェックし、構造バグはここで止める。 +- Docs 整備: + - `docs/development/architecture/loops/loopform_ssot.md` に 4箱構成と環境変数ガード方針を追記。 + - `docs/development/roadmap/phases/phase-26-F/README.md` を新設し、26-F のスコープ/やらないこと/次フェーズ(MIR スキャン本体)への橋渡しを書き切り。 + +**テスト状況(ガード OFF 時点)** +- Phase 26-F-3 → 26-F の流れで、一時的に退行したが、 + - `NYASH_EXIT_LIVE_ENABLE` を既定 OFF にし、 + - BodyLocalInternal 救済ロジックをガード付きに戻したことで、 + - F3 ベースラインより PASS が増え、FAIL が減る状態(例: 365 PASS / 9 FAIL)まで持ち直し済み。 +- FuncScanner 系: + - `mir_funcscanner_skip_ws_direct_vm` + - `mir_funcscanner_parse_params_trim_min_verify_and_vm` + は引き続き「BodyLocal / Exit Liveness のカナリア」として使う(未定義値が出た場合は 26-G 以降で追う)。 + +**このフェーズで残っていること** +- `ExitLivenessProvider` 相当のインターフェースを `ExitPhiBuilder` 周辺に導入し、「ExitLiveness を差し替え可能」な受け口だけ整える(中身は Legacy のまま)。→ **完了**。`MirScanExitLiveness` も追加済み(現状は header/exit_snapshots の union を返す簡易版)。 +- LoopFormOps / MirBuilder に MIR 命令列アクセスを追加する設計を 26-F の README にメモしておき、実装は 26-G 以降に分離する。 + +### 1-02+. Phase 26-G — Exit Liveness MIR Scan(計画開始) + +- 26-F で作った差し替え口に、本物の use/def スキャン実装を載せるフェーズ。 +- `NYASH_EXIT_LIVE_ENABLE=1` で MIR スキャン版を有効にし、FuncScanner カナリア(skip_ws / parse_params_trim)を緑にするのが目標。 +- 新設 docs: `docs/development/roadmap/phases/phase-26-G/README.md` に手順と受け入れ条件を記載済み。 + +### 1-03. Phase 25.3 — FuncScanner / Stage‑B defs 安定化(完了) **目的** - Stage‑B / FuncScanner ラインで defs が欠落したり Loop が脱落する問題を塞ぎ、selfhost 側の canary を緑に戻す。 @@ -464,10 +516,74 @@ Rust 側は LoopForm v2 / Stage‑B fib / Stage‑1 UsingResolver 構造テス - `lang/src/compiler/pipeline_v2/using_resolver_box.hako` 冒頭に、「entry 側=ファイル I/O + using 収集」「pipeline_v2 側=modules_json 上の alias/path 解決」と役割メモを追加。 - RegexFlow ベースの単一路ループは Region 化不要(stateful helper)とし、LoopForm v2 の観点からも「観測対象外」とする。 -### C. Stage‑B / LoopSSA / Selfhost まわり(中期タスク) +### C. FuncScanner / Exit PHI カナリア(Phase 26-F / 26-G への橋渡し) -- C‑1: Stage‑B 再入ガード `env.set/2` の整理(据え置き) +- C‑1: FuncScanner trim/skip_ws/parse_params の最小再現ケース固定(DONE) + - `lang/src/compiler/tests/funcscanner_trim_min.hako` で `_trim` / `trim` / `skip_whitespace` を 1 回ずつ呼ぶ最小 Main を定義。 + - `src/tests/mir_funcscanner_trim_min.rs` で: + - Stage‑3 / using 有効化したパーサ設定で func_scanner.hako + 上記テストを一体コンパイル。 + - `MirVerifier` でモジュール全体の SSA/PHI を検証(`HAKO_MIR_BUILDER_METHODIZE=0` でも常に緑になることを確認)。 + - VM 実行は `NYASH_TRIM_MIN_VM=1` のときだけ有効化(いまは MIR 側の根治が主目的)。 +- C‑2: FuncScanner 側ロジックの構造整理(DONE) + - `lang/src/compiler/entry/func_scanner.hako`: + - `parse_params` を Region+next_i 形の 1 本ループに整理し、先頭スキップとカンマ探索をそれぞれ helper に寄せた。 + - `trim` は先頭側を `skip_whitespace` に全面委譲し、末尾側のみ後ろ向きループで処理するように簡素化。 + - `skip_whitespace` は `loop(i < n)` + if/continue だけにした最小形にし、過去の dev 向けログや loop(1==1) ワークアラウンドを撤去。 + - これにより FuncScanner フロントは LoopForm v2 / LoopSSA v2 から見て「素直なループ+明確な next_i 形」になり、以後の PHI/ExitLiveness 側の根治作業が `.hako` に依存しづらい形になった。 +- C‑3: Phase 26-F / 26-G のカナリアとして位置付け(進行中) + - 26-F 時点では `NYASH_EXIT_LIVE_ENABLE` 既定 OFF で、従来挙動のまま `mir_funcscanner_trim_min` が MIR verify 緑になることを確認済み。 + - 26-G では: + - `NYASH_EXIT_LIVE_ENABLE=1` + MirScanExitLiveness 経由でも `mir_funcscanner_trim_min`(特に FuncScannerBox.trim/1 / skip_whitespace/2 / parse_params/1)が常に緑になることを受け入れ条件にする。 + - そのうえで `mir_funcscanner_skip_ws_direct_vm` / Stage‑B / Stage‑1 UsingResolver 系のカナリアも順に緑に揃える。 + +### D. Stage‑B / LoopSSA / Selfhost まわり(中期タスク) + +- D‑1: Stage‑B 再入ガード `env.set/2` の整理(据え置き) - dev 専用 extern を Rust 側に追加するか、Stage‑B 箱側で state に寄せるかを決める必要があるが、現在はループ/PHI ラインを優先し保留。 + +--- + +## 4. Phase 26-H — JoinIR 設計 & ミニ実験(新規フェーズ) + +**目的** +- 制御構造(if / loop / break / continue / return)を **関数呼び出し+継続** に正規化する中間層(JoinIR)を設計し、LoopForm v2 / PHI / ExitLiveness の負担を将来軽くする足場を作る。 +- 25.1 / 26-F / 26-G の本線を止めずに、「設計+ごく小さな実験」だけを先に進める。 + +**やること(26-H スコープ)** + +- H‑1: JoinIR 設計ドキュメントの追加 + - `docs/development/architecture/join-ir.md` に命令セットと変換規則、対応表を記述(設計反映済み)。 + - `docs/development/roadmap/phases/phase-26-H/README.md` に、26-H のスコープ/やらないこと/他フェーズとの関係を記載(済)。 + +- H‑2: JoinIR 型定義の骨格だけ入れる + - ファイル候補: `src/mir/join_ir.rs`。 + - `JoinFunction`, `JoinInst`, `JoinContId` 等の型だけ定義しておき、MIR→JoinIR 変換のロジックはまだ書かない。 + - MirQueryBox(`src/mir/query.rs`)から JoinIR への変換に必要な最小 API(succs / reads / writes)が揃っているか確認する(26-G との整合を優先)。 + +- H‑3: 1 ケースだけのミニ変換実験 + - 専用の最小 .hako を 1 本用意する(例: `apps/tests/joinir_min_loop.hako`)。 + - `loop(i < 3) { if i >= 2 { break } i = i + 1 } return i` 程度の単純なケース。 + - `src/tests/mir_joinir_min.rs`(仮)で: + - 上記 .hako を MIR までコンパイル。 + - 手書き or ごく簡単なロジックで JoinIR を 1 関数ぶんだけ生成してダンプする。 + - join/loop_step/k_exit の形になっているか、手動で確認する。 + - このフェーズでは JoinIR の VM 実行まではやらず、「形が破綻していないか」を見るだけに留める。 + +**やらないこと(26-H では保留)** + +- 既存の LoopForm v2 / PhiBuilderBox / ExitPhiBuilder を JoinIR ベースに全面移行すること。 +- Stage‑B / Stage‑1 / CLI / selfhost ラインの本線を JoinIR で差し替えること。 +- 既存の SSA/PHI 実装を削除すること(全部別フェーズで検討)。 + +**優先度と位置付け** + +- 本線(いま重視する順): + 1. Phase 25.1 — Stage‑1 UsingResolver / Stage‑1 CLI program-json/mir-json を安定化。 + 2. Phase 26-F / 26-G — Exit PHI / ExitLiveness の根治(LoopForm v2 / PHI SSOT / MirScanExitLiveness)。 +- Phase 26-H は: + - 「本線の合間に進める設計フェーズ」として扱う。 + - JoinIR が小さいケースでうまく動くことを確認できたら、27.x 以降で本格的な導入を検討する。 + - このタスクに着手するときは「prod/CI 経路から完全に切り離した dev ガード」として設計する。 - C‑2: .hako LoopSSA v2 実装(Rust LoopForm v2 への追従) - Rust LoopForm v2 の Carrier/Pinned/BodyLocalInOut モデルを `.hako` の LoopSSA に輸入し、Stage‑B Test2 / selfhost CLI でも MirVerifier 緑を目指す中〜長期タスク。 diff --git a/apps/tests/joinir_min_loop.hako b/apps/tests/joinir_min_loop.hako new file mode 100644 index 00000000..f6eb5b83 --- /dev/null +++ b/apps/tests/joinir_min_loop.hako @@ -0,0 +1,26 @@ +// JoinIR カナリアテスト — 最小ループ+break +// Phase 26-H: JoinIR 型定義妥当性確認用 +// +// 期待される JoinIR イメージ: +// fn main(k_exit) { +// loop_step(0, k_exit) +// } +// +// fn loop_step(i, k_exit) { +// if i >= 2 { +// k_exit(i) // break +// } else { +// loop_step(i + 1, k_exit) // continue +// } +// } + +static box JoinIrMin { + main() { + local i = 0 + loop(i < 3) { + if i >= 2 { break } + i = i + 1 + } + return i + } +} diff --git a/docs/development/architecture/join-ir.md b/docs/development/architecture/join-ir.md new file mode 100644 index 00000000..0428e317 --- /dev/null +++ b/docs/development/architecture/join-ir.md @@ -0,0 +1,278 @@ +# JoinIR — 関数正規化 IR(関数と継続だけで制御を表現する層) + +目的 +- Hakorune の制御構造(`if` / `loop` / `break` / `continue` / `return`)を、**関数呼び出し+継続(continuation)だけに正規化する IR 層**として設計する。 +- これにより: + - φ ノード = 関数の引数 + - merge ブロック = join 関数 + - ループ = 再帰関数(loop_step)+ exit 継続(k_exit) + - break / continue = 適切な関数呼び出し + という形に落とし込み、LoopForm v2 / Exit PHI / BodyLocal / ExitLiveness の負担を**構造的に軽くする**。 + +前提・方針 +- LoopForm / ControlForm は「構造の前段」として残す(削除しない)。 +- JoinIR は「PHI/SSA の実装負担を肩代わりする層」として導入し、ヘッダ/exit φ や BodyLocal の扱いを **関数の引数と継続** に吸収していく。 +- PHI 専用の箱(HeaderPhiBuilder / ExitPhiBuilder / BodyLocalPhiBuilder など)は、最終的には JoinIR 降ろしの補助に縮退させることを目標にする。 + +位置づけ +- 変換パイプラインにおける位置: + +```text +AST → MIR(+LoopForm v2) → JoinIR → VM / LLVM +``` + +- AST / MIR では従来どおり Nyash 構文(if / loop 等)を扱い、 + JoinIR 以降では **関数と値(Box+Primitive)だけ**を見る。 + +--- + +## 1. JoinIR のコアアイデア + +### 1-1. 「ループ = 関数を何回も呼ぶこと」 + +通常の while/loop は: + +```hako +loop(i < n) { + if i >= n { break } + i = i + 1 +} +return i +``` + +JoinIR 的に見ると: + +```text +fn main(k_exit) { + loop_step(0, k_exit) +} + +fn loop_step(i, k_exit) { + if i >= n { + // break + k_exit(i) + } else { + // continue + loop_step(i + 1, k_exit) + } +} +``` + +- ループ本体 = `loop_step(i, k_exit)` という関数。 +- `i` は LoopCarried(carrier)変数 → 関数の引数で表現。 +- break = exit 継続 `k_exit` の呼び出し。 +- continue = `loop_step` をもう一度呼ぶこと。 + +### 1-2. 「if の merge = join 関数」 + +ソース: + +```hako +if cond { + x = 1 +} else { + x = 2 +} +print(x) +``` + +JoinIR 的には: + +```text +fn main(k_exit) { + if cond { + then_branch(k_exit) + } else { + else_branch(k_exit) + } +} + +fn then_branch(k_exit) { + x = 1 + join_after_if(x, k_exit) +} + +fn else_branch(k_exit) { + x = 2 + join_after_if(x, k_exit) +} + +fn join_after_if(x, k_exit) { + print(x) + k_exit(0) +} +``` + +- φ ノード = `join_after_if` の引数 `x`。 +- merge ブロック = `join_after_if` 関数。 +- 「どのブランチから来たか」の情報は、**関数呼び出し**に吸収される。 + +--- + +## 2. JoinIR の型イメージ + +※ Phase 26-H 時点では「設計のみ」を置いておき、実装は最小ケースから。 + +### 2-1. 関数と継続 + +```rust +/// JoinIR 関数ID(MIR 関数とは別 ID でもよい) +struct JoinFuncId(u32); + +/// 継続(join / ループ step / exit continuation)を識別するID +struct JoinContId(u32); + +/// JoinIR 関数 +struct JoinFunction { + id: JoinFuncId, + params: Vec, // 引数(φ に相当) + body: Vec, // 命令列 + exit_cont: Option, // 呼び出し元に返す継続(ルートは None) +} +``` + +### 2-2. 命令セット(最小) + +```rust +enum JoinInst { + /// 通常の関数呼び出し: f(args..., k_next) + Call { + func: JoinFuncId, + args: Vec, + k_next: JoinContId, + }, + + /// 継続呼び出し(join / exit 継続など) + Jump { + cont: JoinContId, + args: Vec, + }, + + /// ルート関数 or 上位への戻り + Ret { + value: Option, + }, + + /// それ以外の演算は、現行 MIR の算術/比較/boxcall を再利用 + Compute(MirLikeInst), +} +``` + +### 2-3. 継続の分類 + +- join 継続: + - if の merge や、ループ body 終了後の「次の場所」を表す。 +- loop_step: + - ループ 1ステップ分の処理。 +- exit 継続: + - ループを抜けた後の処理(`k_exit`)。 +- これらはすべて「関数 or 継続 ID」と「引数」で表現される。 + +--- + +## 3. MIR/LoopForm から JoinIR への変換ルール(v0 草案) + +ここでは Phase 26-H で扱う最小のルールだけを書く。 + +### 3-1. ループ(LoopForm v2)→ loop_step + k_exit + +前提: LoopForm v2 が提供する情報 +- preheader / header / body / latch / exit / continue_merge / break ブロック集合。 +- LoopVarClassBox による分類(Pinned / Carrier / BodyLocalExit / BodyLocalInternal)。 + +変換方針(概略): + +1. LoopCarried 変数の集合 `C = {v1, v2, ...}` を求める。 +2. 「ループを抜けた後で使う変数」の集合 `E` を ExitLiveness から得る(長期的には MirScanExitLiveness)。 +3. JoinIR 上で: + - `fn loop_step(C, k_exit)` を新設。 + - ループ前の値から `loop_step(C0, k_exit0)` を呼び出す。 + - body 内の `break` は `k_exit(E)` に変換。 + - body 内の `continue` は `loop_step(C_next, k_exit)` に変換。 + +今の LoopForm / PhiBuilderBox がやっている仕事(header φ / exit φ 生成)は、 +最終的には「`C` と `E` を決める補助」に寄せていくイメージになる。 + +### 3-2. if → join_after_if + +前提: +- control_flow / if_form.rs で IfForm(cond / then / else / merge)が取れる。 + +変換方針: + +1. merge ブロックで φ が必要な変数集合 `M = {x1, x2, ...}` を求める。 +2. JoinIR 上で: + - `fn join_after_if(M, k_exit)` を新設。 + - then ブランチの末尾に `join_after_if(x1_then, x2_then, ..., k_exit)` を挿入。 + - else ブランチの末尾に `join_after_if(x1_else, x2_else, ..., k_exit)` を挿入。 +3. merge ブロックの本体は `join_after_if` に移す。 + +これにより、φ ノードは `join_after_if` の引数に吸収される。 + +--- + +## 4. フェーズ計画(Phase 26-H 〜 27.x の流れ) + +### Phase 26-H(現在フェーズ)— 設計+ミニ実験 + +- スコープ: + - JoinIR の設計ドキュメント(このファイル+ phase-26-H/README)。 + - `src/mir/join_ir.rs` に型だけ入れる(変換ロジックは最小)。 + - 1 ケース限定の変換実験: + - 例: `apps/tests/joinir_min_loop.hako` → MIR → JoinIR → JoinIR のダンプを Rust テストで確認。 +- ゴール: + - 「関数正規化 IR(JoinIR)が箱理論/LoopForm v2 と矛盾しない」ことを確認する。 + - 大規模リプレースに進む価値があるか、感触を掴む。 + +### Phase 27.x(仮)— 段階的な採用案(まだ構想段階) + +この段階はまだ「構想メモ」として置いておく。 + +- 27.1: LoopForm v2 → JoinIR の変換を部分的に実装 + - まずは「break なしの loop + if」だけを対象にする。 + - Exit φ / header φ の一部を JoinIR の引数に置き換える。 +- 27.2: break / continue / BodyLocalExit を JoinIR で扱う + - BodyLocalPhiBuilder / LoopExitLiveness の「決定」を JoinIR 引数設計に寄せる。 + - ExitLiveness は JoinIR 関数の use/def から算出可能かを検証する。 +- 27.3: SSA/PHI との役割調整 + - LoopForm v2 / PhiBuilderBox は「JoinIR 生成補助」の位置付けに縮退。 + - MirVerifier は JoinIR ベースのチェック(又は MIR/JoinIR 並列)に切り替え検討。 + +※これら 27.x フェーズは、26-F / 26-G で現行ラインの根治が一段落してから進める前提。 + +--- + +## 5. 他ドキュメントとの関係 + +- `docs/development/roadmap/phases/phase-26-H/README.md` + - Phase 26-H のスコープ(設計+ミニ実験)、他フェーズ(25.1 / 26-F / 26-G)との関係を定義。 +- `docs/development/architecture/loops/loopform_ssot.md` + - LoopForm v2 / Exit PHI / 4箱構成(LoopVarClassBox / LoopExitLivenessBox / BodyLocalPhiBuilder / PhiInvariantsBox)の設計ノート。 + - 将来、JoinIR を導入するときは「LoopForm v2 → JoinIR 変換の前段」としてこの SSOT を活かす。 + +このファイルは、JoinIR を「全部関数な言語」のコアとして扱うための設計メモだよ。 +実際の導入は小さな実験(Phase 26-H)から始めて、問題なければ 27.x 以降で段階的に進める想定にしておく。 + +## 6. φ と merge の対応表(抜粋) + +| 構文/概念 | JoinIR での表現 | φ/合流の扱い | +|-----------|-----------------|--------------| +| if/merge | join 関数呼び出し | join 関数の引数が φ 相当 | +| loop | step 再帰 + k_exit 継続 | LoopCarried/Exit の値を引数で渡す | +| break | k_exit 呼び出し | φ 不要(引数で値を渡す) | +| continue | step 呼び出し | φ 不要(引数で値を渡す) | +| return | 継続または ret | そのまま値を返す | + +## 7. 26-H で増やす実装箱 / 概念ラベル + +- 実装として増やす(このフェーズで手を動かす) + - `join_ir.rs`: `JoinFunction/JoinBlock/JoinInst` の最小定義とダンプ + - LoopForm→JoinIR のミニ変換関数(1 ケース限定で OK) + - 実験トグル(例: `NYASH_JOINIR_EXPERIMENT=1`)で JoinIR をダンプするフック + +- 概念ラベルのみ(27.x 以降で拡張を検討) + - MirQueryBox のような MIR ビュー層(reads/writes/succs を trait 化) + - LoopFnLoweringBox / JoinIRBox の分割(必要になったら分ける) + - JoinIR 上での最適化や VM/LLVM 統合 + +このフェーズでは「箱を増やしすぎない」ことを意識し、型定義+最小変換+ダンプ確認だけに留める。 diff --git a/docs/development/architecture/loops/loopform_ssot.md b/docs/development/architecture/loops/loopform_ssot.md index d8e586e2..1ee1ef82 100644 --- a/docs/development/architecture/loops/loopform_ssot.md +++ b/docs/development/architecture/loops/loopform_ssot.md @@ -23,9 +23,24 @@ SSOT(Single Source of Truth) - 1 predecessor なら直接 bind(PHI省略)、2つ以上で PHI を生成。 - 検証は Fail‑Fast ではなく開発時 WARN(`debug_assert`)だが、将来 Core 側で整形に移管予定。 +4箱構成(Phase 26-F 系の整理) +- **LoopVarClassBox**(`loop_var_classifier.rs`) + - 変数のスコープ分類専用箱だよ。Pinned / Carrier / BodyLocalExit / BodyLocalInternal を決めるだけで、PHI 発行はしない。 +- **LoopExitLivenessBox**(`loop_exit_liveness.rs`) + - ループ `exit` 直後で「実際に使われる可能性がある変数」を集める箱だよ。Phase 26-F 時点では `live_at_exit` は保守的近似で、将来 `get_block_instructions()` などを使った MIR スキャンに差し替える予定。 + - `ExitLivenessProvider` を実装していて、`ExitPhiBuilder` は Box を受け取る形にしたので、Legacy(既定)と MirScan 版の差し替えがそのまま出来る。 + - 環境変数 `NYASH_EXIT_LIVE_ENABLE=1` で将来の実装を段階的に有効化、`NYASH_EXIT_LIVENESS_TRACE=1` でトレースを出せるようにしてある。 +- **BodyLocalPhiBuilder**(`body_local_phi_builder.rs`) + - 上の 2つの箱の結果を統合する決定箱だよ。 + - `class.needs_exit_phi()` が true(Pinned / Carrier / BodyLocalExit)のものは従来どおり exit PHI 候補。 + - それ以外でも、`BodyLocalInternal` かつ `live_at_exit` に含まれ、かつ `is_available_in_all` で全 pred 定義が確認できるものだけを安全側で救済候補にできるようにしてある(この救済ロジック自体は `NYASH_EXIT_LIVE_ENABLE` でガード)。 +- **PhiInvariantsBox**(`phi_invariants.rs`) + - 最後に「全 pred で定義されているか」「不正な incoming が無いか」を Fail-Fast でチェックする箱だよ。ここで落ちる場合は LoopForm/BodyLocal 側の構造バグとみなしている。 + 今後の移行 - Bridge 側に `LoopPhiOps` 実装を追加し、`prepare/seal/exit` を直接呼ぶ。 - ループ形状の生成をユーティリティ化(builder/bridge 双方から共通呼び出し)。 +- ExitLivenessProvider は 26-G 以降で MIR 命令列スキャン版に差し替える予定(現状の MirScanExitLiveness は header/exit_snapshots の union 近似)。 --- diff --git a/docs/development/roadmap/phases/phase-26-F/README.md b/docs/development/roadmap/phases/phase-26-F/README.md new file mode 100644 index 00000000..e9271817 --- /dev/null +++ b/docs/development/roadmap/phases/phase-26-F/README.md @@ -0,0 +1,95 @@ +# Phase 26-F — Loop Exit Liveness / BodyLocal PHI Guard Line + +Status: planning(設計 + 受け口整備。MIRスキャン本体は後続フェーズ) + +## ゴール + +- LoopForm v2 / Exit PHI まわりで残っている BodyLocal 変数の未定義バグを、「箱」と「MIR スキャン」に分けて根治する。 +- すでに Phase 26-E までで固めた PHI SSOT(PhiBuilderBox / IfForm / ExitPhiBuilder / BodyLocalPhiBuilder)を前提に: + - **分類**(LoopVarClassBox)と + - **実際の使用**(LoopExitLivenessBox, 将来の MIR スキャン) + をきちんと分離する。 +- その上で FuncScanner / Stage‑B / Stage‑1 入口で発生している `use of undefined value` を、構造から潰す足場を固める。 + +## スコープ(26-F でやること) + +### F‑A: Exit PHI 4箱構成の確定とガード + +- ファイル: + - `src/mir/phi_core/loop_var_classifier.rs` + - `src/mir/phi_core/local_scope_inspector.rs` + - `src/mir/phi_core/body_local_phi_builder.rs` + - `src/mir/phi_core/loop_exit_liveness.rs`(新設済み) + - `src/mir/phi_core/phi_invariants.rs` + - `src/mir/phi_core/exit_phi_builder.rs` +- やること: + - 4箱構成をドキュメントに固定する(コードはほぼ出来ているので設計を追記する): + - LoopVarClassBox: Pinned / Carrier / BodyLocalExit / BodyLocalInternal の分類専用。 + - LoopExitLivenessBox: ループ exit 後で「生きている変数」の集合を返す箱(Phase 26-F 時点では保守的近似+環境変数ガード)。 + - BodyLocalPhiBuilder: 分類結果と `live_at_exit` を OR 判定し、「どの BodyLocal に exit PHI が必要か」だけを決める箱。 + - PhiInvariantsBox: 「全 pred で定義されているか」を Fail‑Fast でチェックする箱。 + - `NYASH_EXIT_LIVE_ENABLE` が未設定のときは **従来挙動(Phase 26-F-3 相当)** に固定されることを明文化。 + - `NYASH_EXIT_LIVENESS_TRACE=1` で、将来の MIR スキャン実装時にトレースを出す方針を記録。 + +### F‑B: MIR スキャン前提の ExitLiveness 受け口設計 + +- ファイル: + - `src/mir/phi_core/loop_exit_liveness.rs` + - `src/mir/phi_core/exit_phi_builder.rs` + - (将来)`src/mir/loop_builder.rs` / `src/mir/function.rs` + - `src/mir/query.rs`(MirQuery/MirQueryBox) +- やること: + - ExitLiveness 用のトレイトを決める → **実装済み**。 + - `ExitLivenessProvider::compute_live_at_exit(header_vals, exit_snapshots) -> BTreeSet` + - 既定実装は `LoopExitLivenessBox`(Phase 26-F-3 と同じ空集合、env ガード付き) + - Phase 2+ 用の `MirScanExitLiveness` を追加済み。現時点では「header_vals + exit_snapshots に出現する変数の union」を返す簡易スキャンで、`NYASH_EXIT_LIVE_ENABLE=1` で opt-in。後続フェーズ(26-G 以降)で MIR 命令列スキャンに差し替える想定。 + - Phase 26-F では「箱とインターフェース」だけ決めて、実装は**保守的近似 or ダミー**のままに留める。 + - `ExitPhiBuilder` は **ExitLivenessProvider の結果だけを見る**ようにしたので、後続フェーズで `MirScanExitLiveness` に差し替え可能(依存逆転)。 + +### F‑C: FuncScanner / parse_params / trim 用の再現ケース固定 + +- ファイル: + - `lang/src/compiler/entry/func_scanner.hako` + - `lang/src/compiler/tests/funcscanner_skip_ws_min.hako` + - `lang/src/compiler/tests/funcscanner_parse_params_trim_min.hako` + - `src/tests/mir_funcscanner_skip_ws.rs` + - `src/tests/mir_funcscanner_parse_params_trim_min.rs` +- やること: + - すでに作成済みの 2 本の Rust テストを「ExitLiveness / BodyLocal PHI のカナリア」として位置づける: + - `mir_funcscanner_skip_ws_direct_vm` + - `mir_funcscanner_parse_params_trim_min_verify_and_vm` + - この 2 本が、今後の MIR スキャン実装(Phase 26-G 相当)で「ExitLiveness を差し込んだときに必ず緑になるべき」ターゲットであることを docs に固定。 + - `_trim` / `skip_whitespace` 本体には、`__mir__.log` ベースの軽量観測が既に仕込まれているので、その存在を `mir-logs-observability.md` 側にリンクしておく。 + +### F‑D: 将来フェーズ(MIR スキャン本体)への橋渡し + +- ファイル候補: + - `docs/development/roadmap/phases/phase-26-F/README.md`(本ファイル) + - `docs/development/architecture/loops/loopform_ssot.md` +- やること: + - Phase 26-F では「箱」と「受け口」と「環境変数ガード」までに留め、MIR 命令列の実スキャンは次フェーズ(26-G など)に分離する方針を書き切る。 + - LoopFormOps を拡張して MIR 命令列にアクセスする案(`get_block_instructions` 等)を、設計レベルでメモしておく。 + - ExitLiveness の MIR スキャン実装は: + - exit ブロック(必要ならその直後)での use/def を収集し、 + - 逆 RPO で固定点反復して `live_in/ live_out` を決める、 + といった最小の liveness アルゴリズムで良いことを明記。 + +## このフェーズで「やらない」こと + +- LoopFormOps / MirBuilder の広範な API 拡張や、大規模な構造変更。 + - MIR スキャン本体の導入は 26-F ではなく 26-G 以降に分離し、ここではあくまで箱と受け口とドキュメントに留める。 +- 既存の LoopForm v2 / Exit PHI ロジックの意味的変更。 + - `NYASH_EXIT_LIVE_ENABLE` が未設定のときの挙動は、Phase 26-F-3 と同等に保つ(テストもそれを期待する)。 +- Stage‑B / Stage‑1 CLI / UsingResolver の本線仕様変更。 + - 26-F で触るのはあくまで PHI/SSA のインフラ層のみ。高レベル仕様は 25.x の各フェーズに従う。 + +## 受け入れ条件(26-F) + +- Docs: + - `docs/development/architecture/loops/loopform_ssot.md` に 4箱構成(LoopVarClassBox / LoopExitLivenessBox / BodyLocalPhiBuilder / PhiInvariantsBox)の役割が追記されている。 + - 本 README に ExitLiveness の受け口設計(ExitLivenessProvider 相当)と、MIR スキャン本体を次フェーズに送る方針が書かれている。 +- コード: + - `NYASH_EXIT_LIVE_ENABLE` が未設定のとき、Phase 26-F-3 と同等かそれ以上のテスト結果(PASS 増 / FAIL 減)を維持している。 + - `LoopExitLivenessBox` / `BodyLocalPhiBuilder` / `PhiInvariantsBox` / `ExitPhiBuilder` の依存関係が一方向(解析→判定→生成→検証)に整理されている。 +- テスト: + - `mir_funcscanner_skip_ws_direct_vm` / `mir_funcscanner_parse_params_trim_min_verify_and_vm` が引き続き「ExitLiveness/BodyLocal PHI カナリア」として動作し、PHI/SSA の変更時に必ず確認される位置づけになっている。 diff --git a/docs/development/roadmap/phases/phase-26-G/README.md b/docs/development/roadmap/phases/phase-26-G/README.md new file mode 100644 index 00000000..7589b360 --- /dev/null +++ b/docs/development/roadmap/phases/phase-26-G/README.md @@ -0,0 +1,109 @@ +# Phase 26-G — Exit Liveness MIR Scan (実装フェーズ) + +Status: planning + +## ゴール(何を達成するフェーズか) + +- ExitLivenessProvider の本実装(MIR 命令列スキャン版)を作り、FuncScanner のカナリア + - `mir_funcscanner_skip_ws_direct_vm` + - `mir_funcscanner_parse_params_trim_min_verify_and_vm` + を緑にする。 +- 26-F で用意した差し替え口(MirScanExitLiveness)に、use/def ベースの liveness を実装する。 +- `NYASH_EXIT_LIVE_ENABLE=1` で MIR スキャン版を有効化し、デフォルトは従来挙動のまま安全側。 + +## スコープ(このフェーズでやること) + +- LoopFormOps / LoopBuilder / JSON bridge (LoopFormJsonOps) に「MirFunction への参照を返す API」を追加する設計と最小実装。 +- MirScanExitLiveness を `MirQuery` ベースの use/def スキャンに置き換える。 +- 代表テスト(FuncScanner カナリア)で緑を確認するところまで。 + +## 実装ステップ(順序) + +### Step 1: API 設計と LoopFormOps 拡張 + +- 目的: Exit 側から MirFunction(または MirQuery)に到達できるようにする。 +- 具体案: + - `LoopFormOps` に「MirFunction への参照を返すメソッド」を追加する: + - 例: `fn mir_function(&self) -> &MirFunction;` + - 実装箇所: + - `impl<'a> LoopFormOps for LoopBuilder<'a>`: + - `self.parent_builder.current_function.as_ref().expect(...)` 経由で &MirFunction を返す。 + - `impl LoopFormOps for LoopFormJsonOps<'_>`: + - 既に `f: &mut MirFunction` を保持しているので、その参照を返す。 + - テスト用 `MockOps` など: + - 最小限の MirFunction スタブを持つ or `unimplemented!()` でテストを段階的に更新。 + +### Step 2: ExitPhiBuilder に MirQuery フックを追加 + +- 目的: Exit PHI 生成時に MirQuery にアクセスできるようにする。 +- 具体案: + - `ExitPhiBuilder::build_exit_phis` のシグネチャを拡張: + - 現状: `fn build_exit_phis(..., exit_snapshots, pinned, carrier) -> Result<(), String>` + - 追加: `mir_query: &dyn MirQuery` を引数に足す、または内部で `ops.mir_function()` から `MirQueryBox` を組み立てる。 + - 検討: + - a) `build_exit_phis` に `&dyn MirQuery` を直渡し → テストで差し替えやすい。 + - b) `LoopFormOps` に `mir_function()` を足し、`ExitPhiBuilder` 側で `MirQueryBox::new(ops.mir_function())` を作る。 + - このフェーズでは b) 案を優先(既に LoopBuilder/JSON bridge は MirFunction を握っているため)。 + +### Step 3: MirScanExitLiveness の本実装(use/def スキャン) + +- 前提: Step 2 で `MirQueryBox` を構築できる。 +- 実装方針: + - 入口: + - `MirScanExitLiveness::compute_live_at_exit(header_vals, exit_snapshots)` の中で + - `MirQueryBox` と `exit_blocks` を受け取れるようにする(必要なら `ExitLivenessProvider` トレイトのシグネチャ拡張も検討)。 + - 最小アルゴリズム: + 1. スキャン対象ブロック集合: + - exit ブロック(LoopShape.exit)+ exit_snapshots に現れる break 元ブロック。 + 2. 初期 live 集合: + - 対象ブロックの命令を後ろから走査し、`reads_of(inst)` を live に追加。 + 3. 1-step backward 伝播: + - それぞれのブロックで `writes_of(inst)` で kill、`reads_of(inst)` で add。 + - `succs(bb)` が対象集合に含まれている場合、succ の live を bb に流す。 + 4. 固定点反復: + - live 集合が変わらなくなるまで 2〜3 を繰り返す。 + 5. 名前へのマッピング: + - header_vals / exit_snapshots に現れる `(name, ValueId)` を逆引きテーブルに集約し、 + live に含まれる ValueId に対応する name だけを `live_at_exit` に含める。 + - 返り値: + - `BTreeSet`(BodyLocalPhiBuilder からそのまま使える)。 + - 既存ロジックとの整合: + - BodyLocalInternal でも「exit後で本当に live」なものだけが rescue されることが期待される。 + +### Step 4: ExitPhiBuilder / BodyLocalPhiBuilder 統合確認 + +- 目的: 新しい live_at_exit を使っても PhiInvariantsBox で落ちないことを確認する。 +- 作業: + - `BodyLocalPhiBuilder::filter_exit_phi_candidates` が + - `class.needs_exit_phi()` (Pinned/Carrier/BodyLocalExit) と + - `live_at_exit` + `is_available_in_all` の OR を行っていることを再確認。 + - `PhiInvariantsBox::ensure_exit_phi_availability` が + - 「選ばれた変数はすべての exit pred で定義済み」であることを保証し、 + - それでも穴があれば即 Fail-Fast で教えてくれることを前提に、MirScan 側のロジックを調整。 + +### Step 5: カナリア検証 + +- コマンド: + - `NYASH_EXIT_LIVE_ENABLE=1 cargo test --release --lib mir_funcscanner_skip_ws_direct_vm` + - `NYASH_EXIT_LIVE_ENABLE=1 cargo test --release --lib mir_funcscanner_parse_params_trim_min_verify_and_vm` +- 期待: + - MirVerifier が `use of undefined value` を報告しない。 + - VM 実行で RC が期待通り(skip_ws の sentinel が正しく動き、trim/parse_params も undefined を出さない)。 + +### Step 6: ドキュメント更新 + +- `loopform_ssot.md` に: + - MirQuery / MirQueryBox の役割と ExitLiveness との関係。 + - 「MirScanExitLiveness は 26-G で use/def スキャン実装済み」と記載。 +- `phase-26-G/README.md` 自体も「実装完了」セクションを追記。 + +## やらないこと + +- Loop 形状や PHI 生成ロジックの意味変更(ExitPhiBuilder/BodyLocalPhiBuilder のアルゴリズム変更はしない)。 +- env 名の変更や追加(既存の `NYASH_EXIT_LIVE_ENABLE` を継続利用)。 + +## 受け入れ条件 + +- `NYASH_EXIT_LIVE_ENABLE=0/未設定` で従来のテスト結果を維持。 +- `NYASH_EXIT_LIVE_ENABLE=1` で FuncScanner カナリアが緑(MirVerifier/VM)。 +- docs 更新: 26-F/loopform_ssot に「MIR スキャン実装済み」を追記。 diff --git a/docs/development/roadmap/phases/phase-26-H/README.md b/docs/development/roadmap/phases/phase-26-H/README.md new file mode 100644 index 00000000..6dd626e1 --- /dev/null +++ b/docs/development/roadmap/phases/phase-26-H/README.md @@ -0,0 +1,217 @@ +# Phase 26-H — JoinIR / 関数正規化フェーズ設計図 + +目的: これまで「構文 → LoopForm → PHI」で説明してきた制御構造を、もう一段抽象度を上げて「関数呼び出し+継続」に正規化する中間層(JoinIR / LoopFnIR)として整理し直すこと。 +最終的には「ループや if の合流点で悩む」のではなく、「関数の引数と戻り先で意味が決まる」世界に寄せ、箱の数と責務を減らしていく。 + +このフェーズ 26‑H ではあくまで「設計とミニ実験」に留め、スモークや本線は既存の MIR/LoopForm ルートのまま維持する。 + +--- + +## 1. 現状: LoopForm 正規化ベースの世界 + +現在のパイプライン(概略): + +```text +AST → MIR / LoopForm v2 → VM / LLVM +``` + +LoopForm v2 / PHI 周辺には、だいたい次のような箱が存在している: + +- 構造系 + - `LoopFormBuilder` / `LoopFormOps` + - `ControlForm`(If/Loop の形と preds) +- PHI 生成系 + - `HeaderPhiBuilder` + - `ExitPhiBuilder` + - `BodyLocalPhiBuilder` + - `IfBodyLocalMergeBox` + - `PhiBuilderBox`(If φ 統合) + - `PhiInvariantsBox`(Fail-Fast チェック) +- 解析/分類系 + - `LoopVarClassBox`(Pinned / Carrier / BodyLocal*) + - `LoopExitLivenessBox`(ExitLiveness、実装は段階的) + - `LocalScopeInspectorBox` + - if 解析系(`if_phi.rs` の補助群) + +これらの箱が「どの変数がループをまたぐか」「どこで φ が必要か」「Exit で何を Live とみなすか」を決めているが、その分、箱の数と責務が多く、ループの形を変えるたびに PHI 側の負担が増えている。 + +--- + +## 2. 代案: 「関数を呼ぶ回数=ループ」というモデル + +発想の転換: + +- 今: 構文を LoopForm に正規化し、ループ構造(header/body/latch/exit)を中心に世界を説明している。 +- 代案: 構文を「関数呼び出し」に正規化し、**関数を繰り返し呼ぶこと自体がループ**というモデルに寄せる。 + +### 2.1 ループの例 + +元のコード(擬似 Nyash): + +```hako +var x = 0 +loop { + x = x + 1 + if x >= 10 { break } +} +print(x) +``` + +関数ループモデルで見ると: + +```hako +// ループ一歩ぶんの関数(Box) +step(x, k_exit) { + if x >= 10 { + k_exit(x) // ループ終了して「先」に進む + } else { + step(x + 1, k_exit) // もう一周 + } +} + +// ループの「先」の処理 +k_exit = (v) => { + print(v) +} + +// 実行開始 +step(0, k_exit) +``` + +- ループ = `step` を何回も呼ぶこと +- `break` = `k_exit(...)` を呼ぶこと +- `continue` = `step(...)` を呼ぶこと +- φ / LoopCarried 変数 = `step` の引数 + +ここでは「ループヘッダの φ で悩む」のではなく、「step の引数・k_exit の引数をどう定義するか」に責務が集中する。 + +### 2.2 パイプラインの再構成案 + +現在: + +```text +AST → MIR / LoopForm v2 → VM/LLVM +``` + +ここに 1 段挟む: + +```text +AST → MIR / LoopForm v2 → ★LoopFnIR(関数ループ層) → VM/LLVM +``` + +この LoopFnIR/JoinIR 層で: + +- 各 LoopForm について「ループ関数(step) + 継続関数(k_exit)」を合成。 +- ループの PHI / carrier / exit φ はすべて `step` / `k_exit` の引数として表現。 +- 下流(VM / LLVM)は「関数呼び出し(および再帰のループ化や展開)」だけを見ればよい。 + +結果として: + +- LoopForm v2 は「LoopFnIR を作る前段」に役割縮小。 +- BodyLocal / Exit φ の詳細設計は「引数に何を持っていくか?」という関数インターフェース設計に吸収される。 + +--- + +## 4. このフェーズで実装する箱 / 概念ラベル + +- 実装として増やす(26-H 内で手を動かすもの) + - `join_ir.rs`: JoinIR 型(関数/ブロック/命令)+ダンプ + - LoopForm→JoinIR のミニ変換(1 ケース限定で OK) + - 実験トグル(例: `NYASH_JOINIR_EXPERIMENT=1`)で JoinIR をダンプするフック + +- 概念ラベル(27.x 以降に検討) + - MirQuery のようなビュー層(reads/writes/succs を trait 化) + - LoopFnLoweringBox / JoinIRBox の分割や最適化パス + - VM/LLVM への統合 + +※ このフェーズでは「設計+ミニ実験のみ」で、本線スモークは既存 MIR/LoopForm 経路を維持する。 + +--- + +## 3. 箱の数と最終形のイメージ + +### 3.1 現在の PHI/Loop 周辺の箱(概略) + +ざっくりカテゴリ分けすると: + +- 構造: + - `LoopFormBuilder` + - `ControlForm` +- PHI 生成: + - `HeaderPhiBuilder` + - `ExitPhiBuilder` + - `BodyLocalPhiBuilder` + - `IfBodyLocalMergeBox` + - `PhiBuilderBox` + - `PhiInvariantsBox` +- 解析: + - `LoopVarClassBox` + - `LoopExitLivenessBox` + - `LocalScopeInspectorBox` + - if 解析系(IfAnalysisBox 的なもの) + +関数正規化前提で進むと、最終的には: + +- PHI を直接扱う箱は「LoopForm→LoopFnIR に変換する前段」に閉じ込める。 +- LoopFnIR 導入後の本線では、次のような少数の箱が中心になる: + - `LoopFnLoweringBox`(LoopForm → LoopFnIR / JoinIR) + - `JoinIRBox`(JoinIR の保持・最適化) + - 既存の VM/LLVM バックエンド(JoinIR からのコード生成側) + +という構造に寄せられる見込み。 + +このフェーズ 26‑H では、「最終的にそこに寄せるための設計図」を書くところまでを目標とする。 + +--- + +## 4. 26-H でやること(スコープ) + +- JoinIR / LoopFnIR の設計ドキュメント作成 + - 命令セット(call / ret / jump / 継続)の最小定義。 + - if / loop / break / continue / return を JoinIR に落とす書き換え規則。 + - φ = 関数引数、merge = join 関数、loop = 再帰関数+exit 継続、という対応表。 +- 最小 1 ケースの手書き変換実験(MIR → JoinIR) + - ループ+break を含む簡単な関数を 1 例だけ JoinIR に落とし、形を確認。 +- MirQueryBox 経由で必要な MIR ビュー API の確認 + - reads/writes/succs など、JoinIR 変換に必要な情報がすでに `MirQuery` で取れるかチェック。 +- すべてトグル OFF で行い、本線(MIR/LoopForm ルート)のスモークには影響させない。 + +--- + +## 5. やらないこと(26-H では保留) + +- 既存ルート(MIR/LoopForm/VM/LLVM)を JoinIR で置き換える。 +- スモーク / CI のデフォルト経路変更。 +- Loop/PHI 既存実装の削除(これは 27.x 以降の段階で検討)。 + +--- + +## 6. 実験計画(段階) + +1. **設計シート** + - `docs/development/architecture/join-ir.md` に命令セット・変換規則・対応表を記述(φ=引数, merge=join, loop=再帰)。 + +2. **ミニ変換実験 1 ケース** + - 最小ループ(例: `loop(i < 3) { if i >= 2 { break } i = i + 1 } return i`)を MIR → JoinIR へ手書き変換し、テストでダンプを確認。VM/LLVM 実行までは行わない。 + +3. **トランスレータ骨格** + - `src/mir/join_ir.rs` などに型定義だけ追加(未配線、トグル OFF)。MirQueryBox(reads/writes/succs)で必要なビューが揃っているか確認。 + +4. **トグル付き実験** + - `NYASH_JOINIR_EXPERIMENT=1` などのトグルで最小ケースを JoinIR 変換・ダンプするルートを作る(デフォルト OFF でスモーク影響なし)。 + +--- + +## 7. 受け入れ基準(このフェーズ) + +- docs に JoinIR / LoopFnIR の設計と変換規則が明記されている。 +- 最小 1 ケースの JoinIR 変換がテストでダンプできる(join/step/k_exit の形になっている)。 +- 本線スモーク(既存 MIR ルート)は影響なし(トグル OFF)。 + +--- + +## 8. 次フェーズへの橋渡し + +- 変換器を拡張して FuncScanner / Stage‑B などカナリアを JoinIR で通す(トグル付き)。 +- ExitLiveness や BodyLocal PHI の一部を LoopFnIR 側に吸収し、PHI/Loop 周辺の箱を徐々に減らす。 +- VM/LLVM 実行経路に JoinIR を統合するのは 27.x 以降を想定し、当面は「設計+ミニ実験」に留める。 diff --git a/docs/development/roadmap/phases/phase-26-H/TASKS.md b/docs/development/roadmap/phases/phase-26-H/TASKS.md new file mode 100644 index 00000000..67f75657 --- /dev/null +++ b/docs/development/roadmap/phases/phase-26-H/TASKS.md @@ -0,0 +1,69 @@ +# Phase 26-H — JoinIR / 関数正規化 タスクチェックリスト + +このフェーズは「設計+ミニ実験」専用だよ。本線のスモーク/CI は既存の MIR/LoopForm 経路のまま維持しつつ、関数正規化(JoinIR/LoopFnIR)の足場を小さく固めることをゴールにする。 + +## A. 設計ドキュメントまわり + +- [ ] A-1: join-ir 設計の骨格を固める + - [ ] `docs/development/architecture/join-ir.md` に: + - [ ] 命令セット(`call`, `ret`, 必要なら `jump`)の最小定義を明記する + - [ ] if / loop / break / continue / return → JoinIR の変換規則を列挙する + - [ ] φ = 関数引数 / merge = join 関数 / loop = 再帰 + exit 継続 の対応表を書く + - [ ] 26-H で「実装として本当に増やす箱」と、「概念ラベルとしてだけ残す箱」を分けてコメントしておく + +- [ ] A-2: 26-H フェーズ README の整備 + - [ ] `docs/development/roadmap/phases/phase-26-H/README.md` に: + - [ ] 現在の箱群(LoopForm/PHI/解析系)の棚卸し + - [ ] 最終的に残したい箱セット(LoopForm 前段 + LoopFnIR/JoinIR + 最小解析箱)を 1 セクションでまとめる + - [ ] 「このフェーズでは設計+ミニ実験のみ、本線は既存ルート」というスコープ/制限を明記する + +## B. JoinIR 型・変換の最小実装 + +- [ ] B-1: JoinIR 型定義ファイルの追加 + - [ ] `src/mir/join_ir.rs` を追加し、以下を定義する(最小限でOK): + - [ ] `JoinFunction`(名前・引数・ブロック一覧) + - [ ] `JoinBlock`(ラベルと命令列) + - [ ] `JoinInst`(call/ret などのバリアント) + - [ ] `Debug`/`Display` などダンプに必要な実装だけ付ける(最初は `Debug` だけでも可) + +- [ ] B-2: LoopForm→JoinIR ミニ変換関数(1ケース用) + - [ ] まずは **汎用変換ではなく、特定の小関数だけを対象にした試験的関数** を 1つ実装する: + - 例: `fn lower_simple_loop_to_joinir(mir: &MirModule, func_name: &str) -> Option` + - [ ] ルールは join-ir.md の「Loop→step/k_exit」変換に従って手作業気味でよい(このフェーズでは general solution を目指さない) + +## C. 最小 .hako + テスト(カナリア) + +- [ ] C-1: 最小 JoinIR 用 .hako を追加 + - [ ] `apps/tests/joinir_min_loop.hako`(例)を作成: + - 例: `static box JoinIrMin { main() { var i = 0; loop(i < 3) { if i >= 2 { break } i = i + 1 } return i } }` + +- [ ] C-2: Rust テストで MIR→JoinIR ダンプを確認 + - [ ] `src/tests/mir_joinir_min.rs`(仮ファイル)を追加し: + - [ ] 上記 .hako を AST→MIR までコンパイル + - [ ] `lower_simple_loop_to_joinir` を呼んで `JoinFunction` を生成 + - [ ] `eprintln!("{:?}", join_fn)` などで形を確認(当面は assert よりも形の手動確認を重視) + - [ ] テストは **デフォルト ON でも軽い** ことを確認(重ければ `#[ignore]` + 手動実行でもOK) + +## D. トグル付き実験フック + +- [ ] D-1: 実験用トグルの追加 + - [ ] `NYASH_JOINIR_EXPERIMENT=1` のときだけ JoinIR ダンプを有効にするパスを Runner 側に追加する(例: `--debug-joinir-min` に相乗りでも可) + - [ ] トグル OFF のときは既存の MIR/LoopForm 経路しか走らないことを確認する + +## E. ループ/PHI ラインとの関係メモ + +- [ ] E-1: 「どの箱が将来 JoinIR に吸収されるか」を一覧にする + - [ ] `loopform_ssot.md` か 26-H README に: + - [ ] Header/Exit PHI まわりの箱で、JoinIR 導入後に **不要or縮退** するもの + - [ ] LoopForm / ControlForm は「前段の構造箱」として残すもの + - [ ] 解析系(LoopVarClassBox / LoopExitLivenessBox など)のうち、JoinIR 後も必要なもの + を簡易な表にして残す + +- [ ] E-2: 「今はここまで、27.x でここまで」を線引きする + - [ ] 26-H のスコープ: 設計+最小 JoinIR ダンプ + - [ ] 27.x 候補: JoinIR 経由で 1〜2 個の本番ループ(FuncScanner/Stage-B)を通す、PHI/ExitLiveness の一部を JoinIR 側に移す + +--- + +このファイルは「26‑H でやることを一覧で見たいときのチェックリスト」として使ってね。 +タスクが増えたら、このファイルに A/B/C… の形で足していく想定だよ。 diff --git a/docs/development/runtime/cli-hakorune-stage1.md b/docs/development/runtime/cli-hakorune-stage1.md index 65d8d7f1..b8ed1ea8 100644 --- a/docs/development/runtime/cli-hakorune-stage1.md +++ b/docs/development/runtime/cli-hakorune-stage1.md @@ -1,6 +1,8 @@ # Stage1 Hakorune CLI Design(Proposal) -Status: design-only + Stage0 stub 実装済み(Phase 25.1 時点では仕様策定と導線の整理まで。selfhost EXE 本体は未実装) +Status: design-only + Stage0 stub 実装済み(Phase 25.1 時点では仕様策定と導線の整理まで。selfhost EXE 本体は未実装)。 +Phase 25.1 A-3: `.hako` 側 Stage1Cli skeleton に env-only 実処理を実装(emit program-json / emit mir-json / run stub)。 +ブリッジ(Stage0 → `.hako` stub)は `NYASH_USE_STAGE1_CLI=1` / `STAGE1_EMIT_PROGRAM_JSON=1` 等の ENV で制御する。 ## ゴール @@ -55,7 +57,8 @@ hakorune [] [options] [-- script_args...] | `emit mir-json` | Program(JSON) → MIR(JSON) を出力 | 実装済み(Stage0 ブリッジ + `.hako` Stage1Cli 完了) | | `check` | 将来の構文/型/using チェック(予約) | プレースホルダ(`[hakorune] check: not implemented yet`) | -Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3 系列のみが実働コード** であり、`run` / `check` はメッセージを返して終了するプレースホルダのまま運用する。CLI の出口コード(90〜93)やログ形式は docs と実装を同期済み。 +Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3 系列のみが実働コード** であり、`run` / `check` はメッセージを返して終了するプレースホルダのまま運用する。CLI の出口コード(90〜93)やログ形式は docs と実装を同期済み。 +Phase 25.1 A-3 時点の stub 実装(`.hako` 側 Stage1Cli)は env-only 仕様で、成功時 0 / 入力不足 96 / 無効モード 97 / 実行失敗 98 を返す。 ### 実装ステータス(Phase 25.1a+) diff --git a/lang/src/compiler/entry/func_scanner.hako b/lang/src/compiler/entry/func_scanner.hako index f9371e81..e5837165 100644 --- a/lang/src/compiler/entry/func_scanner.hako +++ b/lang/src/compiler/entry/func_scanner.hako @@ -307,24 +307,19 @@ static box FuncScannerBox { // Helper: 空白文字(space/tab/newline/CR)を idx 位置からスキップ // 戻り値: スキップ後の位置(空白でない文字の位置、または文字列末尾) method skip_whitespace(s, idx) { - print("🔥🔥🔥 SENTINEL_SKIP_WS_CALLED!!! 🔥🔥🔥") - print("[skip_ws] START idx=" + ("" + idx) + " s.length()=" + ("" + s.length())) + // Minimal, SSA-friendly whitespace skipper. + __mir__.log("skip_ws/head", idx, s.length()) local i = idx local n = s.length() - print("[skip_ws] i=" + ("" + i) + " n=" + ("" + n)) - __mir__.log("skip_ws/head", i, n) - // WORKAROUND: Changed from loop(i < n) to loop with internal if check - // Original: loop(i < n) { ... } was not executing body even when condition was true! - loop(1 == 1) { - __mir__.log("skip_ws/loop", i, n) - print("[skip_ws] LOOP-TOP i=" + ("" + i)) - if i >= n { break } + loop(i < n) { local ch = s.substring(i, i + 1) - print("[skip_ws] LOOP i=" + ("" + i) + " ch='" + ch + "'") - if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { i = i + 1 } else { break } + if ch == " " { i = i + 1 continue } + if ch == "\t" { i = i + 1 continue } + if ch == "\n" { i = i + 1 continue } + if ch == "\r" { i = i + 1 continue } + break } __mir__.log("skip_ws/exit", i, n) - print("[skip_ws] RETURN i=" + ("" + i)) return i } @@ -421,32 +416,30 @@ static box FuncScannerBox { // FuncScannerBox._trim を使用(static helper パターン) // 戻り値: ArrayBox(トリム済みパラメータ名のリスト) method parse_params(params_str) { + // NOTE: keep the control flow simple to reduce SSA/PHI complexity. + // skip_whitespace/trim are already well‑tested helpers, so we reuse them here. + if params_str == null { return new ArrayBox() } local params = new ArrayBox() local pstr = "" + params_str - local pn = pstr.length() - local pstart = 0 + local n = pstr.length() + local pos = 0 - loop(pstart < pn) { - // Skip whitespace - loop(pstart < pn) { - local ch = pstr.substring(pstart, pstart + 1) - if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { pstart = pstart + 1 } else { break } - } - if pstart >= pn { break } + loop(pos < n) { + pos = FuncScannerBox.skip_whitespace(pstr, pos) + if pos >= n { break } - // Find next comma or end - local pend = pstart - loop(pend < pn) { - local ch = pstr.substring(pend, pend + 1) - if ch == "," { break } - pend = pend + 1 + // Find next comma (or end of string). + local next = pos + loop(next < n) { + if pstr.substring(next, next + 1) == "," { break } + next = next + 1 } - // Extract param name (trim) - local pname = pstr.substring(pstart, pend) - pname = FuncScannerBox.trim(pname) + // Trim and collect the parameter name. + local pname = FuncScannerBox.trim(pstr.substring(pos, next)) if pname.length() > 0 { params.push(pname) } - pstart = pend + 1 + + pos = next + 1 } return params @@ -505,16 +498,18 @@ static box FuncScannerBox { local str = "" + s local n = str.length() __mir__.log("trim/pre", n) - local b = 0 - loop(b < n) { - local ch = str.substring(b, b + 1) - if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { b = b + 1 } else { break } - } + + // Leading whitespace removal is delegated to skip_whitespace to keep SSA simple. + local b = FuncScannerBox.skip_whitespace(str, 0) + if b >= n { return "" } + + // Trailing whitespace: walk backwards until a non-space is found. local e = n loop(e > b) { local ch = str.substring(e - 1, e) if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { e = e - 1 } else { break } } + __mir__.log("trim/exit", b, e) if e > b { return str.substring(b, e) } return "" diff --git a/lang/src/compiler/tests/funcscanner_trim_min.hako b/lang/src/compiler/tests/funcscanner_trim_min.hako new file mode 100644 index 00000000..670e6d59 --- /dev/null +++ b/lang/src/compiler/tests/funcscanner_trim_min.hako @@ -0,0 +1,30 @@ +// funcscanner_trim_min.hako +// Minimal reproduction candidate for FuncScannerBox._trim/1 SSA/PHI issues +// +// Purpose: +// - Call FuncScannerBox._trim/1 directly from a tiny Main.main. +// - Keep control‑flow as simple as possible(1回呼び出し+return)。 +// - If the SSA/PHI bug is intrinsic to _trim/1(static helper alias)、 +// this file単体+func_scanner.hako だけで再現できるはず。 + +using lang.compiler.entry.func_scanner as FuncScannerBox + +static box Main { + main(args) { + // 短いテキストを 1 回だけ _trim に通す。 + local src = " abc " + print("[trim/min] input='" + src + "'") + + // static helper 経由(こちらが今回のSSOT対象)。 + local out1 = FuncScannerBox._trim(src) + print("[trim/min] _trim='" + out1 + "'") + + // ついでに直接 trim も呼んでおく(挙動比較用)。 + local out2 = FuncScannerBox.trim(src) + print("[trim/min] trim ='" + out2 + "'") + + // いまは SSA/PHI 崩れの再現が目的なので、戻り値は固定 0。 + return 0 + } +} + diff --git a/lang/src/runner/stage1_cli.hako b/lang/src/runner/stage1_cli.hako index dd405615..c62d4d3a 100644 --- a/lang/src/runner/stage1_cli.hako +++ b/lang/src/runner/stage1_cli.hako @@ -482,4 +482,12 @@ static box Stage1Cli { } } -static box Stage1CliMain { main(args) { return Stage1Cli.stage1_main(args) } } +static box Stage1CliMain { + // Entry point used by the Rust bridge. Keep arity 0 to avoid undefined args. + // Stage1Cli.stage1_main は env-only 仕様なのでここで空配列を渡す。 + main() { + local cli = new Stage1Cli() + local empty = new ArrayBox() + return cli.stage1_main(empty) + } +} diff --git a/src/mir/builder/calls/unified_emitter.rs b/src/mir/builder/calls/unified_emitter.rs index 0e498fd8..0973174e 100644 --- a/src/mir/builder/calls/unified_emitter.rs +++ b/src/mir/builder/calls/unified_emitter.rs @@ -278,6 +278,50 @@ impl UnifiedCallEmitterBox { ); resolver.validate_args(&callee, &args)?; + // Dev trace: resolved callee (static vs instance) and receiver origin + if std::env::var("NYASH_CALL_RESOLVE_TRACE").ok().as_deref() == Some("1") { + use crate::mir::definitions::call_unified::Callee; + match &callee { + Callee::Method { + box_name, + method, + receiver, + .. + } => { + // Try to retrieve origin info for receiver + let recv_meta = receiver.and_then(|r| { + builder + .value_origin_newbox + .get(&r) + .cloned() + .map(|cls| (r, cls)) + }); + eprintln!( + "[call-resolve] Method box='{}' method='{}' recv={:?} recv_origin={:?} args={:?}", + box_name, method, receiver, recv_meta, args + ); + } + Callee::Global(name) => { + eprintln!("[call-resolve] Global name='{}' args={:?}", name, args); + } + Callee::Constructor { box_type, .. } => { + eprintln!( + "[call-resolve] Constructor box='{}' args={:?}", + box_type, args + ); + } + Callee::Closure { .. } => { + eprintln!("[call-resolve] Closure args={:?}", args); + } + Callee::Value(v) => { + eprintln!("[call-resolve] Value callee=%{:?} args={:?}", v.0, args); + } + Callee::Extern(name) => { + eprintln!("[call-resolve] Extern name='{}' args={:?}", name, args); + } + } + } + // Stability guard: decide route via RouterPolicyBox (behavior-preserving rules) if let Callee::Method { box_name, diff --git a/src/mir/join_ir.rs b/src/mir/join_ir.rs new file mode 100644 index 00000000..47cfb712 --- /dev/null +++ b/src/mir/join_ir.rs @@ -0,0 +1,219 @@ +//! JoinIR — 関数正規化 IR(Phase 26-H) +//! +//! 目的: Hakorune の制御構造を **関数呼び出し+継続だけに正規化** する IR 層。 +//! - φ ノード = 関数の引数 +//! - merge ブロック = join 関数 +//! - ループ = 再帰関数(loop_step)+ exit 継続(k_exit) +//! - break / continue = 適切な関数呼び出し +//! +//! 位置づけ: +//! ```text +//! AST → MIR(+LoopForm v2) → JoinIR → VM / LLVM +//! ``` +//! +//! Phase 26-H スコープ: +//! - 型定義のみ(変換ロジックは次フェーズ) +//! - 最小限の命令セット +//! - Debug 出力で妥当性確認 + +use std::collections::BTreeMap; + +use crate::mir::{BasicBlockId, ValueId}; + +/// JoinIR 関数ID(MIR 関数とは別 ID でもよい) +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JoinFuncId(pub u32); + +impl JoinFuncId { + pub fn new(id: u32) -> Self { + JoinFuncId(id) + } +} + +/// 継続(join / ループ step / exit continuation)を識別するID +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JoinContId(pub u32); + +impl JoinContId { + pub fn new(id: u32) -> Self { + JoinContId(id) + } +} + +/// 変数ID(Phase 26-H では MIR の ValueId を再利用) +pub type VarId = ValueId; + +/// JoinIR 関数 +#[derive(Debug, Clone)] +pub struct JoinFunction { + /// 関数ID + pub id: JoinFuncId, + + /// 関数名(デバッグ用) + pub name: String, + + /// 引数(φ に相当) + pub params: Vec, + + /// 命令列(現在は直列、将来的にはブロック構造も可) + pub body: Vec, + + /// 呼び出し元に返す継続(ルートは None) + pub exit_cont: Option, +} + +impl JoinFunction { + pub fn new(id: JoinFuncId, name: String, params: Vec) -> Self { + Self { + id, + name, + params, + body: Vec::new(), + exit_cont: None, + } + } +} + +/// JoinIR 命令セット(最小版) +#[derive(Debug, Clone)] +pub enum JoinInst { + /// 通常の関数呼び出し: f(args..., k_next) + Call { + func: JoinFuncId, + args: Vec, + k_next: Option, + }, + + /// 継続呼び出し(join / exit 継続など) + Jump { + cont: JoinContId, + args: Vec, + }, + + /// ルート関数 or 上位への戻り + Ret { + value: Option, + }, + + /// それ以外の演算は、現行 MIR の算術/比較/boxcall を再利用 + Compute(MirLikeInst), +} + +/// MIR からの算術・比較命令のラッパー(Phase 26-H では最小限) +#[derive(Debug, Clone)] +pub enum MirLikeInst { + /// 定数代入 + Const { + dst: VarId, + value: ConstValue, + }, + + /// 二項演算 + BinOp { + dst: VarId, + op: BinOpKind, + lhs: VarId, + rhs: VarId, + }, + + /// 比較演算 + Compare { + dst: VarId, + op: CompareOp, + lhs: VarId, + rhs: VarId, + }, + + /// Box呼び出し(将来的には統一 Call に統合予定) + BoxCall { + dst: Option, + box_name: String, + method: String, + args: Vec, + }, +} + +/// 定数値(MIR の ConstValue を簡略化) +#[derive(Debug, Clone)] +pub enum ConstValue { + Integer(i64), + Bool(bool), + String(String), + Null, +} + +/// 二項演算種別 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinOpKind { + Add, + Sub, + Mul, + Div, +} + +/// 比較演算種別 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompareOp { + Lt, + Le, + Gt, + Ge, + Eq, + Ne, +} + +/// JoinIR モジュール(複数の関数を保持) +#[derive(Debug, Clone)] +pub struct JoinModule { + /// 関数マップ + pub functions: BTreeMap, + + /// エントリーポイント関数ID + pub entry: Option, +} + +impl JoinModule { + pub fn new() -> Self { + Self { + functions: BTreeMap::new(), + entry: None, + } + } + + pub fn add_function(&mut self, func: JoinFunction) { + self.functions.insert(func.id, func); + } +} + +impl Default for JoinModule { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_join_function_creation() { + let func_id = JoinFuncId::new(0); + let func = JoinFunction::new(func_id, "test_func".to_string(), vec![ValueId(1), ValueId(2)]); + + assert_eq!(func.id, func_id); + assert_eq!(func.name, "test_func"); + assert_eq!(func.params.len(), 2); + assert_eq!(func.body.len(), 0); + assert_eq!(func.exit_cont, None); + } + + #[test] + fn test_join_module() { + let mut module = JoinModule::new(); + let func = JoinFunction::new(JoinFuncId::new(0), "main".to_string(), vec![]); + module.add_function(func); + + assert_eq!(module.functions.len(), 1); + assert!(module.functions.contains_key(&JoinFuncId::new(0))); + } +} diff --git a/src/mir/loop_builder.rs b/src/mir/loop_builder.rs index 6d5ef45b..5d60aa8a 100644 --- a/src/mir/loop_builder.rs +++ b/src/mir/loop_builder.rs @@ -1413,6 +1413,13 @@ impl<'a> LoopFormOps for LoopBuilder<'a> { // Use the inherent method to avoid recursion LoopBuilder::get_variable_at_block(self, name, block) } + + fn mir_function(&self) -> &crate::mir::MirFunction { + self.parent_builder + .current_function + .as_ref() + .expect("LoopBuilder requires current_function") + } } // Phase 26-E-3: PhiBuilderOps 委譲実装(has-a設計) diff --git a/src/mir/mod.rs b/src/mir/mod.rs index d606a613..2ebd1f49 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -36,6 +36,8 @@ pub mod region; // Phase 25.1l: Region/GC観測レイヤ(LoopForm v2 × RefKin pub mod slot_registry; // Phase 9.79b.1: method slot resolution (IDs) pub mod value_id; pub mod value_kind; // Phase 26-A: ValueId型安全化 +pub mod query; // Phase 26-G: MIR read/write/CFGビュー (MirQuery) +pub mod join_ir; // Phase 26-H: 関数正規化IR(JoinIR) pub mod verification; pub mod verification_types; // extracted error types // Optimization subpasses (e.g., type_hints) // Phase 25.1f: Loop/If 共通ビュー(ControlForm) @@ -47,6 +49,7 @@ pub use effect::{Effect, EffectMask}; pub use function::{FunctionSignature, MirFunction, MirModule}; pub use instruction::MirInstruction; pub use optimizer::MirOptimizer; +pub use query::{MirQuery, MirQueryBox}; pub use printer::MirPrinter; pub use slot_registry::{BoxTypeId, MethodSlot}; pub use types::{ diff --git a/src/mir/phi_core/exit_phi_builder.rs b/src/mir/phi_core/exit_phi_builder.rs index 1e830f8d..35d1e6c5 100644 --- a/src/mir/phi_core/exit_phi_builder.rs +++ b/src/mir/phi_core/exit_phi_builder.rs @@ -16,7 +16,7 @@ use crate::mir::{BasicBlockId, ValueId}; use std::collections::{BTreeMap, BTreeSet}; use super::body_local_phi_builder::BodyLocalPhiBuilder; -use super::loop_exit_liveness::LoopExitLivenessBox; // Phase 26-F-4 +use super::loop_exit_liveness::{ExitLivenessProvider, LoopExitLivenessBox}; // Phase 26-F-4 use super::loop_snapshot_merge::LoopSnapshotMergeBox; use super::phi_invariants::PhiInvariantsBox; use super::phi_input_collector::PhiInputCollector; @@ -54,6 +54,8 @@ use super::phi_input_collector::PhiInputCollector; pub struct ExitPhiBuilder { /// Body-local variable builder body_local_builder: BodyLocalPhiBuilder, + /// Exit liveness provider (legacy by default, swappable for MIR scan) + liveness_provider: Box, } impl ExitPhiBuilder { @@ -73,7 +75,28 @@ impl ExitPhiBuilder { /// let exit_builder = ExitPhiBuilder::new(body_builder); /// ``` pub fn new(body_local_builder: BodyLocalPhiBuilder) -> Self { - Self { body_local_builder } + // 環境変数で簡易 MirScan 版を opt-in できるようにする + let use_scan = + std::env::var("NYASH_EXIT_LIVE_ENABLE").ok().as_deref() == Some("1"); + if use_scan { + Self::with_liveness( + body_local_builder, + Box::new(crate::mir::phi_core::loop_exit_liveness::MirScanExitLiveness), + ) + } else { + Self::with_liveness(body_local_builder, Box::new(LoopExitLivenessBox::new())) + } + } + + /// Create ExitPhiBuilder with a custom liveness provider (for tests / future MIR scan) + pub fn with_liveness( + body_local_builder: BodyLocalPhiBuilder, + liveness_provider: Box, + ) -> Self { + Self { + body_local_builder, + liveness_provider, + } } /// [LoopForm] Build Exit PHIs @@ -87,6 +110,7 @@ impl ExitPhiBuilder { /// * `exit_snapshots` - Exit predecessor snapshots (from break statements) /// * `pinned_vars` - Pinned variable names (loop-invariant parameters) /// * `carrier_vars` - Carrier variable names (loop-modified variables) + /// * `mir_func` - Underlying MIR function (for MirQuery) /// /// # Returns /// Result: Ok(()) on success, Err(msg) on failure @@ -159,9 +183,11 @@ impl ExitPhiBuilder { required_vars.extend(pinned_vars.iter().cloned()); required_vars.extend(carrier_vars.iter().cloned()); - // Phase 26-F-4: LoopExitLivenessBox で live_at_exit を計算 - let liveness_box = LoopExitLivenessBox::new(); - let live_at_exit = liveness_box.compute_live_at_exit(header_vals, exit_snapshots); + // Phase 26-F/G: ExitLivenessProvider で live_at_exit を計算(MirQuery 経由) + let query = crate::mir::MirQueryBox::new(ops.mir_function()); + let live_at_exit = self + .liveness_provider + .compute_live_at_exit(&query, exit_id, header_vals, exit_snapshots); let phi_vars = self.body_local_builder.filter_exit_phi_candidates( &required_vars.iter().cloned().collect::>(), @@ -182,6 +208,14 @@ impl ExitPhiBuilder { self.body_local_builder.inspector(), )?; + // Dev trace: which vars became exit-phi candidates + if std::env::var("NYASH_IF_HOLE_TRACE").ok().as_deref() == Some("1") { + eprintln!( + "[exit-phi] exit={:?} header={:?} phi_vars={:?} live_at_exit={:?}", + exit_id, header_id, phi_vars, live_at_exit + ); + } + let include_header_input = exit_preds_set.contains(&header_id) || exit_preds.is_empty(); // 5. PHI生成(PhiInputCollectorで最適化適用) @@ -200,6 +234,14 @@ impl ExitPhiBuilder { } } + // Dev trace: exit φ inputs + if std::env::var("NYASH_IF_HOLE_TRACE").ok().as_deref() == Some("1") { + eprintln!( + "[exit-phi] var='{}' preds={:?} header_included={} inputs={:?}", + var_name, exit_preds, include_header_input, inputs_map + ); + } + let mut inputs: Vec<(BasicBlockId, ValueId)> = inputs_map.into_iter().collect(); LoopSnapshotMergeBox::sanitize_inputs(&mut inputs); @@ -307,6 +349,49 @@ pub trait LoopFormOps { /// Update variable binding fn update_var(&mut self, var_name: String, value_id: ValueId); + + /// Access underlying MirFunction (for MirQuery) + fn mir_function(&self) -> &crate::mir::MirFunction; +} + +// Bridge: allow any LoopFormOps (loopform_builder版) to be used here +impl LoopFormOps for T { + fn set_current_block(&mut self, block_id: BasicBlockId) -> Result<(), String> { + crate::mir::phi_core::loopform_builder::LoopFormOps::set_current_block(self, block_id) + } + + fn get_block_predecessors(&self, block_id: BasicBlockId) -> BTreeSet { + crate::mir::phi_core::loopform_builder::LoopFormOps::get_block_predecessors( + self, + block_id, + ) + .into_iter() + .collect() + } + + fn block_exists(&self, block_id: BasicBlockId) -> bool { + crate::mir::phi_core::loopform_builder::LoopFormOps::block_exists(self, block_id) + } + + fn new_value(&mut self) -> ValueId { + crate::mir::phi_core::loopform_builder::LoopFormOps::new_value(self) + } + + fn emit_phi( + &mut self, + phi_id: ValueId, + inputs: Vec<(BasicBlockId, ValueId)>, + ) -> Result<(), String> { + crate::mir::phi_core::loopform_builder::LoopFormOps::emit_phi(self, phi_id, inputs) + } + + fn update_var(&mut self, var_name: String, value_id: ValueId) { + crate::mir::phi_core::loopform_builder::LoopFormOps::update_var(self, var_name, value_id) + } + + fn mir_function(&self) -> &crate::mir::MirFunction { + crate::mir::phi_core::loopform_builder::LoopFormOps::mir_function(self) + } } // ============================================================================ @@ -327,10 +412,19 @@ mod tests { next_value_id: u32, emitted_phis: Vec<(ValueId, Vec<(BasicBlockId, ValueId)>)>, var_bindings: BTreeMap, + func: crate::mir::MirFunction, } impl MockOps { fn new() -> Self { + // Minimal function for testing (1 block) + let sig = crate::mir::function::FunctionSignature { + name: "mock".to_string(), + params: vec![], + return_type: crate::mir::MirType::Void, + effects: crate::mir::effect::EffectMask::PURE, + }; + let func = crate::mir::MirFunction::new(sig, BasicBlockId(0)); Self { current_block: None, blocks: BTreeSet::new(), @@ -338,6 +432,7 @@ mod tests { next_value_id: 100, emitted_phis: Vec::new(), var_bindings: BTreeMap::new(), + func, } } @@ -388,6 +483,10 @@ mod tests { fn update_var(&mut self, var_name: String, value_id: ValueId) { self.var_bindings.insert(var_name, value_id); } + + fn mir_function(&self) -> &crate::mir::MirFunction { + &self.func + } } #[test] @@ -544,6 +643,7 @@ mod tests { let pinned_vars = vec!["s".to_string()]; let carrier_vars = vec![]; + let _func = ops.func.clone(); let result = exit_builder.build_exit_phis( &mut ops, exit_id, @@ -601,6 +701,7 @@ mod tests { let pinned_vars = vec!["s".to_string()]; let carrier_vars = vec!["idx".to_string()]; + let _func = ops.func.clone(); let result = exit_builder.build_exit_phis( &mut ops, exit_id, @@ -658,6 +759,7 @@ mod tests { let pinned_vars = vec!["s".to_string()]; let carrier_vars = vec!["idx".to_string()]; + let _func = ops.func.clone(); let result = exit_builder.build_exit_phis( &mut ops, exit_id, @@ -710,6 +812,7 @@ mod tests { let pinned_vars = vec!["x".to_string()]; let carrier_vars = vec![]; + let _func = ops.func.clone(); let result = exit_builder.build_exit_phis( &mut ops, exit_id, @@ -755,6 +858,7 @@ mod tests { let pinned_vars = vec!["x".to_string()]; let carrier_vars = vec![]; + let _func = ops.func.clone(); let result = exit_builder.build_exit_phis( &mut ops, exit_id, @@ -800,6 +904,7 @@ mod tests { let pinned_vars = vec!["x".to_string()]; let carrier_vars = vec![]; + let _func = ops.func.clone(); let result = exit_builder.build_exit_phis( &mut ops, exit_id, diff --git a/src/mir/phi_core/loop_exit_liveness.rs b/src/mir/phi_core/loop_exit_liveness.rs index 5e4b589b..ab38062a 100644 --- a/src/mir/phi_core/loop_exit_liveness.rs +++ b/src/mir/phi_core/loop_exit_liveness.rs @@ -27,7 +27,21 @@ use crate::mir::{BasicBlockId, ValueId}; use std::collections::{BTreeMap, BTreeSet}; -/// Loop Exit Liveness Box +/// ExitLivenessProvider +/// +/// ExitPhiBuilder が依存するインターフェース。環境や実装を差し替えやすくするために +/// trait として定義し、Legacy / MIR スキャン版のどちらでも差し込めるようにしている。 +pub trait ExitLivenessProvider: Send + Sync { + fn compute_live_at_exit( + &self, + mir_query: &dyn crate::mir::MirQuery, + exit_block: BasicBlockId, + header_vals: &BTreeMap, + exit_snapshots: &[(BasicBlockId, BTreeMap)], + ) -> BTreeSet; +} + +/// Loop Exit Liveness Box(Legacy/Phase 1) /// /// # Purpose /// Exit後で本当に使われる変数を決定する専門箱 @@ -96,6 +110,8 @@ impl LoopExitLivenessBox { /// // Phase 1: 保守的近似 /// // skip_whitespace: ch は一部 pred でしか定義されないが、exit 後で使われる /// let live_at_exit = liveness_box.compute_live_at_exit( + /// query, + /// exit_block, /// &header_vals, // { i: %10, n: %20 } /// &exit_snapshots, // [{ i: %30, ch: %40 }, { i: %50 }] /// ); @@ -103,6 +119,8 @@ impl LoopExitLivenessBox { /// ``` pub fn compute_live_at_exit( &self, + _mir_query: &dyn crate::mir::MirQuery, + _exit_block: BasicBlockId, _header_vals: &BTreeMap, _exit_snapshots: &[(BasicBlockId, BTreeMap)], ) -> BTreeSet { @@ -155,6 +173,117 @@ impl LoopExitLivenessBox { } } +impl ExitLivenessProvider for LoopExitLivenessBox { + fn compute_live_at_exit( + &self, + _mir_query: &dyn crate::mir::MirQuery, + _exit_block: BasicBlockId, + header_vals: &BTreeMap, + exit_snapshots: &[(BasicBlockId, BTreeMap)], + ) -> BTreeSet { + self.compute_live_at_exit(_mir_query, _exit_block, header_vals, exit_snapshots) + } +} + +/// MirScanExitLiveness - Phase 2+ 用のExitLivenessProviderプレースホルダ +/// +/// Phase 26-F 時点では LoopExitLivenessBox と同じ実装を呼び出すだけの薄い箱だよ。 +/// 将来、LoopFormOps や MirFunction へのアクセスが整備されたら、 +/// ここに本物の MIR スキャン実装(use/def ベースの live_at_exit 計算)を差し込む予定。 +#[derive(Debug, Clone, Default)] +pub struct MirScanExitLiveness; + +impl ExitLivenessProvider for MirScanExitLiveness { + fn compute_live_at_exit( + &self, + mir_query: &dyn crate::mir::MirQuery, + exit_block: BasicBlockId, + header_vals: &BTreeMap, + exit_snapshots: &[(BasicBlockId, BTreeMap)], + ) -> BTreeSet { + let trace = std::env::var("NYASH_EXIT_LIVENESS_TRACE") + .ok() + .as_deref() + == Some("1"); + + // 対象ブロック集合(exit と break preds) + let mut targets: BTreeSet = BTreeSet::new(); + targets.insert(exit_block); + for (bb, _) in exit_snapshots { + targets.insert(*bb); + } + + // live map per block + let mut live_map: BTreeMap> = BTreeMap::new(); + for bb in &targets { + live_map.insert(*bb, BTreeSet::new()); + } + + let mut changed = true; + while changed { + changed = false; + // 逆順で走査(安定順序: BTreeSet) + for bb in targets.iter().rev() { + let mut live: BTreeSet = BTreeSet::new(); + // succ の live を流入(対象集合内のみ) + for succ in mir_query.succs(*bb) { + if targets.contains(&succ) { + if let Some(succ_live) = live_map.get(&succ) { + live.extend(succ_live); + } + } + } + // 命令を逆順スキャン + for inst in mir_query.insts_in_block(*bb).iter().rev() { + // kill writes + for w in mir_query.writes_of(inst) { + live.remove(&w); + } + // add reads + for r in mir_query.reads_of(inst) { + live.insert(r); + } + } + // 更新チェック + let entry = live_map.entry(*bb).or_default(); + if *entry != live { + *entry = live; + changed = true; + } + } + } + + // ValueId→名前の逆引きテーブル(header + exit snapshots) + let mut name_pool: BTreeMap = BTreeMap::new(); + for (name, vid) in header_vals { + name_pool.insert(*vid, name.clone()); + } + for (_bb, snap) in exit_snapshots { + for (name, vid) in snap { + name_pool.insert(*vid, name.clone()); + } + } + + let mut live_names: BTreeSet = BTreeSet::new(); + for live in live_map.values() { + for v in live { + if let Some(name) = name_pool.get(v) { + live_names.insert(name.clone()); + } + } + } + + if trace { + eprintln!( + "[LoopExitLiveness/MirScan] live_at_exit={} vars (use/def scan)", + live_names.len() + ); + } + + live_names + } +} + // ============================================================================ // Unit Tests // ============================================================================ @@ -163,9 +292,26 @@ impl LoopExitLivenessBox { mod tests { use super::*; + struct EmptyQuery; + impl crate::mir::MirQuery for EmptyQuery { + fn insts_in_block(&self, _bb: BasicBlockId) -> &[crate::mir::MirInstruction] { + &[] + } + fn succs(&self, _bb: BasicBlockId) -> Vec { + Vec::new() + } + fn reads_of(&self, _inst: &crate::mir::MirInstruction) -> Vec { + Vec::new() + } + fn writes_of(&self, _inst: &crate::mir::MirInstruction) -> Vec { + Vec::new() + } + } + #[test] fn test_compute_live_at_exit_conservative() { let liveness_box = LoopExitLivenessBox::new(); + let query = EmptyQuery; let mut header_vals = BTreeMap::new(); header_vals.insert("i".to_string(), ValueId(10)); @@ -184,7 +330,8 @@ mod tests { (BasicBlockId(200), snap2), ]; - let live_at_exit = liveness_box.compute_live_at_exit(&header_vals, &exit_snapshots); + let live_at_exit = + liveness_box.compute_live_at_exit(&query, BasicBlockId(0), &header_vals, &exit_snapshots); // Phase 1: 空の live_at_exit(MIRスキャン実装待ち) assert_eq!(live_at_exit.len(), 0); @@ -193,11 +340,13 @@ mod tests { #[test] fn test_compute_live_at_exit_empty() { let liveness_box = LoopExitLivenessBox::new(); + let query = EmptyQuery; let header_vals = BTreeMap::new(); let exit_snapshots = vec![]; - let live_at_exit = liveness_box.compute_live_at_exit(&header_vals, &exit_snapshots); + let live_at_exit = + liveness_box.compute_live_at_exit(&query, BasicBlockId(0), &header_vals, &exit_snapshots); assert_eq!(live_at_exit.len(), 0); } @@ -205,6 +354,7 @@ mod tests { #[test] fn test_compute_live_at_exit_deduplication() { let liveness_box = LoopExitLivenessBox::new(); + let query = EmptyQuery; let mut header_vals = BTreeMap::new(); header_vals.insert("i".to_string(), ValueId(10)); @@ -220,7 +370,8 @@ mod tests { (BasicBlockId(200), snap2), ]; - let live_at_exit = liveness_box.compute_live_at_exit(&header_vals, &exit_snapshots); + let live_at_exit = + liveness_box.compute_live_at_exit(&query, BasicBlockId(0), &header_vals, &exit_snapshots); // Phase 1: 空の live_at_exit(MIRスキャン実装待ち) assert_eq!(live_at_exit.len(), 0); diff --git a/src/mir/phi_core/loopform_builder.rs b/src/mir/phi_core/loopform_builder.rs index 4b927159..08f23702 100644 --- a/src/mir/phi_core/loopform_builder.rs +++ b/src/mir/phi_core/loopform_builder.rs @@ -767,6 +767,9 @@ pub trait LoopFormOps { /// Get variable value at specific block fn get_variable_at_block(&self, name: &str, block: BasicBlockId) -> Option; + + /// Access underlying MirFunction (for liveness/MirQuery) + fn mir_function(&self) -> &crate::mir::MirFunction; } /// Phase 26-B-3: sanitize_phi_inputs() removed - replaced by PhiInputCollector @@ -845,13 +848,22 @@ mod tests { struct MockOps { next_value: u32, params: Vec, + func: crate::mir::MirFunction, } impl MockOps { fn new() -> Self { + let sig = crate::mir::function::FunctionSignature { + name: "mock".to_string(), + params: vec![], + return_type: crate::mir::MirType::Void, + effects: crate::mir::effect::EffectMask::PURE, + }; + let func = crate::mir::MirFunction::new(sig, BasicBlockId::new(0)); Self { next_value: 100, params: vec!["me".to_string(), "limit".to_string()], + func, } } } @@ -929,6 +941,10 @@ mod tests { fn get_variable_at_block(&self, _name: &str, _block: BasicBlockId) -> Option { None } + + fn mir_function(&self) -> &crate::mir::MirFunction { + &self.func + } } let mut ops = MockOps::new(); @@ -1009,13 +1025,22 @@ mod tests { struct MockSealOps { vars_at_block: BTreeMap<(BasicBlockId, String), ValueId>, phi_updates: Vec<(BasicBlockId, ValueId, Vec<(BasicBlockId, ValueId)>)>, + func: crate::mir::MirFunction, } impl MockSealOps { fn new() -> Self { + let sig = crate::mir::function::FunctionSignature { + name: "mock".to_string(), + params: vec![], + return_type: crate::mir::MirType::Void, + effects: crate::mir::effect::EffectMask::PURE, + }; + let func = crate::mir::MirFunction::new(sig, BasicBlockId::new(0)); Self { vars_at_block: BTreeMap::new(), phi_updates: Vec::new(), + func, } } } @@ -1081,6 +1106,10 @@ mod tests { fn get_variable_at_block(&self, name: &str, block: BasicBlockId) -> Option { self.vars_at_block.get(&(block, name.to_string())).copied() } + + fn mir_function(&self) -> &crate::mir::MirFunction { + &self.func + } } let mut ops = MockSealOps::new(); diff --git a/src/mir/phi_core/mod.rs b/src/mir/phi_core/mod.rs index 63d9a354..ac1c2bd9 100644 --- a/src/mir/phi_core/mod.rs +++ b/src/mir/phi_core/mod.rs @@ -42,6 +42,49 @@ pub mod phi_invariants; pub mod loop_exit_liveness; // Public surface for callers that want a stable path: -// Phase 1: No re-exports to avoid touching private builder internals. -// Callers should continue using existing paths. Future phases may expose -// stable wrappers here once migrated. +// Phase 26-F: 軽量ラッパだけ用意しておき、内部構造に触らずに +// 「どこから読めばよいか」の入口を固定する。 + +use crate::mir::control_form::ControlForm; +use crate::mir::{BasicBlockId, ValueId}; +use std::collections::BTreeMap; + +/// 統一 If PHI 入口(薄いラッパ) +/// +/// - 役割: PhiBuilderBox を内部で生成して If PHI を張るだけ。 +/// - 挙動: 既存の PhiBuilderBox::generate_phis と同一(Loop 形は未対応のまま)。 +pub fn build_if_phis( + ops: &mut O, + form: &ControlForm, + pre_snapshot: &BTreeMap, + post_snapshots: &[BTreeMap], +) -> Result<(), String> +where + O: crate::mir::phi_core::phi_builder_box::PhiBuilderOps, +{ + let mut box_ = crate::mir::phi_core::phi_builder_box::PhiBuilderBox::new(); + box_.generate_phis(ops, form, pre_snapshot, post_snapshots) +} + +/// 統一 Exit PHI 入口(ControlForm ラッパ) +/// +/// - 役割: LoopFormBuilder::build_exit_phis を ControlForm ベースで呼ぶ薄いラッパ。 +/// - 挙動: 既存の build_exit_phis_for_control と完全に同じで、単にパスを固定する。 +pub fn build_exit_phis_for_control( + loopform: &crate::mir::phi_core::loopform_builder::LoopFormBuilder, + ops: &mut O, + form: &ControlForm, + exit_snapshots: &[(BasicBlockId, BTreeMap)], + branch_source_block: BasicBlockId, +) -> Result<(), String> +where + O: crate::mir::phi_core::loopform_builder::LoopFormOps, +{ + crate::mir::phi_core::loopform_builder::build_exit_phis_for_control( + loopform, + ops, + form, + exit_snapshots, + branch_source_block, + ) +} diff --git a/src/mir/phi_core/phi_invariants.rs b/src/mir/phi_core/phi_invariants.rs index 625b8a52..5f7e2d2d 100644 --- a/src/mir/phi_core/phi_invariants.rs +++ b/src/mir/phi_core/phi_invariants.rs @@ -43,6 +43,12 @@ impl PhiInvariantsBox { } } if !missing_preds.is_empty() { + if std::env::var("NYASH_IF_HOLE_TRACE").ok().as_deref() == Some("1") { + eprintln!( + "[phi-invariants/exit] missing pred def: var='{}' exit={:?} header={:?} missing={:?}", + var_name, exit_block, header_block, missing_preds + ); + } return Err(format!( "Exit PHI invariant violated: variable '{}' is not available in all exit preds. exit_block={:?}, header_block={:?}, missing_preds={:?}", var_name, exit_block, header_block, missing_preds @@ -68,6 +74,12 @@ impl PhiInvariantsBox { .or(pre_val); if then_v_opt.is_none() && else_v_opt.is_none() { + if std::env::var("NYASH_IF_HOLE_TRACE").ok().as_deref() == Some("1") { + eprintln!( + "[phi-invariants/if] missing values: var='{}' pre={:?} then={:?} else={:?}", + var_name, pre_val, then_v_opt, else_v_opt + ); + } return Err(format!( "If PHI invariant violated: variable '{}' has no value in then/else/pre snapshots", var_name @@ -77,4 +89,3 @@ impl PhiInvariantsBox { Ok(()) } } - diff --git a/src/mir/query.rs b/src/mir/query.rs new file mode 100644 index 00000000..0ec4b2ec --- /dev/null +++ b/src/mir/query.rs @@ -0,0 +1,159 @@ +//! MirQuery - Read/Write/CFGビューを提供する共通窓口 +//! +//! Box理論: MIR 全体の構造は MirQueryBox が保持し、他の箱(ExitLiveness など)は +//! 「見せる窓」である MirQuery トレイト越しにしか触らないようにする。 +//! これにより MIR 構造への依存を最小化し、テスタビリティと疎結合を保つ。 + +use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; + +/// MIR への読み取り専用ビュー +pub trait MirQuery { + /// ブロック内の命令列(PHI を含む)を順序付きで返す + fn insts_in_block(&self, bb: BasicBlockId) -> &[MirInstruction]; + + /// ブロックの後続(succs)を決定的順序で返す + fn succs(&self, bb: BasicBlockId) -> Vec; + + /// 命令が読む(use する)ValueId のリスト + fn reads_of(&self, inst: &MirInstruction) -> Vec; + + /// 命令が書く(def する)ValueId のリスト + fn writes_of(&self, inst: &MirInstruction) -> Vec; +} + +/// MirQuery の標準実装:MirFunction 全体を抱えつつビューを提供 +pub struct MirQueryBox<'m> { + mir: &'m MirFunction, +} + +impl<'m> MirQueryBox<'m> { + pub fn new(mir: &'m MirFunction) -> Self { + Self { mir } + } +} + +impl<'m> MirQuery for MirQueryBox<'m> { + fn insts_in_block(&self, bb: BasicBlockId) -> &[MirInstruction] { + static EMPTY: &[MirInstruction] = &[]; + self.mir + .blocks + .get(&bb) + .map(|bb| bb.instructions.as_slice()) + .unwrap_or(EMPTY) + } + + fn succs(&self, bb: BasicBlockId) -> Vec { + let mut v: Vec<_> = self + .mir + .blocks + .get(&bb) + .map(|bb| bb.successors.iter().copied().collect()) + .unwrap_or_else(Vec::new); + v.sort_by_key(|b| b.0); + v + } + + fn reads_of(&self, inst: &MirInstruction) -> Vec { + use MirInstruction::*; + match inst { + Const { .. } | Nop => Vec::new(), + Copy { src, .. } => vec![*src], + UnaryOp { operand, .. } => vec![*operand], + BinOp { lhs, rhs, .. } | Compare { lhs, rhs, .. } => { + vec![*lhs, *rhs] + } + TypeOp { value, .. } | TypeCheck { value, .. } | Cast { value, .. } => { + vec![*value] + } + Load { ptr, .. } => vec![*ptr], + Store { ptr, value } => vec![*ptr, *value], + ArrayGet { array, index, .. } => vec![*array, *index], + ArraySet { array, index, value, .. } => vec![*array, *index, *value], + Call { args, .. } + | BoxCall { args, .. } + | PluginInvoke { args, .. } + | ExternCall { args, .. } => args.clone(), + Return { value } => value.iter().copied().collect(), + Branch { condition, .. } => vec![*condition], + Jump { .. } => Vec::new(), + Phi { inputs, .. } => inputs.iter().map(|(_, v)| *v).collect(), + NewBox { args, .. } => args.clone(), + Debug { value, .. } | Print { value, .. } => vec![*value], + DebugLog { values, .. } => values.clone(), + Throw { exception, .. } => vec![*exception], + Catch { .. } => Vec::new(), + NewClosure { captures, me, .. } => { + let mut v: Vec = captures.iter().map(|(_, v)| *v).collect(); + if let Some(m) = me { + v.push(*m); + } + v + } + RefNew { box_val, .. } => vec![*box_val], + RefGet { reference, .. } => vec![*reference], + RefSet { reference, value, .. } => vec![*reference, *value], + WeakNew { box_val, .. } => vec![*box_val], + WeakLoad { weak_ref, .. } => vec![*weak_ref], + WeakRef { value, .. } => vec![*value], + BarrierRead { ptr } | BarrierWrite { ptr } | Barrier { ptr, .. } => { + vec![*ptr] + } + FutureNew { value, .. } => vec![*value], + FutureSet { future, value } => vec![*future, *value], + Await { future, .. } => vec![*future], + Safepoint => Vec::new(), + _ => Vec::new(), + } + } + + fn writes_of(&self, inst: &MirInstruction) -> Vec { + use MirInstruction::*; + match inst { + Const { dst, .. } + | UnaryOp { dst, .. } + | BinOp { dst, .. } + | Compare { dst, .. } + | TypeOp { dst, .. } + | Cast { dst, .. } + | Load { dst, .. } + | ArrayGet { dst, .. } + | Call { dst: Some(dst), .. } + | BoxCall { dst: Some(dst), .. } + | PluginInvoke { dst: Some(dst), .. } + | ExternCall { dst: Some(dst), .. } + | Phi { dst, .. } + | NewBox { dst, .. } + | RefNew { dst, .. } + | RefGet { dst, .. } + | WeakNew { dst, .. } + | WeakLoad { dst, .. } + | WeakRef { dst, .. } + | FutureNew { dst, .. } + | NewClosure { dst, .. } + | Await { dst, .. } + | Copy { dst, .. } => vec![*dst], // Copy writes to dst + // No writes + Nop + | Store { .. } + | ArraySet { .. } + | Call { dst: None, .. } + | BoxCall { dst: None, .. } + | PluginInvoke { dst: None, .. } + | ExternCall { dst: None, .. } + | Return { .. } + | Branch { .. } + | Jump { .. } + | Debug { .. } + | DebugLog { .. } + | Print { .. } + | Throw { .. } + | Catch { .. } + | BarrierRead { .. } + | BarrierWrite { .. } + | Barrier { .. } + | FutureSet { .. } + | Safepoint => Vec::new(), + _ => Vec::new(), + } + } +} diff --git a/src/runner/json_v0_bridge/lowering/loop_.rs b/src/runner/json_v0_bridge/lowering/loop_.rs index 2f65894f..d8bc67c1 100644 --- a/src/runner/json_v0_bridge/lowering/loop_.rs +++ b/src/runner/json_v0_bridge/lowering/loop_.rs @@ -192,6 +192,10 @@ impl LoopFormOps for LoopFormJsonOps<'_> { } self.vars.get(name).copied() } + + fn mir_function(&self) -> &crate::mir::MirFunction { + self.f + } } pub(super) fn lower_loop_stmt( diff --git a/src/runner/stage1_bridge/env.rs b/src/runner/stage1_bridge/env.rs index 08bdf183..ed8cc681 100644 --- a/src/runner/stage1_bridge/env.rs +++ b/src/runner/stage1_bridge/env.rs @@ -57,6 +57,11 @@ pub(super) fn configure_stage1_env( if std::env::var("NYASH_BOX_FACTORY_POLICY").is_err() { cmd.env("NYASH_BOX_FACTORY_POLICY", "builtin_first"); } + // Stage‑1 stubは静的 box 呼び出しが多く、methodize 経路だと未定義 receiver に落ちやすい。 + // 既定では methodization を切ってグローバル呼び出しのままにしておく(必要なら opt-in で上書き)。 + if std::env::var("HAKO_MIR_BUILDER_METHODIZE").is_err() { + cmd.env("HAKO_MIR_BUILDER_METHODIZE", "0"); + } // Stage-1 unified input/backend (fallback to legacy) if std::env::var("NYASH_STAGE1_INPUT").is_err() { diff --git a/src/runner/stage1_bridge/mod.rs b/src/runner/stage1_bridge/mod.rs index 1582ac89..c264bb40 100644 --- a/src/runner/stage1_bridge/mod.rs +++ b/src/runner/stage1_bridge/mod.rs @@ -27,12 +27,20 @@ impl NyashRunner { /// If enabled, run the Stage-1 CLI stub as a child process and return its exit code. /// Returns None when the bridge is not engaged. pub(crate) fn maybe_run_stage1_cli_stub(&self, groups: &CliGroups) -> Option { + // Temporary trace: confirm the bridge is evaluated + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2") { + eprintln!("[stage1-bridge/trace] maybe_run_stage1_cli_stub invoked"); + } + // Guard: skip if child invocation if std::env::var("NYASH_STAGE1_CLI_CHILD") .ok() .as_deref() == Some("1") { + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2") { + eprintln!("[stage1-bridge/trace] skip: NYASH_STAGE1_CLI_CHILD=1"); + } return None; } @@ -42,6 +50,9 @@ impl NyashRunner { .as_deref() != Some("1") { + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("2") { + eprintln!("[stage1-bridge/trace] skip: NYASH_USE_STAGE1_CLI!=1"); + } return None; } @@ -73,7 +84,7 @@ impl NyashRunner { }); let mut cmd = std::process::Command::new(exe); let entry_fn = std::env::var("NYASH_ENTRY") - .unwrap_or_else(|_| "Stage1CliMain.main/1".to_string()); + .unwrap_or_else(|_| "Stage1CliMain.main/0".to_string()); cmd.arg(&entry).arg("--"); for a in &args_result.args { cmd.arg(a); diff --git a/src/tests/mir_funcscanner_trim_min.rs b/src/tests/mir_funcscanner_trim_min.rs new file mode 100644 index 00000000..7a471106 --- /dev/null +++ b/src/tests/mir_funcscanner_trim_min.rs @@ -0,0 +1,102 @@ +// mir_funcscanner_trim_min.rs +// Rust-level test for FuncScannerBox._trim/1 minimal SSA/PHI repro +// +// Goal: +// - Compile lang/src/compiler/entry/func_scanner.hako + minimal _trim test .hako +// - Run MirVerifier to see if Undefined / dominator errors already出るか確認。 +// - Optionally execute via VM(将来の回 regressions 用)。 + +use crate::ast::ASTNode; +use crate::mir::{MirCompiler, MirVerifier}; +use crate::parser::NyashParser; + +#[test] +fn mir_funcscanner_trim_min_verify_and_vm() { + // Minimal .hako that calls FuncScannerBox._trim/1 directly。 + let test_file = "lang/src/compiler/tests/funcscanner_trim_min.hako"; + + // Stage‑3 + using 系のパーサ設定を揃える(他の FuncScanner 系テストと同じ)。 + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("HAKO_PARSER_STAGE3", "1"); + std::env::set_var("NYASH_ENABLE_USING", "1"); + std::env::set_var("HAKO_ENABLE_USING", "1"); + std::env::set_var("NYASH_PARSER_ALLOW_SEMICOLON", "1"); + std::env::set_var("NYASH_DISABLE_PLUGINS", "1"); + + // 必要に応じて MIR / SSA デバッグを有効化(手元での調査用)。 + // std::env::set_var("NYASH_MIR_DEBUG_LOG", "1"); + // std::env::set_var("NYASH_VM_VERIFY_MIR", "1"); + // std::env::set_var("NYASH_IF_HOLE_TRACE", "1"); + + // FuncScanner 本体と最小 _trim テストを 1 ソースにまとめる。 + let func_scanner_src = + include_str!("../../lang/src/compiler/entry/func_scanner.hako"); + let test_src = + std::fs::read_to_string(test_file).expect("Failed to read trim_min .hako"); + let src = format!("{func_scanner_src}\n\n{test_src}"); + + let ast: ASTNode = + NyashParser::parse_from_string(&src).expect("trim_min: parse failed"); + + let mut mc = MirCompiler::with_options(false); + let compiled = mc.compile(ast).expect("trim_min: MIR compile failed"); + + eprintln!( + "[trim/min] module functions = {}", + compiled.module.functions.len() + ); + + // Optional: dump key functions when NYASH_MIR_TEST_DUMP=1 + if std::env::var("NYASH_MIR_TEST_DUMP").ok().as_deref() == Some("1") { + use crate::mir::MirPrinter; + let printer = MirPrinter::new(); + for name in [ + "FuncScannerBox._trim/1", + "FuncScannerBox.trim/1", + "FuncScannerBox.skip_whitespace/2", + "FuncScannerBox.parse_params/1", + "main", + ] { + if let Some(func) = compiled.module.functions.get(name) { + let dump = printer.print_function(func); + eprintln!("----- MIR DUMP: {} -----\n{}", name, dump); + } else { + eprintln!("[trim/min] WARN: function not found: {}", name); + } + } + } + + // MIR verify: ここで FuncScannerBox._trim/1 / trim/1 の SSA/PHI 崩れを観測する。 + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&compiled.module) { + eprintln!("[trim/min] MIR verification errors:"); + for e in &errors { + eprintln!("[rust-mir-verify] {}", e); + } + // いまは「バグ検出」が目的なので、失敗したらそのまま赤にしておく。 + panic!("trim_min: MIR verification failed"); + } + + // VM 実行はオプション扱い(NYASH_TRIM_MIN_VM=1 のときだけ実行)。 + if std::env::var("NYASH_TRIM_MIN_VM").ok().as_deref() == Some("1") { + use crate::backend::VM; + let mut vm = VM::new(); + let vm_out = vm + .execute_module(&compiled.module) + .expect("trim_min: VM execution failed"); + let result_str = vm_out.to_string_box().value; + eprintln!("[trim/min] VM result='{}'", result_str); + assert_eq!(result_str, "0", "trim_min: expected exit code 0"); + } + + // Cleanup env vars + std::env::remove_var("NYASH_PARSER_STAGE3"); + std::env::remove_var("HAKO_PARSER_STAGE3"); + std::env::remove_var("NYASH_ENABLE_USING"); + std::env::remove_var("HAKO_ENABLE_USING"); + std::env::remove_var("NYASH_PARSER_ALLOW_SEMICOLON"); + std::env::remove_var("NYASH_DISABLE_PLUGINS"); + std::env::remove_var("NYASH_MIR_DEBUG_LOG"); + std::env::remove_var("NYASH_VM_VERIFY_MIR"); + std::env::remove_var("NYASH_IF_HOLE_TRACE"); +} diff --git a/src/tests/mir_joinir_min.rs b/src/tests/mir_joinir_min.rs new file mode 100644 index 00000000..f7388e17 --- /dev/null +++ b/src/tests/mir_joinir_min.rs @@ -0,0 +1,139 @@ +// mir_joinir_min.rs +// Phase 26-H: JoinIR型定義妥当性確認テスト(最小ループ) +// +// 目的: +// - JoinFunction/JoinInst の型が破綻していないか確認 +// - 手書きで JoinIR を組み立ててみて、設計の妥当性をチェック +// - まだ LoopForm → JoinIR 自動変換は書かない(Phase 27以降) +// +// 実行条件: +// - デフォルトでは #[ignore] にしておいて手動実行用にする +// - 環境変数 NYASH_JOINIR_EXPERIMENT=1 で実験モード有効化 + +use crate::ast::ASTNode; +use crate::mir::join_ir::*; +use crate::mir::{MirCompiler, ValueId}; +use crate::parser::NyashParser; + +#[test] +#[ignore] // 手動実行用(Phase 26-H 実験段階) +fn mir_joinir_min_manual_construction() { + // Phase 26-H スコープ: 型定義の妥当性確認のみ + // LoopForm からの自動変換は Phase 27 以降で実装 + + // 環境変数トグルチェック + if std::env::var("NYASH_JOINIR_EXPERIMENT").ok().as_deref() != Some("1") { + eprintln!("[joinir/min] NYASH_JOINIR_EXPERIMENT=1 not set, skipping manual construction test"); + return; + } + + // Step 1: MIR までコンパイル(既存パイプラインで) + // Stage-3 環境変数を設定(local キーワード対応) + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("HAKO_PARSER_STAGE3", "1"); + + let test_file = "apps/tests/joinir_min_loop.hako"; + let src = std::fs::read_to_string(test_file) + .unwrap_or_else(|_| panic!("Failed to read {}", test_file)); + + let ast: ASTNode = NyashParser::parse_from_string(&src) + .expect("joinir_min: parse failed"); + + let mut mc = MirCompiler::with_options(false); + let compiled = mc.compile(ast).expect("joinir_min: MIR compile failed"); + + eprintln!( + "[joinir/min] MIR module compiled, {} functions", + compiled.module.functions.len() + ); + + // Step 2: 手書きで JoinIR を構築(設計の妥当性チェック) + let mut join_module = JoinModule::new(); + + // fn main(k_exit) { loop_step(0, k_exit) } + let main_id = JoinFuncId::new(0); + let mut main_func = JoinFunction::new(main_id, "main".to_string(), vec![]); + + // 引数: i_init = 0 (ValueId(100) とする) + let i_init = ValueId(100); + main_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: i_init, + value: ConstValue::Integer(0), + })); + + // loop_step(i_init, k_exit) + let loop_step_id = JoinFuncId::new(1); + let k_exit_id = JoinContId::new(0); + main_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_init], + k_next: Some(k_exit_id), + }); + + join_module.add_function(main_func); + + // fn loop_step(i, k_exit) { if i >= 2 { k_exit(i) } else { loop_step(i+1, k_exit) } } + let mut loop_step_func = JoinFunction::new( + loop_step_id, + "loop_step".to_string(), + vec![ValueId(200)], // i の引数 + ); + + let i_param = ValueId(200); + let cmp_result = ValueId(201); + let i_plus_1 = ValueId(202); + + // cmp_result = (i >= 2) + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Compare { + dst: cmp_result, + op: CompareOp::Ge, + lhs: i_param, + rhs: ValueId(203), // const 2 + })); + + // const 2 + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::Const { + dst: ValueId(203), + value: ConstValue::Integer(2), + })); + + // if cmp_result { k_exit(i) } else { loop_step(i+1, k_exit) } + // ここでは簡略化して Jump 命令だけ書く(実際は分岐制御が必要だが Phase 26-H では型チェックのみ) + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id, + args: vec![i_param], + }); + + // i_plus_1 = i + 1 + loop_step_func.body.push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_plus_1, + op: BinOpKind::Add, + lhs: i_param, + rhs: ValueId(204), // const 1 + })); + + join_module.add_function(loop_step_func); + + // Step 3: Debug 出力で妥当性確認 + eprintln!("[joinir/min] JoinIR module constructed:"); + eprintln!("{:#?}", join_module); + + // アサーション(型定義が使えることを確認) + assert_eq!(join_module.functions.len(), 2); + assert!(join_module.functions.contains_key(&main_id)); + assert!(join_module.functions.contains_key(&loop_step_id)); + + eprintln!("[joinir/min] ✅ JoinIR型定義は妥当(Phase 26-H)"); +} + +#[test] +fn mir_joinir_min_type_sanity() { + // Phase 26-H: 型定義の基本的なサニティチェック(常時実行) + let func_id = JoinFuncId::new(0); + let func = JoinFunction::new(func_id, "test".to_string(), vec![ValueId(1)]); + + assert_eq!(func.id, func_id); + assert_eq!(func.name, "test"); + assert_eq!(func.params.len(), 1); + assert_eq!(func.body.len(), 0); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index a8eca7e1..3de3bdf6 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,7 +9,9 @@ pub mod identical_exec_string; pub mod mir_breakfinder_ssa; pub mod mir_funcscanner_skip_ws; pub mod mir_funcscanner_parse_params_trim_min; +pub mod mir_funcscanner_trim_min; pub mod mir_funcscanner_ssa; +pub mod mir_joinir_min; // Phase 26-H: JoinIR型定義妥当性確認 pub mod mir_locals_ssa; pub mod mir_loopform_conditional_reassign; pub mod mir_loopform_exit_phi;