diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md index 07bc71b0..83901178 100644 --- a/docs/development/current/main/10-Now.md +++ b/docs/development/current/main/10-Now.md @@ -2,7 +2,12 @@ ## Current Focus: Phase 29ai(Plan/Frag single-planner) -Next: `docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md` +Next: `docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md` + +**2025-12-29: Phase 29ai P10 完了** ✅ +- 目的: Pattern2 extractor を plan 層へ移設して依存方向を固定(仕様不変) +- 実装: `src/mir/builder/control_flow/plan/extractors/pattern2_break.rs` / `src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs` / `src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs` +- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` PASS **2025-12-29: Phase 29ai P9 完了** ✅ - 目的: Pattern7 split-scan subset を Facts→Planner→DomainPlan まで到達させ、single_planner の Pattern7 で planner-first を開始(仕様不変) diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md index e8de7f05..c2f3dfac 100644 --- a/docs/development/current/main/30-Backlog.md +++ b/docs/development/current/main/30-Backlog.md @@ -19,7 +19,7 @@ Related: - **Phase 29ai(candidate): Plan/Frag single-planner(Facts SSOT)** - 入口: `docs/development/current/main/phases/phase-29ai/README.md` - - Next: P10(Move Pattern2 extractor → plan layer) + - Next: P11(Planner: Pattern2 break subset) - **Phase 29ae P1(✅ COMPLETE): JoinIR Regression Pack (SSOT固定)** - 入口: `docs/development/current/main/phases/phase-29ae/README.md` diff --git a/docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md new file mode 100644 index 00000000..74c2b214 --- /dev/null +++ b/docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md @@ -0,0 +1,114 @@ +# Phase 29ai P11: Planner support + wiring(Pattern2 break subset) + +Date: 2025-12-29 +Status: Ready for execution +Scope: Pattern2(break)を Facts→Planner の subset として吸収し、single_planner で planner-first を開始する(仕様不変) +Goal: 「入口は一本(Facts→Planner→DomainPlan)」を維持しつつ、Pattern2 を段階的に plan 系へ寄せる + +## Objective + +Pattern2 は現在でも `DomainPlan::Pattern2Break` を持っているが、抽出の多くは legacy JoinIR lowering に残っている。 +P11 では “PoC subset” の Pattern2(break-only, 単純形)だけを Facts→Planner で `Ok(Some(DomainPlan::Pattern2Break))` まで到達させ、 +`single_planner` の Pattern2 rule でも planner-first を開始する。 + +既存の Pattern2(LoopBodyLocal promotion / NotApplicable / Freeze 等)の挙動は変えず、planner が `Ok(None)` を返す限り従来経路に落とす。 + +## Non-goals(この P11 ではやらない) + +- Pattern2 の全網羅(LoopBodyLocal promotion / derived slot / real-world seg 等) +- NotApplicable/Freeze の契約変更 +- 既存 fixture の期待値(RC/ログ/エラー文字列)変更 +- 新しい env var / debug トグル追加 +- by-name ルールや特定 Box 名での分岐追加(禁止) + +## Target Subset(PoC) + +`plan/extractors/pattern2_break.rs` が扱っている最小形(例): + +```hako +loop(i < N) { + if (break_cond) { [carrier = expr;] break } + carrier = carrier + expr + i = i + 1 +} +``` + +この形だけを Facts→Planner に吸収し、それ以外は `Ok(None)` で従来経路にフォールバックする。 + +## Implementation Steps(Critical Order) + +### Step 1: Pattern2BreakFacts を追加(Facts層) + +ファイル(推奨): +- `src/mir/builder/control_flow/plan/facts/pattern2_break_facts.rs` ✨ NEW +- `src/mir/builder/control_flow/plan/facts/mod.rs` +- `src/mir/builder/control_flow/plan/facts/loop_facts.rs`(接続) + +方針: +- Facts は “構造的事実” だけを保持し、推測はしない(抽出できない場合は `Ok(None)`)。 +- `DomainPlan` を直接作らない(planner の責務)。 + +最低限必要な情報(例): +- `loop_var`, `carrier_var` +- `break_condition`(AST clone) +- `carrier_update_in_break`(Option) +- `carrier_update_in_body`(AST clone) +- `loop_increment`(AST clone) + +### Step 2: planner で DomainPlan::Pattern2Break を生成 + +ファイル: +- `src/mir/builder/control_flow/plan/planner/build.rs` + +やること: +- `facts.facts.pattern2_break` が `Some` のとき、CandidateSet に `DomainPlan::Pattern2Break(...)` を push。 +- 0/1/2+ の境界は CandidateSet の finalize に委譲(candidate-set 方式を崩さない)。 + +### Step 3: single_planner で Pattern2 を planner-first に + +ファイル: +- `src/mir/builder/control_flow/plan/single_planner/rules.rs` + +やること: +- Pattern6/7 と同様に、Pattern2 rule でも先頭で `planner::build_plan(ctx.condition, ctx.body)` を試す。 +- `Ok(Some(DomainPlan::Pattern2Break(_)))` のときだけ採用。 +- `Ok(Some(other))` は採用せず(将来拡張用に一旦 skip)、`Ok(None)` は従来どおり legacy(plan/extractor 経由)へ。 +- planner の `Ok(None)` で新規ログを出さない(観測差分を抑える)。 + +### Step 4: SSOT fixture/smoke を追加(PoC subset 固定) + +追加(推奨): +- `apps/tests/phase29ai_pattern2_break_plan_subset_ok_min.hako` +- `tools/smokes/v2/profiles/integration/apps/phase29ai_pattern2_break_plan_subset_ok_min_vm.sh` + +方針: +- 既存 Pattern2 の複雑系 fixture は変えない(回帰パック維持)。 +- P11 は “subset が planner で採用される” ことだけを固定する。 + +### Step 5: Docs / Tracking 更新 + +更新: +- `docs/development/current/main/phases/phase-29ai/README.md` +- `docs/development/current/main/10-Now.md` +- `docs/development/current/main/30-Backlog.md` + +最低限: +- P11 完了の記録(目的/影響/検証コマンド)。 +- Next(P12)候補を 1 行(例: Pattern2 LoopBodyLocal promotion の Facts 仕様化)。 + +## Verification Checklist(Acceptance) + +- `cargo build --release` +- `./tools/smokes/v2/run.sh --profile quick` +- `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` +- `./tools/smokes/v2/run.sh --profile integration --filter "phase29ai_pattern2_break_plan_subset_ok_min"` + +期待: +- quick/regression は既定挙動不変で PASS +- subset fixture は PASS(planner-first で採用される) + +## Risk Notes + +- Pattern2 は複雑なので、P11 は “subset のみ” を厳密にする(疑わしければ `Ok(None)`)。 +- 既存の Pattern2 実運用(LoopBodyLocal など)に影響を与えないことが最優先。 + diff --git a/docs/development/current/main/phases/phase-29ai/README.md b/docs/development/current/main/phases/phase-29ai/README.md index 1ff944dd..82bfc510 100644 --- a/docs/development/current/main/phases/phase-29ai/README.md +++ b/docs/development/current/main/phases/phase-29ai/README.md @@ -60,6 +60,13 @@ Goal: pattern 名による分岐を外部APIから消し、Facts(事実)→ - 指示書: `docs/development/current/main/phases/phase-29ai/P10-MOVE-PATTERN2-EXTRACTOR-TO-PLAN-LAYER-INSTRUCTIONS.md` - ねらい: Pattern2 の抽出(pattern固有知識)を plan 側へ寄せて依存方向を一方向に固定(仕様不変) +- 完了: plan/extractors へ移設、JoinIR 側は wrapper 化、legacy_rules を plan 側へ統一 +- 検証: `cargo build --release` / `./tools/smokes/v2/run.sh --profile quick` / `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh` + +## P11: Planner support + wiring(Pattern2 break subset) + +- 指示書: `docs/development/current/main/phases/phase-29ai/P11-PLANNER-PATTERN2-BREAK-SUBSET-WIRE-INSTRUCTIONS.md` +- ねらい: Pattern2(break)の PoC subset を Facts→Planner に吸収し、single_planner で planner-first を開始(仕様不変で段階吸収) ## Verification (SSOT) diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs index a4215da0..6708fd44 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/common_helpers.rs @@ -1,613 +1,10 @@ -//! Common Extraction Helpers for Pattern1-5 +//! Phase 29ai P10: Wrapper for plan-layer common_helpers (SSOT) //! -//! Phase 282 P9a: Extracted from Pattern1-5 extractors to eliminate common duplication. +//! Legacy path preserved: +//! `crate::mir::builder::control_flow::joinir::patterns::extractors::common_helpers::*` //! -//! # Design Principles -//! -//! - **Pure Functions**: No side effects, no builder mutations -//! - **Fail-Fast**: Err for logic bugs, Ok(None) for non-matches -//! - **Configurability**: ControlFlowDetector for pattern-specific behavior -//! - **Scope-Limited**: Common detection only, pattern-specific logic excluded -//! -//! # Groups (P9a Scope-Limited) -//! -//! 1. Control Flow Counting (count_control_flow) - Universal counter -//! 2. Control Flow Detection (has_break_statement, has_continue_statement, etc.) - Common detection -//! 3. Condition Validation (extract_loop_variable, is_true_literal) - Condition helpers -//! 4. Pattern5-Specific Helpers (validate_continue_at_end, validate_break_in_simple_if) - NOT generalized -//! -//! **IMPORTANT**: Pattern-specific interpretation logic (e.g., Pattern3's nested_if) is EXCLUDED. -//! Such logic remains in individual pattern files to maintain clear SSOT boundaries. +//! SSOT implementation lives in: +//! `crate::mir::builder::control_flow::plan::extractors::common_helpers::*` -use crate::ast::ASTNode; +pub(crate) use crate::mir::builder::control_flow::plan::extractors::common_helpers::*; -/// ============================================================ -/// Group 1: Control Flow Counting (汎用カウンター) -/// ============================================================ - -#[derive(Debug, Clone, Default)] -pub(crate) struct ControlFlowCounts { - pub break_count: usize, - pub continue_count: usize, - pub return_count: usize, - pub has_nested_loop: bool, -} - -/// Control flow detection options -/// -/// # P9a Scope-Limited -/// - `detect_nested_if` removed (Pattern3-specific, deferred to P9b) -/// - Only common detection options included -#[derive(Debug, Clone)] -pub(crate) struct ControlFlowDetector { - /// Skip break/continue inside nested loops? - pub skip_nested_control_flow: bool, - /// Count return statements? - pub count_returns: bool, -} - -impl Default for ControlFlowDetector { - fn default() -> Self { - Self { - skip_nested_control_flow: true, - count_returns: false, - } - } -} - -/// Universal control flow counter -/// -/// # Examples (P9a Scope-Limited) -/// - Pattern1: default (skip_nested=true, count_returns=false) -/// - Pattern2: default (skip_nested=true, count_returns=false) -/// - Pattern4: default (skip_nested=true, count_returns=false) -/// - Pattern5: count_returns=true (returns Err if return found) -pub(crate) fn count_control_flow( - body: &[ASTNode], - detector: ControlFlowDetector, -) -> ControlFlowCounts { - let mut counts = ControlFlowCounts::default(); - - fn scan_node( - node: &ASTNode, - counts: &mut ControlFlowCounts, - detector: &ControlFlowDetector, - depth: usize, - ) { - match node { - ASTNode::Break { .. } => { - counts.break_count += 1; - } - ASTNode::Continue { .. } => { - counts.continue_count += 1; - } - ASTNode::Return { .. } if detector.count_returns => { - counts.return_count += 1; - } - ASTNode::Loop { .. } if depth > 0 => { - counts.has_nested_loop = true; - // Skip nested loop bodies if configured - if detector.skip_nested_control_flow { - return; - } - } - ASTNode::If { - then_body, - else_body, - .. - } => { - // Recurse into if/else bodies - for stmt in then_body { - scan_node(stmt, counts, detector, depth + 1); - } - if let Some(else_b) = else_body { - for stmt in else_b { - scan_node(stmt, counts, detector, depth + 1); - } - } - } - _ => {} - } - } - - for stmt in body { - scan_node(stmt, &mut counts, &detector, 0); - } - - counts -} - -/// ============================================================ -/// Group 2: Control Flow Detection (真偽値判定) -/// ============================================================ - -/// Check if body has ANY break statement -pub(crate) fn has_break_statement(body: &[ASTNode]) -> bool { - count_control_flow(body, ControlFlowDetector::default()).break_count > 0 -} - -/// Check if body has ANY continue statement -pub(crate) fn has_continue_statement(body: &[ASTNode]) -> bool { - count_control_flow(body, ControlFlowDetector::default()).continue_count > 0 -} - -/// Check if body has ANY return statement -pub(crate) fn has_return_statement(body: &[ASTNode]) -> bool { - let mut detector = ControlFlowDetector::default(); - detector.count_returns = true; - count_control_flow(body, detector).return_count > 0 -} - -/// Check if body has ANY break or continue -pub(crate) fn has_control_flow_statement(body: &[ASTNode]) -> bool { - let counts = count_control_flow(body, ControlFlowDetector::default()); - counts.break_count > 0 || counts.continue_count > 0 -} - -/// Phase 286 P2.6: Check if body has ANY if statement (recursive) -/// -/// This is a supplementary helper for Pattern1 extraction to prevent -/// Pattern1 from incorrectly matching Pattern3 fixtures (if-else-phi). -/// -/// # Returns -/// - true if any if statement found in body (recursively) -/// - false otherwise -pub(crate) fn has_if_statement(body: &[ASTNode]) -> bool { - for node in body { - match node { - ASTNode::If { .. } => return true, - ASTNode::ScopeBox { body, .. } => { - if has_if_statement(body) { - return true; - } - } - ASTNode::Loop { body, .. } => { - if has_if_statement(body) { - return true; - } - } - _ => {} - } - } - false -} - -/// Phase 286 P2.6: Check if body has ANY if-else statement (recursive) -/// -/// This is more specific than has_if_statement - it only detects if statements -/// with else branches, which are Pattern3 territory (if-phi merge). -/// -/// # Returns -/// - true if any if-else statement found in body (recursively) -/// - false otherwise -pub(crate) fn has_if_else_statement(body: &[ASTNode]) -> bool { - for node in body { - match node { - ASTNode::If { else_body: Some(_), .. } => return true, - ASTNode::ScopeBox { body, .. } => { - if has_if_else_statement(body) { - return true; - } - } - ASTNode::Loop { body, .. } => { - if has_if_else_statement(body) { - return true; - } - } - _ => {} - } - } - false -} - -/// Phase 286: Find first if-else statement in loop body (non-recursive) -/// -/// Returns first if statement with else branch in the given body. -/// This is a PoC subset helper - only finds the FIRST if-else (Pattern3 constraint). -/// -/// # Returns -/// - Some(&ASTNode) if an if-else statement found -/// - None if no if-else statement found -/// -/// # Note -/// This is intentionally non-recursive (top-level only) for Pattern3 extraction. -/// Pattern3 allows multiple if statements - this just finds the first if-else. -pub(crate) fn find_if_else_statement(body: &[ASTNode]) -> Option<&ASTNode> { - for stmt in body { - if matches!(stmt, ASTNode::If { else_body: Some(_), .. }) { - return Some(stmt); // Found first if-else - } - } - None // No if-else found -} - -/// ============================================================ -/// Group 3: Condition Validation (比較演算検証) -/// ============================================================ - -use crate::ast::BinaryOperator; - -/// Validate condition: 比較演算 (左辺が変数) -/// -/// # Returns -/// - Some("var_name") if valid comparison with variable on left -/// - None otherwise -pub(crate) fn extract_loop_variable(condition: &ASTNode) -> Option { - match condition { - ASTNode::BinaryOp { operator, left, .. } => { - if !matches!( - operator, - BinaryOperator::Less - | BinaryOperator::LessEqual - | BinaryOperator::Greater - | BinaryOperator::GreaterEqual - | BinaryOperator::Equal - | BinaryOperator::NotEqual - ) { - return None; - } - - if let ASTNode::Variable { name, .. } = left.as_ref() { - return Some(name.clone()); - } - - None - } - _ => None, - } -} - -/// Check if condition is true literal -pub(crate) fn is_true_literal(condition: &ASTNode) -> bool { - use crate::ast::LiteralValue; - - matches!( - condition, - ASTNode::Literal { - value: LiteralValue::Bool(true), - .. - } - ) -} - -/// ============================================================ -/// Group 4: Loop Increment Extraction (Common for Plan line) -/// ============================================================ - -/// Phase 286 P2.2: Extract loop increment for Plan line patterns -/// -/// Supports ` = + ` pattern only (PoC safety). -/// Used by Pattern1 and Pattern4 Plan line extractors. -/// -/// # Returns -/// - Ok(Some(ASTNode)) if valid increment found -/// - Ok(None) if no increment or unsupported pattern -/// - Err if malformed AST (rare) -pub(crate) fn extract_loop_increment_plan(body: &[ASTNode], loop_var: &str) -> Result, String> { - for stmt in body { - if let ASTNode::Assignment { target, value, .. } = stmt { - if let ASTNode::Variable { name, .. } = target.as_ref() { - if name == loop_var { - // Check for `i = i + ` - if let ASTNode::BinaryOp { operator, left, right, .. } = value.as_ref() { - if matches!(operator, BinaryOperator::Add) { - if let ASTNode::Variable { name: lname, .. } = left.as_ref() { - if lname == loop_var { - if matches!(right.as_ref(), ASTNode::Literal { .. }) { - return Ok(Some(value.as_ref().clone())); - } - } - } - } - } - } - } - } - } - Ok(None) -} - -/// ============================================================ -/// Group 5: Pattern5-Specific Helpers (NOT generalized) -/// ============================================================ -/// -/// **IMPORTANT**: These helpers are Pattern5-specific and intentionally NOT generalized. -/// Other patterns with similar needs should implement their own validation logic -/// to maintain clear SSOT boundaries. - -/// Validate continue is at body end (Pattern5 specific) -/// -/// # Pattern5 Shape Requirement -/// - Continue statement MUST be the last statement in loop body (top-level) -/// - This is Pattern5's specific shape constraint, not a general pattern -pub(crate) fn validate_continue_at_end(body: &[ASTNode]) -> bool { - matches!(body.last(), Some(ASTNode::Continue { .. })) -} - -/// Validate break is in simple if pattern (Pattern5 specific) -/// -/// # Pattern5 Shape Requirement -/// - Break MUST be in simple if: `if (...) { break }` (no else branch) -/// - This is Pattern5's specific shape constraint, not a general pattern -pub(crate) fn validate_break_in_simple_if(body: &[ASTNode]) -> bool { - for stmt in body { - if let ASTNode::If { - then_body, - else_body, - .. - } = stmt - { - if then_body.len() == 1 - && matches!(then_body[0], ASTNode::Break { .. }) - && else_body.is_none() - { - return true; - } - } - } - false -} - -// ============================================================================ -// Unit Tests -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; - - // Test fixtures - fn make_break() -> ASTNode { - ASTNode::Break { - span: Span::unknown(), - } - } - - fn make_continue() -> ASTNode { - ASTNode::Continue { - span: Span::unknown(), - } - } - - fn make_return() -> ASTNode { - ASTNode::Return { - value: None, - span: Span::unknown(), - } - } - - fn make_if_with_break() -> ASTNode { - ASTNode::If { - condition: Box::new(ASTNode::Variable { - name: "done".to_string(), - span: Span::unknown(), - }), - then_body: vec![make_break()], - else_body: None, - span: Span::unknown(), - } - } - - #[test] - fn test_count_control_flow_break_only() { - let body = vec![make_if_with_break()]; - let counts = count_control_flow(&body, ControlFlowDetector::default()); - - assert_eq!(counts.break_count, 1); - assert_eq!(counts.continue_count, 0); - assert_eq!(counts.return_count, 0); - assert!(!counts.has_nested_loop); - } - - #[test] - fn test_count_control_flow_continue_only() { - let body = vec![make_continue()]; - let counts = count_control_flow(&body, ControlFlowDetector::default()); - - assert_eq!(counts.break_count, 0); - assert_eq!(counts.continue_count, 1); - assert_eq!(counts.return_count, 0); - assert!(!counts.has_nested_loop); - } - - #[test] - fn test_count_control_flow_return_with_detector() { - let body = vec![make_return()]; - let mut detector = ControlFlowDetector::default(); - detector.count_returns = true; - let counts = count_control_flow(&body, detector); - - assert_eq!(counts.break_count, 0); - assert_eq!(counts.continue_count, 0); - assert_eq!(counts.return_count, 1); - assert!(!counts.has_nested_loop); - } - - #[test] - fn test_has_break_statement() { - let body = vec![make_if_with_break()]; - assert!(has_break_statement(&body)); - } - - #[test] - fn test_has_break_statement_false() { - let body = vec![make_continue()]; - assert!(!has_break_statement(&body)); - } - - #[test] - fn test_has_continue_statement() { - let body = vec![make_continue()]; - assert!(has_continue_statement(&body)); - } - - #[test] - fn test_has_continue_statement_false() { - let body = vec![make_break()]; - assert!(!has_continue_statement(&body)); - } - - #[test] - fn test_has_return_statement() { - let body = vec![make_return()]; - assert!(has_return_statement(&body)); - } - - #[test] - fn test_has_return_statement_false() { - let body = vec![make_break()]; - assert!(!has_return_statement(&body)); - } - - #[test] - fn test_has_control_flow_statement_break() { - let body = vec![make_if_with_break()]; - assert!(has_control_flow_statement(&body)); - } - - #[test] - fn test_has_control_flow_statement_continue() { - let body = vec![make_continue()]; - assert!(has_control_flow_statement(&body)); - } - - #[test] - fn test_has_control_flow_statement_false() { - let body = vec![ASTNode::Variable { - name: "x".to_string(), - span: Span::unknown(), - }]; - assert!(!has_control_flow_statement(&body)); - } - - #[test] - fn test_extract_loop_variable_success() { - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(10), - span: Span::unknown(), - }), - span: Span::unknown(), - }; - - assert_eq!(extract_loop_variable(&condition), Some("i".to_string())); - } - - #[test] - fn test_extract_loop_variable_not_comparison() { - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(10), - span: Span::unknown(), - }), - span: Span::unknown(), - }; - - assert_eq!(extract_loop_variable(&condition), None); - } - - #[test] - fn test_extract_loop_variable_not_variable() { - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(5), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(10), - span: Span::unknown(), - }), - span: Span::unknown(), - }; - - assert_eq!(extract_loop_variable(&condition), None); - } - - #[test] - fn test_is_true_literal_success() { - let condition = ASTNode::Literal { - value: LiteralValue::Bool(true), - span: Span::unknown(), - }; - - assert!(is_true_literal(&condition)); - } - - #[test] - fn test_is_true_literal_false() { - let condition = ASTNode::Literal { - value: LiteralValue::Bool(false), - span: Span::unknown(), - }; - - assert!(!is_true_literal(&condition)); - } - - #[test] - fn test_is_true_literal_not_literal() { - let condition = ASTNode::Variable { - name: "x".to_string(), - span: Span::unknown(), - }; - - assert!(!is_true_literal(&condition)); - } - - #[test] - fn test_validate_continue_at_end_success() { - let body = vec![make_break(), make_continue()]; - assert!(validate_continue_at_end(&body)); - } - - #[test] - fn test_validate_continue_at_end_false() { - let body = vec![make_continue(), make_break()]; - assert!(!validate_continue_at_end(&body)); - } - - #[test] - fn test_validate_break_in_simple_if_success() { - let body = vec![make_if_with_break()]; - assert!(validate_break_in_simple_if(&body)); - } - - #[test] - fn test_validate_break_in_simple_if_with_else() { - let body = vec![ASTNode::If { - condition: Box::new(ASTNode::Variable { - name: "done".to_string(), - span: Span::unknown(), - }), - then_body: vec![make_break()], - else_body: Some(vec![make_continue()]), - span: Span::unknown(), - }]; - assert!(!validate_break_in_simple_if(&body)); - } - - #[test] - fn test_validate_break_in_simple_if_multiple_statements() { - let body = vec![ASTNode::If { - condition: Box::new(ASTNode::Variable { - name: "done".to_string(), - span: Span::unknown(), - }), - then_body: vec![make_break(), make_continue()], - else_body: None, - span: Span::unknown(), - }]; - assert!(!validate_break_in_simple_if(&body)); - } -} diff --git a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs index 7f133843..6aa0d36c 100644 --- a/src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs +++ b/src/mir/builder/control_flow/joinir/patterns/extractors/pattern2.rs @@ -1,496 +1,23 @@ -//! Phase 282 P4: Pattern2 (Loop with Conditional Break) Extraction -//! Phase 282 P9a: Integrated with common_helpers -//! Phase 286 P3.1: Added extract_pattern2_plan() for Plan/Frag SSOT +//! Phase 29ai P10: Wrapper for plan-layer Pattern2 extractor (SSOT) -use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; +use crate::ast::ASTNode; -// Phase 282 P9a: Use common_helpers -use super::common_helpers::{ - count_control_flow, has_continue_statement as common_has_continue, - has_return_statement as common_has_return, ControlFlowDetector, -}; +pub(crate) use crate::mir::builder::control_flow::plan::extractors::pattern2_break::Pattern2Parts; -#[derive(Debug, Clone)] -pub(crate) struct Pattern2Parts { - pub loop_var: String, // Loop variable name (empty for loop(true)) - pub is_loop_true: bool, // true if loop(true), false otherwise - pub break_count: usize, // Number of break statements (validation) - // Note: AST reused from ctx - no duplication -} - -/// Extract Pattern2 (Loop with Conditional Break) parts -/// -/// # Detection Criteria (Lightweight - SSOT minimal) -/// -/// 1. **Condition**: 比較演算 (left=variable) OR loop(true) -/// 2. **Body**: MUST have at least 1 break statement -/// 3. **Body**: NO continue statements (Pattern4 territory) -/// 4. **Body**: NO return statements (Pattern2 = break-only) -/// -/// # Four-Phase Validation -/// -/// **Phase 1**: Validate condition (比較演算 or loop(true)) -/// **Phase 2**: Validate HAS break (required for Pattern2) -/// **Phase 3**: Validate NO continue/return (break-only) -/// **Phase 4**: Extract core info -/// -/// # Fail-Fast Rules -/// -/// - `Ok(Some(parts))`: Pattern2 match confirmed -/// - `Ok(None)`: Not Pattern2 (no break, has continue/return, or Pattern1) -/// - `Err(msg)`: Logic bug (malformed AST) -/// -/// # Note -/// -/// Carrier update validation is deferred to can_lower() via CommonPatternInitializer (SSOT). pub(crate) fn extract_loop_with_break_parts( condition: &ASTNode, body: &[ASTNode], ) -> Result, String> { - // Phase 1: Validate condition - let (loop_var, is_loop_true) = validate_condition_for_pattern2(condition); - - // Phase 2: Validate HAS break (required) - let break_count = count_break_statements(body); - if break_count == 0 { - return Ok(None); // No break → Pattern1 territory - } - - // Phase 3: Validate NO continue/return (break-only) - if has_continue_statement(body) { - return Ok(None); // Has continue → Pattern4 territory - } - if has_return_statement(body) { - return Ok(None); // Has return → Pattern2 is break-only - } - - // Phase 4: Return extracted info - Ok(Some(Pattern2Parts { - loop_var, - is_loop_true, - break_count, - })) + crate::mir::builder::control_flow::plan::extractors::pattern2_break::extract_loop_with_break_parts( + condition, body, + ) } -/// Validate condition: 比較演算 or loop(true) -fn validate_condition_for_pattern2(condition: &ASTNode) -> (String, bool) { - // Case 1: loop(true) - if matches!(condition, ASTNode::Literal { value: LiteralValue::Bool(true), .. }) { - return ("".to_string(), true); // loop_var determined in can_lower() - } - - // Case 2: 比較演算 (same as Pattern1) - match condition { - ASTNode::BinaryOp { operator, left, .. } => { - if matches!( - operator, - BinaryOperator::Less - | BinaryOperator::LessEqual - | BinaryOperator::Greater - | BinaryOperator::GreaterEqual - | BinaryOperator::Equal - | BinaryOperator::NotEqual - ) { - if let ASTNode::Variable { name, .. } = left.as_ref() { - return (name.clone(), false); - } - } - } - _ => {} - } - - // Unknown condition - let can_lower() decide - ("".to_string(), false) -} - -/// Count break statements recursively -/// -/// # Phase 282 P9a: Delegates to common_helpers::count_control_flow -fn count_break_statements(body: &[ASTNode]) -> usize { - count_control_flow(body, ControlFlowDetector::default()).break_count -} - -/// Check for continue statements -/// -/// # Phase 282 P9a: Delegates to common_helpers::has_continue_statement -fn has_continue_statement(body: &[ASTNode]) -> bool { - common_has_continue(body) -} - -/// Check for return statements (Pattern2 = break-only) -/// -/// # Phase 282 P9a: Delegates to common_helpers::has_return_statement -fn has_return_statement(body: &[ASTNode]) -> bool { - common_has_return(body) -} - -// ============================================================================ -// Phase 286 P3.1: Plan line extraction -// ============================================================================ - -/// Extract Pattern2 Plan for Plan/Frag SSOT migration -/// -/// # PoC Subset (Strict - returns Ok(None) for unsupported forms) -/// -/// Supported form: -/// ```hako -/// loop(i < N) { -/// if (break_cond) { [carrier = expr;] break } -/// carrier = carrier + expr -/// i = i + 1 -/// } -/// ``` -/// -/// Returns Ok(None) for: -/// - loop_increment not extractable (complex structure) -/// - break_cond not single if (nested, multiple conditions) -/// - break_then has multiple statements and carrier update not identifiable -/// - Multiple carriers (PoC is single carrier only) -/// - Body carrier update not identifiable -/// -/// This prevents Fail-Fast regression by falling back to legacy JoinIR line. pub(crate) fn extract_pattern2_plan( condition: &ASTNode, body: &[ASTNode], ) -> Result, String> { - use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern2BreakPlan}; - - // Step 1: Validate via existing extractor (has break, no continue/return) - let parts = extract_loop_with_break_parts(condition, body)?; - if parts.is_none() { - return Ok(None); // Not Pattern2 → legacy fallback - } - let parts = parts.unwrap(); - - // Step 2: PoC constraint - only support non-loop(true) for now - if parts.is_loop_true { - return Ok(None); // loop(true) is complex → legacy fallback - } - - // Step 3: PoC constraint - only support single break - if parts.break_count != 1 { - return Ok(None); // Multiple breaks → legacy fallback - } - - // Step 4: Body structure must be: if { ... break } + carrier_update + loop_increment - // Expected structure: - // body[0] = If { then_body contains break } - // body[1] = carrier update assignment - // body[2] = loop_var update assignment - if body.len() != 3 { - return Ok(None); // Unexpected body length → legacy fallback - } - - // Step 4.1: First element must be an If with break in then_body - let (break_condition, carrier_update_in_break) = match &body[0] { - ASTNode::If { condition: if_cond, then_body, else_body, .. } => { - // PoC: No else branch allowed - if else_body.is_some() { - return Ok(None); - } - - // Extract break condition - let break_cond = if_cond.as_ref().clone(); - - // Check if then_body ends with break - let has_break_at_end = then_body.last() - .map(|n| matches!(n, ASTNode::Break { .. })) - .unwrap_or(false); - if !has_break_at_end { - return Ok(None); - } - - // Extract carrier update in break (if any) - // If then_body is [break], no update - // If then_body is [assignment, break], extract assignment - let carrier_update = if then_body.len() == 1 { - None // Just break, no update - } else if then_body.len() == 2 { - // Should be [assignment, break] - match &then_body[0] { - ASTNode::Assignment { value, .. } => Some(value.as_ref().clone()), - _ => return Ok(None), // Not an assignment → legacy - } - } else { - return Ok(None); // Complex then_body → legacy - }; - - (break_cond, carrier_update) - } - _ => return Ok(None), // First element not If → legacy - }; - - // Step 4.2: Extract carrier update in body (second element) - let (carrier_var, carrier_update_in_body) = match &body[1] { - ASTNode::Assignment { target, value, .. } => { - let carrier_name = match target.as_ref() { - ASTNode::Variable { name, .. } => name.clone(), - _ => return Ok(None), - }; - (carrier_name, value.as_ref().clone()) - } - _ => return Ok(None), - }; - - // Step 4.3: Extract loop increment (third element) - let loop_increment = match super::common_helpers::extract_loop_increment_plan(body, &parts.loop_var)? { - Some(inc) => inc, - None => return Ok(None), // No loop increment found → legacy - }; - - // Step 5: Validate loop condition is ` < ` (same as Pattern1) - if !validate_loop_condition_for_plan(condition, &parts.loop_var) { - return Ok(None); - } - - Ok(Some(DomainPlan::Pattern2Break(Pattern2BreakPlan { - loop_var: parts.loop_var, - carrier_var, - loop_condition: condition.clone(), - break_condition, - carrier_update_in_break, - carrier_update_in_body, - loop_increment, - }))) -} - -/// Validate loop condition: supports ` < ` only (same as Pattern1) -fn validate_loop_condition_for_plan(cond: &ASTNode, loop_var: &str) -> bool { - if let ASTNode::BinaryOp { operator, left, right, .. } = cond { - if !matches!(operator, BinaryOperator::Less) { - return false; // Only < supported for PoC - } - - // Left must be the loop variable - if let ASTNode::Variable { name, .. } = left.as_ref() { - if name != loop_var { - return false; - } - } else { - return false; - } - - // Right must be integer literal - if !matches!(right.as_ref(), ASTNode::Literal { value: LiteralValue::Integer(_), .. }) { - return false; - } - - true - } else { - false - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ast::Span; - - #[test] - fn test_extract_with_break_success() { - // loop(i < 10) { if (i == 5) { break } i = i + 1 } - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(10), - span: Span::unknown(), - }), - span: Span::unknown(), - }; - - let body = vec![ - ASTNode::If { - condition: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Equal, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(5), - span: Span::unknown(), - }), - span: Span::unknown(), - }), - then_body: vec![ASTNode::Break { - span: Span::unknown(), - }], - else_body: None, - span: Span::unknown(), - }, - ASTNode::Assignment { - target: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(1), - span: Span::unknown(), - }), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ]; - - let result = extract_loop_with_break_parts(&condition, &body); - assert!(result.is_ok()); - let parts = result.unwrap(); - assert!(parts.is_some()); - let parts = parts.unwrap(); - assert_eq!(parts.loop_var, "i"); - assert_eq!(parts.is_loop_true, false); - assert_eq!(parts.break_count, 1); - } - - #[test] - fn test_extract_no_break_returns_none() { - // loop(i < 10) { i = i + 1 } - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(10), - span: Span::unknown(), - }), - span: Span::unknown(), - }; - - let body = vec![ASTNode::Assignment { - target: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(1), - span: Span::unknown(), - }), - span: Span::unknown(), - }), - span: Span::unknown(), - }]; - - let result = extract_loop_with_break_parts(&condition, &body); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); // No break → Pattern1 territory - } - - #[test] - fn test_extract_with_continue_returns_none() { - // loop(i < 10) { if (i == 5) { continue } i = i + 1 } - let condition = ASTNode::BinaryOp { - operator: BinaryOperator::Less, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(10), - span: Span::unknown(), - }), - span: Span::unknown(), - }; - - let body = vec![ASTNode::If { - condition: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Equal, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(5), - span: Span::unknown(), - }), - span: Span::unknown(), - }), - then_body: vec![ASTNode::Continue { - span: Span::unknown(), - }], - else_body: None, - span: Span::unknown(), - }]; - - let result = extract_loop_with_break_parts(&condition, &body); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); // Has continue → Pattern4 territory - } - - #[test] - fn test_extract_loop_true_with_break_success() { - // loop(true) { if (i == 5) { break } i = i + 1 } - let condition = ASTNode::Literal { - value: LiteralValue::Bool(true), - span: Span::unknown(), - }; - - let body = vec![ - ASTNode::If { - condition: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Equal, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(5), - span: Span::unknown(), - }), - span: Span::unknown(), - }), - then_body: vec![ASTNode::Break { - span: Span::unknown(), - }], - else_body: None, - span: Span::unknown(), - }, - ASTNode::Assignment { - target: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - value: Box::new(ASTNode::BinaryOp { - operator: BinaryOperator::Add, - left: Box::new(ASTNode::Variable { - name: "i".to_string(), - span: Span::unknown(), - }), - right: Box::new(ASTNode::Literal { - value: LiteralValue::Integer(1), - span: Span::unknown(), - }), - span: Span::unknown(), - }), - span: Span::unknown(), - }, - ]; - - let result = extract_loop_with_break_parts(&condition, &body); - assert!(result.is_ok()); - let parts = result.unwrap(); - assert!(parts.is_some()); - let parts = parts.unwrap(); - assert_eq!(parts.loop_var, ""); // Empty for loop(true) - assert_eq!(parts.is_loop_true, true); - assert_eq!(parts.break_count, 1); - } + crate::mir::builder::control_flow::plan::extractors::pattern2_break::extract_pattern2_plan( + condition, body, + ) } diff --git a/src/mir/builder/control_flow/plan/extractors/common_helpers.rs b/src/mir/builder/control_flow/plan/extractors/common_helpers.rs new file mode 100644 index 00000000..1a091084 --- /dev/null +++ b/src/mir/builder/control_flow/plan/extractors/common_helpers.rs @@ -0,0 +1,549 @@ +//! Common Extraction Helpers for Pattern1-5 +//! +//! Phase 282 P9a: Extracted from Pattern1-5 extractors to eliminate common duplication. +//! Phase 29ai P10: Moved to plan-layer SSOT (JoinIR keeps a wrapper path). +//! +//! # Design Principles +//! +//! - **Pure Functions**: No side effects, no builder mutations +//! - **Fail-Fast**: Err for logic bugs, Ok(None) for non-matches +//! - **Configurability**: ControlFlowDetector for pattern-specific behavior +//! - **Scope-Limited**: Common detection only, pattern-specific logic excluded +//! +//! # Groups (P9a Scope-Limited) +//! +//! 1. Control Flow Counting (count_control_flow) - Universal counter +//! 2. Control Flow Detection (has_break_statement, has_continue_statement, etc.) - Common detection +//! 3. Condition Validation (extract_loop_variable, is_true_literal) - Condition helpers +//! 4. Loop Increment Extraction (extract_loop_increment_plan) - Common plan helper +//! 5. Pattern5-Specific Helpers (validate_continue_at_end, validate_break_in_simple_if) - NOT generalized +//! +//! **IMPORTANT**: Pattern-specific interpretation logic (e.g., Pattern3's nested_if) is EXCLUDED. +//! Such logic remains in individual pattern files to maintain clear SSOT boundaries. + +#![allow(dead_code)] + +use crate::ast::ASTNode; + +/// ============================================================ +/// Group 1: Control Flow Counting (汎用カウンター) +/// ============================================================ + +#[derive(Debug, Clone, Default)] +pub(crate) struct ControlFlowCounts { + pub break_count: usize, + pub continue_count: usize, + pub return_count: usize, + pub has_nested_loop: bool, +} + +/// Control flow detection options +/// +/// # P9a Scope-Limited +/// - `detect_nested_if` removed (Pattern3-specific, deferred to P9b) +/// - Only common detection options included +#[derive(Debug, Clone)] +pub(crate) struct ControlFlowDetector { + /// Skip break/continue inside nested loops? + pub skip_nested_control_flow: bool, + /// Count return statements? + pub count_returns: bool, +} + +impl Default for ControlFlowDetector { + fn default() -> Self { + Self { + skip_nested_control_flow: true, + count_returns: false, + } + } +} + +/// Universal control flow counter +/// +/// # Examples (P9a Scope-Limited) +/// - Pattern1: default (skip_nested=true, count_returns=false) +/// - Pattern2: default (skip_nested=true, count_returns=false) +/// - Pattern4: default (skip_nested=true, count_returns=false) +/// - Pattern5: count_returns=true (returns Err if return found) +pub(crate) fn count_control_flow( + body: &[ASTNode], + detector: ControlFlowDetector, +) -> ControlFlowCounts { + let mut counts = ControlFlowCounts::default(); + + fn scan_node( + node: &ASTNode, + counts: &mut ControlFlowCounts, + detector: &ControlFlowDetector, + depth: usize, + ) { + match node { + ASTNode::Break { .. } => { + counts.break_count += 1; + } + ASTNode::Continue { .. } => { + counts.continue_count += 1; + } + ASTNode::Return { .. } if detector.count_returns => { + counts.return_count += 1; + } + ASTNode::Loop { .. } if depth > 0 => { + counts.has_nested_loop = true; + // Skip nested loop bodies if configured + if detector.skip_nested_control_flow { + return; + } + } + ASTNode::If { + then_body, + else_body, + .. + } => { + // Recurse into if/else bodies + for stmt in then_body { + scan_node(stmt, counts, detector, depth + 1); + } + if let Some(else_b) = else_body { + for stmt in else_b { + scan_node(stmt, counts, detector, depth + 1); + } + } + } + _ => {} + } + } + + for stmt in body { + scan_node(stmt, &mut counts, &detector, 0); + } + + counts +} + +/// ============================================================ +/// Group 2: Control Flow Detection (真偽値判定) +/// ============================================================ + +/// Check if body has ANY break statement +pub(crate) fn has_break_statement(body: &[ASTNode]) -> bool { + count_control_flow(body, ControlFlowDetector::default()).break_count > 0 +} + +/// Check if body has ANY continue statement +pub(crate) fn has_continue_statement(body: &[ASTNode]) -> bool { + count_control_flow(body, ControlFlowDetector::default()).continue_count > 0 +} + +/// Check if body has ANY return statement +pub(crate) fn has_return_statement(body: &[ASTNode]) -> bool { + let mut detector = ControlFlowDetector::default(); + detector.count_returns = true; + count_control_flow(body, detector).return_count > 0 +} + +/// Check if body has ANY break or continue +pub(crate) fn has_control_flow_statement(body: &[ASTNode]) -> bool { + let counts = count_control_flow(body, ControlFlowDetector::default()); + counts.break_count > 0 || counts.continue_count > 0 +} + +/// Phase 286 P2.6: Check if body has ANY if statement (recursive) +/// +/// This is a supplementary helper for Pattern1 extraction to prevent +/// Pattern1 from incorrectly matching Pattern3 fixtures (if-else-phi). +pub(crate) fn has_if_statement(body: &[ASTNode]) -> bool { + for node in body { + match node { + ASTNode::If { .. } => return true, + ASTNode::ScopeBox { body, .. } => { + if has_if_statement(body) { + return true; + } + } + ASTNode::Loop { body, .. } => { + if has_if_statement(body) { + return true; + } + } + _ => {} + } + } + false +} + +/// Phase 286 P2.6: Check if body has ANY if-else statement (recursive) +/// +/// This is more specific than has_if_statement - it only detects if statements +/// with else branches, which are Pattern3 territory (if-phi merge). +pub(crate) fn has_if_else_statement(body: &[ASTNode]) -> bool { + for node in body { + match node { + ASTNode::If { else_body: Some(_), .. } => return true, + ASTNode::ScopeBox { body, .. } => { + if has_if_else_statement(body) { + return true; + } + } + ASTNode::Loop { body, .. } => { + if has_if_else_statement(body) { + return true; + } + } + _ => {} + } + } + false +} + +/// Phase 286: Find first if-else statement in loop body (non-recursive) +pub(crate) fn find_if_else_statement(body: &[ASTNode]) -> Option<&ASTNode> { + for stmt in body { + if matches!(stmt, ASTNode::If { else_body: Some(_), .. }) { + return Some(stmt); // Found first if-else + } + } + None +} + +/// ============================================================ +/// Group 3: Condition Validation (比較演算検証) +/// ============================================================ + +use crate::ast::BinaryOperator; + +/// Validate condition: 比較演算 (左辺が変数) +pub(crate) fn extract_loop_variable(condition: &ASTNode) -> Option { + match condition { + ASTNode::BinaryOp { operator, left, .. } => { + if !matches!( + operator, + BinaryOperator::Less + | BinaryOperator::LessEqual + | BinaryOperator::Greater + | BinaryOperator::GreaterEqual + | BinaryOperator::Equal + | BinaryOperator::NotEqual + ) { + return None; + } + + if let ASTNode::Variable { name, .. } = left.as_ref() { + return Some(name.clone()); + } + + None + } + _ => None, + } +} + +/// Check if condition is true literal +pub(crate) fn is_true_literal(condition: &ASTNode) -> bool { + use crate::ast::LiteralValue; + + matches!( + condition, + ASTNode::Literal { + value: LiteralValue::Bool(true), + .. + } + ) +} + +/// ============================================================ +/// Group 4: Loop Increment Extraction (Common for Plan line) +/// ============================================================ + +/// Phase 286 P2.2: Extract loop increment for Plan line patterns +/// +/// Supports ` = + ` pattern only (PoC safety). +pub(crate) fn extract_loop_increment_plan( + body: &[ASTNode], + loop_var: &str, +) -> Result, String> { + for stmt in body { + if let ASTNode::Assignment { target, value, .. } = stmt { + if let ASTNode::Variable { name, .. } = target.as_ref() { + if name == loop_var { + // Check for `i = i + ` + if let ASTNode::BinaryOp { + operator, + left, + right, + .. + } = value.as_ref() + { + if matches!(operator, BinaryOperator::Add) { + if let ASTNode::Variable { name: lname, .. } = left.as_ref() { + if lname == loop_var { + if matches!(right.as_ref(), ASTNode::Literal { .. }) { + return Ok(Some(value.as_ref().clone())); + } + } + } + } + } + } + } + } + } + Ok(None) +} + +/// ============================================================ +/// Group 5: Pattern5-Specific Helpers (NOT generalized) +/// ============================================================ +/// +/// **IMPORTANT**: These helpers are Pattern5-specific and intentionally NOT generalized. + +/// Validate continue is at body end (Pattern5 specific) +pub(crate) fn validate_continue_at_end(body: &[ASTNode]) -> bool { + matches!(body.last(), Some(ASTNode::Continue { .. })) +} + +/// Validate break is in simple if pattern (Pattern5 specific) +pub(crate) fn validate_break_in_simple_if(body: &[ASTNode]) -> bool { + for stmt in body { + if let ASTNode::If { + then_body, + else_body, + .. + } = stmt + { + if then_body.len() == 1 + && matches!(then_body[0], ASTNode::Break { .. }) + && else_body.is_none() + { + return true; + } + } + } + false +} + +// ============================================================================ +// Unit Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; + + fn make_break() -> ASTNode { + ASTNode::Break { span: Span::unknown() } + } + + fn make_continue() -> ASTNode { + ASTNode::Continue { span: Span::unknown() } + } + + fn make_return() -> ASTNode { + ASTNode::Return { + value: None, + span: Span::unknown(), + } + } + + fn make_if_with_break() -> ASTNode { + ASTNode::If { + condition: Box::new(ASTNode::Variable { + name: "cond".to_string(), + span: Span::unknown(), + }), + then_body: vec![make_break()], + else_body: None, + span: Span::unknown(), + } + } + + #[test] + fn test_count_control_flow_break() { + let body = vec![make_break()]; + let counts = count_control_flow(&body, ControlFlowDetector::default()); + assert_eq!(counts.break_count, 1); + assert_eq!(counts.continue_count, 0); + } + + #[test] + fn test_has_break_statement() { + let body = vec![make_if_with_break()]; + assert!(has_break_statement(&body)); + } + + #[test] + fn test_has_continue_statement() { + let body = vec![make_continue()]; + assert!(has_continue_statement(&body)); + } + + #[test] + fn test_has_continue_statement_false() { + let body = vec![make_break()]; + assert!(!has_continue_statement(&body)); + } + + #[test] + fn test_has_return_statement() { + let body = vec![make_return()]; + assert!(has_return_statement(&body)); + } + + #[test] + fn test_has_return_statement_false() { + let body = vec![make_break()]; + assert!(!has_return_statement(&body)); + } + + #[test] + fn test_has_control_flow_statement_break() { + let body = vec![make_if_with_break()]; + assert!(has_control_flow_statement(&body)); + } + + #[test] + fn test_has_control_flow_statement_continue() { + let body = vec![make_continue()]; + assert!(has_control_flow_statement(&body)); + } + + #[test] + fn test_has_control_flow_statement_false() { + let body = vec![ASTNode::Variable { + name: "x".to_string(), + span: Span::unknown(), + }]; + assert!(!has_control_flow_statement(&body)); + } + + #[test] + fn test_extract_loop_variable_success() { + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert_eq!(extract_loop_variable(&condition), Some("i".to_string())); + } + + #[test] + fn test_extract_loop_variable_not_comparison() { + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert_eq!(extract_loop_variable(&condition), None); + } + + #[test] + fn test_extract_loop_variable_not_variable() { + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(5), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + assert_eq!(extract_loop_variable(&condition), None); + } + + #[test] + fn test_is_true_literal_success() { + let condition = ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }; + + assert!(is_true_literal(&condition)); + } + + #[test] + fn test_is_true_literal_false() { + let condition = ASTNode::Literal { + value: LiteralValue::Bool(false), + span: Span::unknown(), + }; + + assert!(!is_true_literal(&condition)); + } + + #[test] + fn test_is_true_literal_not_literal() { + let condition = ASTNode::Variable { + name: "x".to_string(), + span: Span::unknown(), + }; + + assert!(!is_true_literal(&condition)); + } + + #[test] + fn test_validate_continue_at_end_success() { + let body = vec![make_break(), make_continue()]; + assert!(validate_continue_at_end(&body)); + } + + #[test] + fn test_validate_continue_at_end_false() { + let body = vec![make_continue(), make_break()]; + assert!(!validate_continue_at_end(&body)); + } + + #[test] + fn test_validate_break_in_simple_if_success() { + let body = vec![make_if_with_break()]; + assert!(validate_break_in_simple_if(&body)); + } + + #[test] + fn test_validate_break_in_simple_if_with_else() { + let body = vec![ASTNode::If { + condition: Box::new(ASTNode::Variable { + name: "done".to_string(), + span: Span::unknown(), + }), + then_body: vec![make_break()], + else_body: Some(vec![make_continue()]), + span: Span::unknown(), + }]; + assert!(!validate_break_in_simple_if(&body)); + } + + #[test] + fn test_validate_break_in_simple_if_multiple_statements() { + let body = vec![ASTNode::If { + condition: Box::new(ASTNode::Variable { + name: "done".to_string(), + span: Span::unknown(), + }), + then_body: vec![make_break(), make_continue()], + else_body: None, + span: Span::unknown(), + }]; + assert!(!validate_break_in_simple_if(&body)); + } +} diff --git a/src/mir/builder/control_flow/plan/extractors/mod.rs b/src/mir/builder/control_flow/plan/extractors/mod.rs index ecd41848..c621c4d2 100644 --- a/src/mir/builder/control_flow/plan/extractors/mod.rs +++ b/src/mir/builder/control_flow/plan/extractors/mod.rs @@ -1,4 +1,6 @@ //! Phase 29ai P6: Plan-layer extractors (Pattern6/7) +pub(in crate::mir::builder) mod common_helpers; pub(in crate::mir::builder) mod pattern6_scan_with_init; pub(in crate::mir::builder) mod pattern7_split_scan; +pub(in crate::mir::builder) mod pattern2_break; diff --git a/src/mir/builder/control_flow/plan/extractors/pattern2_break.rs b/src/mir/builder/control_flow/plan/extractors/pattern2_break.rs new file mode 100644 index 00000000..6668a26a --- /dev/null +++ b/src/mir/builder/control_flow/plan/extractors/pattern2_break.rs @@ -0,0 +1,521 @@ +//! Phase 282 P4: Pattern2 (Loop with Conditional Break) Extraction +//! Phase 282 P9a: Integrated with common_helpers +//! Phase 286 P3.1: Added extract_pattern2_plan() for Plan/Frag SSOT +//! Phase 29ai P10: Moved to plan-layer SSOT + +use crate::ast::{ASTNode, BinaryOperator, LiteralValue}; + +// Phase 282 P9a: Use common_helpers +use crate::mir::builder::control_flow::plan::extractors::common_helpers::{ + count_control_flow, has_continue_statement as common_has_continue, + has_return_statement as common_has_return, ControlFlowDetector, +}; + +#[derive(Debug, Clone)] +pub(crate) struct Pattern2Parts { + pub loop_var: String, // Loop variable name (empty for loop(true)) + pub is_loop_true: bool, // true if loop(true), false otherwise + pub break_count: usize, // Number of break statements (validation) + // Note: AST reused from ctx - no duplication +} + +/// Extract Pattern2 (Loop with Conditional Break) parts +/// +/// # Detection Criteria (Lightweight - SSOT minimal) +/// +/// 1. **Condition**: 比較演算 (left=variable) OR loop(true) +/// 2. **Body**: MUST have at least 1 break statement +/// 3. **Body**: NO continue statements (Pattern4 territory) +/// 4. **Body**: NO return statements (Pattern2 = break-only) +/// +/// # Four-Phase Validation +/// +/// **Phase 1**: Validate condition (比較演算 or loop(true)) +/// **Phase 2**: Validate HAS break (required for Pattern2) +/// **Phase 3**: Validate NO continue/return (break-only) +/// **Phase 4**: Extract core info +/// +/// # Fail-Fast Rules +/// +/// - `Ok(Some(parts))`: Pattern2 match confirmed +/// - `Ok(None)`: Not Pattern2 (no break, has continue/return, or Pattern1) +/// - `Err(msg)`: Logic bug (malformed AST) +/// +/// # Note +/// +/// Carrier update validation is deferred to can_lower() via CommonPatternInitializer (SSOT). +pub(crate) fn extract_loop_with_break_parts( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, String> { + // Phase 1: Validate condition + let (loop_var, is_loop_true) = validate_condition_for_pattern2(condition); + + // Phase 2: Validate HAS break (required) + let break_count = count_break_statements(body); + if break_count == 0 { + return Ok(None); // No break → Pattern1 territory + } + + // Phase 3: Validate NO continue/return (break-only) + if has_continue_statement(body) { + return Ok(None); // Has continue → Pattern4 territory + } + if has_return_statement(body) { + return Ok(None); // Has return → Pattern2 is break-only + } + + // Phase 4: Return extracted info + Ok(Some(Pattern2Parts { + loop_var, + is_loop_true, + break_count, + })) +} + +/// Validate condition: 比較演算 or loop(true) +fn validate_condition_for_pattern2(condition: &ASTNode) -> (String, bool) { + // Case 1: loop(true) + if matches!( + condition, + ASTNode::Literal { + value: LiteralValue::Bool(true), + .. + } + ) { + return ("".to_string(), true); // loop_var determined in can_lower() + } + + // Case 2: 比較演算 (same as Pattern1) + match condition { + ASTNode::BinaryOp { operator, left, .. } => { + if matches!( + operator, + BinaryOperator::Less + | BinaryOperator::LessEqual + | BinaryOperator::Greater + | BinaryOperator::GreaterEqual + | BinaryOperator::Equal + | BinaryOperator::NotEqual + ) { + if let ASTNode::Variable { name, .. } = left.as_ref() { + return (name.clone(), false); + } + } + } + _ => {} + } + + // Unknown condition - let can_lower() decide + ("".to_string(), false) +} + +/// Count break statements recursively +/// +/// # Phase 282 P9a: Delegates to common_helpers::count_control_flow +fn count_break_statements(body: &[ASTNode]) -> usize { + count_control_flow(body, ControlFlowDetector::default()).break_count +} + +/// Check for continue statements +/// +/// # Phase 282 P9a: Delegates to common_helpers::has_continue_statement +fn has_continue_statement(body: &[ASTNode]) -> bool { + common_has_continue(body) +} + +/// Check for return statements (Pattern2 = break-only) +/// +/// # Phase 282 P9a: Delegates to common_helpers::has_return_statement +fn has_return_statement(body: &[ASTNode]) -> bool { + common_has_return(body) +} + +// ============================================================================ +// Phase 286 P3.1: Plan line extraction +// ============================================================================ + +/// Extract Pattern2 Plan for Plan/Frag SSOT migration +/// +/// # PoC Subset (Strict - returns Ok(None) for unsupported forms) +/// +/// Supported form: +/// ```hako +/// loop(i < N) { +/// if (break_cond) { [carrier = expr;] break } +/// carrier = carrier + expr +/// i = i + 1 +/// } +/// ``` +/// +/// Returns Ok(None) for: +/// - loop_increment not extractable (complex structure) +/// - break_cond not single if (nested, multiple conditions) +/// - break_then has multiple statements and carrier update not identifiable +/// - Multiple carriers (PoC is single carrier only) +/// - Body carrier update not identifiable +/// +/// This prevents Fail-Fast regression by falling back to legacy JoinIR line. +pub(crate) fn extract_pattern2_plan( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, String> { + use crate::mir::builder::control_flow::plan::{DomainPlan, Pattern2BreakPlan}; + + // Step 1: Validate via existing extractor (has break, no continue/return) + let parts = extract_loop_with_break_parts(condition, body)?; + if parts.is_none() { + return Ok(None); // Not Pattern2 → legacy fallback + } + let parts = parts.unwrap(); + + // Step 2: PoC constraint - only support non-loop(true) for now + if parts.is_loop_true { + return Ok(None); // loop(true) is complex → legacy fallback + } + + // Step 3: PoC constraint - only support single break + if parts.break_count != 1 { + return Ok(None); // Multiple breaks → legacy fallback + } + + // Step 4: Body structure must be: if { ... break } + carrier_update + loop_increment + // Expected structure: + // body[0] = If { then_body contains break } + // body[1] = carrier update assignment + // body[2] = loop_var update assignment + if body.len() != 3 { + return Ok(None); // Unexpected body length → legacy fallback + } + + // Step 4.1: First element must be an If with break in then_body + let (break_condition, carrier_update_in_break) = match &body[0] { + ASTNode::If { + condition: if_cond, + then_body, + else_body, + .. + } => { + // PoC: No else branch allowed + if else_body.is_some() { + return Ok(None); + } + + // Extract break condition + let break_cond = if_cond.as_ref().clone(); + + // Check if then_body ends with break + let has_break_at_end = then_body + .last() + .map(|n| matches!(n, ASTNode::Break { .. })) + .unwrap_or(false); + if !has_break_at_end { + return Ok(None); + } + + // Extract carrier update in break (if any) + // If then_body is [break], no update + // If then_body is [assignment, break], extract assignment + let carrier_update = if then_body.len() == 1 { + None // Just break, no update + } else if then_body.len() == 2 { + // Should be [assignment, break] + match &then_body[0] { + ASTNode::Assignment { value, .. } => Some(value.as_ref().clone()), + _ => return Ok(None), // Not an assignment → legacy + } + } else { + return Ok(None); // Complex then_body → legacy + }; + + (break_cond, carrier_update) + } + _ => return Ok(None), // First element not If → legacy + }; + + // Step 4.2: Extract carrier update in body (second element) + let (carrier_var, carrier_update_in_body) = match &body[1] { + ASTNode::Assignment { target, value, .. } => { + let carrier_name = match target.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => return Ok(None), + }; + (carrier_name, value.as_ref().clone()) + } + _ => return Ok(None), + }; + + // Step 4.3: Extract loop increment (third element) + let loop_increment = match super::common_helpers::extract_loop_increment_plan(body, &parts.loop_var)? { + Some(inc) => inc, + None => return Ok(None), // No loop increment found → legacy + }; + + // Step 5: Validate loop condition is ` < ` (same as Pattern1) + if !validate_loop_condition_for_plan(condition, &parts.loop_var) { + return Ok(None); + } + + Ok(Some(DomainPlan::Pattern2Break(Pattern2BreakPlan { + loop_var: parts.loop_var, + carrier_var, + loop_condition: condition.clone(), + break_condition, + carrier_update_in_break, + carrier_update_in_body, + loop_increment, + }))) +} + +/// Validate loop condition: supports ` < ` only (same as Pattern1) +fn validate_loop_condition_for_plan(cond: &ASTNode, loop_var: &str) -> bool { + if let ASTNode::BinaryOp { + operator, + left, + right, + .. + } = cond + { + if !matches!(operator, BinaryOperator::Less) { + return false; // Only < supported for PoC + } + + // Left must be the loop variable + if let ASTNode::Variable { name, .. } = left.as_ref() { + if name != loop_var { + return false; + } + } else { + return false; + } + + // Right must be integer literal + if !matches!( + right.as_ref(), + ASTNode::Literal { + value: LiteralValue::Integer(_), + .. + } + ) { + return false; + } + + true + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::Span; + + #[test] + fn test_extract_with_break_success() { + // loop(i < 10) { if (i == 5) { break } i = i + 1 } + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let body = vec![ + ASTNode::If { + condition: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(5), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + then_body: vec![ASTNode::Break { + span: Span::unknown(), + }], + else_body: None, + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ]; + + let result = extract_loop_with_break_parts(&condition, &body); + assert!(result.is_ok()); + let parts = result.unwrap(); + assert!(parts.is_some()); + let parts = parts.unwrap(); + assert_eq!(parts.loop_var, "i"); + assert_eq!(parts.is_loop_true, false); + assert_eq!(parts.break_count, 1); + } + + #[test] + fn test_extract_no_break_returns_none() { + // loop(i < 10) { i = i + 1 } + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }]; + + let result = extract_loop_with_break_parts(&condition, &body); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); // No break → Pattern1 territory + } + + #[test] + fn test_extract_with_continue_returns_none() { + // loop(i < 10) { if (i == 5) { continue } i = i + 1 } + let condition = ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(10), + span: Span::unknown(), + }), + span: Span::unknown(), + }; + + let body = vec![ASTNode::If { + condition: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(5), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + then_body: vec![ASTNode::Continue { + span: Span::unknown(), + }], + else_body: None, + span: Span::unknown(), + }]; + + let result = extract_loop_with_break_parts(&condition, &body); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); // Has continue → Pattern4 territory + } + + #[test] + fn test_extract_loop_true_with_break_success() { + // loop(true) { if (i == 5) { break } i = i + 1 } + let condition = ASTNode::Literal { + value: LiteralValue::Bool(true), + span: Span::unknown(), + }; + + let body = vec![ + ASTNode::If { + condition: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Equal, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(5), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + then_body: vec![ASTNode::Break { + span: Span::unknown(), + }], + else_body: None, + span: Span::unknown(), + }, + ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: Span::unknown(), + }), + right: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: Span::unknown(), + }), + span: Span::unknown(), + }), + span: Span::unknown(), + }, + ]; + + let result = extract_loop_with_break_parts(&condition, &body); + assert!(result.is_ok()); + let parts = result.unwrap(); + assert!(parts.is_some()); + let parts = parts.unwrap(); + assert_eq!(parts.loop_var, ""); // Empty for loop(true) + assert_eq!(parts.is_loop_true, true); + assert_eq!(parts.break_count, 1); + } +} diff --git a/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs b/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs index e8ecf382..1b27fbef 100644 --- a/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs +++ b/src/mir/builder/control_flow/plan/single_planner/legacy_rules/pattern2.rs @@ -1,6 +1,6 @@ use crate::mir::builder::control_flow::joinir::patterns::router::LoopPatternContext; use crate::mir::builder::control_flow::plan::DomainPlan; -use crate::mir::builder::control_flow::joinir::patterns::extractors::pattern2::extract_pattern2_plan; +use crate::mir::builder::control_flow::plan::extractors::pattern2_break::extract_pattern2_plan; pub(in crate::mir::builder) fn extract( ctx: &LoopPatternContext,