Phase 25.1c/k: Fix ValueId undefined errors in loops with body-local variables **Problem:** - FuncScannerBox.scan_all_boxes/1 and BreakFinderBox._find_loops/2 had ValueId undefined errors for variables declared inside loop bodies - LoopFormBuilder only generated PHIs for preheader variables, missing body-locals - Example: `local ch = s.substring(i, i+1)` inside loop → undefined on next iteration **Solution:** 1. **Rust AST path** (src/mir/loop_builder.rs): - Detect body-local variables by comparing body_end_vars vs current_vars - Generate empty PHI nodes at loop header for body-local variables - Seal PHIs with latch + continue snapshot inputs after seal_phis() - Added HAKO_LOOP_PHI_TRACE=1 logging for debugging 2. **JSON v0 path** (already fixed in previous session): - src/runner/json_v0_bridge/lowering/loop_.rs handles body-locals - Uses same strategy but for JSON v0 bridge lowering **Results:** - ✅ FuncScannerBox.scan_all_boxes: 41 body-local PHIs generated - ✅ Main.main (demo harness): 23 body-local PHIs generated - ⚠️ Still some ValueId undefined errors remaining (exit PHI issue) **Files changed:** - src/mir/loop_builder.rs: body-local PHI generation logic - lang/src/compiler/entry/func_scanner.hako: debug logging - /tmp/stageb_funcscan_demo.hako: test harness 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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 側の前提を崩さない)。
- Stage‑B を直接叩いたときに
- 実装(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‑B は
- 手動実行時の注意:
- 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‑3 ENV を立てずに Stage‑B / VM を直接叩くと、
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/1ParserStringUtilsBox.skip_ws/2ParserIdentScanBox.scan_ident/2ParserBox.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 サンプルに対して残っている課題は:
-
- 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 整理タスクで修正する前提。
- Rust VM 実行時に
-
if args { ... }まわりの truthy 判定(ArrayBox を boolean 条件に使っている部分)の扱いに起因する型/意味論の揺れが残っており、こちらも 25.1c の型システム整理タスクでArrayBoxの truthy 規約を明文化した上で揃える想定。
-
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")がGlobalcallee に解決できるようにする。- Loop / branch / compare / Array/Map 操作など Stage1 CLI で出現する構造を包括的に lowering するため、
lang/src/mir/builder/internal/*の helper を本番経路に統合する。
- JSON 出力:
- 現状の
"{\"functions\":[...]}\"ベタ書きから、jsonfrag 等を用いた構造的出力に切り替え、複数関数を同一モジュールに含められるようにする。 - 既存の mini パターン用 JSON 組み立てとの互換性を維持しつつ、Stage1 CLI で必要な関数数に耐えられる形に拡張する。
- 現状の
- 運用ポリシー:
- Phase 25.1b 中は
HAKO_SELFHOST_BUILDER_FIRST=0のまま(provider-first)とし、selfhost builder が Stage1 CLI を lowering し切れることを確認した時点で=1への切り替えを検討する。 - lambda/FunctionBox (
NewClosure等) は本フェーズでは扱わず、従来どおり builder からは排除したままにする(別フェーズ 25.2b に委ねる)。
- Phase 25.1b 中は
Guardrails / ポリシー
- Rust Freeze Policy:
- Rust 側の Program→MIR 実装には原則手を入れず、selfhost builder は「Rust の既存挙動に合わせる」方向で実装する。
- 変更は Hakorune 側 (
lang/src/mir/builder/*) とツール (tools/hakorune_emit_mir.sh) に閉じる。
- Fail‑Fast:
- selfhost builder が Program(JSON) の一部に対応していない場合は、明確なタグ付きで失敗させる(例:
[builder/selfhost-first:unsupported:Match])ようにし、silent stub には戻さない。 - provider 経路は退避路として残しつつ、Stage1 CLI の代表ケースでは selfhost builder が先に成功することを目標にする。
- selfhost builder が Program(JSON) の一部に対応していない場合は、明確なタグ付きで失敗させる(例:
LoopForm / PHI ポリシー(重要メモ)
- ループを含む関数本体を selfhost builder で扱うときは、LoopForm 正規化を前提にする:
- 可能な限り
docs/guides/loopform.mdで定義された「キャリア+1個の φ」モデルに従う。 - break/continue を含むループは、LoopForm の制約(更新変数最大2個・セグメント整列など)を満たす範囲でのみ lowering 対象にする。
- 可能な限り
- MirBuilder 側で「生の while/for を直接 MIR の PHI に落とす」ような ad‑hoc 実装は行わない:
- PHI ノードの生成・配置は既存の LoopForm/LowerLoop 系 helper(
loop_scan_box.hako/lower_loop_*_box.hakoなど)に一元化し、builder 本体はそれを利用する立場にとどめる。 - LLVM harness 側の PHI 不変条件(ブロック先頭グルーピング/well‑typed incoming)を崩さない。
- PHI ノードの生成・配置は既存の LoopForm/LowerLoop 系 helper(
- Phase 25.1b では:
- まず LoopForm 前提で安全に扱える最小のループ形(既存 selfhost テストでカバー済みの while/for)から対応し、
- LoopForm 未適用の複雑なループ(例: キャリア3変数以上・ネストが深いもの)は
[builder/selfhost-first:unsupported:loopform]タグなどで Fail‑Fast する。
LoopForm 複雑ケースへの拡張方針(Rust builder をオラクルに使う)
- ねらい:
- 複雑な LoopForm(キャリア複数・条件付き更新など)については、Rust 側 MirBuilder/LoopForm 実装を「正解(オラクル)」として扱い、Hakorune 側の
LowerLoop*Box群をそれに追従させる。 - Hakorune 側は LoopForm の設計や PHI 配線を再実装せず、「入力 JSON のパターンマッチ+既存 LowerLoop* の呼び出し」に専念する。
- 複雑な LoopForm(キャリア複数・条件付き更新など)については、Rust 側 MirBuilder/LoopForm 実装を「正解(オラクル)」として扱い、Hakorune 側の
- 手順イメージ:
- 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) を採取する。 - 同じ .hako を selfhost-first(
HAKO_SELFHOST_BUILDER_FIRST=1 HAKO_MIR_BUILDER_FUNCS=1 HAKO_SELFHOST_TRACE=1)で通し、LowerLoop*Boxがどこまで拾えているか/どのケースが[builder/funcs:unsupported:loopform]になっているかを観測する。 - 差分が出ているループだけを対象に、小さな
LowerLoopXXXBox(または既存 LowerLoop* の強化)を追加する。 - ループの意味論差異(キャリア更新・退出条件・rc)が出ていないかは、VM/EXE canary(rc チェック)で確認する。
- Rust 側 loop スモーク(例:
- ガード:
- 新しい LoopForm 対応はすべて既存
lower_loop_*_box.hako群の中に閉じ込め、FuncLowering/MirBuilder 本体では依然として「LoopForm 結果を_rebindで名前付けするだけ」にとどめる。 - Rust 側の LoopForm/PHI 実装を変えずに selfhost 側のカバー率だけを上げるのが Phase 25.1b の範囲。
- 新しい LoopForm 対応はすべて既存
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") に委譲する。
- Fibonacci 風の「multi-carrier」ループ(
- 現段階の挙動(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 経路へ退避。
- Program(JSON v0) 内で
- スモーク:
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)。
- Stage‑B で
- 今後の拡張:
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)
- 調査:
- Rust 側
program_json_to_mir_json_with_importsの挙動をトレースし、どの AST ノードがどの MIR に降りているかを整理(特に defs/call/loop/boxcall)。 - selfhost builder の現行 JSON 生成経路を洗い出し、stub を生成している箇所を特定。
- Rust 側
- 設計:
Program.defs→ MIR 関数生成のインタフェース(必要なフィールドと lowering 手順)を定義。- call resolve 用の軽量マップ(
name -> qualified)を selfhost builder に導入する。
- 実装:
- defs 対応・call resolve・loop/branch lowering を段階的に導入しつつ、各ステップで mini スモークを追加。
- jsonfrag ベースの出力に切り替えながら、既存の mini テストを全て通ることを確認。
- 検証:
tools/hakorune_emit_mir.sh lang/src/runner/launcher.hako …をHAKO_SELFHOST_BUILDER_FIRST=1で実行し、62KB クラスの MIR(JSON) が selfhost builder 単独で得られることを確認。tools/selfhost/build_stage1.shを selfhost-first でビルドし、Stage1 CLI EXE がemit/buildコマンドを正しく実行できるか(JSON stdout + exit code)をスモークで検証。
設計 TODO(FuncLoweringBox / MirBuilderBox 拡張の方向性)
※ ここから先は「具体的にどこをどう広げるか」の設計メモ。実装はまだ行わない。
-
FuncLowering の対応範囲拡張
_lower_func_bodyを Stage1 CLI で実際に使われているパターンまで広げる:- 単一 Return だけでなく、ローカル変数定義+if 分岐+loop を含む「典型的な CLI ハンドラ」の形をサポート。
MethodCall(args.size()/args.get(i)/FileBox.open/read/write/ArrayBox.pushなど)を MIRmir_callかcallに落とす処理を追加。
func_map/resolve_call_targetを用いて、HakoCli.cmd_emit_*/cmd_build_exeなどの内部 Call をGlobal("HakoCli.cmd_emit_*")系に正規化。
-
MirBuilder 本体の出力構造
- これまでの「Program 全体を 1 関数 main に落とす」前提から、「Program.body + defs を multi‑function MIR モジュールに落とす」前提へシフト:
Program.bodyからはエントリMain.main/1相当の MIR 関数を生成。Program.defsからはHakoCli.*などの補助関数を生成。
_norm_if_apply/inject_funcsの役割を整理し、「main 関数を含まない defs‑only モジュール」を返さないように Fail‑Fast する。
- これまでの「Program 全体を 1 関数 main に落とす」前提から、「Program.body + defs を multi‑function MIR モジュールに落とす」前提へシフト:
-
Fail‑Fast とデバッグ
- Stage1 CLI のような大きな Program(JSON) に対して selfhost builder が未対応の場合は:
[builder/selfhost-first:unsupported:func_body]などのタグ付きで明示的に失敗。- provider 経路(
env.mirbuilder.emit)へのフォールバックは維持するが、「空 EXE になる stub MIR」は生成しない方針に切り替える。
HAKO_SELFHOST_TRACE=1時に、FuncLowering がどの def でどこまで lowering できたかをログに出す。
- Stage1 CLI のような大きな Program(JSON) に対して selfhost builder が未対応の場合は:
-
検証計画
- selfhost‑first canary:
stage1_launcher_program_to_mir_canary_vm.shの selfhost‑first 版を追加し、HAKO_SELFHOST_BUILDER_FIRST=1+MirBuilderBox.emit_from_program_json_v0だけで 60KB 級の MIR(JSON) を生成できることを確認。
- Stage1 build:
tools/selfhost/build_stage1.shを selfhost-first で回し、生成された Stage1 EXE に対してemit program-json/emit mir-json/build exeスモークを追加。
- selfhost‑first canary:
このファイルは引き続き「Phase 25.1b の計画メモ(設計ディープダイブ)」として扱い、実装は Phase 25.1a の安定化完了後に、小さな差分に分割して順次進める。***
実装計画(順番にやる TODO)
備考: ここでは Phase 25.1b を「複数の最小ステップ」に分解して、順番/ゴール/ガードを具体的にメモしておくにゃ。
Step 0 — Fail‑Fast・観測を揃える
- Status: implemented (2025-11-15).
MirBuilderBoxnow tagsdefs_only/no_matchfailures and aborts, andFuncLoweringBoxlogs unsupported defs whenHAKO_SELFHOST_TRACE=1. - 目的: 既存の selfhost builder がどこで諦めているかを正確に観測し、stub MIR を返さずに Fail させる導線を整える。
- 作業:
MirBuilderBox.emit_from_program_json_v0func_defs_mirだけが非空だった場合でも黙って{ "functions": [defs] }を返さず、[builder/selfhost-first:unsupported:defs_only]を出す。- internal lowers がすべて
nullの場合、[builder/selfhost-first:unsupported:<reason>]のタグを付与。
FuncLoweringBox.lower_func_defs- どの関数名で
_lower_func_bodyがnullを返したかをHAKO_SELFHOST_TRACE=1でログ出力。
- どの関数名で
tools/hakorune_emit_mir.sh- 既存の head/tail ログ出力で
[builder/selfhost-first:*]タグがそのまま表示されることを確認済み(追加改修なし)。 - 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を明示)。
- 既存の head/tail ログ出力で
- 成果物:
- 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で起動)。
- Phase 25.1b で「明示 opt-in」(既定=0)に変更。Runner は
- 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 では
.hakoselfhost 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).
FuncBodyBasicLowerBoxnow callsLowerLoopSumBcBox/LowerLoopSimpleBoxfrom_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。
- Loop判定 →
lowerメソッド冒頭でLoop優先処理:- Loop含む場合は
_try_lower_loopを呼び、成功/失敗で明確に分岐。 - Loopが無い場合のみ既存のLocal/If/Return処理に進む。
- Loop含む場合は
- 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」を用意して文字列ハックを撤廃する必要がある。
- Stage‑3 VM では
- 設計方針:
lang/src/shared/common/box_type_inspector_box.hakoを追加し、BoxTypeInspectorBox.kind(value)/is_map(value)/is_array(value)等の API を提供する。- 実装は Stage0 Rust 側に
env.box_introspect(kind, value)的な extern を追加し、hostbridge.extern_invoke("env.box_introspect","kind",[value])で種別名(例:"MapBox","ArrayBox","Int")を返す。 BoxHelpers/JsonEmitBox/LoopOptsBoxなど、Box 種別チェックが必要な箇所はすべてこの API に置き換え、"" + valueを一切使わない。- 返り値は最小で
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.kindprovider → 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.kindprovider 経路+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)
- Stage1 CLI (
-
設計方針:
-
MethodCall → mir_call(Method)(box メソッド呼び出し)
- Stage‑B Program(JSON v0) での形(実測):
return arr.size()は defs 内で次のように現れる:{"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)は:{"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 形を使う:{"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仕様と揃える。
- Stage‑B Program(JSON v0) での形(実測):
-
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に揃えるところまでに留める(広げるのは後続フェーズ)。
- Phase 25.1b ではまず
- Stage1 CLI の
-
ExternCall(hostbridge.extern_invoke)の扱い
- Rust 側では
hostbridge.extern_invoke("env.codegen","emit_object",args)等を特別扱いし、C-ABI 経由でenv.codegenprovider にルーティングしている。 - 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に落とす:{"op":"externcall","func":"env.codegen.emit_object","args":[ /* regs */ ]}
- ここでは「すべての extern を一般化する」のではなく、Stage1 CLI が実際に使っている env 名とメソッド名だけを point fix する(Rust Freeze Policy に従い、意味論は Rust 版を真似るが範囲は狭く保つ)。
- Rust 側では
-
未対応パターンの Fail-Fast
- MethodCall/ExternCall の lowering 中に、
- 複雑なオブジェクト式(ネストした MethodCall/Array/Map リテラルなど)、
- 引数に対応していない型(lambda など)、
- 未サポートの env 名 / メソッド名(
env.codegen以外)、 が見つかった場合は、[builder/funcs:unsupported:call]タグを出してnullで戻る。
- これにより、「知らない形をなんとなく MIR にする」ことを避け、Rust provider や legacy CLI delegate に退避できるようにする。
- MethodCall/ExternCall の lowering 中に、
-
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.rsemit_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)。
- ExternCall 形(
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 に委譲)。
- MIR ビルダ(呼び出し生成):
-
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 が正しく解決されるようになった。
- JSON v0 の
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 を引き起こしていた。
- merge ブロックで古い ValueId(PHI 適用前の値)を参照し、
- 修正後は、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 の設計メモと揃える。
- Hako 側では「どの Box のどのメソッドを MIR の
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::*という構造になっている。
- Rust では最終的に
- 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):
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 生成。
_lower_func_bodyの冒頭か、既存Return(Call)の前後でこれら helper を呼び出し、マッチした場合のみ MIR を返す。- Tag/ログ:
HAKO_SELFHOST_TRACE=1時に[funcs/basic:method.*]/[funcs/basic:extern.*]trace を出し、どの defs が Method/Extern 経由で lowering されたか観測できるようにする。
- スモーク:
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=<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 以降で実装する前提。
- 現段階では stub 実装:
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 想定):
Local argc = Int(0)If cond Var("args") then { Local argc = Method Var("args").size() }If cond Compare(Var("argc") == Int(0)) then { Return Int(1) }Local cmd_raw = Method Var("args").get(Int(0))Local cmd = Binary("+", Str(""), Var("cmd_raw"))- 連続する
Ifでcmd == "run"|"build"|"emit"|"check"を判定し、それぞれReturn Call("cmd_*", [me,args])を持つ。 - 最後に
Return Int(2)(unknown command)を持つ。
- 実装状況(CliRunLowerBox 内):
- ターゲット判定(実装済み)
box_name=="HakoCli" && func_name=="run"以外は即null。
- 構造パターンの検証
_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]を出す。
- 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)。
- argc を
- 環境変数
HAKO_MIR_BUILDER_CLI_RUN=1のときにだけ_emit_mirを呼び、それ以外は shape OK でもnullを返して provider/既存 lower にフォールバックする(既定挙動は不変)。
- タグと 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 が引き継ぎ)。
- 形が完全に一致し、
- 現在のカバレッジ:
.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。
- MVP:
- サブコマンド unknown の場合:
- MVP: 単純に
return 2。 - 実装:
[hakorune] unknown command: <cmd>をprintしてからret 2。
- MVP: 単純に
- 比較まわり:
- 両者とも
"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)は「サイドエフェクトのある前置/後置ブロック」として明示的に扱い、分岐ロジック本体とは分離して考える。
-
拡張の方向性(案):
- 前置 usage ブロックの許容
_check_shapeで「argc==0 → usage print → ret 1」という形を、
「argc == 0の then 側にStringBoxconst /binop/call(print)が含まれてもよい」
というルールに緩和する。- lowering 時には:
- まず usage 用の文字列構築+
printをそのまま MIR に反映(boxcall/externcall env.console.logなど)。 - そのあとで
ret 1を emit する(MVP では usage 文言は provider MIR に揃える)。
- まず usage 用の文字列構築+
- unknown ブロックの後置許容
- MVP では「unknown なら
ret 2」のみを扱っているが、本番では
[hakorune] unknown command: <cmd>を出力してからret 2している。 _check_shapeを「末尾の Return Int(2) の前に StringBox/binop/printパターンが挟まっていてもよい」と解釈できるようにし、
lowering 側でもそれをそのまま MIR に降ろす(print ブロック +ret 2)。
- MVP では「unknown なら
- run 本体(分岐ロジック)との分離
_check_shapeを二層に分ける:check_core_shape… argc/args/cmd/サブコマンド分岐の「副作用なし」部分の形状チェック。check_logging_shape… usage/unknown の印字パターンのみを許容する緩いチェック。
CliRunLowerBoxはまず core を_emit_mir_coreで生成し、logging 部分は必要に応じて前後にブロックを足す形で統合する。
- 前置 usage ブロックの許容
-
トグルと適用範囲:
- Stage1 本番への適用は常に
HAKO_MIR_BUILDER_CLI_RUN=1(既定 OFF)でガードし、
かつ Stage‑B Program(JSON v0) でcheck_core_shape/check_logging_shapeの両方を満たしている場合だけ有効にする。 - それ以外のケース(print の形がずれている / 追加の case が増えたなど)は、今まで通り provider 経路に退避する。
- Stage1 本番への適用は常に
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: "<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 として動作)。
- MethodCall:
- 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 側のメソッドセットを推定するための参考情報として扱う。
- 現行の provider MIR では、Stage1 CLI のメソッド呼び出しはすべて
| 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 / ハーネス側に依存している。
- Stage1 CLI で実際に使われているメソッドは、FileBox(open/read/write/close)、Array/Map 系(size/get/set/push)、String 系(indexOf)など多岐にわたるが、selfhost builder が defs 側で専用に扱えているのは
この表をベースに、今後の小さな 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.shtools/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側に追加する。
- 既存の
- 既存の v2 スモーク構造(
- 運用:
- quick プロファイルでは provider-first スモークを既定 ON とし、selfhost-first スモークは Phase 25.1b 中は任意(開発用)とする。
- selfhost-first スモークが十分に安定し、Stage1 build も selfhost-first で通るようになった時点で、必要に応じて CI quick プロファイルへの昇格を検討する。