# Phase 25.1b — Selfhost Builder Parity (Planning → Design Deep‑Dive) Status: Step0〜3 実装済み・Step4(Method/Extern)実装フェーズ ## ゴール - 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 を使用する。 #### 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 をスキップできる。 #### 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` など)を一括でセットする。 - Phase 25.1b では「multi-carrier fib などの core 小ケースについては、このラッパ経由で呼ぶ限り Stage‑B 自体は十分に安定」とみなし、主な改善対象を Program→MIR の selfhost builder 側に置く。 - 手動実行時の注意: - Stage‑3 ENV を立てずに Stage‑B / VM を直接叩くと、`Undefined variable: local` のようなエラーが発生するが、これは構文/実装バグではなく「Stage‑3 キーワード(local など)を Stage‑1 と同じルールでパースしてしまっている」ため。 - 詳細な原因と対処は `docs/development/troubleshooting/stage3-local-keyword-guide.md` にまとめてあり、selfhost 開発では「まずラッパスクリプトを使う → 必要な場合のみ ENV を明示して直叩きする」方針とする。 #### Stage‑B と selfhost CLI canary(HakoCli.run/2)の現状 - 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 に対して: - `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] ... })`)から「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 自体はもはやブロッカーではない)。 - 対応方針(Phase 25.1b 時点): - 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 行で特定できる。 ### 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): - `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 の中心タスクになっている。 ### 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 する。 #### 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` - 目的: - 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)。 - 今後の拡張: - `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)をスモークで検証。 ## 設計 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:]` のタグを付与。 - `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` で呼び出された場合は「Stage‑B → selfhost builder(+Stage1 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)` のメイン経路は、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 以降)の構造タスクとして扱う。 ### 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)の受理 - 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に退避可能)。 - 成果物: - `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対応の動作確認スモークテスト追加、Step4(MethodCall/ExternCall)へ進む。 #### 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」とみなす。 ### 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 に退避できるようにする。 #### 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)の特別扱い: - `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..]` にバインドする。 - これにより Stage‑B / Stage‑1 側で `_build_module_map()` のような「params: [] だが `me` を使う」メソッドでも、 Rust VM 実行時に `me` 未定義にならず、BoxCall が正しく解決されるようになった。 ### 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 で確認。 - 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 はあくまで 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=` を出力し、戻り値として `{"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 [options]` を `print` してから `ret 1`。 - サブコマンド unknown の場合: - MVP: 単純に `return 2`。 - 実装: `[hakorune] unknown command: ` を `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: ` を出力してから `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 経路に退避する。 ### 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/get(params ベース receiver)と StringBox.length(引数なし)を最小カバー済み。生成される MIR は `mir_call { callee: { type: "Method", box_name: "", 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 プロファイルへの昇格を検討する。 ***