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順序修正
23 KiB
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
アーキテクチャ図
データフロー
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 完了後)
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 生成側に残りやすい。
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/ContinueOnlyInTailNoSideEffectInHeader(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 の必須 Capability(Phase 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 を追加する際は以下を確認:
- enum 定義:
capability_guard.rsのCapabilityTagに variant 追加 - 文字列変換:
to_tag()メソッドに対応を追加(CAP_MISSING_*形式) - 説明文:
description()メソッドに説明を追加 - 検出ロジック:
canonicalizer.rsに検出ロジックを実装 - 対応表更新: このドキュメントの対応表を更新
- Pattern 更新: 必要に応じて各 Pattern の必須 Capability リストを更新
- テスト追加: 新 Capability の検出テストを追加
Corpus / Signature(拡張のための仕組み)
将来の規模増加に備え、ループ形の差分検知を Skeleton ベースで行えるようにする。
LoopSkeletonSignature = hash(steps + exit_contract + carrier_roles + required_caps)- 既知集合との差分が出たら “fixture 化候補” として扱う(設計上の導線)。
実装の境界(非目標)
- 新しい言語仕様/ルール実装はしない(既存の意味論を保つ)。
- 非 JoinIR への退避(prohibited fallback)は導入しない。
- 既定挙動は変えない(必要なら dev-only で段階投入する)。
LoopSkeleton の最小フィールド(SSOT 境界)
Canonicalizer の出力は以下のフィールドに限定する(これ以上細かくしない):
/// ループの骨格(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 等)
再帰設計(Loop / If の境界)
目的: 「複雑な分岐を再帰的に扱いたい」欲求と、責務混在(検出+正規化+配線+PHI)の爆発を両立させる。
原則(おすすめの境界):
- Loop canonicalizer が再帰してよい範囲は「構造の観測/収集」まで。
- loop body 内の if を再帰的に走査して、
break/continue/return/updateの存在・位置・個数・更新種別(ConstStep/ConditionalStep 等)を抽出するのは OK。 - ただし JoinIR/MIR の配線(BlockId/ValueId/PHI/merge/exit_bindings)には踏み込まない。
- loop body 内の if を再帰的に走査して、
- **if の値契約(PHI 相当)**は別箱(If canonicalize/lower)に閉じる。
- loop 側では「if が存在する」「if の中に exit がある」などを
notes/missing_capsに落とし、chosenは最終 lowerer 選択結果として安定に保つ。
- loop 側では「if が存在する」「if の中に exit がある」などを
- **nested loop(loop 内 loop)**は当面 capability で Fail-Fast(将来の P6 で解禁)。
- これにより “再帰地獄” を設計で遮断できる。
- **PHI 排除(env + 継続への正規化)**の本線は
Structured JoinIR → Normalized JoinIR側に置く。- canonicalizer は「どの carrier/env フィールドが更新されるか」を契約として宣言するだけに留める。
将来案(必要になったら):
- LoopSkeleton を肥大化させず、制御だけの再帰ツリー(
ControlTree/StepTree)を 別 SSOT として新設し、LoopSkeletonは “loop の箱” のまま維持する。
実装の入口(現状)
実装(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 | 説明 | 未達時の理由タグ | 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 に集約し、以下に流す:
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(...))を使用し、文字列直書きを増やさない
対象ループ 1: skip_whitespace(受け入れ基準)
対象ファイル
tools/selfhost/test_pattern3_skip_whitespace.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 を境界に渡す)
受け入れ基準
LoopCanonicalizer::canonicalize(ast)が上記 Skeleton を返すRoutingDecision.chosen == Some(Pattern3)RoutingDecision.missing_caps == []- 既存 smoke
phase135_trim_mir_verify.shが退行しない
対象ループ 2: Pattern P5b - Escape Sequence Handling(Phase 91 新規)
目的
エスケープシーケンス対応ループを JoinIR 対象に拡大する。JSON/CSV パーサーの文字列処理で共通パターン。
対象ファイル
tools/selfhost/test_pattern5b_escape_minimal.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:
-
複数エスケープチェック:
if ch == "\\" ... if ch2 == "'" ...- 理由:
CAP_MISSING_SINGLE_ESCAPE_CHECK
- 理由:
-
可変ステップ:
i = i + var(定数でない)- 理由:
CAP_MISSING_CONST_ESCAPE_DELTA
- 理由:
-
無条件に近いループ:
loop(true)without clear boundary- 理由:
CAP_MISSING_CLEAR_BOUNDARY_CONDITION
- 理由:
-
複数 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)
- ✅ Canonicalizer が escape pattern を認識
- ✅ RoutingDecision.chosen == Pattern5bEscape
- ✅ missing_caps == [] (すべての capability 満たす)
- ✅ Strict parity green (
HAKO_JOINIR_STRICT=1) - ✅ 既存テスト退行なし
- ❌ 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に追記してから実装