feat(loop-phi): Add body-local variable PHI generation for Rust AST loops

Phase 25.1c/k: Fix ValueId undefined errors in loops with body-local variables

**Problem:**
- FuncScannerBox.scan_all_boxes/1 and BreakFinderBox._find_loops/2 had ValueId
  undefined errors for variables declared inside loop bodies
- LoopFormBuilder only generated PHIs for preheader variables, missing body-locals
- Example: `local ch = s.substring(i, i+1)` inside loop → undefined on next iteration

**Solution:**
1. **Rust AST path** (src/mir/loop_builder.rs):
   - Detect body-local variables by comparing body_end_vars vs current_vars
   - Generate empty PHI nodes at loop header for body-local variables
   - Seal PHIs with latch + continue snapshot inputs after seal_phis()
   - Added HAKO_LOOP_PHI_TRACE=1 logging for debugging

2. **JSON v0 path** (already fixed in previous session):
   - src/runner/json_v0_bridge/lowering/loop_.rs handles body-locals
   - Uses same strategy but for JSON v0 bridge lowering

**Results:**
-  FuncScannerBox.scan_all_boxes: 41 body-local PHIs generated
-  Main.main (demo harness): 23 body-local PHIs generated
- ⚠️ Still some ValueId undefined errors remaining (exit PHI issue)

**Files changed:**
- src/mir/loop_builder.rs: body-local PHI generation logic
- lang/src/compiler/entry/func_scanner.hako: debug logging
- /tmp/stageb_funcscan_demo.hako: test harness

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-11-19 23:12:01 +09:00
parent fb256670a1
commit 525e59bc8d
35 changed files with 1795 additions and 607 deletions

View File

@ -451,6 +451,22 @@ Status: Step0〜3 実装済み・Step4Method/Extern実装フェーズ
- これにより StageB / Stage1 側で `_build_module_map()` のような「params: [] だが `me` を使う」メソッドでも、
Rust VM 実行時に `me` 未定義にならず、BoxCall が正しく解決されるようになった。
### IfForm / empty else-branch の SSA fixStage1 UsingResolverFull 対応)
- `src/mir/builder/if_form.rs`:
- `if cond { then }` のように else ブランチが省略されたケースでも、
- `else` の入口で pre_if の `variable_map` を使って PHI ノードを生成し、
- その結果の `variable_map` を `else_var_map_end_opt = Some(...)` として merge フェーズに渡すように修正した。
- 以前は empty else の場合に `else_var_map_end_opt` が `None` になり、`merge_modified_vars` が pre_if 時点の ValueId にフォールバックしていたため、
- merge ブロックで古い ValueIdPHI 適用前の値)を参照し、`Undefined value %0` などの SSA violation を引き起こしていた。
- 修正後は、then/else 両ブランチで「PHI 適用後の variable_map」が merge に渡されるため、
empty else でもヘッダ/merge の SSA が崩れない。
- 検証:
- `src/tests/mir_stage1_using_resolver_verify.rs::mir_stage1_using_resolver_full_collect_entries_verifies` が、
- `MirVerifier` 緑UndefinedValue/InvalidPhi なし)、
- `Stage1UsingResolverFull.main/0()` の merge ブロックで PHI 後の値(例: `%24`)を正しく参照していることを MIR dump で確認。
- Selfhost への移植指針Rust SSOT に沿った箱設計):
- `MethodCall`:
- Hako 側では「どの Box のどのメソッドを MIR の `mir_call(Method)` に落とすか」を Box 単位の helper で管理する(`LoopOptsBox` や `Cli*Box` と同様に)。

View File

@ -48,7 +48,7 @@ Status: planning構造整理フェーズ・挙動は変えない
が 1 関数に詰め込まれており、MIR 上でも巨大な `Main.main` になっている。
- 25.1c ではこれを「箱理論」に沿って分割する方針を立てており、Phase 25.1c 冒頭でまず StageB 側を 4 箱構造にリファクタした:
- `Main`(エントリ薄箱): `main(args){ return StageBDriverBox.main(args) }` のみを担当。
- `StageBDriverBox`(オーケストレーション): `StageBArgsBox.resolve_src``StageBBodyExtractorBox.build_body_src``ParserBox.parse_program2` → defs 挿入 → `print(ast_json)` だけを見る。
- `StageBDriverBox`(オーケストレーション): `StageBArgsBox.resolve_src``StageBBodyExtractorBox.build_body_src``ParserBox.parse_block2` → defs 挿入 → `print(ast_json)` だけを見る。
- `StageBArgsBox`CLI 引数と bundle/require の扱いだけを担当): もともとの「args/src/src_file/HAKO_SOURCE_FILE_CONTENT/return 0」ロジックを完全移動。
- `StageBBodyExtractorBox``body_src` 抽出ロジックbundle/using/trim を担当): もともとの `body_src` 抽出〜コメント削除〜BundleResolver/Stage1UsingResolverBox〜前後 trim までを丸ごとカプセル化。
- いずれもロジックはそのまま移動であり、コメント・using・ログを含めて挙動は完全に不変同じ Program(JSON v0)、同じログ、同じ `VM error: Invalid value`)であることを selfhost CLI サンプルで確認済み。エラーの発生箇所は `Main.main` から `StageBArgsBox.resolve_src/1` に関数名だけ変わっており、SSA/Loop 側の根本修正はこの後のタスクLoopBuilder / LocalSSA 整理)で扱う。
@ -92,11 +92,11 @@ Status: planning構造整理フェーズ・挙動は変えない
- 代表 canary:
- `tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.sh`
- `tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh`
- まずは `compiler_stageb.hako` の流れを箱ごとに分解してログする:
- `StageBArgsBox.resolve_src`
- `StageBBodyExtractorBox.build_body_src`
- `ParserBox.parse_program2`
- `FuncScannerBox.scan_all_boxes`
- まずは `compiler_stageb.hako` の流れを箱ごとに分解してログする:
- `StageBArgsBox.resolve_src`
- `StageBBodyExtractorBox.build_body_src`
- `ParserBox.parse_program2` / `ParserBox.parse_block2`
- `StageBFuncScannerBox.scan_all_boxes`Phase 25.1c 時点では StageB ローカル実装)
- 各箱の入口/出口に `[stageb/trace:<box>.<method>:enter|leave]` のような軽いタグを置き、どの箱が rc=1 の直前で止まっているかを特定する(挙動は変えない)。
- 2) Rust Region レイヤを「正解ビュー」として .hako 側を寄せる

View File

@ -63,6 +63,28 @@ Status: in progress.hako 側 LoopSSA v2 本体実装Rust 側は既存 SSA/
- ゴール:
- LoopSSA 本来そのループに属さない block body に含めないようにするValueId(50) のような飛び火を防ぐ)。
### KB2: BreakFinderBox._find_loops/2 ループ構造の整理region box 化の一歩)
- 背景:
- `BreakFinderBox._find_loops/2` のループ内で `header_pos` / `header_id` / `exit_pos` / `exit_id` を扱う際
`header_id == null` `exit_pos` 異常系で `continue` を多用していたため
LoopForm v2 / LoopSSA 側から見るとループ本体が細かい early-continue に分割された形になっていた
- StageB Test2 ではこの経路で `header_pos` `i` まわりの ValueId が観測しづらくなり
SSA バグ調査の足場としても扱いづらかった
- 対応:
- `lang/src/compiler/builder/ssa/exit_phi/break_finder.hako::_find_loops` をリファクタし
`next_i` ローカルを導入して1 イテレーション中のすべての分岐が最後に `i = next_i` に合流する形に整理した
- `header_id == null` / `exit_pos < 0` / 範囲外 / `exit_id == null` の各ケースは
`next_i` の更新だけで表現し`continue` を使わない構造に変更
- 正常系`header_id` / `exit_pos` / `exit_id` が全て有効の場合のみ
`body_blocks` / `ControlFormBox` / `loop_info` の構築と `loops.push(loop_info)` を行い
そのうえで `next_i = exit_pos + 12` を設定
- これにより:
- LoopForm v2 から見るとループ本体が単一の region`next_i` による合流)」として観測できるようになり
break/continue 経路の SSA 構造が単純化された
- ループの意味論自体は従来どおりヘッダ/出口検出ロジックや body_blocks の定義は不変
既存の JSON v0 / LoopSSA の挙動には影響しない
### KC: PhiInjectorBox の v2 への一歩Carrier/Pinned の入口だけ作る)
- 目的:

View File

@ -150,6 +150,24 @@ _build_module_map() {
static box メソッドに対しても暗黙 receiver をパラメータとして扱うべきかどうかを設計レベルで判断する。
- [ ] 必要であれば、別フェーズ(例: 25.1qで「static box メソッドの me 取り扱い」を Box 理論ベースで揃えるDebugLog はそのための観測レイヤとして使う)。
### 6. Static box / me セマンティクス統一(部分完了メモ)
- 25.1c/25.1m までに判明したこと:
- `static box StringHelpers` のような「純粋ユーティリティ箱」で、`me.starts_with(src, i, kw)` のように
同一箱内ヘルパーを receiver 経由で呼ぶと、Stage3 降下で引数ずれ(`i` にソース全文が入る)が発生しうる。
- 実際、`StringHelpers.starts_with_kw/3` → `StringHelpers.starts_with/3` 経路で
`StringHelpers.starts_with("StringHelpers", src, i, kw)` のような形になり、
`i + m > n` が `String > Integer(13)` の比較に化けていた。
- 25.1m での暫定対応(完了済み):
- `StringHelpers.starts_with_kw` 内を `me.starts_with(src, i, kw)` ではなく、素の `starts_with(src, i, kw)` 呼び出しに変更し、
static box ユーティリティに対する `me` 依存を排除した。
- これにより、`starts_with` 内の比較は全て整数同士となり、`String > Integer` 型エラーは解消済み。
- 25.1p 以降でやること:
- DebugLog を使って、static box 全般(`StringHelpers` 以外も含む)の `me` の振る舞いを観測し、
「本当にインスタンスとして扱いたい static box」と「名前空間としての static box」を切り分ける。
- 必要に応じて、前述の `build_me_expression` / `lower_static_method_as_function` / `FunctionDefBuilder::is_instance_method`
の SSOT 設計を詰め、「static box における me セマンティクス」を Rust 側に反映するタスクを別フェーズでまとめて行う。
## いつやるか(優先度メモ)
- 今回は **フォルダ+設計メモだけ** で、実装はまだ行わない。

View File

@ -0,0 +1,92 @@
# Phase 25.1q — LoopForm Front Unification (AST / JSON v0)
Status: planning-onlyPhase 25.1 ラインの安定化が終わったあとに着手)
## ゴール
- ループ lowering / PHI 構築の **SSOT を `phi_core + LoopFormBuilder` に完全に寄せる**
- Rust AST → MIR`MirBuilder` / `LoopBuilder`)と JSON v0 → MIR`json_v0_bridge::lower_loop_stmt`)のフロントを整理し、
「どこを直せば loop/PHI の意味論が変わるか」を 1 箇所に明示する。
- 実装者・LLM の混乱ポイントを減らす:
- StageB / FuncScanner のようなループバグ調査時に、「Rust AST 側を触るべきか」「JSON v0 側を触るべきか」で迷わない構造にする。
- 今回のように `loop_.rs` 側だけにログ・修正を入れてしまう誤りを防ぐ。
## 現状25.1m / 25.1p 時点の構造)
- **バックエンドSSOT**
- `src/mir/phi_core/loop_phi.rs` / `src/mir/phi_core/loopform_builder.rs`
- LoopForm v2 / LoopSSA v2 の本体。
- `LoopFormBuilder` + `LoopFormOps` として、ヘッダ PHI / exit PHI / continue スナップショットなどを一元的に扱う。
- **Rust AST → MIR 経路**
- `ASTNode::Loop`
- `src/mir/builder_modularized/control_flow.rs::build_loop_statement`
- `src/mir/builder/control_flow.rs::cf_loop`
- `src/mir/loop_builder.rs::LoopBuilder::build_loop_with_loopform`
- こちらはすでに LoopForm v2 / ControlForm v2 に統一済みで、「Rust パーサで読んだ .hako」を MIR に落とす主経路。
- **JSON v0 → MIR 経路**
- `Program(JSON v0)``ProgramV0`:
- `src/runner/json_v0_bridge/lowering.rs::lower_stmt_with_vars`
- `StmtV0::Loop { .. } => loop_::lower_loop_stmt(...)`
- `src/runner/json_v0_bridge/lowering/loop_.rs::lower_loop_stmt`
- ここも LoopForm v2 / phi_core を呼ぶ構造にはなっているが、
- ファイルが AST ルートとは別に分かれている
- 追加ログや一時的なデバッグコードが入りやすく、「どの経路でループが下りているか」分かりづらい状態になりがち。
- 結果として:
- StageB / FuncScannerBox のような「Rust AST 経路」を見たいときに、誤って `loop_.rs` 側だけを触る、といった混乱が起きやすい。
- 一方で JSON v0 経路は provider (`env.mirbuilder.emit` / `--program-json-to-mir`) で重要なので、急に削除はできない。
## スコープ25.1q でやること)
1. **LoopForm / phi_core を SSOT として明文化(ドキュメント整理)**
- `docs/development/roadmap/phases/phase-25.1b/` / `phase-25.1m/` / 本 `phase-25.1q` で:
- ループ意味論preheader/header/body/latch/exit、continue/break スナップショット、PHIの SSOT を
`phi_core::loop_phi` / `LoopFormBuilder` に一本化すると明言する。
- `LoopBuilder`Rust AST フロント)と `json_v0_bridge::lower_loop_stmt`JSON フロント)は「薄いアダプタ」に留める方針を書いておく。
2. **json_v0_bridge::lower_loop_stmt の責務縮小(薄いフロント化)**
- 目標: `loop_.rs` は「JSON から LoopForm に渡すための最低限の橋渡し」に限定する。
- 具体案:
- 余計なデバッグログや独自判定を段階的に削り、やることを
- preheader/header/body/latch/exit のブロック ID を用意する
- ループ開始時点の `vars` を LoopPhiOps 実装に渡す
- break / continue のスナップショット記録を呼び出す
に絞る。
- ループ構造・PHI の仕様変更は **phi_core 側だけ** に集約し、`loop_.rs` 側には分岐や条件を増やさない。
3. **ログ・デバッグ経路の整理**
- `HAKO_LOOP_PHI_TRACE` / `NYASH_LOOPFORM_DEBUG` などのトグルについて:
- どのフロントRust AST / JSONからでも同じタグで観測できるようにし、ログの出し場所を整理する。
- `loop_.rs` に残っている「一時的な ALWAYS LOG」などはすでに削除済みだが、今後も dev トレースは必ず env ガード越しに行う。
4. **JSON v0 → AST → MirBuilder 統合の検討(設計レベルのみ)**
- 将来案として:
- `ProgramV0` を一度 Nyash AST 相当の構造体に変換し、`MirBuilder``build_loop_statement` を再利用する形に寄せる。
- これが実現すると、`loop_.rs` 自体を削除しても LoopForm/PHI の意味論は完全に一箇所LoopBuilder + phi_coreに集約される。
- 25.1q ではここまでは踏み込まず、「やるならどのフェーズで、どの単位の差分にするか」を設計メモとして残す。
## スコープ外25.1q ではやらないこと)
- ループ意味論そのものの変更:
- `loop(cond){...}` の評価順序や break/continue の意味論を変えない。
- StageB / Stage1 / 自己ホストルートで既に green な LoopForm/SSA テストの挙動は不変とする。
- 新しいループ構文・最適化の追加:
- `while` / `for` / range loop など、新構文の導入は別フェーズ(言語拡張側)に任せる。
- JSON v0 スキーマの変更:
- `StmtV0::Loop` などの JSON 形は既存のままschema v0/v1 は維持)。
## 他フェーズとの関係
- 25.1mStatic Method / LoopForm v2 continue + PHI Fix:
- ここで LoopForm v2 / continue + header PHI は Rust AST 経路でほぼ安定している。
- 25.1q では、その成果を JSON v0 経路にも構造的に反映し、「LoopForm v2 がどこから使われているか」を明示する役割を担う。
- 25.1pMIR DebugLog 命令):
- DebugLog を使って LoopForm/PHI の ValueId を観測しやすくすることで、25.1q での統一作業時に「AST ルートと JSON ルートの差」を追いやすくする。
- 25.1q は DebugLog 基盤が整っていることを前提に、小さな JSON v0 → MIR のテストケースで CFG/PHI を比較するフェーズとする。
- 25.2Numeric Microbench / EXE Tuning:
- JSON v0 → MIR → EXE 経路は numeric_core / AotPrep と強く結びついているため、25.1q で LoopForm front を整理しておくと、25.2 でのパフォーマンス解析やバグ調査がやりやすくなる。***