Files
hakorune/docs/development/architecture/join-ir.md
nyash-codex 8750186e55 chore: Phase 26-H セッション完了 - 全ドキュメント更新
Phase 26-H 完了内容:
 JoinIR 型定義実装(src/mir/join_ir.rs)
 MIR → JoinIR 自動変換実装(lower_min_loop_to_joinir)
 自動変換テスト実装(mir_joinir_min_auto_lowering)
 PHI/Loop箱 → JoinIR 移行対応表追加(loopform_ssot.md)

ドキュメント更新:
- Phase 27 JoinIR タスク計画追加
- Phase 26-H タスク完了記録
- 各種 README 更新(進捗反映)
- CURRENT_TASK.md 更新

コミット統計: $(git status --short | wc -l) files changed

次のステップ: Phase 27 一般化 MIR → JoinIR 変換
2025-11-23 05:53:27 +09:00

9.5 KiB
Raw Blame History

JoinIR — 関数正規化 IR関数と継続だけで制御を表現する層

目的

  • Hakorune の制御構造(if / loop / break / continue / return)を、関数呼び出し継続continuationだけに正規化する IR 層として設計する。
  • これにより:
    • φ ノード = 関数の引数
    • merge ブロック = join 関数
    • ループ = 再帰関数loop_step exit 継続k_exit
    • break / continue = 適切な関数呼び出し という形に落とし込み、LoopForm v2 / Exit PHI / BodyLocal / ExitLiveness の負担を構造的に軽くする

前提・方針

  • LoopForm / ControlForm は「構造の前段」として残す(削除しない)。
  • JoinIR は「PHI/SSA の実装負担を肩代わりする層」として導入し、ヘッダ/exit φ や BodyLocal の扱いを 関数の引数と継続 に吸収していく。
  • PHI 専用の箱HeaderPhiBuilder / ExitPhiBuilder / BodyLocalPhiBuilder など)は、最終的には JoinIR 降ろしの補助に縮退させることを目標にする。

位置づけ

  • 変換パイプラインにおける位置:
AST  →  MIR+LoopForm v2  →  JoinIR  →  VM / LLVM
  • AST / MIR では従来どおり Nyash 構文if / loop 等)を扱い、 JoinIR 以降では 関数と値BoxPrimitiveだけを見る。

1. JoinIR のコアアイデア

1-1. 「ループ = 関数を何回も呼ぶこと」

通常の while/loop は:

loop(i < n) {
  if i >= n { break }
  i = i + 1
}
return i

JoinIR 的に見ると:

fn main(k_exit) {
    loop_step(0, k_exit)
}

fn loop_step(i, k_exit) {
    if i >= n {
        // break
        k_exit(i)
    } else {
        // continue
        loop_step(i + 1, k_exit)
    }
}
  • ループ本体 = loop_step(i, k_exit) という関数。
  • i は LoopCarriedcarrier変数 → 関数の引数で表現。
  • break = exit 継続 k_exit の呼び出し。
  • continue = loop_step をもう一度呼ぶこと。

1-2. 「if の merge = join 関数」

ソース:

if cond {
  x = 1
} else {
  x = 2
}
print(x)

JoinIR 的には:

fn main(k_exit) {
    if cond {
        then_branch(k_exit)
    } else {
        else_branch(k_exit)
    }
}

fn then_branch(k_exit) {
    x = 1
    join_after_if(x, k_exit)
}

fn else_branch(k_exit) {
    x = 2
    join_after_if(x, k_exit)
}

fn join_after_if(x, k_exit) {
    print(x)
    k_exit(0)
}
  • φ ノード = join_after_if の引数 x
  • merge ブロック = join_after_if 関数。
  • 「どのブランチから来たか」の情報は、関数呼び出しに吸収される。

2. JoinIR の型イメージ

※ Phase 26-H 時点では「設計のみ」を置いておき、実装は最小ケースから。

2-1. 関数と継続

/// JoinIR 関数IDMIR 関数とは別 ID でもよい)
struct JoinFuncId(u32);

/// 継続join / ループ step / exit continuationを識別するID
struct JoinContId(u32);

/// JoinIR 関数
struct JoinFunction {
    id: JoinFuncId,
    params: Vec<VarId>,        // 引数(φ に相当)
    body: Vec<JoinInst>,       // 命令列
    exit_cont: Option<JoinContId>, // 呼び出し元に返す継続(ルートは None
}

2-2. 命令セット(最小)

enum JoinInst {
    /// 通常の関数呼び出し: f(args..., k_next)
    Call {
        func: JoinFuncId,
        args: Vec<VarId>,
        k_next: JoinContId,
    },

    /// 継続呼び出しjoin / exit 継続など)
    Jump {
        cont: JoinContId,
        args: Vec<VarId>,
    },

    /// ルート関数 or 上位への戻り
    Ret {
        value: Option<VarId>,
    },

    /// それ以外の演算は、現行 MIR の算術/比較/boxcall を再利用
    Compute(MirLikeInst),
}

2-3. 継続の分類

  • join 継続:
    • if の merge や、ループ body 終了後の「次の場所」を表す。
  • loop_step:
    • ループ 1ステップ分の処理。
  • exit 継続:
    • ループを抜けた後の処理(k_exit)。
  • これらはすべて「関数 or 継続 ID」と「引数」で表現される。

3. MIR/LoopForm から JoinIR への変換ルールv0 草案)

ここでは Phase 26-H で扱う最小のルールだけを書く。

3-1. ループLoopForm v2→ loop_step + k_exit

前提: LoopForm v2 が提供する情報

  • preheader / header / body / latch / exit / continue_merge / break ブロック集合。
  • LoopVarClassBox による分類Pinned / Carrier / BodyLocalExit / BodyLocalInternal

変換方針(概略):

  1. LoopCarried 変数の集合 C = {v1, v2, ...} を求める。
  2. 「ループを抜けた後で使う変数」の集合 E を ExitLiveness から得る(長期的には MirScanExitLiveness
  3. JoinIR 上で:
    • fn loop_step(C, k_exit) を新設。
    • ループ前の値から loop_step(C0, k_exit0) を呼び出す。
    • body 内の breakk_exit(E) に変換。
    • body 内の continueloop_step(C_next, k_exit) に変換。

今の LoopForm / PhiBuilderBox がやっている仕事header φ / exit φ 生成)は、 最終的には「CE を決める補助」に寄せていくイメージになる。

3-2. if → join_after_if

前提:

  • control_flow / if_form.rs で IfFormcond / then / else / mergeが取れる。

変換方針:

  1. merge ブロックで φ が必要な変数集合 M = {x1, x2, ...} を求める。
  2. JoinIR 上で:
    • fn join_after_if(M, k_exit) を新設。
    • then ブランチの末尾に join_after_if(x1_then, x2_then, ..., k_exit) を挿入。
    • else ブランチの末尾に join_after_if(x1_else, x2_else, ..., k_exit) を挿入。
  3. merge ブロックの本体は join_after_if に移す。

これにより、φ ノードは join_after_if の引数に吸収される。


4. フェーズ計画Phase 26-H 〜 27.x の流れ)

Phase 26-H現在フェーズ— 設計+ミニ実験

  • スコープ:
    • JoinIR の設計ドキュメント(このファイル+ phase-26-H/README
    • src/mir/join_ir.rs に型だけ入れる(変換ロジックは最小)。
    • 1 ケース限定の変換実験:
      • 例: apps/tests/joinir_min_loop.hako → MIR → JoinIR → JoinIR のダンプを Rust テストで確認。
  • ゴール:
    • 「関数正規化 IRJoinIRが箱理論LoopForm v2 と矛盾しない」ことを確認する。
    • 大規模リプレースに進む価値があるか、感触を掴む。

Phase 27.x— 段階的な採用案(まだ構想段階)

この段階はまだ「構想メモ」として置いておく。

  • 27.1: LoopForm v2 → JoinIR の変換を部分的に実装
    • まずは「break なしの loop + if」だけを対象にする。
    • Exit φ / header φ の一部を JoinIR の引数に置き換える。
  • 27.2: break / continue / BodyLocalExit を JoinIR で扱う
    • BodyLocalPhiBuilder / LoopExitLiveness の「決定」を JoinIR 引数設計に寄せる。
    • ExitLiveness は JoinIR 関数の use/def から算出可能かを検証する。
  • 27.3: SSA/PHI との役割調整
    • LoopForm v2 / PhiBuilderBox は「JoinIR 生成補助」の位置付けに縮退。
    • MirVerifier は JoinIR ベースのチェック(又は MIR/JoinIR 並列)に切り替え検討。

※これら 27.x フェーズは、26-F / 26-G で現行ラインの根治が一段落してから進める前提。


5. 他ドキュメントとの関係

  • docs/private/roadmap2/phases/phase-26-H/README.md
    • Phase 26-H のスコープ設計ミニ実験、他フェーズ25.1 / 26-F / 26-Gとの関係を定義。
  • docs/development/architecture/loops/loopform_ssot.md
    • LoopForm v2 / Exit PHI / 4箱構成LoopVarClassBox / LoopExitLivenessBox / BodyLocalPhiBuilder / PhiInvariantsBoxの設計ート。
    • 将来、JoinIR を導入するときは「LoopForm v2 → JoinIR 変換の前段」としてこの SSOT を活かす。

このファイルは、JoinIR を「全部関数な言語」のコアとして扱うための設計メモだよ。
実際の導入は小さな実験Phase 26-Hから始めて、問題なければ 27.x 以降で段階的に進める想定にしておく。

6. φ と merge の対応表(抜粋)

構文/概念 JoinIR での表現 φ/合流の扱い
if/merge join 関数呼び出し join 関数の引数が φ 相当
loop step 再帰 + k_exit 継続 LoopCarried/Exit の値を引数で渡す
break k_exit 呼び出し φ 不要(引数で値を渡す)
continue step 呼び出し φ 不要(引数で値を渡す)
return 継続または ret そのまま値を返す

7. 26-H で増やす実装箱 / 概念ラベル

  • 実装として増やす(このフェーズで手を動かす)

    • join_ir.rs: JoinFunction/JoinBlock/JoinInst の最小定義とダンプ
    • LoopForm→JoinIR のミニ変換関数1 ケース限定で OK
    • 実験トグル(例: NYASH_JOINIR_EXPERIMENT=1)で JoinIR をダンプするフック
  • 概念ラベルのみ27.x 以降で拡張を検討)

    • MirQueryBox のような MIR ビュー層reads/writes/succs を trait 化)
    • LoopFnLoweringBox / JoinIRBox の分割(必要になったら分ける)
    • JoinIR 上での最適化や VM/LLVM 統合

このフェーズでは「箱を増やしすぎない」ことを意識し、型定義+最小変換+ダンプ確認だけに留める。