const IO: u8 = 0b001;
const ALLOC: u8 = 0b010;
const PANIC: u8 = 0b100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct EffectSet(u8);
impl EffectSet {
pub fn pure() -> Self {
Self(0)
}
pub fn full() -> Self {
Self(IO | ALLOC | PANIC)
}
pub fn io() -> Self {
Self(IO)
}
pub fn alloc() -> Self {
Self(ALLOC)
}
pub fn panic() -> Self {
Self(PANIC)
}
pub fn has_io(self) -> bool {
self.0 & IO != 0
}
pub fn has_alloc(self) -> bool {
self.0 & ALLOC != 0
}
pub fn has_panic(self) -> bool {
self.0 & PANIC != 0
}
pub fn is_pure(self) -> bool {
self.0 == 0
}
pub fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub fn is_subset_of(self, other: Self) -> bool {
(self.0 & !other.0) == 0
}
pub fn display(&self) -> String {
if self.is_pure() {
return "pure".to_string();
}
let mut parts: Vec<&'static str> = Vec::with_capacity(3);
if self.has_io() {
parts.push("io");
}
if self.has_alloc() {
parts.push("alloc");
}
if self.has_panic() {
parts.push("panic");
}
parts.join(", ")
}
}
impl core::fmt::Display for EffectSet {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.display())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pure_has_no_flags_set() {
let p = EffectSet::pure();
assert!(p.is_pure());
assert!(!p.has_io());
assert!(!p.has_alloc());
assert!(!p.has_panic());
}
#[test]
fn io_singleton_carries_only_io() {
let e = EffectSet::io();
assert!(e.has_io());
assert!(!e.has_alloc());
assert!(!e.has_panic());
assert!(!e.is_pure());
}
#[test]
fn alloc_singleton_carries_only_alloc() {
let e = EffectSet::alloc();
assert!(!e.has_io());
assert!(e.has_alloc());
assert!(!e.has_panic());
assert!(!e.is_pure());
}
#[test]
fn panic_singleton_carries_only_panic() {
let e = EffectSet::panic();
assert!(!e.has_io());
assert!(!e.has_alloc());
assert!(e.has_panic());
assert!(!e.is_pure());
}
#[test]
fn full_has_every_flag_set() {
let e = EffectSet::full();
assert!(e.has_io());
assert!(e.has_alloc());
assert!(e.has_panic());
assert!(!e.is_pure());
}
#[test]
fn union_with_pure_returns_the_other_set() {
assert_eq!(EffectSet::pure().union(EffectSet::io()), EffectSet::io(),);
assert_eq!(EffectSet::io().union(EffectSet::pure()), EffectSet::io(),);
assert_eq!(
EffectSet::pure().union(EffectSet::full()),
EffectSet::full(),
);
}
#[test]
fn union_combines_distinct_flags() {
let io_alloc = EffectSet::io().union(EffectSet::alloc());
assert!(io_alloc.has_io());
assert!(io_alloc.has_alloc());
assert!(!io_alloc.has_panic());
}
#[test]
fn union_is_commutative() {
let pairs = [
(EffectSet::pure(), EffectSet::io()),
(EffectSet::io(), EffectSet::alloc()),
(EffectSet::alloc(), EffectSet::panic()),
(EffectSet::io(), EffectSet::full()),
(EffectSet::pure(), EffectSet::full()),
];
for (a, b) in pairs {
assert_eq!(
a.union(b),
b.union(a),
"union not commutative on {a:?} and {b:?}"
);
}
}
#[test]
fn union_is_idempotent() {
let samples = [
EffectSet::pure(),
EffectSet::io(),
EffectSet::alloc(),
EffectSet::panic(),
EffectSet::io().union(EffectSet::alloc()),
EffectSet::full(),
];
for s in samples {
assert_eq!(s.union(s), s, "union not idempotent on {s:?}");
}
}
#[test]
fn is_subset_of_matches_lattice_subset_semantics() {
assert!(EffectSet::pure().is_subset_of(EffectSet::full()));
assert!(!EffectSet::full().is_subset_of(EffectSet::pure()));
let io_alloc = EffectSet::io().union(EffectSet::alloc());
assert!(EffectSet::io().is_subset_of(io_alloc));
assert!(!io_alloc.is_subset_of(EffectSet::io()));
assert!(!EffectSet::io().is_subset_of(EffectSet::alloc()));
assert!(!EffectSet::alloc().is_subset_of(EffectSet::io()));
}
#[test]
fn is_subset_of_is_reflexive() {
let samples = [
EffectSet::pure(),
EffectSet::io(),
EffectSet::alloc(),
EffectSet::panic(),
EffectSet::io().union(EffectSet::alloc()),
EffectSet::io().union(EffectSet::panic()),
EffectSet::alloc().union(EffectSet::panic()),
EffectSet::full(),
];
for s in samples {
assert!(s.is_subset_of(s), "subset reflexivity failure on {s:?}");
}
}
#[test]
fn display_renders_each_set_in_fixed_io_alloc_panic_order() {
assert_eq!(EffectSet::pure().display(), "pure");
assert_eq!(EffectSet::io().display(), "io");
assert_eq!(EffectSet::alloc().display(), "alloc");
assert_eq!(EffectSet::panic().display(), "panic");
assert_eq!(
EffectSet::io().union(EffectSet::alloc()).display(),
"io, alloc",
);
assert_eq!(
EffectSet::alloc().union(EffectSet::io()).display(),
"io, alloc",
);
assert_eq!(
EffectSet::io().union(EffectSet::panic()).display(),
"io, panic",
);
assert_eq!(
EffectSet::alloc().union(EffectSet::panic()).display(),
"alloc, panic",
);
assert_eq!(EffectSet::full().display(), "io, alloc, panic");
}
#[test]
fn display_trait_matches_display_method() {
let cases = [
EffectSet::pure(),
EffectSet::io(),
EffectSet::alloc(),
EffectSet::panic(),
EffectSet::io().union(EffectSet::alloc()),
EffectSet::full(),
];
for e in cases {
assert_eq!(format!("{e}"), e.display(), "Display mismatch on {e:?}");
}
}
#[test]
fn equality_distinguishes_distinct_sets() {
assert_eq!(EffectSet::pure(), EffectSet::pure());
assert_ne!(EffectSet::io(), EffectSet::alloc());
assert_ne!(EffectSet::pure(), EffectSet::io());
assert_eq!(
EffectSet::io().union(EffectSet::alloc()),
EffectSet::alloc().union(EffectSet::io()),
);
assert_eq!(EffectSet::default(), EffectSet::pure());
}
#[test]
fn monotonic_ascent_converges_in_at_most_three_rounds() {
let mut e = EffectSet::pure();
let target = EffectSet::full();
let mut rounds = 0;
while e != target {
e = e.union(target);
rounds += 1;
assert!(
rounds <= 3,
"should converge within 3 rounds; took {rounds}"
);
}
assert!(rounds <= 3);
let chain = [EffectSet::io(), EffectSet::alloc(), EffectSet::panic()];
let mut e = EffectSet::pure();
let mut rounds = 0;
for step in chain {
e = e.union(step);
rounds += 1;
}
assert_eq!(e, EffectSet::full());
assert_eq!(rounds, 3);
}
}