Files
hakorune/docs/development/current/main/phase195-pattern3-extension-design.md
nyash-codex dc4eda9cdc docs: Phase 195 implementation status + Phase 196 (Select bug) planning
Phase 195 implementation complete (Lowerer side):
- loop_with_if_phi_minimal.rs: multi-carrier PHI generation (sum + count)
- pattern3_with_if_phi.rs: dynamic single/multi-carrier handling
- ExitLine/CarrierVar.join_id: NO CHANGES NEEDED (existing infra worked!)
- YAGNI principle validated - PhiGroupBox was not necessary

Blocker discovered: Nested Select→Branch+Phi conversion bug
- JoinIR correctly generates: ValueId(20) = phi [(bb3, 14), (bb4, 18)]
- MIR incorrectly produces: %27 = phi [%28, bb8], [%32, bb9] (undefined)
- Root cause: joinir_block.rs Select expansion (bridge layer)
- NOT a Phase 195 issue - pre-existing bug in Select conversion

Phase 196 planned (NEW):
- 196-1: Document select_expansion / instruction_rewriter responsibilities
- 196-2: Formalize "1 Select = 3 blocks + 1 PHI" interface
- 196-3: Debug block reuse / block ID mapping
- 196-4: Fix and E2E test (phase195_sum_count.hako → 72)

Documentation updates:
- phase195-pattern3-extension-design.md: Implementation Status section
- CURRENT_TASK.md: Phase 195 complete + Phase 196 TODO
- joinir-architecture-overview.md: P3 Lowerer complete, bridge blocker noted

Key insight: "Pattern side is done, problem is in bridge side"

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-09 13:58:31 +09:00

21 KiB
Raw Blame History

Phase 195: Pattern 3 拡張if-in-loop + マルチキャリア)

Status: Design Phase Date: 2025-12-09 Prerequisite: Phase 194 complete (JsonParser deployment & validation)


目的

Pattern 3If-Else PHIの対応範囲を拡張し、複数キャリアの条件付き更新を JoinIR で扱えるようにする。

スコープ:

  • if 内で完結する複数キャリア更新2-3個程度
  • ConditionEnv 拡張は行わない外部ローカル・digits 系は Phase 200+ 保留)
  • LoopBodyLocal + MethodCall 混在は Phase 195+ に延期

Task 195-1: 対象ループの絞り込みdoc-only

目標

JsonParser / selfhost から「P3 で攻めたいループ」を 1-2 本だけ選定し、AST 構造を詳細に分析する。

候補ループ

候補 1: JsonParser _parse_string の簡易版(優先度: 高)

ループ構造:

// _parse_string (escape 処理の簡略版)
static box JsonParser {
    _parse_string(s, start) {
        local buffer = ""
        local escaped = false
        local i = start

        loop(i < s.length() and s[i] != '"') {
            local ch = s[i]

            if(ch == '\\') {
                escaped = true           // ← Carrier 1 update
            } else {
                buffer = buffer + ch     // ← Carrier 2 update
                escaped = false          // ← Carrier 1 update
            }

            i = i + 1
        }

        return buffer
    }
}

AST 構造:

Loop {
    condition: BinOp(i < len AND s[i] != '"')
    body: [
        LocalVar { name: "ch", init: ArrayAccess(s, i) },  // body-local
        If {
            condition: Compare(ch == '\\'),
            then: [
                Assign { lhs: "escaped", rhs: BoolLiteral(true) }
            ],
            else: [
                Assign { lhs: "buffer", rhs: BinOp(buffer + ch) },
                Assign { lhs: "escaped", rhs: BoolLiteral(false) }
            ]
        },
        Assign { lhs: "i", rhs: BinOp(i + 1) }
    ]
}

キャリア分析:

Carrier Type Update Pattern Then Branch Else Branch
escaped BoolBox Conditional flag true false
buffer StringBox StringAppend (unchanged) buffer + ch
i IntegerBox CounterLike i + 1 i + 1

P3 で扱う範囲:

  • escaped: 条件フラグ(両分岐で定義)
  • buffer: StringAppendelse のみ更新、then は不変)
  • i: ループ外で統一更新P3 の外で処理)

制約:

  • ch は body-local 変数Phase 191 で対応済み)
  • s[i] の配列アクセスは ConditionEnv で解決(s は outer local → Phase 200+
  • この Phase では ch = "x" のような定数で代替してテスト

候補 2: selfhost の if-sum パターン(優先度: 中)

ループ構造:

// selfhost: 条件付き集計
static box Aggregator {
    sum_positive(array) {
        local sum = 0
        local count = 0
        local i = 0

        loop(i < array.length()) {
            if(array[i] > 0) {
                sum = sum + array[i]      // ← Carrier 1 update
                count = count + 1         // ← Carrier 2 update
            }
            i = i + 1
        }

        return sum
    }
}

AST 構造:

Loop {
    condition: Compare(i < array.length())
    body: [
        If {
            condition: Compare(array[i] > 0),
            then: [
                Assign { lhs: "sum", rhs: BinOp(sum + array[i]) },
                Assign { lhs: "count", rhs: BinOp(count + 1) }
            ],
            else: []  // 空(更新なし)
        },
        Assign { lhs: "i", rhs: BinOp(i + 1) }
    ]
}

キャリア分析:

Carrier Type Update Pattern Then Branch Else Branch
sum IntegerBox NumberAccumulation sum + array[i] (unchanged)
count IntegerBox CounterLike count + 1 (unchanged)
i IntegerBox CounterLike i + 1 i + 1

P3 で扱う範囲:

  • sum: NumberAccumulationthen のみ更新)
  • count: CounterLikethen のみ更新)
  • i: ループ外で統一更新

制約:

  • array[i] の配列アクセスは ConditionEnv で解決(array は outer local → Phase 200+
  • この Phase では i のような既存パラメータで代替してテスト

Phase 195 での選定

優先順位 1: 候補 1_parse_string 簡易版)

  • 理由: JsonParser の実戦コード、flag + buffer の2キャリア
  • 簡略化: ch = "x" 定数で配列アクセス回避

優先順位 2: 候補 2if-sum

  • 理由: selfhost の典型パターン、sum + count の2キャリア
  • 簡略化: i のみで配列アクセス回避

成果物:

  • 選定したループの詳細 AST 構造記録(本セクション)
  • キャリア分析表UpdateKind 分類済み)

Task 195-2: LoopUpdateSummary / CarrierInfo の設計整理

現状の P3 サポートPhase 170-189

既存の P3 は単一キャリアのみ対応:

// 既存の P3 ケース
if(cond) {
    sum = sum + i  // ← 単一キャリア "sum"
} else {
    sum = sum - i
}

LoopUpdateSummary / CarrierInfo の構造:

pub struct CarrierInfo {
    pub carriers: Vec<String>,           // キャリア名リスト
    pub updates: HashMap<String, ASTNode>,  // name → update式
}

pub struct LoopUpdateSummary {
    pub kind: UpdateKind,  // CounterLike | NumberAccumulation | ...
    // ...
}

Phase 195 で扱う「複数キャリア + 条件付き更新」

拡張要件:

// Phase 195 の P3 ケース
if(cond) {
    escaped = true      // ← Carrier 1
    // buffer は更新なし(不変)
} else {
    buffer = buffer + ch  // ← Carrier 2
    escaped = false     // ← Carrier 1
}

設計原則:

  1. 両分岐で同じ Carrier が必ず定義されることPHI 生成の前提)

    • 片方の分岐で更新なし(不変)の場合、明示的に carrier = carrier を挿入
    • OR: PHI 生成時に「更新なし = 前の値を使う」として扱う
  2. 各 Carrier の update 式は既存 UpdateKind 範囲内:

    • CounterLike: count + 1, count - 1
    • NumberAccumulation: sum + i, sum * base + addend
    • StringAppend: buffer + ch
    • BoolFlag: true, false(新規 UpdateKind 候補)
  3. CarrierInfo は複数 Carrier を同時に保持:

    CarrierInfo {
        carriers: vec!["escaped", "buffer"],
        updates: {
            "escaped": ...,  // then/else で異なる式
            "buffer": ...,
        }
    }
    

制約の明確化

P3 で扱う Carrier の制約 (Phase 195):

  • if-else の両分岐で同じ Carrier が定義される(明示的 or 不変)
  • 各 update 式は既存 UpdateKind に対応する
  • MethodCall を含む update は Phase 193 の制約に従う(ループパラメータのみ)
  • 外部ローカル変数(digits 等)は Phase 200+ に保留

OK:

if(ch == '\\') {
    escaped = true           // ✅ BoolFlag
} else {
    buffer = buffer + ch     // ✅ StringAppend
    escaped = false          // ✅ BoolFlag
}

NG - Phase 195 範囲外):

if(cond) {
    digit = digits.indexOf(ch)  // ❌ 外部ローカル (Phase 200+)
    sum = sum + digit
}

成果物

  • 複数キャリア対応の設計原則(本セクション)
  • UpdateKind 拡張候補BoolFlagの検討
  • CarrierInfo 構造の拡張仕様(擬似コード)

Task 195-3: Pattern3 lowerer の設計更新

現状の P3 lowererPhase 170-189

単一キャリアの処理フロー:

1. If-Else AST を検出
2. Then/Else 各分岐で carrier 更新式を抽出
3. JoinIR で Then/Else ブロック生成
4. Merge 点で PHI 命令生成1つの carrier のみ)
5. ExitLine で PHI 結果を variable_map に接続

Phase 195 での拡張設計

1. 複数 Carrier の同時処理

設計案: 複数 PHI を同じ Merge 点で生成

// 擬似コード: Pattern3 lowerer 拡張

// Step 1: If-Else で更新される全 Carrier を収集
let carriers_in_then = extract_carriers(&if_node.then_branch);
let carriers_in_else = extract_carriers(&if_node.else_branch);
let all_carriers = carriers_in_then.union(&carriers_in_else);

// Step 2: 各 Carrier について Then/Else の update 式を取得
let mut carrier_updates = HashMap::new();
for carrier in all_carriers {
    let then_update = get_update_or_unchanged(&if_node.then_branch, carrier);
    let else_update = get_update_or_unchanged(&if_node.else_branch, carrier);
    carrier_updates.insert(carrier, (then_update, else_update));
}

// Step 3: JoinIR で Then/Else ブロック生成(複数 update を emit
let then_block = emit_then_branch(&carrier_updates);
let else_block = emit_else_branch(&carrier_updates);

// Step 4: Merge 点で複数 PHI 生成
let merge_block = create_merge_block();
for (carrier, (then_val, else_val)) in carrier_updates {
    let phi_result = emit_phi(merge_block, then_val, else_val);
    // ExitLine で variable_map に接続
    exit_line.connect(carrier, phi_result);
}

2. PhiGroupBox vs ExitLine/CarrierInfo 拡張

Option A: PhiGroupBox新規箱

pub struct PhiGroupBox {
    pub phis: Vec<PhiInfo>,  // 複数 PHI の束
}

pub struct PhiInfo {
    pub carrier_name: String,
    pub then_value: ValueId,
    pub else_value: ValueId,
    pub result: ValueId,
}

メリット:

  • 複数 PHI の関係性を明示的に管理
  • 単一責任の原則PHI グループ専用)

デメリット:

  • 新規箱の追加(複雑度増加)
  • 既存の ExitLine との統合が必要

Option B: ExitLine/CarrierInfo 拡張(既存箱再利用)

// 既存の ExitLine を拡張
pub struct ExitLine {
    pub phi_connections: HashMap<String, ValueId>,  // carrier → PHI result
}

// CarrierInfo は既に複数 Carrier 対応
pub struct CarrierInfo {
    pub carriers: Vec<String>,
    pub updates: HashMap<String, ASTNode>,
}

メリット:

  • 新規箱なし(既存インフラ再利用)
  • ExitLine が既に複数 Carrier 接続をサポート

デメリット:

  • ExitLine の責務が拡大

Phase 195 での判断: → Option B既存箱拡張を採用

理由:

  • ExitLine は既に「variable_map への接続」を担当
  • 複数 Carrier → 複数 PHI は自然な拡張
  • 新規箱を作る必要性が低いYAGNI 原則)

3. 更新なし(不変)の扱い

ケース: 片方の分岐で Carrier が更新されない

if(ch == '\\') {
    escaped = true
    // buffer は更新なし(不変)
} else {
    buffer = buffer + ch
    escaped = false
}

設計案: PHI 生成時に「前の値」を使用

// Then 分岐で buffer 更新なし
let then_buffer_value = previous_buffer_value;  // ← ループ header の PHI param

// Else 分岐で buffer 更新あり
let else_buffer_value = emit_string_append(...);

// Merge 点で PHI
let buffer_phi = emit_phi(merge, then_buffer_value, else_buffer_value);

実装詳細:

  • get_update_or_unchanged() 関数で検出
  • 更新なし → ValueId として「前の値」を返す
  • PHI 生成時に自動的に接続

成果物

  • Pattern3 lowerer の擬似コード(本セクション)
  • PhiGroupBox vs ExitLine 拡張の判断Option B 採用)
  • 更新なし(不変)の扱い方設計

Task 195-4: 実装スコープの決定(どこまでやるか)

Phase 195-impl の範囲

Phase 195-impl で実装する:

  1. 複数 Carrier の P3 処理2-3個程度

    • escaped + buffer のような flag + accumulation
    • sum + count のような accumulation + counter
  2. 既存 UpdateKind 範囲内の update:

    • CounterLike: count + 1
    • NumberAccumulation: sum + i
    • StringAppend: buffer + ch
    • BoolFlag: true, false(新規 UpdateKind 追加候補)
  3. 両分岐での定義確認:

    • Then/Else で同じ Carrier が定義されるケース
    • 更新なし(不変)の場合は自動的に「前の値」を使用
  4. E2E テスト:

    • 簡易版 _parse_stringch = "x" 定数版)
    • 簡易版 if-sum配列アクセスなし版

Phase 195-impl で実装しない:

  1. LoopBodyLocal + MethodCall 混在:

    • local ch = s[i]; if(...) { buf += ch }
    • → Phase 191body-local initと Phase 193MethodCallの組み合わせ
    • → Phase 195+ に延期(複雑度高い)
  2. 外部ローカル変数ConditionEnv 拡張):

    • local digits = "012..."; digit = digits.indexOf(ch)
    • → Phase 200+ に保留(設計判断済み)
  3. ネストした If:

    • if(...) { if(...) { ... } }
    • → Phase 196+ に延期P3 の P3

ゴールの明確化

Phase 195-impl のゴール:

「Named carrier が 2-3 個あっても P3 lowerer が壊れない」こと。

JsonParser/simple selfhost で「flag + count」くらいの例が通ること。

成功基準:

  • 簡易版 _parse_string が JoinIR で動作escaped + buffer
  • 簡易版 if-sum が JoinIR で動作sum + count
  • 既存テストphase190-194が退行しない
  • ドキュメント更新Implementation Status セクション追加)

テストケース設計

テスト 1: flag + buffer (BoolFlag + StringAppend)

ファイル: apps/tests/phase195_flag_buffer.hako

static box Main {
    main() {
        local buffer = ""
        local escaped = false
        local i = 0

        loop(i < 3) {
            local ch = "a"  // ← 定数(配列アクセス回避)

            if(i == 1) {
                escaped = true
            } else {
                buffer = buffer + ch
                escaped = false
            }

            i = i + 1
        }

        print(buffer)  // Expected: "aa" (i=0,2 で追加)
        return 0
    }
}

テスト 2: sum + count (NumberAccumulation + CounterLike)

ファイル: apps/tests/phase195_sum_count.hako

static box Main {
    main() {
        local sum = 0
        local count = 0
        local i = 0

        loop(i < 5) {
            if(i > 2) {
                sum = sum + i      // i=3,4 で加算
                count = count + 1
            }
            i = i + 1
        }

        print(sum)    // Expected: 7 (3+4)
        print(count)  // Expected: 2
        return 0
    }
}

Task 195-5: CURRENT_TASK / overview 更新

CURRENT_TASK.md 更新内容

## Phase 195: Pattern 3 拡張(設計フェーズ)(完了予定: 2025-12-XX)

**目的**: P3If-Else PHIを複数キャリア対応に拡張する設計

**タスク**:
- [ ] 195-1: 対象ループ絞り込み_parse_string 簡易版、if-sum
- [ ] 195-2: LoopUpdateSummary/CarrierInfo 設計整理(複数キャリア対応)
- [ ] 195-3: Pattern3 lowerer 設計更新PhiGroup vs ExitLine 拡張判断)
- [ ] 195-4: 実装スコープ決定2-3キャリア、既存 UpdateKind 範囲内)
- [ ] 195-5: ドキュメント更新(本項目 + overview 更新)

**設計判断**:
- ✅ ExitLine 拡張で対応PhiGroupBox は作らない)
- ✅ 両分岐での Carrier 定義確認(更新なし = 前の値使用)
- ❌ ConditionEnv 拡張なしPhase 200+ 保留)
- ❌ LoopBodyLocal + MethodCall 混在は Phase 195+ 延期

**期待成果**:
- phase195-pattern3-extension-design.md完全設計書
- Phase 195-impl の実装スコープ明確化
- JsonParser カバレッジ 40% → 60% への道筋

joinir-architecture-overview.md 更新内容

Section 7.2 "残タスク" に追記:

- [ ] **Phase 195**: Pattern 3 拡張(複数キャリア対応)
  - 設計フェーズ: P3 で 2-3 個の Carrier を同時処理
  - ExitLine 拡張で複数 PHI 生成PhiGroupBox は不要)
  - 対象: _parse_stringflag+buffer、if-sumsum+count
  - Phase 195-impl で実装予定

成功基準(設計フェーズ)

  • 対象ループ選定完了_parse_string 簡易版、if-sum
  • キャリア分析表作成UpdateKind 分類済み)
  • 複数キャリア対応の設計原則明確化
  • Pattern3 lowerer の擬似コード作成
  • PhiGroupBox vs ExitLine 拡張の判断Option B 採用)
  • 実装スコープ決定Phase 195-impl 範囲明確化)
  • テストケース設計2ケース
  • ドキュメント更新計画作成

関連ファイル

設計対象

  • src/mir/builder/control_flow/joinir/patterns/pattern3_with_if.rsPhase 195-impl で実装)
  • src/mir/join_ir/lowering/loop_update_summary.rsBoolFlag UpdateKind 追加候補)
  • src/mir/join_ir/lowering/carrier_info.rs(複数 Carrier 対応確認)

テストファイルPhase 195-impl で作成)

  • apps/tests/phase195_flag_buffer.hakoBoolFlag + StringAppend
  • apps/tests/phase195_sum_count.hakoNumberAccumulation + CounterLike

ドキュメント

  • docs/development/current/main/phase195-pattern3-extension-design.md(本ファイル)
  • docs/development/current/main/joinir-architecture-overview.md(更新予定)
  • CURRENT_TASK.mdPhase 195 設計タスク追加)

次のステップ

Phase 195-impl: Pattern3 拡張実装

設計書(本ファイル)に基づいて実装:

  1. Pattern3 lowerer に複数 Carrier 処理追加
  2. ExitLine で複数 PHI 接続
  3. BoolFlag UpdateKind 追加(必要に応じて)
  4. E2E テストphase195_flag_buffer.hako, phase195_sum_count.hako
  5. ドキュメント更新Implementation Status セクション)

Phase 196+: 候補

  • Pattern 3 のネスト対応if-in-if
  • LoopBodyLocal + MethodCall + P3 の統合
  • JsonParser の _parse_string 完全版(配列アクセス対応 = Phase 200+

設計原則Phase 195

  1. 既存箱再利用:

    • PhiGroupBox を作らず、ExitLine/CarrierInfo を拡張
    • YAGNIYou Aren't Gonna Need It原則
  2. 段階的拡張:

    • 単一キャリアPhase 170-189→ 複数キャリアPhase 195
    • 2-3 個程度に限定(無理に全部対応しない)
  3. Fail-Fast 継承:

    • ConditionEnv 拡張なしPhase 200+ 保留)
    • 複雑なパターンは Phase 195+ に延期
  4. 箱理論の実践:

    • 設計フェーズで構造を固める
    • 実装フェーズは lowerer のみに集中
    • ドキュメント駆動開発

Implementation Status

完了日: 2025-12-09 状態: Lowerer/ExitLine 側は完了、JoinIR→MIR 変換バグにより E2E ブロック

実装サマリ

Phase 195-impl で実装したこと:

  1. loop_with_if_phi_minimal.rs (+91行):

    • multi-carrier PHI 生成を追加sum + count の 2 キャリア対応)
    • 各キャリアについて then/else 値を収集し、個別に PHI を生成
    • JoinIR 上で正しい PHI 構造を出力
  2. pattern3_with_if_phi.rs (+68行):

    • 単一キャリア(後方互換)と複数キャリアを動的に扱うよう拡張
    • exit_bindings に複数キャリアを載せる処理を追加
  3. ExitLine / LoopExitBinding / CarrierVar.join_id:

    • 変更不要! 既存インフラをそのまま利用できた
    • Phase 170-189 で整備された CarrierVar.join_id が multi-carrier を想定していた
    • YAGNI 原則の正しさが証明された

Blocker: Nested Select→Branch+Phi 変換バグ

問題の概要:

  • JoinIR 側では正しい PHI が生成されている
  • MIR 変換時に PHI inputs が undefined を参照するようになる

具体例:

JoinIR (正しい):
  ValueId(20) = phi [(BasicBlockId(3), ValueId(14)), (BasicBlockId(4), ValueId(18))]

MIR (壊れている):
  bb10:
      %27 = phi [%28, bb8], [%32, bb9]  // ← %28, %32 は undefined

原因分析:

  • joinir_block.rs の Select 命令処理で、Nested Selectif-else が複数キャリアを持つ場合)の変換が壊れている
  • Select → 3ブロック + 1 PHI の展開時に、block ID マッピングまたは ValueId マッピングが不整合
  • Pattern3 の multi-carrier 対応自体は問題なく、bridge 層の既存バグ

対応方針:

  • Phase 195 の scope からは外すLowerer/ExitLine の責務は完了)
  • Phase 196 として Select 展開バグの調査・修正を別タスク化

テスト状況

後方互換テスト:

  • 単一キャリア P3 テストloop_if_phi.hako 等): 退行確認が必要
  • Pattern1/2/4 代表テスト: 影響なしSelect 変換に依存しないパターン)

multi-carrier E2E テスト:

  • phase195_sum_count.hako: JoinIR 生成は正しい、MIR 変換でブロック

次のステップ

Phase 196: Select 展開/変換バグ調査&修正(別タスク)

  • select_expansion / instruction_rewriter の責務を doc に整理
  • 「1 Select = 3 ブロック + 1 PHI」変換インタフェースを明文化
  • block reuse / block ID マッピングの切り分け

Phase 195 完了時点での成果:

  • P3 Lowerer は複数キャリア対応完了
  • ExitLine/CarrierVar は既に対応済み(変更不要)
  • JoinIR→MIR bridge の Select バグは別 Issue として分離