diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 6b7d99b3..6623d885 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,14 +1,62 @@ # Current Task — Phase 21.8 / 25 / 25.1 / 25.2(Numeric Core & Stage0/Stage1 Bootstrap) -Update (2025-11-18 — Phase 25.1i: LoopSSA v2 (.hako) & ControlFormBox 統合の設計開始) +Update (2025-11-18 — Phase 25.1k: LoopSSA v2 (.hako) & Stage‑B harness 追従 / Rust receiver 統合) - Context: - - Rust 側の LoopForm v2 / Conservative PHI / ControlForm 統合(25.1g)まで完了し、Loop/If の SSA/PHI は ControlForm ベースで安定。 - - Stage‑B 最小ハーネス `tools/test_stageb_min.sh` は、Test1=緑、Test2/3 は `.hako` 側 LoopSSA / BreakFinderBox / PhiInjectorBox 起源のエラーが残っている状態。 -- Plan (25.1i): - - LoopSSA (`lang/src/compiler/builder/ssa/loopssa.hako`) の責務を ControlFormBox 前提に整理し、Rust 側 LoopShape/IfShape に揃えた説明を書く。 - - `lang/src/shared/mir/control_form_box.hako` に `from_loop` / `from_if` などの最小メソッドを追加し、JSON v0 から loop/if 形を復元できる足場を作る。 - - BreakFinderBox / PhiInjectorBox はひとまず現状維持としつつ、ControlFormBox を生成してログに出す観測導線を追加(挙動はまだ変えない)。 - - `HAKO_COMPILER_BUILDER_TRACE=1` で Stage‑B Test2/3 実行時に loop/exit/body の構造が観測できるようにし、undefined ValueId / `%0` の原因追跡に備える。 + - Rust 側の LoopForm v2 / Conservative PHI / ControlForm 統合(25.1g〜h)と、レガシー LoopBuilder 削除(7‑F〜H)まで完了し、Loop/If の SSA/PHI は ControlForm ベースで安定。 + - 25.1f〜j で ControlForm / ControlFormBox / LoopSSA/BreakFinderBox/PhiInjectorBox の責務整理・観測導線が整った状態。 + - Stage‑B 最小ハーネス `tools/test_stageb_min.sh` は: + - Test 1: 直接 VM 実行(`stageb_min_sample.hako`)→ 緑(RC=0) + - Test 2: `.hako` Stage‑B コンパイラ(`compiler_stageb.hako` + LoopSSA v2)経由で同ソースをコンパイルする **「.hako パーサ+Stage‑B コンパイラ経路」** + → `BreakFinderBox.find_breaks/2` → `_find_loops/2` の receiver 未定義(`use of undefined value ValueId(..)`)が継続 + - Test 3: 同テストファイルを Rust MIR ビルダーで実行(`NYASH_VM_VERIFY_MIR=1`)→ `%0` 由来 SSA エラー(LoopSSA 無効時の baseline として扱う) +- Done (Rust側 25.1k 前半): + - Receiver 実体化の一本化: + - `CallMaterializerBox::materialize_receiver_in_callee`(`src/mir/builder/calls/materializer.rs`)を no-op にし、receiver の pinning/LocalSSA は `receiver::finalize_method_receiver` へ集約。 + - `emit_guard::finalize_call_operands` からのみ `receiver::finalize_method_receiver` を呼ぶ構造に整理。 + - pin slot ↔ LocalSSA の連携強化: + - `MirBuilder` に `pin_slot_names: HashMap` を追加(`src/mir/builder.rs`)し、`pin_to_slot` 実行時に元値と pinned 値の両方に slot 名を記録。 + - `LocalSSA.ensure`(`src/mir/builder/ssa/local.rs`)で: + - `variable_map` と `pin_slot_names` から slot 名を復元し、同 slot にぶら下がる最新 ValueId があればそちらに **phi‑redirect** するように変更。 + - これにより `__pin$*@recv` 由来の古い ValueId から最新 PHI 値への構造的な追従が可能になった。 + - Compiler系 Box の StaticCompiler 明文化: + - `CalleeResolverBox::classify_box_kind`(`src/mir/builder/calls/resolver.rs`)に `BreakFinderBox` / `PhiInjectorBox` / `LoopSSA` を追加し、 + Stage‑1/Stage‑B 用 LoopSSA 箱を `CalleeBoxKind::StaticCompiler` として扱うようにした(静的 compiler box 群として明示)。 +- Plan (25.1k 後半 / .hako 側): + - Stage‑B Test2 の `BreakFinderBox._find_loops/2` receiver 未定義(ValueId 50→46→39)の最小再現と切り分け: + - `tools/test_stageb_min.sh` Test2 で得られる Program(JSON v0) を保存し、Rust MIR 側で `NYASH_VM_VERIFY_MIR=1` を通して Rust SSA/PHI を再確認。 + - 同じ JSON に対して `.hako` の `LoopSSA.stabilize_merges(json)` を単独実行するテストを用意し、BreakFinderBox / PhiInjectorBox 前後で JSON を比較。 + - `.hako` LoopSSA v2 本体のステップ実装(phase-25.1k README の K‑A〜D に沿って進める): + - K‑A: ValueId(50) 系エラーの発生位置を JSON v0 レベルで特定。 + - K‑B: BreakFinderBox の LoopScope 検出を ControlFormBox / Rust LoopForm v2 に合わせて保守的に改善。 + - K‑C: PhiInjectorBox に Carrier/Pinned ベースの v2 入口を追加(既存 `_collect_phi_vars` は当面維持)。 + - K‑D: Stage‑B Test3 の `%0` SSA 問題が悪化していないことを確認しつつ、LoopSSA 有効/無効の切り分けを残す。 + +Update (2025-11-18 — Phase 25.1l: Region/GC 観測レイヤー導入(Rust 側のみ)) +- Context: + - LoopForm v2 / ControlForm / Conservative PHI Box により、If/Loop の SSA/PHI は Rust 側で安定しているが、 + GC や Stage‑B 由来の寿命問題を構造的に見るための「Region/スコープ」ビューが不足していた。 + - 25.1l では GC 挙動を変えずに、Rust MIR 側に観測専用の Region/RefSlotKind レイヤーを導入する。 +- Done: + - `src/mir/region/mod.rs`: + - `RefSlotKind`(`StrongRoot/WeakRoot/Borrowed/NonRef`)、`SlotMetadata { name, ref_kind }`、`RegionId`、`RegionKind(Loop/If)`、`Region { id, kind, entry_block, exit_blocks, slots }` を定義。 + - `Region::classify_ref_kind(MirType)` で Box/Array/Future を StrongRoot、それ以外のプリミティブを NonRef として暫定分類。 + - `src/mir/region/observer.rs`: + - `observe_control_form(builder: &MirBuilder, form: &ControlForm)` を実装し、`NYASH_REGION_TRACE=1` のときだけ Region 情報をログ出力。 + - 現時点では `builder.variable_map` と `builder.value_types` から live スロットと RefKind を推定し、`[region/observe] fn=... id=RegionId(..) kind=Loop/If entry=bb.. exits=[..] slots=[..]` の形で eprintln する(メモリには保存しない)。 + - dev 用フィルタとして `func_name.contains("StageB")` の関数のみを観測対象にし、Stage‑B BodyExtractor まわりの Region/Slot 状況が見えるようにした。 + - `src/mir/loop_builder.rs`: + - ループ構築部と `lower_if_in_loop` の If 降下部で、`ControlForm::from_loop` / `ControlForm::from_if` 生成直後に `observe_control_form` を呼び、Loop/If ごとに Region ログを出せるようにした(NYASH_REGION_TRACE=0 のときは完全に無効)。 + - テスト: + - `cargo test -q mir_loopform_exit_phi -- --nocapture` は NYASH_REGION_TRACE=0/1 の両方で緑(Loop/If SSA/PHI 挙動に変化なし)。 +- Plan(25.1l 後半〜次フェーズへのブリッジ): + - Region 木と FunctionRegion: + - 次フェーズで `RegionKind::Function` と FunctionRegion (#0) を導入し、関数スコープを含む Region 木を構築する(entry=関数 bb0, exits=ret ブロック群)。 + - MirBuilder に「現在属している RegionId スタック」を追加し、Loop/If 降下時に parent/children 関係を記録できるようにする(観測専用)。 + - SlotRegistry(関数ごと 1 箱)の設計: + - 長期的には `FunctionSlotRegistry` のような箱を導入し、`slot_id -> { name, MirType, RefSlotKind }` / `name -> slot_id` を SSOT として管理する。 + - 25.1l ではまだ `variable_map` ベースの暫定観測に留め、SlotId ベースへの移行は Region 木導入後に小さく刻んで進める。 + - GC 統合: + - Region/Slot 観測結果をもとに、どの Region 境界で StrongRoot スロットを retain/release すべきかの設計を 25.1m 以降で行う(Rust の既存 GC/Barrier 実装を壊さない範囲に限定)。 Update (2025-11-18 — Phase 25.1g: Conservative PHI ↔ ControlForm 統合の準備) - Context: diff --git a/docs/development/roadmap/phases/phase-25.1k/README.md b/docs/development/roadmap/phases/phase-25.1k/README.md index f0440676..82684934 100644 --- a/docs/development/roadmap/phases/phase-25.1k/README.md +++ b/docs/development/roadmap/phases/phase-25.1k/README.md @@ -1,6 +1,6 @@ # Phase 25.1k — LoopSSA v2 実装 & Stage‑B SSA 安定化(.hako 本体版) -Status: planning(.hako 側 LoopSSA v2 本体実装/Rust 側は既存 SSA/PHI を SSOT として維持) +Status: in progress(.hako 側 LoopSSA v2 本体実装/Rust 側は既存 SSA/PHI を SSOT として維持) ## ゴール @@ -8,11 +8,14 @@ Status: planning(.hako 側 LoopSSA v2 本体実装/Rust 側は既存 SSA/PHI `.hako` 側 LoopSSA v2 の **実装本体** に踏み込むフェーズだよ。 - 具体的には: - Stage‑B minimal harness(`tools/test_stageb_min.sh`)の Test 2/3 で出ている: - - `BreakFinderBox._find_loops/2` 周辺の `use of undefined value ValueId(50)`(Rust VM 側エラー) - - `%0` 由来の SSA エラー(`NYASH_VM_VERIFY_MIR=1` 時) - を **LoopSSA v2 の改善によって減らす/消す** ことを狙う。 + - Test 2: `.hako` Stage‑B コンパイラ(`compiler_stageb.hako`)が `stageb_min_sample.hako` をコンパイルする + **「.hako パーサ+Stage‑B コンパイラ経路(FuncScanner / LoopSSA / BreakFinderBox / PhiInjectorBox)」** での Rust VM エラー + (`BreakFinderBox.find_breaks/2` → `_find_loops/2` の receiver 未定義 `use of undefined value ValueId(..)`) + - Test 3: 同テストファイルを Rust MIR ビルダーで実行したときの `%0` 由来 SSA エラー(`NYASH_VM_VERIFY_MIR=1` 時) + を **LoopSSA v2 の改善によって減らす/消す** ことを狙う(Rust 側 LoopForm v2 / Conservative PHI Box は既に緑で SSOT 済み)。 - 文字列ハードコードベースの `_collect_phi_vars` / synthetic `"r{block}_{var}"` を、 Carrier/Pinned ベースの設計に一歩近づける(完全置き換えまでは行かなくても OK)。 + - Rust 側 LoopForm v2 / Conservative PHI Box は SSOT として維持し、.hako 側 LoopSSA v2 は dev トグルで常時検証しつつ徐々に寄せていく。 ## 前提(25.1j までで揃っているもの) @@ -38,15 +41,15 @@ Status: planning(.hako 側 LoopSSA v2 本体実装/Rust 側は既存 SSA/PHI ### K‑A: Stage‑B Test2 の ValueId(50) 問題の最小再現と LoopSSA 切り分け - 目的: - - `BreakFinderBox._find_loops/2` で発生している `use of undefined value ValueId(50)` を、 - 「LoopSSA が生成した JSON の問題なのか」「他フェーズの JSON なのか」切り分ける。 + - `BreakFinderBox._find_loops/2` で発生している `use of undefined value ValueId(50)`(現在は 46→39 と推移中)を、 + 「LoopSSA / BreakFinderBox が生成した JSON / MIR の問題なのか」「それ以前の Stage‑B パイプラインの問題なのか」切り分ける。 - ステップ: - 1. `tools/test_stageb_min.sh` Test2 の JSON 出力を一時ファイルに保存(Stage‑B → Program(JSON v0) 直後)。 + 1. `tools/test_stageb_min.sh` Test2 の Program(JSON v0) 出力を一時ファイルに保存(Stage‑B → Program(JSON v0) 直後)。 2. その JSON に対して: - - Rust 側 MirBuilder に直接食わせて `NYASH_VM_VERIFY_MIR=1` を通す。 - - `.hako` 側 LoopSSA を単独で呼び出す(`LoopSSA.stabilize_merges(json)`)最小ハーネスを用意。 - 3. どの時点で ValueId(50) が未定義になるかを特定し、 - それが LoopSSA の改変前後で変わっているかを確認する。 + - Rust 側 MirBuilder / LoopForm v2 / Conservative PHI を使って `NYASH_VM_VERIFY_MIR=1` を通し、Rust 側 SSA/PHI の健全性を確認する。 + - `.hako` 側 LoopSSA を単独で呼び出す(`LoopSSA.stabilize_merges(json)`)最小ハーネスを用意し、BreakFinderBox / PhiInjectorBox の前後で JSON を比較。 + 3. どの時点で `BreakFinderBox._find_loops/2` の receiver が「定義のない pinned ValueId(例: 39/46/50)」になるかを特定し、 + LoopSSA v2 の変更前後で挙動が悪化していないかを確認する。 ### K‑B: BreakFinderBox の LoopScope 精度の向上(保守的に) @@ -81,10 +84,23 @@ Status: planning(.hako 側 LoopSSA v2 本体実装/Rust 側は既存 SSA/PHI - 必要なら、Test3 から LoopSSA を一時オフ(`HAKO_LOOPSSA_EXIT_PHI=0`)にした場合のログも取っておき、 「LoopSSA が原因の部分」と「それ以外」を明確に切り分ける。 +### K‑E: デバッグ用ハーネス・プリセットの整備 + +- 目的: + - LoopSSA/BreakFinderBox/PhiInjectorBox 周辺をデバッグしやすくするための「共通の足場」を用意し、 + 25.1k 以降の作業で毎回同じ ENV/コマンドを手で組み立てなくて済むようにする。 +- 実装メモ: + - `tools/stageb_loopssa_debug.sh`: + - Stage‑B 最小ハーネス `tools/test_stageb_min.sh` を、LoopSSA v2 デバッグ向けの ENV プリセット + (`HAKO_LOOPSSA_EXIT_PHI=1`, `HAKO_COMPILER_BUILDER_TRACE=1`, `NYASH_VM_TRACE=1`, + `NYASH_LOCAL_SSA_TRACE=1`, `NYASH_BUILDER_TRACE_RECV=1` など)付きで実行する小さなラッパ。 + - `lang/src/compiler/tests/loopssa_breakfinder_slot.hako` + `tools/test_loopssa_breakfinder_slot.sh`: + - Program(JSON v0) を直接文字列として持つ LoopSSA ハーネス(現在は最小緑 JSON、将来は Stage‑B Test2 から抽出した失敗 JSON を貼り付ける「スロット」として運用)。 + - `HAKO_LOOPSSA_EXIT_PHI=1` で LoopSSA v2 / BreakFinderBox / PhiInjectorBox の経路だけを通し、ValueId(..) 問題を Stage‑B 抜きで再現できるようにする。 + ## このフェーズで「しない」こと - PhiInjectorBox の `_collect_phi_vars` / `_get_var_value` を **完全刷新すること**: - これは 25.1k の次、25.1l 以降の「本格 v2 実装」のタスクとして分ける。 - Rust 側 LoopForm v2 / Conservative PHI の設計を変えること: - Rust 側はあくまで SSOT であり、.hako 側はそれに追従する形で徐々に近づける。 - diff --git a/docs/development/roadmap/phases/phase-25.1l/README.md b/docs/development/roadmap/phases/phase-25.1l/README.md new file mode 100644 index 00000000..e64a7f70 --- /dev/null +++ b/docs/development/roadmap/phases/phase-25.1l/README.md @@ -0,0 +1,114 @@ +# Phase 25.1l — Region/GC 観測レイヤー(LoopForm v2 × RefSlotKind) + +Status: in progress(Rust 側観測レイヤーの最小実装まで完了/挙動変更なし) + +## ゴール + +- 既に導入済みの **LoopForm v2 / ControlForm** を「Region Box(寿命管理の箱)」として扱い、 + Rust MIR 側に **スコープと参照種別(RefKind)の観測レイヤー**を追加するフェーズだよ。 +- このフェーズではあくまで: + - `RefSlotKind` / `Region` / `SlotMetadata` といった **型と観測器(Observer)** を定義し、 + - `NYASH_REGION_TRACE=1` のときだけ Region 単位の live スロットと RefKind をログ出力する。 +- **挙動(実行結果・SSA・PHI)は一切変えない**ことが前提。GC の retain/release 挿入や `.hako` 側のロジック変更は後続フェーズ(25.1m 以降)の仕事にする。 + +## 背景と位置づけ + +- これまでの 25.1d〜25.1k で: + - LoopForm v2 / ControlForm / Conservative PHI Box によって、If/Loop の SSA/PHI は Rust 側で安定。 + - Stage‑B/LoopSSA 由来の SSA 問題の多くは `.hako` 側(特に `StageBBodyExtractorBox.build_body_src/2`)の + 「巨大なループ+if 内での一時値の扱い」に起因することが分かってきた。 +- いっぽう GC 観点では: + - 変数スロットごとに「StrongRoot / WeakRoot / Borrowed / NonRef」を区別し、 + - **Region(= LoopForm/IfForm)境界を GC スコープ境界として扱う**設計が箱理論的にきれいにハマる。 +- 25.1l はそのための **観測フェーズ** であり: + - LoopForm v2 = Region Box という概念を Rust 型として明示し、 + - Stage‑B など複雑な関数で「どの Region でどのスロットが生きているか」をログで確認できるようにする。 + +## スコープ(25.1l でやること) + +### L‑A: RefSlotKind / SlotMetadata / Region 型の導入(実装済み) + +- 新規モジュール: `src/mir/region/mod.rs` +- 型設計: + - `RefSlotKind`: + - `StrongRoot` — GC root 候補となる強参照スロット。 + - `WeakRoot` — 弱参照(GC root としては数えない)。 + - `Borrowed` — 借用(SSA 寿命のみ管理、GC root ではない)。 + - `NonRef` — プリミティブなど、GC 対象外の値。 + - `SlotMetadata`: + - `name: String` — 変数スロット名(`i`, `body_src`, `args` など)。 + - `ref_kind: RefSlotKind` — 上記の種別。 + - `Region`: + - `id: RegionId`(`u32` ラッパ)。 + - `kind: RegionKind` — `Loop` / `If`(25.1l 時点では Function は未導入)。 + - `parent: Option` / `children: Vec` — 制御構造の親子関係(将来用。25.1l では未設定)。 + - `entry_block: BasicBlockId` / `exit_blocks: Vec` — ControlForm から引き継ぐ。 + - `slots: Vec` — その Region で live とみなすスロット一覧(観測用)。 +- RefKind 判定は 25.1l では **簡易ヒューリスティック** に留める: + - `MirType::Box(_)` / `Array(_)` / `Future(_)` → `StrongRoot` + - 明らかなプリミティブ(整数/bool/文字列)→ `NonRef` + - Weak 系/借用系の精密な分類は後続フェーズで詰める。 + +### L‑B: RegionObserver の実装と ControlForm からの接続(実装済み) + +- 新規モジュール: `src/mir/region/observer.rs` +- 現実装の責務: + - `observe_control_form(builder: &MirBuilder, form: &ControlForm)` という関数型 API で: + - `ControlForm` から `entry`/`exits`/Loop/If の形を読む。 + - 該当スコープ時点の `builder.variable_map` に載っている「名前付きスロット」を走査して `SlotMetadata` を構築。 + - `Region` を作成し、`NYASH_REGION_TRACE=1` のときだけ: + - `[region/observe] fn=StageBBodyExtractorBox.build_body_src/2 id=RegionId(..) kind=Loop entry=bb.. exits=[..] slots=[..]` + という形でログ出力する(メモリには保持しない)。 +- Hook の置き場所(いずれも読み取り専用): + - `src/mir/loop_builder.rs`: + - ループ構築完了後、`LoopShape`→`ControlForm::from_loop` 生成直後に `observe_control_form(self.parent_builder, &form)` を呼ぶ。 + - `src/mir/loop_builder.rs`(if 降下部): + - `IfShape`→`ControlForm::from_if` 生成直後に `observe_control_form(self.parent_builder, &form)` を呼ぶ。 + - `.hako` / GC / SSA ロジックには一切影響しない。 +- dev フィルタ: + - 現時点では Stage‑B 周辺の観測に絞るため、`func_name.contains("StageB")` の関数のみログ対象にしている(ログ爆発防止)。 + +### L‑C: 関数スコープ Slot 管理箱(SlotRegistry)との関係(設計メモ) + +- 将来像: + - 各関数ごとに「SlotRegistry(仮称)」箱を 1 つだけ持ち、 + - `slot_id -> { name, MirType, RefSlotKind }` + - `name -> slot_id` + を管理する SSOT として扱う。 + - RegionBox(本 README での Region)は、この SlotRegistry 上で + 「どの SlotId がこの Region で live か」を指すだけにする。 +- 25.1l の暫定実装: + - まだ明示的な SlotRegistry 型は導入せず、 + - `MirBuilder.variable_map.keys()` を「その地点で live なスロット一覧」とみなす。 + - `value_types` から MirType を拾って RefSlotKind を判定する。 + - これは SlotRegistry 導入までのリーズナブルな暫定策として位置付け、 + 将来は `variable_map`→SlotRegistry/SlotId ベースに移行する。 + +### L‑D: Region メタデータの足場(将来の JSON 拡張のための入口だけ) + +- 25.1l では **まだ MIR JSON への出力は行わない**が、将来のために: + - `Region::to_json()` 相当のメソッドを `#[cfg(feature = "region-meta")]` 等のガード付きで用意しておく。 + - `MirCompiler` 側に「RegionObserver を渡しておけば、あとでメタデータを JSON に差し込める」拡張ポイントだけ作っておく。 +- GC 統合フェーズ(25.1m 以降)では、このメタデータを: + - `Program(JSON v0) → MIR(JSON)` 変換結果の横に `"regions":[...]` として添付する形を想定している。 + +## このフェーズで「やらない」こと + +- **GC の retain/release 挿入**: + - Region 情報を使って `retain(slot)` / `release(slot)` 命令を実際に MIR に埋め込むのは、25.1m 以降のタスクとし、このフェーズでは一切行わない。 +- **LoopForm v2 / Conservative PHI / ControlForm の設計変更**: + - 25.1l はあくまで「読み取り専用の観測レイヤー」を追加するだけで、既存の SSA/PHI 実装には手を入れない。 +- **.hako 側の GC 実装や Stage‑B 本体の大規模リファクタ**: + - Stage‑B/LoopSSA の箱分解(`StageBBodyExtractorBox` のサブ箱化)や、`.hako` 側 GC API の導入は、Region 観測結果を見ながら次フェーズで設計する。 +- **関数スコープ SlotRegistry の本実装**: + - 現段階では `variable_map` ベースの暫定観測に留め、SlotId ベースの SSOT 化は次のフェーズ(Region 木 + FunctionRegion 導入時)に回す。 + +## 受け入れ条件(25.1l) + +- `NYASH_REGION_TRACE=0`(既定)のとき: + - すべての既存テスト(LoopForm v2 / Stage‑1 resolver / Stage‑B Rust テスト)が挙動一切変化せず緑のまま。 +- `NYASH_REGION_TRACE=1` のとき: + - 代表関数(特に `StageBBodyExtractorBox.build_body_src/2`)に対して Region/Slot のログが出力される。 + - ログは「Region id / kind(If/Loop) / entry/exit blocks / slots + RefKind」を含み、 + 今後の GC/寿命設計の議論に耐えうる解像度になっている。 +- 変更差分は Rust 側に限定され、`.hako` 側コードや Stage‑B 本体には影響を与えない。*** End Patch***"/> diff --git a/lang/src/compiler/builder/ssa/exit_phi/break_finder.hako b/lang/src/compiler/builder/ssa/exit_phi/break_finder.hako index f789f2f3..fafe82c6 100644 --- a/lang/src/compiler/builder/ssa/exit_phi/break_finder.hako +++ b/lang/src/compiler/builder/ssa/exit_phi/break_finder.hako @@ -19,6 +19,13 @@ using selfhost.shared.mir.control_form as ControlFormBox local trace = trace_flag if trace == 1 { print("[break-finder] start") + // Program(JSON v0) の先頭だけ観測用に出力する + local preview = "" + json_str + local max_len = 200 + if preview.length() > max_len { + preview = preview.substring(0, max_len) + } + print("[break-finder/json] " + preview) } local breaks = new ArrayBox() @@ -80,6 +87,16 @@ using selfhost.shared.mir.control_form as ControlFormBox local loops = new ArrayBox() local s = "" + json_str + // trace=1 のときは Program(JSON v0) の先頭だけ観測用に出力する + if trace == 1 { + local preview = s + local max_len = 200 + if preview.length() > max_len { + preview = preview.substring(0, max_len) + } + print("[break-finder/json] " + preview) + } + // Simple pattern: find "loop_header":NNN, "loop_exit":MMM // This is a simplified version - just finds explicit loop markers local i = 0 diff --git a/lang/src/compiler/builder/ssa/loopssa.hako b/lang/src/compiler/builder/ssa/loopssa.hako index b50f9b53..065039c0 100644 --- a/lang/src/compiler/builder/ssa/loopssa.hako +++ b/lang/src/compiler/builder/ssa/loopssa.hako @@ -9,8 +9,10 @@ // - 出力: exit PHI 相当の命令が入った Program(JSON v0)(文字列)。 // - 解析: BreakFinderBox.find_breaks(json, trace_flag) が JSON を読み取り専用で解析。 // - 変換: PhiInjectorBox.inject_exit_phis(json, breaks, trace_flag) が exit block をテキストベースで書き換える。 -// - 環境変数/トレース: HAKO_LOOPSSA_EXIT_PHI / HAKO_COMPILER_BUILDER_TRACE を LoopSSA 側で解釈し、 -// 下流には 0/1 の trace_flag だけを渡す(箱ごとに ENV を直読しない)。 +// - 環境変数/トレース: +// - HAKO_LOOPSSA_TRACE=1 : LoopSSA/BreakFinder/PhiInjector 専用のトレースON +// - HAKO_COMPILER_BUILDER_TRACE=1 : (後方互換)未設定時のフォールバックとして扱う +// LoopSSA 側で trace_flag を 0/1 に正規化し、下流には数値だけを渡す(箱ごとに ENV を直読しない)。 using lang.compiler.builder.ssa.exit_phi.break_finder as BreakFinderBox using lang.compiler.builder.ssa.exit_phi.phi_injector as PhiInjectorBox @@ -20,7 +22,11 @@ using lang.compiler.builder.ssa.exit_phi.phi_injector as PhiInjectorBox // Phase 2-5 implementation: detect breaks and inject exit PHIs stabilize_merges(stage1_json) { // Resolve trace flag once at the entry point(0/1 に正規化) - local trace_env = env.get("HAKO_COMPILER_BUILDER_TRACE") + // 優先: HAKO_LOOPSSA_TRACE / 後方互換: HAKO_COMPILER_BUILDER_TRACE + local trace_env = env.get("HAKO_LOOPSSA_TRACE") + if trace_env == null { + trace_env = env.get("HAKO_COMPILER_BUILDER_TRACE") + } local trace_flag = 0 if trace_env != null && ("" + trace_env) == "1" { trace_flag = 1 } diff --git a/lang/src/compiler/tests/breakfinder_direct_min.hako b/lang/src/compiler/tests/breakfinder_direct_min.hako new file mode 100644 index 00000000..843369a1 --- /dev/null +++ b/lang/src/compiler/tests/breakfinder_direct_min.hako @@ -0,0 +1,37 @@ +// breakfinder_direct_min.hako — BreakFinderBox 直接呼び出し用の極小ハーネス +// +// 目的: +// - Stage‑B/LoopSSA を経由せずに、BreakFinderBox.find_breaks/2 自体の +// MIR 形状と SSA 性を確認するための最小サンプルだよ。 +// - Program(JSON v0) 文字列を 1 本用意して、BreakFinderBox.find_breaks(json, trace) +// を直接呼び出し、VM/Verifier で undefined receiver などのエラーを観測する。 +// +// JSON 形状(loopssa_breakfinder_min.hako と同じ単純な緑ケース): +// { +// "kind":"Program", +// "functions":[ +// { +// "name":"main", +// "blocks":[ +// {"id":0,"loop_header":0,"loop_exit":2}, +// {"id":1}, +// {"id":2} +// ] +// } +// ] +// } + +using lang.compiler.builder.ssa.exit_phi.break_finder as BreakFinderBox + +static box Main { + method main(args) { + // 単純な Program(JSON v0) を直接埋め込む + local json = "{\"kind\":\"Program\",\"functions\":[{\"name\":\"main\",\"blocks\":[{\"id\":0,\"loop_header\":0,\"loop_exit\":2},{\"id\":1},{\"id\":2}]}]}" + + // trace_flag=1 で BreakFinderBox 内部の挙動を観測する + local breaks = BreakFinderBox.find_breaks(json, 1) + print(breaks) + return 0 + } +} + diff --git a/lang/src/compiler/tests/loopssa_breakfinder_slot.hako b/lang/src/compiler/tests/loopssa_breakfinder_slot.hako new file mode 100644 index 00000000..ad8ba5a2 --- /dev/null +++ b/lang/src/compiler/tests/loopssa_breakfinder_slot.hako @@ -0,0 +1,29 @@ +// loopssa_breakfinder_slot.hako — LoopSSA/BreakFinder 用「最小失敗 JSON」スロット +// +// 目的: +// - Stage‑B 最小サンプル(stageb_min_sample.hako など)から切り出した +// 「問題のある Program(JSON v0)」を貼り付けて、LoopSSA v2 / +// BreakFinderBox / PhiInjectorBox の挙動を単体で再現するためのスロットだよ。 +// - 初期状態では loopssa_breakfinder_min.hako と同じ、単純な緑の JSON を使っておく。 +// バグが再現できる JSON v0 が手に入ったら、下の `json` 文字列を書き換えて使う。 +// +// 注意: +// - ここは「.hako パーサ → LoopSSA」経路のデバッグ専用。Stage‑B 全体ではなく、 +// LoopSSA/BreakFinderBox 周辺の挙動だけを確認したいときに使ってね。 +// + +using lang.compiler.builder.ssa.loopssa as LoopSSA + +static box Main { + method main(args) { + // TODO: Stage‑B Test2 から切り出した Program(JSON v0) で上書きして使う。 + // 現在は loopssa_breakfinder_min と同じ単純な緑の JSON だよ。 + local json = "{\"kind\":\"Program\",\"functions\":[{\"name\":\"main\",\"blocks\":[{\"id\":0,\"loop_header\":0,\"loop_exit\":2},{\"id\":1},{\"id\":2}]}]}" + + // LoopSSA v2 を直接呼び出す(Exit PHI パスのみ) + local out = LoopSSA.stabilize_merges(json) + print(out) + return 0 + } +} + diff --git a/lang/src/compiler/tests/stageb_mini_driver.hako b/lang/src/compiler/tests/stageb_mini_driver.hako new file mode 100644 index 00000000..dfdaccca --- /dev/null +++ b/lang/src/compiler/tests/stageb_mini_driver.hako @@ -0,0 +1,100 @@ +// stageb_mini_driver.hako — Stage‑B 近似の極小ドライバ +// +// 目的: +// - StageBDriverBox.main/1 の経路を「最小限」に真似したテスト用ドライバだよ。 +// - ParserBox + CompilerBuilder.apply_all(LoopSSA/BreakFinder/PhiInjector を含む) +// だけを通し、Stage‑B 本体の箱(StageBArgsBox / StageBBodyExtractorBox / FuncScannerBox) +// を介さずに SSA/ValueId 問題を再現できるか確認する。 +// +// 振る舞い: +// - lang/src/compiler/tests/stageb_min_sample.hako と同等のソースコード文字列を +// 内部に埋め込み、ParserBox.parse_program2 → CompilerBuilder.apply_all を実行する。 +// - 出力は Stage‑1 Program(JSON v0)(LoopSSA 適用後)。VM/Verifier/ログで +// BreakFinderBox/LoopSSA 周辺の挙動を観測する。 + +using lang.compiler.builder.mod as CompilerBuilder + +static box StageBMiniDriverBox { + method run(args) { + // stageb_min_sample.hako 相当のソースをそのまま埋め込む + local src = "" + + // static box TestArgs { ... } + src = src + "static box TestArgs {\n" + src = src + " method process(args) {\n" + src = src + " if args != null {\n" + src = src + " local n = args.length()\n" + src = src + " local i = 0\n" + src = src + " loop(i < n) {\n" + src = src + " local item = args.get(i)\n" + src = src + " print(item)\n" + src = src + " i = i + 1\n" + src = src + " }\n" + src = src + " }\n" + src = src + " return 0\n" + src = src + " }\n" + src = src + "}\n\n" + + // static box TestSimple { ... } + src = src + "static box TestSimple {\n" + src = src + " method run() {\n" + src = src + " local x = new ArrayBox()\n" + src = src + " local len = x.length()\n" + src = src + " print(len)\n" + src = src + " return 0\n" + src = src + " }\n" + src = src + "}\n\n" + + // static box TestNested { ... } + src = src + "static box TestNested {\n" + src = src + " method complex(data) {\n" + src = src + " if data != null {\n" + src = src + " local count = data.length()\n" + src = src + " if count > 0 {\n" + src = src + " local j = 0\n" + src = src + " loop(j < count) {\n" + src = src + " local val = data.get(j)\n" + src = src + " if val != null {\n" + src = src + " local s = \"\" + val\n" + src = src + " print(s)\n" + src = src + " }\n" + src = src + " j = j + 1\n" + src = src + " }\n" + src = src + " }\n" + src = src + " }\n" + src = src + " return 0\n" + src = src + " }\n" + src = src + "}\n\n" + + // static box Main { ... } + src = src + "static box Main {\n" + src = src + " method main(args) {\n" + src = src + " local t1 = TestArgs.process(args)\n" + src = src + " local t2 = TestSimple.run()\n" + src = src + " local test_data = new ArrayBox()\n" + src = src + " local t3 = TestNested.complex(test_data)\n" + src = src + " return 0\n" + src = src + " }\n" + src = src + "}\n" + + // 1) Stage‑1 Program(JSON v0) を生成 + local p = new ParserBox() + p.stage3_enable(1) + local ast_json = p.parse_program2(src) + + // 2) CompilerBuilder パイプライン適用(Rewrite/LocalSSA/LoopSSA 等) + ast_json = CompilerBuilder.apply_all(ast_json) + + // 3) 出力をそのまま表示(Stage‑B 本体の emit に相当) + print(ast_json) + return 0 + } +} + +// エントリポイント: Main.main → StageBMiniDriverBox.run +static box Main { + method main(args) { + return StageBMiniDriverBox.run(args) + } +} + diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 4459ca12..20e62285 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -185,6 +185,9 @@ pub struct MirBuilder { pub(super) local_ssa_map: HashMap<(BasicBlockId, ValueId, u8), ValueId>, /// BlockSchedule cache: deduplicate materialize copies per (bb, src) pub(super) schedule_mat_map: HashMap<(BasicBlockId, ValueId), ValueId>, + /// Mapping from ValueId to its pin slot name (e.g., "__pin$3$@recv") + /// Used by LocalSSA to redirect old pinned values to the latest slot value. + pub(super) pin_slot_names: HashMap, /// Guard flag to prevent re-entering emit_unified_call from BoxCall fallback. /// Used when RouterPolicyBox in emit_unified_call has already decided to @@ -256,6 +259,7 @@ impl MirBuilder { local_ssa_map: HashMap::new(), schedule_mat_map: HashMap::new(), + pin_slot_names: HashMap::new(), in_unified_boxcall_fallback: false, recursion_depth: 0, diff --git a/src/mir/builder/calls/materializer.rs b/src/mir/builder/calls/materializer.rs index f0def3a7..2f67464f 100644 --- a/src/mir/builder/calls/materializer.rs +++ b/src/mir/builder/calls/materializer.rs @@ -106,46 +106,15 @@ impl CallMaterializerBox { /// - SSA不変条件の保持(receiverが常に定義済みであることを保証) /// - デバッグトレース出力(NYASH_BUILDER_TRACE_RECV=1) pub fn materialize_receiver_in_callee( - builder: &mut MirBuilder, + _builder: &mut MirBuilder, callee: Callee, ) -> Result { - match callee { - Callee::Method { box_name, method, receiver: Some(r), certainty, box_kind } => { - if std::env::var("NYASH_BUILDER_TRACE_RECV").ok().as_deref() == Some("1") { - let current_fn = builder - .current_function - .as_ref() - .map(|f| f.signature.name.clone()) - .unwrap_or_else(|| "".to_string()); - let bb = builder.current_block; - let names: Vec = builder - .variable_map - .iter() - .filter(|(_, &vid)| vid == r) - .map(|(k, _)| k.clone()) - .collect(); - // CRITICAL DEBUG: Show type information sources - let origin = builder.value_origin_newbox.get(&r).cloned(); - let vtype = builder.value_types.get(&r).cloned(); - eprintln!( - "[builder/recv-trace] fn={} bb={:?} method={}.{} recv=%{} aliases={:?}", - current_fn, - bb, - box_name.clone(), - method, - r.0, - names - ); - eprintln!( - "[builder/recv-trace] value_origin_newbox: {:?}, value_types: {:?}", - origin, vtype - ); - } - // Prefer pinning to a slot so start_new_block can propagate it across entries. - let r_pinned = builder.pin_to_slot(r, "@recv").unwrap_or(r); - Ok(Callee::Method { box_name, method, receiver: Some(r_pinned), certainty, box_kind }) - } - other => Ok(other), - } + // Phase 25.1j+: + // Receiver 実体化(pinning + LocalSSA)は ReceiverMaterializationBox + // (crate::mir::builder::receiver)側に一本化したよ。 + // ここでは Callee 構造は変更せず、そのまま返す。 + // + // NYASH_BUILDER_TRACE_RECV は新しい receiver.rs 側で扱う。 + Ok(callee) } } diff --git a/src/mir/builder/calls/resolver.rs b/src/mir/builder/calls/resolver.rs index f2234891..260a6813 100644 --- a/src/mir/builder/calls/resolver.rs +++ b/src/mir/builder/calls/resolver.rs @@ -151,6 +151,8 @@ impl<'a> CalleeResolverBox<'a> { "ParserLiteralBox" | "ParserTokenBox" | // Scanner/builder boxes "FuncScannerBox" | "MirBuilderBox" | + // LoopSSA / Exit PHI analyzers (Stage-1/Stage-B) + "BreakFinderBox" | "PhiInjectorBox" | "LoopSSA" | // Other compiler-internal boxes "JsonFragBox" => CalleeBoxKind::StaticCompiler, diff --git a/src/mir/builder/ssa/local.rs b/src/mir/builder/ssa/local.rs index 1e351edf..aff7c2a4 100644 --- a/src/mir/builder/ssa/local.rs +++ b/src/mir/builder/ssa/local.rs @@ -54,20 +54,31 @@ pub fn ensure(builder: &mut MirBuilder, v: ValueId, kind: LocalKind) -> ValueId // CRITICAL FIX: If `v` is from a pinned slot, check if there's a PHI value for that slot // in the current block's variable_map. If so, use the PHI value directly instead of // emitting a Copy from the old value (which might not be defined in this block). + // Try to detect pinned slots for this value and redirect to the latest slot value. + // 1) First, look for "__pin$" entries in variable_map that still point to v. + // 2) If not found, consult builder.pin_slot_names to recover the slot name + // and then look up the current ValueId for that slot. + let mut slot_name_opt: Option = None; + let names_for_v: Vec = builder.variable_map.iter() .filter(|(k, &vid)| vid == v && k.starts_with("__pin$")) .map(|(k, _)| k.clone()) .collect(); if let Some(first_pin_name) = names_for_v.first() { - // This value is from a pinned slot. Check if the slot has been updated - // (e.g., by a PHI) in the current block. - if let Some(¤t_val) = builder.variable_map.get(first_pin_name) { + slot_name_opt = Some(first_pin_name.clone()); + } else if let Some(name) = builder.pin_slot_names.get(&v) { + slot_name_opt = Some(name.clone()); + } + + if let Some(slot_name) = slot_name_opt { + if let Some(¤t_val) = builder.variable_map.get(&slot_name) { if current_val != v { - // The slot has been updated (likely by a PHI). Use the updated value. + // The slot has been updated (likely by a PHI or header rewrite). + // Use the updated value instead of the stale pinned ValueId. if std::env::var("NYASH_LOCAL_SSA_TRACE").ok().as_deref() == Some("1") { eprintln!("[local-ssa] phi-redirect bb={:?} kind={:?} slot={} %{} -> %{}", - bb, kind, first_pin_name, v.0, current_val.0); + bb, kind, slot_name, v.0, current_val.0); } builder.local_ssa_map.insert(key, current_val); return current_val; diff --git a/src/mir/builder/utils.rs b/src/mir/builder/utils.rs index 7973b10d..28cdab04 100644 --- a/src/mir/builder/utils.rs +++ b/src/mir/builder/utils.rs @@ -329,6 +329,10 @@ impl super::MirBuilder { } // Propagate lightweight metadata so downstream resolution/type inference remains stable crate::mir::builder::metadata::propagate::propagate(self, v, dst); + // Remember pin slot name for both the original and the pinned value. + // LocalSSA uses this to redirect old pinned values to the latest slot value. + self.pin_slot_names.insert(v, slot_name.clone()); + self.pin_slot_names.insert(dst, slot_name.clone()); self.variable_map.insert(slot_name, dst); Ok(dst) } diff --git a/src/mir/loop_builder.rs b/src/mir/loop_builder.rs index 116fba39..7190bbc3 100644 --- a/src/mir/loop_builder.rs +++ b/src/mir/loop_builder.rs @@ -349,6 +349,11 @@ impl<'a> LoopBuilder<'a> { }; let form = ControlForm::from_loop(loop_shape.clone()); + // Region/GC 観測レイヤ(Phase 25.1l): + // NYASH_REGION_TRACE=1 のときだけ、Stage‑B 周辺ループの + // Region 情報(entry/exit/slots)をログに出すよ。 + crate::mir::region::observer::observe_control_form(self.parent_builder, &form); + // Build exit PHIs for break statements using ControlForm wrapper let exit_snaps = self.exit_snapshots.clone(); crate::mir::phi_core::loopform_builder::build_exit_phis_for_control( @@ -778,7 +783,6 @@ impl<'a> LoopBuilder<'a> { } // Reset to pre-if snapshot, then delegate to shared helper self.parent_builder.variable_map = pre_if_var_map.clone(); - let mut ops = Ops(self); // Phase 25.1h: ControlForm統合版に切り替え let if_shape = IfShape { @@ -789,6 +793,13 @@ impl<'a> LoopBuilder<'a> { }; let form = ControlForm::from_if(if_shape.clone()); + // Region/GC 観測レイヤ(Phase 25.1l): + // NYASH_REGION_TRACE=1 のときだけ、Stage‑B 周辺 If 構造の + // Region 情報(entry/exit/slots)をログに出すよ。 + crate::mir::region::observer::observe_control_form(self.parent_builder, &form); + + let mut ops = Ops(self); + crate::mir::phi_core::if_phi::merge_modified_with_control( &mut ops, &form, diff --git a/src/mir/mod.rs b/src/mir/mod.rs index 5c46c1e7..538650ea 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -23,6 +23,7 @@ pub mod optimizer; pub mod utils; // Phase 15 control flow utilities for root treatment // pub mod lowerers; // reserved: Stage-3 loop lowering (while/for-range) pub mod phi_core; // Phase 1 scaffold: unified PHI entry (re-exports only) +pub mod region; // Phase 25.1l: Region/GC観測レイヤ(LoopForm v2 × RefKind) pub mod optimizer_passes; // optimizer passes (normalize/diagnostics) pub mod optimizer_stats; // extracted stats struct pub mod passes; diff --git a/src/mir/region/mod.rs b/src/mir/region/mod.rs new file mode 100644 index 00000000..0085de80 --- /dev/null +++ b/src/mir/region/mod.rs @@ -0,0 +1,73 @@ +/*! + * MIR Region/GC Observation Layer (Phase 25.1l) + * + * 目的: + * - LoopForm v2 / ControlForm を「Region Box(寿命管理の箱)」として眺めるための + * 型定義と観測用ヘルパーを提供するよ。 + * - このフェーズでは GC の retain/release などは一切挿入せず、あくまで + * 「どの制御構造でどのスロットが生きているか」をログで観測するだけだよ。 + * + * 注意: + * - 既存の SSA/PHI 挙動には影響を与えない(NYASH_REGION_TRACE=1 のときだけ動く)。 + * - .hako 側やランタイム側の GC 実装には触れない。 + */ + +use crate::mir::{BasicBlockId, MirType}; + +/// GC/寿命管理の観点から見たスロット種別だよ。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RefSlotKind { + /// GC root 候補となる強参照スロット(Box 系など) + StrongRoot, + /// 弱参照(将来の WeakRefBox 等)。Phase 25.1l ではまだ未使用。 + WeakRoot, + /// 借用スロット(寿命は SSA で管理、GC root ではない想定) + Borrowed, + /// 非参照(プリミティブ値など、GC 対象外) + NonRef, +} + +/// 1 つの変数スロットに関するメタデータだよ。 +#[derive(Debug, Clone)] +pub struct SlotMetadata { + pub name: String, + pub ref_kind: RefSlotKind, +} + +/// Region ID の薄い newtype だよ(デバッグ用途)。 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RegionId(pub u32); + +/// Region の種別(Loop / If など)だよ。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RegionKind { + Loop, + If, +} + +/// ControlForm から派生した Region 情報だよ(観測専用)。 +#[derive(Debug, Clone)] +pub struct Region { + pub id: RegionId, + pub kind: RegionKind, + pub parent: Option, + pub entry_block: BasicBlockId, + pub exit_blocks: Vec, + pub slots: Vec, +} + +impl Region { + /// MirType から簡易的に RefSlotKind を推定するよ(観測専用)。 + pub fn classify_ref_kind(ty: &MirType) -> RefSlotKind { + match ty { + MirType::Box(_) | MirType::Array(_) | MirType::Future(_) => RefSlotKind::StrongRoot, + MirType::Integer | MirType::Float | MirType::Bool | MirType::String => { + RefSlotKind::NonRef + } + MirType::Void | MirType::Unknown => RefSlotKind::NonRef, + } + } +} + +pub mod observer; + diff --git a/src/mir/region/observer.rs b/src/mir/region/observer.rs new file mode 100644 index 00000000..957907d3 --- /dev/null +++ b/src/mir/region/observer.rs @@ -0,0 +1,107 @@ +/*! + * RegionObserver – ControlForm から Region/Slot を観測する薄いレイヤだよ。 + * + * - NYASH_REGION_TRACE=1 のときだけ動作し、Loop/If ごとの Region 情報を eprintln! するよ。 + * - 既存の SSA/PHI 挙動には一切影響しない(読み取り専用)。 + */ + +use crate::mir::builder::MirBuilder; +use crate::mir::control_form::{ControlForm, ControlKind}; +use crate::mir::region::{RefSlotKind, Region, RegionId, RegionKind, SlotMetadata}; +use crate::mir::ValueId; +use std::sync::atomic::{AtomicU32, Ordering}; + +static NEXT_REGION_ID: AtomicU32 = AtomicU32::new(0); + +fn is_region_trace_on() -> bool { + std::env::var("NYASH_REGION_TRACE") + .ok() + .as_deref() + == Some("1") +} + +/// ControlForm と MirBuilder から Region 情報を観測・ログ出力するよ。 +/// +/// - 25.1l では Stage‑B 周辺のデバッグが主目的なので、 +/// まずは `StageBBodyExtractorBox.*` などに絞って使う想定だよ。 +pub fn observe_control_form(builder: &MirBuilder, form: &ControlForm) { + if !is_region_trace_on() { + return; + } + + // いまのところ compilation_context が Some のケースは観測対象外にしておく。 + // (BoxCompilationContext 内の variable_map は別経路で管理されているため) + if builder.compilation_context.is_some() { + return; + } + + let func_name = builder + .current_function + .as_ref() + .map(|f| f.signature.name.as_str()) + .unwrap_or(""); + + // dev 用フィルタ: Stage‑B 周辺を優先的に見る(必要なら将来拡張)。 + // ここでは "StageB" を含む関数だけログする。 + if !func_name.contains("StageB") { + return; + } + + let id = RegionId(NEXT_REGION_ID.fetch_add(1, Ordering::Relaxed)); + + let kind = match form.kind { + ControlKind::Loop(_) => RegionKind::Loop, + ControlKind::If(_) => RegionKind::If, + }; + + let entry_block = form.entry; + let exit_blocks = form.exits.clone(); + + // 変数スロットは variable_map と value_types から best-effort で推定するよ。 + let mut slots: Vec = Vec::new(); + + for (name, &vid) in builder.variable_map.iter() { + let ref_kind = classify_slot(builder, vid, name.as_str()); + slots.push(SlotMetadata { + name: name.clone(), + ref_kind, + }); + } + + let region = Region { + id, + kind, + parent: None, + entry_block, + exit_blocks, + slots, + }; + + eprintln!( + "[region/observe] fn={} id={:?} kind={:?} entry={:?} exits={:?} slots={:?}", + func_name, region.id, region.kind, region.entry_block, region.exit_blocks, region.slots + ); +} + +fn classify_slot(builder: &MirBuilder, v: ValueId, name: &str) -> RefSlotKind { + if let Some(ty) = builder.value_types.get(&v) { + return Region::classify_ref_kind(ty); + } + + // 型情報が無い場合は名前ヒューリスティックで軽く分類する(観測専用)。 + if matches!( + name, + "args" + | "src" + | "body_src" + | "bundles" + | "bundle_names" + | "bundle_srcs" + | "require_mods" + ) { + RefSlotKind::StrongRoot + } else { + RefSlotKind::NonRef + } +} + diff --git a/src/mir/verification.rs b/src/mir/verification.rs index 0b65a45d..f40bda3b 100644 --- a/src/mir/verification.rs +++ b/src/mir/verification.rs @@ -34,6 +34,87 @@ impl MirVerifier { for (_name, function) in &module.functions { if let Err(mut func_errors) = self.verify_function(function) { + // Dev-only trace: BreakFinderBox / LoopSSA 周辺のSSAバグを詳細に観測する。 + // + // NYASH_BREAKFINDER_SSA_TRACE=1 のときだけ有効になり、 + // compiler_stageb.hako など大規模モジュール内での + // BreakFinderBox.* に対する UndefinedValue を詳細に出力する。 + // + // 併せて、同じトグルで「任意の関数」に対する UndefinedValue も + // 簡易ログとして出力し、どの関数で支配関係が崩れているかを + // 追いやすくしている(箱理論の観測レイヤー強化)。 + if std::env::var("NYASH_BREAKFINDER_SSA_TRACE") + .ok() + .as_deref() == Some("1") + { + // 1) BreakFinderBox / LoopSSA 向けの詳細ログ + if function.signature.name.starts_with("BreakFinderBox.") + || function.signature.name.starts_with("LoopSSA.") + { + for e in &func_errors { + if let VerificationError::UndefinedValue { + value, + block, + instruction_index, + } = e + { + if let Some(bb) = function.blocks.get(block) { + let inst = bb.instructions.get(*instruction_index); + eprintln!( + "[breakfinder/ssa] UndefinedValue {:?} in fn {} at bb={:?}, inst={} => {:?}", + value, + function.signature.name, + block, + instruction_index, + inst, + ); + } else { + eprintln!( + "[breakfinder/ssa] UndefinedValue {:?} in fn {} at bb={:?}, inst={}", + value, + function.signature.name, + block, + instruction_index + ); + } + } + } + } + + // 2) 任意の関数向けの簡易 UndefinedValue ログ + for e in &func_errors { + match e { + VerificationError::UndefinedValue { + value, + block, + instruction_index, + } => { + eprintln!( + "[mir-ssa-debug] UndefinedValue {:?} in fn {} at bb={:?}, inst={}", + value, + function.signature.name, + block, + instruction_index + ); + } + VerificationError::DominatorViolation { + value, + use_block, + def_block, + } => { + eprintln!( + "[mir-ssa-debug] DominatorViolation {:?} in fn {}: use_block={:?}, def_block={:?}", + value, + function.signature.name, + use_block, + def_block + ); + } + _ => {} + } + } + } + // Add function context to errors for _error in &mut func_errors { // Could add function name to error context here diff --git a/src/tests/mir_breakfinder_ssa.rs b/src/tests/mir_breakfinder_ssa.rs new file mode 100644 index 00000000..bafbf921 --- /dev/null +++ b/src/tests/mir_breakfinder_ssa.rs @@ -0,0 +1,174 @@ +/*! + * BreakFinder / LoopSSA SSA smoke tests + * + * 目的: + * - .hako 側 LoopSSA / BreakFinderBox を Rust MIR 経由で軽くカバーする足場だよ。 + * - Stage‑B 本体とは独立に、「最小の Program(JSON v0) で LoopSSA/BreakFinder を通す」 + * パターンが MIR 的に健全(Undefined Value が出ない)であることを確認する。 + * + * 注意: + * - Stage‑B Test2 で見えている BreakFinderBox._find_loops/2 の receiver 未定義バグは、 + * 現時点ではまだこのテストでは再現していない(より複雑な Stage‑B パイプライン固有の条件)。 + * - ここでは「最小緑ケース」の SSA を固定し、将来 Stage‑B 由来の JSON v0 を切り出せたときに + * 追加のテストを足せるようにするのが狙いだよ。 + */ + +use crate::ast::ASTNode; +use crate::mir::{MirCompiler, MirPrinter, MirVerifier}; +use crate::parser::NyashParser; + +fn ensure_stage3_env() { + std::env::set_var("NYASH_PARSER_STAGE3", "1"); + std::env::set_var("NYASH_PARSER_ALLOW_SEMICOLON", "1"); +} + +/// LoopSSA.stabilize_merges を最小 JSON で通す SSA スモーク +/// +/// lang/src/compiler/tests/loopssa_breakfinder_min.hako 相当: +/// +/// using lang.compiler.builder.ssa.loopssa as LoopSSA +/// +/// static box Main { +/// method main(args) { +/// local json = "{\"kind\":\"Program\",\"functions\":[{\"name\":\"main\",\"blocks\":[{\"id\":0,\"loop_header\":0,\"loop_exit\":2},{\"id\":1},{\"id\":2}]}]}" +/// local out = LoopSSA.stabilize_merges(json) +/// print(out) +/// return 0 +/// } +/// } +#[test] +fn mir_loopssa_breakfinder_min_verifies() { + ensure_stage3_env(); + + let src = r#" +using lang.compiler.builder.ssa.loopssa as LoopSSA + +static box Main { + method main(args) { + local json = "{\"kind\":\"Program\",\"functions\":[{\"name\":\"main\",\"blocks\":[{\"id\":0,\"loop_header\":0,\"loop_exit\":2},{\"id\":1},{\"id\":2}]}]}" + + local out = LoopSSA.stabilize_merges(json) + print(out) + return 0 + } +} +"#; + + let ast: ASTNode = NyashParser::parse_from_string(src).expect("parse ok"); + let mut mc = MirCompiler::with_options(false); + let cr = mc.compile(ast).expect("compile ok"); + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&cr.module) { + // 既知のベースライン: bb0 での %0 未定義(エントリ初期値)が混ざることがある。 + // ここではそれらを除外し、「それ以外のエラーが無いこと」を確認対象とする。 + let non_baseline: Vec<_> = errors + .iter() + .filter(|e| { + let msg = e.to_string(); + !(msg.contains("Undefined value %0 used in block bb0")) + }) + .collect(); + + if non_baseline.is_empty() { + // ベースライン (%0/bb0) だけなら許容(別タスクで扱う) + return; + } + + if std::env::var("NYASH_MIR_TEST_DUMP").ok().as_deref() == Some("1") { + let dump = MirPrinter::new().print_module(&cr.module); + eprintln!("----- MIR DUMP (LoopSSA.min) -----\n{}", dump); + } + for e in &non_baseline { + eprintln!("[rust-mir-verify] {}", e); + } + panic!("MIR verification failed for LoopSSA.breakfinder minimal JSON case (non-baseline errors present)"); + } +} + +/// BreakFinderBox.find_breaks/2 を直接呼び出す極小スモーク +/// +/// lang/src/compiler/tests/breakfinder_direct_min.hako 相当: +/// +/// using lang.compiler.builder.ssa.exit_phi.break_finder as BreakFinderBox +/// +/// static box Main { +/// method main(args) { +/// local json = "{\"kind\":\"Program\",\"functions\":[{\"name\":\"main\",\"blocks\":[{\"id\":0,\"loop_header\":0,\"loop_exit\":2},{\"id\":1},{\"id\":2}]}]}" +/// local breaks = BreakFinderBox.find_breaks(json, 1) +/// print(breaks) +/// return 0 +/// } +/// } +/// +/// 注意: +/// - 現状、このケースでは SSA/receiver 周りの既知バグは再現していない。 +/// - ここでは「BreakFinderBox 自体の MIR 生成がクラッシュせず通る」ことだけを確認する。 +#[test] +fn mir_breakfinder_direct_min_compiles() { + ensure_stage3_env(); + + let src = r#" +using lang.compiler.builder.ssa.exit_phi.break_finder as BreakFinderBox + +static box Main { + method main(args) { + local json = "{\"kind\":\"Program\",\"functions\":[{\"name\":\"main\",\"blocks\":[{\"id\":0,\"loop_header\":0,\"loop_exit\":2},{\"id\":1},{\"id\":2}]}]}" + + local breaks = BreakFinderBox.find_breaks(json, 1) + print(breaks) + return 0 + } +} +"#; + + let ast: ASTNode = NyashParser::parse_from_string(src).expect("parse ok"); + let mut mc = MirCompiler::with_options(false); + // コンパイルが通ることのみを保証する(SSA まではチェックしない) + mc.compile(ast).expect("compile ok"); +} + +/// Debug-only: compiler_stageb.hako 全体を MIR 化して BreakFinder/LoopSSA SSA を観測する +/// +/// - 現時点では既知の SSA 問題があるため #[ignore] 付き。 +/// - NYASH_BREAKFINDER_SSA_TRACE=1 で実行すると、UndefinedValue が出たときに +/// verification.rs 側の dev フックが詳細な bb/inst/命令をダンプしてくれる。 +#[test] +#[ignore] +fn mir_compiler_stageb_breakfinder_ssa_debug() { + ensure_stage3_env(); + std::env::set_var("NYASH_BREAKFINDER_SSA_TRACE", "1"); + + // compiler_stageb.hako 全文をそのまま読み込んで MIR 化する + let src = include_str!("../../lang/src/compiler/entry/compiler_stageb.hako"); + let ast: ASTNode = NyashParser::parse_from_string(src).expect("parse compiler_stageb.hako ok"); + + let mut mc = MirCompiler::with_options(false); + let cr = mc.compile(ast).expect("compile compiler_stageb.hako ok"); + + // 必要に応じて StageBBodyExtractorBox.build_body_src/2 だけの MIR をダンプする。 + if std::env::var("NYASH_MIR_TEST_DUMP") + .ok() + .as_deref() == Some("1") + { + if let Some(func) = cr + .module + .functions + .get("StageBBodyExtractorBox.build_body_src/2") + { + let dump = MirPrinter::new().print_function(func); + eprintln!( + "----- MIR DUMP: StageBBodyExtractorBox.build_body_src/2 -----\n{}", + dump + ); + } + } + + let mut verifier = MirVerifier::new(); + if let Err(errors) = verifier.verify_module(&cr.module) { + for e in &errors { + eprintln!("[rust-mir-verify] {}", e); + } + panic!("MIR verification failed for compiler_stageb.hako (debug)"); + } +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 0e4e3ebf..bc46ac75 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -11,6 +11,7 @@ pub mod mir_stageb_loop_break_continue; pub mod mir_stage1_using_resolver_verify; pub mod mir_locals_ssa; pub mod mir_loopform_exit_phi; +pub mod mir_breakfinder_ssa; pub mod mir_vm_poc; pub mod nyash_abi_basic; pub mod plugin_hygiene; diff --git a/tools/stageb_loopssa_debug.sh b/tools/stageb_loopssa_debug.sh new file mode 100644 index 00000000..55d56dab --- /dev/null +++ b/tools/stageb_loopssa_debug.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# stageb_loopssa_debug.sh — Stage‑B LoopSSA 専用トレースプリセット +# +# 目的: +# - Stage‑B 最小ハーネス(tools/test_stageb_min.sh)を、 +# LoopSSA v2 / BreakFinderBox / PhiInjectorBox / LocalSSA / receiver 実体化の +# デバッグに適した ENV セットで実行するよ。 +# - 「いつも同じ環境変数を手で並べる」手間を減らし、ログの再現性を高める。 +# +# 実行内容: +# - HAKO_LOOPSSA_EXIT_PHI=1 : .hako LoopSSA Exit PHI を有効化 +# - HAKO_LOOPSSA_TRACE=1 : LoopSSA/BreakFinder/PhiInjector 専用トレース ON +# - HAKO_COMPILER_BUILDER_TRACE=0 : Stage‑B 本体の builder トレースは既定で OFF(必要なら手動で1にする) +# - NYASH_VM_TRACE=1 : Rust VM 実行トレース(Call/Branch 等) +# - NYASH_LOCAL_SSA_TRACE=1 : LocalSSA (recv/arg/cond) の Copy 発行トレース +# - NYASH_BUILDER_TRACE_RECV=1 : receiver 実体化(pin_to_slot/LocalSSA)のトレース +# ※ 既に値が設定されている場合はそちらを優先するよ。 +# +set -e + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +export HAKO_LOOPSSA_EXIT_PHI="${HAKO_LOOPSSA_EXIT_PHI:-1}" +export HAKO_LOOPSSA_TRACE="${HAKO_LOOPSSA_TRACE:-1}" +# Builder 側はデフォルト OFF。既に値があればそれを尊重する。 +export HAKO_COMPILER_BUILDER_TRACE="${HAKO_COMPILER_BUILDER_TRACE:-0}" +export NYASH_VM_TRACE="${NYASH_VM_TRACE:-1}" +export NYASH_LOCAL_SSA_TRACE="${NYASH_LOCAL_SSA_TRACE:-1}" +export NYASH_BUILDER_TRACE_RECV="${NYASH_BUILDER_TRACE_RECV:-1}" + +echo "=== Stage-B LoopSSA Debug Profile ===" +echo "HAKO_LOOPSSA_EXIT_PHI=${HAKO_LOOPSSA_EXIT_PHI}" +echo "HAKO_LOOPSSA_TRACE=${HAKO_LOOPSSA_TRACE}" +echo "HAKO_COMPILER_BUILDER_TRACE=${HAKO_COMPILER_BUILDER_TRACE}" +echo "NYASH_VM_TRACE=${NYASH_VM_TRACE}" +echo "NYASH_LOCAL_SSA_TRACE=${NYASH_LOCAL_SSA_TRACE}" +echo "NYASH_BUILDER_TRACE_RECV=${NYASH_BUILDER_TRACE_RECV}" +echo "" + +exec "${ROOT}/tools/test_stageb_min.sh" diff --git a/tools/test_loopssa_breakfinder_slot.sh b/tools/test_loopssa_breakfinder_slot.sh new file mode 100644 index 00000000..9a415f6b --- /dev/null +++ b/tools/test_loopssa_breakfinder_slot.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# test_loopssa_breakfinder_slot.sh — LoopSSA/BreakFinder 「最小失敗 JSON」スロットテスト +# +# 目的: +# - lang/src/compiler/tests/loopssa_breakfinder_slot.hako を直接 VM で実行し、 +# Program(JSON v0) スロットに貼り付けた JSON に対する LoopSSA.stabilize_merges +# の挙動を観測するよ。 +# - Stage‑B 最小サンプルから抽出した「失敗する JSON v0」をここに貼っておけば、 +# Stage‑B 全体を回さずに LoopSSA/BreakFinderBox 周辺だけを再現できる。 +# +set -e + +NYASH_BIN="${NYASH_BIN:-./target/release/hakorune}" +TEST_FILE="lang/src/compiler/tests/loopssa_breakfinder_slot.hako" + +echo "=== LoopSSA BreakFinder Slot JSON Test ===" +echo "Test file: $TEST_FILE" +echo "Binary: $NYASH_BIN" +echo "" + +# LoopSSA v2 の EXIT PHI を有効化して実行(BreakFinder/PhiInjector 経由) +HAKO_LOOPSSA_EXIT_PHI=1 \ +NYASH_DISABLE_PLUGINS=1 NYASH_PARSER_STAGE3=1 \ + "$NYASH_BIN" --backend vm "$TEST_FILE" 2>&1 + +echo "" +echo "=== Test complete ===" +