argumentation/aspic/
argument.rs1use super::kb::{KnowledgeBase, Premise};
18use super::language::Literal;
19use super::rules::{Rule, RuleId};
20use std::collections::{HashMap, HashSet};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct ArgumentId(pub usize);
25
26impl std::fmt::Display for ArgumentId {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "A{}", self.0)
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct Argument {
35 pub id: ArgumentId,
37 pub conclusion: Literal,
39 pub origin: Origin,
41 pub sub_arguments: Vec<ArgumentId>,
43}
44
45#[derive(Debug, Clone)]
47pub enum Origin {
48 Premise(Premise),
50 RuleApplication(RuleId),
52}
53
54impl Argument {
55 pub fn is_premise(&self) -> bool {
57 matches!(self.origin, Origin::Premise(_))
58 }
59
60 pub fn top_rule_is_defeasible(&self, rules: &[Rule]) -> bool {
62 if let Origin::RuleApplication(rid) = &self.origin {
63 rules
64 .iter()
65 .find(|r| r.id == *rid)
66 .map(|r| r.is_defeasible())
67 .unwrap_or(false)
68 } else {
69 false
70 }
71 }
72}
73
74fn rule_set_has_cycle(rules: &[Rule]) -> bool {
80 let mut deps: HashMap<RuleId, Vec<RuleId>> = HashMap::new();
81 for r in rules {
82 let r_deps: Vec<RuleId> = rules
83 .iter()
84 .filter(|s| r.premises.contains(&s.conclusion))
85 .map(|s| s.id)
86 .collect();
87 deps.insert(r.id, r_deps);
88 }
89 let mut visited: HashSet<RuleId> = HashSet::new();
90 let mut on_stack: HashSet<RuleId> = HashSet::new();
91 for r in rules {
92 if !visited.contains(&r.id) && dfs_has_cycle(r.id, &deps, &mut visited, &mut on_stack) {
93 return true;
94 }
95 }
96 false
97}
98
99fn dfs_has_cycle(
100 node: RuleId,
101 deps: &HashMap<RuleId, Vec<RuleId>>,
102 visited: &mut HashSet<RuleId>,
103 on_stack: &mut HashSet<RuleId>,
104) -> bool {
105 visited.insert(node);
106 on_stack.insert(node);
107 if let Some(neighbors) = deps.get(&node) {
108 for &next in neighbors {
109 if !visited.contains(&next) {
110 if dfs_has_cycle(next, deps, visited, on_stack) {
111 return true;
112 }
113 } else if on_stack.contains(&next) {
114 return true;
115 }
116 }
117 }
118 on_stack.remove(&node);
119 false
120}
121
122pub fn construct_arguments(
128 kb: &KnowledgeBase,
129 rules: &[Rule],
130) -> Result<Vec<Argument>, crate::Error> {
131 if rule_set_has_cycle(rules) {
132 return Err(crate::Error::Aspic(
133 "cyclic rule dependencies detected; forward chaining would not terminate".into(),
134 ));
135 }
136 let mut arguments: Vec<Argument> = Vec::new();
137 let mut next_id = 0usize;
138 for p in kb.premises() {
140 arguments.push(Argument {
141 id: ArgumentId(next_id),
142 conclusion: p.literal().clone(),
143 origin: Origin::Premise(p.clone()),
144 sub_arguments: Vec::new(),
145 });
146 next_id += 1;
147 }
148 loop {
150 let before = arguments.len();
151 let mut by_concl: HashMap<Literal, Vec<ArgumentId>> = HashMap::new();
153 for a in &arguments {
154 by_concl.entry(a.conclusion.clone()).or_default().push(a.id);
155 }
156 for rule in rules {
158 let sub_options: Vec<&[ArgumentId]> = rule
159 .premises
160 .iter()
161 .map(|p| by_concl.get(p).map(|v| v.as_slice()).unwrap_or(&[]))
162 .collect();
163 if sub_options.iter().any(|v| v.is_empty()) {
164 continue;
165 }
166 let combos = cartesian(&sub_options);
168 for combo in combos {
169 let already_exists = arguments.iter().any(|existing| {
170 matches!(&existing.origin, Origin::RuleApplication(rid) if *rid == rule.id)
171 && existing.sub_arguments == combo
172 });
173 if already_exists {
174 continue;
175 }
176 arguments.push(Argument {
177 id: ArgumentId(next_id),
178 conclusion: rule.conclusion.clone(),
179 origin: Origin::RuleApplication(rule.id),
180 sub_arguments: combo,
181 });
182 next_id += 1;
183 }
184 }
185 if arguments.len() == before {
186 return Ok(arguments);
187 }
188 }
189}
190
191fn cartesian(options: &[&[ArgumentId]]) -> Vec<Vec<ArgumentId>> {
192 let mut result = vec![Vec::new()];
193 for opt in options {
194 let mut next = Vec::new();
195 for prefix in &result {
196 for &id in *opt {
197 let mut new_prefix = prefix.clone();
198 new_prefix.push(id);
199 next.push(new_prefix);
200 }
201 }
202 result = next;
203 }
204 result
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::aspic::rules::{Rule, RuleId};
211
212 #[test]
213 fn single_premise_yields_leaf_argument() {
214 let mut kb = KnowledgeBase::new();
215 kb.add_ordinary(Literal::atom("p"));
216 let args = construct_arguments(&kb, &[]).unwrap();
217 assert_eq!(args.len(), 1);
218 assert!(args[0].is_premise());
219 assert_eq!(args[0].conclusion, Literal::atom("p"));
220 }
221
222 #[test]
223 fn rule_application_creates_compound_argument() {
224 let mut kb = KnowledgeBase::new();
225 kb.add_ordinary(Literal::atom("p"));
226 let rules = vec![Rule::defeasible(
227 RuleId(0),
228 vec![Literal::atom("p")],
229 Literal::atom("q"),
230 )];
231 let args = construct_arguments(&kb, &rules).unwrap();
232 assert_eq!(args.len(), 2);
233 let q_arg = args
234 .iter()
235 .find(|a| a.conclusion == Literal::atom("q"))
236 .unwrap();
237 assert!(matches!(q_arg.origin, Origin::RuleApplication(RuleId(0))));
238 assert_eq!(q_arg.sub_arguments.len(), 1);
239 }
240
241 #[test]
242 fn missing_premise_blocks_rule() {
243 let mut kb = KnowledgeBase::new();
244 kb.add_ordinary(Literal::atom("p"));
245 let rules = vec![Rule::defeasible(
246 RuleId(0),
247 vec![Literal::atom("r")],
248 Literal::atom("q"),
249 )];
250 let args = construct_arguments(&kb, &rules).unwrap();
251 assert_eq!(args.len(), 1);
252 assert_eq!(args[0].conclusion, Literal::atom("p"));
253 }
254
255 #[test]
256 fn cyclic_rules_are_rejected() {
257 let mut kb = KnowledgeBase::new();
258 kb.add_ordinary(Literal::atom("p"));
259 let rules = vec![Rule::defeasible(
260 RuleId(0),
261 vec![Literal::atom("p")],
262 Literal::atom("p"),
263 )];
264 let result = construct_arguments(&kb, &rules);
265 assert!(matches!(result, Err(crate::Error::Aspic(_))));
266 }
267
268 #[test]
269 fn indirect_cyclic_rules_are_rejected() {
270 let mut kb = KnowledgeBase::new();
271 kb.add_ordinary(Literal::atom("p"));
272 let rules = vec![
273 Rule::defeasible(RuleId(0), vec![Literal::atom("p")], Literal::atom("q")),
274 Rule::defeasible(RuleId(1), vec![Literal::atom("q")], Literal::atom("p")),
275 ];
276 let result = construct_arguments(&kb, &rules);
277 assert!(matches!(result, Err(crate::Error::Aspic(_))));
278 }
279
280 #[test]
281 fn argument_id_displays_as_a_prefix() {
282 assert_eq!(format!("{}", ArgumentId(0)), "A0");
283 assert_eq!(format!("{}", ArgumentId(42)), "A42");
284 }
285}