Files
hakorune/docs/archive/phases/phase-33/phase33-13-trim-pattern-observation.md
nyash-codex a7dbc15878 feat(joinir): Phase 240-EX - Pattern2 header condition ExprLowerer integration
Implementation:
- Add make_pattern2_scope_manager() helper for DRY
- Header conditions use ExprLowerer for supported patterns
- Legacy fallback for unsupported patterns
- Fail-Fast on supported patterns that fail

Tests:
- 4 new tests (all pass)
- test_expr_lowerer_supports_simple_header_condition_i_less_literal
- test_expr_lowerer_supports_header_condition_var_less_var
- test_expr_lowerer_header_condition_generates_expected_instructions
- test_pattern2_header_condition_via_exprlowerer

Also: Archive old phase documentation (34k lines removed)

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 00:33:04 +09:00

12 KiB
Raw Blame History

Phase 33-13: trim Pattern Observation

33-13-1: 代表ケースの再観測結果

テストケース

// local_tests/test_trim_simple_pattern.hako
method trim_string_simple(s) {
    if s == null { return "" }

    local start = 0
    local end = s.length()

    // Loop 1: Trim leading spaces (Pattern2: loop with break)
    loop(start < end) {
        local ch = s.substring(start, start + 1)
        if ch == " " {
            start = start + 1
        } else {
            break
        }
    }

    // Loop 2: Trim trailing spaces (Pattern2: loop with break)
    loop(end > start) {
        local ch = s.substring(end - 1, end)
        if ch == " " {
            end = end - 1
        } else {
            break
        }
    }

    return s.substring(start, end)
}

観測結果

ルーティング

  • Main.trim_string_simple/1 がホワイトリストに追加済み
  • Pattern2_WithBreak として正しく検出

Loop 1 (start キャリア)

[trace:varmap] pattern2_start: ... end→r22, ... start→r19
[cf_loop/pattern2] Phase 171-fix: ConditionEnv contains 2 variables:
  Loop param 'start' → JoinIR ValueId(0)
  1 condition-only bindings:
    'end': HOST ValueId(22) → JoinIR ValueId(1)
[joinir/pattern2] Phase 172-3: ExitMeta { start → ValueId(9) }
  • キャリア: start
  • ExitMeta: start → ValueId(9)
  • 条件バインディング: end (read-only)

Loop 2 (end キャリア)

[trace:varmap] pattern2_start: ... end→r22, ... start→r32
[cf_loop/pattern2] Phase 171-fix: ConditionEnv contains 2 variables:
  Loop param 'end' → JoinIR ValueId(0)
  1 condition-only bindings:
    'start': HOST ValueId(32) → JoinIR ValueId(1)
[joinir/pattern2] Phase 172-3: ExitMeta { end → ValueId(9) }
  • キャリア: end
  • ExitMeta: end → ValueId(9)
  • 条件バインディング: start (should use Loop 1's exit value!)

問題発見

SSA-undef エラー:

[ssa-undef-debug] fn=Main.trim_string_simple/1 bb=BasicBlockId(16)
  used=ValueId(32)
  inst=Copy { dst: ValueId(37), src: ValueId(32) }

エラーメッセージ:

[ERROR] ❌ [rust-vm] VM error: Invalid value:
  use of undefined value ValueId(32)
  (fn=Main.trim_string_simple/1, last_block=Some(BasicBlockId(16)),
   last_inst=Some(Copy { dst: ValueId(37), src: ValueId(32) }))

根本原因分析

問題: 連続ループの SSA 接続

  1. ループ1終了: start の exit value が variable_map に更新される
  2. ループ2開始: start を condition_binding として読む
    • BUG: HOST ValueId(32) を参照ループ1の古い start
    • 期待: variable_map["start"] の更新後の値を参照すべき

仮説

ExitLineReconnectorvariable_map を更新しているはずだが、 Pattern2 lowerer が condition_bindings を作成する時点で、 その更新後の値を参照していない。

// Pattern2 lowerer の問題箇所
let host_id = self.variable_map.get(var_name)
    .copied()
    .ok_or_else(|| ...)?;

この variable_map.get() 呼び出し時点で、 前のループの ExitLineReconnector がまだ実行されていない可能性がある。

解決方向性

Option A: ExitLineReconnector の即時実行保証

  • merge_joinir_mir_blocks() 完了後すぐに variable_map が更新されていることを保証

Option B: condition_bindings の遅延解決

  • condition_bindings を作成する時点ではなく、 JoinIR merge 時に variable_map から最新値を取得

Option C: PHI 接続の修正

  • ループ2 の PHI 入力が ループ1 の exit block から正しく接続されていることを確認

次のステップ (33-13-2)

  1. ExitLineReconnector の呼び出しタイミングを確認
  2. variable_map の更新フローを追跡
  3. 連続ループの SSA 接続を設計

33-13-2: LoopExitContract 設計

現在の ExitMeta 構造

pub struct ExitMeta {
    pub exit_values: BTreeMap<String, ValueId>,
}

trim の理想的な ExitMeta

ループ1:

ExitMeta {
    exit_values: {
        "start": ValueId(X)  // ループ出口での start の値
    }
}

ループ2:

ExitMeta {
    exit_values: {
        "end": ValueId(Y)  // ループ出口での end の値
    }
}

問題: 連続ループの variable_map 更新

初期状態:
  variable_map["start"] = r19
  variable_map["end"] = r22

ループ1 JoinIR merge 後:
  variable_map["start"] = r35 (remapped exit value)
  variable_map["end"] = r22 (unchanged)

ループ2 condition_bindings 構築:
  start: HOST r??? → JoinIR ValueId(1)  // r35 or r32?

ループ2 JoinIR merge 後:
  variable_map["end"] = r48 (remapped exit value)

契約 (Contract)

ExitLineReconnector の契約:

  1. merge_joinir_mir_blocks() 完了時点で variable_map が更新されている
  2. 後続のコードは variable_map["carrier"] で最新の出口値を取得できる

Pattern2 lowerer の契約:

  1. condition_bindings は variable_map の 現在の値 を使用する
  2. ループ開始時点での variable_map が正しく更新されていることを前提とする

検証ポイント

  1. merge_joinir_mir_blocks() の最後で ExitLineOrchestrator が呼ばれているか?
  2. ExitLineReconnector が variable_map を正しく更新しているか?
  3. Pattern2 lowerer が condition_bindings を構築するタイミングは正しいか?

🎯 33-13-2: 根本原因特定!

問題のフロー

1. ExitMetaCollector: exit_bindings 作成
   - start → JoinIR ValueId(9)

2. merge_joinir_mir_blocks:
   - JoinIR ValueId(9) → HOST ValueId(32) (remap)

3. exit_phi_builder: PHI 作成
   - phi_dst = builder.value_gen.next() → ValueId(0) ← NEW VALUE!
   - PHI { dst: ValueId(0), inputs: [..., ValueId(32)] }

4. ExitLineReconnector: variable_map 更新
   - variable_map["start"] = remapper.get_value(JoinIR ValueId(9)) = ValueId(32)

5. 問題!
   - variable_map["start"] = ValueId(32)
   - BUT PHI が定義しているのは ValueId(0)
   - → ValueId(32) は未定義!

根本原因

exit_phi_builder と ExitLineReconnector の不整合:

  • exit_phi_builder は新しい phi_dst を割り当て
  • ExitLineReconnectorremapped_exit を variable_map に設定
  • PHI が定義している ValueId と variable_map が指す ValueId が異なる

設計上の制限

単一 PHI 問題:

  • 現在の exit_phi_builder1つの PHI しか作らない
  • しかし trim は 2つのキャリアstart, endを持つ
  • 複数キャリアには 複数の exit PHI が必要

解決方向性

Option A: ExitLineReconnector を exit_phi_result を使うように変更

  • シンプルだが、複数キャリアには対応できない

Option B: exit_phi_builder を複数キャリア対応に拡張

  • 各 exit_binding ごとに PHI を作成
  • ExitLineReconnector はその PHI の dst を variable_map に設定

Option C: exit_bindings から直接 PHI を作成する新 Box

  • ExitLinePHIBuilder Box を新設
  • 責任分離: PHI 作成と variable_map 更新を統合

推奨: Option B + C のハイブリッド

  • exit_phi_builder を拡張して exit_bindings を受け取る
  • 各キャリアごとに PHI を作成し、その dst を返す
  • ExitLineReconnector はその結果を variable_map に設定

次のステップ

Phase 33-13-3: exit_phi_builder の複数キャリア対応

  1. exit_bindings を exit_phi_builder に渡す
  2. 各キャリアごとに PHI を作成
  3. 各 PHI の dst を carrier → phi_dst マップとして返す
  4. ExitLineReconnector がそのマップを使って variable_map を更新

Phase 33-13-4: 統合テスト

  1. 単一キャリアPattern 2 simpleが動作確認
  2. 複数キャリアtrimが動作確認

Phase 33-13-2: 「式結果 PHI」と「キャリア PHI」の責務分離設計

アーキテクチャ分析

現在のフロー:

1. instruction_rewriter: Return の戻り値を exit_phi_inputs に収集
   - exit_phi_inputs.push((new_block_id, remapped_val))
   - これは「式としての戻り値」(例: loop_min_while() の結果)

2. exit_phi_builder: exit_phi_inputs から式結果 PHI を1つ作成
   - PHI { dst: NEW_ID, inputs: exit_phi_inputs }
   - 「式としての戻り値」用

3. ExitLineReconnector: exit_bindings の remapped_exit を variable_map に設定
   - variable_map[carrier] = remapper.get_value(join_exit_value)
   - これは「キャリア更新」用

根本問題の再確認

問題: ExitLineReconnector が設定する remapped_exit は、 exit_block に到達する前のブロックで定義されている。

しかし SSA では、exit_block 以降から参照するには、 exit_block 内で PHI で合流させる必要がある

# 問題のあるコード                  # 正しいコード
k_exit:                             k_exit:
  // 何もない                         %start_final = phi [(%bb_A, %32), (%bb_B, %35)]
  // exit_block 以降で %32 参照       // exit_block 以降で %start_final 参照
  // → %32 は未定義!                 // → OK

責務分離設計

式結果 PHI (exit_phi_builder 担当)

用途: ループが「値を返す式」として使われる場合

local result = loop_min_while(...)  // ← 式として使用

実装:

  • instruction_rewriter が Return の戻り値を収集
  • exit_phi_builder がそれらを合流させる PHI を生成
  • 返り値: Option<ValueId> (PHI の dst、または None)

キャリア PHI (新設: ExitCarrierPHIBuilder 担当)

用途: ループが「状態を更新するだけ」の場合

// trim パターン: start, end を更新
loop(start < end) { ... }  // start キャリア
loop(end > start) { ... }  // end キャリア

実装案:

  1. exit_bindings から各キャリアの exit value を取得
  2. 各キャリアごとに PHI を生成
  3. PHI の dst を返す BTreeMap<String, ValueId>
  4. ExitLineReconnector がその phi_dst を variable_map に設定

修正計画

Phase 33-13-3: exit_phi_builder をキャリア PHI 対応に拡張

変更前:

pub fn build_exit_phi(
    builder: &mut MirBuilder,
    exit_block_id: BasicBlockId,
    exit_phi_inputs: &[(BasicBlockId, ValueId)],  // 式結果のみ
    debug: bool,
) -> Result<Option<ValueId>, String>

変更後:

pub struct ExitPhiResult {
    pub expr_result: Option<ValueId>,              // 式結果 PHI (従来)
    pub carrier_phis: BTreeMap<String, ValueId>,   // キャリア PHI (新設)
}

pub fn build_exit_phi(
    builder: &mut MirBuilder,
    exit_block_id: BasicBlockId,
    exit_phi_inputs: &[(BasicBlockId, ValueId)],
    carrier_inputs: &BTreeMap<String, Vec<(BasicBlockId, ValueId)>>,  // NEW
    debug: bool,
) -> Result<ExitPhiResult, String>

Phase 33-13-4: ExitLineReconnector を phi_dst を使うように修正

変更前 (reconnector.rs 99-107行):

if let Some(remapped_value) = remapper.get_value(binding.join_exit_value) {
    if let Some(var_vid) = builder.variable_map.get_mut(&binding.carrier_name) {
        *var_vid = remapped_value;  // ← remapped_exit を直接使用
    }
}

変更後:

if let Some(phi_dst) = carrier_phis.get(&binding.carrier_name) {
    if let Some(var_vid) = builder.variable_map.get_mut(&binding.carrier_name) {
        *var_vid = *phi_dst;  // ← PHI の dst を使用
    }
}

設計上の決定事項

  1. 式結果 PHI は exit_phi_builder が担当 (変更なし)
  2. キャリア PHI は exit_phi_builder が追加で担当 (拡張)
  3. ExitLineReconnector は PHI の dst を variable_map に設定 (修正)
  4. exit_bindings は SSOT として維持 (変更なし)

Date: 2025-12-07

Status: In Progress - Design Phase Complete!

Status: Historical