Files
hakorune/docs/development/architecture/join-ir.md
nyash-codex 2692eafbbf feat(mir): Phase 26-H JoinIR型定義実装完了 - ChatGPT設計
## 実装内容(Step 1-3 完全達成)

### Step 1: src/mir/join_ir.rs 型定義追加
- **JoinFuncId / JoinContId**: 関数・継続ID型
- **JoinFunction**: 関数(引数 = φノード)
- **JoinInst**: Call/Jump/Ret/Compute 最小命令セット
- **MirLikeInst**: 算術・比較命令ラッパー
- **JoinModule**: 複数関数保持コンテナ
- **単体テスト**: 型サニティチェック追加

### Step 2: テストケース追加
- **apps/tests/joinir_min_loop.hako**: 最小ループ+breakカナリア
- **src/tests/mir_joinir_min.rs**: 手書きJoinIR構築テスト
  - MIR → JoinIR手動構築で型妥当性確認
  - #[ignore] で手動実行専用化
  - NYASH_JOINIR_EXPERIMENT=1 トグル制御

### Step 3: 環境変数トグル実装
- **NYASH_JOINIR_EXPERIMENT=1**: 実験モード有効化
- **デフォルト挙動**: 既存MIR/LoopForm経路のみ(破壊的変更なし)
- **トグルON時**: JoinIR手書き構築テスト実行

## Phase 26-H スコープ遵守
 型定義のみ(変換ロジックは未実装)
 最小限の命令セット
 Debug 出力で妥当性確認
 既存パイプライン無影響

## テスト結果
```
$ NYASH_JOINIR_EXPERIMENT=1 cargo test --release mir_joinir_min_manual_construction -- --ignored --nocapture
[joinir/min] MIR module compiled, 3 functions
[joinir/min] JoinIR module constructed:
[joinir/min]  JoinIR型定義は妥当(Phase 26-H)
test result: ok. 1 passed; 0 failed
```

## JoinIR理論の実証
- **φノード = 関数引数**: `fn loop_step(i, k_exit)`
- **merge = join関数**: 分岐後の合流点
- **ループ = 再帰関数**: `loop_step` 自己呼び出し
- **break = 継続呼び出し**: `k_exit(i)`

## 次フェーズ (Phase 27.x)
- LoopForm v2 → JoinIR 自動変換実装
- break/continue ハンドリング
- Exit PHI の JoinIR 引数化

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

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: ChatGPT <noreply@openai.com>
2025-11-23 04:10:12 +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/development/roadmap/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 統合

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