1use crate::precedence::Precedence;
12use expectation_model::{Condition, Diagnostic, ExpId, Expectation, Shift, Suite, Trigger, VarId};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Kind {
16 Outcome,
17 Invariant,
18}
19
20#[derive(Clone, Debug)]
21pub struct Conflict {
22 pub kind: Kind,
23 pub a: ExpId,
25 pub b: ExpId,
26 pub vars: Vec<VarId>,
28 pub situation: Condition,
30 pub trigger: Trigger,
32 pub winner: Option<ExpId>,
34}
35
36impl Conflict {
37 pub fn involves(&self, x: ExpId, y: ExpId) -> bool {
38 (self.a == x && self.b == y) || (self.a == y && self.b == x)
39 }
40
41 pub fn loser(&self) -> Option<ExpId> {
42 self.winner.map(|w| if w == self.a { self.b } else { self.a })
43 }
44}
45
46pub fn detect(suite: &Suite, prec: &Precedence) -> Vec<Conflict> {
47 let mut out = vec![];
48 let winner = |a: ExpId, b: ExpId| {
49 if prec.beats(a, b) {
50 Some(a)
51 } else if prec.beats(b, a) {
52 Some(b)
53 } else {
54 None
55 }
56 };
57
58 let transitions: Vec<&Expectation> = suite.transitions().collect();
59 for (i, a) in transitions.iter().enumerate() {
60 for b in &transitions[i + 1..] {
61 if !a.trigger.compatible(&b.trigger) {
62 continue;
63 }
64 let situation = a.given.and(&b.given);
65 if !situation.is_satisfiable() {
66 continue;
67 }
68 let trigger = Trigger { event: a.trigger.event, changes: a.trigger.changes.and(&b.trigger.changes) };
69 let before = situation.overlay(&trigger.changes);
70 let vars = disagree(suite, a, b, &before);
71 if vars.is_empty() {
72 continue;
73 }
74 out.push(Conflict {
75 kind: Kind::Outcome,
76 a: a.id,
77 b: b.id,
78 vars,
79 situation,
80 trigger,
81 winner: winner(a.id, b.id),
82 });
83 }
84 }
85
86 for x in &suite.expectations {
87 let after = x.outcome(&suite.vocabulary);
88 for inv in suite.invariants() {
89 if inv.id == x.id || out.iter().any(|c| c.kind == Kind::Invariant && c.involves(x.id, inv.id)) {
90 continue;
91 }
92 let scope = after.and(&inv.given);
93 if !scope.is_satisfiable() || scope.and(&inv.then).is_satisfiable() {
94 continue;
95 }
96 let vars = scope.contradictions(&inv.then);
100 let changed: Vec<VarId> = x.sets().chain(x.trigger.changes.vars()).collect();
101 if !changed.iter().any(|v| inv.given.get(*v).is_some() || vars.contains(v)) {
102 continue;
103 }
104 out.push(Conflict {
105 kind: Kind::Invariant,
106 a: x.id,
107 b: inv.id,
108 vars,
109 situation: scope,
110 trigger: x.trigger.clone(),
111 winner: winner(x.id, inv.id),
112 });
113 }
114 }
115 out
116}
117
118fn disagree(suite: &Suite, a: &Expectation, b: &Expectation, before: &Condition) -> Vec<VarId> {
122 let v = &suite.vocabulary;
123 let (ea, eb) = (a.effect(v, before), b.effect(v, before));
124 let mut vars = ea.contradictions(&eb);
125 let shift = |e: &Expectation, x: VarId| {
126 e.shifts.iter().find(|(w, _)| *w == x).map(|(_, s)| match s {
127 Shift::Same => 0.0,
128 Shift::By(d) => *d,
129 })
130 };
131 for x in a.sets().filter(|x| b.sets().any(|y| y == *x)) {
132 let differ = match (shift(a, x), shift(b, x)) {
133 (Some(p), Some(q)) => p != q,
134 (Some(_), None) | (None, Some(_)) => ea.get(x) != eb.get(x),
135 (None, None) => false,
136 };
137 if differ && !vars.contains(&x) {
138 vars.push(x);
139 }
140 }
141 vars
142}
143
144pub fn diagnose(suite: &Suite, prec: &Precedence, conflicts: &[Conflict]) -> Vec<Diagnostic> {
147 let v = &suite.vocabulary;
148 let mut diags = vec![];
149 for c in conflicts.iter().filter(|c| c.winner.is_none()) {
150 let (a, b) = (suite.exp(c.a), suite.exp(c.b));
151 let subjects: Vec<String> = c.vars.iter().map(|&x| v.var(x).subject.clone()).collect();
152 let before = c.situation.overlay(&c.trigger.changes);
153 let on = |e: &Expectation| v.describe(&e.effect(v, &before).restrict(c.vars.iter().copied()));
154 let d = match c.kind {
155 Kind::Outcome => {
156 let mut when = v.describe(&c.situation);
157 if !c.trigger.is_none() {
158 when = format!("{when}, and {}", suite.describe_trigger(&c.trigger));
159 }
160 Diagnostic::error(
161 "conflict",
162 format!(
163 "«{}» and «{}» disagree about {}",
164 a.name,
165 b.name,
166 expectation_model::text::join_and(&subjects)
167 ),
168 )
169 .at(&a.span, format!("«{}» says {}", a.name, on(a)))
170 .at(&b.span, format!("«{}» says {}", b.name, on(b)))
171 .note_that(format!("both apply when {when}"))
172 .help(exception_help(a, b))
173 }
174 Kind::Invariant => Diagnostic::error("conflict", format!("«{}» breaks «{}»", a.name, b.name))
175 .at(&a.span, format!("afterwards {}", v.describe(&c.situation.restrict(c.vars.iter().copied()))))
176 .at(&b.span, format!("but whenever {}, {}", v.describe(&b.given), on(b)))
177 .help(format!(
178 "narrow one of the Givens, or if «{}» is a deliberate exception, add `Overrides: {}` to it",
179 a.name, b.name
180 )),
181 };
182 diags.push(d);
183 }
184
185 for &(w, l) in &prec.declared {
186 if !conflicts.iter().any(|c| c.involves(w, l)) {
187 let (w, l) = (suite.exp(w), suite.exp(l));
188 diags.push(
189 Diagnostic::warning(
190 "unneeded-override",
191 format!("«{}» overrides «{}», but they never disagree", w.name, l.name),
192 )
193 .at(&w.span, "")
194 .help("either they can't apply at the same time or they agree; remove the Overrides line"),
195 );
196 }
197 }
198
199 for section in &suite.sections {
200 for inv in suite.invariants() {
201 let scope = section.start.and(&inv.given);
202 if scope.is_satisfiable() && !scope.and(&inv.then).is_satisfiable() {
203 let mut d = Diagnostic::error(
204 "conflict",
205 format!("the Background of «{}» breaks «{}»", section.name, inv.name),
206 )
207 .at(&inv.span, format!("whenever {}, {}", v.describe(&inv.given), v.describe(&inv.then)));
208 if let Some(s) = section.setup_steps.first() {
209 d = d.at(&s.span, format!("but «{}» starts with {}", section.name, v.describe(§ion.start)));
210 }
211 diags.push(d);
212 }
213 }
214 }
215 diags
216}
217
218fn exception_help(a: &Expectation, b: &Expectation) -> String {
219 let (special, general) = if a.given.implies(&b.given) && !b.given.implies(&a.given) { (a, b) } else { (b, a) };
221 let specific = special.given.implies(&general.given) && !general.given.implies(&special.given);
222 if specific {
223 format!(
224 "«{}» is the more specific; if it's an exception, add `Overrides: {}` to it. Otherwise narrow a Given so they can't both apply",
225 special.name, general.name
226 )
227 } else {
228 "if one is an exception to the other, add `Overrides: <the other>` to it; otherwise narrow a Given so they can't both apply".into()
229 }
230}