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>
12 KiB
12 KiB
Phase 25.1c — Env / Extern / BoxIntrospect Structural Cleanup
Status: planning(構造整理フェーズ・挙動は変えない)
ゴール
env.*/hostbridge.*/env.box_introspect.*の責務と経路を整理し、型システムまわりの「正しい入口」を 1 箇所に揃える。- Box 型情報 API(
env.box_introspect.kind+BoxTypeInspectorBox)を コア型システムとして扱えるようにする(plugins の有無に依存しない)。 - i64 / MapBox / ArrayBox の unwrap ロジックを SSOT に寄せ、MirBuilder / JsonEmit / LoopOpts / BoxHelpers が同じ前提で動くようにする。
スコープ(何をここで扱うか)
- 対象:
- Rust 側:
extern_registry.rs/handlers/externals.rs/handlers/extern_provider.rs/runtime/plugin_loader_v2/* - Hako 側:
BoxTypeInspectorBox/BoxHelpers/JsonEmitBox/MirSchemaBox/LoopOptsBox - ドキュメント:
docs/specs(env externs / box_introspect / numeric view の設計メモ)
- Rust 側:
- 非対象:
- 新しい言語機能や VM 命令の追加(Phase 25 ポリシーに従い、仕様拡張はしない)。
- MirBuilder の意味論変更(multi‑carrier や LoopForm の設計は Phase 25.1b の範囲に留める)。
やりたい整理(タスクリスト)
-
env. extern の SSOT を決める*
env.get/env.mirbuilder.emit/env.codegen.emit_object/env.codegen.link_object/env.box_introspect.kindを一覧化し、仕様(引数・戻り値・MIR 形)をdocs/specs/env_externs.md(仮)に明文化する。- JSON v0 → MIR ブリッジ(
MapVars::resolve/ lowering)で、上記が必ずExternCall("env.*", ..)に落ちることを確認・修正する。
-
hostbridge.extern_invoke を「互換レイヤ」に押し込める
- 方針: 「
env.*で表現できるものは ExternCall を正義とし、hostbridge.extern_invokeは互換用ラッパに限定する」。
- 方針: 「
- Hako 側:
hostbridge.extern_invoke("env.*", ..)は内部でenv.*を呼ぶだけにする(新規コードは直接env.*を使う)。 - Rust 側:
"hostbridge.extern_invoke"の実装は、extern_provider_dispatch("env.*", ..)に委譲する薄いブリッジに整理する。
-
BoxIntrospect をコア型システムに昇格させる
env.box_introspect.kindの実装を plugin loader v2 直下ではなく、コア runtime(例:runtime/box_introspect.rs)に寄せる。- コア型(MapBox / ArrayBox / StringBox / IntegerBox / BoolBox / NullBox)は runtime 側で
build_box_infoを定義し、plugin loader は「ユーザー Box の拡張」だけを担当する。 BoxTypeInspectorBoxはenv.box_introspect.kind(value)を唯一の情報源として扱い、repr ベースの fallback は「plugins も env.* も使えないデバッグ環境のみ」で使うことをコメントで明示する。
-
Numeric view(i64 unwrap)の SSOT 化
- Hako 側:
string_helpers/BoxHelpers/MirSchemaBox/JsonEmitBox/LoopOptsBoxに散っている i64 unwrap ロジックを、小さなユーティリティ(仮:box_numeric_view.hako)に寄せる。 - Rust 側:
NyashBoxから i64 を取り出すas_i64的な関数を 1 箇所に置き、extern / BoxIntrospect 経路からはそれを使う。
-
Stage‑B Main を箱に分割して SSA/デバッグを軽くする
- 現状の
compiler_stageb.hako: Main.mainは:- CLI 引数パース (
--source/--bundle-*/--require-mod) - bundle/require 解決 (
BundleResolver) - body 抽出 (
body_srcの抽出ロジック) - ParserBox 呼び出し (
parse_program2→ emit JSON) - defs スキャン (
FuncScannerBox.scan_all_boxes) が 1 関数に詰め込まれており、MIR 上でも巨大なMain.mainになっている。
- CLI 引数パース (
- 25.1c ではこれを「箱理論」に沿って分割する方針を立てており、Phase 25.1c 冒頭でまず Stage‑B 側を 4 箱構造にリファクタした:
Main(エントリ薄箱):main(args){ return StageBDriverBox.main(args) }のみを担当。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 整理)で扱う。
- 現状の
-
LoopBuilder / pin スロットの型付け・箱化
- いまの LoopBuilder は
__pin$*$@recvのような文字列ベースの「内部変数名」をvariable_mapに直接突っ込んで、SSA/phi/pin を管理している。 - 25.1c では、Loop 状態を「箱」として切り出して型付けする:
- 例:
LoopStateBox(Rust 側構造体)にrecv_slots(Method receiver 用)index_slots(ループカウンタ用)limit_slots(limit/上限 expr 用) を明示的に持たせる。
- 例:
LoopBuilder::emit_phi_at_block_start/update_variableは、この LoopStateBox を通じてのみ pin/phi を操作し、「recv に Null/未定義が混ざらない」ことを構造レベルで保証する。
- いまの LoopBuilder は
-
ビルダー観測用の専用レイヤ(デバッグ箱)
- すでに
NYASH_BUILDER_TRACE_RECV/NYASH_BUILDER_DEBUGなどで ad-hoc に eprintln を入れているが、出力箇所が複数ファイルに散っていて再利用しにくい。 - 25.1c ではこれを
builder.observe的なモジュール(箱)に集約する:- 例:
observe::recv::log(fn, bb, name, src, dst)、observe::phi::log(fn, bb, dst, inputs)など。
- 例:
- ポリシー:
- すべて dev トグル(NYASH_BUILDER_TRACE_*)越しに呼ぶ。
- 本番挙動は変えず、「どこをどうトレースできるか」を構造として明示する。
- すでに
-
Stage‑B 向けの極小 MIR 再現ハーネス
docs/private/roadmap/phases/phase-20.33/DEBUG.mdにあるような Stage‑B 向けメモを踏まえ、Stage‑B/MirBuilder 用の「極小 Hako → MIR テスト」を 1 つ用意する。- 例:
- 100〜200 行程度の
.hakoをlang/src/compiler/tests/stageb_min_sample.hakoのようなファイルに固定。 - Rust 側で MirBuilder に直接その AST を食わせて MIR を生成し、
NYASH_VM_VERIFY_MIR=1で「Undefined value」が出ないことを確認するユニット/スモークを足す(構造バグ検知用)。
- 100〜200 行程度の
- これにより、Stage‑B/LoopBuilder に関する修正が
.hako本番コード全体に依存せず、小さな再現ケースで検証できるようにする。
進め方メモ
- 先にドキュメントを書く(env extern / BoxIntrospect / numeric view の仕様を
docs/specs配下に整理)→ そのあとで Bridge / VM / Hako を小さく揃える。 - 既存フェーズとの関係:
- Phase 25.1b: selfhost builder / multi‑carrier / BoxTypeInspector 実装フェーズ(機能側)。
- Phase 25.1c: そのうち「env.* / hostbridge.* / BoxIntrospect」に加えて、Stage‑B Main / LoopBuilder / builder 観測レイヤの構造と責務も整理するメタフェーズ(構造側)。
- 挙動を変えないこと(Fail‑Fast / default path は現状維持)を前提に、小さな差分で進める。
デバッグ方針(25.1c で踏みたい順番)
-
- Stage‑B rc=1 の発生箇所を箱単位まで特定する
- 代表 canary:
tools/smokes/v2/profiles/quick/core/phase251/stageb_fib_program_defs_canary_vm.shtools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh- まずは
compiler_stageb.hakoの流れを箱ごとに分解してログする: StageBArgsBox.resolve_srcStageBBodyExtractorBox.build_body_srcParserBox.parse_program2/ParserBox.parse_block2StageBFuncScannerBox.scan_all_boxes(Phase 25.1c 時点では Stage‑B ローカル実装)
- 各箱の入口/出口に
[stageb/trace:<box>.<method>:enter|leave]のような軽いタグを置き、どの箱が rc=1 の直前で止まっているかを特定する(挙動は変えない)。
-
- Rust Region レイヤを「正解ビュー」として .hako 側を寄せる
- Rust 側にはすでに
Region / RefSlotKind / FunctionSlotRegistry + ControlFormがあり、StageBBodyExtractorBox.*周辺のスロット(src/body_src/bundle_*など)がどの Loop/If Region に属しているかをNYASH_REGION_TRACE=1で観測できる。 - 25.1c では .hako 側に観測専用 Box(案:
StageBRegionObserverBox)を追加し、enter_region(kind, name, slots_json)leave_region()のような API で「箱名・構造名・スロット名の集合」だけを JSON で print する。
StageBBodyExtractorBoxの外側ループ・内側 if など、問題になりやすい箇所でこの Box を呼び出し、Rust の[region/observe]ログと「木構造+スロット名」が対応しているかを確認する。
→ ずれている Region(例:body_srcなどが早期に欠落するスコープ)から優先的に修正する。
-
- Stage‑B 用の極小ハーネスを Rust / .hako 両方に用意する
- fib canary はやや重いため、100〜200 行程度の「using + 1 box + 1 loop」だけの最小サンプルを
lang/src/compiler/tests/stageb_min_sample.hakoのようなファイルとして固定する。 - Rust 側:
- そのサンプルを AST→MIR に通し、
NYASH_VM_VERIFY_MIR=1でUndefined valueが出ないことを確認する小さなテストを用意(既存の Stage‑B 向け MIR テスト群に揃える)。
- そのサンプルを AST→MIR に通し、
- .hako 側:
- 同じサンプルを入力として
StageBDriverBoxの簡易版(mini driver)を作り、StageBArgsBox.resolve_src→StageBBodyExtractorBox.build_body_src→ParserBox.parse_program2だけを通す driver を追加する。 - これにより、Stage‑B/LoopBuilder に対する修正を「本番 compiler_stageb.hako 全体」ではなく「ミニマムな Hako 断片」で検証できるようにする。
- 同じサンプルを入力として
-
- Stage‑1 CLI (
HakoCli.run) の selfhost ラインは Stage‑B が緑になってから扱う
selfhost_cli_run_basic_vm.shの現状の失敗は、Stage‑B が rc=1 で Program(JSON) を 1 行も返していないことが原因であり、HakoCli.run の MIR 生成まで到達していない。- 25.1c ではまず Stage‑B 側(fib defs / stageb_min / mini driver)を rc=0 に戻し、
その Program(JSON v0) を固定入力にして Phase 25.1b 側の selfhost builder / HakoCli.run MIR を Rust ラインと diff する、という順番で進める。
- Stage‑1 CLI (