1use super::argument::{Argument, ArgumentId, Origin};
33use super::kb::Premise;
34use super::rules::Rule;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum AttackKind {
39 Undermine,
41 Undercut,
43 Rebut,
45}
46
47impl std::fmt::Display for AttackKind {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 AttackKind::Undermine => write!(f, "undermine"),
51 AttackKind::Undercut => write!(f, "undercut"),
52 AttackKind::Rebut => write!(f, "rebut"),
53 }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
59pub struct Attack {
60 pub attacker: ArgumentId,
62 pub target: ArgumentId,
64 pub kind: AttackKind,
66}
67
68fn defeasible_rules_used(
70 arg: &Argument,
71 args: &[Argument],
72 rules: &[Rule],
73) -> Vec<super::rules::RuleId> {
74 let mut out = Vec::new();
75 match &arg.origin {
76 Origin::Premise(_) => {}
77 Origin::RuleApplication(rid) => {
78 if let Some(r) = rules.iter().find(|r| r.id == *rid)
79 && r.is_defeasible()
80 {
81 out.push(*rid);
82 }
83 for sub_id in &arg.sub_arguments {
84 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
85 out.extend(defeasible_rules_used(sub, args, rules));
86 }
87 }
88 }
89 }
90 out
91}
92
93fn ordinary_premises_used<'a>(arg: &'a Argument, args: &'a [Argument]) -> Vec<&'a Premise> {
95 let mut out = Vec::new();
96 match &arg.origin {
97 Origin::Premise(p) => {
98 if p.is_defeasible() {
99 out.push(p);
100 }
101 }
102 Origin::RuleApplication(_) => {
103 for sub_id in &arg.sub_arguments {
104 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
105 out.extend(ordinary_premises_used(sub, args));
106 }
107 }
108 }
109 }
110 out
111}
112
113fn all_sub_arguments<'a>(arg: &'a Argument, args: &'a [Argument]) -> Vec<&'a Argument> {
119 let mut out = vec![arg];
120 for sub_id in &arg.sub_arguments {
121 if let Some(sub) = args.iter().find(|a| a.id == *sub_id) {
122 out.extend(all_sub_arguments(sub, args));
123 }
124 }
125 out
126}
127
128pub fn compute_attacks(args: &[Argument], rules: &[Rule]) -> Vec<Attack> {
130 let mut premises_by_target: std::collections::HashMap<ArgumentId, Vec<&Premise>> =
133 std::collections::HashMap::with_capacity(args.len());
134 let mut defeasible_rules_by_target: std::collections::HashMap<
135 ArgumentId,
136 Vec<super::rules::RuleId>,
137 > = std::collections::HashMap::with_capacity(args.len());
138 let mut subs_by_target: std::collections::HashMap<ArgumentId, Vec<&Argument>> =
139 std::collections::HashMap::with_capacity(args.len());
140 for target in args {
141 premises_by_target.insert(target.id, ordinary_premises_used(target, args));
142 defeasible_rules_by_target.insert(target.id, defeasible_rules_used(target, args, rules));
143 subs_by_target.insert(target.id, all_sub_arguments(target, args));
144 }
145
146 let mut attacks = Vec::new();
147 for attacker in args {
148 for target in args {
149 if attacker.id == target.id {
150 continue;
151 }
152 let subs = subs_by_target.get(&target.id).expect("target precomputed");
157 if subs.iter().any(|sub| {
158 attacker.conclusion.is_contrary_of(&sub.conclusion)
159 && sub.top_rule_is_defeasible(rules)
160 }) {
161 attacks.push(Attack {
162 attacker: attacker.id,
163 target: target.id,
164 kind: AttackKind::Rebut,
165 });
166 }
167 let target_premises = premises_by_target
170 .get(&target.id)
171 .expect("target precomputed");
172 if target_premises
173 .iter()
174 .any(|prem| attacker.conclusion.is_contrary_of(prem.literal()))
175 {
176 attacks.push(Attack {
177 attacker: attacker.id,
178 target: target.id,
179 kind: AttackKind::Undermine,
180 });
181 }
182 let target_rules = defeasible_rules_by_target
186 .get(&target.id)
187 .expect("target precomputed");
188 if target_rules
189 .iter()
190 .any(|rid| attacker.conclusion == super::language::Literal::undercut_marker(rid.0))
191 {
192 attacks.push(Attack {
193 attacker: attacker.id,
194 target: target.id,
195 kind: AttackKind::Undercut,
196 });
197 }
198 }
199 }
200 attacks
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::aspic::argument::construct_arguments;
207 use crate::aspic::kb::KnowledgeBase;
208 use crate::aspic::language::Literal;
209 use crate::aspic::rules::{Rule, RuleId};
210
211 #[test]
212 fn rebut_detected_between_contrary_conclusions() {
213 let mut kb = KnowledgeBase::new();
214 kb.add_ordinary(Literal::atom("p"));
215 kb.add_ordinary(Literal::atom("r"));
216 let rules = vec![
217 Rule::defeasible(RuleId(0), vec![Literal::atom("p")], Literal::atom("q")),
218 Rule::defeasible(RuleId(1), vec![Literal::atom("r")], Literal::neg("q")),
219 ];
220 let args = construct_arguments(&kb, &rules).unwrap();
221 let attacks = compute_attacks(&args, &rules);
222 let rebuts: Vec<&Attack> = attacks
223 .iter()
224 .filter(|a| a.kind == AttackKind::Rebut)
225 .collect();
226 assert!(rebuts.len() >= 2);
227 }
228
229 #[test]
230 fn undermine_detected_on_ordinary_premise() {
231 let mut kb = KnowledgeBase::new();
232 kb.add_ordinary(Literal::atom("p"));
233 kb.add_ordinary(Literal::atom("r"));
234 let rules = vec![Rule::defeasible(
235 RuleId(0),
236 vec![Literal::atom("r")],
237 Literal::neg("p"),
238 )];
239 let args = construct_arguments(&kb, &rules).unwrap();
240 let attacks = compute_attacks(&args, &rules);
241 let undermines: Vec<&Attack> = attacks
242 .iter()
243 .filter(|a| a.kind == AttackKind::Undermine)
244 .collect();
245 assert!(!undermines.is_empty());
246 }
247
248 #[test]
249 fn undercut_detected_via_reserved_marker() {
250 let mut kb = KnowledgeBase::new();
256 kb.add_ordinary(Literal::atom("p"));
257 kb.add_ordinary(Literal::atom("trigger"));
258 let rules = vec![
259 Rule::defeasible(RuleId(0), vec![Literal::atom("p")], Literal::atom("q")),
260 Rule::defeasible(
261 RuleId(1),
262 vec![Literal::atom("trigger")],
263 Literal::undercut_marker(0),
264 ),
265 ];
266 let args = construct_arguments(&kb, &rules).unwrap();
267 let attacks = compute_attacks(&args, &rules);
268 let undercuts: Vec<&Attack> = attacks
269 .iter()
270 .filter(|a| a.kind == AttackKind::Undercut)
271 .collect();
272 assert!(
273 !undercuts.is_empty(),
274 "expected at least one Undercut attack, got {:?}",
275 attacks
276 );
277 let q_arg = args
279 .iter()
280 .find(|a| a.conclusion == Literal::atom("q"))
281 .unwrap();
282 assert!(
283 undercuts.iter().any(|u| u.target == q_arg.id),
284 "expected an undercut attack targeting the q-argument"
285 );
286 }
287
288 #[test]
289 fn rebut_propagates_through_strict_wrapper() {
290 let mut kb = KnowledgeBase::new();
316 kb.add_ordinary(Literal::atom("WearsRing"));
317 kb.add_ordinary(Literal::atom("PartyAnimal"));
318 let rules = vec![
319 Rule::defeasible(
320 RuleId(0),
321 vec![Literal::atom("WearsRing")],
322 Literal::atom("Married"),
323 ),
324 Rule::defeasible(
325 RuleId(1),
326 vec![Literal::atom("PartyAnimal")],
327 Literal::atom("Bachelor"),
328 ),
329 Rule::strict(
330 RuleId(2),
331 vec![Literal::atom("Married")],
332 Literal::neg("Bachelor"),
333 ),
334 Rule::strict(
335 RuleId(3),
336 vec![Literal::atom("Bachelor")],
337 Literal::neg("Married"),
338 ),
339 ];
340 let args = construct_arguments(&kb, &rules).unwrap();
341 let all_attacks = compute_attacks(&args, &rules);
342 let rebuts: Vec<&Attack> = all_attacks
343 .iter()
344 .filter(|a| a.kind == AttackKind::Rebut)
345 .collect();
346 let married = args
348 .iter()
349 .find(|a| a.conclusion == Literal::atom("Married"))
350 .unwrap();
351 let bachelor = args
352 .iter()
353 .find(|a| a.conclusion == Literal::atom("Bachelor"))
354 .unwrap();
355 let not_bachelor = args
356 .iter()
357 .find(|a| a.conclusion == Literal::neg("Bachelor"))
358 .unwrap();
359 let not_married = args
360 .iter()
361 .find(|a| a.conclusion == Literal::neg("Married"))
362 .unwrap();
363
364 assert!(
366 rebuts
367 .iter()
368 .any(|r| r.attacker == not_bachelor.id && r.target == bachelor.id),
369 "expected ¬Bachelor rebuts Bachelor (direct sub)"
370 );
371 assert!(
372 rebuts
373 .iter()
374 .any(|r| r.attacker == not_married.id && r.target == married.id),
375 "expected ¬Married rebuts Married (direct sub)"
376 );
377
378 assert!(
380 rebuts
381 .iter()
382 .any(|r| r.attacker == not_bachelor.id && r.target == not_married.id),
383 "expected ¬Bachelor rebuts ¬Married via sub-argument B2 (strict wrapper)"
384 );
385 assert!(
386 rebuts
387 .iter()
388 .any(|r| r.attacker == not_married.id && r.target == not_bachelor.id),
389 "expected ¬Married rebuts ¬Bachelor via sub-argument A2 (strict wrapper)"
390 );
391 }
392
393 #[test]
394 fn attack_kind_displays_as_lowercase_word() {
395 assert_eq!(format!("{}", AttackKind::Undermine), "undermine");
396 assert_eq!(format!("{}", AttackKind::Undercut), "undercut");
397 assert_eq!(format!("{}", AttackKind::Rebut), "rebut");
398 }
399
400 #[test]
401 fn compute_attacks_is_stable_across_refactors() {
402 let mut kb = KnowledgeBase::new();
403 kb.add_ordinary(Literal::atom("p"));
404 kb.add_ordinary(Literal::atom("r"));
405 kb.add_ordinary(Literal::atom("trigger"));
406 let rules = vec![
407 Rule::defeasible(RuleId(0), vec![Literal::atom("p")], Literal::atom("q")),
408 Rule::defeasible(RuleId(1), vec![Literal::atom("r")], Literal::neg("q")),
409 Rule::strict(
410 RuleId(2),
411 vec![Literal::atom("q")],
412 Literal::atom("derived"),
413 ),
414 Rule::defeasible(
415 RuleId(3),
416 vec![Literal::atom("trigger")],
417 Literal::undercut_marker(0),
418 ),
419 ];
420 let args = construct_arguments(&kb, &rules).unwrap();
421 let attacks = compute_attacks(&args, &rules);
422
423 let rebuts = attacks
424 .iter()
425 .filter(|a| a.kind == AttackKind::Rebut)
426 .count();
427 let undermines = attacks
428 .iter()
429 .filter(|a| a.kind == AttackKind::Undermine)
430 .count();
431 let undercuts = attacks
432 .iter()
433 .filter(|a| a.kind == AttackKind::Undercut)
434 .count();
435
436 assert!(rebuts >= 3, "expected >= 3 rebuts, got {}", rebuts);
437 assert_eq!(undermines, 0);
438 assert_eq!(undercuts, 2);
442 }
443
444 #[test]
445 fn rebut_propagates_through_three_level_strict_wrapper() {
446 let mut kb = KnowledgeBase::new();
467 kb.add_ordinary(Literal::atom("p"));
468 kb.add_ordinary(Literal::atom("q"));
469 let rules = vec![
470 Rule::defeasible(RuleId(0), vec![Literal::atom("p")], Literal::atom("x")),
471 Rule::strict(RuleId(1), vec![Literal::atom("x")], Literal::atom("y")),
472 Rule::strict(RuleId(2), vec![Literal::atom("y")], Literal::atom("z")),
473 Rule::defeasible(RuleId(3), vec![Literal::atom("q")], Literal::neg("x")),
474 ];
475 let args = construct_arguments(&kb, &rules).unwrap();
476 let attacks = compute_attacks(&args, &rules);
477 let rebuts: Vec<&Attack> = attacks
478 .iter()
479 .filter(|a| a.kind == AttackKind::Rebut)
480 .collect();
481
482 let a2 = args
483 .iter()
484 .find(|a| a.conclusion == Literal::atom("x"))
485 .expect("x-argument");
486 let a3 = args
487 .iter()
488 .find(|a| a.conclusion == Literal::atom("y"))
489 .expect("y-argument");
490 let a4 = args
491 .iter()
492 .find(|a| a.conclusion == Literal::atom("z"))
493 .expect("z-argument");
494 let b2 = args
495 .iter()
496 .find(|a| a.conclusion == Literal::neg("x"))
497 .expect("¬x-argument");
498
499 assert!(
500 rebuts
501 .iter()
502 .any(|r| r.attacker == b2.id && r.target == a2.id),
503 "expected B2 rebuts A2 (direct)"
504 );
505 assert!(
506 rebuts
507 .iter()
508 .any(|r| r.attacker == b2.id && r.target == a3.id),
509 "expected B2 rebuts A3 (1-level strict wrap over A2)"
510 );
511 assert!(
512 rebuts
513 .iter()
514 .any(|r| r.attacker == b2.id && r.target == a4.id),
515 "expected B2 rebuts A4 (2-level strict wrap over A2)"
516 );
517 }
518}