Files
hakorune/docs/development/current/main/design/loop-canonicalizer.md
nyash-codex 5edd81e373 refactor(mir): Phase 138-P1-A - loop_canonicalizer を4モジュール分割
## 概要
- 931行の mod.rs を 4モジュール + 161行 mod.rs に分割
- 全14テスト PASS、退行なし

## モジュール構成
- skeleton_types.rs (213行) - LoopSkeleton/SkeletonStep/UpdateKind/CarrierSlot/ExitContract
- capability_guard.rs (104行) - RoutingDecision/capability_tags
- pattern_recognizer.rs (249行) - try_extract_skip_whitespace_pattern
- canonicalizer.rs (414行) - canonicalize_loop_expr + 統合テスト
- mod.rs (161行) - 型定義と re-export

## ファイルサイズ達成
- 最大ファイル: canonicalizer.rs 414行(目標250行を一部超過するが許容範囲)
- mod.rs: 931行 → 161行 (83%削減)
- 合計: 1141行(元の931行 + tests分離で増加)

## テスト結果
- 14 tests passed
- loop_canonicalizer::* 全テスト green
2025-12-16 06:41:46 +09:00

12 KiB
Raw Blame History

Loop Canonicalizer設計 SSOT

Status: Phase 5 doneDecision Policy SSOT まで)
Scope: ループ形の組み合わせ爆発を抑えるための “前処理” の設計fixture/shape guard/fail-fast と整合)
Related:

  • SSOT (契約/不変条件): docs/development/current/main/joinir-architecture-overview.md
  • SSOT (地図/入口): docs/development/current/main/design/joinir-design-map.md
  • SSOT (パターン空間): docs/development/current/main/loop_pattern_space.md

目的

  • 実アプリ由来のループ形を、fixture + shape guard + Fail-Fast の段階投入で飲み込む方針を維持したまま、
    “パターン数を増やさない” 形でスケールさせる。
  • ループ lowerer が「検出 + 正規化 + merge 契約」を同時に背負って肥大化するのを防ぎ、責務を前処理に寄せる。

推奨配置(結論)

おすすめ: AST → LoopSkeleton → JoinIR(Structured) の前処理として Canonicalizer を置く。

  • 「組み合わせ爆発」が Pattern 検出/shape guard の手前で起きるため、Normalized 変換だけでは吸収しきれない。
  • LoopSkeleton を SSOT にすると、lowerer は “骨格を吐く” だけの薄い箱になり、Fail-Fast の理由が明確になる。

代替案(参考):

  • Structured JoinIR → Normalized JoinIR を実質 Canonicalizer とみなす(既存延長)。
    ただし「検出/整理の肥大」は Structured 生成側に残りやすい。

LoopSkeletonSSOT になる出力)

Canonicalizer の出力は “ループの骨格” に限る。例示フィールド:

  • steps: Vec<Step>
    • HeaderCond(あれば)
    • BodyInitbody-local 初期化を分離するなら)
    • BreakCheck / ContinueCheck(あれば)
    • Updatescarrier 更新規則)
    • Tail(継続呼び出し/次ステップ)
  • carriers: Vec<CarrierSlot>loop var を含む。役割/更新規則/境界通過の契約)
  • exits: ExitContractbreak/continue/return の有無と payload
  • captured: Vec<CapturedSlot>(外側変数の取り込み)
  • derived: Vec<DerivedSlot>digit_pos 等の派生値)

Capability Guardshape guard の上位化)

Skeleton を生成できても、lower/merge 契約が成立するとは限らない。
そこで SkeletonGuard を “Capability の集合” として設計する。

例:

  • RequiresConstStepIncrementi=i+const のみ)
  • BreakOnlyOnce / ContinueOnlyInTail
  • NoSideEffectInHeaderheader に副作用がない)
  • ExitBindingsComplete(境界へ渡す値が過不足ない)

未達の場合は Fail-Fast理由を RoutingDecision に載せる)。

RoutingDecision理由の SSOT

Canonicalizer は “できない理由” を機械的に返す。

  • RoutingDecision { chosen, missing_caps, notes, error_tags }
  • missing_caps は定型の語彙で出す(ログ/デバッグ/統計で集約可能にする)

Corpus / Signature拡張のための仕組み

将来の規模増加に備え、ループ形の差分検知を Skeleton ベースで行えるようにする。

  • LoopSkeletonSignature = hash(steps + exit_contract + carrier_roles + required_caps)
  • 既知集合との差分が出たら “fixture 化候補” として扱う(設計上の導線)。

実装の境界(非目標)

  • 新しい言語仕様/ルール実装はしない(既存の意味論を保つ)。
  • 非 JoinIR への退避prohibited fallbackは導入しない。
  • 既定挙動は変えない(必要なら dev-only で段階投入する)。

LoopSkeleton の最小フィールドSSOT 境界)

Canonicalizer の出力は以下のフィールドに限定する(これ以上細かくしない):

/// ループの骨格Canonicalizer の唯一の出力)
pub struct LoopSkeleton {
    /// ステップの列HeaderCond, BodyInit, BreakCheck, Updates, Tail
    pub steps: Vec<SkeletonStep>,

    /// キャリア(ループ変数・更新規則・境界通過の契約)
    pub carriers: Vec<CarrierSlot>,

    /// 出口契約break/continue/return の有無と payload
    pub exits: ExitContract,

    /// 外部キャプチャ(省略可: 外側変数の取り込み)
    pub captured: Option<Vec<CapturedSlot>>,
}

/// ステップの種類(最小限)
pub enum SkeletonStep {
    /// ループ継続条件loop(cond) の cond
    HeaderCond { expr: AstExpr },

    /// 早期終了チェックif cond { break }
    BreakCheck { cond: AstExpr, has_value: bool },

    /// スキップチェックif cond { continue }
    ContinueCheck { cond: AstExpr },

    /// キャリア更新i = i + 1 など)
    Update { carrier_name: String, update_kind: UpdateKind },

    /// 本体(その他の命令)
    Body { stmts: Vec<AstStmt> },
}

/// キャリアの更新種別
pub enum UpdateKind {
    /// 定数ステップi = i + const
    ConstStep { delta: i64 },

    /// 条件付き更新if cond { x = a } else { x = b }
    Conditional { then_value: AstExpr, else_value: AstExpr },

    /// 任意更新(上記以外)
    Arbitrary,
}

/// 出口契約
pub struct ExitContract {
    pub has_break: bool,
    pub has_continue: bool,
    pub has_return: bool,
    pub break_has_value: bool,
}

/// キャリアスロット
pub struct CarrierSlot {
    pub name: String,
    pub role: CarrierRole,
    pub update_kind: UpdateKind,
}

/// キャリアの役割
pub enum CarrierRole {
    /// ループカウンタi < n の i
    Counter,
    /// アキュムレータsum += x の sum
    Accumulator,
    /// 条件変数while(is_valid) の is_valid
    ConditionVar,
    /// 派生値digit_pos 等)
    Derived,
}

SSOT 境界の原則

  • 入力: ASTLoopExpr
  • 出力: LoopSkeleton のみJoinIR は生成しない)
  • 禁止: Skeleton に JoinIR 固有の情報を含めないBlockId, ValueId 等)

実装の入口(現状)

実装Phase 12はここ

  • src/mir/loop_canonicalizer/mod.rs

注意:

  • Phase 2 で canonicalize_loop_expr(...) -> Result<(LoopSkeleton, RoutingDecision), String> を導入し、JoinIR ループ入口で dev-only 観測できるようにした(既定挙動は不変)。
  • 観測ポイントJoinIR ループ入口): src/mir/builder/control_flow/joinir/routing.rsjoinir_dev_enabled() 配下)
  • Phase 3 で skip_whitespace の最小形を Pattern3IfPhi として安定に識別できるようにしたdev-only 観測)。

Capability の語彙Fail-Fast reason タグ)

Skeleton を生成できても lower/merge できるとは限らない。以下の Capability で判定する:

Capability 説明 未達時の理由タグ
ConstStepIncrement キャリア更新が定数ステップi=i+const CAP_MISSING_CONST_STEP
SingleBreakPoint break が単一箇所のみ CAP_MISSING_SINGLE_BREAK
SingleContinuePoint continue が単一箇所のみ CAP_MISSING_SINGLE_CONTINUE
NoSideEffectInHeader ループ条件に副作用がない CAP_MISSING_PURE_HEADER
OuterLocalCondition 条件変数が外側スコープで定義済み CAP_MISSING_OUTER_LOCAL_COND
ExitBindingsComplete 境界へ渡す値が過不足ない CAP_MISSING_EXIT_BINDINGS
CarrierPromotion LoopBodyLocal を昇格可能 CAP_MISSING_CARRIER_PROMOTION
BreakValueConsistent break 値の型が一貫 CAP_MISSING_BREAK_VALUE_TYPE

語彙の安定性

  • reason タグは CAP_MISSING_* プレフィックスで統一
  • 新規追加時は loop-canonicalizer.md に先に追記してからコード実装
  • ログ / 統計 / error_tags で集約可能

RoutingDecision の出力先

Canonicalizer の判定結果は RoutingDecision に集約し、以下に流す:

pub struct RoutingDecision {
    /// 選択された PatternNone = Fail-Fast
    /// Phase 137-5: ExitContract に基づく最終 lowerer 選択を反映
    pub chosen: Option<LoopPatternKind>,

    /// 不足している Capability のリスト
    pub missing_caps: Vec<&'static str>,

    /// 選択理由(デバッグ用)
    pub notes: Vec<String>,

    /// error_tags への追記contract_checks 用)
    pub error_tags: Vec<String>,
}

Phase 137-5: Decision Policy SSOT

原則: RoutingDecision.chosen は「lowerer 選択の最終結果」を返す(構造クラス名ではなく)。

  • ExitContract が優先: has_break=true なら Pattern2Breakhas_continue=true なら Pattern4Continue
  • 構造的特徴は notes へ: 「if-else 構造がある」等の情報は notes フィールドに記録
  • 一致保証: Router と Canonicalizer の pattern 選択が一致することを parity check で検証

: skip_whitespace パターン

  • 構造: if-else 形式Pattern3IfPhi に似ている)
  • ExitContract: has_break=true
  • chosen: Pattern2BreakExitContract が決定)
  • notes: "if-else structure with break in else branch"(構造特徴を記録)

出力先マッピング

出力先 条件 用途
error_tags chosen.is_none() Fail-Fast のエラーメッセージ
contract_checks debug build + 契約違反時 Phase 135 P1 の verifier に統合
JoinIR dev/debug joinir_dev_enabled()==true 開発時のルーティング追跡
統計 JSON 将来拡張 Corpus 分析Skeleton Signature

error_tags との統合

  • RoutingDecision の Fail-Fast 文言は src/mir/join_ir/lowering/error_tags.rs の語彙に寄せる
  • 既存のエラータグ(例: error_tags::freeze(...))を使用し、文字列直書きを増やさない

最初の対象ループ: skip_whitespace受け入れ基準

対象ファイル

tools/selfhost/test_pattern3_skip_whitespace.hako

loop(p < len) {
  local ch = s.substring(p, p + 1)
  local is_ws = ... // whitespace 判定
  if is_ws == 1 {
    p = p + 1
  } else {
    break
  }
}

Skeleton 差分

フィールド
steps[0] HeaderCond { expr: p < len }
steps[1] Body { ... } (ch, is_ws 計算)
steps[2] BreakCheck { cond: is_ws == 0 }
steps[3] Update { carrier: "p", ConstStep(1) }
carriers[0] { name: "p", role: Counter, ConstStep }
exits { has_break: true, break_has_value: false }

必要 Capability

  • ConstStepIncrement (p = p + 1)
  • SingleBreakPoint (else { break } のみ)
  • OuterLocalCondition (p, len は外側定義)
  • ExitBindingsComplete (p を境界に渡す)

受け入れ基準

  1. LoopCanonicalizer::canonicalize(ast) が上記 Skeleton を返す
  2. RoutingDecision.chosen == Some(Pattern3)
  3. RoutingDecision.missing_caps == []
  4. 既存 smoke phase135_trim_mir_verify.sh が退行しない

追加・変更チェックリスト

  • 追加するループ形を最小 fixture に落とす(再現固定)
  • LoopSkeleton の差分steps/exits/carriersを明示する
  • 必要 Capability を列挙し、未達は Fail-Fast理由が出る
  • 既存 smoke/verify が退行しないquick は重くしない)
  • 新規 Capability は先に loop-canonicalizer.md に追記してから実装