Skip to main content

argumentation/
framework.rs

1//! Abstract argumentation framework: arguments and attack relations.
2
3use petgraph::Direction;
4use petgraph::graph::{DiGraph, NodeIndex};
5use std::collections::HashMap;
6use std::hash::Hash;
7
8/// A Dung-style argumentation framework over argument type `A`.
9#[derive(Debug, Clone)]
10pub struct ArgumentationFramework<A: Clone + Eq + Hash> {
11    graph: DiGraph<A, ()>,
12    index: HashMap<A, NodeIndex>,
13}
14
15impl<A: Clone + Eq + Hash> ArgumentationFramework<A> {
16    /// Create an empty framework.
17    pub fn new() -> Self {
18        Self {
19            graph: DiGraph::new(),
20            index: HashMap::new(),
21        }
22    }
23
24    /// Add an argument. Adding an argument that already exists is a no-op.
25    pub fn add_argument(&mut self, a: A) {
26        if !self.index.contains_key(&a) {
27            let idx = self.graph.add_node(a.clone());
28            self.index.insert(a, idx);
29        }
30    }
31
32    /// Add an attack from `attacker` to `target`. Both must already be present.
33    ///
34    /// Adding the same attack edge twice is a no-op: `ArgumentationFramework`
35    /// does not model weighted or multi-edge attacks.
36    ///
37    /// The `Debug` bound on `A` lets the error message name the offending
38    /// argument when one is missing. Arguments are taken by reference to avoid
39    /// cloning owned types like `String` or `Arc<T>` on every call.
40    pub fn add_attack(&mut self, attacker: &A, target: &A) -> Result<(), crate::Error>
41    where
42        A: std::fmt::Debug,
43    {
44        let a = *self
45            .index
46            .get(attacker)
47            .ok_or_else(|| crate::Error::ArgumentNotFound(format!("{:?}", attacker)))?;
48        let t = *self
49            .index
50            .get(target)
51            .ok_or_else(|| crate::Error::ArgumentNotFound(format!("{:?}", target)))?;
52        // Dedupe: petgraph's DiGraph is a multigraph; we want at most one
53        // edge per (attacker, target) pair.
54        if self.graph.find_edge(a, t).is_none() {
55            self.graph.add_edge(a, t, ());
56        }
57        Ok(())
58    }
59
60    /// Iterate over all arguments in the framework.
61    pub fn arguments(&self) -> impl Iterator<Item = &A> {
62        self.graph.node_weights()
63    }
64
65    /// Number of arguments currently in the framework.
66    ///
67    /// Constant-time. Useful for size-gating exponential enumerators
68    /// against [`crate::ENUMERATION_LIMIT`] before calling them.
69    #[must_use]
70    pub fn len(&self) -> usize {
71        self.graph.node_count()
72    }
73
74    /// Whether the framework contains zero arguments.
75    #[must_use]
76    pub fn is_empty(&self) -> bool {
77        self.graph.node_count() == 0
78    }
79
80    /// Return the arguments that attack `a`.
81    pub fn attackers(&self, a: &A) -> Vec<&A> {
82        let Some(&idx) = self.index.get(a) else {
83            return Vec::new();
84        };
85        self.graph
86            .neighbors_directed(idx, Direction::Incoming)
87            .map(|n| &self.graph[n])
88            .collect()
89    }
90
91    /// Return the arguments attacked by `a`.
92    pub fn attacked_by(&self, a: &A) -> Vec<&A> {
93        let Some(&idx) = self.index.get(a) else {
94            return Vec::new();
95        };
96        self.graph
97            .neighbors_directed(idx, Direction::Outgoing)
98            .map(|n| &self.graph[n])
99            .collect()
100    }
101}
102
103impl<A: Clone + Eq + Hash> Default for ArgumentationFramework<A> {
104    fn default() -> Self {
105        Self::new()
106    }
107}
108
109// Compile-time guarantee: the canonical owned-string framework is
110// thread-safe. Consumers (e.g. encounter's multi-character resolution)
111// rely on being able to ship a framework across threads.
112const _: fn() = || {
113    fn assert_send<T: Send>() {}
114    fn assert_sync<T: Sync>() {}
115    assert_send::<ArgumentationFramework<String>>();
116    assert_sync::<ArgumentationFramework<String>>();
117};
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn empty_framework_has_no_arguments() {
125        let af: ArgumentationFramework<&str> = ArgumentationFramework::new();
126        assert_eq!(af.arguments().count(), 0);
127    }
128
129    #[test]
130    fn add_argument_adds_one() {
131        let mut af = ArgumentationFramework::new();
132        af.add_argument("a");
133        assert_eq!(af.arguments().count(), 1);
134    }
135
136    #[test]
137    fn add_attack_creates_edge() {
138        let mut af = ArgumentationFramework::new();
139        af.add_argument("a");
140        af.add_argument("b");
141        af.add_attack(&"a", &"b").unwrap();
142        assert_eq!(af.attackers(&"b").len(), 1);
143        assert!(af.attackers(&"b").iter().any(|x| **x == "a"));
144    }
145
146    #[test]
147    fn add_attack_is_idempotent() {
148        let mut af = ArgumentationFramework::new();
149        af.add_argument("a");
150        af.add_argument("b");
151        af.add_attack(&"a", &"b").unwrap();
152        af.add_attack(&"a", &"b").unwrap();
153        // Duplicate attack must not create a second edge.
154        assert_eq!(af.attackers(&"b").len(), 1);
155    }
156
157    #[test]
158    fn add_attack_on_missing_argument_reports_which() {
159        let mut af: ArgumentationFramework<&str> = ArgumentationFramework::new();
160        af.add_argument("a");
161        let err = af.add_attack(&"a", &"missing").unwrap_err();
162        // Error message must include the offending argument, not just the type.
163        let msg = err.to_string();
164        assert!(
165            msg.contains("missing"),
166            "error should mention the missing argument, got: {}",
167            msg
168        );
169    }
170
171    #[test]
172    fn self_attack_is_allowed() {
173        let mut af = ArgumentationFramework::new();
174        af.add_argument("a");
175        af.add_attack(&"a", &"a").unwrap();
176        assert_eq!(af.attackers(&"a").len(), 1);
177    }
178
179    #[test]
180    fn add_argument_is_idempotent() {
181        let mut af = ArgumentationFramework::new();
182        af.add_argument("a");
183        af.add_argument("a");
184        assert_eq!(af.arguments().count(), 1);
185    }
186}