//! Tests for loop canonicalizer //! //! Separated from canonicalizer.rs for better maintainability. use super::canonicalizer::canonicalize_loop_expr; use super::skeleton_types::{CarrierRole, LoopPatternKind, SkeletonStep, UpdateKind}; use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; #[test] fn test_canonicalize_rejects_non_loop() { let not_loop = ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown(), }; let result = canonicalize_loop_expr(¬_loop); assert!(result.is_err()); assert!(result.unwrap_err().contains("Expected Loop node")); } #[test] fn test_skip_whitespace_pattern_recognition() { // Build skip_whitespace pattern: loop(p < len) { if is_ws == 1 { p = p + 1 } else { break } } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "is_ws".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: Some(vec![ASTNode::Break { span: Span::unknown(), }]), span: Span::unknown(), }], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // Phase 137-5: Pattern choice reflects ExitContract (has_break=true → Pattern2Break) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break)); assert_eq!(decision.missing_caps.len(), 0); // Verify skeleton structure assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "p"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(skeleton.exits.has_break); assert!(!skeleton.exits.has_continue); assert!(!skeleton.exits.has_return); assert!(!skeleton.exits.break_has_value); } #[test] fn test_skip_whitespace_with_body_statements() { // Build pattern with body statements before the if: // loop(p < len) { // local ch = get_char(p) // if is_ws { p = p + 1 } else { break } // } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // Body statement ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::FunctionCall { name: "get_char".to_string(), arguments: vec![ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }], span: Span::unknown(), }), span: Span::unknown(), }, // The if-else pattern ASTNode::If { condition: Box::new(ASTNode::Variable { name: "is_ws".to_string(), span: Span::unknown(), }), then_body: vec![ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: Some(vec![ASTNode::Break { span: Span::unknown(), }]), span: Span::unknown(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // Phase 137-5: Pattern choice reflects ExitContract (has_break=true → Pattern2Break) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break)); // Verify skeleton has Body step assert_eq!(skeleton.steps.len(), 3); // HeaderCond + Body + Update assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); assert!(matches!(skeleton.steps[1], SkeletonStep::Body { .. })); assert!(matches!(skeleton.steps[2], SkeletonStep::Update { .. })); // Verify body contains 1 statement match &skeleton.steps[1] { SkeletonStep::Body { stmts } => assert_eq!(stmts.len(), 1), _ => panic!("Expected Body step"), } } #[test] fn test_skip_whitespace_fails_without_else() { // Build pattern without else branch (should fail) let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown(), }), body: vec![ASTNode::If { condition: Box::new(ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown(), }), then_body: vec![ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: None, // No else branch span: Span::unknown(), }], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (_, decision) = result.unwrap(); assert!(decision.is_fail_fast()); assert!(decision.notes[0].contains("Loop does not match")); assert!(decision.notes[0].contains("skip_whitespace")); } #[test] fn test_skip_whitespace_fails_with_wrong_delta() { // Build pattern with wrong update (p = p * 2, not +/-) let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown(), }), body: vec![ASTNode::If { condition: Box::new(ASTNode::Literal { value: LiteralValue::Bool(true), span: Span::unknown(), }), then_body: vec![ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Multiply, // Wrong operator left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(2), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: Some(vec![ASTNode::Break { span: Span::unknown(), }]), span: Span::unknown(), }], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (_, decision) = result.unwrap(); assert!(decision.is_fail_fast()); } #[test] fn test_trim_leading_pattern_recognized() { // Phase 142 P0: Test trim_leading pattern (start = start + 1) // Build: loop(start < end) { if is_ws { start = start + 1 } else { break } } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "start".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "is_ws".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "start".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "start".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: Some(vec![ASTNode::Break { span: Span::unknown(), }]), span: Span::unknown(), }], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern2Break (ExitContract priority) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "start"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(skeleton.exits.has_break); assert!(!skeleton.exits.has_continue); assert!(!skeleton.exits.has_return); } #[test] fn test_simple_continue_pattern_recognized() { // Phase 142 P1: Test simple continue pattern // Build: loop(i < n) { if is_even { i = i + 1; continue } sum = sum + i; i = i + 1 } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "n".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // if is_even { i = i + 1; continue } ASTNode::If { condition: Box::new(ASTNode::Variable { name: "is_even".to_string(), span: Span::unknown(), }), then_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(), }, ASTNode::Continue { span: Span::unknown(), }, ], else_body: None, span: Span::unknown(), }, // sum = sum + i ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "sum".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "sum".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, // i = i + 1 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(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern4Continue assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure // HeaderCond + Body (sum = sum + i) + Update assert!(skeleton.steps.len() >= 2); assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "i"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(!skeleton.exits.has_break); assert!(skeleton.exits.has_continue); assert!(!skeleton.exits.has_return); } #[test] fn test_trim_trailing_pattern_recognized() { // Phase 142 P0: Test trim_trailing pattern (end = end - 1) // Build: loop(end > start) { if is_ws { end = end - 1 } else { break } } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Greater, left: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "start".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "is_ws".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Subtract, left: Box::new(ASTNode::Variable { name: "end".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: Some(vec![ASTNode::Break { span: Span::unknown(), }]), span: Span::unknown(), }], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern2Break (ExitContract priority) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "end"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, -1), // Negative step _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(skeleton.exits.has_break); assert!(!skeleton.exits.has_continue); assert!(!skeleton.exits.has_return); } #[test] fn test_parse_string_pattern_recognized() { // Phase 143-P1: Test parse_string pattern (both continue AND return) // Build: loop(p < len) { // local ch = s.substring(p, p + 1) // if ch == "\"" { return 0 } // if ch == "\\" { p = p + 1; continue } // p = p + 1 // } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // Body statement: local ch = s.substring(p, p + 1) ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::FunctionCall { name: "substring".to_string(), arguments: vec![ ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }, ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }, ], span: Span::unknown(), }), span: Span::unknown(), }, // Return check: if ch == "\"" { return 0 } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("\"".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Return { value: Some(Box::new(ASTNode::Literal { value: LiteralValue::Integer(0), span: Span::unknown(), })), span: Span::unknown(), }], else_body: None, span: Span::unknown(), }, // Escape check: if ch == "\\" { p = p + 1; continue } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("\\".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, ASTNode::Continue { span: Span::unknown(), }, ], else_body: None, span: Span::unknown(), }, // Carrier update: p = p + 1 ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern4Continue (has both continue and return) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure // HeaderCond + Body (ch assignment) + Update assert!(skeleton.steps.len() >= 2); assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "p"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(!skeleton.exits.has_break); assert!(skeleton.exits.has_continue); assert!(skeleton.exits.has_return); assert!(!skeleton.exits.break_has_value); } #[test] fn test_parse_array_pattern_recognized() { // Phase 143-P2: Test parse_array pattern (both continue AND return) // Build: loop(p < len) { // local ch = s.substring(p, p + 1) // if ch == "]" { return 0 } // if ch == "," { p = p + 1; continue } // p = p + 1 // } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // Body statement: local ch = s.substring(p, p + 1) ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::FunctionCall { name: "substring".to_string(), arguments: vec![ ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }, ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }, ], span: Span::unknown(), }), span: Span::unknown(), }, // Stop check: if ch == "]" { return 0 } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("]".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Return { value: Some(Box::new(ASTNode::Literal { value: LiteralValue::Integer(0), span: Span::unknown(), })), span: Span::unknown(), }], else_body: None, span: Span::unknown(), }, // Separator check: if ch == "," { p = p + 1; continue } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String(",".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, ASTNode::Continue { span: Span::unknown(), }, ], else_body: None, span: Span::unknown(), }, // Regular update: p = p + 1 ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern4Continue (has both continue and return) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure // HeaderCond + Body (ch assignment) + Update assert!(skeleton.steps.len() >= 2); assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "p"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(!skeleton.exits.has_break); assert!(skeleton.exits.has_continue); assert!(skeleton.exits.has_return); assert!(!skeleton.exits.break_has_value); } #[test] fn test_parse_object_pattern_recognized() { // Phase 143-P3: Test parse_object pattern (same structure as parse_array) // Build: loop(p < len) { // local ch = s.substring(p, p + 1) // if ch == "}" { return 0 } // if ch == "," { p = p + 1; continue } // p = p + 1 // } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // Body statement: local ch = s.substring(p, p + 1) ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::FunctionCall { name: "substring".to_string(), arguments: vec![ ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }, ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }, ], span: Span::unknown(), }), span: Span::unknown(), }, // Stop check: if ch == "}" { return 0 } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("}".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Return { value: Some(Box::new(ASTNode::Literal { value: LiteralValue::Integer(0), span: Span::unknown(), })), span: Span::unknown(), }], else_body: None, span: Span::unknown(), }, // Separator check: if ch == "," { p = p + 1; continue } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String(",".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, ASTNode::Continue { span: Span::unknown(), }, ], else_body: None, span: Span::unknown(), }, // Regular update: p = p + 1 ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "p".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(1), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern4Continue (has both continue and return) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure // HeaderCond + Body (ch assignment) + Update assert!(skeleton.steps.len() >= 2); assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "p"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(!skeleton.exits.has_break); assert!(skeleton.exits.has_continue); assert!(skeleton.exits.has_return); assert!(!skeleton.exits.break_has_value); } #[test] fn test_parse_number_pattern_recognized() { // Phase 143-P0: Test parse_number pattern (break in THEN clause) // Build: loop(i < len) { digit_pos = digits.indexOf(ch); if digit_pos < 0 { break } result = result + ch; i = i + 1 } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // Body statement: digit_pos = digits.indexOf(ch) ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "digit_pos".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::FunctionCall { name: "indexOf".to_string(), arguments: vec![ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }], span: Span::unknown(), }), span: Span::unknown(), }, // Break check: if digit_pos < 0 { break } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "digit_pos".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::Integer(0), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Break { span: Span::unknown(), }], else_body: None, // No else branch span: Span::unknown(), }, // Rest: result = result + ch ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "result".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Add, left: Box::new(ASTNode::Variable { name: "result".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }, // Carrier update: i = i + 1 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(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok()); let (skeleton, decision) = result.unwrap(); // Verify success assert!(decision.is_success()); // chosen == Pattern2Break (has_break=true) assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break)); // missing_caps == [] assert!(decision.missing_caps.is_empty()); // Verify skeleton structure // HeaderCond + Body (digit_pos assignment) + Body (result assignment) + Update assert!(skeleton.steps.len() >= 3); assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. })); // Verify carrier assert_eq!(skeleton.carriers.len(), 1); assert_eq!(skeleton.carriers[0].name, "i"); assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter); match &skeleton.carriers[0].update_kind { UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1), _ => panic!("Expected ConstStep update"), } // Verify exit contract assert!(skeleton.exits.has_break); assert!(!skeleton.exits.has_continue); assert!(!skeleton.exits.has_return); assert!(!skeleton.exits.break_has_value); } #[test] fn test_escape_skip_pattern_recognition() { // Phase 91 P5b: Escape sequence handling pattern // Build: loop(i < len) { // ch = get_char(i) // if ch == "\"" { break } // if ch == "\\" { i = i + 2 } else { i = i + 1 } // } let loop_node = ASTNode::Loop { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Less, left: Box::new(ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Variable { name: "len".to_string(), span: Span::unknown(), }), span: Span::unknown(), }), body: vec![ // Body: ch = get_char(i) ASTNode::Assignment { target: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), value: Box::new(ASTNode::FunctionCall { name: "get_char".to_string(), arguments: vec![ASTNode::Variable { name: "i".to_string(), span: Span::unknown(), }], span: Span::unknown(), }), span: Span::unknown(), }, // Break check: if ch == "\"" { break } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("\"".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_body: vec![ASTNode::Break { span: Span::unknown(), }], else_body: None, span: Span::unknown(), }, // Escape check: if ch == "\\" { i = i + 2 } else { i = i + 1 } ASTNode::If { condition: Box::new(ASTNode::BinaryOp { operator: BinaryOperator::Equal, left: Box::new(ASTNode::Variable { name: "ch".to_string(), span: Span::unknown(), }), right: Box::new(ASTNode::Literal { value: LiteralValue::String("\\".to_string()), span: Span::unknown(), }), span: Span::unknown(), }), then_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(2), span: Span::unknown(), }), span: Span::unknown(), }), span: Span::unknown(), }], else_body: Some(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(), }]), span: Span::unknown(), }, ], span: Span::unknown(), }; let result = canonicalize_loop_expr(&loop_node); assert!(result.is_ok(), "Escape pattern canonicalization should succeed"); let (skeleton, decision) = result.unwrap(); // Verify decision success assert!(decision.is_success(), "Decision should indicate success"); assert_eq!( decision.chosen, Some(LoopPatternKind::Pattern2Break), "P5b should route to Pattern2Break (has_break=true)" ); assert!(decision.missing_caps.is_empty(), "No missing capabilities"); // Verify skeleton structure // Expected: HeaderCond + Body + Update assert!( skeleton.steps.len() >= 3, "Expected at least 3 steps: HeaderCond, Body, Update" ); assert!( matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }), "First step should be HeaderCond" ); assert!( matches!(skeleton.steps[skeleton.steps.len() - 1], SkeletonStep::Update { .. }), "Last step should be Update" ); // Verify carrier (counter variable "i") assert_eq!(skeleton.carriers.len(), 1, "Should have 1 carrier"); let carrier = &skeleton.carriers[0]; assert_eq!(carrier.name, "i", "Carrier should be named 'i'"); assert_eq!(carrier.role, CarrierRole::Counter, "Carrier should be a Counter"); // Verify ConditionalStep with escape_delta=2, normal_delta=1 // Phase 92 P0-3: ConditionalStep now includes cond match &carrier.update_kind { UpdateKind::ConditionalStep { cond: _, // Phase 92 P0-3: Condition for Select (don't check exact AST) then_delta, else_delta, } => { assert_eq!(*then_delta, 2, "Escape delta (then) should be 2"); assert_eq!(*else_delta, 1, "Normal delta (else) should be 1"); } other => panic!( "Expected ConditionalStep, got {:?}", other ), } // Verify exit contract (P5b has break for string boundary) assert!(skeleton.exits.has_break, "P5b should have break"); assert!(!skeleton.exits.has_continue, "P5b should not have continue"); assert!(!skeleton.exits.has_return, "P5b should not have return"); assert!(!skeleton.exits.break_has_value, "Break should not have value"); }