Files
hakorune/docs/development/roadmap/phases/phase-25.1b/README.md
nyash-codex 9bdf2ff069 chore: Phase 25.2関連ドキュメント更新&レガシーテストアーカイブ整理
## ドキュメント更新
- CURRENT_TASK.md: Phase 25.2完了記録
- phase-25.1b/e/q/25.2 README更新
- json_v0_bridge/README.md新規追加

## テストファイル整理
- vtable_*テストをtests/archive/に移動(6ファイル)
- json_program_loop.rsテスト追加

## コード整理
- プラグイン(egui/python-compiler)微修正
- benchmarks.rs, instance_v2.rs更新
- MIR関連ファイル微調整

## 全体成果
Phase 25.2完了により:
- LoopSnapshotMergeBox統一管理実装
- ValueId(1283)バグ根本解決
- ~35行コード削減(目標210行の16%)
- 11テスト全部PASS、3実行テストケースPASS
2025-11-20 03:56:12 +09:00

701 lines
71 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 25.1b — Selfhost Builder Parity (Planning → Design DeepDive)
Status: Step0〜3 実装済み・Step4Method/Extern実装フェーズ
## ゴール
- Rust 側 Program→MIR (`env.mirbuilder.emit`) と Hakorune 側 selfhost builder (`MirBuilderBox.emit_from_program_json_v0`) の機能差を埋め、Stage1 CLIlauncher.hakoレベルの Program(JSON) を selfhost builder 単独で lowering できるようにする。
- `.hako → Program(JSON v0) → MIR(JSON)` のうち、「Program→MIR」を selfhost builder だけでも成立させ、provider 経路はあくまで退避路に下げていく。
- 最終的には `HAKO_SELFHOST_BUILDER_FIRST=1` を既定に戻し、Stage1 CLI EXE の I/OJSON stdout + exit codeを Rust/llvmlite と同じ契約に揃える。
## 現状Phase 25.1a 時点)
### StageBProgram(JSON v0) emit
- `compiler_stageb.hako``defs` を含む Program(JSON v0) を出力できる:
- `HakoCli.run` / `HakoCli.cmd_emit_*` / `HakoCli.cmd_build_*` などのメソッドを `Program.defs` 配列として含む。
- `FuncScannerBox` `HAKO_STAGEB_FUNC_SCAN=1` により、`static box` メソッド(暗黙の `me` 引数付き)も defs に載る。
- using 解決:
- `Stage1UsingResolverBox``lang/src/compiler/entry/using_resolver_box.hako` `HAKO_STAGEB_MODULES_LIST``nyash.toml``[modules]` を参照し、`using lang.mir.builder.MirBuilderBox` 等をファイル結合前に解決。
- StageB entry 側は string literal using を廃止し、`using lang.compiler.entry.using_resolver as Stage1UsingResolverBox` のように module alias を使用する。
#### StageB func_scan トグルのデフォルトHAKO_STAGEB_FUNC_SCAN
- 目的:
- StageB を直接叩いたときに `HAKO_STAGEB_FUNC_SCAN` を立て忘れても、`HakoCli.run``TestBox.fib` のようなメソッド定義が `Program.defs` にきちんと入るようにするselfhost builder / FuncLowering 側の前提を崩さない)。
- 実装compiler_stageb.hako 内):
- 以前: `HAKO_STAGEB_FUNC_SCAN=1` のときだけ `FuncScannerBox.scan_all_boxes` を呼び出し、それ以外は defs を生成しなかった。
- 現在: `HAKO_STAGEB_FUNC_SCAN` が未設定 (`null`) のときは既定で ON とみなし、**明示的に `"0"` が入っているときだけ OFF** として扱う。
- これにより、`tools/hakorune_emit_mir.sh` や v2 スモーク以外から StageB を直接呼び出しても、defs が常に生成される。
- 既存のテストで func_scan を無効化したいケースでは、`HAKO_STAGEB_FUNC_SCAN=0` を明示すれば従来どおり defs をスキップできる。
#### StageB の安定度と使用上の注意
- 正規経路:
- StageB は `tools/hakorune_emit_mir.sh` / `tools/selfhost/selfhost_build.sh` 経由で呼び出すことを前提としており、これらのラッパが Stage3 用 ENV`NYASH_PARSER_STAGE3=1` / `HAKO_PARSER_STAGE3=1` / `NYASH_PARSER_ALLOW_SEMICOLON=1` など)を一括でセットする。
- Phase 25.1b では「multi-carrier fib などの core 小ケースについては、このラッパ経由で呼ぶ限り StageB 自体は十分に安定」とみなし、主な改善対象を Program→MIR の selfhost builder 側に置く。
- 手動実行時の注意:
- Stage3 ENV を立てずに StageB / VM を直接叩くと、`Undefined variable: local` のようなエラーが発生するが、これは構文/実装バグではなく「Stage3 キーワードlocal など)を Stage1 と同じルールでパースしてしまっている」ため。
- 詳細な原因と対処は `docs/development/troubleshooting/stage3-local-keyword-guide.md` にまとめてあり、selfhost 開発では「まずラッパスクリプトを使う → 必要な場合のみ ENV を明示して直叩きする」方針とする。
#### StageB と selfhost CLI canaryHakoCli.run/2の現状
- selfhost CLI の最小ケース(`tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh` が生成する HakoCli.run サンプル)に対しては、修正前は StageB 実行中に VM エラー:
- `❌ VM error: Invalid value: use of undefined value ValueId(N)`%0 / 97842 / 22 などが発生し、Program(JSON v0) が 1 行としては出力されなかった(`tools/hakorune_emit_mir.sh` が Program 抽出に失敗する)。
- `NYASH_VM_VERIFY_MIR=1` を立てて `lang/src/compiler/entry/compiler_stageb.hako` を直接叩くと、修正前は StageB が生成した MIR に対して:
- `Stage1UsingResolverBox._collect_using_entries/1`
- `ParserStringUtilsBox.skip_ws/2`
- `ParserIdentScanBox.scan_ident/2`
- `ParserBox.parse_stmt2/2`
- などに `Undefined value %0 used in block ...` が多数報告されていた(詳細は `docs/private/roadmap/phases/phase-20.33/DEBUG.md` の「Invalid value: use of undefined value ValueId(N)」節を参照)。
- Task先生による Rust MIR builder 側の修正ValueId 割り当て統一loop PHI/pinned 変数の扱い修正)後は:
- `%0` / 97842 / 22 に起因する Undefined value / Invalid value エラーは `NYASH_VM_VERIFY_MIR=1` / 実行時ともに解消済み。
- pinned 変数(`__pin$*@recv` など)も loop header/exit で正しく PHI に乗るようになり、ループ後のメソッドレシーバーが未定義になる問題も再現しなくなった。
- 現時点で selfhost CLI サンプルに対して残っている課題は:
- 1) Rust VM 実行時に `❌ VM error: Invalid value: use of undefined value ValueId(17)` が発生しており、拡張済みエラーメッセージ(`fn=Main.main, last_block=Some(BasicBlockId(3419)), last_inst=Some(Call { ... ParserBox.length() [recv: %17] ... })`から「StageB Main.main 内の `ParserBox.length()` 呼び出しにおいて recv スロットValueId(17)) が定義されていない」ことが分かっている。これは verifier がまだチェックしていない「Callee.receiver 側の SSA 漏れ」であり、Phase 25.1c の StageB / LoopBuilder / LocalSSA 整理タスクで修正する前提。
- 2) `if args { ... }` まわりの truthy 判定ArrayBox を boolean 条件に使っている部分)の扱いに起因する型/意味論の揺れが残っており、こちらも 25.1c の型システム整理タスクで `ArrayBox` の truthy 規約を明文化した上で揃える想定。
- 3) `NewBox HakoCli` が plugin 前提で解決されてしまう問題は、VM 側の static box factory 統合(静的 Box を User factory にも広告する)により解消済みであり、`NYASH_DISABLE_PLUGINS=1` でも静的 Box として HakoCli を生成できるようになっているselfhost CLI canary では NewBox 自体はもはやブロッカーではない)。
- 対応方針Phase 25.1b 時点):
- BoxTypeInspector / multicarrier LoopForm 経路とは独立した **StageB/MIR 側の SSA型システムBox 解決の構造問題** として扱い、selfhost CLI canaryHakoCli.run/2 loweringはこれらが片付くまでは「25.1c の構造タスク待ち」として扱う。
- `tools/hakorune_emit_mir.sh``diagnose_stageb_failure()` は維持し、StageB の標準出力に `Invalid value: use of undefined value` が含まれている場合には `NYASH_VM_VERIFY_MIR=1``compiler_stageb.hako` 直叩き、および `docs/private/roadmap/phases/phase-20.33/DEBUG.md` への導線を表示する。加えて、VM 側の `MirInterpreter::reg_load``fn` / `last_block` / `last_inst` を含めてエラー文字列を出すようになったため、StageB 由来の undefined value は「どの関数のどの Callどの recv」で発生しているかを 1 行で特定できる。
### Rust provider (`env.mirbuilder.emit`)
- `program_json_to_mir_json_with_imports`:
- `Program.body``Program.defs` の両方を受理し、`FuncDefV0` から `func_map` を構築して Call を解決。
- `HAKO_MIRBUILDER_IMPORTS` 経由で `MirBuilderBox` / `BuildBox` などの static box 名をインポートし、`Const(String(alias))` として扱う。
- JSON v0 ブリッジ:
- `args` を暗黙パラメータとして扱う修正済み(`undefined variable: args` は解消)。
- `hostbridge` は wellknown 変数として `Const(String("hostbridge"))` を生成し、`hostbridge.extern_invoke(...)` を含む Program(JSON) でも undefined にならない。
- 結果:
- `launcher.hako` に対して ~62KB の MIR(JSON) を安定して生成できており、Phase 25.1a では provider 経路が事実上のメインルート。
### selfhost builder (`MirBuilderBox.emit_from_program_json_v0`)
- エントリ:
- 入口で `HAKO_MIR_BUILDER_FUNCS=1` のときに `FuncLoweringBox.lower_func_defs(program_json, program_json)` を呼び出し、defs 専用の MIR 関数群を文字列として受け取る(`func_defs_mir`)。
- その後、`internal` 配下の多数の `Lower*Box.try_lower` を順番に適用し、Program 全体を 1 関数(`main`)ベースの MIR(JSON) に落とす。
- `FuncLoweringBox` の現状:
- `lower_func_defs` は Program(JSON) から `defs` 配列をパースし、各 def ごとに `_lower_func_body` を呼ぶ。
- `_lower_func_body` がサポートするのは **単一の Return を持つ最小パターンのみ**:
- `Return(Int)`
- `Return(Binary(op, lhs, rhs))``+,-,*,/` のみ、かつ `Int/Var` 組み合わせ限定)
- `Return(Call(name, args))`Call 名は `func_map` を用いて `Box.method` に解決)
- 複数ステートメント、`If``Loop`、メソッドチェインなど Stage1 CLI に実際に現れる構造はすべて `null` でスキップされる。
- `MirBuilderBox` の挙動:
- 何らかの `Lower*Box` が Program 全体にマッチした場合は、その MIR(JSON) に対して `_norm_if_apply` を通し、`FuncLoweringBox.inject_funcs` で defs 分の MIR 関数を **追加注入** する。
- どの `Lower*Box` もマッチしないが `func_defs_mir` が非空の場合は、`"{\"functions\":[" + defs + "]}"` という最小モジュールを組み立てて `_norm_if_apply` に渡す。
- このケースでは main 関数を含まない defs だけの MIR モジュールになり、ny-llvmc 側でエントリポイント不足や空挙動 EXE を生む原因になる。
- `func_defs_mir` も空で internal lowers も不発の場合は `null` を返し、最後に provider delegate`env.mirbuilder.emit`)へフォールバックする。
- 現時点での不足点要約・2025-11-16:
- `FuncBodyBasicLowerBox` が本番でカバーできているのは、Local/If/Return の基本形LoopForm 正規化済み Loopごく一部の MethodCall`args.size/get``String.length` の Return 形に限られる。Stage1 CLI のような複雑な関数本体(複数 LocalIf ネストLoopMethod/Extern 混在)は、ほぼすべて Rust provider 経路にフォールバックしている。
- ExternCall は `hostbridge.extern_invoke("env.codegen","emit_object"|"link_object", arg)``ExternCallLowerBox` で最小サポートしているだけで、それ以外の extern`env.mirbuilder.emit` や console 系など)は現状 selfhost builder の対象外。
- `HakoCli.run` 専用 lower`CliRunLowerBox`)は MVP 用のシンプルな run パターンのみを想定しており、実際の `launcher.hako` の runusage/unknown ログ付き)は shape mismatch で selfhost 降ろしの対象外。ここを Stage1 実形に合わせて広げることが Phase 25.1b の中心タスクになっている。
### Stage1 EXE / build_stage1.sh の現状
- `tools/selfhost/build_stage1.sh`:
- 既定値 `HAKO_SELFHOST_BUILDER_FIRST=0`provider-firstでは、Stage1 EXE ビルドは成功し、`emit program-json` / `emit mir-json` / `build exe` の I/O も Rust/llvmlite と同等の JSON+exit code 契約を満たす。
- `HAKO_SELFHOST_BUILDER_FIRST=1`selfhost-firstでは、Stage1 CLI のような複雑な Program(JSON) に対して selfhost builder が「defs のみ」MIR か mini stub MIR を返し、結果として「Result: 0 だけ出す空 EXE」になる。
- スモーク:
- `tools/smokes/v2/profiles/quick/core/phase251/stage1_launcher_program_to_mir_canary_vm.sh` は provider-first ルートのみをカバーしており、selfhost builder 単独経路のギャップは検出できていない(今後 canary を追加する必要がある)。
## 25.1b のスコープ(案)
- selfhost builder 本体Hakorune 側):
- `Program.defs` の処理を実装し、`box + method` ごとに MIR 関数を生成する。
- 例: `HakoCli.run` / `HakoCli.cmd_emit_program_json` / `HakoCli.cmd_emit_mir_json` / `HakoCli.cmd_build_exe` 等。
- `call` / `BoxCall` / `ExternCall` の解決(`func_lowering` + call resolve 相当)を Hakorune 側にも実装し、`Call("cmd_emit_mir_json")``Global` callee に解決できるようにする。
- Loop / branch / compare / Array/Map 操作など Stage1 CLI で出現する構造を包括的に lowering するため、`lang/src/mir/builder/internal/*` の helper を本番経路に統合する。
- JSON 出力:
- 現状の `"{\"functions\":[...]}\"` ベタ書きから、jsonfrag 等を用いた構造的出力に切り替え、複数関数を同一モジュールに含められるようにする。
- 既存の mini パターン用 JSON 組み立てとの互換性を維持しつつ、Stage1 CLI で必要な関数数に耐えられる形に拡張する。
- 運用ポリシー:
- Phase 25.1b 中は `HAKO_SELFHOST_BUILDER_FIRST=0` のままprovider-firstとし、selfhost builder が Stage1 CLI を lowering し切れることを確認した時点で `=1` への切り替えを検討する。
- lambda/FunctionBox (`NewClosure` 等) は本フェーズでは扱わず、従来どおり builder からは排除したままにする(別フェーズ 25.2b に委ねる)。
## Guardrails / ポリシー
- Rust 側変更の方針SelfHost First / Minimal Core:
- Rust 側の Program→MIR 実装は「LoopForm / SSA / VM コア」の安定化に限定し、言語機能や高レイヤのロジックは .hako/selfhost 側で実装する。
- 変更を入れる場合も、LoopForm v2 / PHI / VM バグ修正など **構造的な安定化・根治** に目的を絞り、広域な新機能追加や仕様変更は行わない。
- selfhost builder 側は「Rust 実装をオラクルとして参照しつつ追従する」方針を維持する。
- FailFast:
- selfhost builder が Program(JSON) の一部に対応していない場合は、明確なタグ付きで失敗させる(例: `[builder/selfhost-first:unsupported:Match]`ようにし、silent stub には戻さない。
- provider 経路は退避路として残しつつ、Stage1 CLI の代表ケースでは selfhost builder が先に成功することを目標にする。
### LoopForm / PHI ポリシー(重要メモ)
- ループを含む関数本体を selfhost builder で扱うときは、**LoopForm 正規化を前提にする**:
- 可能な限り `docs/guides/loopform.md` で定義された「キャリア1個の φ」モデルに従う。
- break/continue を含むループは、LoopForm の制約更新変数最大2個・セグメント整列などを満たす範囲でのみ lowering 対象にする。
- MirBuilder 側で「生の while/for を直接 MIR の PHI に落とす」ような adhoc 実装は行わない:
- PHI ノードの生成・配置は既存の LoopForm/LowerLoop 系 helper`loop_scan_box.hako``lower_loop_*_box.hako` などに一元化し、builder 本体はそれを利用する立場にとどめる。
- LLVM harness 側の PHI 不変条件ブロック先頭グルーピングwelltyped incomingを崩さない。
- Phase 25.1b では:
- まず LoopForm 前提で安全に扱える最小のループ形(既存 selfhost テストでカバー済みの while/forから対応し、
- LoopForm 未適用の複雑なループ(例: キャリア3変数以上・ネストが深いもの`[builder/selfhost-first:unsupported:loopform]` タグなどで FailFast する。
#### LoopForm 複雑ケースへの拡張方針Rust builder をオラクルに使う)
- ねらい:
- 複雑な LoopFormキャリア複数・条件付き更新などについては、**Rust 側 MirBuilder/LoopForm 実装を「正解(オラクル)」として扱い**、Hakorune 側の `LowerLoop*Box` 群をそれに追従させる。
- Hakorune 側は LoopForm の設計や PHI 配線を再実装せず、「入力 JSON のパターンマッチ+既存 LowerLoop* の呼び出し」に専念する。
- 手順イメージ:
1. Rust 側 loop スモーク(例: `docs/development/roadmap/phases/phase-17-loopform-selfhost/``phase-21.6/21.8` 関連)に対応する .hako を特定し、provider-first`HAKO_SELFHOST_BUILDER_FIRST=0`)で MIR(JSON) を採取する。
2. 同じ .hako を selfhost-first`HAKO_SELFHOST_BUILDER_FIRST=1 HAKO_MIR_BUILDER_FUNCS=1 HAKO_SELFHOST_TRACE=1`)で通し、`LowerLoop*Box` がどこまで拾えているか/どのケースが `[builder/funcs:unsupported:loopform]` になっているかを観測する。
3. 差分が出ているループだけを対象に、小さな `LowerLoopXXXBox`(または既存 LowerLoop* の強化)を追加する。
4. ループの意味論差異キャリア更新・退出条件・rcが出ていないかは、VM/EXE canaryrc チェック)で確認する。
- ガード:
- 新しい LoopForm 対応はすべて既存 `lower_loop_*_box.hako` 群の中に閉じ込め、FuncLowering/MirBuilder 本体では依然として「LoopForm 結果を `_rebind` で名前付けするだけ」にとどめる。
- Rust 側の LoopForm/PHI 実装を変えずに selfhost 側のカバー率だけを上げるのが Phase 25.1b の範囲。
#### 25.1b で追加する LoopForm 用の新箱(足場)
- `lang/src/mir/builder/internal/lower_loop_multi_carrier_box.hako`
- 目的:
- Fibonacci 風の「multi-carrier」ループ`i,a,b,t` など複数のキャリアを持つループ)を selfhost builder 側で検出・LoopFormBox (`mode="multi_count"`) に委譲する。
- 現段階の挙動2025-11-16 時点):
- Program(JSON v0) 内で `Loop` + `Compare` を検出し、キャリア初期値(`local a = 0; local b = 1; ...`)を 2 個以上抽出。
- `i < N``N` については、`Int` リテラルだけでなく `n` のようなパラメータ `Var` もサポートし、`limit_kind=const|param` として `LoopFormBox.build2` に伝達。
- 成功時は `[mirbuilder/internal/loop:multi_carrier:detected:limit_kind=param,value=...|param_reg=...]` タグを確実に出力し、そのまま LoopForm から返ってきた MIR を `_rebind` する。
- ループ limit が `local` 参照など selfhost で扱えない場合は `[mirbuilder/internal/loop:multi_carrier:limit:unsupported]` を出して `null` を返し、Rust provider 経路へ退避。
- スモーク:
- `tools/smokes/v2/profiles/quick/core/phase251/selfhost_mir_loopform_multi_carrier_vm.sh`
- StageB で `TestBox.fib(n)` を emit し、selfhost builder が `[funcs/basic:loop.multi_carrier]` を出したうえで `TestBox.fib/1` を含む MIR(JSON) を生成するかをチェック。
- `carriers` 長や `limit_kind=param` ログを条件に PASS/FAIL を分岐provider fallback 時は SKIP
- 今後の拡張:
- `LoopFormBox.build_loop_multi_carrier``limit_kind=param` 実装を一般化し(現在は param register コピー → `reg_limit` 初期化まで対応済み、break/continue 付き multi-carrier も下ろせるようにする。
- 代表ケースとして `tools/smokes/v2/profiles/quick/core/vm_loop_phi_multi_carriers.sh` と同型の .hako を selfhost-first で通す canary を追加し、VM/EXE rc を Rust オラクルと比較する。
## Next Steps実装フェーズに入るときの TODO
1. 調査:
- Rust 側 `program_json_to_mir_json_with_imports` の挙動をトレースし、どの AST ノードがどの MIR に降りているかを整理(特に defs/call/loop/boxcall
- selfhost builder の現行 JSON 生成経路を洗い出し、stub を生成している箇所を特定。
2. 設計:
- `Program.defs` → MIR 関数生成のインタフェース(必要なフィールドと lowering 手順)を定義。
- call resolve 用の軽量マップ(`name -> qualified`)を selfhost builder に導入する。
3. 実装:
- defs 対応・call resolve・loop/branch lowering を段階的に導入しつつ、各ステップで mini スモークを追加。
- jsonfrag ベースの出力に切り替えながら、既存の mini テストを全て通ることを確認。
4. 検証:
- `tools/hakorune_emit_mir.sh lang/src/runner/launcher.hako …``HAKO_SELFHOST_BUILDER_FIRST=1` で実行し、62KB クラスの MIR(JSON) が selfhost builder 単独で得られることを確認。
- `tools/selfhost/build_stage1.sh` を selfhost-first でビルドし、Stage1 CLI EXE が `emit`/`build` コマンドを正しく実行できるかJSON stdout + exit codeをスモークで検証。
## 設計 TODOFuncLoweringBox / MirBuilderBox 拡張の方向性)
※ ここから先は「具体的にどこをどう広げるか」の設計メモ。実装はまだ行わない。
1. FuncLowering の対応範囲拡張
- `_lower_func_body` を Stage1 CLI で実際に使われているパターンまで広げる:
- 単一 Return だけでなく、ローカル変数定義if 分岐loop を含む「典型的な CLI ハンドラ」の形をサポート。
- `MethodCall``args.size()` / `args.get(i)` / `FileBox.open/read/write` / `ArrayBox.push` など)を MIR `mir_call``call` に落とす処理を追加。
- `func_map` / `resolve_call_target` を用いて、`HakoCli.cmd_emit_*` / `cmd_build_exe` などの内部 Call を `Global("HakoCli.cmd_emit_*")` 系に正規化。
2. MirBuilder 本体の出力構造
- これまでの「Program 全体を 1 関数 main に落とす」前提から、「Program.body + defs を multifunction MIR モジュールに落とす」前提へシフト:
- `Program.body` からはエントリ `Main.main/1` 相当の MIR 関数を生成。
- `Program.defs` からは `HakoCli.*` などの補助関数を生成。
- `_norm_if_apply` / `inject_funcs` の役割を整理し、「main 関数を含まない defsonly モジュール」を返さないように FailFast する。
3. FailFast とデバッグ
- Stage1 CLI のような大きな Program(JSON) に対して selfhost builder が未対応の場合は:
- `[builder/selfhost-first:unsupported:func_body]` などのタグ付きで明示的に失敗。
- provider 経路(`env.mirbuilder.emit`)へのフォールバックは維持するが、「空 EXE になる stub MIR」は生成しない方針に切り替える。
- `HAKO_SELFHOST_TRACE=1` 時に、FuncLowering がどの def でどこまで lowering できたかをログに出す。
4. 検証計画
- selfhostfirst canary:
- `stage1_launcher_program_to_mir_canary_vm.sh` の selfhostfirst 版を追加し、`HAKO_SELFHOST_BUILDER_FIRST=1` `MirBuilderBox.emit_from_program_json_v0` だけで 60KB 級の MIR(JSON) を生成できることを確認。
- Stage1 build:
- `tools/selfhost/build_stage1.sh` を selfhost-first で回し、生成された Stage1 EXE に対して `emit program-json` / `emit mir-json` / `build exe` スモークを追加。
このファイルは引き続き「Phase 25.1b の計画メモ(設計ディープダイブ)」として扱い、実装は Phase 25.1a の安定化完了後に、小さな差分に分割して順次進める。***
---
## 実装計画(順番にやる TODO
備考: ここでは Phase 25.1b を「複数の最小ステップ」に分解して、順番/ゴール/ガードを具体的にメモしておくにゃ。
### Step 0 — FailFast・観測を揃える
- Status: implemented (2025-11-15). `MirBuilderBox` now tags `defs_only` / `no_match` failures and aborts, and `FuncLoweringBox` logs unsupported defs when `HAKO_SELFHOST_TRACE=1`.
- 目的: 既存の selfhost builder がどこで諦めているかを正確に観測し、stub MIR を返さずに Fail させる導線を整える。
- 作業:
- `MirBuilderBox.emit_from_program_json_v0`
- `func_defs_mir` だけが非空だった場合でも黙って `{ "functions": [defs] }` を返さず、`[builder/selfhost-first:unsupported:defs_only]` を出す。
- internal lowers がすべて `null` の場合、`[builder/selfhost-first:unsupported:<reason>]` のタグを付与。
- `FuncLoweringBox.lower_func_defs`
- どの関数名で `_lower_func_body``null` を返したかを `HAKO_SELFHOST_TRACE=1` でログ出力。
- `tools/hakorune_emit_mir.sh`
- 既存の head/tail ログ出力で `[builder/selfhost-first:*]` タグがそのまま表示されることを確認済み(追加改修なし)。
- Phase 25.1b 以降、`HAKO_SELFHOST_BUILDER_FIRST=1` で呼び出された場合は「StageB → selfhost builderStage1 CLI」経路のみを試行し、selfhost builder が失敗した場合は即座に非0で終了する。selfhost-first モードでは `env.mirbuilder.emit` / provider delegate へのフォールバックは行わず、MirBuilder の未整備を隠さない方針とするprovider 経路を使うときは `HAKO_SELFHOST_BUILDER_FIRST=0` を明示)。
- 成果物:
- selfhost-first で Stage1 CLI を通したときに、どの関数/構造がまだ未サポートなのかがログで推測できる状態。
#### 補足: Ny selfhost パイプラインとの関係Phase 25.1b 時点)
- `.hako → Program(JSON v0) → MIR(JSON)` のメイン経路は、StageB`compiler_stageb.hako`)と MirBuilder/selfhost builder で完結させる。Ny selfhost`src/runner/selfhost.rs`)は `.ny` 用の補助ルートとして扱い、Phase 25.1b のスコープからは外す。
- `NYASH_USE_NY_COMPILER`:
- Phase 25.1b で「明示 opt-in」既定=0に変更。Runner は `NYASH_USE_NY_COMPILER=1` が立っているときだけ selfhost パイプラインNy→JSON v0を試行し、それ以外では従来どおり Rust parser/JSON v0 bridge を使う。
- `.hako` 実行や `tools/hakorune_emit_mir.sh` 経由の StageB/MirBuilder/selfhost builder には影響しない(これらは `NYASH_USE_NY_COMPILER=0` / `NYASH_DISABLE_NY_COMPILER=1` で起動)。
- Python MVP:
- `tools/ny_parser_mvp.py` は Phase 15 時点の Ny→JSON v0 実験用ハーネスであり、Phase 25.1b では `NYASH_NY_COMPILER_USE_PY=1` のときだけ有効にする。
- 既定では Ny selfhost パイプラインから Python には落ちない(脱 Python 方針に合わせて dev 専用の補助線に格下げ)。
- inline selfhost compiler`inline_selfhost_emit.hako`:
- `try_run_selfhost_pipeline` の最終手段として、`using lang.compiler.parser.box as ParserBox` / `using lang.compiler.stage1.emitter_box as EmitterBox` を含む小さな Hako を生成し、`ParserBox.parse_program2``EmitterBox.emit_program` で JSON v0 を得る経路が残っている。
- 現状、この inline 経路は `.ny` の大きなソースに対して 60s タイムアウト+ `Undefined variable: local` を伴うことがあり、ParserBox/Stage3/using 周りに無限ループ相当のバグが残っている疑いがある。
- Phase 25.1b では `.hako` selfhost builder から Ny selfhost 経路を切り離すことを優先し、inline 経路のバグは `[ny-compiler] inline timeout ...` `[ny-inline:hint]`stdout/stderr の head を添えるで可視化したうえで、後続フェーズ25.1c 以降)の構造タスクとして扱う。
### Step 1 — defs injection の再設計
- Status: initial-implementedmain 必須チェックはトグル付き; multi-function への完全移行は後続 Step
- 目的: `FuncLoweringBox.inject_funcs` で main 関数の有無を意識し、multi-function モジュールの土台を整える。
- 作業:
- `inject_funcs` 内で `HAKO_MIR_BUILDER_REQUIRE_MAIN=1` のときに `"name":"main"` を含まない `functions` 配列を拒否し、`[builder/funcs:fail:no-main]` をトレース出力して injection をスキップする(既定では OFF なので既存挙動は維持)。
- 将来フェーズで、`Program.body` から生成した main と defs を「2段構え」でマージする APImain 名の受け渡しなど)を追加する。
- 成果物(現段階):
- Env トグルを有効化した状態では「main を含まない MIR に defs だけ差し込む」ケースを検知できるようになり、Stage1 実装時に安全に stricter モードへ移行する足場ができた。
### Step 2 — `_lower_func_body` の拡張ローカルif
- 目的: Stage1 CLI の `HakoCli.cmd_emit_program_json` のような「Local / Assign / If / Return だけで構成された関数」を selfhost builder で lowering できるようにする。
- 作業:
- Body を JsonFrag で走査し、ローカル導入・代入・分岐を MIR ブロックへ展開する最小ロジックを FuncLoweringBox に追加。
- Loop が出た時は `[builder/funcs:unsupported:loop]`(仮タグ)を出して Fail-FastStep 3 で LoopForm 対応を行う)。
- 成果物:
- Loop を含まない defs は selfhost builder で MIR 関数にできるようになり、Stage1 CLI の emit/build ハンドラの半分程度を selfhost パスで賄える。
### Step 3 — LoopLoopFormの受理
- Status: initial-implemented (2025-11-15). `FuncBodyBasicLowerBox` now calls `LowerLoopSumBcBox`/`LowerLoopSimpleBox` from `_try_lower_loop`, tags unsupported loops with `[builder/funcs:unsupported:loopform]`, and delegates all PHI/carrier handling to LoopForm lowers.
- 目的: LoopForm 正規化済みの while/for を MIR に落とす。ループの正規化・PHI設計はLoopForm/既存lower_loop系Boxに任せ、FuncLowering/MirBuilder側はそれを使うだけにする。
- 作業内容(実装済み):
- `FuncBodyBasicLowerBox``_try_lower_loop`メソッド追加:
- Loop判定 → `LowerLoopSumBcBox.try_lower``LowerLoopSimpleBox.try_lower` の順に試す。
- 成功時は`_rebind`で関数名を`Box.method/arity`に付け替え。
- 失敗時は`[builder/funcs:unsupported:loopform]`でFail-Fast。
- `lower`メソッド冒頭でLoop優先処理:
- Loop含む場合は`_try_lower_loop`を呼び、成功/失敗で明確に分岐。
- Loopが無い場合のみ既存のLocal/If/Return処理に進む。
- PHI地獄防止ポリシー徹底:
- FuncBodyBasicLowerBox/FuncLowering側でPHIやキャリアを直接いじらない。
- LoopForm制約外は必ずタグ付きでFail-FastRust providerに退避可能
- 成果物:
- `cmd_build_exe``loop(i < argc)`等、Stage1 CLIの代表的なwhile/forパターンをselfhost builderで通せる基礎が整った。
- 追加アップデート2025-11-16: multi-carrier ループ(`TestBox.fib(n)` など)も `LowerLoopMultiCarrierBox``LoopFormBox.build_loop_multi_carrier` 経由で selfhost lowering できるようになり、limit が `Int` でなく `Var(n)` でも `[mirbuilder/internal/loop:multi_carrier:detected:limit_kind=param,...]` を出して処理できる。
- 次のステップ: LoopForm対応の動作確認スモークテスト追加、Step4MethodCall/ExternCallへ進む。
#### Step 3.1 — Box 型情報 APIRust Parity★New
- 背景:
- Stage3 VM では `"" + MapBox` のような「Box を文字列に暗黙変換する演算」が禁止されており、既存の `JsonEmitBox` / `BoxHelpers``LoopOptsBox.build2` から呼び出される)が `repr` 判定に依存しているため multi-carrier の JSON 生成が `Type error` で停止した。
- Rust 側の MirBuilder は enum で型が決まっており `match` で分岐できる。Hakorune 側でも同等の「Box の種別を問い合わせる API」を用意して文字列ハックを撤廃する必要がある。
- 設計方針:
1. `lang/src/shared/common/box_type_inspector_box.hako` を追加し、`BoxTypeInspectorBox.kind(value)` / `is_map(value)` / `is_array(value)` 等の API を提供する。
2. 実装は Stage0 Rust 側に `env.box_introspect(kind, value)` 的な extern を追加し、`hostbridge.extern_invoke("env.box_introspect","kind",[value])` で種別名(例: `"MapBox"`, `"ArrayBox"`, `"Int"`)を返す。
3. `BoxHelpers` / `JsonEmitBox` / `LoopOptsBox` など、Box 種別チェックが必要な箇所はすべてこの API に置き換え、`"" + value` を一切使わない。
4. 返り値は最小で `Null/Bool/Int/String``MapBox/ArrayBox/HostHandleBox`Stage1 で使用する型)をカバーし、将来的に `type_id` などを拡張する。
- 追加で行うこと:
- `CURRENT_TASK.md` に Box 型 API 実装タスクを追加し、LoopForm multi-carrier の JSON 出力がこの API 依存であることを明示。
- Stage0 側での対応(`env.box_introspect` 新規 externの設計も合わせて `phase-25.1b/README.md` に記述しておくSelfhost 側で API 追加→Rust 側 stub→VM 反映の順)。
- 現状2025-11-16 時点): Stage3 VM 経路で `BoxTypeInspectorBox.kind` / `is_map` / `is_array` が MapBox / ArrayBox を正しく認識し、小さな Hako テストで `hostbridge.extern_invoke("env.box_introspect","kind",[value])``env.box_introspect.kind` provider → plugin loader v2 の BoxIntrospect 実装までが endtoend で動作することを確認済み。
- fib multicarrier 経路と selfhost multicarrier smoke 用の canary ケース(`tools/smokes/v2/profiles/quick/core/phase251/selfhost_mir_loopform_multi_carrier_vm.sh`は、20251116 時点で `env.box_introspect.kind` provider 経路BoxTypeInspector 経由の multi-carrier LoopForm で PASS 済み。ログに `[mirbuilder/internal/loop:multi_carrier:detected:limit_kind=param,...]``[funcs/basic:loop.multi_carrier] -> TestBox.fib/1` が現れ、出力 MIR(JSON) に `"name":"TestBox.fib/1"` が含まれることを確認したため、「env.box_introspect.kind provider 経路完了 / multicarrier selfhost-first canary PASS」とみなす。
### Step 4 — MethodCall / ExternCall パリティ設計メモ・Rust層読解込み
- Status: design-onlyRust 層の挙動を踏まえた設計まで)
- 目的: `hostbridge.extern_invoke` / `FileBox` / `ArrayBox` など Stage1 CLI で多用される呼び出しを selfhost builder でも再現し、Rust 側の `build_method_call` / extern handler と意味論を揃える(ただしスコープは Stage1 必要最小限に限定)。
- 対象Phase 25.1b で扱う範囲に限定):
- Stage1 CLI (`lang/src/runner/launcher.hako`) 内で出現する代表パターン:
- `FileBox` 系: `fb.open(path,"r")` / `fb.read()` / `fb.write(content)` / `fb.close()`
- `ArrayBox` 系: `args.size()` / `args.get(i)` / `args.push(v)`
- `MapBox` 系(必要になれば): `m.get(k)` / `m.set(k,v)` / `m.size()`
- `String` 系: `s.length()`(現状 Step2 でも使われている)
- self-call: `me.cmd_emit_program_json(args)` / `me.cmd_emit_mir_json(args)` / `me.cmd_build_exe(args)` など
- ExternCall 的なもの: `hostbridge.extern_invoke("env.codegen","emit_object",args)` / `hostbridge.extern_invoke("env.codegen","link_object",args)`
- 設計方針:
1. **MethodCall → mir_call(Method)**box メソッド呼び出し)
- StageB Program(JSON v0) での形(実測):
- `return arr.size()` は defs 内で次のように現れる:
```json
{"type":"Local","name":"arr","expr":{"type":"New","class":"ArrayBox","args":[]}}
{"type":"Return","expr":{
"type":"Method",
"recv":{"type":"Var","name":"arr"},
"method":"size",
"args":[]
}}
```
- `hostbridge.extern_invoke("env.codegen","emit_object", a)` は:
```json
{"type":"Expr","expr":{
"type":"Method",
"recv":{"type":"Var","name":"hostbridge"},
"method":"extern_invoke",
"args":[
{"type":"Str","value":"env.codegen"},
{"type":"Str","value":"emit_object"},
{"type":"Var","name":"a"}
]
}}
```
- FuncLoweringBox / FuncBodyBasicLowerBox 側で扱う基本パターン:
- `Return(Method recv.method(args))` を検出し、
- `recv` が **パラメータ由来の Var**(例: `args.size()`)のときだけ selfhost lowering 対象にする。
- ローカル由来(`local fb = new FileBox(); return fb.read()`)は Phase 25.1b では対象外とし、今後のフェーズでローカルテーブル導入後に扱う。
- 引数は Int/Var/String のみ対象lambda や複合式は未対応)。
- MIR 側では `mir_call` の Method 形を使う:
```json
{"op":"mir_call","dst":R,
"mir_call":{
"callee":{"type":"Method","box_name":"ArrayBox","method":"size","receiver":recv_reg},
"args":[ /* 必要に応じて追加 */ ],
"effects":[]
}}
```
- `box_name` / `method` は whitelisted な Box のみ(当面は `ArrayBox` と `String` 程度)をハードコードで対応。
- receiver は `receiver` フィールド(または args の先頭でレジスタ番号を渡し、Rust 側の `mir_call` 仕様と揃える。
2. **self-callme.cmd_*)の解決**
- Stage1 CLI の `me.cmd_*` は、StageB の FuncScanner から `Program.defs` に `box:"HakoCli", name:"cmd_*"` として載る。
- これを FuncLoweringBox の `func_map` に登録済みなので、
- `Call("cmd_emit_mir_json", args)` → `func_map("cmd_emit_mir_json") = "HakoCli.cmd_emit_mir_json"`
- という形で Global 関数名を解決できる。
- Step4 では `Return(Call("cmd_*", ...))` だけでなく、「単独の Call 文」や「MethodCall 内からの self-call」も対応させる余地があるが、
- Phase 25.1b ではまず `Return(Call(...))` パターンの範囲内で self-call を `Box.method/N` に揃えるところまでに留める(広げるのは後続フェーズ)。
3. **ExternCallhostbridge.extern_invokeの扱い**
- Rust 側では `hostbridge.extern_invoke("env.codegen","emit_object",args)` 等を特別扱いし、C-ABI 経由で `env.codegen` provider にルーティングしている。
- selfhost builder 側では、Stage1 CLI の以下のパターンのみをサポート対象とする:
- `"env.codegen","emit_object",[mir_json]`
- `"env.codegen","link_object",[obj_path,(out_path)]`
- `"env.mirbuilder","emit",[program_json]`(必要なら)
- JSON 上では `expr.type:"Method"` `recv:Var("hostbridge")` `method:"extern_invoke"` で表現されているので、
- `args[0]` / `args[1]` が `"env.codegen"`, `"emit_object"` or `"link_object"` であることを確認し、
- static なパターンマッチで MIR の `extern_call` に落とす:
```json
{"op":"externcall","func":"env.codegen.emit_object","args":[ /* regs */ ]}
```
- ここでは「すべての extern を一般化する」のではなく、Stage1 CLI が実際に使っている env 名とメソッド名だけを point fix するRust Freeze Policy に従い、意味論は Rust 版を真似るが範囲は狭く保つ)。
4. **未対応パターンの Fail-Fast**
- MethodCall/ExternCall の lowering 中に、
- 複雑なオブジェクト式(ネストした MethodCall/Array/Map リテラルなど)、
- 引数に対応していない型lambda など)、
- 未サポートの env 名 / メソッド名(`env.codegen` 以外)、
が見つかった場合は、`[builder/funcs:unsupported:call]` タグを出して `null` で戻る。
- これにより、「知らない形をなんとなく MIR にする」ことを避け、Rust provider や legacy CLI delegate に退避できるようにする。
#### Step 4.1 — Rust 層 Call/ExternCall 契約の整理(移植元 SSOT
- 目的:
- Stage1 側の MethodCall/ExternCall lowering を「Rust 実装の振る舞い」に正確に揃えるため、Rust 層の Call/ExternCall/hostbridge 経路を SSOT として整理しておく。
- ここでの整理は構造レベルに留め、意味論の“拡張”は行わないHako 側はこの契約に従うだけ)。
- Rust 側のコア断面(ざっくり構造):
- **MIR ビルダ(呼び出し生成)**:
- `src/mir/builder/builder_calls.rs`
- `emit_unified_call(dst, CallTarget, args)`:
- `CallTarget::Method { box_type, method, receiver }` → `Callee::Method` を作り、`MirInstruction::Call { callee: Some(Callee::Method{..}), ... }` を emit。
- `CallTarget::Extern(name)` → 文字列 `"env.codegen.emit_object"` などを `ExternCall` に変換(`iface_name="env.codegen"`, `method_name="emit_object"`)。
- `CallTarget::Global(name)` → `Callee::Global(name)` 付き `Call` を emit`execute_global_function` へ)。
- **VM 側 Call ハンドラ**:
- `src/backend/mir_interpreter/handlers/calls/global.rs`:
- `execute_global_function(func_name, args)`:
- まず `functions` テーブルにあれば module 内関数として実行。
- そうでない場合、`normalize_arity_suffix("name/1")` した base 名に対して:
- `"print"` → `execute_extern_function("print", args)`。
- `"hostbridge.extern_invoke"` → `execute_extern_function("hostbridge.extern_invoke", args)`SSOT: hostbridge 経由の extern は必ずここを通る)。
- `"env.mirbuilder.emit"` / `"env.codegen.emit_object"` / `"env.codegen.link_object"`:
- それぞれ `crate::host_providers::{mir_builder,llvm_codegen}` を直接呼ぶ「グローバル関数版」ルート。
- `src/backend/mir_interpreter/handlers/calls/externs.rs`:
- `execute_extern_function(iface, method, args)`:
- `("env.mirbuilder","emit")` / `("env.codegen","emit_object")` / `("env.codegen","link_object")` などを `extern_provider_dispatch` に委譲。
- `"hostbridge.extern_invoke"` base 名もここから `extern_provider_dispatch("hostbridge.extern_invoke", args)` に流す。
- **ExternCall / hostbridge.extern_invoke の provider**:
- `src/backend/mir_interpreter/handlers/externals.rs`:
- ExternCall 形(`MirInstruction::ExternCall`) を `iface_name`,`method_name` ごとに振り分け:
- `("env.mirbuilder","emit")` → `extern_provider_dispatch("env.mirbuilder.emit", args)`。
- `("env.codegen","emit_object")` → `extern_provider_dispatch("env.codegen.emit_object", args)`。
- `("env.codegen","link_object")` → 第3引数 ArrayBox `[obj_path, exe_out?]` を取り出して C-API ルートへ。
- `("hostbridge","extern_invoke")` → `extern_provider_dispatch("hostbridge.extern_invoke", args)`(なければ Invalid
- `src/backend/mir_interpreter/handlers/extern_provider.rs`:
- `extern_provider_dispatch(key, args)`:
- `"env.mirbuilder.emit"`:
- `args[0]` を `program_json` にし、`HAKO_MIRBUILDER_IMPORTS` から imports マップを読む。
- `host_providers::mir_builder::program_json_to_mir_json_with_imports` を呼んで MIR(JSON) 文字列を返す。
- `"env.codegen.emit_object"`:
- `args[0]` を MIR(JSON) 文字列にして v1 へ normalize → `llvm_codegen::mir_json_to_object`。
- `"env.codegen.link_object"`:
- `args[0]`=obj_path, `args[1]`=exe_out を文字列化し、C-API ルート(`NYASH_LLVM_USE_CAPI=1` + `HAKO_V1_EXTERN_PROVIDER_C_ABI=1`)で `link_object_capi`。
- `"env.get"` / `"env.box_introspect.kind"` / `"hostbridge.extern_invoke"` もここで扱うBoxIntrospect は plugin_loader_v2 に委譲)。
- plugin_loader v2 側の env.*:
- `src/runtime/plugin_loader_v2/enabled/extern_functions.rs`:
- `extern_call(iface_name, method_name, args)` で `env.*` を一括処理。
- `handle_mirbuilder("emit", args)`:
- `args[0]` の Program(JSON v0) 文字列を受け取り、`host_providers::mir_builder::program_json_to_mir_json` で MIR(JSON v0) を返す。
- `handle_codegen("emit_object", args)`:
- `args[0]` の MIR(JSON v0) 文字列を受け取り、ny-llvmc ラッパ (`llvm_codegen::mir_json_to_object`) で object (.o) のパスを返す。
- BridgeJSON v0 → MIRの特別扱い:
- `src/runner/json_v0_bridge/lowering/expr.rs` / `lowering.rs`:
- `MapVars::resolve`:
- `hostbridge` / `env` を特殊変数として扱い、それぞれ Const(String) `"hostbridge"` / `"env"` を生成するMethod チェーンを降ろすためのプレースホルダ)。
- `me` については、Bridge 環境の `allow_me_dummy` が ON のときだけ NewBox を注入する(通常は JSON defs 側で明示パラメータとして扱う)。
- `lower_expr_with_scope`:
- `ExprV0::Extern { iface, method, args }` → `MirInstruction::ExternCall { iface_name, method_name, ... }`。
- `ExprV0::Method` の特別ケース:
- `ConsoleBox` の `print/println/log` → `ExternCall env.console.log`。
- `env.box_introspect.kind(value)` パターン → `ExternCall env.box_introspect.kind` に正規化。
- defs 降下(`lowering.rs`:
- JSON v0 の `defs` に対して、`box_name != "Main"` の関数を **インスタンスメソッド** とみなし、
- `signature.params` に「暗黙 `me` + 明示パラメータ」を載せる。
- `func_var_map` に `me` → `func.params[0]` を、残りのパラメータ名を `params[1..]` にバインドする。
- これにより 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` と同様に)。
- Rust 側の `CallTarget::Method` → `Callee::Method` の変換ルールreceiver レジスタの扱い、box_name/method 名)を Step 4 の設計メモと揃える。
- `ExternCall`:
- `hostbridge.extern_invoke("env.codegen","emit_object"/"link_object", args)` や `env.mirbuilder.emit` などは、
- Rust では最終的に `ExternCall` → `extern_provider_dispatch("env.*", args)` → `plugin_loader_v2::extern_call("env.*", method, args)` / `host_providers::*` という構造になっている。
- Hako 側では「env 名+メソッド名の組(= key」を列挙した薄い `*BridgeBox` でラップし、そのうえで `ExternCallLowerBox` が `externcall func="env.codegen.emit_object"` を emit する。
- 未対応の name/method 組は必ず Fail-Fastタグ付きで provider に回す。
この Step 4.1 を「Rust 側の SSOT」として固定しておき、Phase 25.1c 以降ではこの契約に沿って Hako 側の MethodCall/ExternCall lowering 箱を実装・整理していくRust 側に新ルールは追加しない)方針とする。
- 実装イメージPhase 25.1b 中にやるときの TODO:
1. `FuncLoweringBox` に小さな helper を追加:
- `_lower_method_call(body_json, func_name, box_name, params_arr)` → MethodCall パターン検出+`mir_call Method` 生成。
- `_lower_hostbridge_extern(body_json, ...)` → env.codegen/env.mirbuilder 用の最小 ExternCall 生成。
2. `_lower_func_body` の冒頭か、既存 `Return(Call)` の前後でこれら helper を呼び出し、マッチした場合のみ MIR を返す。
3. Tag/ログ:
- `HAKO_SELFHOST_TRACE=1` 時に `[funcs/basic:method.*]` / `[funcs/basic:extern.*]` trace を出し、どの defs が Method/Extern 経由で lowering されたか観測できるようにする。
4. スモーク:
- `tools/smokes/v2/profiles/quick/core/phase251` に `selfhost_mir_methodcall_basic_vm.sh` のような canary を追加し、
- ArrayBox.push / FileBox.open/read/write / env.codegen.emit_object/link_object の代表ケースを selfhost builder-first で通し、
- `mir_call` / extern call が出力 MIR に含まれていることを確認する。
### Step 1 — CLI entry detection (CliEntryLowerBox)
- 目的: Stage1 CLI の入口構造(`Main.main` → `HakoCli.run`)を Program(JSON v0) 上で検出し、selfhost builder が「どの Program が CLI エントリを含むか」を把握できるようにする(観測専用)。
- 作業:
- `lang/src/mir/builder/func_body/cli_entry_box.hako` に `CliEntryLowerBox` を追加。
- `scan(program_json)` で以下を確認し、すべて満たすときだけタグを出す:
- `Program.body` 内に `New(HakoCli)` 相当の JSON`"class":"HakoCli"`)が存在する。
- `defs` 配列に `{"box":"HakoCli","name":"run", ...}` が存在する。
- 入口付近に `method":"run"` を含む Method 呼び出しがある(軽いヒューリスティック)。
- 条件を満たした場合、`HAKO_SELFHOST_TRACE=1` のときに
`[builder/cli:entry_detected] main=Main.main run=HakoCli.run/2`
を出力し、戻り値は常に `null`MIR は生成しない)。
- `FuncLoweringBox.lower_func_defs` の先頭で `CliEntryLowerBox.scan(program_json)` を呼び出し、defs 降ろし処理とは独立に「入口構造だけを観測」できるようにする。
- レイヤリング:
- この Step1 はあくまで ring1Stage1 CLI ソース)の Program(JSON v0) を観測するだけで、ring0Rust env.mirbuilder/env.codegenには一切影響を与えない。
- 今後の Step2/3 で `HakoCli.run` / `Main.main` の本体を MIR に降ろすときの「入り口インデックス」として使う想定。
### Step 2 — HakoCli.run 形状スキャンCliRunShapeScannerBox lower stub
- 目的: Stage1 CLI の `HakoCli.run` がどのような分岐run/build/emit/check 等)を持っているかを Program(JSON v0) から把握し、将来の専用 lowerCliRunLowerBoxが安全に使えるかを事前に観測する。
- 作業:
- `lang/src/mir/builder/func_body/cli_run_shape_box.hako` を追加CliRunShapeScannerBox
- `scan(program_json)` で:
- `{"box":"HakoCli","name":"run", ...}` を defs 内から検出し、
- Program 全体の文字列から `"run"`, `"build"`, `"emit"`, `"check"` などのリテラルを簡易に拾って、`branches` 配列として記録。
- `HAKO_SELFHOST_TRACE=1` のときに
`[builder/cli:run_shape] has_run=1 branches=<count>`
を出力し、戻り値として `{"has_run":1,"branches":[...]} (MapBox)` を返す(現状はタグ主体で利用)。
- `lang/src/mir/builder/func_body/cli_run_lower_box.hako` を追加CliRunLowerBox
- 現段階では stub 実装:
- `lower_run(func_name, box_name, params_arr, body_json, func_map)` は `HakoCli.run` だけをターゲットにし、`HAKO_SELFHOST_TRACE=1` 時に
`[builder/cli:run_lower:stub] box=HakoCli func=run`
を出して常に `null` を返す。
- 実際の MIR 降ろしrun/build/emit/check 分岐を持つ run 本体の loweringは、Step2 後半〜Step3 以降で実装する前提。
- `FuncLoweringBox` への統合:
- `lower_func_defs` の先頭で `CliRunShapeScannerBox.scan(program_json)` を呼び、Stage1 CLI run の形状をタグ付きで観測。
- `_lower_func_body` の冒頭で `box_name=="HakoCli" && func_name=="run"` のときだけ `CliRunLowerBox.lower_run(...)` を呼び出す。現状は常に `null` なので、従来の BasicLowerBox / provider 経路と挙動は変わらない。
- レイヤリング:
- Step2 も Step1 と同様、ring1Stage1 Hako CLIの構造を観測する箱のみを追加する。
- MIR 生成はまだ Rust provider /既存 lower に任せており、ring0 の責務env.* extern や実行エンジン)にも影響を与えない。
- 専用 lower`CliRunLowerBox` が実際に MIR を返す形は、StageB Program(JSON v0) の形状を十分観察してから、小さなパターン(シンプルな run/build/emit/check 分岐)ごとに段階的に実装する。
### Step 2.x — HakoCli.run lowering設計メモMVP 実装状況)
- ゴール:
- `HakoCli.run(me,args)` のうち「単純な run/build/emit/check 分岐」だけを selfhost builder で MIR 関数に降ろす。
- 形が少しでも崩れていたら必ず `null` を返し、Rust provider にフォールバックするFailFast
- 対象とする JSON v0 の形MVP 想定):
1. `Local argc = Int(0)`
2. `If cond Var("args") then { Local argc = Method Var("args").size() }`
3. `If cond Compare(Var("argc") == Int(0)) then { Return Int(1) }`
4. `Local cmd_raw = Method Var("args").get(Int(0))`
5. `Local cmd = Binary("+", Str(""), Var("cmd_raw"))`
6. 連続する `If` で `cmd == "run"|"build"|"emit"|"check"` を判定し、それぞれ `Return Call("cmd_*", [me,args])` を持つ。
7. 最後に `Return Int(2)`unknown commandを持つ。
- 実装状況CliRunLowerBox 内):
1. ターゲット判定(実装済み)
- `box_name=="HakoCli" && func_name=="run"` 以外は即 `null`。
2. 構造パターンの検証 `_check_shape`(実装済み)
- `body_json` を文字列として走査し、上記 1〜7 のステートメントが順番どおりに現れているかを `JsonFragBox` で確認(ローカル名やメソッド名も一致させる)。
- OK のとき `1`、不一致のとき `0` を返し、`HAKO_SELFHOST_TRACE=1` で `[builder/cli:run_lower:shape-ok]` / `[builder/cli:run_lower:unsupported]` を出す。
3. MIR 生成MVP `_emit_mir`(実装済み・既定 OFF
- params_arr=["me","args"] を r1,r2 とみなし、固定レイアウトのレジスタ配置で簡略 MIR(JSON) を構築。
- ブロック構造(要約):
- argc を `boxcall size(box=2)` で計算し、`argc==0` のときは `ret 1`。
- `args.get(0)` で cmd_raw を取得し、`"run"|"build"|"emit"|"check"` との比較で `cmd_run/cmd_build/cmd_emit/cmd_check` を `boxcall` で呼び出してそのまま ret。
- どれにもマッチしない場合は `ret 2`unknown command
- 環境変数 `HAKO_MIR_BUILDER_CLI_RUN=1` のときにだけ `_emit_mir` を呼び、それ以外は shape OK でも `null` を返して provider/既存 lower にフォールバックする(既定挙動は不変)。
4. タグと FailFast実装済み
- 形が完全に一致し、`HAKO_MIR_BUILDER_CLI_RUN=1` のときにだけ MIR を返し、`HAKO_SELFHOST_TRACE=1` で `[builder/cli:run_lower:ok] HakoCli.run/2` を出す。
- 途中でパターンが崩れていた場合は `[builder/cli:run_lower:unsupported] reason=...` を出して `null` を返すprovider が引き継ぎ)。
5. 現在のカバレッジ:
- `.hako` に HakoCli.run + cmd_* を直接書いた最小ケースでは、selfhost builder だけで `HakoCli.run/2` の MIR(JSON) を生成できることを
`tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh` で確認済み。
- 実際の Stage1 launcher.hako の `run` は usage メッセージ出力などを含むため、この MVP はまだ Stage1 本体には適用していない(今後 StageB Program(JSON v0) を詳細に比較しながら対応範囲を広げる)。
#### Stage1 HakoCli.run本番とのギャップ整理provider MIR ベース)
現状、このホスト環境では StageB 実行中に `Undefined variable: local`Stage3 キーワード)で Program(JSON v0) 抽出が失敗しているため、実 `launcher.hako` の HakoCli.run 形状は Rust provider の MIR(JSON) から推定している(`/tmp/launcher_provider_mir.json` で確認)。
MVP run パターンと Stage1 実装の主な差分:
- argc==0 の場合:
- MVP: `if argc == 0 { return 1 }`(副作用なし)。
- 実装: usage メッセージ `[hakorune] usage: hakorune <command> [options]` を `print` してから `ret 1`。
- サブコマンド unknown の場合:
- MVP: 単純に `return 2`。
- 実装: `[hakorune] unknown command: <cmd>` を `print` してから `ret 2`。
- 比較まわり:
- 両者とも `"run"|"build"|"emit"|"check"` 文字列と一致判定しているが、実装側は usage/unknown メッセージ用の追加 `binop`/`call` を複数ブロックに分割しているMIR 上でブロック数が多い)。
- 呼び出しターゲット:
- 両者とも `cmd_run/cmd_build/cmd_emit/cmd_check` を呼び出す点は一致MIR 上では `boxcall`、将来 `FuncLowering` の call_resolve で Global/Method に寄せる予定)。
今後 StageB Program(JSON v0) が安定して取れるようになったら、上記の差分を JSON レベルで再確認し、
- usage/unknown の `print` ブロックを「前置/後置のサイドエフェクト」として `_check_shape` の許容パターンに追加するか、
- あるいは run 本体を「MVP サブセット(引数分岐)+印字専用ブロック」に分けて扱うか、
を決める予定。
#### Stage1 用 run パターン拡張方針(設計)
Stage1 launcher.hako の本番 `HakoCli.run` を selfhost lowering の対象に含めるための方針は次のとおり:
- 目的:
- Rust CLIStage0と同じ意味論exit code とメッセージを維持したまま、Stage1 側の run を selfhost builder からも扱えるようにする。
- logging/usage 部分printは「サイドエフェクトのある前置/後置ブロック」として明示的に扱い、分岐ロジック本体とは分離して考える。
- 拡張の方向性(案):
1. **前置 usage ブロックの許容**
- `_check_shape` で「argc==0 → usage print → ret 1」という形を、
「`argc == 0` の then 側に `StringBox` const / `binop` / `call(print)` が含まれてもよい」
というルールに緩和する。
- lowering 時には:
- まず usage 用の文字列構築+`print` をそのまま MIR に反映(`boxcall` / `externcall env.console.log` など)。
- そのあとで `ret 1` を emit するMVP では usage 文言は provider MIR に揃える)。
2. **unknown ブロックの後置許容**
- MVP では「unknown なら `ret 2`」のみを扱っているが、本番では
`[hakorune] unknown command: <cmd>` を出力してから `ret 2` している。
- `_check_shape` を「末尾の Return Int(2) の前に StringBox/`binop`/`print` パターンが挟まっていてもよい」と解釈できるようにし、
lowering 側でもそれをそのまま MIR に降ろすprint ブロック + `ret 2`)。
3. **run 本体(分岐ロジック)との分離**
- `_check_shape` を二層に分ける:
- `check_core_shape` … argc/args/cmd/サブコマンド分岐の「副作用なし」部分の形状チェック。
- `check_logging_shape` … usage/unknown の印字パターンのみを許容する緩いチェック。
- `CliRunLowerBox` はまず core を `_emit_mir_core` で生成し、logging 部分は必要に応じて前後にブロックを足す形で統合する。
- トグルと適用範囲:
- Stage1 本番への適用は常に `HAKO_MIR_BUILDER_CLI_RUN=1`(既定 OFFでガードし、
かつ StageB Program(JSON v0) で `check_core_shape` / `check_logging_shape` の両方を満たしている場合だけ有効にする。
- それ以外のケースprint の形がずれている / 追加の case が増えたなど)は、今まで通り provider 経路に退避する。
### Step 5 — call resolve / methodize 後処理
- 目的: 生成済み MIR に対して call resolver / methodize pass をかけ、Rust provider と同じ命名・呼び出し形式を実現。
- 作業:
- `HAKO_MIR_BUILDER_CALL_RESOLVE=1` を本格利用し、call の `Const("Box.method/N")` を `mir_call` に変換。Stage1 CLI で `mir_call Method` を使うケースをテストし、Methodize との組み合わせでも崩れないことを確認。
### Step 6 — selfhost-first canary / build_stage1.sh
- 目的: selfhost builder を既定 ON に戻す準備。
- 作業:
- `stage1_launcher_program_to_mir_canary_vm.sh` の selfhost-first 版を追加して selfhost builder 単独で 60KB 級 MIR を生成できることを検証。
- `tools/selfhost/build_stage1.sh` を selfhost-first で回し、selfhost builder 由来の MIR で `emit program-json` / `emit mir-json` / `build exe` が通ることを確認。
- 問題が無ければ `HAKO_SELFHOST_BUILDER_FIRST=1` を既定に戻す別PRでも可
作業順は Step 0 → 1 → 2 → 3 → 4 → 5 → 6 を想定、各ステップで必ず docsこのファイルCURRENT_TASKとスモーク/テストを更新する。
現状2025-11-16 時点)の進捗:
- Step0〜3: 実装済みFail-Fast 導線・Local/If/Return 基本形・LoopForm 正規化済み Loop の取り込み)。
- Step4:
- MethodCall: `FuncBodyBasicLowerBox._try_lower_return_method` で `Return(Method recv.method(args))` 形のうち、ArrayBox.size/getparams ベース receiverと StringBox.length引数なしを最小カバー済み。生成される MIR は `mir_call { callee: { type: "Method", box_name: "<ArrayBox|StringBox>", method, receiver }, args: [...] }` 形式。
- ExternCall: `lang/src/mir/builder/func_body/extern_call_box.hako` に `ExternCallLowerBox.lower_hostbridge` を追加し、`Return(hostbridge.extern_invoke("env.codegen","emit_object"|"link_object", Var(arg)))` を `externcall env.codegen.emit_object|link_object``ret` に lowering する最小パターンを実装。`FuncLoweringBox._lower_func_body` から BasicLowerBox の次に呼び出すよう配線。
- Canary: `tools/smokes/v2/profiles/quick/core/phase251/selfhost_mir_methodcall_basic_vm.sh` / `selfhost_mir_extern_codegen_basic_vm.sh` を追加(現状は「対象パターンにまだ到達していない」場合に SKIP する canary として動作)。
- Step5〜6: 未着手Method/Extern のカバー範囲が実際の Stage1 CLI パターンまで広がった段階で selfhost-first canary / build_stage1.sh へ進める)。
### Stage1 CLI defs vs selfhost builder 対応状況(スナップショット)
Stage1 CLI ランチャ(`lang/src/runner/launcher.hako`)について、`tools/hakorune_emit_mir.sh` を provider-first`HAKO_SELFHOST_BUILDER_FIRST=0`で実行し、Rust provider が出力した MIR(JSON) から各関数の Method/Extern 風パターンを集計した結果:
- 集計コマンド(例):
- `HAKO_SELFHOST_BUILDER_FIRST=0 NYASH_JSON_ONLY=1 bash tools/hakorune_emit_mir.sh lang/src/runner/launcher.hako /tmp/launcher_provider_mir.json`
- Python で `functions[].blocks[].instructions[].op in {"boxcall","mir_call","externcall"}` を走査し、Method/Extern らしき箇所を抽出。
- 注意:
- 現行の provider MIR では、Stage1 CLI のメソッド呼び出しはすべて `boxcall` で表現されており、`mir_call(Method)` にはまだ正規化されていない。
- selfhost builder は StageB の Program(JSON v0) 上で `Method` ノードを見て lowering する設計であるため、下表の「Method 名」は Program 側のメソッドセットを推定するための参考情報として扱う。
| MIR function | Method パターンprovider MIR 上の boxcall/mir_call | Extern 風パターン | selfhost builder 側の現状 |
|----------------------------------|------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|--------------------------------------|
| `HakoCli._read_file/3` | `open`, `read`, `close`FileBox 由来と推定) | なし | FileBox 系メソッドは未対応 |
| `HakoCli._write_file/5` | `open`, `write`, `close`FileBox | なし | 同上(未対応) |
| `HakoCli.cmd_build/2` | `cmd_build_exe`, `get`, `size``args.get/size` など) | なし | `args.size/get` 形は Step4 helper あり(ただし関数本体全体は未対応) |
| `HakoCli.cmd_build_exe/2` | `_read_file`, `emit_from_program_json_v0`, `emit_program_json_v0`, `extern_invoke`, `get`, `indexOf`, `push`, `size` | `extern_invoke` が hostbridge 経由の Extern 相当 | `extern_invoke("env.codegen",…)` 用 ExternCallLowerBox 追加済み/他メソッドは未対応 |
| `HakoCli.cmd_emit/2` | `cmd_emit_mir_json`, `cmd_emit_program_json`, `get`, `size` | なし | `args.size/get` のみ helper 対象候補 |
| `HakoCli.cmd_emit_mir_json/2` | `_read_file`, `_write_file`, `emit_from_program_json_v0`, `emit_program_json_v0`, `get`, `indexOf`, `set`, `size` | なし | FileBox 系/`indexOf``set` は未対応 |
| `HakoCli.cmd_emit_program_json/2`| `_read_file`, `_write_file`, `emit_program_json_v0`, `get`, `indexOf`, `size` | なし | 同上 |
| `HakoCli.run/2` | `cmd_build`, `cmd_check`, `cmd_emit`, `cmd_run`, `get`, `size` | なし | `args.size/get` helper 対象/`me.cmd_*` self-call は未対応 |
| `main` | `run``Main.main → HakoCli.run` 呼び出し) | なし | main 相当は provider/main 降ろしに依存 |
このスナップショットから分かる不足点2025-11-16 現在):
- MethodCall 側:
- Stage1 CLI で実際に使われているメソッドは、FileBoxopen/read/write/close、Array/Map 系size/get/set/push、String 系indexOfなど多岐にわたるが、selfhost builder が defs 側で専用に扱えているのは **`args.size/get` と `String.length` の単純な Return 形のみ**。
- `me.cmd_*` 系 self-call`cmd_build_exe` など)は、現状 `_lower_return_call` ベースの簡易 Call 降ろしに頼っており、Stage1 CLI の複雑な本体にはまだ対応できていない。
- ExternCall 側:
- Stage1 CLI の AOT 経路で重要な `hostbridge.extern_invoke("env.codegen","emit_object|link_object", args)` は、ExternCallLowerBox で最小対応済みだが、実際の Stage1 CLI defs からこの helper に到達しているかどうかは、今後 Program(JSON v0) 側のパターンを精査する必要がある。
- それ以外の Extern`env.mirbuilder.emit`, `env.console.*` などは、selfhost builder では現時点で扱っておらず、Rust provider / ハーネス側に依存している。
この表をベースに、今後の小さな helper 拡張(例: FileBox 用メソッド降ろし箱、Array/Map の `push/set/indexOf` 降ろし箱、`me.cmd_*` self-call 用の専用 CallLowerBox など)を段階的に追加していく予定。
Phase 25.1b の残タスクとしては、「Stage1 CLI で本当に必要なメソッドExtern パターン」だけを優先し、それ以外は引き続き Rust provider を退避路として使う方針を維持する。
### スモーク構成方針Rust builder と selfhost builder のペアリング)
- 目的:
- Rust 側の loop/method/extern スモークを **そのまま「正解」として使い回しつつ**、同じ .hako を selfhost builder で通す canary を横に並べ、「どの経路が壊れているか」をフォルダ名だけで判別できるようにする。
- 基本ルール:
- 既存の v2 スモーク構造(`tools/smokes/v2/profiles/quick/core/phaseXXXX/`は維持し、その直下に「provider-first」と「selfhost-first」の **ペアスクリプト** を置く。
- 命名例:
- `*_provider_vm.sh` … 既存どおり Rust builderprovider-first経路を確認するスモーク。
- `*_selfhost_vm.sh` … 同じ .hako / 期待 rc を selfhost-first`HAKO_SELFHOST_BUILDER_FIRST=1`)で確認するスモーク。
- ループ系:
- 例として `phase2100` の LoopForm/PHI canaryRust ベース)に対応して、
- `tools/smokes/v2/profiles/quick/core/phase2100/loop_jsonfrag_provider_vm.sh`
- `tools/smokes/v2/profiles/quick/core/phase251/loop_jsonfrag_selfhost_vm.sh`
のような組み合わせを想定(実際のファイル名は今後の実装で確定)。
- Stage1 CLI 系:
- 既存の `stage1_launcher_program_to_mir_canary_vm.sh`provider-firstに対して、
- `stage1_launcher_program_to_mir_selfhost_vm.sh`selfhost-first; builder MIR で 60KB 級出力を期待)
を `phase251` 側に追加する。
- 運用:
- quick プロファイルでは provider-first スモークを既定 ON とし、selfhost-first スモークは Phase 25.1b 中は任意(開発用)とする。
- selfhost-first スモークが十分に安定し、Stage1 build も selfhost-first で通るようになった時点で、必要に応じて CI quick プロファイルへの昇格を検討する。
***