1433 lines
54 KiB
Rust
1433 lines
54 KiB
Rust
//! 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");
|
|
}
|