Files
hakorune/docs/development/current/main/design/loop-canonicalizer.md
nyash-codex a0009d474d feat(mir): Loop Canonicalizer Phase 3 - skip_whitespace pattern recognition
## 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
2025-12-16 05:38:18 +09:00

281 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Loop Canonicalizer設計 SSOT
Status: Phase 2 donedev-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 生成側に残りやすい。
## LoopSkeletonSSOT になる出力)
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 Guardshape 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 境界の原則
- **入力**: 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.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` に集約し、以下に流す:
```rust
pub struct RoutingDecision {
/// 選択された PatternNone = 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`
```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` に追記してから実装