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
This commit is contained in:
@ -28,7 +28,7 @@
|
|||||||
- **Phase 134 完了**: Plugin loader best-effort loading(決定的順序 + failure 集約 + 継続)を導入。
|
- **Phase 134 完了**: Plugin loader best-effort loading(決定的順序 + failure 集約 + 継続)を導入。
|
||||||
- **Phase 135 完了**: ConditionLoweringBox allocator SSOT(P0: 根治修正 + P1: contract_checks Fail-Fast 強化)。
|
- **Phase 135 完了**: ConditionLoweringBox allocator SSOT(P0: 根治修正 + P1: contract_checks Fail-Fast 強化)。
|
||||||
- **Phase 136 完了**: MirBuilder Context SSOT 化(+ ValueId allocator 掃討)。
|
- **Phase 136 完了**: MirBuilder Context SSOT 化(+ ValueId allocator 掃討)。
|
||||||
- **Phase 137-1 完了**: Loop Canonicalizer(型/語彙の SSOT)を導入(routing/lowering には未介入)。
|
- **Phase 137-2 完了**: Loop Canonicalizer(dev-only 観測)まで完了(既定挙動は不変)。
|
||||||
- **Phase 88 完了**: continue + 可変ステップ(i=i+const 差分)を dev-only fixture で固定、StepCalculator Box 抽出。
|
- **Phase 88 完了**: continue + 可変ステップ(i=i+const 差分)を dev-only fixture で固定、StepCalculator Box 抽出。
|
||||||
- **Phase 89 完了**: P0(ContinueReturn detector)+ P1(lowering 実装)完了。
|
- **Phase 89 完了**: P0(ContinueReturn detector)+ P1(lowering 実装)完了。
|
||||||
- **Phase 90 完了**: ParseStringComposite + `Null` literal + ContinueReturn(同一値の複数 return-if)を dev-only fixture で固定。
|
- **Phase 90 完了**: ParseStringComposite + `Null` literal + ContinueReturn(同一値の複数 return-if)を dev-only fixture で固定。
|
||||||
@ -53,15 +53,15 @@
|
|||||||
|
|
||||||
## 次の指示書(優先順位)
|
## 次の指示書(優先順位)
|
||||||
|
|
||||||
### P0: Loop Canonicalizer の Phase 2(dev-only 観測の導入)
|
### P0: Loop Canonicalizer の Phase 3(Skeleton→Decision の精密化)
|
||||||
|
|
||||||
**状態**: ✅ 設計 + Phase 1(型)完了、Phase 2 へ
|
**状態**: ✅ Phase 2 まで完了、Phase 3 へ
|
||||||
|
|
||||||
**目的**: `LoopSkeleton` / `Capability` / `RoutingDecision` を実際のループ入口で “観測できる” 状態にして、組み合わせ爆発の手前で理由付き Fail-Fast ができる基盤を作る(ただし既定挙動は不変)。
|
**目的**: `LoopSkeleton` から `RoutingDecision`(chosen/missing_caps/notes)を安定に計算し、代表ケース(`skip_whitespace`)で “期待する選択” をテストで固定する(既定挙動は不変)。
|
||||||
|
|
||||||
SSOT:
|
SSOT:
|
||||||
- `docs/development/current/main/design/loop-canonicalizer.md`
|
- `docs/development/current/main/design/loop-canonicalizer.md`
|
||||||
実装(Phase 1):
|
実装:
|
||||||
- `src/mir/loop_canonicalizer/mod.rs`
|
- `src/mir/loop_canonicalizer/mod.rs`
|
||||||
|
|
||||||
**次に触るSSOT**:
|
**次に触るSSOT**:
|
||||||
|
|||||||
@ -42,7 +42,7 @@ JoinIR の箱構造と責務、ループ/if の lowering パターンを把握
|
|||||||
- `docs/development/current/main/design/joinir-design-map.md`
|
- `docs/development/current/main/design/joinir-design-map.md`
|
||||||
5. Loop Canonicalizer(設計 SSOT)
|
5. Loop Canonicalizer(設計 SSOT)
|
||||||
- `docs/development/current/main/design/loop-canonicalizer.md`
|
- `docs/development/current/main/design/loop-canonicalizer.md`
|
||||||
- 実装(Phase 137-1): `src/mir/loop_canonicalizer/mod.rs`
|
- 実装(Phase 137-2): `src/mir/loop_canonicalizer/mod.rs`
|
||||||
6. MIR Builder(Context 分割の入口)
|
6. MIR Builder(Context 分割の入口)
|
||||||
- `src/mir/builder/README.md`
|
- `src/mir/builder/README.md`
|
||||||
7. Scope/BindingId(shadowing・束縛同一性の段階移行)
|
7. Scope/BindingId(shadowing・束縛同一性の段階移行)
|
||||||
|
|||||||
@ -27,11 +27,11 @@
|
|||||||
- Phase 136: MirBuilder の Context 分割を完了し、状態の SSOT を Context に一本化。
|
- Phase 136: MirBuilder の Context 分割を完了し、状態の SSOT を Context に一本化。
|
||||||
- 詳細: `docs/development/current/main/phases/phase-136/README.md`
|
- 詳細: `docs/development/current/main/phases/phase-136/README.md`
|
||||||
|
|
||||||
## 2025‑12‑16:Phase 137‑1(短報)
|
## 2025‑12‑16:Phase 137‑2(短報)
|
||||||
|
|
||||||
- Loop Canonicalizer の Phase 1(型/語彙の SSOT)を導入(routing/lowering には未介入)。
|
- Loop Canonicalizer の Phase 2(dev-only 観測)まで完了(既定挙動は不変)。
|
||||||
- 設計 SSOT: `docs/development/current/main/design/loop-canonicalizer.md`
|
- 設計 SSOT: `docs/development/current/main/design/loop-canonicalizer.md`
|
||||||
- 実装: `src/mir/loop_canonicalizer/mod.rs`
|
- 実装: `src/mir/loop_canonicalizer/mod.rs`(+ 観測: `src/mir/builder/control_flow/joinir/routing.rs`)
|
||||||
|
|
||||||
## 2025‑12‑14:現状サマリ
|
## 2025‑12‑14:現状サマリ
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Loop Canonicalizer(設計 SSOT)
|
# Loop Canonicalizer(設計 SSOT)
|
||||||
|
|
||||||
Status: Phase 1 done(型定義まで)
|
Status: Phase 2 done(dev-only 観測まで)
|
||||||
Scope: ループ形の組み合わせ爆発を抑えるための “前処理” の設計(fixture/shape guard/fail-fast と整合)
|
Scope: ループ形の組み合わせ爆発を抑えるための “前処理” の設計(fixture/shape guard/fail-fast と整合)
|
||||||
Related:
|
Related:
|
||||||
- SSOT (契約/不変条件): `docs/development/current/main/joinir-architecture-overview.md`
|
- SSOT (契約/不変条件): `docs/development/current/main/joinir-architecture-overview.md`
|
||||||
@ -160,12 +160,12 @@ pub enum CarrierRole {
|
|||||||
|
|
||||||
## 実装の入口(現状)
|
## 実装の入口(現状)
|
||||||
|
|
||||||
Phase 1(型定義のみ)の実装はここ:
|
実装(Phase 1–2)はここ:
|
||||||
- `src/mir/loop_canonicalizer/mod.rs`
|
- `src/mir/loop_canonicalizer/mod.rs`
|
||||||
|
|
||||||
注意:
|
注意:
|
||||||
- ここは「型と語彙の SSOT」を置く場所で、routing/lowering にはまだ介入しない。
|
- Phase 2 で `canonicalize_loop_expr(...) -> Result<(LoopSkeleton, RoutingDecision), String>` を導入し、JoinIR ループ入口で dev-only 観測できるようにした(既定挙動は不変)。
|
||||||
- Phase 2 以降で `canonicalize(loop_ast) -> (LoopSkeleton, RoutingDecision)` を導入し、dev-only で観測から始める。
|
- 観測ポイント(JoinIR ループ入口): `src/mir/builder/control_flow/joinir/routing.rs`(`joinir_dev_enabled()` 配下)
|
||||||
|
|
||||||
## Capability の語彙(Fail-Fast reason タグ)
|
## Capability の語彙(Fail-Fast reason タグ)
|
||||||
|
|
||||||
@ -197,7 +197,7 @@ Canonicalizer の判定結果は `RoutingDecision` に集約し、以下に流
|
|||||||
```rust
|
```rust
|
||||||
pub struct RoutingDecision {
|
pub struct RoutingDecision {
|
||||||
/// 選択された Pattern(None = Fail-Fast)
|
/// 選択された Pattern(None = Fail-Fast)
|
||||||
pub chosen: Option<LoopPattern>,
|
pub chosen: Option<LoopPatternKind>,
|
||||||
|
|
||||||
/// 不足している Capability のリスト
|
/// 不足している Capability のリスト
|
||||||
pub missing_caps: Vec<&'static str>,
|
pub missing_caps: Vec<&'static str>,
|
||||||
@ -206,7 +206,7 @@ pub struct RoutingDecision {
|
|||||||
pub notes: Vec<String>,
|
pub notes: Vec<String>,
|
||||||
|
|
||||||
/// error_tags への追記(contract_checks 用)
|
/// error_tags への追記(contract_checks 用)
|
||||||
pub error_tags: Vec<ErrorTag>,
|
pub error_tags: Vec<String>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Phase 137: Loop Canonicalizer(前処理 SSOT)
|
# Phase 137: Loop Canonicalizer(前処理 SSOT)
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
- 状態: 🔶 進行中(Phase 1 完了)
|
- 状態: 🔶 進行中(Phase 2 完了)
|
||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
- ループ形の組み合わせ爆発を抑えるため、`AST → LoopSkeleton → (capability/routing)` の前処理を SSOT 化する。
|
- ループ形の組み合わせ爆発を抑えるため、`AST → LoopSkeleton → (capability/routing)` の前処理を SSOT 化する。
|
||||||
@ -15,11 +15,17 @@
|
|||||||
- `RoutingDecision` / capability tags(`CAP_MISSING_*`)
|
- `RoutingDecision` / capability tags(`CAP_MISSING_*`)
|
||||||
- 注意: Phase 1 は「型と語彙」のみ。routing/lowering にはまだ介入しない。
|
- 注意: Phase 1 は「型と語彙」のみ。routing/lowering にはまだ介入しない。
|
||||||
|
|
||||||
## Phase 2(次): dev-only 観測の導入
|
## Phase 2(完了): dev-only 観測の導入
|
||||||
|
|
||||||
- 目標: ループ入口で `LoopSkeleton` と `RoutingDecision` を生成し、dev-only で観測できるようにする。
|
- 入口:
|
||||||
- 既定挙動: 不変(dev-only の観測から開始)
|
- Canonicalize: `src/mir/loop_canonicalizer/mod.rs`(`canonicalize_loop_expr`)
|
||||||
- スイッチ: 新しい env を増やさず、`joinir_dev_enabled()`(既存)配下で有効化する。
|
- 観測ポイント: `src/mir/builder/control_flow/joinir/routing.rs`(`joinir_dev_enabled()` 配下)
|
||||||
|
- 既定挙動: 不変(dev-only 観測のみ)
|
||||||
|
|
||||||
|
## Phase 3(次): Pattern 検出(Skeleton→Decision の精密化)
|
||||||
|
|
||||||
|
- 目標: `skip_whitespace` を Skeleton から安定に識別し、`RoutingDecision.chosen` と `missing_caps` を期待通りに固定する。
|
||||||
|
- 注意: routing/lowering の変更は dev-only の観測結果が固まってから。
|
||||||
|
|
||||||
## SSOT
|
## SSOT
|
||||||
|
|
||||||
|
|||||||
@ -142,9 +142,12 @@ impl MirBuilder {
|
|||||||
eprintln!("[loop_canonicalizer] Has exits: {}", skeleton.exits.has_any_exit());
|
eprintln!("[loop_canonicalizer] Has exits: {}", skeleton.exits.has_any_exit());
|
||||||
eprintln!("[loop_canonicalizer] Decision: {}",
|
eprintln!("[loop_canonicalizer] Decision: {}",
|
||||||
if decision.is_success() { "SUCCESS" } else { "FAIL_FAST" });
|
if decision.is_success() { "SUCCESS" } else { "FAIL_FAST" });
|
||||||
|
if let Some(pattern) = decision.chosen {
|
||||||
|
eprintln!("[loop_canonicalizer] Chosen pattern: {:?}", pattern);
|
||||||
|
}
|
||||||
|
eprintln!("[loop_canonicalizer] Missing caps: {:?}", decision.missing_caps);
|
||||||
if decision.is_fail_fast() {
|
if decision.is_fail_fast() {
|
||||||
eprintln!("[loop_canonicalizer] Reason: {}", decision.notes.join("; "));
|
eprintln!("[loop_canonicalizer] Reason: {}", decision.notes.join("; "));
|
||||||
eprintln!("[loop_canonicalizer] Missing caps: {:?}", decision.missing_caps);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@ -326,14 +326,116 @@ impl std::fmt::Display for CarrierRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Phase 2: Canonicalization Entry Point
|
// Phase 3: Pattern Recognition Helpers
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Canonicalize a loop AST into LoopSkeleton (Phase 2: Minimal Implementation)
|
/// Try to extract skip_whitespace pattern from loop
|
||||||
|
///
|
||||||
|
/// Pattern structure:
|
||||||
|
/// ```
|
||||||
|
/// loop(cond) {
|
||||||
|
/// // ... optional body statements (Body)
|
||||||
|
/// if check_cond {
|
||||||
|
/// carrier = carrier + const
|
||||||
|
/// } else {
|
||||||
|
/// break
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Returns (carrier_name, delta, body_stmts) if pattern matches.
|
||||||
|
fn try_extract_skip_whitespace_pattern(
|
||||||
|
body: &[ASTNode],
|
||||||
|
) -> Option<(String, i64, Vec<ASTNode>)> {
|
||||||
|
if body.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last statement must be if-else with break
|
||||||
|
let last_stmt = &body[body.len() - 1];
|
||||||
|
|
||||||
|
let (then_body, else_body) = match last_stmt {
|
||||||
|
ASTNode::If {
|
||||||
|
then_body,
|
||||||
|
else_body: Some(else_body),
|
||||||
|
..
|
||||||
|
} => (then_body, else_body),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Then branch must be single assignment: carrier = carrier + const
|
||||||
|
if then_body.len() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (carrier_name, delta) = match &then_body[0] {
|
||||||
|
ASTNode::Assignment { target, value, .. } => {
|
||||||
|
// Extract target variable name
|
||||||
|
let target_name = match target.as_ref() {
|
||||||
|
ASTNode::Variable { name, .. } => name.clone(),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Value must be: target + const
|
||||||
|
match value.as_ref() {
|
||||||
|
ASTNode::BinaryOp {
|
||||||
|
operator: crate::ast::BinaryOperator::Add,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// Left must be same variable
|
||||||
|
let left_name = match left.as_ref() {
|
||||||
|
ASTNode::Variable { name, .. } => name,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if left_name != &target_name {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right must be integer literal
|
||||||
|
let delta = match right.as_ref() {
|
||||||
|
ASTNode::Literal {
|
||||||
|
value: crate::ast::LiteralValue::Integer(n),
|
||||||
|
..
|
||||||
|
} => *n,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
(target_name, delta)
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Else branch must be single break
|
||||||
|
if else_body.len() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match &else_body[0] {
|
||||||
|
ASTNode::Break { .. } => {
|
||||||
|
// Success! Extract body statements (all except last if)
|
||||||
|
let body_stmts = body[..body.len() - 1].to_vec();
|
||||||
|
Some((carrier_name, delta, body_stmts))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Phase 3: Canonicalization Entry Point
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Canonicalize a loop AST into LoopSkeleton (Phase 3: skip_whitespace pattern recognition)
|
||||||
///
|
///
|
||||||
/// Currently supports only the skip_whitespace pattern:
|
/// Currently supports only the skip_whitespace pattern:
|
||||||
/// ```
|
/// ```
|
||||||
/// loop(cond) {
|
/// loop(cond) {
|
||||||
|
/// // ... optional body statements
|
||||||
/// if check_cond {
|
/// if check_cond {
|
||||||
/// carrier = carrier + step
|
/// carrier = carrier + step
|
||||||
/// } else {
|
/// } else {
|
||||||
@ -363,66 +465,55 @@ pub fn canonicalize_loop_expr(
|
|||||||
_ => return Err(format!("Expected Loop node, got: {:?}", loop_expr)),
|
_ => return Err(format!("Expected Loop node, got: {:?}", loop_expr)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Phase 2: Minimal implementation - detect skip_whitespace pattern only
|
// Phase 3: Try to extract skip_whitespace pattern
|
||||||
// Pattern: loop(cond) { if check { update } else { break } }
|
if let Some((carrier_name, delta, body_stmts)) = try_extract_skip_whitespace_pattern(body) {
|
||||||
|
// Build skeleton for skip_whitespace pattern
|
||||||
// Check for minimal pattern: single if-else with break
|
|
||||||
if body.len() != 1 {
|
|
||||||
return Ok((
|
|
||||||
LoopSkeleton::new(span),
|
|
||||||
RoutingDecision::fail_fast(
|
|
||||||
vec![capability_tags::CAP_MISSING_SINGLE_BREAK],
|
|
||||||
format!("Phase 2: Only single-statement loops supported (got {} statements)", body.len()),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's an if-else statement
|
|
||||||
let _if_stmt = match &body[0] {
|
|
||||||
ASTNode::If {
|
|
||||||
condition: _if_cond,
|
|
||||||
then_body: _then_body,
|
|
||||||
else_body,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// Must have else branch
|
|
||||||
if else_body.is_none() {
|
|
||||||
return Ok((
|
|
||||||
LoopSkeleton::new(span),
|
|
||||||
RoutingDecision::fail_fast(
|
|
||||||
vec![capability_tags::CAP_MISSING_SINGLE_BREAK],
|
|
||||||
"Phase 2: If statement must have else branch".to_string(),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// Phase 2: Just validate structure, don't extract components yet
|
|
||||||
()
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Ok((
|
|
||||||
LoopSkeleton::new(span),
|
|
||||||
RoutingDecision::fail_fast(
|
|
||||||
vec![capability_tags::CAP_MISSING_SINGLE_BREAK],
|
|
||||||
"Phase 2: Loop body must be single if-else statement".to_string(),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build minimal skeleton
|
|
||||||
let mut skeleton = LoopSkeleton::new(span);
|
let mut skeleton = LoopSkeleton::new(span);
|
||||||
|
|
||||||
// Add header condition
|
// Step 1: Header condition
|
||||||
skeleton.steps.push(SkeletonStep::HeaderCond {
|
skeleton.steps.push(SkeletonStep::HeaderCond {
|
||||||
expr: Box::new(condition.clone()),
|
expr: Box::new(condition.clone()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// For now, just mark as unsupported - full pattern detection will come in Phase 3
|
// Step 2: Body statements (if any)
|
||||||
|
if !body_stmts.is_empty() {
|
||||||
|
skeleton.steps.push(SkeletonStep::Body {
|
||||||
|
stmts: body_stmts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Update step
|
||||||
|
skeleton.steps.push(SkeletonStep::Update {
|
||||||
|
carrier_name: carrier_name.clone(),
|
||||||
|
update_kind: UpdateKind::ConstStep { delta },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add carrier slot
|
||||||
|
skeleton.carriers.push(CarrierSlot {
|
||||||
|
name: carrier_name,
|
||||||
|
role: CarrierRole::Counter,
|
||||||
|
update_kind: UpdateKind::ConstStep { delta },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set exit contract
|
||||||
|
skeleton.exits = ExitContract {
|
||||||
|
has_break: true,
|
||||||
|
has_continue: false,
|
||||||
|
has_return: false,
|
||||||
|
break_has_value: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Success! Return Pattern3WithIfPhi
|
||||||
|
let decision = RoutingDecision::success(LoopPatternKind::Pattern3IfPhi);
|
||||||
|
return Ok((skeleton, decision));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern not recognized - fail fast
|
||||||
Ok((
|
Ok((
|
||||||
skeleton,
|
LoopSkeleton::new(span),
|
||||||
RoutingDecision::fail_fast(
|
RoutingDecision::fail_fast(
|
||||||
vec![capability_tags::CAP_MISSING_CONST_STEP],
|
vec![capability_tags::CAP_MISSING_CONST_STEP],
|
||||||
"Phase 2: Pattern detection not yet implemented".to_string(),
|
"Phase 3: Loop does not match skip_whitespace pattern".to_string(),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@ -534,22 +625,60 @@ mod tests {
|
|||||||
assert!(result.unwrap_err().contains("Expected Loop node"));
|
assert!(result.unwrap_err().contains("Expected Loop node"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
// ============================================================================
|
||||||
fn test_canonicalize_minimal_loop_structure() {
|
// Phase 3: skip_whitespace Pattern Tests
|
||||||
use crate::ast::LiteralValue;
|
// ============================================================================
|
||||||
|
|
||||||
// Build minimal loop: loop(true) { if true { } else { break } }
|
#[test]
|
||||||
|
fn test_skip_whitespace_pattern_recognition() {
|
||||||
|
use crate::ast::{BinaryOperator, LiteralValue};
|
||||||
|
|
||||||
|
// Build skip_whitespace pattern: loop(p < len) { if is_ws == 1 { p = p + 1 } else { break } }
|
||||||
let loop_node = ASTNode::Loop {
|
let loop_node = ASTNode::Loop {
|
||||||
condition: Box::new(ASTNode::Literal {
|
condition: Box::new(ASTNode::BinaryOp {
|
||||||
value: LiteralValue::Bool(true),
|
operator: BinaryOperator::Less,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Variable {
|
||||||
|
name: "len".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}),
|
}),
|
||||||
body: vec![ASTNode::If {
|
body: vec![ASTNode::If {
|
||||||
condition: Box::new(ASTNode::Literal {
|
condition: Box::new(ASTNode::BinaryOp {
|
||||||
value: LiteralValue::Bool(true),
|
operator: BinaryOperator::Equal,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "is_ws".to_string(),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}),
|
}),
|
||||||
then_body: vec![],
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
then_body: vec![ASTNode::Assignment {
|
||||||
|
target: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
value: Box::new(ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Add,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
else_body: Some(vec![ASTNode::Break {
|
else_body: Some(vec![ASTNode::Break {
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}]),
|
}]),
|
||||||
@ -562,37 +691,106 @@ mod tests {
|
|||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let (skeleton, decision) = result.unwrap();
|
let (skeleton, decision) = result.unwrap();
|
||||||
// Should have header condition step
|
|
||||||
assert_eq!(skeleton.steps.len(), 1);
|
// Verify success
|
||||||
|
assert!(decision.is_success());
|
||||||
|
assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern3IfPhi));
|
||||||
|
assert_eq!(decision.missing_caps.len(), 0);
|
||||||
|
|
||||||
|
// Verify skeleton structure
|
||||||
|
assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
skeleton.steps[0],
|
skeleton.steps[0],
|
||||||
SkeletonStep::HeaderCond { .. }
|
SkeletonStep::HeaderCond { .. }
|
||||||
));
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
skeleton.steps[1],
|
||||||
|
SkeletonStep::Update { .. }
|
||||||
|
));
|
||||||
|
|
||||||
// Phase 2: Should fail-fast (pattern detection not implemented)
|
// Verify carrier
|
||||||
assert!(decision.is_fail_fast());
|
assert_eq!(skeleton.carriers.len(), 1);
|
||||||
assert!(decision.notes[0].contains("Pattern detection not yet implemented"));
|
assert_eq!(skeleton.carriers[0].name, "p");
|
||||||
|
assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
|
||||||
|
match &skeleton.carriers[0].update_kind {
|
||||||
|
UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
|
||||||
|
_ => panic!("Expected ConstStep update"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify exit contract
|
||||||
|
assert!(skeleton.exits.has_break);
|
||||||
|
assert!(!skeleton.exits.has_continue);
|
||||||
|
assert!(!skeleton.exits.has_return);
|
||||||
|
assert!(!skeleton.exits.break_has_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_canonicalize_rejects_multi_statement_loop() {
|
fn test_skip_whitespace_with_body_statements() {
|
||||||
use crate::ast::LiteralValue;
|
use crate::ast::{BinaryOperator, LiteralValue};
|
||||||
|
|
||||||
// Build loop with 2 statements
|
// Build pattern with body statements before the if:
|
||||||
|
// loop(p < len) {
|
||||||
|
// local ch = get_char(p)
|
||||||
|
// if is_ws { p = p + 1 } else { break }
|
||||||
|
// }
|
||||||
let loop_node = ASTNode::Loop {
|
let loop_node = ASTNode::Loop {
|
||||||
condition: Box::new(ASTNode::Literal {
|
condition: Box::new(ASTNode::BinaryOp {
|
||||||
value: LiteralValue::Bool(true),
|
operator: BinaryOperator::Less,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Variable {
|
||||||
|
name: "len".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}),
|
}),
|
||||||
body: vec![
|
body: vec![
|
||||||
ASTNode::Print {
|
// Body statement
|
||||||
expression: Box::new(ASTNode::Literal {
|
ASTNode::Assignment {
|
||||||
value: LiteralValue::String("test".to_string()),
|
target: Box::new(ASTNode::Variable {
|
||||||
|
name: "ch".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
value: Box::new(ASTNode::FunctionCall {
|
||||||
|
name: "get_char".to_string(),
|
||||||
|
arguments: vec![ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}),
|
}),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
},
|
},
|
||||||
ASTNode::Break {
|
// The if-else pattern
|
||||||
|
ASTNode::If {
|
||||||
|
condition: Box::new(ASTNode::Variable {
|
||||||
|
name: "is_ws".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
then_body: vec![ASTNode::Assignment {
|
||||||
|
target: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
value: Box::new(ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Add,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
|
else_body: Some(vec![ASTNode::Break {
|
||||||
|
span: Span::unknown(),
|
||||||
|
}]),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -602,16 +800,36 @@ mod tests {
|
|||||||
let result = canonicalize_loop_expr(&loop_node);
|
let result = canonicalize_loop_expr(&loop_node);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
let (_, decision) = result.unwrap();
|
let (skeleton, decision) = result.unwrap();
|
||||||
assert!(decision.is_fail_fast());
|
|
||||||
assert!(decision.notes[0].contains("Only single-statement loops supported"));
|
// Verify success
|
||||||
|
assert!(decision.is_success());
|
||||||
|
assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern3IfPhi));
|
||||||
|
|
||||||
|
// Verify skeleton has Body step
|
||||||
|
assert_eq!(skeleton.steps.len(), 3); // HeaderCond + Body + Update
|
||||||
|
assert!(matches!(
|
||||||
|
skeleton.steps[0],
|
||||||
|
SkeletonStep::HeaderCond { .. }
|
||||||
|
));
|
||||||
|
assert!(matches!(skeleton.steps[1], SkeletonStep::Body { .. }));
|
||||||
|
assert!(matches!(
|
||||||
|
skeleton.steps[2],
|
||||||
|
SkeletonStep::Update { .. }
|
||||||
|
));
|
||||||
|
|
||||||
|
// Verify body contains 1 statement
|
||||||
|
match &skeleton.steps[1] {
|
||||||
|
SkeletonStep::Body { stmts } => assert_eq!(stmts.len(), 1),
|
||||||
|
_ => panic!("Expected Body step"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_canonicalize_rejects_if_without_else() {
|
fn test_skip_whitespace_fails_without_else() {
|
||||||
use crate::ast::LiteralValue;
|
use crate::ast::{BinaryOperator, LiteralValue};
|
||||||
|
|
||||||
// Build loop with if (no else)
|
// Build pattern without else branch (should fail)
|
||||||
let loop_node = ASTNode::Loop {
|
let loop_node = ASTNode::Loop {
|
||||||
condition: Box::new(ASTNode::Literal {
|
condition: Box::new(ASTNode::Literal {
|
||||||
value: LiteralValue::Bool(true),
|
value: LiteralValue::Bool(true),
|
||||||
@ -622,7 +840,25 @@ mod tests {
|
|||||||
value: LiteralValue::Bool(true),
|
value: LiteralValue::Bool(true),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}),
|
}),
|
||||||
then_body: vec![],
|
then_body: vec![ASTNode::Assignment {
|
||||||
|
target: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
value: Box::new(ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Add,
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
else_body: None, // No else branch
|
else_body: None, // No else branch
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
}],
|
}],
|
||||||
@ -634,6 +870,58 @@ mod tests {
|
|||||||
|
|
||||||
let (_, decision) = result.unwrap();
|
let (_, decision) = result.unwrap();
|
||||||
assert!(decision.is_fail_fast());
|
assert!(decision.is_fail_fast());
|
||||||
assert!(decision.notes[0].contains("must have else branch"));
|
assert!(decision
|
||||||
|
.notes[0]
|
||||||
|
.contains("does not match skip_whitespace pattern"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_skip_whitespace_fails_with_wrong_delta() {
|
||||||
|
use crate::ast::{BinaryOperator, LiteralValue};
|
||||||
|
|
||||||
|
// Build pattern with wrong update (p = p - 1 instead of p = p + 1)
|
||||||
|
let loop_node = ASTNode::Loop {
|
||||||
|
condition: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Bool(true),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
body: vec![ASTNode::If {
|
||||||
|
condition: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Bool(true),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
then_body: vec![ASTNode::Assignment {
|
||||||
|
target: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
value: Box::new(ASTNode::BinaryOp {
|
||||||
|
operator: BinaryOperator::Subtract, // Wrong operator
|
||||||
|
left: Box::new(ASTNode::Variable {
|
||||||
|
name: "p".to_string(),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
right: Box::new(ASTNode::Literal {
|
||||||
|
value: LiteralValue::Integer(1),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
|
else_body: Some(vec![ASTNode::Break {
|
||||||
|
span: Span::unknown(),
|
||||||
|
}]),
|
||||||
|
span: Span::unknown(),
|
||||||
|
}],
|
||||||
|
span: Span::unknown(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = canonicalize_loop_expr(&loop_node);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
|
||||||
|
let (_, decision) = result.unwrap();
|
||||||
|
assert!(decision.is_fail_fast());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user