Files
hakorune/docs/development/current/main/design/loop-canonicalizer.md
nyash-codex 568619df89 feat(mir): Phase 92 P2-2 - Body-local variable support for ConditionalStep
Phase 92 P2-2完了:ConditionalStepのcondition(ch == '\\'など)でbody-local変数をサポート

## 主要変更

### 1. condition_lowerer.rs拡張
- `lower_condition_to_joinir`に`body_local_env`パラメータ追加
- 変数解決優先度:ConditionEnv → LoopBodyLocalEnv
- すべての再帰ヘルパー(comparison, logical_and, logical_or, not, value_expression)対応

### 2. conditional_step_emitter.rs修正
- `emit_conditional_step_update`に`body_local_env`パラメータ追加
- condition loweringにbody-local環境を渡す

### 3. loop_with_break_minimal.rs修正
- break condition loweringをbody-local init の**後**に移動(line 411)
- header_break_lowering::lower_break_conditionにbody_local_env渡す
- emit_conditional_step_updateにbody_local_env渡す(line 620)

### 4. header_break_lowering.rs修正
- `lower_break_condition`に`body_local_env`パラメータ追加
- scope_managerにbody-local環境を渡す

### 5. 全呼び出し箇所修正
- expr_lowerer.rs (2箇所)
- method_call_lowerer.rs (2箇所)
- loop_with_if_phi_if_sum.rs (3箇所)
- loop_with_continue_minimal.rs (1箇所)
- carrier_update_emitter.rs (1箇所・legacy)

## アーキテクチャ改善

### Break Condition Lowering順序修正
旧: Header → **Break Cond** → Body-local Init
新: Header → **Body-local Init** → Break Cond

理由:break conditionが`ch == '\"'`のようにbody-local変数を参照する場合、body-local initが先に必要

### 変数解決優先度(Phase 92 P2-2)
1. ConditionEnv(ループパラメータ、captured変数)
2. LoopBodyLocalEnv(body-local変数like `ch`)

## テスト

### ビルド
 cargo build --release成功(30 warnings、0 errors)

### E2E
⚠️ body-local promotion問題でブロック(Phase 92範囲外)
- Pattern2はbody-local変数をcarrier promotionする必要あり
- 既存パターン(A-3 Trim, A-4 DigitPos)に`ch = get_char(i)`が該当しない
- **Phase 92 P2-2目標(condition loweringでbody-local変数サポート)は達成**

## 次タスク(Phase 92 P3以降)
- body-local variable promotion拡張(Pattern2で`ch`のような変数を扱う)
- P5b E2Eテスト完全動作確認

## Phase 92 P2-2完了
 Body-local変数のcondition lowering対応完了
 ConditionalStepでbody-local変数参照可能
 Break condition lowering順序修正
2025-12-16 17:08:15 +09:00

563 lines
23 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 141 完了(ドキュメント充実)
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`
## アーキテクチャ図
### データフロー
```mermaid
graph LR
A[AST Loop] --> B[Canonicalizer]
B --> C{LoopSkeleton}
B --> D{RoutingDecision}
C --> E[Capability Guard]
D --> E
E --> F{Pass?}
F -->|Yes| G[Pattern Router]
F -->|No| H[Fail-Fast Error]
G --> I[JoinIR Lowerer]
```
### モジュール構成
```
loop_canonicalizer/
├── skeleton_types.rs [Data Structures]
│ ├── LoopSkeleton
│ ├── SkeletonStep
│ ├── UpdateKind
│ └── CarrierSlot
├── capability_guard.rs [Validation]
│ ├── RoutingDecision
│ ├── CapabilityTag (enum)
│ └── Decision constructors
├── pattern_recognizer.rs [Pattern Detection]
│ └── detect patterns (ast_feature_extractor 呼び出し)
└── canonicalizer.rs [Main Logic]
└── canonicalize_loop_expr()
LoopSkeleton + RoutingDecision
Pattern Router (patterns/router.rs)
Pattern Lowerer (pattern1-5_*.rs)
```
### 処理フローPhase 140 完了後)
```mermaid
sequenceDiagram
participant AST as AST Loop
participant Canon as Canonicalizer
participant Guard as Capability Guard
participant Router as Pattern Router
participant Lower as JoinIR Lowerer
AST->>Canon: canonicalize_loop_expr()
Canon->>Canon: extract pattern (ast_feature_extractor)
Canon->>Canon: build LoopSkeleton
Canon->>Guard: validate capabilities
alt All capabilities satisfied
Guard->>Router: RoutingDecision(Pattern2)
Router->>Lower: lower to JoinIR
Lower-->>AST: MIR blocks
else Missing capabilities
Guard-->>AST: Fail-Fast error
end
```
## 目的
- 実アプリ由来のループ形を、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` は定型の語彙で出す(ログ/デバッグ/統計で集約可能にする)
## Capability Tags 対応表
### 各 Tag の詳細
| Tag | 必要な条件 | 対応Pattern | 検出方法 |
|-----|----------|------------|---------|
| `ConstStep` | キャリア更新が定数ステップ(`i = i + const` | P1, P2, P3 | UpdateKind 分析ConstStep variant |
| `SingleBreak` | break 文が単一箇所のみ | P2 | AST 走査でカウント(`count_break_checks() == 1` |
| `SingleContinue` | continue 文が単一箇所のみ | P1, P3 | AST 走査でカウント(`count_continue_checks() == 1` |
| `PureHeader` | ループ条件に副作用がない | P1, P2, P3, P4 | 副作用解析(将来実装) |
| `OuterLocalCond` | 条件変数が外側スコープで定義済み | P3 | Scope 分析BindingContext 参照) |
| `ExitBindings` | 境界へ渡す値が過不足ない | P2, P3 | Carrier 収支計算ExitContract 分析) |
| `CarrierPromotion` | LoopBodyLocal を昇格可能 | P3, P4 | Binding 解析promoted carriers 検出) |
| `BreakValueType` | break 値の型が一貫 | P2 | 型推論TypeContext 参照) |
### 各 Pattern の必須 CapabilityPhase 140 時点)
#### Pattern1 (Minimal)
-`ConstStep` - 定数ステップ増分
-`PureHeader` - 副作用なし条件
-`SingleContinue` - continue 単一箇所
#### Pattern2 (Break)
-`ConstStep` - 定数ステップ増分
-`PureHeader` - 副作用なし条件
-`SingleBreak` - break 単一箇所
-`ExitBindings` - 出口値の完全性
**例**: `skip_whitespace``has_break=true` なので Pattern2 へルーティングPhase 137-5
#### Pattern3 (IfPhi)
-`ConstStep` - 定数ステップ増分
-`PureHeader` - 副作用なし条件
-`OuterLocalCond` - 外側スコープ条件変数
-`CarrierPromotion` - LoopBodyLocal 昇格
**例**: Trim パターンPhase 133
#### Pattern4 (Composite)
-`PureHeader` - 副作用なし条件
-`CarrierPromotion` - LoopBodyLocal 昇格
-`ExitBindings` - 出口値の完全性
#### Pattern5 (Future)
- 🚧 TBD - 将来定義
### Capability 追加時のチェックリスト
新しい Capability を追加する際は以下を確認:
1. **enum 定義**: `capability_guard.rs``CapabilityTag` に variant 追加
2. **文字列変換**: `to_tag()` メソッドに対応を追加(`CAP_MISSING_*` 形式)
3. **説明文**: `description()` メソッドに説明を追加
4. **検出ロジック**: `canonicalizer.rs` に検出ロジックを実装
5. **対応表更新**: このドキュメントの対応表を更新
6. **Pattern 更新**: 必要に応じて各 Pattern の必須 Capability リストを更新
7. **テスト追加**: 新 Capability の検出テストを追加
## 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 等)
---
## 再帰設計Loop / If の境界)
目的: 「複雑な分岐を再帰的に扱いたい」欲求と、責務混在検出正規化配線PHIの爆発を両立させる。
原則(おすすめの境界):
- **Loop canonicalizer が再帰してよい範囲**は「構造の観測/収集」まで。
- loop body 内の if を再帰的に走査して、`break/continue/return/update` の存在・位置・個数・更新種別ConstStep/ConditionalStep 等)を抽出するのは OK。
- ただし **JoinIR/MIR の配線BlockId/ValueId/PHI/merge/exit_bindingsには踏み込まない**
- **if の値契約PHI 相当)**は別箱If canonicalize/lowerに閉じる。
- loop 側では「if が存在する」「if の中に exit がある」などを `notes` / `missing_caps` に落とし、`chosen` は最終 lowerer 選択結果として安定に保つ。
- **nested looploop 内 loop**は当面 capability で Fail-Fast将来の P6 で解禁)。
- これにより “再帰地獄” を設計で遮断できる。
- **PHI 排除env + 継続への正規化)**の本線は `Structured JoinIR → Normalized JoinIR` 側に置く。
- canonicalizer は「どの carrier/env フィールドが更新されるか」を契約として宣言するだけに留める。
将来案(必要になったら):
- LoopSkeleton を肥大化させず、制御だけの再帰ツリー(`ControlTree/StepTree`)を **別 SSOT** として新設し、`LoopSkeleton` は “loop の箱” のまま維持する。
## 実装の入口(現状)
実装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()` 配下)
- Phase 3 で `skip_whitespace` の最小形を `Pattern3IfPhi` として安定に識別できるようにしたdev-only 観測)。
## Capability の語彙Fail-Fast reason タグ)
Skeleton を生成できても lower/merge できるとは限らない。以下の Capability で判定する:
| Capability | 説明 | 未達時の理由タグ | Pattern対応 |
|--------------------------|------------------------------------------|-------------------------------------|------------|
| `ConstStepIncrement` | キャリア更新が定数ステップi=i+const | `CAP_MISSING_CONST_STEP` | P1-P5 |
| `SingleBreakPoint` | break が単一箇所のみ | `CAP_MISSING_SINGLE_BREAK` | P1-P5 |
| `SingleContinuePoint` | continue が単一箇所のみ | `CAP_MISSING_SINGLE_CONTINUE` | P4 |
| `NoSideEffectInHeader` | ループ条件に副作用がない | `CAP_MISSING_PURE_HEADER` | P1-P5 |
| `OuterLocalCondition` | 条件変数が外側スコープで定義済み | `CAP_MISSING_OUTER_LOCAL_COND` | P1-P5 |
| `ExitBindingsComplete` | 境界へ渡す値が過不足ない | `CAP_MISSING_EXIT_BINDINGS` | P1-P5 |
| `CarrierPromotion` | LoopBodyLocal を昇格可能 | `CAP_MISSING_CARRIER_PROMOTION` | P2-P3 |
| `BreakValueConsistent` | break 値の型が一貫 | `CAP_MISSING_BREAK_VALUE_TYPE` | P2-P5 |
| `EscapeSequencePattern` | エスケープシーケンス対応P5b専用 | `CAP_MISSING_ESCAPE_PATTERN` | **P5b** |
**新規 P5b 関連 Capability**:
| Capability | 説明 | 必須条件 |
|--------------------------|------------------------------------------|---------------------------------------|
| `ConstEscapeDelta` | escape_delta が定数 | `if ch == "\\" { i = i + const }` |
| `ConstNormalDelta` | normal_delta が定数 | `i = i + const` (after escape block) |
| `SingleEscapeCheck` | escape check が単一箇所のみ | 複数の escape 処理がない |
| `ClearBoundaryCondition` | 文字列終端検出が明確 | `if ch == boundary { break }` |
### 語彙の安定性
- reason タグは `CAP_MISSING_*` プレフィックスで統一
- 新規追加時は `loop-canonicalizer.md` に先に追記してからコード実装
- ログ / 統計 / error_tags で集約可能
---
## RoutingDecision の出力先
Canonicalizer の判定結果は `RoutingDecision` に集約し、以下に流す:
```rust
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` なら `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(...)`)を使用し、文字列直書きを増やさない
---
## 対象ループ 1: 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` が退行しない
---
## 対象ループ 2: Pattern P5b - Escape Sequence HandlingPhase 91 新規)
### 目的
エスケープシーケンス対応ループを JoinIR 対象に拡大する。JSON/CSV パーサーの文字列処理で共通パターン。
### 対象ファイル
`tools/selfhost/test_pattern5b_escape_minimal.hako`
```hako
loop(i < n) {
local ch = s.substring(i, i+1)
if ch == "\"" { break } // String boundary
if ch == "\\" {
i = i + 1 // Skip escape character (conditional +2 total)
ch = s.substring(i, i+1) // Read escaped character
}
out = out + ch // Process character
i = i + 1 // Standard increment
}
```
### Pattern P5b の特徴
| 特性 | 説明 |
|-----|------|
| **Header** | `loop(i < n)` - Bounded loop on string length |
| **Escape Check** | `if ch == escape_char { i = i + escape_delta }` |
| **Normal Increment** | `i = i + 1` (always +1) |
| **Accumulator** | `out = out + char` - String append pattern |
| **Boundary** | `if ch == boundary { break }` - String terminator |
| **Carriers** | Position (`i`), Accumulator (`out`) |
| **Deltas** | normal_delta=1, escape_delta=2 (or variable) |
### 必要 Capability (P5b 拡張)
-`ConstStepIncrement` (normal: i = i + 1)
-`SingleBreakPoint` (boundary check only)
-`NoSideEffectInHeader` (i < n pure)
- `OuterLocalCondition` (i, n は外側定義)
- **`ConstEscapeDelta`** (escape: i = i + 2, etc.) - **P5b 専用**
- **`SingleEscapeCheck`** (one escape pattern only) - **P5b 専用**
- **`ClearBoundaryCondition`** (explicit boundary detection) - **P5b 専用**
### Fail-Fast 基準 (P5b 非対応のケース)
以下のいずれかに該当する場合Fail-Fast:
1. **複数エスケープチェック**: `if ch == "\\" ... if ch2 == "'" ...`
- 理由: `CAP_MISSING_SINGLE_ESCAPE_CHECK`
2. **可変ステップ**: `i = i + var` (定数でない)
- 理由: `CAP_MISSING_CONST_ESCAPE_DELTA`
3. **無条件に近いループ**: `loop(true)` without clear boundary
- 理由: `CAP_MISSING_CLEAR_BOUNDARY_CONDITION`
4. **複数 break 点**: String boundary + escape processing exit
- 理由: `CAP_MISSING_SINGLE_BREAK`
### 認識アルゴリズム (高レベル)
```
1. Header carrier 抽出: loop(i < n) から i を取得
2. Escape check block 発見: if ch == "\" { ... }
3. Escape delta 抽出: i = i + const
4. Accumulator パターン発見: out = out + ch
5. Normal increment 抽出: i = i + 1 (escape if block 外)
6. Boundary check 発見: if ch == "\"" { break }
7. LoopSkeleton 構築
- carriers: [i (dual deltas), out (append)]
- exits: has_break=true
8. RoutingDecision: Pattern5bEscape
```
### 実装予定 (Phase 91)
**Step 1** (このドキュメント):
- [ ] Pattern P5b 設計書完成
- [ ] テストフィクスチャ作成
- [ ] Capability 定義追加
**Step 2** (Phase 91 本実装):
- [ ] `detect_escape_pattern()` in Canonicalizer
- [ ] Unit tests (P5b recognition)
- [ ] Parity verification (strict mode)
- [ ] Documentation update
**Step 3** (Phase 92 lowering):
- [ ] Pattern5bEscape lowerer 実装
- [ ] E2E test with escape fixture
- [ ] VM/LLVM parity verification
### 受け入れ基準 (Phase 91)
1. Canonicalizer escape pattern を認識
2. RoutingDecision.chosen == Pattern5bEscape
3. missing_caps == [] (すべての capability 満たす)
4. Strict parity green (`HAKO_JOINIR_STRICT=1`)
5. 既存テスト退行なし
6. Lowering Step 3 (Phase 91 では recognition のみ)
### References
- **P5b 詳細設計**: `docs/development/current/main/design/pattern-p5b-escape-design.md`
- **テストフィクスチャ**: `tools/selfhost/test_pattern5b_escape_minimal.hako`
- **Phase 91 計画**: `docs/development/current/main/phases/phase-91/README.md`
---
## 追加・変更チェックリスト
- [ ] 追加するループ形を最小 fixture に落とす再現固定
- [ ] LoopSkeleton の差分steps/exits/carriersを明示する
- [ ] 必要 Capability を列挙し未達は Fail-Fast理由が出る
- [ ] 既存 smoke/verify が退行しないquick は重くしない
- [ ] 新規 Capability は先に `loop-canonicalizer.md` に追記してから実装