Files
hakorune/docs/development/roadmap/phases/phase-25.1b
nyash-codex 1747ec976c refactor(json_v0_bridge): Phase 25.1p - FunctionDefBuilder箱化+me予約修正
【変更内容】
1. FunctionDefBuilder 箱化(SSOT化)
   - インスタンスメソッド判定の一元化
   - パラメータ ValueId 生成の統一
   - 変数マップ初期化の統一

2. ValueId(0) me 予約バグ修正
   - is_instance_method() で box_name != "Main" 判定
   - インスタンスメソッドは me を ValueId(0) に予約
   - variable_map["me"] = ValueId(0) を自動設定

3. コード削減・可読性向上
   - 60行 → 40行(関数定義処理)
   - 重複ロジック削除
   - デバッグログ追加(is_instance表示)

【効果】
- json_v0_bridge 経路の ValueId(0) 未定義エラー解消
- Stage-B compiler で static box メソッドが正しく動作
- 設計の一貫性向上(me の扱いが明確)

【非スコープ】
- Rust MirBuilder 側は未修正(Phase 26で統一予定)
- lower_static_method_as_function は現状維持

関連: Phase 25.1m (静的メソッド修正), Phase 25.1c/k (SSA修正)

🐱 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 10:08:04 +09:00
..

Phase 25.1b — Selfhost Builder Parity (Planning → Design DeepDive)

Status: Step0〜3 実装済み・Step4Method/Extern実装フェーズ

ゴール

  • Rust 側 Program→MIR (env.mirbuilder.emit) と Hakorune 側 selfhost builder (MirBuilderBox.emit_from_program_json_v0) の機能差を埋め、Stage1 CLIlauncher.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/OJSON stdout + exit codeを Rust/llvmlite と同じ契約に揃える。

現状Phase 25.1a 時点)

StageBProgram(JSON v0) emit

  • compiler_stageb.hakodefs を含む Program(JSON v0) を出力できる:
    • HakoCli.run / HakoCli.cmd_emit_* / HakoCli.cmd_build_* などのメソッドを Program.defs 配列として含む。
    • FuncScannerBox HAKO_STAGEB_FUNC_SCAN=1 により、static box メソッド(暗黙の me 引数付き)も defs に載る。
  • using 解決:
    • Stage1UsingResolverBoxlang/src/compiler/entry/using_resolver_box.hako HAKO_STAGEB_MODULES_LISTnyash.toml[modules] を参照し、using lang.mir.builder.MirBuilderBox 等をファイル結合前に解決。
  • StageB entry 側は string literal using を廃止し、using lang.compiler.entry.using_resolver as Stage1UsingResolverBox のように module alias を使用する。

StageB func_scan トグルのデフォルトHAKO_STAGEB_FUNC_SCAN

  • 目的:
    • StageB を直接叩いたときに HAKO_STAGEB_FUNC_SCAN を立て忘れても、HakoCli.runTestBox.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 スモーク以外から StageB を直接呼び出しても、defs が常に生成される。
      • 既存のテストで func_scan を無効化したいケースでは、HAKO_STAGEB_FUNC_SCAN=0 を明示すれば従来どおり defs をスキップできる。

StageB の安定度と使用上の注意

  • 正規経路:
    • StageB は tools/hakorune_emit_mir.sh / tools/selfhost/selfhost_build.sh 経由で呼び出すことを前提としており、これらのラッパが Stage3 用 ENVNYASH_PARSER_STAGE3=1 / HAKO_PARSER_STAGE3=1 / NYASH_PARSER_ALLOW_SEMICOLON=1 など)を一括でセットする。
    • Phase 25.1b では「multi-carrier fib などの core 小ケースについては、このラッパ経由で呼ぶ限り StageB 自体は十分に安定」とみなし、主な改善対象を Program→MIR の selfhost builder 側に置く。
  • 手動実行時の注意:
    • Stage3 ENV を立てずに StageB / VM を直接叩くと、Undefined variable: local のようなエラーが発生するが、これは構文/実装バグではなく「Stage3 キーワードlocal など)を Stage1 と同じルールでパースしてしまっている」ため。
    • 詳細な原因と対処は docs/development/troubleshooting/stage3-local-keyword-guide.md にまとめてあり、selfhost 開発では「まずラッパスクリプトを使う → 必要な場合のみ ENV を明示して直叩きする」方針とする。

StageB と selfhost CLI canaryHakoCli.run/2の現状

  • selfhost CLI の最小ケース(tools/smokes/v2/profiles/quick/core/phase251/selfhost_cli_run_basic_vm.sh が生成する HakoCli.run サンプル)に対しては、修正前は StageB 実行中に 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 を直接叩くと、修正前は StageB が生成した 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] ... })から「StageB Main.main 内の ParserBox.length() 呼び出しにおいて recv スロットValueId(17)) が定義されていない」ことが分かっている。これは verifier がまだチェックしていない「Callee.receiver 側の SSA 漏れ」であり、Phase 25.1c の StageB / LoopBuilder / LocalSSA 整理タスクで修正する前提。
      1. if args { ... } まわりの truthy 判定ArrayBox を boolean 条件に使っている部分)の扱いに起因する型/意味論の揺れが残っており、こちらも 25.1c の型システム整理タスクで ArrayBox の truthy 規約を明文化した上で揃える想定。
      1. NewBox HakoCli が plugin 前提で解決されてしまう問題は、VM 側の static box factory 統合(静的 Box を User factory にも広告する)により解消済みであり、NYASH_DISABLE_PLUGINS=1 でも静的 Box として HakoCli を生成できるようになっているselfhost CLI canary では NewBox 自体はもはやブロッカーではない)。
  • 対応方針Phase 25.1b 時点):
    • BoxTypeInspector / multicarrier LoopForm 経路とは独立した StageB/MIR 側の SSA型システムBox 解決の構造問題 として扱い、selfhost CLI canaryHakoCli.run/2 loweringはこれらが片付くまでは「25.1c の構造タスク待ち」として扱う。
    • tools/hakorune_emit_mir.shdiagnose_stageb_failure() は維持し、StageB の標準出力に Invalid value: use of undefined value が含まれている場合には NYASH_VM_VERIFY_MIR=1compiler_stageb.hako 直叩き、および docs/private/roadmap/phases/phase-20.33/DEBUG.md への導線を表示する。加えて、VM 側の MirInterpreter::reg_loadfn / last_block / last_inst を含めてエラー文字列を出すようになったため、StageB 由来の undefined value は「どの関数のどの Callどの recv」で発生しているかを 1 行で特定できる。

Rust provider (env.mirbuilder.emit)

  • program_json_to_mir_json_with_imports:
    • Program.bodyProgram.defs の両方を受理し、FuncDefV0 から func_map を構築して Call を解決。
    • HAKO_MIRBUILDER_IMPORTS 経由で MirBuilderBox / BuildBox などの static box 名をインポートし、Const(String(alias)) として扱う。
  • JSON v0 ブリッジ:
    • args を暗黙パラメータとして扱う修正済み(undefined variable: args は解消)。
    • hostbridge は wellknown 変数として 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 に解決)
    • 複数ステートメント、IfLoop、メソッドチェインなど 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 delegateenv.mirbuilder.emit)へフォールバックする。
  • 現時点での不足点要約・2025-11-16:
    • FuncBodyBasicLowerBox が本番でカバーできているのは、Local/If/Return の基本形LoopForm 正規化済み Loopごく一部の MethodCallargs.size/getString.length の Return 形に限られる。Stage1 CLI のような複雑な関数本体(複数 LocalIf ネストLoopMethod/Extern 混在)は、ほぼすべて Rust provider 経路にフォールバックしている。
    • ExternCall は hostbridge.extern_invoke("env.codegen","emit_object"|"link_object", arg)ExternCallLowerBox で最小サポートしているだけで、それ以外の externenv.mirbuilder.emit や console 系など)は現状 selfhost builder の対象外。
    • HakoCli.run 専用 lowerCliRunLowerBox)は MVP 用のシンプルな run パターンのみを想定しており、実際の launcher.hako の runusage/unknown ログ付き)は shape mismatch で selfhost 降ろしの対象外。ここを Stage1 実形に合わせて広げることが Phase 25.1b の中心タスクになっている。

Stage1 EXE / build_stage1.sh の現状

  • tools/selfhost/build_stage1.sh:
    • 既定値 HAKO_SELFHOST_BUILDER_FIRST=0provider-firstでは、Stage1 EXE ビルドは成功し、emit program-json / emit mir-json / build exe の I/O も Rust/llvmlite と同等の JSON+exit code 契約を満たす。
    • HAKO_SELFHOST_BUILDER_FIRST=1selfhost-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) に閉じる。
  • FailFast:
    • 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 に落とす」ような adhoc 実装は行わない:
    • PHI ノードの生成・配置は既存の LoopForm/LowerLoop 系 helperloop_scan_box.hakolower_loop_*_box.hako などに一元化し、builder 本体はそれを利用する立場にとどめる。
    • LLVM harness 側の PHI 不変条件ブロック先頭グルーピングwelltyped incomingを崩さない。
  • Phase 25.1b では:
    • まず LoopForm 前提で安全に扱える最小のループ形(既存 selfhost テストでカバー済みの while/forから対応し、
    • LoopForm 未適用の複雑なループ(例: キャリア3変数以上・ネストが深いもの[builder/selfhost-first:unsupported:loopform] タグなどで FailFast する。

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-firstHAKO_SELFHOST_BUILDER_FIRST=0)で MIR(JSON) を採取する。
    2. 同じ .hako を selfhost-firstHAKO_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 canaryrc チェック)で確認する。
  • ガード:
    • 新しい 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 < NN については、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
        • StageB で 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_carrierlimit_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をスモークで検証。

設計 TODOFuncLoweringBox / MirBuilderBox 拡張の方向性)

※ ここから先は「具体的にどこをどう広げるか」の設計メモ。実装はまだ行わない。

  1. FuncLowering の対応範囲拡張

    • _lower_func_body を Stage1 CLI で実際に使われているパターンまで広げる:
      • 単一 Return だけでなく、ローカル変数定義if 分岐loop を含む「典型的な CLI ハンドラ」の形をサポート。
      • MethodCallargs.size() / args.get(i) / FileBox.open/read/write / ArrayBox.push など)を MIR mir_callcall に落とす処理を追加。
    • func_map / resolve_call_target を用いて、HakoCli.cmd_emit_* / cmd_build_exe などの内部 Call を Global("HakoCli.cmd_emit_*") 系に正規化。
  2. MirBuilder 本体の出力構造

    • これまでの「Program 全体を 1 関数 main に落とす」前提から、「Program.body + defs を multifunction MIR モジュールに落とす」前提へシフト:
      • Program.body からはエントリ Main.main/1 相当の MIR 関数を生成。
      • Program.defs からは HakoCli.* などの補助関数を生成。
    • _norm_if_apply / inject_funcs の役割を整理し、「main 関数を含まない defsonly モジュール」を返さないように FailFast する。
  3. FailFast とデバッグ

    • 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. 検証計画

    • selfhostfirst canary:
      • stage1_launcher_program_to_mir_canary_vm.sh の selfhostfirst 版を追加し、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 — FailFast・観測を揃える

  • Status: implemented (2025-11-15). MirBuilderBox now tags defs_only / no_match failures and aborts, and FuncLoweringBox logs unsupported defs when HAKO_SELFHOST_TRACE=1.
  • 目的: 既存の selfhost builder がどこで諦めているかを正確に観測し、stub MIR を返さずに Fail させる導線を整える。
  • 作業:
    • MirBuilderBox.emit_from_program_json_v0
      • func_defs_mir だけが非空だった場合でも黙って { "functions": [defs] } を返さず、[builder/selfhost-first:unsupported:defs_only] を出す。
      • internal lowers がすべて null の場合、[builder/selfhost-first:unsupported:<reason>] のタグを付与。
    • FuncLoweringBox.lower_func_defs
      • どの関数名で _lower_func_bodynull を返したかを HAKO_SELFHOST_TRACE=1 でログ出力。
    • tools/hakorune_emit_mir.sh
      • 既存の head/tail ログ出力で [builder/selfhost-first:*] タグがそのまま表示されることを確認済み(追加改修なし)。
      • Phase 25.1b 以降、HAKO_SELFHOST_BUILDER_FIRST=1 で呼び出された場合は「StageB → selfhost builderStage1 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) のメイン経路は、StageBcompiler_stageb.hako)と MirBuilder/selfhost builder で完結させる。Ny selfhostsrc/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 経由の StageB/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 compilerinline_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_program2EmitterBox.emit_program で JSON v0 を得る経路が残っている。
    • 現状、この inline 経路は .ny の大きなソースに対して 60s タイムアウト+ Undefined variable: local を伴うことがあり、ParserBox/Stage3/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-implementedmain 必須チェックはトグル付き; 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段構え」でマージする APImain 名の受け渡しなど)を追加する。
  • 成果物(現段階):
    • 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-FastStep 3 で LoopForm 対応を行う)。
  • 成果物:
    • Loop を含まない defs は selfhost builder で MIR 関数にできるようになり、Stage1 CLI の emit/build ハンドラの半分程度を selfhost パスで賄える。

Step 3 — LoopLoopFormの受理

  • 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_lowerLowerLoopSimpleBox.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-FastRust providerに退避可能
  • 成果物:
    • cmd_build_exeloop(i < argc)等、Stage1 CLIの代表的なwhile/forパターンをselfhost builderで通せる基礎が整った。
    • 追加アップデート2025-11-16: multi-carrier ループ(TestBox.fib(n) など)も LowerLoopMultiCarrierBoxLoopFormBox.build_loop_multi_carrier 経由で selfhost lowering できるようになり、limit が Int でなく Var(n) でも [mirbuilder/internal/loop:multi_carrier:detected:limit_kind=param,...] を出して処理できる。
    • 次のステップ: LoopForm対応の動作確認スモークテスト追加、Step4MethodCall/ExternCallへ進む。

Step 3.1 — Box 型情報 APIRust Parity★New

  • 背景:
    • Stage3 VM では "" + MapBox のような「Box を文字列に暗黙変換する演算」が禁止されており、既存の JsonEmitBox / BoxHelpersLoopOptsBox.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/StringMapBox/ArrayBox/HostHandleBoxStage1 で使用する型)をカバーし、将来的に 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 時点): Stage3 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 実装までが endtoend で動作することを確認済み。
    • fib multicarrier 経路と selfhost multicarrier smoke 用の canary ケース(tools/smokes/v2/profiles/quick/core/phase251/selfhost_mir_loopform_multi_carrier_vm.shは、20251116 時点で 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 経路完了 / multicarrier selfhost-first canary PASS」とみなす。

Step 4 — MethodCall / ExternCall パリティ設計メモ・Rust層読解込み

  • Status: design-onlyRust 層の挙動を踏まえた設計まで)

  • 目的: 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 メソッド呼び出し)

      • StageB 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 のみ(当面は ArrayBoxString 程度)をハードコードで対応。
        • receiver は receiver フィールド(または args の先頭でレジスタ番号を渡し、Rust 側の mir_call 仕様と揃える。
    2. self-callme.cmd_*)の解決

      • Stage1 CLI の me.cmd_* は、StageB の FuncScanner から Program.defsbox:"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. ExternCallhostbridge.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 に落とす:
          {"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 を emitexecute_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) のパスを返す。
  • BridgeJSON 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 の特別ケース:
          • ConsoleBoxprint/println/logExternCall 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_mapmefunc.params[0] を、残りのパラメータ名を params[1..] にバインドする。
        • これにより StageB / Stage1 側で _build_module_map() のような「params: [] だが me を使う」メソッドでも、 Rust VM 実行時に me 未定義にならず、BoxCall が正しく解決されるようになった。
  • Selfhost への移植指針Rust SSOT に沿った箱設計):

    • MethodCall:
      • Hako 側では「どの Box のどのメソッドを MIR の mir_call(Method) に落とすか」を Box 単位の helper で管理する(LoopOptsBoxCli*Box と同様に)。
      • Rust 側の CallTarget::MethodCallee::Method の変換ルールreceiver レジスタの扱い、box_name/method 名)を Step 4 の設計メモと揃える。
    • ExternCall:
      • hostbridge.extern_invoke("env.codegen","emit_object"/"link_object", args)env.mirbuilder.emit などは、
        • Rust では最終的に ExternCallextern_provider_dispatch("env.*", args)plugin_loader_v2::extern_call("env.*", method, args) / host_providers::* という構造になっている。
      • Hako 側では「env 名+メソッド名の組(= key」を列挙した薄い *BridgeBox でラップし、そのうえで ExternCallLowerBoxexterncall 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/phase251selfhost_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.mainHakoCli.run)を Program(JSON v0) 上で検出し、selfhost builder が「どの Program が CLI エントリを含むか」を把握できるようにする(観測専用)。
  • 作業:
    • lang/src/mir/builder/func_body/cli_entry_box.hakoCliEntryLowerBox を追加。
      • 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
        を出力し、戻り値は常に nullMIR は生成しない)。
    • FuncLoweringBox.lower_func_defs の先頭で CliEntryLowerBox.scan(program_json) を呼び出し、defs 降ろし処理とは独立に「入口構造だけを観測」できるようにする。
  • レイヤリング:
    • この Step1 はあくまで ring1Stage1 CLI ソース)の Program(JSON v0) を観測するだけで、ring0Rust 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) から把握し、将来の専用 lowerCliRunLowerBoxが安全に使えるかを事前に観測する。
  • 作業:
    • lang/src/mir/builder/func_body/cli_run_shape_box.hako を追加CliRunShapeScannerBox
      • scan(program_json) で:
        • {"box":"HakoCli","name":"run", ...} を defs 内から検出し、
        • Program 全体の文字列から "run", "build", "emit", "check" などのリテラルを簡易に拾って、branches 配列として記録。
      • HAKO_SELFHOST_TRACE=1 のときに
        [builder/cli:run_shape] has_run=1 branches=<count>
        を出力し、戻り値として {"has_run":1,"branches":[...]} (MapBox) を返す(現状はタグ主体で利用)。
    • lang/src/mir/builder/func_body/cli_run_lower_box.hako を追加CliRunLowerBox
      • 現段階では stub 実装:
        • lower_run(func_name, box_name, params_arr, body_json, func_map)HakoCli.run だけをターゲットにし、HAKO_SELFHOST_TRACE=1 時に
          [builder/cli:run_lower:stub] box=HakoCli func=run
          を出して常に null を返す。
        • 実際の MIR 降ろしrun/build/emit/check 分岐を持つ run 本体の loweringは、Step2 後半〜Step3 以降で実装する前提。
    • FuncLoweringBox への統合:
      • lower_func_defs の先頭で CliRunShapeScannerBox.scan(program_json) を呼び、Stage1 CLI run の形状をタグ付きで観測。
      • _lower_func_body の冒頭で box_name=="HakoCli" && func_name=="run" のときだけ CliRunLowerBox.lower_run(...) を呼び出す。現状は常に null なので、従来の BasicLowerBox / provider 経路と挙動は変わらない。
  • レイヤリング:
    • Step2 も Step1 と同様、ring1Stage1 Hako CLIの構造を観測する箱のみを追加する。
    • MIR 生成はまだ Rust provider /既存 lower に任せており、ring0 の責務env.* extern や実行エンジン)にも影響を与えない。
    • 専用 lowerCliRunLowerBox が実際に MIR を返す形は、StageB 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 にフォールバックするFailFast
  • 対象とする 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. 連続する Ifcmd == "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_checkboxcall で呼び出してそのまま ret。
        • どれにもマッチしない場合は ret 2unknown command
      • 環境変数 HAKO_MIR_BUILDER_CLI_RUN=1 のときにだけ _emit_mir を呼び、それ以外は shape OK でも null を返して provider/既存 lower にフォールバックする(既定挙動は不変)。
    4. タグと FailFast実装済み
      • 形が完全に一致し、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 本体には適用していない(今後 StageB Program(JSON v0) を詳細に比較しながら対応範囲を広げる)。

Stage1 HakoCli.run本番とのギャップ整理provider MIR ベース)

現状、このホスト環境では StageB 実行中に Undefined variable: localStage3 キーワード)で Program(JSON v0) 抽出が失敗しているため、実 launcher.hako の HakoCli.run 形状は Rust provider の MIR(JSON) から推定している(/tmp/launcher_provider_mir.json で確認)。

MVP run パターンと Stage1 実装の主な差分:

  • argc==0 の場合:
    • MVP: if argc == 0 { return 1 }(副作用なし)。
    • 実装: usage メッセージ [hakorune] usage: hakorune <command> [options]print してから ret 1
  • サブコマンド unknown の場合:
    • MVP: 単純に return 2
    • 実装: [hakorune] unknown command: <cmd>print してから ret 2
  • 比較まわり:
    • 両者とも "run"|"build"|"emit"|"check" 文字列と一致判定しているが、実装側は usage/unknown メッセージ用の追加 binop/call を複数ブロックに分割しているMIR 上でブロック数が多い)。
  • 呼び出しターゲット:
    • 両者とも cmd_run/cmd_build/cmd_emit/cmd_check を呼び出す点は一致MIR 上では boxcall、将来 FuncLowering の call_resolve で Global/Method に寄せる予定)。

今後 StageB Program(JSON v0) が安定して取れるようになったら、上記の差分を JSON レベルで再確認し、

  • usage/unknown の print ブロックを「前置/後置のサイドエフェクト」として _check_shape の許容パターンに追加するか、
  • あるいは run 本体を「MVP サブセット(引数分岐)+印字専用ブロック」に分けて扱うか、
    を決める予定。

Stage1 用 run パターン拡張方針(設計)

Stage1 launcher.hako の本番 HakoCli.run を selfhost lowering の対象に含めるための方針は次のとおり:

  • 目的:

    • Rust CLIStage0と同じ意味論exit code とメッセージを維持したまま、Stage1 側の run を selfhost builder からも扱えるようにする。
    • logging/usage 部分printは「サイドエフェクトのある前置/後置ブロック」として明示的に扱い、分岐ロジック本体とは分離して考える。
  • 拡張の方向性(案):

    1. 前置 usage ブロックの許容
      • _check_shape で「argc==0 → usage print → ret 1」という形を、
        argc == 0 の then 側に StringBox const / binop / call(print) が含まれてもよい」
        というルールに緩和する。
      • lowering 時には:
        • まず usage 用の文字列構築+print をそのまま MIR に反映(boxcall / externcall env.console.log など)。
        • そのあとで ret 1 を emit するMVP では usage 文言は provider MIR に揃える)。
    2. unknown ブロックの後置許容
      • MVP では「unknown なら ret 2」のみを扱っているが、本番では
        [hakorune] unknown command: <cmd> を出力してから ret 2 している。
      • _check_shape を「末尾の Return Int(2) の前に StringBox/binop/print パターンが挟まっていてもよい」と解釈できるようにし、
        lowering 側でもそれをそのまま MIR に降ろすprint ブロック + ret 2)。
    3. run 本体(分岐ロジック)との分離
      • _check_shape を二層に分ける:
        • check_core_shape … argc/args/cmd/サブコマンド分岐の「副作用なし」部分の形状チェック。
        • check_logging_shape … usage/unknown の印字パターンのみを許容する緩いチェック。
      • CliRunLowerBox はまず core を _emit_mir_core で生成し、logging 部分は必要に応じて前後にブロックを足す形で統合する。
  • トグルと適用範囲:

    • Stage1 本番への適用は常に HAKO_MIR_BUILDER_CLI_RUN=1(既定 OFFでガードし、
      かつ StageB 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_methodReturn(Method recv.method(args)) 形のうち、ArrayBox.size/getparams ベース 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.hakoExternCallLowerBox.lower_hostbridge を追加し、Return(hostbridge.extern_invoke("env.codegen","emit_object"|"link_object", Var(arg)))externcall env.codegen.emit_object|link_objectret に 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-firstHAKO_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 は StageB の Program(JSON v0) 上で Method ノードを見て lowering する設計であるため、下表の「Method 名」は Program 側のメソッドセットを推定するための参考情報として扱う。
MIR function Method パターンprovider MIR 上の boxcall/mir_call Extern 風パターン selfhost builder 側の現状
HakoCli._read_file/3 open, read, closeFileBox 由来と推定) なし FileBox 系メソッドは未対応
HakoCli._write_file/5 open, write, closeFileBox なし 同上(未対応)
HakoCli.cmd_build/2 cmd_build_exe, get, sizeargs.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 系/indexOfset は未対応
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 runMain.main → HakoCli.run 呼び出し) なし main 相当は provider/main 降ろしに依存

このスナップショットから分かる不足点2025-11-16 現在):

  • MethodCall 側:
    • Stage1 CLI で実際に使われているメソッドは、FileBoxopen/read/write/close、Array/Map 系size/get/set/push、String 系indexOfなど多岐にわたるが、selfhost builder が defs 側で専用に扱えているのは args.size/getString.length の単純な Return 形のみ
    • me.cmd_* 系 self-callcmd_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) 側のパターンを精査する必要がある。
    • それ以外の Externenv.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 builderprovider-first経路を確認するスモーク。
      • *_selfhost_vm.sh … 同じ .hako / 期待 rc を selfhost-firstHAKO_SELFHOST_BUILDER_FIRST=1)で確認するスモーク。
    • ループ系:
      • 例として phase2100 の LoopForm/PHI canaryRust ベース)に対応して、
        • 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.shprovider-firstに対して、
        • stage1_launcher_program_to_mir_selfhost_vm.shselfhost-first; builder MIR で 60KB 級出力を期待) を phase251 側に追加する。
  • 運用:
    • quick プロファイルでは provider-first スモークを既定 ON とし、selfhost-first スモークは Phase 25.1b 中は任意(開発用)とする。
    • selfhost-first スモークが十分に安定し、Stage1 build も selfhost-first で通るようになった時点で、必要に応じて CI quick プロファイルへの昇格を検討する。