use std::cmp::Ordering;
use std::fmt;
use std::ops::{BitAnd, BitOr, Not};
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Truth {
False,
Unknown,
True,
}
impl Truth {
pub const fn from_bool(b: bool) -> Truth {
if b { Truth::True } else { Truth::False }
}
#[must_use]
pub const fn is_true(self) -> bool {
matches!(self, Truth::True)
}
#[must_use]
pub const fn is_false(self) -> bool {
matches!(self, Truth::False)
}
#[must_use]
pub const fn is_known(self) -> bool {
!matches!(self, Truth::Unknown)
}
pub const fn not(self) -> Truth {
match self {
Truth::False => Truth::True,
Truth::Unknown => Truth::Unknown,
Truth::True => Truth::False,
}
}
pub const fn and(self, other: Truth) -> Truth {
match (self, other) {
(Truth::False, _) | (_, Truth::False) => Truth::False,
(Truth::True, Truth::True) => Truth::True,
_ => Truth::Unknown,
}
}
pub const fn or(self, other: Truth) -> Truth {
match (self, other) {
(Truth::True, _) | (_, Truth::True) => Truth::True,
(Truth::False, Truth::False) => Truth::False,
_ => Truth::Unknown,
}
}
pub const fn implies(self, other: Truth) -> Truth {
self.not().or(other)
}
pub const fn iff(self, other: Truth) -> Truth {
match (self, other) {
(Truth::Unknown, _) | (_, Truth::Unknown) => Truth::Unknown,
(Truth::True, Truth::True) | (Truth::False, Truth::False) => Truth::True,
_ => Truth::False,
}
}
pub fn all(items: impl IntoIterator<Item = Truth>) -> Truth {
let mut acc = Truth::True;
for t in items {
match t {
Truth::False => return Truth::False,
Truth::Unknown => acc = Truth::Unknown,
Truth::True => {}
}
}
acc
}
pub fn any(items: impl IntoIterator<Item = Truth>) -> Truth {
let mut acc = Truth::False;
for t in items {
match t {
Truth::True => return Truth::True,
Truth::Unknown => acc = Truth::Unknown,
Truth::False => {}
}
}
acc
}
pub fn or_else(self, f: impl FnOnce() -> Truth) -> Truth {
match self {
Truth::Unknown => f(),
known => known,
}
}
#[must_use]
pub const fn to_option(self) -> Option<bool> {
match self {
Truth::False => Some(false),
Truth::Unknown => None,
Truth::True => Some(true),
}
}
}
impl Not for Truth {
type Output = Truth;
fn not(self) -> Truth {
Truth::not(self)
}
}
impl BitAnd for Truth {
type Output = Truth;
fn bitand(self, rhs: Truth) -> Truth {
self.and(rhs)
}
}
impl BitOr for Truth {
type Output = Truth;
fn bitor(self, rhs: Truth) -> Truth {
self.or(rhs)
}
}
impl From<bool> for Truth {
fn from(b: bool) -> Truth {
Truth::from_bool(b)
}
}
impl From<Option<bool>> for Truth {
fn from(o: Option<bool>) -> Truth {
match o {
Some(b) => Truth::from_bool(b),
None => Truth::Unknown,
}
}
}
impl From<Truth> for Option<bool> {
fn from(t: Truth) -> Option<bool> {
t.to_option()
}
}
impl fmt::Display for Truth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Truth::False => "false",
Truth::Unknown => "unknown",
Truth::True => "true",
})
}
}
#[must_use]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct SignSet(u8);
const ATOMS: [SignSet; 4] = [
SignSet::NEGATIVE,
SignSet::ZERO,
SignSet::POSITIVE,
SignSet::NONREAL,
];
const ATOM_NAMES: [&str; 4] = ["<0", "0", ">0", "nonreal"];
impl SignSet {
pub const EMPTY: SignSet = SignSet(0b0000);
pub const NEGATIVE: SignSet = SignSet(0b0001);
pub const ZERO: SignSet = SignSet(0b0010);
pub const POSITIVE: SignSet = SignSet(0b0100);
pub const NONREAL: SignSet = SignSet(0b1000);
pub const REAL: SignSet = Self::NEGATIVE.union(Self::ZERO).union(Self::POSITIVE);
pub const NONNEGATIVE: SignSet = Self::ZERO.union(Self::POSITIVE);
pub const NONPOSITIVE: SignSet = Self::NEGATIVE.union(Self::ZERO);
pub const NONZERO: SignSet = Self::NEGATIVE.union(Self::POSITIVE).union(Self::NONREAL);
pub const NONZERO_REAL: SignSet = Self::NEGATIVE.union(Self::POSITIVE);
pub const UNKNOWN: SignSet = Self::REAL.union(Self::NONREAL);
#[must_use]
pub const fn contains(self, other: SignSet) -> bool {
(self.0 & other.0) == other.0
}
#[must_use]
pub const fn is_subset(self, other: SignSet) -> bool {
other.contains(self)
}
pub const fn union(self, other: SignSet) -> SignSet {
SignSet(self.0 | other.0)
}
pub const fn intersect(self, other: SignSet) -> SignSet {
SignSet(self.0 & other.0)
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn is_single(self) -> bool {
self.0.count_ones() == 1
}
pub fn atoms(self) -> impl Iterator<Item = SignSet> {
ATOMS.into_iter().filter(move |&a| self.contains(a))
}
pub const fn neg(self) -> SignSet {
lift1(Unary::Neg, self)
}
pub const fn add(self, other: SignSet) -> SignSet {
lift2(Binary::Add, self, other)
}
pub const fn mul(self, other: SignSet) -> SignSet {
lift2(Binary::Mul, self, other)
}
pub const fn recip(self) -> SignSet {
lift1(Unary::Recip, self)
}
pub const fn pow_int(self, n: i64) -> SignSet {
lift1(Unary::Pow(n), self)
}
pub const fn square(self) -> SignSet {
self.pow_int(2)
}
pub const fn abs(self) -> SignSet {
lift1(Unary::Abs, self)
}
pub const fn conj(self) -> SignSet {
self
}
pub const fn is_in(self, property: SignSet) -> Truth {
if self.is_empty() {
return Truth::Unknown;
}
if self.is_subset(property) {
Truth::True
} else if self.intersect(property).is_empty() {
Truth::False
} else {
Truth::Unknown
}
}
pub const fn is_positive(self) -> Truth {
self.is_in(Self::POSITIVE)
}
pub const fn is_negative(self) -> Truth {
self.is_in(Self::NEGATIVE)
}
pub const fn is_zero(self) -> Truth {
self.is_in(Self::ZERO)
}
pub const fn is_nonzero(self) -> Truth {
self.is_in(Self::NONZERO)
}
pub const fn is_nonnegative(self) -> Truth {
self.is_in(Self::NONNEGATIVE)
}
pub const fn is_nonpositive(self) -> Truth {
self.is_in(Self::NONPOSITIVE)
}
pub const fn is_real(self) -> Truth {
self.is_in(Self::REAL)
}
pub const fn is_nonreal(self) -> Truth {
self.is_in(Self::NONREAL)
}
pub const fn from_ordering(o: Ordering) -> SignSet {
match o {
Ordering::Less => Self::NEGATIVE,
Ordering::Equal => Self::ZERO,
Ordering::Greater => Self::POSITIVE,
}
}
pub const fn from_sign_i8(s: i8) -> SignSet {
match s {
i8::MIN..=-1 => Self::NEGATIVE,
0 => Self::ZERO,
1..=i8::MAX => Self::POSITIVE,
}
}
#[must_use]
pub const fn to_ordering(self) -> Option<Ordering> {
match self {
Self::NEGATIVE => Some(Ordering::Less),
Self::ZERO => Some(Ordering::Equal),
Self::POSITIVE => Some(Ordering::Greater),
_ => None,
}
}
}
impl fmt::Display for SignSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("{")?;
let mut first = true;
for (atom, name) in ATOMS.into_iter().zip(ATOM_NAMES) {
if self.contains(atom) {
if !first {
f.write_str(", ")?;
}
f.write_str(name)?;
first = false;
}
}
f.write_str("}")
}
}
impl fmt::Debug for SignSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
#[derive(Clone, Copy)]
enum Unary {
Neg,
Recip,
Abs,
Pow(i64),
}
#[derive(Clone, Copy)]
enum Binary {
Add,
Mul,
}
const fn lift1(op: Unary, s: SignSet) -> SignSet {
let mut out = SignSet::EMPTY;
let mut i = 0;
while i < ATOMS.len() {
if s.contains(ATOMS[i]) {
out = out.union(unary_atom(op, ATOMS[i]));
}
i += 1;
}
out
}
const fn lift2(op: Binary, x: SignSet, y: SignSet) -> SignSet {
let mut out = SignSet::EMPTY;
let mut i = 0;
while i < ATOMS.len() {
let mut j = 0;
while j < ATOMS.len() {
if x.contains(ATOMS[i]) && y.contains(ATOMS[j]) {
out = out.union(binary_atom(op, ATOMS[i], ATOMS[j]));
}
j += 1;
}
i += 1;
}
out
}
const fn unary_atom(op: Unary, a: SignSet) -> SignSet {
match op {
Unary::Neg => match a {
SignSet::NEGATIVE => SignSet::POSITIVE,
SignSet::POSITIVE => SignSet::NEGATIVE,
SignSet::ZERO | SignSet::NONREAL => a,
_ => SignSet::UNKNOWN,
},
Unary::Recip => match a {
SignSet::ZERO => SignSet::EMPTY,
SignSet::NEGATIVE | SignSet::POSITIVE | SignSet::NONREAL => a,
_ => SignSet::UNKNOWN,
},
Unary::Abs => match a {
SignSet::ZERO => SignSet::ZERO,
SignSet::NEGATIVE | SignSet::POSITIVE | SignSet::NONREAL => SignSet::POSITIVE,
_ => SignSet::UNKNOWN,
},
Unary::Pow(n) => pow_atom(a, n),
}
}
const fn pow_atom(a: SignSet, n: i64) -> SignSet {
if n == 0 {
return SignSet::POSITIVE;
}
let odd = n % 2 != 0;
match a {
SignSet::NEGATIVE if odd => SignSet::NEGATIVE,
SignSet::NEGATIVE => SignSet::POSITIVE,
SignSet::ZERO if n > 0 => SignSet::ZERO,
SignSet::ZERO => SignSet::EMPTY,
SignSet::POSITIVE => SignSet::POSITIVE,
SignSet::NONREAL => match n.unsigned_abs() {
1 => SignSet::NONREAL,
2 => SignSet::NEGATIVE.union(SignSet::NONREAL),
_ => SignSet::NONZERO,
},
_ => SignSet::UNKNOWN,
}
}
const fn binary_atom(op: Binary, a: SignSet, b: SignSet) -> SignSet {
match op {
Binary::Add => add_atoms(a, b),
Binary::Mul => mul_atoms(a, b),
}
}
const fn add_atoms(a: SignSet, b: SignSet) -> SignSet {
match (a, b) {
(SignSet::NONREAL, SignSet::NONREAL) => SignSet::UNKNOWN,
(SignSet::NONREAL, SignSet::NEGATIVE | SignSet::ZERO | SignSet::POSITIVE)
| (SignSet::NEGATIVE | SignSet::ZERO | SignSet::POSITIVE, SignSet::NONREAL) => {
SignSet::NONREAL
}
(SignSet::ZERO, SignSet::NEGATIVE | SignSet::ZERO | SignSet::POSITIVE) => b,
(SignSet::NEGATIVE | SignSet::POSITIVE, SignSet::ZERO) => a,
(SignSet::NEGATIVE, SignSet::NEGATIVE) => SignSet::NEGATIVE,
(SignSet::POSITIVE, SignSet::POSITIVE) => SignSet::POSITIVE,
(SignSet::NEGATIVE, SignSet::POSITIVE) | (SignSet::POSITIVE, SignSet::NEGATIVE) => {
SignSet::REAL
}
_ => SignSet::UNKNOWN,
}
}
const fn mul_atoms(a: SignSet, b: SignSet) -> SignSet {
match (a, b) {
(
SignSet::ZERO,
SignSet::NEGATIVE | SignSet::ZERO | SignSet::POSITIVE | SignSet::NONREAL,
)
| (SignSet::NEGATIVE | SignSet::POSITIVE | SignSet::NONREAL, SignSet::ZERO) => {
SignSet::ZERO
}
(SignSet::NONREAL, SignSet::NONREAL) => SignSet::NONZERO,
(SignSet::NONREAL, SignSet::NEGATIVE | SignSet::POSITIVE)
| (SignSet::NEGATIVE | SignSet::POSITIVE, SignSet::NONREAL) => SignSet::NONREAL,
(SignSet::NEGATIVE, SignSet::NEGATIVE) | (SignSet::POSITIVE, SignSet::POSITIVE) => {
SignSet::POSITIVE
}
(SignSet::NEGATIVE, SignSet::POSITIVE) | (SignSet::POSITIVE, SignSet::NEGATIVE) => {
SignSet::NEGATIVE
}
_ => SignSet::UNKNOWN,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Evidence {
Exact,
Certified,
Approximate,
Probabilistic,
}
impl Evidence {
const fn rank(self) -> u8 {
match self {
Evidence::Exact => 3,
Evidence::Certified => 2,
Evidence::Approximate => 1,
Evidence::Probabilistic => 0,
}
}
#[must_use]
pub const fn is_proof(self) -> bool {
matches!(self, Evidence::Exact | Evidence::Certified)
}
#[must_use]
pub const fn at_least(self, min: Evidence) -> bool {
self.rank() >= min.rank()
}
#[must_use]
pub const fn stronger_than(self, other: Evidence) -> bool {
self.rank() > other.rank()
}
#[must_use]
pub const fn weaker(self, other: Evidence) -> Evidence {
if other.rank() < self.rank() {
other
} else {
self
}
}
}
#[must_use]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Verdict<T> {
pub value: T,
pub evidence: Evidence,
}
impl<T> Verdict<T> {
pub const fn new(value: T, evidence: Evidence) -> Self {
Verdict { value, evidence }
}
#[must_use]
pub fn proven(self) -> Option<T> {
self.evidence.is_proof().then_some(self.value)
}
#[must_use]
pub fn at_least(self, min: Evidence) -> Option<T> {
self.evidence.at_least(min).then_some(self.value)
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Verdict<U> {
Verdict {
value: f(self.value),
evidence: self.evidence,
}
}
}
impl Verdict<Truth> {
pub const fn truth_at_least(self, min: Evidence) -> Truth {
if self.evidence.at_least(min) {
self.value
} else {
Truth::Unknown
}
}
}
impl Verdict<SignSet> {
pub const fn signs_at_least(self, min: Evidence) -> SignSet {
if self.evidence.at_least(min) {
self.value
} else {
SignSet::UNKNOWN
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::collections::HashSet;
const TRUTHS: [Truth; 3] = [Truth::False, Truth::Unknown, Truth::True];
const STRONGEST_FIRST: [Evidence; 4] = [
Evidence::Exact,
Evidence::Certified,
Evidence::Approximate,
Evidence::Probabilistic,
];
fn possible(t: Truth) -> &'static [bool] {
match t {
Truth::False => &[false],
Truth::Unknown => &[false, true],
Truth::True => &[true],
}
}
fn from_possible(values: &[bool]) -> Truth {
match (values.contains(&false), values.contains(&true)) {
(true, false) => Truth::False,
(false, true) => Truth::True,
(true, true) => Truth::Unknown,
(false, false) => panic!("no possible value"),
}
}
fn lifted1(a: Truth, op: fn(bool) -> bool) -> Truth {
let values: Vec<bool> = possible(a).iter().map(|&x| op(x)).collect();
from_possible(&values)
}
fn lifted2(a: Truth, b: Truth, op: fn(bool, bool) -> bool) -> Truth {
let values: Vec<bool> = possible(a)
.iter()
.flat_map(|&x| possible(b).iter().map(move |&y| op(x, y)))
.collect();
from_possible(&values)
}
#[test]
fn kleene_connectives_are_the_set_lifted_boolean_ones() {
let mut cases = 0;
for a in TRUTHS {
assert_eq!(a.not(), lifted1(a, |x| !x), "not {a}");
assert_eq!(!a, a.not(), "!{a}");
for b in TRUTHS {
assert_eq!(a.and(b), lifted2(a, b, |x, y| x && y), "{a} and {b}");
assert_eq!(a.or(b), lifted2(a, b, |x, y| x || y), "{a} or {b}");
assert_eq!(
a.implies(b),
lifted2(a, b, |x, y| !x || y),
"{a} implies {b}"
);
assert_eq!(a.iff(b), lifted2(a, b, |x, y| x == y), "{a} iff {b}");
assert_eq!(a & b, a.and(b), "{a} & {b}");
assert_eq!(a | b, a.or(b), "{a} | {b}");
cases += 1;
}
}
assert_eq!(cases, 9);
assert_eq!(Truth::Unknown.and(Truth::False), Truth::False);
assert_eq!(Truth::Unknown.or(Truth::True), Truth::True);
assert_eq!(Truth::False.implies(Truth::Unknown), Truth::True);
assert_eq!(Truth::Unknown.iff(Truth::Unknown), Truth::Unknown);
}
#[test]
fn truth_conversions_and_readings() {
for b in [false, true] {
assert_eq!(Truth::from(b), Truth::from_bool(b));
assert_eq!(Truth::from(Some(b)), Truth::from_bool(b));
assert_eq!(Truth::from_bool(b).to_option(), Some(b));
}
assert_eq!(Truth::from(None::<bool>), Truth::Unknown);
for t in TRUTHS {
assert_eq!(Truth::from(t.to_option()), t);
assert_eq!(Option::<bool>::from(t), t.to_option());
let not_proved = !t.is_true();
let refuted = t.not().is_true();
assert_eq!(not_proved == refuted, t.is_known(), "{t}");
}
let readings = |t: Truth| [t.is_true(), t.is_false(), t.is_known()];
assert_eq!(readings(Truth::True), [true, false, true]);
assert_eq!(readings(Truth::False), [false, true, true]);
assert_eq!(readings(Truth::Unknown), [false, false, false]);
assert_eq!(Truth::True.to_string(), "true");
assert_eq!(Truth::False.to_string(), "false");
assert_eq!(Truth::Unknown.to_string(), "unknown");
}
fn sequences(max_len: usize) -> Vec<Vec<Truth>> {
let mut all = vec![vec![]];
let mut frontier: Vec<Vec<Truth>> = vec![vec![]];
for _ in 0..max_len {
let mut next = Vec::new();
for s in &frontier {
for t in TRUTHS {
let mut longer = s.clone();
longer.push(t);
next.push(longer);
}
}
all.extend(next.iter().cloned());
frontier = next;
}
all
}
#[test]
fn all_and_any_agree_with_folds_and_short_circuit() {
let seqs = sequences(4);
assert_eq!(seqs.len(), 1 + 3 + 9 + 27 + 81);
for s in &seqs {
let and_fold = s.iter().fold(Truth::True, |acc, &t| acc.and(t));
let or_fold = s.iter().fold(Truth::False, |acc, &t| acc.or(t));
assert_eq!(Truth::all(s.iter().copied()), and_fold, "all {s:?}");
assert_eq!(Truth::any(s.iter().copied()), or_fold, "any {s:?}");
for decisive in [Truth::False, Truth::True] {
let pulled = Cell::new(0);
let lazy = s.iter().inspect(|_| pulled.set(pulled.get() + 1)).copied();
let _ = if decisive.is_false() {
Truth::all(lazy)
} else {
Truth::any(lazy)
};
let expected = s
.iter()
.position(|&t| t == decisive)
.map_or(s.len(), |i| i + 1);
assert_eq!(
pulled.get(),
expected,
"short circuit on {decisive} in {s:?}"
);
}
}
assert_eq!(Truth::all(std::iter::empty::<Truth>()), Truth::True);
assert_eq!(Truth::any(std::iter::empty::<Truth>()), Truth::False);
}
#[test]
fn or_else_asks_only_when_unknown() {
for first in TRUTHS {
for second in TRUTHS {
let calls = Cell::new(0);
let got = first.or_else(|| {
calls.set(calls.get() + 1);
second
});
if first.is_known() {
assert_eq!((got, calls.get()), (first, 0), "{first} or_else {second}");
} else {
assert_eq!((got, calls.get()), (second, 1), "{first} or_else {second}");
}
}
}
assert_eq!(Truth::False.or_else(|| Truth::True), Truth::False);
assert_eq!(Truth::False.or(Truth::True), Truth::True);
}
const CONST_TRUTH: Truth = Truth::True.and(Truth::Unknown).not().or(Truth::False);
const CONST_SIGNS: SignSet = SignSet::NEGATIVE
.mul(SignSet::NONREAL)
.add(SignSet::ZERO)
.pow_int(2);
#[test]
fn core_operations_are_const() {
assert_eq!(CONST_TRUTH, Truth::Unknown);
assert_eq!(CONST_SIGNS, SignSet::NEGATIVE.union(SignSet::NONREAL));
}
#[derive(Clone, Copy, Debug)]
struct Cart {
re: f64,
im: f64,
}
impl Cart {
fn class(self) -> SignSet {
if self.im != 0.0 {
return SignSet::NONREAL;
}
match self.re.partial_cmp(&0.0) {
Some(Ordering::Less) => SignSet::NEGATIVE,
Some(Ordering::Equal) => SignSet::ZERO,
Some(Ordering::Greater) => SignSet::POSITIVE,
None => panic!("NaN sample"),
}
}
fn add(self, other: Cart) -> Cart {
Cart {
re: self.re + other.re,
im: self.im + other.im,
}
}
fn neg(self) -> Cart {
Cart {
re: -self.re,
im: -self.im,
}
}
fn conj(self) -> Cart {
Cart {
re: self.re,
im: -self.im,
}
}
fn abs(self) -> Cart {
Cart {
re: self.re.hypot(self.im),
im: 0.0,
}
}
}
fn cartesian_samples() -> Vec<Cart> {
let h = 3f64.sqrt() / 2.0; let s = std::f64::consts::FRAC_1_SQRT_2; let mut out = Vec::new();
for r in [3.0, 1.0, 0.25, 1e-9] {
out.push(Cart { re: -r, im: 0.0 });
out.push(Cart { re: r, im: 0.0 });
}
out.push(Cart { re: 0.0, im: 0.0 });
out.push(Cart { re: -0.0, im: 0.0 });
for (re, im) in [
(0.0, 1.0),
(0.0, -1.0),
(1.0, 1.0),
(1.0, -1.0),
(-1.0, 1.0),
(-1.0, -1.0),
(-2.0, 0.5),
(0.3, -4.0),
(0.5, h),
(0.5, -h),
(-0.5, h),
(-0.5, -h),
(s, s),
(s, -s),
(-s, s),
(-s, -s),
(0.0, 1e-9),
(3.0, -1e-9),
] {
out.push(Cart { re, im });
}
out
}
#[derive(Clone, Copy, Debug)]
enum Polar {
Zero,
Ray { p: i64, q: i64 },
}
impl Polar {
fn class(self) -> SignSet {
match self {
Polar::Zero => SignSet::ZERO,
Polar::Ray { p, q } => {
let m = p.rem_euclid(2 * q);
if m == 0 {
SignSet::POSITIVE
} else if m == q {
SignSet::NEGATIVE
} else {
SignSet::NONREAL
}
}
}
}
fn mul(self, other: Polar) -> Polar {
match (self, other) {
(Polar::Ray { p: a, q: b }, Polar::Ray { p: c, q: d }) => Polar::Ray {
p: a * d + c * b,
q: b * d,
},
_ => Polar::Zero,
}
}
fn recip(self) -> Option<Polar> {
match self {
Polar::Zero => None,
Polar::Ray { p, q } => Some(Polar::Ray { p: -p, q }),
}
}
fn pow(self, n: i64) -> Option<Polar> {
match self {
Polar::Ray { p, q } => Some(Polar::Ray { p: n * p, q }),
Polar::Zero => match n.cmp(&0) {
Ordering::Greater => Some(Polar::Zero),
Ordering::Equal => Some(Polar::Ray { p: 0, q: 1 }),
Ordering::Less => None,
},
}
}
fn neg(self) -> Polar {
match self {
Polar::Zero => Polar::Zero,
Polar::Ray { p, q } => Polar::Ray { p: p + q, q },
}
}
fn conj(self) -> Polar {
match self {
Polar::Zero => Polar::Zero,
Polar::Ray { p, q } => Polar::Ray { p: -p, q },
}
}
fn abs(self) -> Polar {
match self {
Polar::Zero => Polar::Zero,
Polar::Ray { .. } => Polar::Ray { p: 0, q: 1 },
}
}
}
fn polar_samples() -> Vec<Polar> {
let mut out = vec![Polar::Zero];
for (p, q) in [
(1, 1),
(-1, 1),
(3, 1),
(0, 1),
(2, 1),
(-4, 2),
(1, 2),
(-1, 2),
(1, 3),
(-1, 3),
(2, 3),
(-2, 3),
(1, 4),
(-1, 4),
(3, 4),
(-3, 4),
(1, 5),
(2, 5),
(3, 5),
(4, 5),
(-1, 5),
(-2, 5),
(1, 6),
(5, 6),
(-5, 6),
(1, 7),
(-3, 7),
] {
out.push(Polar::Ray { p, q });
}
out
}
fn all_sets() -> impl Iterator<Item = SignSet> {
(0u8..16).map(SignSet)
}
fn atom_index(a: SignSet) -> usize {
ATOMS.iter().position(|&b| b == a).expect("a single atom")
}
type AtomTable = [[SignSet; 4]; 4];
fn observe2<S: Copy>(
samples: &[S],
class: fn(S) -> SignSet,
op: impl Fn(S, S) -> Option<S>,
) -> AtomTable {
let mut seen = [[SignSet::EMPTY; 4]; 4];
for &x in samples {
for &y in samples {
if let Some(z) = op(x, y) {
let (i, j) = (atom_index(class(x)), atom_index(class(y)));
seen[i][j] = seen[i][j].union(class(z));
}
}
}
seen
}
fn observe1<S: Copy>(
samples: &[S],
class: fn(S) -> SignSet,
op: impl Fn(S) -> Option<S>,
) -> [SignSet; 4] {
let mut seen = [SignSet::EMPTY; 4];
for &x in samples {
if let Some(z) = op(x) {
let i = atom_index(class(x));
seen[i] = seen[i].union(class(z));
}
}
seen
}
fn assert_sound_and_tight(what: &str, table: SignSet, observed: SignSet) {
assert!(
observed.is_subset(table),
"{what}: unsound — observed {observed}, table says {table}"
);
assert!(
table.is_subset(observed),
"{what}: not tight — table says {table}, observed only {observed}"
);
}
fn check_binary(name: &str, op: fn(SignSet, SignSet) -> SignSet, seen: &AtomTable) {
for (i, &a) in ATOMS.iter().enumerate() {
for (j, &b) in ATOMS.iter().enumerate() {
assert_sound_and_tight(&format!("{a} {name} {b}"), op(a, b), seen[i][j]);
}
}
let mut pairs = 0;
for a in all_sets() {
for b in all_sets() {
let mut union = SignSet::EMPTY;
for (i, &x) in ATOMS.iter().enumerate() {
for (j, &y) in ATOMS.iter().enumerate() {
if a.contains(x) && b.contains(y) {
union = union.union(seen[i][j]);
}
}
}
assert_eq!(op(a, b), union, "{a} {name} {b}");
pairs += 1;
}
}
assert_eq!(pairs, 256);
}
fn check_unary(name: &str, op: impl Fn(SignSet) -> SignSet, seen: &[SignSet; 4]) {
for (i, &a) in ATOMS.iter().enumerate() {
assert_sound_and_tight(&format!("{name} for x in {a}"), op(a), seen[i]);
}
for s in all_sets() {
let union = ATOMS
.iter()
.zip(seen)
.filter(|(a, _)| s.contains(**a))
.fold(SignSet::EMPTY, |acc, (_, &r)| acc.union(r));
assert_eq!(op(s), union, "{name} for x in {s}");
}
}
#[test]
fn samples_cover_every_atom() {
let cart = cartesian_samples();
let polar = polar_samples();
for atom in ATOMS {
let c = cart.iter().filter(|z| z.class() == atom).count();
let p = polar.iter().filter(|z| z.class() == atom).count();
assert!(c >= 1 && p >= 1, "{atom}: {c} Cartesian, {p} polar samples");
}
}
#[test]
fn add_is_sound_and_tight() {
let seen = observe2(&cartesian_samples(), Cart::class, |x, y| Some(x.add(y)));
check_binary("+", SignSet::add, &seen);
}
#[test]
fn mul_is_sound_and_tight() {
let seen = observe2(&polar_samples(), Polar::class, |x, y| Some(x.mul(y)));
check_binary("·", SignSet::mul, &seen);
}
#[test]
fn unary_ops_are_sound_and_tight() {
let cart = cartesian_samples();
let polar = polar_samples();
let c = |op: fn(Cart) -> Cart| observe1(&cart, Cart::class, move |x| Some(op(x)));
let p = |op: fn(Polar) -> Polar| observe1(&polar, Polar::class, move |x| Some(op(x)));
check_unary("−x", SignSet::neg, &c(Cart::neg));
check_unary("−x", SignSet::neg, &p(Polar::neg));
check_unary("conj x", SignSet::conj, &c(Cart::conj));
check_unary("conj x", SignSet::conj, &p(Polar::conj));
check_unary("|x|", SignSet::abs, &c(Cart::abs));
check_unary("|x|", SignSet::abs, &p(Polar::abs));
check_unary(
"1/x",
SignSet::recip,
&observe1(&polar, Polar::class, Polar::recip),
);
let squares = observe1(&polar, Polar::class, |x| x.pow(2));
check_unary("x²", SignSet::square, &squares);
}
#[test]
fn pow_int_is_sound_and_tight_for_small_exponents() {
let polar = polar_samples();
for n in -5..=5 {
let seen = observe1(&polar, Polar::class, |x| x.pow(n));
check_unary(&format!("x^{n}"), |s| s.pow_int(n), &seen);
}
}
#[test]
fn pow_int_relations() {
for s in all_sets() {
assert_eq!(s.pow_int(1), s, "{s}");
assert_eq!(s.pow_int(-1), s.recip(), "{s}");
assert_eq!(s.pow_int(2), s.square(), "{s}");
let one = if s.is_empty() {
SignSet::EMPTY
} else {
SignSet::POSITIVE
};
assert_eq!(s.pow_int(0), one, "{s}");
let inv = s.recip();
let (mut product, mut inv_product) = (s, inv);
for n in 2..=6 {
product = product.mul(s);
inv_product = inv_product.mul(inv);
assert!(s.pow_int(n).is_subset(product), "{s}^{n}");
assert!(s.pow_int(-n).is_subset(inv_product), "{s}^-{n}");
}
for n in [
7,
8,
1000,
1001,
i64::MAX,
-7,
-8,
-1000,
-1001,
i64::MIN + 1,
i64::MIN,
] {
let m = if n % 2 == 0 { 4 } else { 3 };
let representative = if n > 0 { m } else { -m };
assert_eq!(s.pow_int(n), s.pow_int(representative), "{s}^{n}");
}
if !s.contains(SignSet::ZERO) {
for n in 1..=6 {
assert_eq!(s.pow_int(-n), s.pow_int(n), "{s}^±{n}");
}
}
}
}
struct Property {
name: &'static str,
query: fn(SignSet) -> Truth,
set: SignSet,
holds: fn(Cart) -> bool,
}
#[test]
fn projections_on_all_sets() {
let props = [
Property {
name: "positive",
query: SignSet::is_positive,
set: SignSet::POSITIVE,
holds: |z| z.im == 0.0 && z.re > 0.0,
},
Property {
name: "negative",
query: SignSet::is_negative,
set: SignSet::NEGATIVE,
holds: |z| z.im == 0.0 && z.re < 0.0,
},
Property {
name: "zero",
query: SignSet::is_zero,
set: SignSet::ZERO,
holds: |z| z.im == 0.0 && z.re == 0.0,
},
Property {
name: "nonzero",
query: SignSet::is_nonzero,
set: SignSet::NONZERO,
holds: |z| z.im != 0.0 || z.re != 0.0,
},
Property {
name: "nonnegative",
query: SignSet::is_nonnegative,
set: SignSet::NONNEGATIVE,
holds: |z| z.im == 0.0 && z.re >= 0.0,
},
Property {
name: "nonpositive",
query: SignSet::is_nonpositive,
set: SignSet::NONPOSITIVE,
holds: |z| z.im == 0.0 && z.re <= 0.0,
},
Property {
name: "real",
query: SignSet::is_real,
set: SignSet::REAL,
holds: |z| z.im == 0.0,
},
Property {
name: "nonreal",
query: SignSet::is_nonreal,
set: SignSet::NONREAL,
holds: |z| z.im != 0.0,
},
];
let samples = cartesian_samples();
let mut checked = 0;
for s in all_sets() {
let members: Vec<Cart> = samples
.iter()
.copied()
.filter(|z| s.contains(z.class()))
.collect();
for p in &props {
let all = members.iter().all(|&z| (p.holds)(z));
let any = members.iter().any(|&z| (p.holds)(z));
let expected = match (members.is_empty(), all, any) {
(true, _, _) => Truth::Unknown,
(false, true, _) => Truth::True,
(false, false, false) => Truth::False,
(false, false, true) => Truth::Unknown,
};
assert_eq!((p.query)(s), expected, "{s}.is_{}", p.name);
assert_eq!(s.is_in(p.set), expected, "{s}.is_in({})", p.set);
checked += 1;
}
if !s.is_empty() {
assert_eq!(s.is_nonzero(), s.is_zero().not(), "{s}");
assert_eq!(s.is_nonreal(), s.is_real().not(), "{s}");
}
}
assert_eq!(checked, 16 * 8);
}
fn model(s: SignSet) -> [bool; 4] {
[0, 1, 2, 3].map(|i| (s.0 & (1 << i)) != 0)
}
#[test]
fn set_operations_match_a_membership_model() {
for (i, a) in ATOMS.iter().enumerate() {
assert_eq!(a.0, 1 << i, "{a}");
}
for a in all_sets() {
let ma = model(a);
assert_eq!(a.is_empty(), ma == [false; 4], "{a}");
assert_eq!(a.is_single(), ma.iter().filter(|&&m| m).count() == 1, "{a}");
let atoms: Vec<SignSet> = ATOMS
.iter()
.zip(ma)
.filter(|(_, m)| *m)
.map(|(&x, _)| x)
.collect();
assert_eq!(a.atoms().collect::<Vec<_>>(), atoms, "{a}");
for b in all_sets() {
let mb = model(b);
let subset = (0..4).all(|i| !ma[i] || mb[i]);
assert_eq!(a.is_subset(b), subset, "{a} ⊆ {b}");
assert_eq!(b.contains(a), subset, "{b} ⊇ {a}");
assert_eq!(
model(a.union(b)),
[0, 1, 2, 3].map(|i| ma[i] || mb[i]),
"{a} ∪ {b}"
);
assert_eq!(
model(a.intersect(b)),
[0, 1, 2, 3].map(|i| ma[i] && mb[i]),
"{a} ∩ {b}"
);
}
}
let named = [
(SignSet::EMPTY, [false, false, false, false]),
(SignSet::NEGATIVE, [true, false, false, false]),
(SignSet::ZERO, [false, true, false, false]),
(SignSet::POSITIVE, [false, false, true, false]),
(SignSet::NONREAL, [false, false, false, true]),
(SignSet::REAL, [true, true, true, false]),
(SignSet::NONNEGATIVE, [false, true, true, false]),
(SignSet::NONPOSITIVE, [true, true, false, false]),
(SignSet::NONZERO, [true, false, true, true]),
(SignSet::NONZERO_REAL, [true, false, true, false]),
(SignSet::UNKNOWN, [true, true, true, true]),
];
for (s, members) in named {
assert_eq!(model(s), members, "{s}");
}
}
#[test]
fn ordering_and_i8_conversions() {
let pairs = [
(Ordering::Less, SignSet::NEGATIVE),
(Ordering::Equal, SignSet::ZERO),
(Ordering::Greater, SignSet::POSITIVE),
];
for (o, s) in pairs {
assert_eq!(SignSet::from_ordering(o), s);
assert_eq!(s.to_ordering(), Some(o));
}
for v in i8::MIN..=i8::MAX {
assert_eq!(
SignSet::from_sign_i8(v),
SignSet::from_ordering(v.cmp(&0)),
"{v}"
);
}
for s in all_sets() {
let expected = pairs.iter().find(|&&(_, t)| t == s).map(|&(o, _)| o);
assert_eq!(s.to_ordering(), expected, "{s}");
}
}
#[test]
fn display_and_debug_list_the_atoms() {
assert_eq!(SignSet::EMPTY.to_string(), "{}");
assert_eq!(SignSet::NONPOSITIVE.to_string(), "{<0, 0}");
assert_eq!(SignSet::NONZERO.to_string(), "{<0, >0, nonreal}");
assert_eq!(SignSet::UNKNOWN.to_string(), "{<0, 0, >0, nonreal}");
assert_eq!(format!("{:?}", SignSet::ZERO), "{0}");
let distinct: HashSet<String> = all_sets().map(|s| s.to_string()).collect();
assert_eq!(distinct.len(), 16);
}
#[test]
fn evidence_order_is_explicit() {
for (i, a) in STRONGEST_FIRST.into_iter().enumerate() {
assert_eq!(a.is_proof(), i < 2, "{a:?}");
for (j, b) in STRONGEST_FIRST.into_iter().enumerate() {
assert_eq!(a.at_least(b), i <= j, "{a:?} at least {b:?}");
assert_eq!(a.stronger_than(b), i < j, "{a:?} stronger than {b:?}");
assert_eq!(
a.weaker(b),
STRONGEST_FIRST[i.max(j)],
"weaker of {a:?}, {b:?}"
);
}
}
}
#[test]
fn verdicts_hide_weak_answers() {
for (i, e) in STRONGEST_FIRST.into_iter().enumerate() {
let t = Verdict::new(Truth::True, e);
let s = Verdict::new(SignSet::NEGATIVE, e);
assert_eq!(t.proven(), (i < 2).then_some(Truth::True), "{e:?}");
assert_eq!(s.proven(), (i < 2).then_some(SignSet::NEGATIVE), "{e:?}");
for (j, min) in STRONGEST_FIRST.into_iter().enumerate() {
let enough = i <= j;
assert_eq!(
t.at_least(min),
enough.then_some(Truth::True),
"{e:?} vs {min:?}"
);
assert_eq!(
s.at_least(min),
enough.then_some(SignSet::NEGATIVE),
"{e:?} vs {min:?}"
);
let truth = if enough { Truth::True } else { Truth::Unknown };
assert_eq!(t.truth_at_least(min), truth, "{e:?} vs {min:?}");
let signs = if enough {
SignSet::NEGATIVE
} else {
SignSet::UNKNOWN
};
assert_eq!(s.signs_at_least(min), signs, "{e:?} vs {min:?}");
}
assert_eq!(s.map(SignSet::is_negative), Verdict::new(Truth::True, e));
}
}
}