Skip to main content

pumpkin_checking/
deduction_checker.rs

1use crate::AtomicConstraint;
2use crate::VariableState;
3
4/// An inference that was ignored when checking a deduction.
5///
6/// Returned as an error when checking a deduction. These inferences were added to the proof stage,
7/// but never used. Hence, they likely point to why the proof stage is rejected.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct IgnoredInference<Atomic> {
10    /// The inference that was ignored.
11    pub inference: SupportingInference<Atomic>,
12
13    /// The premises that were not satisfied when the inference was evaluated.
14    pub unsatisfied_premises: Vec<Atomic>,
15}
16
17/// A deduction is rejected by the checker.
18///
19/// The inferences in the proof stage do not derive an empty domain or an explicit
20/// conflict.
21#[derive(thiserror::Error, Debug, PartialEq, Eq)]
22#[error("no conflict was derived after applying all inferences")]
23pub struct InvalidDeduction<Atomic>(pub Vec<IgnoredInference<Atomic>>);
24
25/// An inference used to support a deduction.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct SupportingInference<Atomic> {
28    /// The premises of the inference.
29    pub premises: Vec<Atomic>,
30    /// The consequent of the inference.
31    ///
32    /// [`None`] represents the literal false. I.e., if the consequent is [`None`], then the
33    /// premises imply false.
34    pub consequent: Option<Atomic>,
35}
36
37/// Verify that a deduction is valid given the inferences in the proof stage.
38///
39/// The `inferences` are considered in the order they are provided.
40pub fn verify_deduction<Atomic>(
41    premises: impl IntoIterator<Item = Atomic>,
42    inferences: impl IntoIterator<Item = SupportingInference<Atomic>>,
43) -> Result<(), InvalidDeduction<Atomic>>
44where
45    Atomic: AtomicConstraint,
46{
47    // To verify a deduction, we assume that the premises are true. Then we go over all the
48    // facts in the sequence, and if all the premises are satisfied, we apply the consequent.
49    // At some point, this should either reach a fact without a consequent or derive an
50    // inconsistent domain.
51
52    let Ok(mut variable_state) = VariableState::prepare_for_conflict_check(premises, None) else {
53        // If the deduction contains inconsistent premises, its trivially valid.
54        return Ok(());
55    };
56
57    let mut unused_inferences = Vec::new();
58
59    for inference in inferences.into_iter() {
60        // Collect all premises that do not evaluate to `true` under the current variable
61        // state.
62        let unsatisfied_premises = inference
63            .premises
64            .iter()
65            .filter(|premise| !variable_state.is_true(premise))
66            .cloned()
67            .collect::<Vec<_>>();
68
69        // If at least one premise is unassigned, this fact is ignored for the conflict
70        // check and recorded as unused.
71        if !unsatisfied_premises.is_empty() {
72            unused_inferences.push(IgnoredInference {
73                inference,
74                unsatisfied_premises,
75            });
76
77            continue;
78        }
79
80        // At this point the premises are satisfied so we handle the consequent of the
81        // inference.
82        match &inference.consequent {
83            Some(consequent) => {
84                if !variable_state.apply(consequent) {
85                    // If applying the consequent yields an empty domain for a
86                    // variable, then the deduction is valid.
87                    return Ok(());
88                }
89            }
90            // If the consequent is explicitly false, then the deduction is valid.
91            None => return Ok(()),
92        }
93    }
94
95    // Reaching this point means that the conjunction of inferences did not yield to a
96    // conflict. Therefore the deduction is invalid.
97    Err(InvalidDeduction(unused_inferences))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::test_atomic;
104
105    /// Create a [`SupportingInference`] in a DSL.
106    ///
107    /// # Example
108    /// ```
109    /// inference!([x >= 5] & [y <= 10] -> [z == 5]);
110    /// inference!([x >= 5] & [y <= 10] -> false);
111    /// ```
112    #[macro_export]
113    macro_rules! inference {
114        // Case: consequent is an Atomic
115        (
116            $($prem:tt)&+ -> [$($cons:tt)+]
117        ) => {
118            SupportingInference {
119                premises: vec![$( test_atomic!($prem) ),+],
120                consequent: Some(test_atomic!([$($cons)+])),
121            }
122        };
123
124        // Case: consequent is false (i.e., None)
125        (
126            $($prem:tt)&+ -> false
127        ) => {
128            SupportingInference {
129                premises: vec![$( test_atomic!($prem) ),+],
130                consequent: None,
131            }
132        };
133    }
134
135    #[test]
136    fn a_sequence_is_correctly_traversed() {
137        let premises = vec![test_atomic!([x >= 5])];
138
139        let inferences = vec![
140            inference!([x >= 5] -> [y <= 4]),
141            inference!([y <= 7] -> [z != 10]),
142            inference!([y <= 5] & [z != 10] -> [x <= 4]),
143        ];
144
145        verify_deduction(premises, inferences).expect("valid deduction");
146    }
147
148    #[test]
149    fn an_inference_implying_false_is_a_valid_stopping_condition() {
150        let premises = vec![test_atomic!([x >= 5])];
151
152        let inferences = vec![
153            inference!([x >= 5] -> [y <= 4]),
154            inference!([y <= 7] -> [z != 10]),
155            inference!([y <= 5] & [z != 10] -> false),
156        ];
157
158        verify_deduction(premises, inferences).expect("valid deduction");
159    }
160
161    #[test]
162    fn inconsistent_premises_are_no_problem() {
163        let premises = vec![test_atomic!([x >= 5]), test_atomic!([x <= 4])];
164
165        let inferences = vec![inference!([x == 5] -> false)];
166
167        verify_deduction(premises, inferences).expect("no inconsistency");
168    }
169
170    #[test]
171    fn sequence_that_does_not_terminate_in_conflict_is_rejected() {
172        let premises = vec![test_atomic!([x >= 5])];
173
174        let inferences = vec![
175            inference!([x >= 5] -> [y <= 4]),
176            inference!([y <= 7] -> [z != 10]),
177        ];
178
179        let error = verify_deduction(premises, inferences).expect_err("conflict is not reached");
180        assert_eq!(InvalidDeduction(vec![]), error);
181    }
182
183    #[test]
184    fn inferences_with_unsatisfied_premises_are_ignored() {
185        let premises = vec![test_atomic!([x >= 5])];
186
187        let inferences = vec![
188            inference!([x >= 5] -> [y <= 4]),
189            inference!([y <= 7] & [x >= 6] -> [z != 10]),
190            inference!([y <= 5] & [z != 10] -> false),
191        ];
192
193        let error = verify_deduction(premises, inferences).expect_err("premises are not satisfied");
194        assert_eq!(
195            InvalidDeduction(vec![
196                IgnoredInference {
197                    inference: inference!([y <= 7] & [x >= 6] -> [z != 10]),
198                    unsatisfied_premises: vec![test_atomic!([x >= 6])],
199                },
200                IgnoredInference {
201                    inference: inference!([y <= 5] & [z != 10] -> false),
202                    unsatisfied_premises: vec![test_atomic!([z != 10])],
203                }
204            ]),
205            error
206        );
207    }
208}