205 lines
18 KiB
Markdown
205 lines
18 KiB
Markdown
|
|
# Phase 25.1b — Selfhost Builder Parity (Planning → Design Deep‑Dive)
|
|||
|
|
|
|||
|
|
Status: planning-only(実装着手前の設計・分析フェーズ)
|
|||
|
|
|
|||
|
|
## ゴール
|
|||
|
|
|
|||
|
|
- Rust 側 Program→MIR (`env.mirbuilder.emit`) と Hakorune 側 selfhost builder (`MirBuilderBox.emit_from_program_json_v0`) の機能差を埋め、Stage1 CLI(launcher.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/O(JSON stdout + exit code)を Rust/llvmlite と同じ契約に揃える。
|
|||
|
|
|
|||
|
|
## 現状(Phase 25.1a 時点)
|
|||
|
|
|
|||
|
|
### Stage‑B(Program(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` 等をファイル結合前に解決。
|
|||
|
|
- Stage‑B entry 側は string literal using を廃止し、`using lang.compiler.entry.using_resolver as Stage1UsingResolverBox` のように module alias を使用する。
|
|||
|
|
|
|||
|
|
### 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` は well‑known 変数として `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`)へフォールバックする。
|
|||
|
|
|
|||
|
|
### 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 Freeze Policy:
|
|||
|
|
- Rust 側の Program→MIR 実装には原則手を入れず、selfhost builder は「Rust の既存挙動に合わせる」方向で実装する。
|
|||
|
|
- 変更は Hakorune 側 (`lang/src/mir/builder/*`) とツール (`tools/hakorune_emit_mir.sh`) に閉じる。
|
|||
|
|
- Fail‑Fast:
|
|||
|
|
- 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 に落とす」ような ad‑hoc 実装は行わない:
|
|||
|
|
- PHI ノードの生成・配置は既存の LoopForm/LowerLoop 系 helper(`loop_scan_box.hako`/`lower_loop_*_box.hako` など)に一元化し、builder 本体はそれを利用する立場にとどめる。
|
|||
|
|
- LLVM harness 側の PHI 不変条件(ブロック先頭グルーピング/well‑typed incoming)を崩さない。
|
|||
|
|
- Phase 25.1b では:
|
|||
|
|
- まず LoopForm 前提で安全に扱える最小のループ形(既存 selfhost テストでカバー済みの while/for)から対応し、
|
|||
|
|
- LoopForm 未適用の複雑なループ(例: キャリア3変数以上・ネストが深いもの)は `[builder/selfhost-first:unsupported:loopform]` タグなどで Fail‑Fast する。
|
|||
|
|
|
|||
|
|
## 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)をスモークで検証。
|
|||
|
|
|
|||
|
|
## 設計 TODO(FuncLoweringBox / 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 を multi‑function MIR モジュールに落とす」前提へシフト:
|
|||
|
|
- `Program.body` からはエントリ `Main.main/1` 相当の MIR 関数を生成。
|
|||
|
|
- `Program.defs` からは `HakoCli.*` などの補助関数を生成。
|
|||
|
|
- `_norm_if_apply` / `inject_funcs` の役割を整理し、「main 関数を含まない defs‑only モジュール」を返さないように Fail‑Fast する。
|
|||
|
|
|
|||
|
|
3. Fail‑Fast とデバッグ
|
|||
|
|
- 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. 検証計画
|
|||
|
|
- selfhost‑first canary:
|
|||
|
|
- `stage1_launcher_program_to_mir_canary_vm.sh` の selfhost‑first 版を追加し、`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 — Fail‑Fast・観測を揃える
|
|||
|
|
- 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:*]` タグがそのまま表示されることを確認済み(追加改修なし)。
|
|||
|
|
- 成果物:
|
|||
|
|
- selfhost-first で Stage1 CLI を通したときに、どの関数/構造がまだ未サポートなのかがログで推測できる状態。
|
|||
|
|
|
|||
|
|
### Step 1 — defs injection の再設計
|
|||
|
|
- Status: initial-implemented(main 必須チェックはトグル付き; 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段構え」でマージする API(main 名の受け渡しなど)を追加する。
|
|||
|
|
- 成果物(現段階):
|
|||
|
|
- 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-Fast(Step 3 で LoopForm 対応を行う)。
|
|||
|
|
- 成果物:
|
|||
|
|
- Loop を含まない defs は selfhost builder で MIR 関数にできるようになり、Stage1 CLI の emit/build ハンドラの半分程度を selfhost パスで賄える。
|
|||
|
|
|
|||
|
|
### Step 3 — Loop(LoopForm)の受理
|
|||
|
|
- 目的: LoopForm 正規化済みの while/for を MIR に落とす。
|
|||
|
|
- 作業:
|
|||
|
|
- `lower_loop_simple_box` / `lower_loop_sum_bc_box` など既存 helper を FuncLoweringBox 側からも利用できるようにし、LoopForm のキャリアを MIR へ展開。
|
|||
|
|
- LoopForm の制約を満たさないケースは `[builder/funcs:unsupported:loopform]` で Fail-Fast。
|
|||
|
|
- 成果物:
|
|||
|
|
- `cmd_build_exe` の `loop(i < argc)` 等、Stage1 CLI の代表的な while/for パターンを selfhost builder で通せる。
|
|||
|
|
|
|||
|
|
### Step 4 — MethodCall / ExternCall パリティ
|
|||
|
|
- 目的: `hostbridge.extern_invoke` / `FileBox` / `ArrayBox` など Stage1 CLI で多用される呼び出しを selfhost builder でも再現。
|
|||
|
|
- 作業:
|
|||
|
|
- `FuncLoweringBox` に MethodCall/ExternCall の lowering ルートを追加し、`mir_call Method` もしくは `extern_call` を生成。
|
|||
|
|
- `func_map` により `me.cmd_build_exe` のような self-call も Box.method/N に resolve(未対応の呼び出しは `[builder/funcs:unsupported:call]` で Fail)。
|
|||
|
|
- 成果物:
|
|||
|
|
- Stage1 CLI の主要メソッド体が selfhost builder で MIR 関数化できる。
|
|||
|
|
|
|||
|
|
### 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)とスモーク/テストを更新する。***
|