loop_canonicalizer: split canonicalizer tests

This commit is contained in:
2025-12-27 16:20:11 +09:00
parent 7be0b0c28e
commit e6aeccb793
12 changed files with 1484 additions and 1432 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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(&not_loop);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Expected Loop node"));
}

View File

@ -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);
}

View File

@ -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");
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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());
}

View File

@ -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);
}

View File

@ -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);
}