diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs b/src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs index a8d33baa..a228a949 100644 --- a/src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs +++ b/src/mir/builder/control_flow/joinir/patterns/pattern4_with_continue.rs @@ -36,6 +36,8 @@ use crate::mir::builder::MirBuilder; use crate::mir::ValueId; use super::super::trace; use crate::mir::loop_pattern_detection::error_messages; +use crate::mir::join_ir::lowering::loop_update_analyzer::UpdateExpr; +use std::collections::BTreeMap; /// Phase 194+: Detection function for Pattern 4 /// @@ -144,261 +146,258 @@ impl MirBuilder { _func_name: &str, debug: bool, ) -> Result, String> { - use crate::mir::join_ir::lowering::loop_with_continue_minimal::lower_loop_with_continue_minimal; - use super::pattern4_carrier_analyzer::Pattern4CarrierAnalyzer; - // Phase 195: Use unified trace trace::trace().debug("pattern4", "Calling Pattern 4 minimal lowerer"); - // Phase 33-23: Use Pattern4CarrierAnalyzer for normalization - // This transforms: if (cond) { body } else { continue } - // into: if (!cond) { continue } else { body } - let normalized_body = Pattern4CarrierAnalyzer::normalize_continue_branches(_body); - let body_to_analyze = &normalized_body; + let prepared = prepare_pattern4_context(self, condition, _body)?; + lower_pattern4_joinir(self, condition, &prepared, debug) + } +} - // Phase 179-B: Use PatternPipelineContext for unified preprocessing - // Note: Pattern 4 still needs inline processing for carrier analysis - use super::pattern_pipeline::{build_pattern_context, PatternVariant}; - let ctx = build_pattern_context( - self, - condition, - body_to_analyze, - PatternVariant::Pattern4, - )?; +/// Preprocessed data for Pattern 4 lowering. +struct Pattern4Prepared { + loop_var_name: String, + loop_scope: crate::mir::join_ir::lowering::loop_scope_shape::LoopScopeShape, + carrier_info: crate::mir::join_ir::lowering::carrier_info::CarrierInfo, + carrier_updates: BTreeMap, +} - // Extract from context - let loop_var_name = ctx.loop_var_name.clone(); - let _loop_var_id = ctx.loop_var_id; - let carrier_info_prelim = ctx.carrier_info.clone(); - let scope = ctx.loop_scope.clone(); +/// Normalize, build context, analyze carriers, and promote loop-body locals. +fn prepare_pattern4_context( + builder: &mut MirBuilder, + condition: &ASTNode, + body: &[ASTNode], +) -> Result { + use super::pattern4_carrier_analyzer::Pattern4CarrierAnalyzer; + use super::pattern_pipeline::{build_pattern_context, PatternVariant}; + use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox; + use crate::mir::loop_pattern_detection::loop_body_cond_promoter::{ + LoopBodyCondPromoter, ConditionPromotionRequest, ConditionPromotionResult, + }; - // Phase 33-23: Use Pattern4CarrierAnalyzer for carrier filtering - // Analyze carrier update expressions FIRST to identify actual carriers - let carrier_updates = Pattern4CarrierAnalyzer::analyze_carrier_updates( - body_to_analyze, - &carrier_info_prelim.carriers, - ); + // Normalize continue branches for analysis/lowering + let normalized_body = Pattern4CarrierAnalyzer::normalize_continue_branches(body); - // Phase 33-23: Filter carriers using the new analyzer - // This prevents constant variables (like M, args) from being treated as carriers - // Phase 171-C-4: carrier_info is now mutable for promotion merging - let mut carrier_info = Pattern4CarrierAnalyzer::analyze_carriers( - body_to_analyze, - &carrier_info_prelim, - )?; + // Build preprocessing context + let ctx = build_pattern_context( + builder, + condition, + &normalized_body, + PatternVariant::Pattern4, + )?; + let loop_var_name = ctx.loop_var_name.clone(); + let loop_scope = ctx.loop_scope.clone(); + let carrier_info_prelim = ctx.carrier_info.clone(); + + // Analyze carrier updates and filter carriers + let carrier_updates = Pattern4CarrierAnalyzer::analyze_carrier_updates( + &normalized_body, + &carrier_info_prelim.carriers, + ); + let mut carrier_info = Pattern4CarrierAnalyzer::analyze_carriers( + &normalized_body, + &carrier_info_prelim, + )?; + + trace::trace().debug( + "pattern4", + &format!( + "CarrierInfo: loop_var={}, carriers={:?}", + carrier_info.loop_var_name, + carrier_info.carriers.iter().map(|c| &c.name).collect::>() + ) + ); + + trace::trace().debug( + "pattern4", + &format!("Analyzed {} carrier update expressions", carrier_updates.len()) + ); + for (carrier_name, update_expr) in &carrier_updates { trace::trace().debug( "pattern4", - &format!( - "CarrierInfo: loop_var={}, carriers={:?}", - carrier_info.loop_var_name, - carrier_info.carriers.iter().map(|c| &c.name).collect::>() - ) + &format!(" {} → {:?}", carrier_name, update_expr) ); + } - trace::trace().debug( - "pattern4", - &format!("Analyzed {} carrier update expressions", carrier_updates.len()) - ); - for (carrier_name, update_expr) in &carrier_updates { - trace::trace().debug( - "pattern4", - &format!(" {} → {:?}", carrier_name, update_expr) - ); - } + // LoopBodyLocal promotion for conditions (skip_whitespace etc.) + let continue_cond = LoopBodyCondPromoter::extract_continue_condition(&normalized_body); + let conditions_to_analyze: Vec<&ASTNode> = if let Some(cont_cond) = continue_cond { + vec![condition, cont_cond] + } else { + vec![condition] + }; - // Phase 195: Use unified trace - trace::trace().varmap("pattern4_start", &self.variable_map); + let cond_scope = LoopConditionScopeBox::analyze( + &loop_var_name, + &conditions_to_analyze, + Some(&loop_scope), + ); - // Phase 223-3: LoopBodyLocal Condition Promotion - // - // Check for LoopBodyLocal in loop/continue conditions and attempt promotion. - // Safe Trim patterns (Category A-3: skip_whitespace) are promoted to carriers. - { - use crate::mir::loop_pattern_detection::loop_condition_scope::LoopConditionScopeBox; - use crate::mir::loop_pattern_detection::loop_body_cond_promoter::{ - LoopBodyCondPromoter, ConditionPromotionRequest, ConditionPromotionResult, - }; + if cond_scope.has_loop_body_local() { + let promotion_req = ConditionPromotionRequest { + loop_param_name: &loop_var_name, + cond_scope: &cond_scope, + scope_shape: Some(&loop_scope), + break_cond: None, // Pattern 4 has no break + continue_cond, + loop_body: &normalized_body, + }; - // Extract continue condition from body (if present) - // Handles skip_whitespace pattern: if ch == " " || ... { continue } - let continue_cond = LoopBodyCondPromoter::extract_continue_condition(body_to_analyze); + match LoopBodyCondPromoter::try_promote_for_condition(promotion_req) { + ConditionPromotionResult::Promoted { + carrier_info: promoted_carrier, + promoted_var, + carrier_name, + } => { + trace::trace().debug( + "pattern4/cond_promoter", + &format!( + "LoopBodyLocal '{}' promoted to carrier '{}'", + promoted_var, carrier_name + ), + ); - // Analyze both header condition and continue condition for LoopBodyLocal - let conditions_to_analyze: Vec<&ASTNode> = if let Some(cont_cond) = continue_cond { - vec![condition, cont_cond] - } else { - vec![condition] - }; + carrier_info.merge_from(&promoted_carrier); - let cond_scope = LoopConditionScopeBox::analyze( - &loop_var_name, - &conditions_to_analyze, - Some(&scope), - ); - - if cond_scope.has_loop_body_local() { - // Try promotion using LoopBodyCondPromoter - let promotion_req = ConditionPromotionRequest { - loop_param_name: &loop_var_name, - cond_scope: &cond_scope, - scope_shape: Some(&scope), - break_cond: None, // Pattern 4 has no break - continue_cond, - loop_body: body_to_analyze, - }; - - match LoopBodyCondPromoter::try_promote_for_condition(promotion_req) { - ConditionPromotionResult::Promoted { - carrier_info: promoted_carrier, - promoted_var, + trace::trace().debug( + "pattern4/cond_promoter", + &format!( + "Merged carrier '{}' into CarrierInfo (total carriers: {})", carrier_name, - } => { - eprintln!( - "[pattern4/cond_promoter] LoopBodyLocal '{}' promoted to carrier '{}'", - promoted_var, carrier_name + carrier_info.carrier_count() + ), + ); + + if let Some(helper) = carrier_info.trim_helper() { + if helper.is_safe_trim() { + trace::trace().debug( + "pattern4/cond_promoter", + "Safe Trim/skip_whitespace pattern detected", ); - - // Merge promoted carrier into existing CarrierInfo - carrier_info.merge_from(&promoted_carrier); - - eprintln!( - "[pattern4/cond_promoter] Merged carrier '{}' into CarrierInfo (total carriers: {})", - carrier_name, - carrier_info.carrier_count() + trace::trace().debug( + "pattern4/cond_promoter", + &format!( + "Carrier: '{}', original var: '{}', whitespace chars: {:?}", + helper.carrier_name, helper.original_var, helper.whitespace_chars + ), ); - - // Check if this is a safe Trim pattern - if let Some(helper) = carrier_info.trim_helper() { - if helper.is_safe_trim() { - eprintln!( - "[pattern4/cond_promoter] Safe Trim/skip_whitespace pattern detected" - ); - eprintln!( - "[pattern4/cond_promoter] Carrier: '{}', original var: '{}', whitespace chars: {:?}", - helper.carrier_name, helper.original_var, helper.whitespace_chars - ); - // Continue with Pattern4 lowering (fall through) - } else { - return Err(error_messages::format_error_pattern4_trim_not_safe( - &helper.carrier_name, - helper.whitespace_count() - )); - } - } - // Carrier promoted and merged, proceed with normal lowering - } - ConditionPromotionResult::CannotPromote { reason, vars } => { - // Fail-Fast on promotion failure - return Err(error_messages::format_error_pattern4_promotion_failed(&vars, &reason)); + } else { + return Err(error_messages::format_error_pattern4_trim_not_safe( + &helper.carrier_name, + helper.whitespace_count() + )); } } } - } - - // Phase 202-C: Create JoinValueSpace for unified ValueId allocation - use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; - let mut join_value_space = JoinValueSpace::new(); - - // Phase 169 / Phase 202-C: Call Pattern 4 lowerer with JoinValueSpace - let (join_module, exit_meta) = match lower_loop_with_continue_minimal( - scope, - condition, - self, - &carrier_info, - &carrier_updates, - &mut join_value_space, - ) { - Ok(result) => result, - Err(e) => { - // Phase 195: Use unified trace - trace::trace().debug("pattern4", &format!("Pattern 4 lowerer failed: {}", e)); - return Err(error_messages::format_error_pattern4_lowering_failed(&e)); - } - }; - - trace::trace().debug( - "pattern4", - &format!("ExitMeta: {} exit bindings", exit_meta.exit_values.len()) - ); - for (carrier_name, join_value) in &exit_meta.exit_values { - trace::trace().debug( - "pattern4", - &format!(" {} → ValueId({})", carrier_name, join_value.0) - ); - } - - // Phase 179 Task 2: Use ExitMetaCollector for unified exit binding generation - // Replaces manual loop (Phase 196 implementation) with modular Box approach - // Phase 228-8: Pass carrier_info to include ConditionOnly carriers - use super::super::merge::exit_line::meta_collector::ExitMetaCollector; - let exit_bindings = ExitMetaCollector::collect( - self, - &exit_meta, - Some(&carrier_info), // Phase 228-8: Include ConditionOnly carriers - debug, - ); - - // Validate that all expected carriers have bindings - // (ExitMetaCollector silently skips missing carriers, but Pattern 4 expects all) - for carrier in &carrier_info.carriers { - if !exit_bindings.iter().any(|b| b.carrier_name == carrier.name) { - return Err(error_messages::format_error_pattern4_carrier_not_found(&carrier.name)); + ConditionPromotionResult::CannotPromote { reason, vars } => { + return Err(error_messages::format_error_pattern4_promotion_failed(&vars, &reason)); } } - - // Phase 196: Build host_inputs dynamically - // Order: [loop_var, carrier1, carrier2, ...] - let mut host_inputs = vec![carrier_info.loop_var_id]; - for carrier in &carrier_info.carriers { - host_inputs.push(carrier.host_id); - } - - trace::trace().debug( - "pattern4", - &format!("host_inputs: {:?}", host_inputs.iter().map(|v| v.0).collect::>()) - ); - - // Merge JoinIR blocks into current function - // Phase 196: Use dynamically generated exit_bindings and host_inputs - // Build join_inputs dynamically to match host_inputs length: - // [ValueId(0), ValueId(1), ValueId(2), ...] for i + N carriers - let mut join_inputs = vec![ValueId(0)]; // ValueId(0) = i_init in JoinIR - for idx in 0..carrier_info.carriers.len() { - join_inputs.push(ValueId((idx + 1) as u32)); // ValueId(1..N) = carrier inits - } - - trace::trace().debug( - "pattern4", - &format!("join_inputs: {:?}", join_inputs.iter().map(|v| v.0).collect::>()) - ); - - // Phase 201: Use JoinInlineBoundaryBuilder for clean construction - // Canonical Builder pattern - see docs/development/current/main/joinir-boundary-builder-pattern.md - use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; - let boundary = JoinInlineBoundaryBuilder::new() - .with_inputs(join_inputs, host_inputs) // Dynamic carrier count - .with_exit_bindings(exit_bindings) - .with_loop_var_name(Some(loop_var_name.clone())) // Phase 33-19: Enable exit PHI collection - .with_carrier_info(carrier_info.clone()) // Phase 228-6: Pass carrier_info for ConditionOnly header PHI init - .build(); - - // Phase 33-22: Use JoinIRConversionPipeline for unified conversion flow - use super::conversion_pipeline::JoinIRConversionPipeline; - let _result_val = JoinIRConversionPipeline::execute( - self, - join_module, - Some(&boundary), - "pattern4", - debug, - )?; - - // Phase 33-19: Like Pattern3, return void (loop result is read via variable_map) - let void_val = crate::mir::builder::emission::constant::emit_void(self); - - // Phase 195: Use unified trace - trace::trace().debug("pattern4", &format!("Loop complete, returning Void {:?}", void_val)); - - Ok(Some(void_val)) } + + Ok(Pattern4Prepared { + loop_var_name, + loop_scope, + carrier_info, + carrier_updates, + }) +} + +/// JoinIR lowering stage for Pattern 4 using prepared context. +fn lower_pattern4_joinir( + builder: &mut MirBuilder, + condition: &ASTNode, + prepared: &Pattern4Prepared, + debug: bool, +) -> Result, String> { + use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; + use crate::mir::join_ir::lowering::loop_with_continue_minimal::lower_loop_with_continue_minimal; + use super::super::merge::exit_line::meta_collector::ExitMetaCollector; + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + use super::conversion_pipeline::JoinIRConversionPipeline; + + trace::trace().varmap("pattern4_start", &builder.variable_map); + + let mut join_value_space = JoinValueSpace::new(); + + let (join_module, exit_meta) = match lower_loop_with_continue_minimal( + prepared.loop_scope.clone(), + condition, + builder, + &prepared.carrier_info, + &prepared.carrier_updates, + &mut join_value_space, + ) { + Ok(result) => result, + Err(e) => { + trace::trace().debug("pattern4", &format!("Pattern 4 lowerer failed: {}", e)); + return Err(error_messages::format_error_pattern4_lowering_failed(&e)); + } + }; + + trace::trace().debug( + "pattern4", + &format!("ExitMeta: {} exit bindings", exit_meta.exit_values.len()) + ); + for (carrier_name, join_value) in &exit_meta.exit_values { + trace::trace().debug( + "pattern4", + &format!(" {} → ValueId({})", carrier_name, join_value.0) + ); + } + + let exit_bindings = ExitMetaCollector::collect( + builder, + &exit_meta, + Some(&prepared.carrier_info), // Phase 228-8: Include ConditionOnly carriers + debug, + ); + + for carrier in &prepared.carrier_info.carriers { + if !exit_bindings.iter().any(|b| b.carrier_name == carrier.name) { + return Err(error_messages::format_error_pattern4_carrier_not_found(&carrier.name)); + } + } + + // Build inputs for boundary: [loop_var, carriers...] + let mut host_inputs = vec![prepared.carrier_info.loop_var_id]; + for carrier in &prepared.carrier_info.carriers { + host_inputs.push(carrier.host_id); + } + + trace::trace().debug( + "pattern4", + &format!("host_inputs: {:?}", host_inputs.iter().map(|v| v.0).collect::>()) + ); + + let mut join_inputs = vec![ValueId(0)]; // ValueId(0) = i_init in JoinIR + for idx in 0..prepared.carrier_info.carriers.len() { + join_inputs.push(ValueId((idx + 1) as u32)); // ValueId(1..N) = carrier inits + } + + trace::trace().debug( + "pattern4", + &format!("join_inputs: {:?}", join_inputs.iter().map(|v| v.0).collect::>()) + ); + + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(join_inputs, host_inputs) // Dynamic carrier count + .with_exit_bindings(exit_bindings) + .with_loop_var_name(Some(prepared.loop_var_name.clone())) + .with_carrier_info(prepared.carrier_info.clone()) + .build(); + + let _result_val = JoinIRConversionPipeline::execute( + builder, + join_module, + Some(&boundary), + "pattern4", + debug, + )?; + + let void_val = crate::mir::builder::emission::constant::emit_void(builder); + trace::trace().debug("pattern4", &format!("Loop complete, returning Void {:?}", void_val)); + + Ok(Some(void_val)) } diff --git a/src/mir/join_ir/lowering/loop_update_summary.rs b/src/mir/join_ir/lowering/loop_update_summary.rs index 6c2a80a6..136b78b7 100644 --- a/src/mir/join_ir/lowering/loop_update_summary.rs +++ b/src/mir/join_ir/lowering/loop_update_summary.rs @@ -12,7 +12,7 @@ //! ## 使用例 //! //! ```ignore -//! let summary = analyze_loop_updates(&carrier_names); +//! let summary = analyze_loop_updates_by_name(&carrier_names); //! if summary.has_single_counter() { //! // StringExamination パターン //! } @@ -345,24 +345,11 @@ pub fn analyze_loop_updates_from_ast( LoopUpdateSummary { carriers } } -/// Phase 219: Legacy wrapper for backward compatibility +/// Name-only heuristic for loop update summary (legacy compatibility) /// -/// # Deprecated (Phase 219) -/// -/// This function uses name-based heuristics and is deprecated. -/// Use `analyze_loop_updates_from_ast()` instead. -/// -/// # Arguments -/// -/// * `carrier_names` - キャリア変数名のリスト -/// -/// # Returns -/// -/// 各キャリアの更新パターンをまとめた LoopUpdateSummary -#[deprecated(since = "Phase 219", note = "Use analyze_loop_updates_from_ast() instead")] -pub fn analyze_loop_updates(carrier_names: &[String]) -> LoopUpdateSummary { - // Phase 219: Fallback to simple heuristic (for legacy call sites) - // This will be removed once all call sites are migrated +/// This path is kept for callers that only have carrier names (no AST). +/// Prefer `analyze_loop_updates_from_ast()` when loop body is available. +pub fn analyze_loop_updates_by_name(carrier_names: &[String]) -> LoopUpdateSummary { let carriers = carrier_names .iter() .map(|name| CarrierUpdateInfo { diff --git a/src/mir/loop_pattern_detection/mod.rs b/src/mir/loop_pattern_detection/mod.rs index b5d74eec..cbf31ac6 100644 --- a/src/mir/loop_pattern_detection/mod.rs +++ b/src/mir/loop_pattern_detection/mod.rs @@ -219,7 +219,7 @@ pub(crate) fn extract_features(loop_form: &LoopForm, scope: Option<&LoopScopeSha // Note: carriers is BTreeSet, so each item is already a String let update_summary = scope.map(|s| { let carrier_names: Vec = s.carriers.iter().cloned().collect(); - crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates(&carrier_names) + crate::mir::join_ir::lowering::loop_update_summary::analyze_loop_updates_by_name(&carrier_names) }); LoopFeatures { @@ -664,107 +664,178 @@ fn has_simple_condition(_loop_form: &LoopForm) -> bool { #[cfg(test)] mod tests { - + use super::*; + use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; + use crate::mir::loop_pattern_detection::LoopFeatures; - // ======================================================================== - // Pattern 1: Simple While Loop Tests - // ======================================================================== + fn span() -> Span { + Span::unknown() + } - #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern1_simple_while_detection() { - // TODO: Add unit test for simple while pattern detection - // Step 1: Create mock LoopForm with: - // - Empty break_targets - // - Empty continue_targets - // - Single latch - // Step 2: Call is_simple_while_pattern() - // Step 3: Assert returns true + fn var(name: &str) -> ASTNode { + ASTNode::Variable { + name: name.to_string(), + span: span(), + } + } + + fn lit_i(n: i64) -> ASTNode { + ASTNode::Literal { + value: LiteralValue::Integer(n), + span: span(), + } + } + + fn bin(op: BinaryOperator, left: ASTNode, right: ASTNode) -> ASTNode { + ASTNode::BinaryOp { + operator: op, + left: Box::new(left), + right: Box::new(right), + span: span(), + } + } + + fn assignment(target: ASTNode, value: ASTNode) -> ASTNode { + ASTNode::Assignment { + target: Box::new(target), + value: Box::new(value), + span: span(), + } + } + + fn has_continue(node: &ASTNode) -> bool { + match node { + ASTNode::Continue { .. } => true, + ASTNode::If { then_body, else_body, .. } => { + then_body.iter().any(has_continue) || else_body.as_ref().map_or(false, |b| b.iter().any(has_continue)) + } + ASTNode::Loop { body, .. } => body.iter().any(has_continue), + _ => false, + } + } + + fn has_break(node: &ASTNode) -> bool { + match node { + ASTNode::Break { .. } => true, + ASTNode::If { then_body, else_body, .. } => { + then_body.iter().any(has_break) || else_body.as_ref().map_or(false, |b| b.iter().any(has_break)) + } + ASTNode::Loop { body, .. } => body.iter().any(has_break), + _ => false, + } + } + + fn has_if(body: &[ASTNode]) -> bool { + body.iter().any(|n| matches!(n, ASTNode::If { .. })) + } + + fn carrier_count(body: &[ASTNode]) -> usize { + fn count(nodes: &[ASTNode]) -> usize { + let mut c = 0; + for n in nodes { + match n { + ASTNode::Assignment { .. } => c += 1, + ASTNode::If { then_body, else_body, .. } => { + c += count(then_body); + if let Some(else_body) = else_body { + c += count(else_body); + } + } + _ => {} + } + } + c + } + if count(body) > 0 { 1 } else { 0 } + } + + fn classify_body(body: &[ASTNode]) -> LoopPatternKind { + let has_continue_flag = body.iter().any(has_continue); + let has_break_flag = body.iter().any(has_break); + let features = LoopFeatures { + has_break: has_break_flag, + has_continue: has_continue_flag, + has_if: has_if(body), + has_if_else_phi: false, + carrier_count: carrier_count(body), + break_count: if has_break_flag { 1 } else { 0 }, + continue_count: if has_continue_flag { 1 } else { 0 }, + update_summary: None, + }; + classify(&features) } #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern1_rejects_break() { - // TODO: Add test that rejects loop with break - // Step 1: Create mock LoopForm with non-empty break_targets - // Step 2: Call is_simple_while_pattern() - // Step 3: Assert returns false - } - - // ======================================================================== - // Pattern 2: Loop with Break Tests - // ======================================================================== - - #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern2_break_detection() { - // TODO: Add unit test for break pattern detection - // Step 1: Create mock LoopForm with: - // - Non-empty break_targets (exactly 1) - // - Empty continue_targets - // - If statement with break - // Step 2: Call is_loop_with_break_pattern() - // Step 3: Assert returns true + fn pattern1_simple_while_is_detected() { + // loop(i < len) { i = i + 1 } + let body = vec![assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1)))]; + let kind = classify_body(&body); + assert_eq!(kind, LoopPatternKind::Pattern1SimpleWhile); } #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern2_rejects_no_break() { - // TODO: Add test that rejects loop without break - // Step 1: Create mock LoopForm with empty break_targets - // Step 2: Call is_loop_with_break_pattern() - // Step 3: Assert returns false - } - - // ======================================================================== - // Pattern 3: Loop with If-Else PHI Tests - // ======================================================================== - - #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern3_if_else_phi_detection() { - // TODO: Add unit test for if-else phi pattern detection - // Step 1: Create mock LoopForm with: - // - Empty break_targets - // - Empty continue_targets - // - If-else statement in body - // - Multiple carrier variables - // Step 2: Call is_loop_with_conditional_phi_pattern() - // Step 3: Assert returns true + fn pattern2_break_loop_is_detected() { + // loop(i < len) { if i > 0 { break } i = i + 1 } + let cond = bin(BinaryOperator::Greater, var("i"), lit_i(0)); + let body = vec![ + ASTNode::If { + condition: Box::new(cond), + then_body: vec![ASTNode::Break { span: span() }], + else_body: None, + span: span(), + }, + assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))), + ]; + let kind = classify_body(&body); + assert_eq!(kind, LoopPatternKind::Pattern2Break); } #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern3_rejects_break() { - // TODO: Add test that rejects loop with break - // Step 1: Create mock LoopForm with non-empty break_targets - // Step 2: Call is_loop_with_conditional_phi_pattern() - // Step 3: Assert returns false - } - - // ======================================================================== - // Pattern 4: Loop with Continue Tests - // ======================================================================== - - #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern4_continue_detection() { - // TODO: Add unit test for continue pattern detection - // Step 1: Create mock LoopForm with: - // - Non-empty continue_targets (at least 1) - // - Empty break_targets - // - If statement with continue - // Step 2: Call is_loop_with_continue_pattern() - // Step 3: Assert returns true + fn pattern3_if_sum_shape_is_detected() { + // loop(i < len) { if i % 2 == 1 { sum = sum + 1 } i = i + 1 } + let cond = bin( + BinaryOperator::Equal, + bin(BinaryOperator::Modulo, var("i"), lit_i(2)), + lit_i(1), + ); + let body = vec![ + ASTNode::If { + condition: Box::new(cond), + then_body: vec![assignment( + var("sum"), + bin(BinaryOperator::Add, var("sum"), lit_i(1)), + )], + else_body: None, + span: span(), + }, + assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))), + ]; + let kind = classify_body(&body); + assert_eq!(kind, LoopPatternKind::Pattern3IfPhi); } #[test] - #[ignore] // TODO: Implement test after detection logic is complete - fn test_pattern4_rejects_no_continue() { - // TODO: Add test that rejects loop without continue - // Step 1: Create mock LoopForm with empty continue_targets - // Step 2: Call is_loop_with_continue_pattern() - // Step 3: Assert returns false + fn pattern4_continue_loop_is_detected() { + // loop(i < len) { if (i % 2 == 0) { continue } sum = sum + i; i = i + 1 } + let cond = bin( + BinaryOperator::Equal, + bin(BinaryOperator::Modulo, var("i"), lit_i(2)), + lit_i(0), + ); + let body = vec![ + ASTNode::If { + condition: Box::new(cond), + then_body: vec![ASTNode::Continue { span: span() }], + else_body: Some(vec![assignment( + var("sum"), + bin(BinaryOperator::Add, var("sum"), var("i")), + )]), + span: span(), + }, + assignment(var("i"), bin(BinaryOperator::Add, var("i"), lit_i(1))), + ]; + let kind = classify_body(&body); + assert_eq!(kind, LoopPatternKind::Pattern4Continue); } }