From e6aeccb793b62fc029c586e5e98518dac75286d2 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sat, 27 Dec 2025 16:20:11 +0900 Subject: [PATCH] loop_canonicalizer: split canonicalizer tests --- .../loop_canonicalizer/canonicalizer_tests.rs | 1432 ----------------- .../canonicalizer_tests/basic.rs | 13 + .../canonicalizer_tests/continue_pattern.rs | 128 ++ .../canonicalizer_tests/escape_skip.rs | 176 ++ .../canonicalizer_tests/mod.rs | 19 + .../canonicalizer_tests/parse_array.rs | 176 ++ .../canonicalizer_tests/parse_number.rs | 132 ++ .../canonicalizer_tests/parse_object.rs | 176 ++ .../canonicalizer_tests/parse_string.rs | 176 ++ .../canonicalizer_tests/skip_whitespace.rs | 308 ++++ .../canonicalizer_tests/trim_leading.rs | 90 ++ .../canonicalizer_tests/trim_trailing.rs | 90 ++ 12 files changed, 1484 insertions(+), 1432 deletions(-) delete mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/basic.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/continue_pattern.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/escape_skip.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/mod.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/parse_array.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/parse_number.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/parse_object.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/parse_string.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/skip_whitespace.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/trim_leading.rs create mode 100644 src/mir/loop_canonicalizer/canonicalizer_tests/trim_trailing.rs diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests.rs b/src/mir/loop_canonicalizer/canonicalizer_tests.rs deleted file mode 100644 index 0c191c3c..00000000 --- a/src/mir/loop_canonicalizer/canonicalizer_tests.rs +++ /dev/null @@ -1,1432 +0,0 @@ -//! Tests for loop canonicalizer -//! -//! Separated from canonicalizer.rs for better maintainability. - -use super::canonicalizer::canonicalize_loop_expr; -use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; -use crate::mir::loop_pattern_detection::LoopPatternKind; -use super::skeleton_types::{CarrierRole, SkeletonStep, UpdateKind}; - -#[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"); -} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/basic.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/basic.rs new file mode 100644 index 00000000..0f51c8df --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/basic.rs @@ -0,0 +1,13 @@ +use super::*; + +#[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")); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/continue_pattern.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/continue_pattern.rs new file mode 100644 index 00000000..96f465f6 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/continue_pattern.rs @@ -0,0 +1,128 @@ +use super::*; + +#[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); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/escape_skip.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/escape_skip.rs new file mode 100644 index 00000000..76ec86f0 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/escape_skip.rs @@ -0,0 +1,176 @@ +use super::*; + +#[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"); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/mod.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/mod.rs new file mode 100644 index 00000000..4d2b6e1c --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/mod.rs @@ -0,0 +1,19 @@ +//! Tests for loop canonicalizer +//! +//! Separated from canonicalizer.rs for better maintainability. + +use super::canonicalizer::canonicalize_loop_expr; +use super::skeleton_types::{CarrierRole, SkeletonStep, UpdateKind}; +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span}; +use crate::mir::loop_pattern_detection::LoopPatternKind; + +mod basic; +mod skip_whitespace; +mod trim_leading; +mod continue_pattern; +mod trim_trailing; +mod parse_string; +mod parse_array; +mod parse_object; +mod parse_number; +mod escape_skip; diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/parse_array.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_array.rs new file mode 100644 index 00000000..96bcf51d --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_array.rs @@ -0,0 +1,176 @@ +use super::*; + +#[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); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/parse_number.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_number.rs new file mode 100644 index 00000000..7af422c8 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_number.rs @@ -0,0 +1,132 @@ +use super::*; + +#[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); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/parse_object.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_object.rs new file mode 100644 index 00000000..9f742046 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_object.rs @@ -0,0 +1,176 @@ +use super::*; + +#[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); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/parse_string.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_string.rs new file mode 100644 index 00000000..26fc6af2 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/parse_string.rs @@ -0,0 +1,176 @@ +use super::*; + +#[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); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/skip_whitespace.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/skip_whitespace.rs new file mode 100644 index 00000000..f617df21 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/skip_whitespace.rs @@ -0,0 +1,308 @@ +use super::*; + +#[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![ + 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(), + }, + 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()); + assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break)); + assert!(decision.missing_caps.is_empty()); + + // Verify skeleton structure + 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 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_fails_without_else() { + // Build pattern with missing else: loop(p < len) { if is_ws == 1 { 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![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: 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()); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/trim_leading.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/trim_leading.rs new file mode 100644 index 00000000..5203f9a3 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/trim_leading.rs @@ -0,0 +1,90 @@ +use super::*; + +#[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); +} diff --git a/src/mir/loop_canonicalizer/canonicalizer_tests/trim_trailing.rs b/src/mir/loop_canonicalizer/canonicalizer_tests/trim_trailing.rs new file mode 100644 index 00000000..189ef740 --- /dev/null +++ b/src/mir/loop_canonicalizer/canonicalizer_tests/trim_trailing.rs @@ -0,0 +1,90 @@ +use super::*; + +#[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); +}