2025-11-15 22:32:13 +09:00
# Phase 25.1b — Selfhost Builder Parity (Planning → Design Deep‑ Dive)
2025-11-16 03:11:49 +09:00
Status: Step0〜3 実装済み・Step4( Method/Extern) 実装フェーズ
2025-11-15 22:32:13 +09:00
## ゴール
- 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` 等をファイル結合前に解決。
2025-11-17 00:48:18 +09:00
- Stage‑ B entry 側は string literal using を廃止し、`using lang.compiler.entry.using_resolver as Stage1UsingResolverBox` のように module alias を使用する。
#### Stage‑ B func_scan トグルのデフォルト( HAKO_STAGEB_FUNC_SCAN)
- 目的:
- Stage‑ B を直接叩いたときに `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 スモーク以外から Stage‑ B を直接呼び出しても、defs が常に生成される。
- 既存のテストで func_scan を無効化したいケースでは、`HAKO_STAGEB_FUNC_SCAN=0` を明示すれば従来どおり defs をスキップできる。
2025-11-15 22:32:13 +09:00
2025-11-16 03:11:49 +09:00
#### Stage‑ B の安定度と使用上の注意
- 正規経路:
- Stage‑ B は `tools/hakorune_emit_mir.sh` / `tools/selfhost/selfhost_build.sh` 経由で呼び出すことを前提としており、これらのラッパが Stage‑ 3 用 ENV( `NYASH_PARSER_STAGE3=1` / `HAKO_PARSER_STAGE3=1` / `NYASH_PARSER_ALLOW_SEMICOLON=1` など)を一括でセットする。
2025-11-17 00:48:18 +09:00
- Phase 25.1b では「multi-carrier fib などの core 小ケースについては、このラッパ経由で呼ぶ限り Stage‑ B 自体は十分に安定」とみなし、主な改善対象を Program→MIR の selfhost builder 側に置く。
2025-11-16 03:11:49 +09:00
- 手動実行時の注意:
- Stage‑ 3 ENV を立てずに Stage‑ B / VM を直接叩くと、`Undefined variable: local` のようなエラーが発生するが、これは構文/実装バグではなく「Stage‑ 3 キーワード( local など)を Stage‑ 1 と同じルールでパースしてしまっている」ため。
- 詳細な原因と対処は `docs/development/troubleshooting/stage3-local-keyword-guide.md` にまとめてあり、selfhost 開発では「まずラッパスクリプトを使う → 必要な場合のみ ENV を明示して直叩きする」方針とする。
2025-11-17 00:48:18 +09:00
#### Stage‑ B と selfhost CLI canary( HakoCli.run/2) の現状
2025-11-17 03:19:03 +09:00
- selfhost CLI の最小ケース(`tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh` が生成する HakoCli.run サンプル)に対しては、修正前は Stage‑ B 実行中に 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` を直接叩くと、修正前は Stage‑ B が生成した MIR に対して:
2025-11-17 00:48:18 +09:00
- `Stage1UsingResolverBox._collect_using_entries/1`
- `ParserStringUtilsBox.skip_ws/2`
- `ParserIdentScanBox.scan_ident/2`
- `ParserBox.parse_stmt2/2`
2025-11-17 03:19:03 +09:00
- などに `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] ... })` ) から「Stage‑ B Main.main 内の `ParserBox.length()` 呼び出しにおいて recv スロット( ValueId(17)) が定義されていない」ことが分かっている。これは verifier がまだチェックしていない「Callee.receiver 側の SSA 漏れ」であり、Phase 25.1c の Stage‑ B / 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 自体はもはやブロッカーではない)。
2025-11-17 00:48:18 +09:00
- 対応方針( Phase 25.1b 時点):
2025-11-17 03:19:03 +09:00
- BoxTypeInspector / multi‑ carrier LoopForm 経路とは独立した **Stage‑ B/MIR 側の SSA/ 型システム/ Box 解決の構造問題** として扱い、selfhost CLI canary( HakoCli.run/2 lowering) はこれらが片付くまでは「25.1c の構造タスク待ち」として扱う。
- `tools/hakorune_emit_mir.sh` の `diagnose_stageb_failure()` は維持し、Stage‑ B の標準出力に `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` を含めてエラー文字列を出すようになったため、Stage‑ B 由来の undefined value は「どの関数のどの Call( どの recv) 」で発生しているかを 1 行で特定できる。
2025-11-17 00:48:18 +09:00
2025-11-15 22:32:13 +09:00
### 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` )へフォールバックする。
2025-11-16 03:11:49 +09:00
- 現時点での不足点( 要約・2025-11-16) :
- `FuncBodyBasicLowerBox` が本番でカバーできているのは、Local/If/Return の基本形+ LoopForm 正規化済み Loop+ ごく一部の MethodCall( `args.size/get` と `String.length` の Return 形) に限られる。Stage1 CLI のような複雑な関数本体(複数 Local+ If ネスト+ Loop+ Method/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` の run( usage/unknown ログ付き)は shape mismatch で selfhost 降ろしの対象外。ここを Stage1 実形に合わせて広げることが Phase 25.1b の中心タスクになっている。
2025-11-15 22:32:13 +09:00
### 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 する。
2025-11-16 03:11:49 +09:00
#### 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 canary( rc チェック)で確認する。
- ガード:
- 新しい 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`
- 目的:
2025-11-17 00:48:18 +09:00
- 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`
- Stage‑ B で `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) 。
2025-11-16 03:11:49 +09:00
- 今後の拡張:
2025-11-17 00:48:18 +09:00
- `LoopFormBox.build_loop_multi_carrier` の `limit_kind=param` 実装を一般化し(現在は param register コピー → `reg_limit` 初期化まで対応済み) 、break/continue 付き multi-carrier も下ろせるようにする。
2025-11-16 03:11:49 +09:00
- 代表ケースとして `tools/smokes/v2/profiles/quick/core/vm_loop_phi_multi_carriers.sh` と同型の .hako を selfhost-first で通す canary を追加し、VM/EXE rc を Rust オラクルと比較する。
2025-11-15 22:32:13 +09:00
## 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:*]` タグがそのまま表示されることを確認済み(追加改修なし)。
2025-11-17 00:48:18 +09:00
- Phase 25.1b 以降、`HAKO_SELFHOST_BUILDER_FIRST=1` で呼び出された場合は「Stage‑ B → selfhost builder( + Stage1 CLI) 」経路のみを試行し、selfhost builder が失敗した場合は即座に非0で終了する。selfhost-first モードでは `env.mirbuilder.emit` / provider delegate へのフォールバックは行わず、MirBuilder の未整備を隠さない方針とする( provider 経路を使うときは `HAKO_SELFHOST_BUILDER_FIRST=0` を明示)。
2025-11-15 22:32:13 +09:00
- 成果物:
- selfhost-first で Stage1 CLI を通したときに、どの関数/構造がまだ未サポートなのかがログで推測できる状態。
2025-11-17 00:48:18 +09:00
#### 補足: Ny selfhost パイプラインとの関係( Phase 25.1b 時点)
- `.hako → Program(JSON v0) → MIR(JSON)` のメイン経路は、Stage‑ B( `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` 経由の Stage‑ B/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/Stage‑ 3/using 周りに無限ループ相当のバグが残っている疑いがある。
- Phase 25.1b では `.hako` selfhost builder から Ny selfhost 経路を切り離すことを優先し、inline 経路のバグは `[ny-compiler] inline timeout ...` + `[ny-inline:hint]` ( stdout/stderr の head を添える) で可視化したうえで、後続フェーズ( 25.1c 以降)の構造タスクとして扱う。
2025-11-15 22:32:13 +09:00
### 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) の受理
2025-11-15 22:40:12 +09:00
- 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-Fast( Rust providerに退避可能) 。
2025-11-15 22:32:13 +09:00
- 成果物:
2025-11-15 22:40:12 +09:00
- `cmd_build_exe` の`loop(i < argc)` 等、Stage1 CLIの代表的なwhile/forパターンをselfhost builderで通せる基礎が整った。
2025-11-17 00:48:18 +09:00
- 追加アップデート( 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,...]` を出して処理できる。
2025-11-15 22:40:12 +09:00
- 次のステップ: LoopForm対応の動作確認スモークテスト追加、Step4( MethodCall/ExternCall) へ進む。
2025-11-15 22:32:13 +09:00
2025-11-17 00:48:18 +09:00
#### Step 3.1 — Box 型情報 API( Rust Parity) ★New
- 背景:
- Stage‑ 3 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 時点): Stage‑ 3 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 実装までが end‑ to‑ end で動作することを確認済み。
- fib multi‑ carrier 経路と selfhost multi‑ carrier smoke 用の canary ケース(`tools/smokes/v2/profiles/quick/core/phase251/selfhost_mir_loopform_multi_carrier_vm.sh` ) は、2025‑ 11‑ 16 時点で `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 経路完了 / multi‑ carrier selfhost-first canary PASS」とみなす。
2025-11-16 03:11:49 +09:00
### Step 4 — MethodCall / ExternCall パリティ( 設計メモ・Rust層読解込み)
- Status: design-only( Rust 層の挙動を踏まえた設計まで)
- 目的: `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 メソッド呼び出し)
- Stage‑ B 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-call( me.cmd_*)の解決**
- Stage1 CLI の `me.cmd_*` は、Stage‑ B の 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. **ExternCall( hostbridge.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 に退避できるようにする。
2025-11-17 00:48:18 +09:00
#### 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) のパスを返す。
- Bridge( JSON v0 → MIR) の特別扱い:
2025-11-19 10:08:04 +09:00
- `src/runner/json_v0_bridge/lowering/expr.rs` / `lowering.rs` :
2025-11-17 00:48:18 +09:00
- `MapVars::resolve` :
- `hostbridge` / `env` を特殊変数として扱い、それぞれ Const(String) `"hostbridge"` / `"env"` を生成する( Method チェーンを降ろすためのプレースホルダ)。
2025-11-19 10:08:04 +09:00
- `me` については、Bridge 環境の `allow_me_dummy` が ON のときだけ NewBox を注入する(通常は JSON defs 側で明示パラメータとして扱う)。
2025-11-17 00:48:18 +09:00
- `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` に正規化。
2025-11-19 10:08:04 +09:00
- defs 降下(`lowering.rs` ) :
- JSON v0 の `defs` に対して、`box_name != "Main"` の関数を **インスタンスメソッド** とみなし、
- `signature.params` に「暗黙 `me` + 明示パラメータ」を載せる。
- `func_var_map` に `me` → `func.params[0]` を、残りのパラメータ名を `params[1..]` にバインドする。
- これにより Stage‑ B / Stage‑ 1 側で `_build_module_map()` のような「params: [] だが `me` を使う」メソッドでも、
Rust VM 実行時に `me` 未定義にならず、BoxCall が正しく解決されるようになった。
2025-11-17 00:48:18 +09:00
2025-11-19 23:12:01 +09:00
### IfForm / empty else-branch の SSA fix( Stage‑ 1 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 ブロックで古い ValueId( PHI 適用前の値)を参照し、`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 で確認。
2025-11-17 00:48:18 +09:00
- 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 側に新ルールは追加しない)方針とする。
2025-11-16 03:11:49 +09:00
- 実装イメージ( 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 エントリを含むか」を把握できるようにする(観測専用)。
2025-11-15 22:32:13 +09:00
- 作業:
2025-11-16 03:11:49 +09:00
- `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 はあくまで ring1( Stage1 CLI ソース)の Program(JSON v0) を観測するだけで、ring0( Rust 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) から把握し、将来の専用 lower( CliRunLowerBox) が安全に使えるかを事前に観測する。
- 作業:
- `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 と同様、ring1( Stage1 Hako CLI) の構造を観測する箱のみを追加する。
- MIR 生成はまだ Rust provider /既存 lower に任せており、ring0 の責務( env.* extern や実行エンジン)にも影響を与えない。
- 専用 lower( `CliRunLowerBox` が実際に MIR を返す形) は、Stage‑ B 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 にフォールバックする( Fail‑ Fast) 。
- 対象とする 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. タグと Fail‑ Fast( 実装済み)
- 形が完全に一致し、`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 本体には適用していない(今後 Stage‑ B Program(JSON v0) を詳細に比較しながら対応範囲を広げる)。
#### Stage1 HakoCli.run( 本番) とのギャップ整理( provider MIR ベース)
現状、このホスト環境では Stage‑ B 実行中に `Undefined variable: local` ( Stage‑ 3 キーワード)で 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 に寄せる予定)。
今後 Stage‑ B Program(JSON v0) が安定して取れるようになったら、上記の差分を JSON レベルで再確認し、
- usage/unknown の `print` ブロックを「前置/後置のサイドエフェクト」として `_check_shape` の許容パターンに追加するか、
- あるいは run 本体を「MVP サブセット(引数分岐)+印字専用ブロック」に分けて扱うか、
を決める予定。
#### Stage1 用 run パターン拡張方針(設計)
Stage1 launcher.hako の本番 `HakoCli.run` を selfhost lowering の対象に含めるための方針は次のとおり:
- 目的:
- Rust CLI( Stage0) と同じ意味論( 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) でガードし、
かつ Stage‑ B Program(JSON v0) で `check_core_shape` / `check_logging_shape` の両方を満たしている場合だけ有効にする。
- それ以外のケース( print の形がずれている / 追加の case が増えたなど)は、今まで通り provider 経路に退避する。
2025-11-15 22:32:13 +09:00
### 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でも可) 。
2025-11-16 03:11:49 +09:00
作業順は 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/get( params ベース 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 は Stage‑ B の 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 で実際に使われているメソッドは、FileBox( open/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 builder( provider-first) 経路を確認するスモーク。
- `*_selfhost_vm.sh` … 同じ .hako / 期待 rc を selfhost-first( `HAKO_SELFHOST_BUILDER_FIRST=1` )で確認するスモーク。
- ループ系:
- 例として `phase2100` の LoopForm/PHI canary( Rust ベース)に対応して、
- `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 プロファイルへの昇格を検討する。
***