diff --git a/apps/tests/loop_continue_else_pattern.hako b/apps/tests/loop_continue_else_pattern.hako new file mode 100644 index 00000000..e7789b07 --- /dev/null +++ b/apps/tests/loop_continue_else_pattern.hako @@ -0,0 +1,26 @@ +// Phase 33-19: Pattern B test - else-side continue +// Tests: if (cond) { body } else { continue } +// This should be normalized to: if (!cond) { continue } else { body } + +static box Main { + main(args) { + local i = 0 + local sum = 0 + local M = 5 + + loop(i < M) { + i = i + 1 + // Pattern B: Process when i != M, continue when i == M + if (i != M) { + sum = sum + i + } else { + continue + } + } + + // Expected: sum = 1 + 2 + 3 + 4 = 10 + // (i=5 is skipped by continue) + print(sum) + return 0 + } +} diff --git a/apps/tests/loop_continue_else_simple.hako b/apps/tests/loop_continue_else_simple.hako new file mode 100644 index 00000000..e7fb78ab --- /dev/null +++ b/apps/tests/loop_continue_else_simple.hako @@ -0,0 +1,25 @@ +// Phase 33-19: Pattern B test - else-side continue (simplified) +// Tests: if (cond) { body } else { continue } +// This version uses a hardcoded constant instead of variable M + +static box Main { + main(args) { + local i = 0 + local sum = 0 + + loop(i < 5) { + i = i + 1 + // Pattern B: Process when i != 5, continue when i == 5 + if (i != 5) { + sum = sum + i + } else { + continue + } + } + + // Expected: sum = 1 + 2 + 3 + 4 = 10 + // (i=5 is skipped by continue) + print(sum) + return 0 + } +} diff --git a/apps/tests/loop_continue_then_simple.hako b/apps/tests/loop_continue_then_simple.hako new file mode 100644 index 00000000..999834d1 --- /dev/null +++ b/apps/tests/loop_continue_then_simple.hako @@ -0,0 +1,23 @@ +// Pattern A test - then-side continue (for comparison) +// Tests: if (cond) { continue } else { body } + +static box Main { + main(args) { + local i = 0 + local sum = 0 + + loop(i < 5) { + i = i + 1 + // Pattern A: Skip when i == 5, process otherwise + if (i == 5) { + continue + } else { + sum = sum + i + } + } + + // Expected: sum = 1 + 2 + 3 + 4 = 10 + print(sum) + return 0 + } +} 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 9caf356e..8351899d 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 @@ -124,6 +124,7 @@ impl MirBuilder { debug: bool, ) -> Result, String> { use crate::mir::join_ir::lowering::carrier_info::{CarrierInfo, CarrierVar}; + use crate::mir::join_ir::lowering::continue_branch_normalizer::ContinueBranchNormalizer; use crate::mir::join_ir::lowering::loop_update_analyzer::LoopUpdateAnalyzer; use crate::mir::join_ir::lowering::loop_with_continue_minimal::lower_loop_with_continue_minimal; use crate::mir::join_ir_vm_bridge::convert_join_module_to_mir_with_meta; @@ -133,6 +134,12 @@ impl MirBuilder { // Phase 195: Use unified trace trace::trace().debug("pattern4", "Calling Pattern 4 minimal lowerer"); + // Phase 33-19: Normalize else-continue patterns to then-continue + // This transforms: if (cond) { body } else { continue } + // into: if (!cond) { continue } else { body } + let normalized_body = ContinueBranchNormalizer::normalize_loop_body(_body); + let body_to_analyze = &normalized_body; + // Extract loop variables from condition (i and sum) let loop_var_name = self.extract_loop_variable_from_condition(condition)?; let loop_var_id = self @@ -146,18 +153,30 @@ impl MirBuilder { ) })?; - // Phase 196: Build CarrierInfo from variable_map - // Collect all non-loop-var variables as potential carriers - let mut carriers = Vec::new(); + // Phase 197: Analyze carrier update expressions FIRST to identify actual carriers + // Phase 33-19: Use normalized_body for analysis (after else-continue transformation) + // Build temporary carrier list for initial analysis + let mut temp_carriers = Vec::new(); for (var_name, &var_id) in &self.variable_map { if var_name != &loop_var_name { - carriers.push(CarrierVar { + temp_carriers.push(CarrierVar { name: var_name.clone(), host_id: var_id, }); } } + let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(body_to_analyze, &temp_carriers); + + // Phase 33-19: Build CarrierInfo with ONLY updated variables as carriers + // This prevents constant variables (like M, args) from being treated as carriers + let mut carriers = Vec::new(); + for temp_carrier in &temp_carriers { + if carrier_updates.contains_key(&temp_carrier.name) { + carriers.push(temp_carrier.clone()); + } + } + let carrier_info = CarrierInfo { loop_var_name: loop_var_name.clone(), loop_var_id, @@ -173,9 +192,6 @@ impl MirBuilder { ) ); - // Phase 197: Analyze carrier update expressions from loop body - let carrier_updates = LoopUpdateAnalyzer::analyze_carrier_updates(_body, &carrier_info.carriers); - trace::trace().debug( "pattern4", &format!("Analyzed {} carrier update expressions", carrier_updates.len()) diff --git a/src/mir/join_ir/lowering/continue_branch_normalizer.rs b/src/mir/join_ir/lowering/continue_branch_normalizer.rs new file mode 100644 index 00000000..86cda7c2 --- /dev/null +++ b/src/mir/join_ir/lowering/continue_branch_normalizer.rs @@ -0,0 +1,342 @@ +//! Phase 33-19: Continue Branch Normalizer +//! +//! Normalize if/else patterns with continue to simplify Pattern 4 lowering +//! +//! Pattern: if (cond) { body } else { continue } +//! Transforms to: if (!cond) { continue } else { body } +//! +//! This allows Pattern 4 to handle all continue patterns uniformly by +//! ensuring continue is always in the then branch. + +use crate::ast::{ASTNode, UnaryOperator}; + +pub struct ContinueBranchNormalizer; + +impl ContinueBranchNormalizer { + /// Normalize if node for continue handling + /// + /// Returns transformed AST node if pattern matches, otherwise returns clone of input. + /// + /// # Pattern Detection + /// + /// Matches: `if (cond) { body } else { continue }` + /// Where else_body is a single Continue statement (or block containing only Continue). + /// + /// # Transformation + /// + /// - Creates negated condition: `!cond` + /// - Swaps branches: then becomes else, continue becomes then + /// - Result: `if (!cond) { continue } else { body }` + /// + /// # Examples + /// + /// ```text + /// Input: if (i != M) { sum = sum + i } else { continue } + /// Output: if (!(i != M)) { continue } else { sum = sum + i } + /// ``` + pub fn normalize_if_for_continue(if_node: &ASTNode) -> ASTNode { + // Check if this is an If node with else branch containing only continue + match if_node { + ASTNode::If { + condition, + then_body, + else_body, + span, + } => { + // Check if else_body is a single continue statement + if let Some(else_stmts) = else_body { + if Self::is_continue_only(else_stmts) { + // Pattern matched: if (cond) { ... } else { continue } + // Transform to: if (!cond) { continue } else { ... } + + eprintln!("[continue_normalizer] Pattern matched: else-continue detected"); + eprintln!("[continue_normalizer] Original condition: {:?}", condition); + + // Create negated condition: !cond + let negated_cond = Box::new(ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand: condition.clone(), + span: span.clone(), + }); + + eprintln!("[continue_normalizer] Negated condition created"); + + // Swap branches: continue becomes then, body becomes else + let result = ASTNode::If { + condition: negated_cond, + then_body: else_stmts.clone(), // Continue + else_body: Some(then_body.clone()), // Original body + span: span.clone(), + }; + + eprintln!("[continue_normalizer] Transformation complete"); + return result; + } + } + + // Pattern not matched: return original if unchanged + if_node.clone() + } + _ => { + // Not an if node: return as-is + if_node.clone() + } + } + } + + /// Check if statements contain only a continue statement + /// + /// Returns true if: + /// - Single statement: Continue + /// - Or statements that effectively contain only continue + fn is_continue_only(stmts: &[ASTNode]) -> bool { + if stmts.is_empty() { + return false; + } + + // Check for single continue + if stmts.len() == 1 { + matches!(stmts[0], ASTNode::Continue { .. }) + } else { + // For now, only handle single continue statement + // Could be extended to handle blocks with continue as last statement + false + } + } + + /// Check if a loop body contains an if-else pattern with else-continue + /// + /// This is used by the pattern router to detect Pattern B cases. + pub fn has_else_continue_pattern(body: &[ASTNode]) -> bool { + for node in body { + if let ASTNode::If { else_body, .. } = node { + if let Some(else_stmts) = else_body { + if Self::is_continue_only(else_stmts) { + return true; + } + } + } + } + false + } + + /// Normalize all if-statements in a loop body for continue handling + /// + /// This is called by the loop lowerer before pattern matching. + /// It transforms all else-continue patterns to then-continue patterns. + pub fn normalize_loop_body(body: &[ASTNode]) -> Vec { + body.iter() + .map(|node| Self::normalize_if_for_continue(node)) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::{BinaryOperator, LiteralValue, Span}; + + #[test] + fn test_normalize_else_continue() { + // if (i != M) { sum = sum + i } else { continue } + // → if (!(i != M)) { continue } else { sum = sum + i } + + let span = Span::default(); + let condition = Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::NotEqual, + left: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: span.clone(), + }), + right: Box::new(ASTNode::Variable { + name: "M".to_string(), + span: span.clone(), + }), + span: span.clone(), + }); + + let then_body = vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "sum".to_string(), + span: span.clone(), + }), + value: Box::new(ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left: Box::new(ASTNode::Variable { + name: "sum".to_string(), + span: span.clone(), + }), + right: Box::new(ASTNode::Variable { + name: "i".to_string(), + span: span.clone(), + }), + span: span.clone(), + }), + span: span.clone(), + }]; + + let else_body = Some(vec![ASTNode::Continue { span: span.clone() }]); + + let input = ASTNode::If { + condition, + then_body, + else_body, + span: span.clone(), + }; + + let result = ContinueBranchNormalizer::normalize_if_for_continue(&input); + + // Verify result is an If node + if let ASTNode::If { + condition: result_cond, + then_body: result_then, + else_body: result_else, + .. + } = result + { + // Condition should be negated (UnaryOp Not) + assert!(matches!( + *result_cond, + ASTNode::UnaryOp { + operator: UnaryOperator::Not, + .. + } + )); + + // Then body should be continue + assert_eq!(result_then.len(), 1); + assert!(matches!(result_then[0], ASTNode::Continue { .. })); + + // Else body should be original then body + assert!(result_else.is_some()); + let else_stmts = result_else.unwrap(); + assert_eq!(else_stmts.len(), 1); + assert!(matches!(else_stmts[0], ASTNode::Assignment { .. })); + } else { + panic!("Expected If node"); + } + } + + #[test] + fn test_no_op_then_continue() { + // if (i != M) { continue } else { sum = sum + i } + // Should NOT transform (continue is in then branch) + + let span = Span::default(); + let condition = Box::new(ASTNode::Variable { + name: "cond".to_string(), + span: span.clone(), + }); + + let then_body = vec![ASTNode::Continue { span: span.clone() }]; + let else_body = Some(vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + }), + value: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: span.clone(), + }), + span: span.clone(), + }]); + + let input = ASTNode::If { + condition: condition.clone(), + then_body: then_body.clone(), + else_body: else_body.clone(), + span: span.clone(), + }; + + let result = ContinueBranchNormalizer::normalize_if_for_continue(&input); + + // Should return unchanged (continue already in then) + // We can't use PartialEq on ASTNode due to Span, so check structure + if let ASTNode::If { + condition: result_cond, + then_body: result_then, + .. + } = result + { + // Condition should NOT be negated + assert!(matches!(*result_cond, ASTNode::Variable { .. })); + + // Then should still be continue + assert_eq!(result_then.len(), 1); + assert!(matches!(result_then[0], ASTNode::Continue { .. })); + } else { + panic!("Expected If node"); + } + } + + #[test] + fn test_no_op_no_else() { + // if (i != M) { sum = sum + i } + // Should NOT transform (no else branch) + + let span = Span::default(); + let input = ASTNode::If { + condition: Box::new(ASTNode::Variable { + name: "cond".to_string(), + span: span.clone(), + }), + then_body: vec![ASTNode::Assignment { + target: Box::new(ASTNode::Variable { + name: "x".to_string(), + span: span.clone(), + }), + value: Box::new(ASTNode::Literal { + value: LiteralValue::Integer(1), + span: span.clone(), + }), + span: span.clone(), + }], + else_body: None, + span: span.clone(), + }; + + let result = ContinueBranchNormalizer::normalize_if_for_continue(&input); + + // Should return unchanged + if let ASTNode::If { else_body, .. } = result { + assert!(else_body.is_none()); + } else { + panic!("Expected If node"); + } + } + + #[test] + fn test_has_else_continue_pattern() { + let span = Span::default(); + + // Body with else-continue pattern + let body_with = vec![ASTNode::If { + condition: Box::new(ASTNode::Variable { + name: "cond".to_string(), + span: span.clone(), + }), + then_body: vec![], + else_body: Some(vec![ASTNode::Continue { span: span.clone() }]), + span: span.clone(), + }]; + + assert!(ContinueBranchNormalizer::has_else_continue_pattern( + &body_with + )); + + // Body without else-continue pattern + let body_without = vec![ASTNode::If { + condition: Box::new(ASTNode::Variable { + name: "cond".to_string(), + span: span.clone(), + }), + then_body: vec![ASTNode::Continue { span: span.clone() }], + else_body: None, + span: span.clone(), + }]; + + assert!(!ContinueBranchNormalizer::has_else_continue_pattern( + &body_without + )); + } +} diff --git a/src/mir/join_ir/lowering/loop_update_analyzer.rs b/src/mir/join_ir/lowering/loop_update_analyzer.rs index 05d1fe7c..811846d9 100644 --- a/src/mir/join_ir/lowering/loop_update_analyzer.rs +++ b/src/mir/join_ir/lowering/loop_update_analyzer.rs @@ -68,22 +68,49 @@ impl LoopUpdateAnalyzer { // Extract carrier names for quick lookup let carrier_names: Vec<&str> = carriers.iter().map(|c| c.name.as_str()).collect(); - // Scan all statements in the loop body - for node in body { - if let ASTNode::Assignment { target, value, .. } = node { - // Check if this is a carrier update (e.g., sum = sum + i) - if let Some(target_name) = Self::extract_variable_name(target) { - if carrier_names.contains(&target_name.as_str()) { - // This is a carrier update, analyze the RHS - if let Some(update_expr) = Self::analyze_update_value(&target_name, value) { - updates.insert(target_name, update_expr); + // Recursively scan all statements in the loop body + Self::scan_nodes(body, &carrier_names, &mut updates); + + updates + } + + /// Recursively scan AST nodes for carrier updates + /// + /// Phase 33-19: Extended to scan into if-else branches to handle + /// Pattern B (else-continue) after normalization. + fn scan_nodes( + nodes: &[ASTNode], + carrier_names: &[&str], + updates: &mut HashMap, + ) { + for node in nodes { + match node { + ASTNode::Assignment { target, value, .. } => { + // Check if this is a carrier update (e.g., sum = sum + i) + if let Some(target_name) = Self::extract_variable_name(target) { + if carrier_names.contains(&target_name.as_str()) { + // This is a carrier update, analyze the RHS + if let Some(update_expr) = Self::analyze_update_value(&target_name, value) { + updates.insert(target_name, update_expr); + } } } } + // Phase 33-19: Recursively scan if-else branches + ASTNode::If { + then_body, + else_body, + .. + } => { + Self::scan_nodes(then_body, carrier_names, updates); + if let Some(else_stmts) = else_body { + Self::scan_nodes(else_stmts, carrier_names, updates); + } + } + // Add more recursive cases as needed (loops, etc.) + _ => {} } } - - updates } /// Extract variable name from AST node (for assignment target) diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index 5bf8014f..54d83829 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -23,6 +23,7 @@ pub mod bool_expr_lowerer; // Phase 168: Boolean expression lowering for complex pub mod carrier_info; // Phase 196: Carrier metadata for loop lowering pub mod common; pub mod condition_to_joinir; // Phase 169: JoinIR condition lowering helper +pub mod continue_branch_normalizer; // Phase 33-19: Continue branch normalization for Pattern B pub mod loop_update_analyzer; // Phase 197: Update expression analyzer for carrier semantics pub mod loop_update_summary; // Phase 170-C-2: Update pattern summary for shape detection pub mod exit_args_resolver;