## 目的 Canonicalizer の RoutingDecision.chosen を「lowerer 選択の最終結果」にする (構造クラス名ではなく ExitContract ベースの決定) ## 実装内容 ### 1. Canonicalizer の決定ロジック修正 - `src/mir/loop_canonicalizer/mod.rs` - `skip_whitespace` パターン認識で ExitContract (has_break=true) を考慮 - Pattern3IfPhi → Pattern2Break に修正(構造は似ているが break あり) - 単体テスト更新(Pattern2Break 期待に変更) ### 2. Parity 検証テスト修正 - `src/mir/builder/control_flow/joinir/routing.rs` - `test_parity_check_mismatch_detected` → `test_parity_check_skip_whitespace_match` - Canonicalizer と Router の一致を検証(ミスマッチ検出からマッチ検証へ) - Phase 137-5 の SSOT 原則を反映 ### 3. ドキュメント更新 - `docs/development/current/main/design/loop-canonicalizer.md` - Phase 137-5: Decision Policy SSOT セクション追加 - ExitContract 優先の原則を明記 - skip_whitespace の例を追加 - `docs/development/current/main/phases/phase-137/README.md` - Phase 4 完了マーク追加 - Phase 5 完了セクション追加(実装・検証・効果) ## 検証結果 - ✅ 単体テスト: `cargo test --release --lib loop_canonicalizer::tests` (11/11 passed) - ✅ Parity テスト: `cargo test --release --lib 'routing::tests::test_parity'` (2/2 passed) - ✅ Strict モード: `HAKO_JOINIR_STRICT=1` で skip_whitespace parity OK ## 効果 - Router と Canonicalizer の pattern 選択が一致 - ExitContract が pattern 決定の SSOT として明確化 - 構造的特徴(if-else 等)は notes に記録(将来拡張に備える) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
297 lines
12 KiB
Markdown
297 lines
12 KiB
Markdown
# Loop Canonicalizer(設計 SSOT)
|
||
|
||
Status: Phase 3 done(skip_whitespace の安定認識まで)
|
||
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` / `ContinueOnlyInTail`
|
||
- `NoSideEffectInHeader`(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 の出力は以下のフィールドに限定する(これ以上細かくしない):
|
||
|
||
```rust
|
||
/// ループの骨格(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()` 配下)
|
||
- 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` に集約し、以下に流す:
|
||
|
||
```rust
|
||
pub struct RoutingDecision {
|
||
/// 選択された Pattern(None = 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` なら `Pattern2Break`、`has_continue=true` なら `Pattern4Continue`
|
||
- **構造的特徴は notes へ**: 「if-else 構造がある」等の情報は `notes` フィールドに記録
|
||
- **一致保証**: Router と Canonicalizer の pattern 選択が一致することを parity check で検証
|
||
|
||
**例**: `skip_whitespace` パターン
|
||
- 構造: if-else 形式(Pattern3IfPhi に似ている)
|
||
- ExitContract: `has_break=true`
|
||
- **chosen**: `Pattern2Break`(ExitContract が決定)
|
||
- **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`
|
||
|
||
```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` に追記してから実装
|