1
//! Normalising rules for boolean operations (not, and, or, ->).
2

            
3
use conjure_cp::ast::{Atom, Expression as Expr, Moo, SymbolTable};
4
use conjure_cp::essence_expr;
5
use conjure_cp::into_matrix_expr;
6
use conjure_cp::rule_engine::{
7
    ApplicationError, ApplicationError::RuleNotApplicable, ApplicationResult, RuleEffect,
8
    register_rule,
9
};
10

            
11
/// Removes double negations
12
///
13
/// ```text
14
/// not(not(a)) = a
15
/// ```
16
#[register_rule("Base", 8400, [Not])]
17
272530
fn remove_double_negation(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
18
272530
    match expr {
19
268461
        Expr::Not(_, contents) => match contents.as_ref() {
20
1736
            Expr::Not(_, expr_box) => Ok(RuleEffect::pure(Moo::unwrap_or_clone(expr_box.clone()))),
21
266725
            _ => Err(ApplicationError::RuleNotApplicable),
22
        },
23
4069
        _ => Err(ApplicationError::RuleNotApplicable),
24
    }
25
272530
}
26

            
27
/// Distributes `ands` contained in `ors`
28
///
29
/// ```text
30
/// or(and(a, b), c) ~> and(or(a, c), or(b, c))
31
/// ```
32
///
33
/// Size-increasing cases with a non-trivial rest are refused: see
34
/// [`distribution_would_duplicate_nontrivial_rest`]. Nested `and`/`or` remain
35
/// valid for Minion (`WatchedAnd`/`WatchedOr`) and for SAT Tseytin encoding.
36
#[register_rule("Base", 8400, [Or])]
37
1710534
fn distribute_or_over_and(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
38
1677038
    fn find_and(exprs: &[Expr]) -> Option<usize> {
39
        // ToDo: may be better to move this to some kind of utils module?
40
1677038
        for (i, e) in exprs.iter().enumerate() {
41
3593905
            if let Expr::And(_, _) = e {
42
411373
                return Some(i);
43
3182532
            }
44
        }
45
1265665
        None
46
1677038
    }
47

            
48
1710534
    match expr {
49
        Expr::Or(_, e) => {
50
            // Look for the `and` by reference. A wide disjunction containing no `and` is the
51
            // common case, and it costs a walk of the disjuncts rather than a copy of them.
52
1706601
            let Some(exprs) = e.unwrap_list_cow() else {
53
29563
                return Err(RuleNotApplicable);
54
            };
55

            
56
1677038
            match find_and(&exprs) {
57
                Some(idx) => {
58
411373
                    let mut rest = exprs.to_vec();
59
411373
                    let and_expr = rest.remove(idx);
60

            
61
411373
                    match and_expr {
62
                        Expr::And(metadata, e) => {
63
411373
                            let Some(and_exprs) = e.as_ref().clone().into_list() else {
64
8
                                return Err(RuleNotApplicable);
65
                            };
66

            
67
                            // Refuse CNF blow-ups such as or([and(a,b), and(c,d), ...]) that
68
                            // copy every sibling disjunct across each conjunct.
69
411365
                            if distribution_would_duplicate_nontrivial_rest(and_exprs.len(), &rest)
70
                            {
71
387091
                                return Err(RuleNotApplicable);
72
24274
                            }
73

            
74
24274
                            let mut new_and_contents = Vec::new();
75

            
76
24274
                            for e in and_exprs {
77
                                // ToDo: Cloning everything may be a bit inefficient - discuss
78
52503
                                let mut new_or_contents = rest.clone();
79
52503
                                new_or_contents.push(e.clone());
80
52503
                                new_and_contents.push(Expr::Or(
81
52503
                                    metadata.clone(),
82
52503
                                    Moo::new(into_matrix_expr![new_or_contents]),
83
52503
                                ))
84
                            }
85

            
86
24274
                            Ok(RuleEffect::pure(Expr::And(
87
24274
                                metadata,
88
24274
                                Moo::new(into_matrix_expr![new_and_contents]),
89
24274
                            )))
90
                        }
91
                        _ => Err(ApplicationError::RuleNotApplicable),
92
                    }
93
                }
94
1265665
                None => Err(ApplicationError::RuleNotApplicable),
95
            }
96
        }
97
3933
        _ => Err(ApplicationError::RuleNotApplicable),
98
    }
99
1710534
}
100

            
101
/// Returns true when distributing `or(and(a1..ak), r1..rm)` would copy a
102
/// non-trivial rest across multiple conjuncts.
103
///
104
/// Refused when either:
105
/// - there are two or more rest siblings (`k * m` growth), or
106
/// - any rest sibling is itself an `And` (DNF seed: `or(and, and)`).
107
///
108
/// A single non-`And` rest sibling (`or(and(a,b), c)`) remains allowed.
109
411368
fn distribution_would_duplicate_nontrivial_rest(and_len: usize, rest: &[Expr]) -> bool {
110
411368
    and_len >= 2 && (rest.len() >= 2 || rest.iter().any(|e| matches!(e, Expr::And(_, _))))
111
411368
}
112

            
113
/// Distributes `not` over `and` by De Morgan's Law
114
///
115
/// ```text
116
/// not(and(a, b)) ~> or(not(a), not(b))
117
/// ```
118
#[register_rule("Base", 8400, [Not])]
119
272537
fn distribute_not_over_and(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
120
2149957
    if expr.any_expression(|child| {
121
2149957
        matches!(
122
2149957
            child,
123
            Expr::UnsafeDiv(_, _, _) | Expr::Bubble(_, _, _) | Expr::UnsafeMod(_, _, _)
124
        )
125
2149957
    }) {
126
28111
        return Err(RuleNotApplicable);
127
244426
    }
128
244426
    match expr {
129
240447
        Expr::Not(_, contents) => match contents.as_ref() {
130
            Expr::And(metadata, e) => {
131
53420
                let Some(exprs) = e.as_ref().clone().into_list() else {
132
364
                    return Err(RuleNotApplicable);
133
                };
134

            
135
53056
                if exprs.len() == 1 {
136
                    let single_expr = exprs[0].clone();
137
                    return Ok(RuleEffect::pure(essence_expr!(!&single_expr)));
138
53056
                }
139

            
140
53056
                let mut new_exprs = Vec::new();
141
53056
                for e in exprs {
142
110774
                    new_exprs.push(essence_expr!(!&e));
143
                }
144
53056
                Ok(RuleEffect::pure(Expr::Or(
145
53056
                    metadata.clone(),
146
53056
                    Moo::new(into_matrix_expr![new_exprs]),
147
53056
                )))
148
            }
149
187027
            _ => Err(ApplicationError::RuleNotApplicable),
150
        },
151
3979
        _ => Err(ApplicationError::RuleNotApplicable),
152
    }
153
272537
}
154

            
155
/// Distributes `not` over `or` by De Morgan's Law
156
///
157
/// ```text
158
/// not(or(a, b)) ~> and(not(a), not(b))
159
/// ```
160
#[register_rule("Base", 8400, [Not])]
161
272537
fn distribute_not_over_or(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
162
272537
    match expr {
163
268468
        Expr::Not(_, contents) => match contents.as_ref() {
164
            Expr::Or(metadata, e) => {
165
19768
                let Some(exprs) = e.as_ref().clone().into_list() else {
166
252
                    return Err(RuleNotApplicable);
167
                };
168

            
169
19516
                if exprs.len() == 1 {
170
                    let single_expr = exprs[0].clone();
171
                    return Ok(RuleEffect::pure(essence_expr!(!&single_expr)));
172
19516
                }
173

            
174
19516
                let mut new_exprs = Vec::new();
175

            
176
19516
                for e in exprs {
177
74382
                    new_exprs.push(essence_expr!(!&e));
178
                }
179

            
180
19516
                Ok(RuleEffect::pure(Expr::And(
181
19516
                    metadata.clone(),
182
19516
                    Moo::new(into_matrix_expr![new_exprs]),
183
19516
                )))
184
            }
185
248700
            _ => Err(ApplicationError::RuleNotApplicable),
186
        },
187
4069
        _ => Err(ApplicationError::RuleNotApplicable),
188
    }
189
272537
}
190

            
191
/// Removes ands with a single argument.
192
///
193
/// ```text
194
/// and([a]) ~> a
195
/// ```
196
#[register_rule("Base", 8800, [And])]
197

            
198
3195274
fn remove_unit_vector_and(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
199
3195274
    match expr {
200
        Expr::And(_, e) => {
201
            // The failure path is only a shape check and must stay O(1) on a wide conjunction.
202
3191134
            if e.list_len() != Some(1) {
203
2973306
                return Err(RuleNotApplicable);
204
217828
            }
205
217828
            let mut exprs = e.unwrap_list().ok_or(RuleNotApplicable)?;
206
217828
            Ok(RuleEffect::pure(
207
217828
                exprs.pop().expect("singleton length checked above"),
208
217828
            ))
209
        }
210
4140
        _ => Err(ApplicationError::RuleNotApplicable),
211
    }
212
3195274
}
213

            
214
/// Removes ors with a single argument.
215
///
216
/// ```text
217
/// or([a]) ~> a
218
/// ```
219
#[register_rule("Base", 8800, [Or])]
220
2201986
fn remove_unit_vector_or(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
221
2201986
    let Expr::Or(_, e) = expr else {
222
4285
        return Err(RuleNotApplicable);
223
    };
224

            
225
    // The failure path is only a shape check and must stay O(1) on a wide disjunction.
226
2197701
    if e.list_len() != Some(1) {
227
2093820
        return Err(RuleNotApplicable);
228
103881
    }
229
103881
    let mut exprs = e.unwrap_list().ok_or(RuleNotApplicable)?;
230
103881
    let single = exprs.pop().expect("singleton length checked above");
231

            
232
    // do not conflict with unwrap_nested_or rule.
233
103881
    if matches!(&single, Expr::Or(_, _)) {
234
9807
        return Err(RuleNotApplicable);
235
94074
    }
236

            
237
94074
    Ok(RuleEffect::pure(single))
238
2201986
}
239

            
240
/// Applies the contrapositive of implication.
241
///
242
/// ```text
243
/// !p -> !q ~> q -> p
244
/// ```
245
/// where p,q are safe.
246
#[register_rule("Base", 8800, [Imply])]
247
264618
fn normalise_implies_contrapositive(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
248
264618
    let Expr::Imply(_, e1, e2) = expr else {
249
4307
        return Err(RuleNotApplicable);
250
    };
251

            
252
260311
    let Expr::Not(_, p) = e1.as_ref() else {
253
260249
        return Err(RuleNotApplicable);
254
    };
255

            
256
62
    let Expr::Not(_, q) = e2.as_ref() else {
257
48
        return Err(RuleNotApplicable);
258
    };
259

            
260
    // we only negate e1, e2 if they are safe.
261
14
    if !e1.is_safe() || !e2.is_safe() {
262
        return Err(RuleNotApplicable);
263
14
    }
264

            
265
14
    Ok(RuleEffect::pure(essence_expr!(&q -> &p)))
266
264618
}
267

            
268
/// Simplifies the negation of implication.
269
///
270
/// ```text
271
/// !(p->q) ~> p /\ !q
272
/// ```,
273
///
274
/// where p->q is safe
275
#[register_rule("Base", 8800, [Not])]
276
437526
fn normalise_implies_negation(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
277
437526
    let Expr::Not(_, e1) = expr else {
278
4421
        return Err(RuleNotApplicable);
279
    };
280

            
281
433105
    let Expr::Imply(_, p, q) = e1.as_ref() else {
282
433091
        return Err(RuleNotApplicable);
283
    };
284

            
285
    // p->q must be safe to negate
286
14
    if !e1.is_safe() {
287
        return Err(RuleNotApplicable);
288
14
    }
289

            
290
14
    Ok(RuleEffect::pure(essence_expr!(r"&p /\ !&q")))
291
437526
}
292

            
293
/// Applies left distributivity to implication.
294
///
295
/// ```text
296
/// ((r -> p) -> (r->q)) ~> (r -> (p -> q))
297
/// ```
298
///
299
/// This rule relies on CSE to unify the two instances of `r` to a single atom; therefore, it might
300
/// not work as well when optimisations are disabled.
301
///
302
/// Has a higher priority than `normalise_implies_uncurry` as this should apply first. See the
303
/// docstring for `normalise_implies_uncurry`.
304
#[register_rule("Base", 8800, [Imply])]
305
264618
fn normalise_implies_left_distributivity(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
306
264618
    let Expr::Imply(_, e1, e2) = expr else {
307
4307
        return Err(RuleNotApplicable);
308
    };
309

            
310
260311
    let Expr::Imply(_, r1, p) = e1.as_ref() else {
311
260297
        return Err(RuleNotApplicable);
312
    };
313

            
314
14
    let Expr::Imply(_, r2, q) = e2.as_ref() else {
315
        return Err(RuleNotApplicable);
316
    };
317

            
318
    // Instead of checking deep equality, let CSE unify them to a common variable and check for
319
    // that.
320

            
321
14
    let r1_atom: &Atom = r1.as_ref().try_into().or(Err(RuleNotApplicable))?;
322
14
    let r2_atom: &Atom = r2.as_ref().try_into().or(Err(RuleNotApplicable))?;
323

            
324
14
    if !(r1_atom == r2_atom) {
325
        return Err(RuleNotApplicable);
326
14
    }
327

            
328
14
    Ok(RuleEffect::pure(essence_expr!(&r1 -> (&p -> &q))))
329
264618
}
330

            
331
/// Applies import-export to implication, i.e. uncurrying.
332
///
333
/// ```text
334
/// p -> (q -> r) ~> (p/\q) -> r
335
/// ```
336
///
337
/// This rule has a lower priority of 8400 to allow distributivity, contraposition, etc. to
338
/// apply first.
339
///
340
/// For example, we want to do:
341
///
342
/// ```text
343
/// ((r -> p) -> (r -> q)) ~> (r -> (p -> q))  [left-distributivity]
344
/// (r -> (p -> q)) ~> (r/\p) ~> q [uncurry]
345
/// ```
346
///
347
/// not
348
///
349
/// ```text
350
/// ((r->p) -> (r->q)) ~> ((r->p) /\ r) -> q) ~> [uncurry]
351
/// ```
352
///
353
/// # Rationale
354
///
355
/// With this rule, I am assuming (without empirical evidence) that and is a cheaper constraint
356
/// than implication (in particular, Minion's reifyimply constraint).
357
#[register_rule("Base", 8400, [Imply])]
358
218705
fn normalise_implies_uncurry(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
359
218705
    let Expr::Imply(_, p, e1) = expr else {
360
3997
        return Err(RuleNotApplicable);
361
    };
362

            
363
214708
    let Expr::Imply(_, q, r) = e1.as_ref() else {
364
214644
        return Err(RuleNotApplicable);
365
    };
366

            
367
64
    Ok(RuleEffect::pure(essence_expr!(r"(&p /\ &q) -> &r")))
368
218705
}
369

            
370
#[cfg(test)]
371
mod tests {
372
    use super::*;
373
    use conjure_cp::ast::{Atom, DeclarationPtr, Domain, Metadata, Name, Reference};
374
    use conjure_cp::matrix_expr;
375
    use conjure_cp::rule_engine::{ApplicationError, get_rule_by_name};
376

            
377
    /// Builds a boolean decision-variable reference for distribution tests.
378
11
    fn bool_ref(machine_id: i32) -> Atom {
379
11
        Atom::Reference(Reference::new(DeclarationPtr::new_find(
380
11
            Name::Machine(machine_id),
381
11
            Domain::bool(),
382
11
        )))
383
11
    }
384

            
385
    /// Atomic expression wrapping a boolean reference.
386
12
    fn atom_expr(atom: Atom) -> Expr {
387
12
        Expr::Atomic(Metadata::new(), atom)
388
12
    }
389

            
390
    #[test]
391
1
    fn distribution_guard_allows_single_atomic_rest_sibling() {
392
1
        let rest = [atom_expr(bool_ref(9))];
393
1
        assert!(!distribution_would_duplicate_nontrivial_rest(2, &rest));
394
1
    }
395

            
396
    #[test]
397
1
    fn distribution_guard_refuses_multi_rest_or_and_rest() {
398
1
        let a = atom_expr(bool_ref(1));
399
1
        let b = atom_expr(bool_ref(2));
400
1
        let multi = [atom_expr(bool_ref(3)), atom_expr(bool_ref(4))];
401
1
        assert!(distribution_would_duplicate_nontrivial_rest(2, &multi));
402
1
        let and_rest = [Expr::And(Metadata::new(), Moo::new(matrix_expr![a, b]))];
403
1
        assert!(distribution_would_duplicate_nontrivial_rest(2, &and_rest));
404
1
    }
405

            
406
    #[test]
407
1
    fn distribute_or_over_and_still_applies_with_one_rest_sibling() {
408
1
        let d1 = bool_ref(1);
409
1
        let d2 = bool_ref(2);
410
1
        let expr = Expr::Or(
411
1
            Metadata::new(),
412
1
            Moo::new(matrix_expr![
413
1
                Expr::And(
414
1
                    Metadata::new(),
415
1
                    Moo::new(matrix_expr![atom_expr(d1.clone()), atom_expr(d2.clone())]),
416
1
                ),
417
1
                atom_expr(d2.clone()),
418
1
            ]),
419
1
        );
420
1
        let rule = get_rule_by_name("distribute_or_over_and").expect("rule registered");
421
1
        assert!(
422
1
            rule.apply(&expr, &SymbolTable::new()).is_ok(),
423
            "or(and(a,b), c) must remain distributable"
424
        );
425
1
    }
426

            
427
    #[test]
428
1
    fn distribute_or_over_and_refuses_or_of_many_ands() {
429
1
        let a = atom_expr(bool_ref(1));
430
1
        let b = atom_expr(bool_ref(2));
431
1
        let c = atom_expr(bool_ref(3));
432
1
        let d = atom_expr(bool_ref(4));
433
        // Two and-disjuncts: rest is a single And, which must still be refused.
434
1
        let expr = Expr::Or(
435
1
            Metadata::new(),
436
1
            Moo::new(matrix_expr![
437
1
                Expr::And(Metadata::new(), Moo::new(matrix_expr![a, b])),
438
1
                Expr::And(Metadata::new(), Moo::new(matrix_expr![c, d])),
439
1
            ]),
440
1
        );
441
1
        let rule = get_rule_by_name("distribute_or_over_and").expect("rule registered");
442
1
        let err = rule.apply(&expr, &SymbolTable::new()).unwrap_err();
443
1
        assert!(
444
1
            matches!(err, ApplicationError::RuleNotApplicable),
445
            "or(and(a,b), and(c,d)) must not CNF-expand"
446
        );
447
1
    }
448
}