#[cfg(test)]
mod tests {
use proptest::prelude::*;
use crate::engine::grammar::SPG;
use crate::typing::TypingSynth;
use crate::typing::{Context, Type};
use crate::validation::parseable::{
arithmetic::ARITHMETIC_GRAMMAR, check_all_prefixes_parseable, check_parse_fails,
load_example_grammar,
};
fn parse_accepted(grammar: &SPG, input: &str, ctx: &Context) -> bool {
let mut s = TypingSynth::new(grammar.clone(), input);
s.parse_with(ctx).is_ok()
}
#[test]
fn empty_prefix_always_accepted_stlc() {
let g = load_example_grammar("stlc");
assert!(parse_accepted(&g, "", &Context::new()));
}
#[test]
fn empty_prefix_always_accepted_imp() {
let g = load_example_grammar("imp");
assert!(parse_accepted(&g, "", &Context::new()));
}
#[test]
fn empty_prefix_always_accepted_arithmetic() {
let g = SPG::load(ARITHMETIC_GRAMMAR).unwrap();
assert!(parse_accepted(&g, "", &Context::new()));
}
#[test]
fn empty_prefix_always_accepted_fun() {
let g = load_example_grammar("fun");
assert!(parse_accepted(&g, "", &Context::new()));
}
prop_compose! {
fn arith_input()(
nums in prop::collection::vec(1u32..=99, 1..=6),
ops in prop::collection::vec(prop::sample::select(vec!["+", "-", "*", "/"]), 0..=5),
) -> String {
let terms: usize = nums.len();
let op_count = ops.len().min(terms.saturating_sub(1));
let mut parts = Vec::new();
for i in 0..terms {
parts.push(nums[i].to_string());
if i < op_count {
parts.push(ops[i].to_string());
}
}
parts.join(" ")
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(120))]
#[test]
fn arith_prefix_monotone(input in arith_input()) {
let mut g = SPG::load(ARITHMETIC_GRAMMAR).unwrap();
let ctx = Context::new();
if parse_accepted(&g, &input, &ctx) {
let result = check_all_prefixes_parseable(&mut g, &input, &ctx);
prop_assert!(
result.is_pass(),
"prefix monotonicity violated for input {:?}",
input
);
}
}
}
prop_compose! {
fn stlc_chain_input()(n in 1usize..=6) -> (String, Context) {
let type_names: Vec<_> = (0..=n).map(|i| format!("T{i}")).collect();
let mut f_ty = type_names[n].clone();
for i in (0..n).rev() {
f_ty = format!("{}->{}", type_names[i], f_ty);
}
let g = load_example_grammar("stlc");
let mut ctx = Context::new();
ctx.add("f".to_string(), Type::parse(&g, &f_ty).unwrap());
for (i, name) in (0..n).map(|i| (i, format!("x{i}"))) {
ctx.add(name, Type::parse(&g, &type_names[i]).unwrap());
}
let input = std::iter::once("f".to_string())
.chain((0..n).map(|i| format!("x{i}")))
.collect::<Vec<_>>()
.join(" ");
(input, ctx)
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(60))]
#[test]
fn stlc_chain_prefix_monotone((input, ctx) in stlc_chain_input()) {
let mut g = load_example_grammar("stlc");
let result = check_all_prefixes_parseable(&mut g, &input, &ctx);
prop_assert!(
result.is_pass(),
"prefix monotonicity violated for stlc chain {:?}",
input
);
}
}
fn feed_replay_ok(g: &SPG, input: &str, ctx: &Context) -> bool {
let mut grammar = g.clone();
let tokens: Vec<String> = match grammar.tokenize(input) {
Ok(segs) => segs.into_iter().map(|s| s.text().to_string()).collect(),
Err(_) => return false,
};
let mut s = TypingSynth::new(g.clone(), "");
let mut built = String::new();
for token in &tokens {
if built.is_empty() {
built = token.clone();
} else {
built.push(' ');
built.push_str(token);
}
s.set_input(built.as_str());
if s.parse_with(ctx).is_err() {
return false;
}
}
true
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(60))]
#[test]
fn stlc_chain_feed_agrees_with_parse((input, ctx) in stlc_chain_input()) {
let g = load_example_grammar("stlc");
let ok = feed_replay_ok(&g, &input, &ctx);
prop_assert!(
ok,
"feed–parse disagreement on input {:?}",
input,
);
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(80))]
#[test]
fn arith_feed_agrees_with_parse(input in arith_input()) {
let g = SPG::load(ARITHMETIC_GRAMMAR).unwrap();
let ctx = Context::new();
if parse_accepted(&g, &input, &ctx) {
let ok = feed_replay_ok(&g, &input, &ctx);
prop_assert!(
ok,
"feed–parse disagreement on input {:?}",
input,
);
}
}
}
prop_compose! {
fn random_context()(
names in prop::collection::vec("[a-z]{1,3}", 0..=4),
) -> Context {
let mut ctx = Context::new();
for (i, name) in names.into_iter().enumerate() {
let ty_str = if i % 2 == 0 { "Int" } else { "Bool" };
ctx.add(name, Type::raw(ty_str));
}
ctx
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(80))]
#[test]
fn arith_invalid_stays_rejected_under_extended_ctx(ctx in random_context()) {
let g = SPG::load(ARITHMETIC_GRAMMAR).unwrap();
let invalid_inputs = [
"+ 1",
"1 + + 2",
")",
"( )",
"1 2",
];
for input in invalid_inputs {
let result = check_parse_fails(&g, input, &ctx);
prop_assert!(
result.is_pass(),
"invalid input {:?} unexpectedly accepted under context {:?}",
input,
ctx
);
}
}
}
#[test]
fn all_invalid_arithmetic_cases_rejected() {
let g = SPG::load(ARITHMETIC_GRAMMAR).unwrap();
let ctx = Context::new();
for case in crate::validation::parseable::arithmetic::invalid_expressions_cases() {
let result = check_parse_fails(&g, case.input, &ctx);
assert!(
result.is_pass(),
"invalid case {:?} (input={:?}) was not rejected",
case.description,
case.input
);
}
}
#[test]
fn all_invalid_stlc_cases_rejected() {
let g = load_example_grammar("stlc");
let ctx = Context::new();
for case in crate::validation::parseable::stlc::invalid_expressions_cases() {
let result = check_parse_fails(&g, case.input, &ctx);
assert!(
result.is_pass(),
"invalid case {:?} (input={:?}) was not rejected",
case.description,
case.input
);
}
}
}