#![allow(clippy::unnecessary_wraps)] use crate::backend::Transpiler;
use crate::frontend::ast::{BinaryOp, Expr, ExprKind, Literal, StringPart, UnaryOp};
use crate::frontend::{Parser, RecoveryParser};
#[allow(unused_imports)]
use crate::testing::generators::{arb_expr, arb_well_typed_expr};
use proptest::prelude::*;
use proptest::test_runner::TestCaseError;
pub fn prop_parser_never_panics(input: &str) -> Result<(), TestCaseError> {
let mut parser = Parser::new(input);
let _ = parser.parse();
Ok(())
}
pub fn prop_recovery_parser_always_produces_ast(input: &str) -> Result<(), TestCaseError> {
let mut parser = RecoveryParser::new(input);
let result = parser.parse_with_recovery();
if !input.trim().is_empty() {
prop_assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Recovery parser should produce AST or errors for non-empty input"
);
}
Ok(())
}
pub fn prop_transpilation_preserves_structure(expr: &Expr) -> Result<(), TestCaseError> {
let mut transpiler = Transpiler::new();
if let Ok(rust_code) = transpiler.transpile(expr) {
let code_str = rust_code.to_string();
prop_assert!(!code_str.is_empty(), "Transpiled code should not be empty");
} else {
}
Ok(())
}
pub fn prop_string_interpolation_transpiles(parts: &[StringPart]) -> Result<(), TestCaseError> {
let transpiler = Transpiler::new();
let result = transpiler.transpile_string_interpolation(parts);
if let Ok(tokens) = result {
let code = tokens.to_string();
prop_assert!(
code.contains("format!")
|| code.contains("format !")
|| code.starts_with('"')
|| code.is_empty(),
"String interpolation should produce format! call or string literal, got: {}",
code
);
}
Ok(())
}
pub fn prop_parse_print_roundtrip(expr: &Expr) -> Result<(), TestCaseError> {
let mut transpiler = Transpiler::new();
let _ = transpiler.transpile(expr);
Ok(())
}
pub fn prop_well_typed_always_transpiles(expr: &Expr) -> Result<(), TestCaseError> {
let mut transpiler = Transpiler::new();
if is_well_typed(expr) {
match transpiler.transpile(expr) {
Ok(_) => Ok(()),
Err(e) => {
prop_assert!(
false,
"Well-typed expression failed to transpile: {:?}\nError: {}",
expr,
e
);
Ok(())
}
}
} else {
Ok(())
}
}
pub fn prop_recovery_handles_truncation(input: &str) -> Result<(), TestCaseError> {
if input.is_empty() {
return Ok(());
}
let max_test_length = 50; let test_input = if input.len() > max_test_length {
&input[..max_test_length]
} else {
input
};
for i in 0..test_input.len() {
let truncated = &test_input[..i];
let mut parser = RecoveryParser::new(truncated);
let result = parser.parse_with_recovery();
if !truncated.trim().is_empty() {
prop_assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Recovery parser should handle truncated input at position {i}"
);
}
}
Ok(())
}
fn is_well_typed(expr: &Expr) -> bool {
match &expr.kind {
ExprKind::Literal(_) | ExprKind::Identifier(_) => true,
ExprKind::Binary { left, right, op } => {
match op {
BinaryOp::Add | BinaryOp::Subtract | BinaryOp::Multiply | BinaryOp::Divide => {
is_numeric(left) && is_numeric(right)
}
BinaryOp::And | BinaryOp::Or => is_boolean(left) && is_boolean(right),
BinaryOp::Equal | BinaryOp::NotEqual => {
is_well_typed(left) && is_well_typed(right)
}
_ => is_well_typed(left) && is_well_typed(right),
}
}
ExprKind::Unary { operand, op } => match op {
UnaryOp::Not => is_boolean(operand),
UnaryOp::Negate | UnaryOp::BitwiseNot => is_numeric(operand),
UnaryOp::Reference | UnaryOp::MutableReference | UnaryOp::Deref => true, },
ExprKind::If {
condition,
then_branch,
else_branch,
} => {
is_boolean(condition)
&& is_well_typed(then_branch)
&& else_branch.as_ref().is_none_or(|e| is_well_typed(e))
}
_ => false, }
}
fn is_numeric(expr: &Expr) -> bool {
matches!(
&expr.kind,
ExprKind::Literal(Literal::Integer(_, _) | Literal::Float(_))
)
}
fn is_boolean(expr: &Expr) -> bool {
matches!(&expr.kind, ExprKind::Literal(Literal::Bool(_)))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
proptest! {
#![proptest_config(proptest::test_runner::Config::with_cases(100))]
#[test]
fn test_parser_never_panics(input in prop::string::string_regex("[a-zA-Z0-9 (){}\n]{0,50}").unwrap()) {
prop_parser_never_panics(&input)?;
}
#[test]
fn test_recovery_parser_always_produces_ast(input in prop::string::string_regex("[a-zA-Z0-9 (){}\n]{0,50}").unwrap()) {
prop_recovery_parser_always_produces_ast(&input)?;
}
#[test]
fn test_transpilation_preserves_structure(expr in arb_expr()) {
prop_transpilation_preserves_structure(&expr)?;
}
#[test]
fn test_well_typed_always_transpiles(expr in arb_well_typed_expr()) {
prop_well_typed_always_transpiles(&expr)?;
}
#[test]
fn test_recovery_handles_truncation(input in "[a-zA-Z0-9 +\\-*/()]{0,30}") {
prop_recovery_handles_truncation(&input)?;
}
#[test]
fn test_parse_print_roundtrip(expr in arb_well_typed_expr()) {
prop_parse_print_roundtrip(&expr)?;
}
}
#[test]
#[ignore = "Parser has infinite loop on certain inputs"]
fn test_specific_recovery_cases() {
let mut parser = RecoveryParser::new("let x =");
let result = parser.parse_with_recovery();
assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Failed: let x ="
);
let mut parser = RecoveryParser::new("if x >");
let result = parser.parse_with_recovery();
assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Failed: if x >"
);
let mut parser = RecoveryParser::new("fun foo(");
let result = parser.parse_with_recovery();
assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Failed: fun foo("
);
let mut parser = RecoveryParser::new("[1, 2,");
let result = parser.parse_with_recovery();
assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Failed: [1, 2,"
);
let mut parser = RecoveryParser::new("1 + + 2");
let result = parser.parse_with_recovery();
assert!(
result.ast.is_some() || !result.errors.is_empty(),
"Failed: 1 + + 2"
);
}
#[test]
fn test_prop_parser_never_panics_unit() {
let test_cases = vec![
"",
"42",
"hello",
"1 + 2",
"invalid syntax !@#$",
"let x = 42 in x",
"fun test() { }",
"if true { 1 } else { 2 }",
];
for input in test_cases {
let result = prop_parser_never_panics(input);
assert!(result.is_ok(), "Parser panicked on input: {input}");
}
}
#[test]
fn test_prop_recovery_parser_unit() {
let test_cases = vec![
("", false), ("42", true), ("1 + 2", true), ("invalid", true), ];
for (input, expect_output) in test_cases {
let result = prop_recovery_parser_always_produces_ast(input);
if expect_output {
assert!(result.is_ok(), "Recovery parser failed on: {input}");
} else {
assert!(result.is_ok(), "Recovery parser failed on empty input");
}
}
}
#[test]
fn test_prop_transpilation_unit() {
let test_exprs = vec![
Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
),
Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default()),
Expr::new(ExprKind::Identifier("x".to_string()), Default::default()),
];
for expr in test_exprs {
let result = prop_transpilation_preserves_structure(&expr);
assert!(result.is_ok(), "Transpilation property failed");
}
}
#[test]
fn test_prop_string_interpolation_unit() {
let test_cases = [
vec![], vec![StringPart::Text("hello".to_string())], vec![StringPart::Expr(Box::new(Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
)))], ];
for (i, parts) in test_cases.iter().enumerate() {
println!("Testing case {i}: {parts:?}");
let transpiler = Transpiler::new();
let result = transpiler.transpile_string_interpolation(parts);
match result {
Ok(tokens) => {
let code = tokens.to_string();
println!("Generated code: {code}");
assert!(
code.contains("format!")
|| code.contains("format !")
|| code.starts_with('"')
|| code.is_empty(),
"String interpolation should produce format! call or string literal, got: {code}"
);
}
Err(e) => {
println!("Transpilation error (acceptable): {e:?}");
}
}
}
}
#[test]
fn test_is_well_typed_function() {
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
assert!(is_well_typed(&int_expr));
let float_expr = Expr::new(ExprKind::Literal(Literal::Float(3.15)), Default::default());
assert!(is_well_typed(&float_expr));
let bool_expr = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
assert!(is_well_typed(&bool_expr));
let id_expr = Expr::new(ExprKind::Identifier("x".to_string()), Default::default());
assert!(is_well_typed(&id_expr));
}
#[test]
fn test_is_numeric_function() {
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
assert!(is_numeric(&int_expr));
let float_expr = Expr::new(ExprKind::Literal(Literal::Float(3.15)), Default::default());
assert!(is_numeric(&float_expr));
let bool_expr = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
assert!(!is_numeric(&bool_expr));
let string_expr = Expr::new(
ExprKind::Literal(Literal::String("hello".to_string())),
Default::default(),
);
assert!(!is_numeric(&string_expr));
}
#[test]
fn test_is_boolean_function() {
let bool_expr = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
assert!(is_boolean(&bool_expr));
let bool_false_expr =
Expr::new(ExprKind::Literal(Literal::Bool(false)), Default::default());
assert!(is_boolean(&bool_false_expr));
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
assert!(!is_boolean(&int_expr));
let string_expr = Expr::new(
ExprKind::Literal(Literal::String("hello".to_string())),
Default::default(),
);
assert!(!is_boolean(&string_expr));
}
#[test]
fn test_well_typed_binary_expressions() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
);
let add_expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Add,
right: Box::new(right),
},
Default::default(),
);
assert!(is_well_typed(&add_expr));
let bool_left = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let bool_right = Expr::new(ExprKind::Literal(Literal::Bool(false)), Default::default());
let boolean_and_expr = Expr::new(
ExprKind::Binary {
left: Box::new(bool_left),
op: BinaryOp::And,
right: Box::new(bool_right),
},
Default::default(),
);
assert!(is_well_typed(&boolean_and_expr));
}
#[test]
fn test_well_typed_unary_expressions() {
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let neg_expr = Expr::new(
ExprKind::Unary {
operand: Box::new(int_expr),
op: UnaryOp::Negate,
},
Default::default(),
);
assert!(is_well_typed(&neg_expr));
let bool_expr = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let not_expr = Expr::new(
ExprKind::Unary {
operand: Box::new(bool_expr),
op: UnaryOp::Not,
},
Default::default(),
);
assert!(is_well_typed(¬_expr));
}
#[test]
fn test_well_typed_if_expressions() {
let condition = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let then_branch = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let else_branch = Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
);
let if_expr = Expr::new(
ExprKind::If {
condition: Box::new(condition),
then_branch: Box::new(then_branch),
else_branch: Some(Box::new(else_branch)),
},
Default::default(),
);
assert!(is_well_typed(&if_expr));
}
#[test]
fn test_ill_typed_expressions() {
let bool_expr = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let bad_add = Expr::new(
ExprKind::Binary {
left: Box::new(bool_expr),
op: BinaryOp::Add,
right: Box::new(int_expr),
},
Default::default(),
);
assert!(!is_well_typed(&bad_add));
}
#[test]
fn test_parser_with_edge_cases() {
let edge_cases = vec![
"\0", "\n\n\n", " ", "\t\t\t", "\"", "(", "}", ];
for case in edge_cases {
let result = prop_parser_never_panics(case);
assert!(result.is_ok(), "Parser should not panic on: {case:?}");
}
}
#[test]
fn test_transpilation_with_complex_expressions() {
let complex_exprs = vec![
Expr::new(
ExprKind::Binary {
left: Box::new(Expr::new(
ExprKind::Binary {
left: Box::new(Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
)),
op: BinaryOp::Add,
right: Box::new(Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
)),
},
Default::default(),
)),
op: BinaryOp::Multiply,
right: Box::new(Expr::new(
ExprKind::Literal(Literal::Integer(3, None)),
Default::default(),
)),
},
Default::default(),
),
];
for expr in complex_exprs {
let result = prop_transpilation_preserves_structure(&expr);
assert!(
result.is_ok(),
"Transpilation should handle complex expressions"
);
}
}
#[test]
fn test_string_interpolation_edge_cases() {
let edge_cases = vec![
vec![StringPart::Text(String::new())],
vec![
StringPart::Text("Hello ".to_string()),
StringPart::Expr(Box::new(Expr::new(
ExprKind::Identifier("name".to_string()),
Default::default(),
))),
StringPart::Text("!".to_string()),
],
];
for parts in edge_cases {
let result = prop_string_interpolation_transpiles(&parts);
assert!(
result.is_ok(),
"String interpolation should handle edge cases"
);
}
}
#[test]
fn test_property_functions_return_ok() {
assert!(prop_parser_never_panics("42").is_ok());
assert!(prop_recovery_parser_always_produces_ast("42").is_ok());
let simple_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
assert!(prop_transpilation_preserves_structure(&simple_expr).is_ok());
let simple_parts = vec![StringPart::Text("hello".to_string())];
assert!(prop_string_interpolation_transpiles(&simple_parts).is_ok());
}
#[test]
fn test_prop_recovery_handles_truncation_empty() {
let result = prop_recovery_handles_truncation("");
assert!(result.is_ok());
}
#[test]
fn test_prop_recovery_handles_truncation_short() {
let result = prop_recovery_handles_truncation("1+2");
assert!(result.is_ok());
}
#[test]
fn test_prop_recovery_handles_truncation_long() {
let long_input = "a".repeat(100);
let result = prop_recovery_handles_truncation(&long_input);
assert!(result.is_ok());
}
#[test]
fn test_well_typed_with_subtract() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(10, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(3, None)),
Default::default(),
);
let sub_expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Subtract,
right: Box::new(right),
},
Default::default(),
);
assert!(is_well_typed(&sub_expr));
}
#[test]
fn test_well_typed_with_divide() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(20, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(4, None)),
Default::default(),
);
let div_expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Divide,
right: Box::new(right),
},
Default::default(),
);
assert!(is_well_typed(&div_expr));
}
#[test]
fn test_well_typed_with_or() {
let left = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let right = Expr::new(ExprKind::Literal(Literal::Bool(false)), Default::default());
let or_expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Or,
right: Box::new(right),
},
Default::default(),
);
assert!(is_well_typed(&or_expr));
}
#[test]
fn test_well_typed_with_equal() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(5, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(5, None)),
Default::default(),
);
let eq_expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Equal,
right: Box::new(right),
},
Default::default(),
);
assert!(is_well_typed(&eq_expr));
}
#[test]
fn test_well_typed_with_not_equal() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(5, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(10, None)),
Default::default(),
);
let neq_expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::NotEqual,
right: Box::new(right),
},
Default::default(),
);
assert!(is_well_typed(&neq_expr));
}
#[test]
fn test_well_typed_with_bitwise_not() {
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let bnot_expr = Expr::new(
ExprKind::Unary {
operand: Box::new(int_expr),
op: UnaryOp::BitwiseNot,
},
Default::default(),
);
assert!(is_well_typed(&bnot_expr));
}
#[test]
fn test_well_typed_with_reference() {
let expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let ref_expr = Expr::new(
ExprKind::Unary {
operand: Box::new(expr),
op: UnaryOp::Reference,
},
Default::default(),
);
assert!(is_well_typed(&ref_expr));
}
#[test]
fn test_well_typed_with_mutable_reference() {
let expr = Expr::new(ExprKind::Identifier("x".to_string()), Default::default());
let mut_ref_expr = Expr::new(
ExprKind::Unary {
operand: Box::new(expr),
op: UnaryOp::MutableReference,
},
Default::default(),
);
assert!(is_well_typed(&mut_ref_expr));
}
#[test]
fn test_well_typed_with_deref() {
let expr = Expr::new(ExprKind::Identifier("ptr".to_string()), Default::default());
let deref_expr = Expr::new(
ExprKind::Unary {
operand: Box::new(expr),
op: UnaryOp::Deref,
},
Default::default(),
);
assert!(is_well_typed(&deref_expr));
}
#[test]
fn test_well_typed_if_no_else() {
let condition = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let then_branch = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let if_no_else = Expr::new(
ExprKind::If {
condition: Box::new(condition),
then_branch: Box::new(then_branch),
else_branch: None,
},
Default::default(),
);
assert!(is_well_typed(&if_no_else));
}
#[test]
fn test_is_well_typed_string_literal() {
let str_expr = Expr::new(
ExprKind::Literal(Literal::String("hello".to_string())),
Default::default(),
);
assert!(is_well_typed(&str_expr));
}
#[test]
fn test_is_well_typed_char_literal() {
let char_expr = Expr::new(ExprKind::Literal(Literal::Char('a')), Default::default());
assert!(is_well_typed(&char_expr));
}
#[test]
fn test_is_well_typed_unit_literal() {
let unit_expr = Expr::new(ExprKind::Literal(Literal::Unit), Default::default());
assert!(is_well_typed(&unit_expr));
}
#[test]
fn test_ill_typed_and_with_int() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
);
let bad_and = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::And,
right: Box::new(right),
},
Default::default(),
);
assert!(!is_well_typed(&bad_and));
}
#[test]
fn test_ill_typed_or_with_int() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
);
let bad_or = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Or,
right: Box::new(right),
},
Default::default(),
);
assert!(!is_well_typed(&bad_or));
}
#[test]
fn test_ill_typed_not_with_int() {
let int_expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let bad_not = Expr::new(
ExprKind::Unary {
operand: Box::new(int_expr),
op: UnaryOp::Not,
},
Default::default(),
);
assert!(!is_well_typed(&bad_not));
}
#[test]
fn test_ill_typed_negate_with_bool() {
let bool_expr = Expr::new(ExprKind::Literal(Literal::Bool(true)), Default::default());
let bad_neg = Expr::new(
ExprKind::Unary {
operand: Box::new(bool_expr),
op: UnaryOp::Negate,
},
Default::default(),
);
assert!(!is_well_typed(&bad_neg));
}
#[test]
fn test_ill_typed_if_non_bool_condition() {
let condition = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let then_branch = Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
);
let bad_if = Expr::new(
ExprKind::If {
condition: Box::new(condition),
then_branch: Box::new(then_branch),
else_branch: None,
},
Default::default(),
);
assert!(!is_well_typed(&bad_if));
}
#[test]
fn test_prop_parse_print_roundtrip_direct() {
let expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let result = prop_parse_print_roundtrip(&expr);
assert!(result.is_ok());
}
#[test]
fn test_prop_well_typed_always_transpiles_simple() {
let expr = Expr::new(
ExprKind::Literal(Literal::Integer(42, None)),
Default::default(),
);
let result = prop_well_typed_always_transpiles(&expr);
assert!(result.is_ok());
}
#[test]
fn test_prop_well_typed_always_transpiles_complex() {
let left = Expr::new(
ExprKind::Literal(Literal::Integer(1, None)),
Default::default(),
);
let right = Expr::new(
ExprKind::Literal(Literal::Integer(2, None)),
Default::default(),
);
let expr = Expr::new(
ExprKind::Binary {
left: Box::new(left),
op: BinaryOp::Add,
right: Box::new(right),
},
Default::default(),
);
let result = prop_well_typed_always_transpiles(&expr);
assert!(result.is_ok());
}
}