diff --git a/docs/development/current/main/design/loop-canonicalizer.md b/docs/development/current/main/design/loop-canonicalizer.md index 2026e762..6e967767 100644 --- a/docs/development/current/main/design/loop-canonicalizer.md +++ b/docs/development/current/main/design/loop-canonicalizer.md @@ -72,10 +72,201 @@ Canonicalizer は “できない理由” を機械的に返す。 - 非 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` に追記してから実装