# Loop Canonicalizer(設計 SSOT) Status: Design (P0) 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` - `HeaderCond`(あれば) - `BodyInit`(body-local 初期化を分離するなら) - `BreakCheck` / `ContinueCheck`(あれば) - `Updates`(carrier 更新規則) - `Tail`(継続呼び出し/次ステップ) - `carriers: Vec`(loop var を含む。役割/更新規則/境界通過の契約) - `exits: ExitContract`(break/continue/return の有無と payload) - `captured: Vec`(外側変数の取り込み) - `derived: Vec`(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, /// キャリア(ループ変数・更新規則・境界通過の契約) pub carriers: Vec, /// 出口契約(break/continue/return の有無と payload) pub exits: ExitContract, /// 外部キャプチャ(省略可: 外側変数の取り込み) pub captured: Option>, } /// ステップの種類(最小限) 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 }, } /// キャリアの更新種別 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 等) --- ## 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) pub chosen: Option, /// 不足している Capability のリスト pub missing_caps: Vec<&'static str>, /// 選択理由(デバッグ用) pub notes: Vec, /// error_tags への追記(contract_checks 用) pub error_tags: Vec, } ``` ### 出力先マッピング | 出力先 | 条件 | 用途 | |----------------------------|-------------------------------|-----------------------------------| | `error_tags` | `chosen.is_none()` | Fail-Fast のエラーメッセージ | | `contract_checks` | debug build + 契約違反時 | Phase 135 P1 の verifier に統合 | | `NYASH_LOOP_ROUTING_TRACE` | 環境変数 ON 時 | 開発時のルーティング追跡 | | 統計 JSON | 将来拡張 | Corpus 分析(Skeleton Signature) | ### error_tags との統合 - `RoutingDecision.error_tags` は `src/mir/builder/control_flow/joinir/error_tags.rs` に追記 - 既存の `ErrorTagCollector` を使用(新規 Box は作らない) --- ## 最初の対象ループ: 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` に追記してから実装