use std::time::{Duration, Instant};
use implicit_predicates_gen::{Error, canonical, difference_graph, filter_bound, generate, parse};
const REGENERATE: &str = "run the tests.";
const ORIENT3D: &str = include_str!("fixtures/orient3d.pred");
const INSPHERE: &str = include_str!("fixtures/insphere.pred");
const LIMIT: Duration = Duration::from_secs(30);
const MAX_NODES: usize = 256;
fn timed(source: &str) -> (Result<usize, Error>, Duration) {
let start = Instant::now();
let result = generate(source, "t.pred", REGENERATE).map(|all| all.len());
(result, start.elapsed())
}
fn rejected_quickly(source: &str, (line, column): (usize, usize), fragment: &str) {
let (result, elapsed) = timed(source);
let e = result.expect_err("the definition must be rejected");
assert!(elapsed < LIMIT, "took {elapsed:?}: {e}");
assert_eq!(e.file, "t.pred");
assert_eq!((e.line, e.column), (line, column), "{e}");
assert!(e.message.contains(fragment), "{e}");
}
fn squarings(base: &str, times: usize) -> String {
let mut lines = format!(" let t0 = {base};\n");
for i in 1..=times {
lines.push_str(&format!(" let t{i} = t{} * t{};\n", i - 1, i - 1));
}
lines
}
#[test]
fn a_form_squared_32_times_is_an_error_not_a_panic() {
let source = format!(
"fn p(a: Point3, d: Point3) -> Sign {{\n a.x - d.x\n}}\n\
#[exact]\nfn p(a: Point3, d: Point3) -> Sign {{\n{} t32 - d.x\n}}\n",
squarings("a.x", 32)
);
rejected_quickly(&source, (5, 4), "degree");
}
#[test]
fn canonical_returns_an_error_on_a_form_too_large_to_expand() {
let exponent = format!(
"fn p(a: Point3, d: Point3) -> Sign {{\n{} t32 - d.x\n}}\n",
squarings("a.x", 32)
);
let wide = format!(
"fn p(a: Point3, d: Point3) -> Sign {{\n{} t5\n}}\n",
squarings("(a.x - d.x) + (a.y - d.y) + (a.z - d.z)", 5)
);
let deep = format!(
"fn p(a: Point3, d: Point3) -> Sign {{\n{} t14\n}}\n",
squarings("a.x - d.x", 14)
);
for (source, fragment) in [
(exponent, "a degree above 4294967295"),
(wide, "operations on its terms"),
(deep, "operations on its terms"),
] {
let p = parse(&source, "t.pred").unwrap().remove(0);
let start = Instant::now();
let e = canonical(&p.main, "t.pred", p.position).expect_err("too large");
assert!(start.elapsed() < LIMIT, "took {:?}", start.elapsed());
assert_eq!((e.file.as_str(), e.line, e.column), ("t.pred", 1, 4), "{e}");
assert!(e.message.contains(fragment), "{e}");
}
}
#[test]
fn a_high_degree_main_form_is_rejected_before_expansion() {
let source = format!(
"fn p(a: Point3, d: Point3) -> Sign {{\n{} t14\n}}\n",
squarings("a.x - d.x", 14)
);
rejected_quickly(&source, (1, 4), "the degree 16384 is too high");
}
#[test]
fn a_wide_base_squared_repeatedly_is_rejected_quickly() {
let source = format!(
"fn p(a: Point3, d: Point3) -> Sign {{\n{} t5\n}}\n",
squarings("(a.x - d.x) + (a.y - d.y) + (a.z - d.z)", 5)
);
rejected_quickly(&source, (1, 4), "too large to expand");
}
fn long_sum(terms: usize) -> String {
let sum = vec!["(a.x - b.x)"; terms].join(" + ");
format!("fn p(a: Point3, b: Point3) -> Sign {{ {sum} }}")
}
#[test]
fn a_very_long_sum_is_rejected_quickly() {
for terms in [2_000, 8_000, 30_000, 100_000] {
rejected_quickly(&long_sum(terms), (1, 4), "error bound of the filter");
}
}
#[test]
fn a_sum_of_a_few_hundred_terms_is_accepted() {
for terms in [400, 1_500] {
let p = parse(&long_sum(terms), "t.pred").unwrap().remove(0);
let g = difference_graph(&p.main, "t.pred", p.position).unwrap();
let start = Instant::now();
assert!(filter_bound(&g, "t.pred", p.position).is_ok(), "{terms}");
assert!(start.elapsed() < LIMIT, "took {:?}", start.elapsed());
}
assert_eq!(timed(&long_sum(100)).0, Ok(1));
}
#[test]
fn the_real_predicates_are_accepted() {
for source in [ORIENT3D, INSPHERE] {
let (result, elapsed) = timed(source);
assert_eq!(result, Ok(1));
assert!(elapsed < LIMIT, "took {elapsed:?}");
}
}
fn balanced_sum(terms: usize) -> String {
let points = ["a", "b", "c", "d"];
let mut lines = String::new();
let mut leaves = 0;
for p in points {
for q in points.iter().filter(|&&q| q != p) {
for axis in ["x", "y", "z"] {
lines.push_str(&format!(" let l{leaves} = {p}.{axis} - {q}.{axis};\n"));
leaves += 1;
}
}
}
let mut level: Vec<String> = (0..terms)
.map(|i| format!("l{} * l{}", i % leaves, (i / leaves + i) % leaves))
.collect();
let mut next_name = 0;
while level.len() > 1 {
let mut next = Vec::new();
for pair in level.chunks(2) {
if let [x, y] = pair {
lines.push_str(&format!("let s{next_name} = {x} + {y};\n"));
next.push(format!("s{next_name}"));
next_name += 1;
} else {
next.push(pair[0].clone());
}
}
level = next;
}
format!(
"fn p(a: Point3, b: Point3, c: Point3, d: Point3) -> Sign {{\n{lines} {}\n}}\n",
level[0]
)
}
#[test]
fn a_wide_balanced_sum_is_rejected_quickly() {
rejected_quickly(&balanced_sum(250_000), (1, 4), "subexpressions");
}
#[test]
fn the_largest_accepted_chain_generates_in_a_few_seconds() {
let start = Instant::now();
assert_eq!(timed(&long_sum(MAX_NODES)).0, Ok(1));
let elapsed = start.elapsed();
assert!(elapsed < Duration::from_secs(90), "took {elapsed:?}");
rejected_quickly(&long_sum(MAX_NODES + 1), (1, 4), "subexpressions");
}