pub mod arithmetic;
pub mod fun;
pub mod imp;
pub mod stlc;
pub mod toy;
pub mod weird;
use crate::logic::grammar::Grammar;
use crate::logic::partial::MetaParser;
use crate::logic::typing::core::Context;
use crate::logic::typing::Type;
use rayon::prelude::*;
use std::time::{Duration, Instant};
const DEPTH_BASE: usize = 10;
const META_START_DEPTH: usize = 5;
const META_DEPTH_FACTOR: f64 = 1.5;
fn snap_meta_depth(target: usize) -> usize {
if target <= META_START_DEPTH {
return META_START_DEPTH;
}
let mut d = META_START_DEPTH;
while d < target {
let next = ((d as f64) * META_DEPTH_FACTOR).ceil() as usize;
d = if next <= d { d + 1 } else { next };
}
d
}
pub fn valid_depth_for(input: &str) -> usize {
DEPTH_BASE + input.len() / 2
}
pub fn xfail_depth_for(input: &str) -> usize {
DEPTH_BASE + input.len() / 4
}
#[derive(Debug)]
pub enum ParseResult {
Pass {
duration: Duration,
prefix_count: usize,
},
Fail {
failing_prefix: String,
error: String,
prefix_index: usize,
},
}
impl ParseResult {
pub fn is_pass(&self) -> bool {
matches!(self, ParseResult::Pass { .. })
}
}
#[derive(Debug, Clone)]
pub struct ParseTestCase {
pub description: &'static str,
pub input: &'static str,
pub xfail: bool,
pub check_typing: bool,
pub context: Vec<(&'static str, &'static str)>,
pub parse_max_depth: Option<usize>,
}
impl ParseTestCase {
pub fn valid(desc: &'static str, input: &'static str) -> Self {
Self {
description: desc,
input,
xfail: false,
check_typing: true,
context: vec![],
parse_max_depth: Some(valid_depth_for(input)),
}
}
pub fn structural(desc: &'static str, input: &'static str) -> Self {
Self {
description: desc,
input,
xfail: false,
check_typing: false,
context: vec![],
parse_max_depth: Some(valid_depth_for(input)),
}
}
pub fn invalid(desc: &'static str, input: &'static str) -> Self {
Self {
description: desc,
input,
xfail: true,
check_typing: false,
context: vec![],
parse_max_depth: Some(xfail_depth_for(input)),
}
}
pub fn type_error(desc: &'static str, input: &'static str) -> Self {
Self {
description: desc,
input,
xfail: true,
check_typing: true,
context: vec![],
parse_max_depth: Some(xfail_depth_for(input)),
}
}
pub fn with_typing(mut self) -> Self {
self.check_typing = true;
self
}
pub fn with_context(mut self, ctx: Vec<(&'static str, &'static str)>) -> Self {
self.context = ctx;
self
}
pub fn with_parse_max_depth(mut self, depth: usize) -> Self {
self.parse_max_depth = Some(depth);
self
}
}
pub fn check_all_prefixes_parseable(
grammar: &Grammar,
input: &str,
check_typing: bool,
ctx: &Context,
parse_max_depth: Option<usize>,
) -> ParseResult {
let start = Instant::now();
let prefixes: Vec<(usize, String)> = match grammar.tokenize(input) {
Ok(segments) => {
let mut cuts = vec![0usize];
cuts.extend(segments.iter().map(|s| s.end));
if !cuts.contains(&input.len()) {
cuts.push(input.len());
}
cuts.sort_unstable();
cuts.dedup();
cuts.into_iter()
.map(|byte_end| {
let p = input[..byte_end].to_string();
(p.chars().count(), p)
})
.filter(|(len, prefix)| *len == 0 || !prefix.trim().is_empty())
.collect()
}
Err(_) => {
let chars: Vec<char> = input.chars().collect();
(0..=chars.len())
.map(|len| (len, chars[..len].iter().collect::<String>()))
.filter(|(len, prefix)| *len == 0 || !prefix.trim().is_empty())
.collect()
}
};
let parse_prefix = |prefix: &str| {
let depth = match parse_max_depth {
Some(d) => snap_meta_depth(d),
None => snap_meta_depth(valid_depth_for(prefix)),
};
let mut parser = MetaParser::new(grammar.clone()).with_max_depth(depth);
let res: Result<(), String> = if check_typing {
parser.partial_typed_ctx(prefix, ctx).map(|_| ())
} else {
parser.partial(prefix).map(|_| ())
};
match res {
Ok(_) => None,
Err(e) => Some((e, depth)),
}
};
let deep_budget = parse_max_depth.is_some_and(|d| snap_meta_depth(d) >= 41);
let results: Vec<Option<(String, usize)>> = if deep_budget {
prefixes
.iter()
.map(|(_, prefix)| parse_prefix(prefix))
.collect()
} else {
prefixes
.par_iter()
.map(|(_, prefix)| parse_prefix(prefix))
.collect()
};
let prefix_count = prefixes.len();
for ((len, prefix), opt_err) in prefixes.into_iter().zip(results.into_iter()) {
if let Some((e, depth)) = opt_err {
return ParseResult::Fail {
failing_prefix: prefix,
error: format!("{} (depth={})", e, depth),
prefix_index: len,
};
}
}
ParseResult::Pass {
duration: start.elapsed(),
prefix_count,
}
}
pub fn check_parse_fails(
grammar: &Grammar,
input: &str,
check_typing: bool,
parse_max_depth: Option<usize>,
) -> ParseResult {
let start = Instant::now();
let mut parser = match parse_max_depth {
Some(d) => MetaParser::new(grammar.clone()).with_max_depth(d),
None => MetaParser::new(grammar.clone()),
};
if check_typing {
match parser.partial(input) {
Ok(ast) => {
if ast.typed_complete(grammar).is_ok() {
ParseResult::Fail {
failing_prefix: input.to_string(),
error: "Expected type failure but found a complete well-typed tree"
.to_string(),
prefix_index: input.chars().count(),
}
} else {
ParseResult::Pass {
duration: start.elapsed(),
prefix_count: 1,
}
}
}
Err(_) => ParseResult::Pass {
duration: start.elapsed(),
prefix_count: 1,
},
}
} else {
match parser.partial(input) {
Ok(t) => ParseResult::Fail {
failing_prefix: input.to_string(),
error: format!("Expected parse/type failure but succeeded with {}", t).to_string(),
prefix_index: input.chars().count(),
},
Err(_) => ParseResult::Pass {
duration: start.elapsed(),
prefix_count: 1,
},
}
}
}
fn build_context(pairs: &[(&str, &str)]) -> Context {
let mut ctx = Context::new();
for (name, ty_str) in pairs {
let ty = Type::parse_raw(ty_str)
.unwrap_or_else(|e| panic!("Failed to parse type '{}' in test context: {}", ty_str, e));
ctx.add(name.to_string(), ty);
}
ctx
}
pub fn run_parse_test(grammar: &Grammar, case: &ParseTestCase) -> ParseResult {
let ctx = build_context(&case.context);
if case.xfail {
check_parse_fails(grammar, case.input, case.check_typing, case.parse_max_depth)
} else {
check_all_prefixes_parseable(
grammar,
case.input,
case.check_typing,
&ctx,
case.parse_max_depth,
)
}
}
#[derive(Debug)]
pub struct BatchResult {
pub passed: usize,
pub failed: usize,
pub failures: Vec<(String, ParseResult)>,
pub total_duration: Duration,
pub avg_duration: Duration,
}
impl BatchResult {
pub fn format_failures(&self) -> String {
if self.failures.is_empty() {
return String::new();
}
let mut msg = format!("\n\n{} test(s) failed:\n", self.failures.len());
msg.push_str("=".repeat(60).as_str());
msg.push('\n');
for (i, (desc, result)) in self.failures.iter().enumerate() {
msg.push_str(&format!("\n[{}] {}\n", i + 1, desc));
msg.push_str("-".repeat(60).as_str());
msg.push('\n');
match result {
ParseResult::Fail {
failing_prefix,
error,
prefix_index,
} => {
msg.push_str(&format!(" Failing prefix: '{}'\n", failing_prefix));
msg.push_str(&format!(" Prefix index: {}\n", prefix_index));
msg.push_str(&format!(" Error: {}\n", error));
}
ParseResult::Pass { .. } => {
msg.push_str(" (unexpected pass - should not be in failures list)\n");
}
}
}
msg.push_str("\n");
msg.push_str("=".repeat(60).as_str());
msg
}
}
pub fn run_parse_batch(
grammar: &Grammar,
cases: &[ParseTestCase],
) -> (BatchResult, Vec<serde_json::Value>) {
let start = Instant::now();
let mut passed = 0;
let mut failed = 0;
let mut failures = Vec::new();
let mut case_records: Vec<serde_json::Value> = Vec::with_capacity(cases.len());
for case in cases {
let start = Instant::now();
let result = run_parse_test(grammar, case);
let elapsed = start.elapsed();
{
use serde_json::json;
let (passed_flag, prefix_count, failing_prefix, error, prefix_index) = match &result {
ParseResult::Pass { prefix_count, .. } => (
true,
Some(*prefix_count as usize),
None::<String>,
None::<String>,
None::<usize>,
),
ParseResult::Fail {
failing_prefix,
error,
prefix_index,
} => (
false,
None,
Some(failing_prefix.clone()),
Some(error.clone()),
Some(*prefix_index),
),
};
let case_obj = json!({
"module": "parseable",
"desc": case.description,
"input": case.input,
"xfail": case.xfail,
"passed": passed_flag,
"time_ms": elapsed.as_millis(),
"time_us": elapsed.as_micros(),
"prefix_count": prefix_count,
"failing_prefix": failing_prefix,
"error": error,
"prefix_index": prefix_index,
});
case_records.push(case_obj.clone());
}
match &result {
ParseResult::Pass { .. } => {
passed += 1;
}
ParseResult::Fail { .. } => {
failures.push((case.description.to_string(), result));
failed += 1;
}
}
}
let total_duration = start.elapsed();
let avg_duration = if cases.is_empty() {
Duration::ZERO
} else {
total_duration / cases.len() as u32
};
(
BatchResult {
passed,
failed,
failures,
total_duration,
avg_duration,
},
case_records,
)
}
pub fn all_suites() -> Vec<(
&'static str,
Grammar,
Vec<ParseTestCase>,
Vec<ParseTestCase>,
)> {
let mut modules: Vec<(&str, Grammar, Vec<ParseTestCase>, Vec<ParseTestCase>)> = vec![
(
"arithmetic",
arithmetic::arithmetic_grammar(),
arithmetic::valid_expressions_cases(),
arithmetic::invalid_expressions_cases(),
),
(
"fun",
load_example_grammar("fun"),
fun::valid_expressions_cases(),
fun::invalid_expressions_cases(),
),
(
"imp",
load_example_grammar("imp"),
imp::valid_expressions_cases(),
imp::invalid_expressions_cases(),
),
(
"stlc",
load_example_grammar("stlc"),
stlc::valid_expressions_cases(),
stlc::invalid_expressions_cases(),
),
(
"toy",
load_example_grammar("toy"),
toy::valid_expressions_cases(),
toy::invalid_expressions_cases(),
),
];
modules.extend(weird::suites());
modules
}
pub fn load_example_grammar(name: &str) -> Grammar {
use std::path::Path;
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = Path::new(manifest_dir)
.join("examples")
.join(format!("{}.auf", name));
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("Failed to read {}: {}", path.display(), e));
Grammar::load(&content).unwrap_or_else(|e| panic!("Failed to load {}: {}", name, e))
}
#[cfg(test)]
mod tests {
use super::snap_meta_depth;
#[test]
fn snap_depth_uses_meta_rungs() {
assert_eq!(snap_meta_depth(1), 5);
assert_eq!(snap_meta_depth(5), 5);
assert_eq!(snap_meta_depth(6), 8);
assert_eq!(snap_meta_depth(10), 12);
assert_eq!(snap_meta_depth(23), 27);
assert_eq!(snap_meta_depth(40), 41);
}
}