mod point_text;
use std::collections::BTreeMap;
use implicit_predicates_gen::{
PointNode, PointType, error_bound, generate, generate_points, parse_points,
};
use num_bigint::BigUint;
use point_text::{ORIENT3D, POINTS, VARIED, declarations, generated, method};
use proc_macro2::TokenStream;
fn expected_terms(point: &PointType) -> Vec<Vec<String>> {
let nodes = &point.graph.nodes;
let zero = |id: usize| nodes[id] == PointNode::Int(0);
let mut terms: Vec<Vec<String>> = Vec::new();
for (id, node) in nodes.iter().enumerate() {
let has = |x: usize, terms: &[Vec<String>]| !terms[x].is_empty();
let mut t = Vec::new();
match *node {
PointNode::Neg(x) if has(x, &terms) => t.push(format!("e{x}")),
PointNode::Add(x, y) | PointNode::Sub(x, y) => {
for o in [x, y] {
if has(o, &terms) {
t.push(format!("e{o}"));
}
}
if !zero(x) && !zero(y) {
t.push(format!("sum_rounding(v{x}, v{y}, v{id})"));
}
}
PointNode::Mul(x, y) => {
if has(y, &terms) {
t.push(format!("covered_product(v{x}.abs(), e{y})"));
}
if has(x, &terms) {
t.push(format!("covered_product(v{y}.abs(), e{x})"));
}
if has(x, &terms) && has(y, &terms) {
t.push(format!("covered_product(e{x}, e{y})"));
}
if !zero(x) && !zero(y) {
t.push(format!("product_rounding(v{x}, v{y}, v{id})"));
}
}
_ => {}
}
terms.push(t);
}
terms
}
fn rounds_from_text(body: &str) -> BTreeMap<usize, u32> {
let mut rounds: BTreeMap<usize, u32> = BTreeMap::new();
for line in body.lines() {
let Some(rest) = line.strip_prefix("let e") else {
continue;
};
if !rest.starts_with(|c: char| c.is_ascii_digit()) {
continue;
}
let (name, value) = rest.trim_end_matches(';').split_once(" = ").unwrap();
let of = |e: &str| rounds[&e.strip_prefix('e').unwrap().parse::<usize>().unwrap()];
let mut total: Option<u32> = None;
for term in value.split(" + ") {
let r = if term.starts_with("sum_rounding(") {
2
} else if term.starts_with("product_rounding(") {
3
} else if let Some(args) = term.strip_prefix("covered_product(") {
let (a, b) = args.strip_suffix(')').unwrap().split_once(", ").unwrap();
let a = if a.starts_with('v') { 0 } else { of(a) };
a + of(b) + 2
} else {
of(term)
};
total = Some(total.map_or(r, |t| t.max(r) + 1));
}
rounds.insert(name.parse().unwrap(), total.unwrap());
}
rounds
}
#[test]
fn evaluate_follows_the_rule_and_its_rounds_are_the_derived_ones() {
for (point, stages, _) in declarations() {
if point.is_explicit() {
continue;
}
let body = method(&stages, &point.name, "evaluate");
let expected = expected_terms(&point);
for (id, terms) in expected.iter().enumerate() {
let line = body
.lines()
.find(|l| l.starts_with(&format!("let e{id} = ")));
match line {
None => assert!(terms.is_empty(), "{}: e{id}", point.name),
Some(line) => assert_eq!(
line,
format!("let e{id} = {};", terms.join(" + ")),
"{}",
point.name
),
}
}
let rounds = rounds_from_text(body);
let bound = error_bound(&point, "t.pred").unwrap();
for (k, &root) in point.graph.roots.iter().enumerate() {
let error = if expected[root].is_empty() {
assert_eq!(bound.rounds[k], 0);
"0.0".to_string()
} else {
assert_eq!(bound.rounds[k], rounds[&root], "{} {k}", point.name);
format!("e{root}")
};
let finish = format!("finish(approx[{k}], {error}, Self::ERR_SLACK),");
assert!(
body.lines().any(|l| l == finish),
"{}: {finish}",
point.name
);
}
let slack = format!("const ERR_SLACK: f64 = {:?};", bound.slack);
assert!(
stages.lines().any(|l| l == slack),
"{}: {slack}",
point.name
);
}
}
fn slack_by_integers(h: u32) -> f64 {
let power = (BigUint::from(1u8) << 53u32) - 1u32;
let left = power.pow(h);
let right = BigUint::from(1u8) << (52 + 53 * h);
let k = (0u64..)
.find(|&k| (BigUint::from(1u64 << 52) + k) * &left >= right)
.unwrap();
1.0 + k as f64 * f64::EPSILON
}
#[test]
fn the_error_bound_of_lnc_has_six_roundings_and_its_derived_slack_factor() {
let points = parse_points(POINTS, "t.pred").unwrap();
let lnc = points.iter().find(|p| p.name == "Lnc").unwrap();
let bound = error_bound(lnc, "t.pred").unwrap();
assert_eq!(bound.rounds, [6; 3]);
assert_eq!(bound.slack, slack_by_integers(6));
assert!(bound.slack >= 1.0 + 4.0 * f64::EPSILON, "{}", bound.slack);
assert_eq!(bound.slack, 1.0 + 4.0 * f64::EPSILON);
let explicit = points.iter().find(|p| p.name == "Explicit").unwrap();
let bound = error_bound(explicit, "t.pred").unwrap();
assert_eq!(bound.rounds, [0; 3]);
assert_eq!(bound.slack, 1.0);
}
#[test]
fn the_slack_factor_is_the_least_for_its_rounds() {
for point in parse_points(VARIED, "t.pred").unwrap() {
let bound = error_bound(&point, "t.pred").unwrap();
let most = bound.rounds.into_iter().max().unwrap();
assert_eq!(bound.slack, slack_by_integers(most), "{}", point.name);
}
}
#[test]
fn the_modules_lex_as_rust_and_are_deterministic() {
for source in [POINTS, VARIED] {
let g = generated(source);
assert_eq!(g.name, "points");
g.stages.parse::<TokenStream>().unwrap();
g.checks.parse::<TokenStream>().unwrap();
assert_eq!(generated(source), g);
assert!(g.stages.starts_with(
"// Generated by implicit-predicates-gen from predicates/points.pred. Do not edit.\n"
));
}
}
#[test]
fn the_explicit_type_wraps_its_coordinates_and_has_no_checks() {
let g = generated("point Explicit(p: F64x3) = p;");
assert!(
g.stages
.contains("#[repr(transparent)]\npub struct Explicit([f64; 3]);")
);
assert!(
g.stages
.contains("fn err(&self) -> &[f64; 3] {\n&[0.0; 3]\n}")
);
assert!(
g.stages
.contains("fn is_explicit(&self) -> bool {\ntrue\n}")
);
assert!(g.stages.contains(
"pub enum AnyPoint {\n/// A point of type [`Explicit`].\nExplicit(Explicit),\n}"
));
assert!(g.checks.is_empty());
}
#[test]
fn the_where_clause_is_checked_exactly_one_parameter_at_a_time() {
let g = generated(POINTS);
let new = method(&g.stages, "Lnc", "new");
assert!(
new.contains("if t < 0.0 {\nreturn Err(PointError::ParamOutOfRange);\n}"),
"{new}"
);
assert!(
new.contains("if t > 1.0 {\nreturn Err(PointError::ParamOutOfRange);\n}"),
"{new}"
);
let g = generated(VARIED);
let new = method(&g.stages, "Mix", "new");
assert!(new.contains("if s < 0.0 {"), "{new}");
assert!(new.contains("if t < 1.0 {"), "{new}");
assert!(!method(&g.stages, "Twice", "new").contains("ParamOutOfRange"));
}
fn rejected(source: &str, (line, column): (usize, usize), fragment: &str) {
let e = generate_points(source, "t.pred", "x").expect_err("the source must be rejected");
assert_eq!((e.line, e.column), (line, column), "{source}: {e}");
assert!(e.message.contains(fragment), "{source}: {e}");
}
#[test]
fn declarations_that_cannot_be_emitted_are_rejected_with_their_position() {
rejected(
"point A(approx: F64x3) = approx;",
(1, 9),
"cannot name a parameter",
);
rejected(
"point A(p: F64x3, new: f64) = p * new;",
(1, 19),
"cannot name a parameter",
);
for name in [
"Ok",
"Err",
"Some",
"None",
"UNIT",
"TINY",
"SUM_OPERAND_LIMIT",
"ERR_SLACK",
"P",
"_t",
] {
rejected(
&format!("point A(p: F64x3, {name}: f64) = p * {name};"),
(1, 19),
"must start with a lowercase letter",
);
}
rejected(
"point AnyPoint(p: F64x3) = p;",
(1, 7),
"cannot name a point type",
);
rejected(
"point UNIT(p: F64x3) = p;",
(1, 7),
"cannot name a point type",
);
for name in ["Result", "Ok", "Err", "Option", "Some", "None", "From"] {
rejected(
&format!("point {name}(p: F64x3, t: f64) = t * p;"),
(1, 7),
"cannot name a point type",
);
}
for name in ["f64", "bool", "usize", "core", "summable", "lnc"] {
rejected(
&format!("point {name}(p: F64x3, t: f64) = t * p;"),
(1, 7),
"must start with an uppercase letter",
);
}
rejected(
"point A(p: F64x3, u: f64, v: f64) = p * u * v\n where u + v <= 1;",
(2, 11),
"only a comparison of one parameter",
);
rejected(
"point A(p: F64x3, t: f64) = p * t where t <= t + 1;",
(1, 41),
"only a comparison of one parameter",
);
rejected(
"point A(p: F64x3, t: f64) = p * t where t + t <= 1;",
(1, 41),
"only a comparison of one parameter",
);
rejected(
"point A(p: F64x3, t: f64) = p * t where t <= 9007199254740993;",
(1, 41),
"at most 2^53",
);
rejected(
"point A(p: F64x3) = 9007199254740993 * p;",
(1, 7),
"not exactly an `f64`",
);
rejected(
"point LncA(p: F64x3) = -p;\npoint Lnc_a(p: F64x3) = --p;",
(2, 7),
"exact_lnc_a",
);
let deep = "point A(p: F64x3, s: f64, t: f64) = \
(s - t) * (s - t) * (s - t) * (s - t) * (s - t) * (s - t) * (s - t) * p;";
rejected(deep, (1, 7), "on the stack");
let long = format!("point A(p: F64x3) = p{};", " + p".repeat(2000));
rejected(&long, (1, 7), "roundings");
}
#[test]
fn a_file_of_only_predicates_has_no_point_module_and_one_of_only_points_no_predicates() {
assert_eq!(generate_points(ORIENT3D, "t.pred", "x").unwrap(), None);
assert_eq!(generate(POINTS, "t.pred", "x").unwrap(), []);
assert!(generate_points(POINTS, "t.pred", "x").unwrap().is_some());
}
#[test]
fn a_very_large_declaration_is_rejected_quickly_without_panicking() {
let source = format!(
"point A(p: F64x3, t: f64) = p{};",
" + t * p".repeat(250_000)
);
let start = std::time::Instant::now();
let e = generate_points(&source, "t.pred", "x").unwrap_err();
assert!(e.message.contains("roundings"), "{e}");
assert!(start.elapsed().as_secs() < 30);
}
#[test]
fn the_helpers_of_the_error_bound_are_the_documented_ones() {
let g = generated(POINTS);
for helper in [
"fn covered_product(a: f64, b: f64) -> f64 {\n\
if a == 0.0 || b == 0.0 { 0.0 } else { a * b + TINY }\n}",
"fn sum_rounding(x: f64, y: f64, s: f64) -> f64 {\n\
if x != 0.0 && y != 0.0 { covered_product(UNIT, s.abs()) } else { 0.0 }\n}",
"fn product_rounding(x: f64, y: f64, p: f64) -> f64 {\n\
if x != 0.0 && y != 0.0 { covered_product(UNIT, p.abs()) + TINY } else { 0.0 }\n}",
"fn finish(approx: f64, err: f64, slack: f64) -> f64 {\n\
let bound = err * slack;\n\
if !(approx.is_finite() && bound.is_finite()) {\nf64::INFINITY\n} \
else if err == 0.0 {\n0.0\n} else {\nbound.next_up()\n}\n}",
"const UNIT: f64 = 1.1102230246251565e-16;",
"const TINY: f64 = 5e-324;",
"const SUM_OPERAND_LIMIT: f64 = 1.1235582092889474e307;",
] {
assert!(g.stages.contains(helper), "{helper}");
}
assert_eq!(1.1102230246251565e-16, 2f64.powi(-53));
assert_eq!(5e-324, f64::from_bits(1));
assert_eq!(1.1235582092889474e307, 2f64.powi(1020));
}
#[test]
fn parameter_names_must_be_snake_case() {
for (name, column) in [("tA", 19), ("t__a", 19), ("tAb", 19)] {
rejected(
&format!("point A(p: F64x3, {name}: f64) = p * {name};"),
(1, column),
"snake case",
);
}
for name in ["t_a", "t1", "t__", "t_1_"] {
let source = format!("point A(p: F64x3, {name}: f64) = p * {name};");
assert!(generate_points(&source, "t.pred", "x").is_ok(), "{name}");
}
}