Skip to main content

argumentation_bipolar/
framework.rs

1//! `BipolarFramework<A>`: arguments, attacks, and supports.
2//!
3//! Stores the framework as two independent directed edge sets over the
4//! same node set. The node set is the union of all distinct arguments
5//! introduced via [`BipolarFramework::add_argument`] or as an endpoint
6//! of a call to [`BipolarFramework::add_attack`] or
7//! [`BipolarFramework::add_support`].
8
9use crate::error::Error;
10use std::collections::{HashMap, HashSet};
11use std::hash::Hash;
12
13/// A bipolar argumentation framework over argument type `A`.
14///
15/// The type is generic over `A` to match the core crate's convention —
16/// `A` can be `String`, `&'static str`, a custom `ArgumentId` newtype, etc.
17#[derive(Debug, Clone)]
18pub struct BipolarFramework<A: Clone + Eq + Hash> {
19    arguments: HashSet<A>,
20    attacks: HashSet<(A, A)>,
21    supports: HashSet<(A, A)>,
22}
23
24impl<A: Clone + Eq + Hash> BipolarFramework<A> {
25    /// Create an empty framework.
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            arguments: HashSet::new(),
30            attacks: HashSet::new(),
31            supports: HashSet::new(),
32        }
33    }
34
35    /// Add an argument. Adding an argument that already exists is a no-op.
36    pub fn add_argument(&mut self, a: A) {
37        self.arguments.insert(a);
38    }
39
40    /// Add an attack `attacker → target`. Both arguments are implicitly
41    /// added to the framework if not already present. Adding the same
42    /// attack twice is a no-op.
43    pub fn add_attack(&mut self, attacker: A, target: A) {
44        self.arguments.insert(attacker.clone());
45        self.arguments.insert(target.clone());
46        self.attacks.insert((attacker, target));
47    }
48
49    /// Add a support `supporter → supported`. Both arguments are
50    /// implicitly added. Adding the same support twice is a no-op.
51    /// Returns [`Error::IllegalSelfSupport`] if `supporter == supported`
52    /// — an argument cannot be its own necessary supporter.
53    pub fn add_support(&mut self, supporter: A, supported: A) -> Result<(), Error>
54    where
55        A: std::fmt::Debug,
56    {
57        if supporter == supported {
58            return Err(Error::IllegalSelfSupport(format!("{:?}", supporter)));
59        }
60        self.arguments.insert(supporter.clone());
61        self.arguments.insert(supported.clone());
62        self.supports.insert((supporter, supported));
63        Ok(())
64    }
65
66    /// Remove a support edge. Returns true if the edge was present.
67    /// Used by consumers modelling betrayal (a support edge is retracted).
68    /// Does NOT remove the endpoint arguments from the framework.
69    pub fn remove_support(&mut self, supporter: &A, supported: &A) -> bool {
70        self.supports
71            .remove(&(supporter.clone(), supported.clone()))
72    }
73
74    /// Remove an attack edge. Returns true if the edge was present.
75    pub fn remove_attack(&mut self, attacker: &A, target: &A) -> bool {
76        self.attacks.remove(&(attacker.clone(), target.clone()))
77    }
78
79    /// Iterate over all arguments in the framework.
80    pub fn arguments(&self) -> impl Iterator<Item = &A> {
81        self.arguments.iter()
82    }
83
84    /// Iterate over all direct attack edges.
85    pub fn attacks(&self) -> impl Iterator<Item = (&A, &A)> {
86        self.attacks.iter().map(|(a, b)| (a, b))
87    }
88
89    /// Iterate over all direct support edges.
90    pub fn supports(&self) -> impl Iterator<Item = (&A, &A)> {
91        self.supports.iter().map(|(a, b)| (a, b))
92    }
93
94    /// Number of arguments in the framework.
95    #[must_use]
96    pub fn len(&self) -> usize {
97        self.arguments.len()
98    }
99
100    /// Whether the framework has zero arguments.
101    #[must_use]
102    pub fn is_empty(&self) -> bool {
103        self.arguments.is_empty()
104    }
105
106    /// Direct attackers of `a` (arguments `X` such that `X → a` in the
107    /// attack edge set). Does NOT include derived attackers — see
108    /// [`crate::derived`] for the closure.
109    pub fn direct_attackers(&self, a: &A) -> Vec<&A> {
110        self.attacks
111            .iter()
112            .filter(|(_, target)| target == a)
113            .map(|(attacker, _)| attacker)
114            .collect()
115    }
116
117    /// Direct supporters of `a` (arguments `X` such that `X → a` in the
118    /// support edge set).
119    pub fn direct_supporters(&self, a: &A) -> Vec<&A> {
120        self.supports
121            .iter()
122            .filter(|(_, target)| target == a)
123            .map(|(supporter, _)| supporter)
124            .collect()
125    }
126
127    /// Map of each argument to its direct necessary supporters.
128    ///
129    /// Used by the support-closure filter in [`crate::semantics`] and
130    /// by [`crate::queries`] for transitive queries.
131    pub fn supporter_map(&self) -> HashMap<&A, HashSet<&A>> {
132        let mut map: HashMap<&A, HashSet<&A>> =
133            self.arguments.iter().map(|a| (a, HashSet::new())).collect();
134        for (supporter, supported) in &self.supports {
135            map.entry(supported).or_default().insert(supporter);
136        }
137        map
138    }
139}
140
141impl<A: Clone + Eq + Hash> Default for BipolarFramework<A> {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147// Compile-time guarantee that the canonical owned-string bipolar
148// framework is thread-safe, matching the core crate's guarantee.
149const _: fn() = || {
150    fn assert_send<T: Send>() {}
151    fn assert_sync<T: Sync>() {}
152    assert_send::<BipolarFramework<String>>();
153    assert_sync::<BipolarFramework<String>>();
154};
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn empty_framework_has_no_arguments() {
162        let bf: BipolarFramework<&str> = BipolarFramework::new();
163        assert!(bf.is_empty());
164        assert_eq!(bf.len(), 0);
165    }
166
167    #[test]
168    fn add_argument_is_idempotent() {
169        let mut bf = BipolarFramework::new();
170        bf.add_argument("a");
171        bf.add_argument("a");
172        assert_eq!(bf.len(), 1);
173    }
174
175    #[test]
176    fn add_attack_registers_both_endpoints() {
177        let mut bf = BipolarFramework::new();
178        bf.add_attack("a", "b");
179        assert_eq!(bf.len(), 2);
180        assert_eq!(bf.direct_attackers(&"b"), vec![&"a"]);
181        assert!(bf.direct_attackers(&"a").is_empty());
182    }
183
184    #[test]
185    fn add_support_registers_both_endpoints() {
186        let mut bf = BipolarFramework::new();
187        bf.add_support("a", "b").unwrap();
188        assert_eq!(bf.len(), 2);
189        assert_eq!(bf.direct_supporters(&"b"), vec![&"a"]);
190    }
191
192    #[test]
193    fn self_support_is_rejected() {
194        let mut bf: BipolarFramework<&str> = BipolarFramework::new();
195        let err = bf.add_support("a", "a").unwrap_err();
196        assert!(matches!(err, Error::IllegalSelfSupport(_)));
197    }
198
199    #[test]
200    fn remove_support_returns_whether_edge_was_present() {
201        let mut bf = BipolarFramework::new();
202        bf.add_support("a", "b").unwrap();
203        assert!(bf.remove_support(&"a", &"b"));
204        assert!(!bf.remove_support(&"a", &"b"));
205        // Arguments stay in the framework even after the edge is removed.
206        assert_eq!(bf.len(), 2);
207    }
208
209    #[test]
210    fn supporter_map_includes_all_arguments_even_unsupported_ones() {
211        let mut bf = BipolarFramework::new();
212        bf.add_argument("a");
213        bf.add_support("b", "c").unwrap();
214        let map = bf.supporter_map();
215        assert_eq!(map.len(), 3);
216        assert!(map[&"a"].is_empty());
217        assert!(map[&"b"].is_empty());
218        assert_eq!(map[&"c"].len(), 1);
219        assert!(map[&"c"].contains(&"b"));
220    }
221}