## Summary
skip_whitespace パターンを Skeleton→Decision で認識可能に。
dev-only 観測で chosen=Pattern3IfPhi / missing_caps=[] を固定。
## Changes
- src/mir/loop_canonicalizer/mod.rs:
- try_extract_skip_whitespace_pattern() 追加
- loop(cond) { ... if check { p = p + 1 } else { break } } パターン認識
- carrier name, delta, body statements を抽出
- canonicalize_loop_expr() 拡張(skip_whitespace 対応)
- Pattern3IfPhi 成功時は RoutingDecision::success 返却
- Skeleton に HeaderCond, Body, Update ステップ追加
- CarrierSlot に Counter role 設定
- ExitContract に has_break=true 設定
- Phase 3 unit tests 追加
- test_skip_whitespace_pattern_recognition: 基本パターン
- test_skip_whitespace_with_body_statements: body 付きパターン
- test_skip_whitespace_fails_without_else: else なし失敗
- test_skip_whitespace_fails_with_wrong_delta: 減算パターン失敗
- Phase 2 obsolete tests 削除
- src/mir/builder/control_flow/joinir/routing.rs:
- Debug 出力拡張(chosen pattern 表示)
## Tests
- cargo test --release --lib loop_canonicalizer::tests: PASS(11 tests)
- cargo test --release --lib: PASS(1044 tests, 退行なし)
- HAKO_JOINIR_DEBUG=1 test_pattern3_skip_whitespace.hako:
- chosen=Pattern3IfPhi ✅
- missing_caps=[] ✅
## Validation
- ✅ dev-only 観測(HAKO_JOINIR_DEBUG=1)のときだけログ出力
- ✅ フラグ OFF 時は完全不変
- ✅ skip_whitespace パターンで SUCCESS 固定
- ✅ unit tests で全パターン固定
Phase 137-3 complete
11 KiB
11 KiB
Loop Canonicalizer(設計 SSOT)
Status: Phase 2 done(dev-only 観測まで)
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 生成側に残りやすい。
LoopSkeleton(SSOT になる出力)
Canonicalizer の出力は “ループの骨格” に限る。例示フィールド:
steps: Vec<Step>HeaderCond(あれば)BodyInit(body-local 初期化を分離するなら)BreakCheck/ContinueCheck(あれば)Updates(carrier 更新規則)Tail(継続呼び出し/次ステップ)
carriers: Vec<CarrierSlot>(loop var を含む。役割/更新規則/境界通過の契約)exits: ExitContract(break/continue/return の有無と payload)captured: Vec<CapturedSlot>(外側変数の取り込み)derived: Vec<DerivedSlot>(digit_pos 等の派生値)
Capability Guard(shape guard の上位化)
Skeleton を生成できても、lower/merge 契約が成立するとは限らない。
そこで SkeletonGuard を “Capability の集合” として設計する。
例:
RequiresConstStepIncrement(i=i+const のみ)BreakOnlyOnce/ContinueOnlyInTailNoSideEffectInHeader(header に副作用がない)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 境界の原則
- 入力: AST(LoopExpr)
- 出力: LoopSkeleton のみ(JoinIR は生成しない)
- 禁止: Skeleton に JoinIR 固有の情報を含めない(BlockId, ValueId 等)
実装の入口(現状)
実装(Phase 1–2)はここ:
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.rs(joinir_dev_enabled()配下)
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 {
/// 選択された Pattern(None = Fail-Fast)
pub chosen: Option<LoopPatternKind>,
/// 不足している Capability のリスト
pub missing_caps: Vec<&'static str>,
/// 選択理由(デバッグ用)
pub notes: Vec<String>,
/// error_tags への追記(contract_checks 用)
pub error_tags: Vec<String>,
}
出力先マッピング
| 出力先 | 条件 | 用途 |
|---|---|---|
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 を境界に渡す)
受け入れ基準
LoopCanonicalizer::canonicalize(ast)が上記 Skeleton を返すRoutingDecision.chosen == Some(Pattern3)RoutingDecision.missing_caps == []- 既存 smoke
phase135_trim_mir_verify.shが退行しない
追加・変更チェックリスト
- 追加するループ形を最小 fixture に落とす(再現固定)
- LoopSkeleton の差分(steps/exits/carriers)を明示する
- 必要 Capability を列挙し、未達は Fail-Fast(理由が出る)
- 既存 smoke/verify が退行しない(quick は重くしない)
- 新規 Capability は先に
loop-canonicalizer.mdに追記してから実装