Files
hakorune/src/mir/loop_canonicalizer/canonicalizer_tests.rs
tomoaki cb8a6984d4 refactor: modularize shape_guard.rs and separate canonicalizer tests
shape_guard.rs (1401 lines → 6 files):
- pattern2.rs: Break-loop patterns (132 lines)
- pattern3.rs: If-sum patterns (67 lines)
- pattern4.rs: Continue patterns (152 lines)
- selfhost.rs: Selfhost-specific detectors (245 lines)
- utils.rs: Shared helpers (33 lines)
- mod.rs: API coordination & tests (776 lines)

canonicalizer.rs (1863 lines → 433 lines):
- Separated 1430 lines of tests to canonicalizer_tests.rs

Benefits:
- Improved maintainability (smaller, focused files)
- Clear separation of concerns (pattern families)
- Preserved API compatibility (re-exports)
- No new warnings

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 07:29:26 +09:00

1432 lines
54 KiB
Rust

//! Tests for loop canonicalizer
//!
//! Separated from canonicalizer.rs for better maintainability.
use super::canonicalizer::canonicalize_loop_expr;
use super::skeleton_types::{CarrierRole, LoopPatternKind, SkeletonStep, UpdateKind};
use crate::ast::{ASTNode, BinaryOperator, LiteralValue, Span};
#[test]
fn test_canonicalize_rejects_non_loop() {
let not_loop = ASTNode::Literal {
value: LiteralValue::Bool(true),
span: Span::unknown(),
};
let result = canonicalize_loop_expr(&not_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");
}