From 33f8ec1acb1af7b86475509752e5a5908f5b783e Mon Sep 17 00:00:00 2001 From: Langston Barrett Date: Sat, 19 Nov 2022 09:59:31 -0500 Subject: [PATCH] Initial commit --- .github/workflows/ci.yml | 29 +++++ .github/workflows/release.yml | 37 ++++++ .gitignore | 2 + Cargo.toml | 23 ++++ LICENSE | 21 ++++ README.md | 6 + nix/shell.nix | 7 ++ src/dag.rs | 181 ++++++++++++++++++++++++++++ src/lib.rs | 14 +++ src/pairs.rs | 221 ++++++++++++++++++++++++++++++++++ src/trait.rs | 36 ++++++ 11 files changed, 577 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 nix/shell.nix create mode 100644 src/dag.rs create mode 100644 src/lib.rs create mode 100644 src/pairs.rs create mode 100644 src/trait.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7b61054 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +env: + # The NAME makes it easier to copy/paste snippets from other CI configs + NAME: fin-part-ord + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: cargo fmt -- --check + - run: | + rustup update + rustup component add clippy + - run: cargo clippy -- -D warnings + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: cargo build + - run: env QUICKCHECK_GENERATOR_SIZE=32 cargo test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dc40b07 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Release + +on: + push: + branches: + - release* + tags: + - 'v*' + +env: + # The NAME makes it easier to copy/paste snippets from other CI configs + NAME: fin-part-ord + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: ncipollo/release-action@v1 + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + with: + body: "See [CHANGELOG.md](https://github.com/langston-barrett/${NAME}/blob/main/CHANGELOG.md)" + draft: true + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to crates.io + env: + CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} + # Only push on actual release tags + PUSH: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + if [[ ${PUSH} == true ]]; then + cargo publish --token ${CRATES_IO_TOKEN} + else + cargo publish --dry-run --token ${CRATES_IO_TOKEN} + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..52a604b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "fin-part-ord" +version = "0.0.0" +edition = "2021" +description = "Datatype for finite partial orders" +keywords = ["datatype", "finite", "partial-order", "ordering"] +authors = ["Langston Barrett "] +license = "MIT" +readme = "README.md" +homepage = "https://github.com/langston-barrett/fin-part-ord" +repository = "https://github.com/langston-barrett/fin-part-ord" + +[dev-dependencies] +quickcheck = "1" + +[features] +default = ["dag", "pairs"] +dag = ["dep:daggy", "dep:petgraph"] +pairs = [] + +[dependencies] +daggy = { optional = true, version = "0.8" } +petgraph = { optional = true, version = "0.6" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21edc8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright © 2022 Brian Langston Barrett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6173dcb --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Finite Partial Orders + +This crate provides a trait and datatypes for representing finite partial +orders. See [the API documentation][api] for more information. + +[api]: https://docs.rs/fin-part-ord/0.1.0/ \ No newline at end of file diff --git a/nix/shell.nix b/nix/shell.nix new file mode 100644 index 0000000..4a2d53e --- /dev/null +++ b/nix/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import { } +, unstable ? import { } +}: + +pkgs.mkShell { + nativeBuildInputs = [ pkgs.rust-analyzer pkgs.rustup ]; +} diff --git a/src/dag.rs b/src/dag.rs new file mode 100644 index 0000000..4d0b630 --- /dev/null +++ b/src/dag.rs @@ -0,0 +1,181 @@ +use std::collections::HashMap; +use std::hash::Hash; + +use daggy::{Dag, NodeIndex, WouldCycle}; +use petgraph::visit::Dfs; + +use crate::r#trait::FinPartOrd; + +/// A datatype for representing finite partial orders. +/// +/// Stores partial order as a [`Dag`]. Doesn't store edges that can be deduced +/// by reflexivity or transitivity. Maintains the invariant that it always +/// represents a valid partial order, specifically that the set of edges obeys +/// antisymmetry, that is, forms a DAG. +#[derive(Clone, Debug)] +struct DagPartOrd { + dag: Dag, + ids: HashMap, +} + +impl FinPartOrd for DagPartOrd +where + T: Clone, + T: Eq, + T: Hash, +{ + type Error = WouldCycle<()>; + + #[must_use] + fn empty() -> Self { + DagPartOrd { + dag: Dag::new(), + ids: HashMap::new(), + } + } + + fn add(mut self, lo: T, hi: T) -> Result { + if lo == hi { + return Ok(self); + } + let lo_idx = match self.ids.get(&lo) { + Some(lo_idx) => *lo_idx, + None => { + let id = self.dag.add_node(lo.clone()); + self.ids.insert(lo, id); + id + } + }; + let hi_idx = match self.ids.get(&hi) { + Some(hi_idx) => *hi_idx, + None => { + let id = self.dag.add_node(hi.clone()); + self.ids.insert(hi, id); + id + } + }; + self.dag.add_edge(lo_idx, hi_idx, ())?; + Ok(self) + } + + fn lt(&self, lo: &T, hi: &T) -> Result { + match (self.ids.get(lo), self.ids.get(hi)) { + (Some(lo_idx), Some(hi_idx)) => { + let mut dfs = Dfs::new(&self.dag, *lo_idx); + while let Some(n) = dfs.next(&self.dag) { + if n == *hi_idx { + return Ok(true); + } + } + Ok(false) + } + _ => Ok(false), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use quickcheck::{quickcheck, Arbitrary, Gen}; + + impl Arbitrary for DagPartOrd { + fn arbitrary(g: &mut Gen) -> Self { + let mut ppo = DagPartOrd::empty(); + let pairs = Vec::<(u8, u8)>::arbitrary(g); + for (x, y) in pairs { + if x <= y { + ppo = ppo.add(x, y).unwrap(); + } else { + ppo = ppo.add(y, x).unwrap(); + } + } + ppo + } + + fn shrink(&self) -> Box> { + let mut iters = Vec::new(); + match self.dag.graph().node_indices().next() { + Some(ix) => { + let mut new = self.clone(); + new.dag.remove_node(ix); + iters.push(new); + } + None => (), + } + Box::new(iters.into_iter()) + } + } + + #[test] + fn empty() { + let _ = DagPartOrd::<()>::empty(); + } + + #[test] + fn push_unit() { + let unit = (); + DagPartOrd::empty().add(&unit, &unit).unwrap(); + } + + #[test] + fn strings() { + let mut ppo = DagPartOrd::empty(); + ppo = ppo.add("x".to_string(), "y".to_string()).unwrap(); + ppo = ppo.add("y".to_string(), "z".to_string()).unwrap(); + assert!(ppo.le(&"x".to_string(), &"z".to_string()).unwrap()); + } + + quickcheck! { + fn antisymmetric_two(x: u8, y: u8) -> bool { + if x == y { + return true; + } + let mut ppo = DagPartOrd::empty(); + ppo = ppo.add(&x, &y).unwrap(); + println!("{:?}", ppo); + ppo.add(&y, &x).is_err() + } + + fn transitive_three(x: u8, y: u8, z: u8) -> bool { + if x == z { + return true; + } + let mut ppo = DagPartOrd::empty(); + ppo = ppo.add(x, y).unwrap(); + ppo = ppo.add(y, z).unwrap(); + ppo.le(&x, &z).unwrap() + } + + fn add_le(ppo: DagPartOrd, x: u8, y: u8) -> bool { + match ppo.add(x, y) { + Err(_) => true, + Ok(ppo) => { + ppo.le(&x, &y).unwrap() && (!ppo.le(&y, &x).unwrap() || x == y) + } + } + } + + + fn reflexive(ppo: DagPartOrd, x: u8) -> bool { + ppo.le(&x, &x).unwrap() + } + + fn antisymmetric(ppo: DagPartOrd, x: u8, y: u8) -> bool { + if ppo.le(&x, &y).unwrap() && ppo.le(&y, &x).unwrap() { + x == y + } else { + true + } + } + + fn transitive(ppo: DagPartOrd, x: u8, y: u8, z: u8) -> bool { + if ppo.le(&x, &y).unwrap() && ppo.le(&y, &z).unwrap() { + ppo.le(&x, &z).unwrap() + } else { + true + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3935e3a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +//! [FinPartOrd] is a trait for representing finite partial orders. + +#[cfg(feature = "dag")] +mod dag; +#[cfg(feature = "dag")] +pub use dag::*; + +#[cfg(feature = "pairs")] +mod pairs; +#[cfg(feature = "pairs")] +pub use pairs::*; + +mod r#trait; +pub use r#trait::*; diff --git a/src/pairs.rs b/src/pairs.rs new file mode 100644 index 0000000..627a4af --- /dev/null +++ b/src/pairs.rs @@ -0,0 +1,221 @@ +use crate::r#trait::FinPartOrd; + +#[derive(Clone, Debug)] +struct Pair { + lo: T, + hi: T, +} + +impl Pair { + fn new(lo: T, hi: T) -> Self { + Pair { lo, hi } + } +} + +/// A datatype for representing finite partial orders. +/// +/// Stores partial orders as a vector of pairs. Doesn't store pairs that can +/// be deduced by reflexivity or transitivity. Maintains the invariant that it +/// always represents a valid partial order, specifically that the set of pairs +/// obeys antisymmetry. Provides poor computational complexity bounds but low +/// memory usage. Only requires that the contained type is `Eq`. +/// +/// Not suitable for production use. +#[derive(Clone, Debug)] +pub struct PairPartOrd +where + T: Eq, +{ + pairs: Vec>, +} + +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AntisymmetryError; + +impl FinPartOrd for PairPartOrd +where + T: Clone, +{ + type Error = AntisymmetryError; + + #[must_use] + fn empty() -> Self { + let fpo = PairPartOrd { pairs: Vec::new() }; + debug_assert!(fpo.valid()); + fpo + } + + fn add(mut self, lo: T, hi: T) -> Result { + if lo == hi { + return Ok(self); + } + self.pairs.push(Pair::new(lo, hi)); + if !self.valid() { + return Err(AntisymmetryError); + } + Ok(self) + } + + fn lt(&self, lo: &T, hi: &T) -> Result { + for p in &self.pairs { + if &p.lo == lo { + if &p.hi == hi { + return Ok(true); + } + // DFS + if let Ok(b) = self.lt(&p.hi, hi) { + if b { + return Ok(true); + } + } + } + } + Ok(false) + } +} + +impl PairPartOrd +where + T: Clone, +{ + pub fn is_empty(&self) -> bool { + self.pairs.is_empty() + } + + /// For debugging only, return value should be considered unstable. + pub fn len(&self) -> usize { + self.pairs.len() + } + + pub fn reserve(&mut self, additional: usize) { + self.pairs.reserve(additional); + } + + fn check(&self) -> Result { + for p in &self.pairs { + if p.lo == p.hi { + return Ok(false); + } + if self.lt(&p.lo, &p.lo)? || self.lt(&p.hi, &p.hi)? { + return Ok(false); + } + } + Ok(true) + } + + fn valid(&self) -> bool { + self.check().unwrap_or(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use quickcheck::{quickcheck, Arbitrary, Gen}; + + impl Arbitrary for PairPartOrd { + fn arbitrary(g: &mut Gen) -> Self { + let mut ppo = PairPartOrd::empty(); + let pairs = Vec::<(u8, u8)>::arbitrary(g); + for (x, y) in pairs { + if x <= y { + ppo = ppo.add(x, y).unwrap(); + } else { + ppo = ppo.add(y, x).unwrap(); + } + } + ppo + } + + fn shrink(&self) -> Box> { + let mut iters = Vec::new(); + for i in 0..self.pairs.len() { + let mut pairs = self.pairs.clone(); + pairs.remove(i); + iters.push(PairPartOrd { pairs }); + } + Box::new(iters.into_iter()) + } + } + + #[test] + fn empty_valid() { + let mut empty = PairPartOrd::<()>::empty(); + assert!(empty.valid()); + empty.reserve(8); + assert!(empty.valid()); + } + + #[test] + fn push_unit_valid() { + let unit = (); + let mut ppo = PairPartOrd::empty(); + ppo = ppo.add(&unit, &unit).unwrap(); + assert!(ppo.valid()); + ppo = ppo.add(&unit, &unit).unwrap(); + assert!(ppo.valid()); + } + + #[test] + fn strings() { + let mut ppo = PairPartOrd::empty(); + ppo = ppo.add("x".to_string(), "y".to_string()).unwrap(); + ppo = ppo.add("y".to_string(), "z".to_string()).unwrap(); + assert!(ppo.le(&"x".to_string(), &"z".to_string()).unwrap()); + } + + quickcheck! { + fn antisymmetric_two(x: u8, y: u8) -> bool { + if x == y { + return true; + } + let mut ppo = PairPartOrd::empty(); + ppo = ppo.add(&x, &y).unwrap(); + assert!(ppo.valid()); + ppo.add(&y, &x).is_err() + } + + fn transitive_three(x: u8, y: u8, z: u8) -> bool { + if x == z { + return true; + } + let mut ppo = PairPartOrd::empty(); + ppo = ppo.add(x, y).unwrap(); + ppo = ppo.add(y, z).unwrap(); + assert!(ppo.valid()); + ppo.le(&x, &z).unwrap() + } + + // TODO: Stack overflow :-( + // fn add_le(ppo: PairPartOrd, x: u8, y: u8) -> bool { + // match ppo.add(x, y) { + // Err(_) => true, + // Ok(ppo) => { + // ppo.le(&x, &y).unwrap() && (!ppo.le(&y, &x).unwrap() || x == y) + // } + // } + // } + + + fn reflexive(ppo: PairPartOrd, x: u8) -> bool { + ppo.le(&x, &x).unwrap() + } + + fn antisymmetric(ppo: PairPartOrd, x: u8, y: u8) -> bool { + if ppo.le(&x, &y).unwrap() && ppo.le(&y, &x).unwrap() { + x == y + } else { + true + } + } + + fn transitive(ppo: PairPartOrd, x: u8, y: u8, z: u8) -> bool { + if ppo.le(&x, &y).unwrap() && ppo.le(&y, &z).unwrap() { + ppo.le(&x, &z).unwrap() + } else { + true + } + } + } +} diff --git a/src/trait.rs b/src/trait.rs new file mode 100644 index 0000000..cdbf389 --- /dev/null +++ b/src/trait.rs @@ -0,0 +1,36 @@ +/// Laws: +/// +/// - Reflexivity: `self.le(x, x) == true` +/// - Antisymmetry: `self.le(x, y) && self.le(y, x) ==> x == y` +/// - Transitivity: `self.le(x, y) && self.le(y, z) ==> self.le(x, z)` +/// - Compatibility: `self.le(x, y) <==> x == y || self.lt(x, y)` +/// - Add/less: `{ self.add(x, y); self.le(x, y) } == true` +pub trait FinPartOrd +where + Self: Sized, +{ + type Error; + + #[must_use] + fn empty() -> Self; + + fn add(self, lo: T, hi: T) -> Result; + + /// Check if one element is less than another. + /// + /// May return `true` when `lo == hi`, even if that element hasn't been + /// explicitly added. + fn lt(&self, lo: &T, hi: &T) -> Result; + + /// May return `true` when `lo == hi`, even if that element hasn't been + /// explicitly added. + fn le(&self, lo: &T, hi: &T) -> Result + where + T: PartialEq, + { + if lo == hi { + return Ok(true); + } + self.lt(lo, hi) + } +}