Step2実装内容: - FuncBodyBasicLowerBox導入(defs専用下請けモジュール) - _try_lower_local_if_return実装(Local+単純if) - _inline_local_ints実装(軽い正規化) - minimal lowers統合(Return/BinOp/IfCompare/MethodArray系) Fail-Fast体制確立: - MirBuilderBox: defs_onlyでも必ずタグ出力 - [builder/selfhost-first:unsupported:defs_only] - [builder/selfhost-first:unsupported:no_match] Phase構造整備: - Phase 25.1b README新設(Step0-3計画) - Phase 25.2b README新設(次期計画) - UsingResolverBox追加(using system対応準備) スモークテスト: - stage1_launcher_program_to_mir_canary_vm.sh追加 Next: Step3 LoopForm対応 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
18 KiB
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")がGlobalcallee に解決できるようにする。- 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 に委ねる)。
- Phase 25.1b 中は
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 が先に成功することを目標にする。
- selfhost builder が Program(JSON) の一部に対応していない場合は、明確なタグ付きで失敗させる(例:
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)を崩さない。
- PHI ノードの生成・配置は既存の LoopForm/LowerLoop 系 helper(
- Phase 25.1b では:
- まず LoopForm 前提で安全に扱える最小のループ形(既存 selfhost テストでカバー済みの while/for)から対応し、
- LoopForm 未適用の複雑なループ(例: キャリア3変数以上・ネストが深いもの)は
[builder/selfhost-first:unsupported:loopform]タグなどで Fail‑Fast する。
Next Steps(実装フェーズに入るときの TODO)
- 調査:
- Rust 側
program_json_to_mir_json_with_importsの挙動をトレースし、どの AST ノードがどの MIR に降りているかを整理(特に defs/call/loop/boxcall)。 - selfhost builder の現行 JSON 生成経路を洗い出し、stub を生成している箇所を特定。
- Rust 側
- 設計:
Program.defs→ MIR 関数生成のインタフェース(必要なフィールドと lowering 手順)を定義。- call resolve 用の軽量マップ(
name -> qualified)を selfhost builder に導入する。
- 実装:
- defs 対応・call resolve・loop/branch lowering を段階的に導入しつつ、各ステップで mini スモークを追加。
- jsonfrag ベースの出力に切り替えながら、既存の mini テストを全て通ることを確認。
- 検証:
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 拡張の方向性)
※ ここから先は「具体的にどこをどう広げるか」の設計メモ。実装はまだ行わない。
-
FuncLowering の対応範囲拡張
_lower_func_bodyを Stage1 CLI で実際に使われているパターンまで広げる:- 単一 Return だけでなく、ローカル変数定義+if 分岐+loop を含む「典型的な CLI ハンドラ」の形をサポート。
MethodCall(args.size()/args.get(i)/FileBox.open/read/write/ArrayBox.pushなど)を MIRmir_callかcallに落とす処理を追加。
func_map/resolve_call_targetを用いて、HakoCli.cmd_emit_*/cmd_build_exeなどの内部 Call をGlobal("HakoCli.cmd_emit_*")系に正規化。
-
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 する。
- これまでの「Program 全体を 1 関数 main に落とす」前提から、「Program.body + defs を multi‑function MIR モジュールに落とす」前提へシフト:
-
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 できたかをログに出す。
- Stage1 CLI のような大きな Program(JSON) に対して selfhost builder が未対応の場合は:
-
検証計画
- 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スモークを追加。
- selfhost‑first canary:
このファイルは引き続き「Phase 25.1b の計画メモ(設計ディープダイブ)」として扱い、実装は Phase 25.1a の安定化完了後に、小さな差分に分割して順次進める。***
実装計画(順番にやる TODO)
備考: ここでは Phase 25.1b を「複数の最小ステップ」に分解して、順番/ゴール/ガードを具体的にメモしておくにゃ。
Step 0 — Fail‑Fast・観測を揃える
- Status: implemented (2025-11-15).
MirBuilderBoxnow tagsdefs_only/no_matchfailures and aborts, andFuncLoweringBoxlogs unsupported defs whenHAKO_SELFHOST_TRACE=1. - 目的: 既存の selfhost builder がどこで諦めているかを正確に観測し、stub MIR を返さずに Fail させる導線を整える。
- 作業:
MirBuilderBox.emit_from_program_json_v0func_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:*]タグがそのまま表示されることを確認済み(追加改修なし)。
- 既存の head/tail ログ出力で
- 成果物:
- 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)とスモーク/テストを更新する。***