use crate::ast::restricted::{BinaryOp, Literal, UnaryOp};
use crate::ast::{Expr, Function, RestrictedAst, Stmt, Type};
use crate::models::{Config, ShellDialect, VerificationLevel};
use crate::services::parse;
use crate::transpile;
use proptest::prelude::*;
pub mod generators {
use super::*;
pub fn any_valid_identifier() -> impl Strategy<Value = String> {
"[a-zA-Z_][a-zA-Z0-9_]{0,20}".prop_filter("Avoid reserved identifiers", |s| {
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false",
"fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut",
"pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
"true", "type", "unsafe", "use", "where", "while", "async", "await", "dyn",
"abstract", "become", "box", "do", "final", "macro", "override", "priv", "typeof",
"unsized", "virtual", "yield",
];
!s.is_empty()
&& s != "_"
&& s != "main"
&& !s.starts_with("__")
&& !RUST_KEYWORDS.contains(&s.as_str())
})
}
pub fn any_safe_string() -> impl Strategy<Value = String> {
"[a-zA-Z0-9 _.-]{0,50}"
}
pub fn any_u16_literal() -> impl Strategy<Value = Literal> {
(0u16..10000u16).prop_map(Literal::U16)
}
pub fn any_u32_literal() -> impl Strategy<Value = Literal> {
(0u32..10000u32).prop_map(Literal::U32)
}
pub fn any_bool_literal() -> impl Strategy<Value = Literal> {
prop::bool::ANY.prop_map(Literal::Bool)
}
pub fn any_string_literal() -> impl Strategy<Value = Literal> {
any_safe_string().prop_map(Literal::Str)
}
pub fn any_literal() -> impl Strategy<Value = Literal> {
prop_oneof![
any_u16_literal(),
any_u32_literal(),
any_bool_literal(),
any_string_literal()
]
}
pub fn any_binary_op() -> impl Strategy<Value = BinaryOp> {
prop_oneof![
Just(BinaryOp::Add),
Just(BinaryOp::Sub),
Just(BinaryOp::Mul),
Just(BinaryOp::Div),
Just(BinaryOp::Eq),
Just(BinaryOp::Ne),
Just(BinaryOp::Lt),
Just(BinaryOp::Le),
Just(BinaryOp::Gt),
Just(BinaryOp::Ge),
Just(BinaryOp::And),
Just(BinaryOp::Or),
]
}
pub fn any_unary_op() -> impl Strategy<Value = UnaryOp> {
prop_oneof![Just(UnaryOp::Not), Just(UnaryOp::Neg)]
}
pub fn leaf_expr() -> impl Strategy<Value = Expr> {
prop_oneof![
any_literal().prop_map(Expr::Literal),
any_valid_identifier().prop_map(Expr::Variable),
]
}
pub fn simple_expr() -> impl Strategy<Value = Expr> {
prop_oneof![
leaf_expr(),
(any_binary_op(), leaf_expr(), leaf_expr()).prop_map(|(op, left, right)| {
Expr::Binary {
op,
left: Box::new(left),
right: Box::new(right),
}
}),
(any_unary_op(), leaf_expr()).prop_map(|(op, expr)| Expr::Unary {
op,
operand: Box::new(expr),
}),
(
any_valid_identifier(),
prop::collection::vec(leaf_expr(), 0..3)
)
.prop_map(|(name, args)| Expr::FunctionCall { name, args }),
]
}
pub fn any_type() -> impl Strategy<Value = Type> {
prop_oneof![
Just(Type::Void),
Just(Type::Bool),
Just(Type::U32),
Just(Type::Str),
]
}
pub fn simple_stmt() -> impl Strategy<Value = Stmt> {
prop_oneof![
(any_valid_identifier(), simple_expr()).prop_map(|(name, value)| Stmt::Let {
name,
value,
declaration: true,
}),
simple_expr().prop_map(Stmt::Expr),
prop::option::of(simple_expr()).prop_map(Stmt::Return),
]
}
pub fn any_function() -> impl Strategy<Value = Function> {
(
any_valid_identifier(),
prop::collection::vec(simple_stmt(), 0..5),
any_type(),
)
.prop_map(|(name, body, return_type)| Function {
name,
params: vec![], return_type,
body,
})
}
pub fn valid_ast() -> impl Strategy<Value = RestrictedAst> {
prop::collection::vec(any_function(), 1..3).prop_map(|mut functions| {
functions[0].name = "main".to_string();
for (i, function) in functions.iter_mut().enumerate().skip(1) {
let name = &function.name;
if name == "_" || name == "main" || name.starts_with("__") {
function.name = format!("func_{i}");
}
}
RestrictedAst {
functions,
entry_point: "main".to_string(),
}
})
}
pub fn any_config() -> impl Strategy<Value = Config> {
(
prop_oneof![
Just(ShellDialect::Posix),
Just(ShellDialect::Bash),
Just(ShellDialect::Ash)
],
prop_oneof![
Just(VerificationLevel::None),
Just(VerificationLevel::Basic),
Just(VerificationLevel::Strict),
Just(VerificationLevel::Paranoid)
],
prop::bool::ANY,
prop::bool::ANY,
)
.prop_map(|(target, verify, emit_proof, optimize)| Config {
target,
verify,
emit_proof,
optimize,
validation_level: Some(crate::validation::ValidationLevel::Minimal),
strict_mode: false,
})
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn prop_valid_asts_validate(ast in generators::valid_ast()) {
let _ = ast.validate();
}
#[test]
fn prop_valid_identifiers_parse(name in "[a-zA-Z][a-zA-Z0-9_]{0,20}") {
const RUST_KEYWORDS: &[&str] = &[
"as", "break", "const", "continue", "crate", "else", "enum", "extern",
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match",
"mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static",
"struct", "super", "trait", "true", "type", "unsafe", "use", "where",
"while", "async", "await", "dyn", "abstract", "become", "box", "do",
"final", "macro", "override", "priv", "try", "typeof", "unsized", "virtual", "yield"
];
prop_assume!(
!name.is_empty()
&& name != "_"
&& name != "main"
&& !name.starts_with("__")
&& !RUST_KEYWORDS.contains(&name.as_str())
);
let source = format!("fn {name}() {{ let x = 42; }} fn main() {{ {name}(); }}");
let result = parse(&source);
prop_assert!(result.is_ok(), "Failed to parse with identifier: {}", name);
if let Ok(ast) = result {
let found_function = ast.functions.iter().find(|f| f.name == name);
prop_assert!(found_function.is_some(), "Function {} not found in AST", name);
}
}
#[test]
fn prop_literals_transpile(lit in generators::any_literal()) {
let source = match &lit {
Literal::Bool(b) => format!("fn main() {{ let x = {b}; }}"),
Literal::U16(n) => format!("fn main() {{ let x = {n}u16; }}"),
Literal::U32(n) => format!("fn main() {{ let x = {n}; }}"),
Literal::I32(n) => format!("fn main() {{ let x = {n}; }}"),
Literal::Str(s) => format!(r#"fn main() {{ let x = "{s}"; }}"#),
};
let result = transpile(&source, &Config::default());
prop_assert!(result.is_ok(), "Failed to transpile literal: {:?}", lit);
}
#[test]
fn prop_binary_ops_associative(
op in generators::any_binary_op(),
a in 1u32..100u32,
b in 1u32..100u32,
c in 1u32..100u32
) {
prop_assume!(!matches!(op, BinaryOp::Div));
let left_assoc = format!("fn main() {{ let x = ({a} + {b}) + {c}; }}");
let right_assoc = format!("fn main() {{ let x = {a} + ({b} + {c}); }}");
let result1 = transpile(&left_assoc, &Config::default());
let result2 = transpile(&right_assoc, &Config::default());
prop_assert!(result1.is_ok() && result2.is_ok());
}
#[test]
fn prop_function_names_preserved(name in generators::any_valid_identifier()) {
let source = format!("fn {name}() {{}} fn main() {{ {name}(); }}");
if let Ok(shell_code) = transpile(&source, &Config::default()) {
prop_assert!(shell_code.contains(&name));
}
}
#[test]
fn prop_generated_scripts_non_empty(_ast in generators::valid_ast()) {
let source = "fn main() { let x = 42; }";
if let Ok(shell_code) = transpile(source, &Config::default()) {
prop_assert!(!shell_code.trim().is_empty());
prop_assert!(shell_code.contains("#!/bin/sh") || shell_code.contains("#!/bin/bash"));
}
}
#[test]
fn prop_transpilation_deterministic(config in generators::any_config()) {
let source = "fn main() { let x = 42; let y = \"hello\"; }";
let result1 = transpile(source, &config);
let result2 = transpile(source, &config);
match (result1, result2) {
(Ok(code1), Ok(code2)) => prop_assert_eq!(code1, code2),
(Err(_), Err(_)) => {}, _ => prop_assert!(false, "Non-deterministic behavior detected"),
}
}
#[test]
fn prop_string_literals_quoted(s in generators::any_safe_string()) {
let source = format!(r#"fn main() {{ let x = "{s}"; }}"#);
if let Ok(shell_code) = transpile(&source, &Config::default()) {
prop_assert!(shell_code.contains(&s));
}
}
#[test]
fn prop_variable_names_shell_safe(name in generators::any_valid_identifier()) {
let source = format!("fn main() {{ let {name} = 42; }}");
if let Ok(shell_code) = transpile(&source, &Config::default()) {
if shell_code.contains(&name) {
prop_assert!(!name.chars().next().unwrap().is_ascii_digit());
}
}
}
#[test]
fn prop_config_robustness(config in generators::any_config()) {
let test_sources = vec![
"fn main() {}",
"fn main() { let x = 42; }",
"fn main() { let s = \"test\"; }",
"fn helper() {} fn main() { helper(); }",
];
for source in test_sources {
let result = transpile(source, &config);
match result {
Ok(_) | Err(_) => {}, }
}
}
}
#[cfg(test)]
#[path = "quickcheck_tests_tests_null_charact.rs"]
mod tests_extracted;
#[cfg(test)]
#[path = "quickcheck_tests_stdlib.rs"]
mod stdlib_tests;