sat_solver/sat/expr.rs
1//! Defines an arbitrary boolean expression type and functions for its manipulation.
2//!
3//! This module provides an `Expr` enum to represent boolean expressions involving
4//! variables, logical NOT, AND, OR, and constant boolean values (true/false).
5//! It includes helper methods for checking expression types, unwrapping values,
6//! and constructing complex expressions from slices.
7//!
8//! Furthermore, it implements several logical transformation functions:
9//! - `demorgans_laws`: Applies De Morgan's laws to push negations inwards.
10//! - `distributive_laws`: Applies distributive laws (e.g. AND over OR) to transform
11//! expressions towards a conjunctive or disjunctive normal form.
12//! - `apply_laws`: A higher-level function that repeatedly applies De Morgan's and
13//! distributive laws until the expression stabilizes, aiming to convert it into
14//! a form suitable for CNF conversion (though full CNF conversion often requires
15//! more, like Tseytin transformation for non-exponential results).
16
17#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
18
19use crate::sat::literal::Variable;
20
21/// Represents a boolean expression.
22///
23/// Expressions can be:
24/// - A `Var(Variable)`: A propositional variable.
25/// - A `Not(Box<Expr>)`: The logical negation of an expression.
26/// - An `And(Box<Expr>, Box<Expr>)`: The logical conjunction of two expressions.
27/// - An `Or(Box<Expr>, Box<Expr>)`: The logical disjunction of two expressions.
28/// - A `Val(bool)`: A constant boolean value (`true` or `false`).
29#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub enum Expr {
31 /// A propositional variable, identified by `Variable`.
32 Var(Variable),
33 /// Logical negation of an inner expression.
34 Not(Box<Expr>),
35 /// Logical conjunction (AND) of two inner expressions.
36 And(Box<Expr>, Box<Expr>),
37 /// Logical disjunction (OR) of two inner expressions.
38 Or(Box<Expr>, Box<Expr>),
39 /// A constant boolean value (`true` or `false`).
40 Val(bool),
41}
42
43impl Expr {
44 /// Checks if the expression is a `Val` variant.
45 #[must_use]
46 pub const fn is_val(&self) -> bool {
47 matches!(self, Self::Val(_))
48 }
49
50 /// Checks if the expression is a `Var` variant.
51 #[must_use]
52 pub const fn is_var(&self) -> bool {
53 matches!(self, Self::Var(_))
54 }
55
56 /// Checks if the expression is a `Not` variant.
57 #[must_use]
58 pub const fn is_not(&self) -> bool {
59 matches!(self, Self::Not(_))
60 }
61
62 /// Checks if the expression is an `And` variant.
63 #[must_use]
64 pub const fn is_and(&self) -> bool {
65 matches!(self, Self::And(_, _))
66 }
67
68 /// Checks if the expression is an `Or` variant.
69 #[must_use]
70 pub const fn is_or(&self) -> bool {
71 matches!(self, Self::Or(_, _))
72 }
73
74 /// Checks if the expression is `Val(true)`.
75 #[must_use]
76 pub const fn is_true(&self) -> bool {
77 match self {
78 Self::Val(b) => *b,
79 _ => false,
80 }
81 }
82
83 /// Checks if the expression is `Val(false)`.
84 #[must_use]
85 pub const fn is_false(&self) -> bool {
86 match self {
87 Self::Val(b) => !*b,
88 _ => false,
89 }
90 }
91
92 /// Unwraps the boolean value if the expression is a `Val` variant.
93 ///
94 /// # Panics
95 ///
96 /// Panics if the expression is not `Expr::Val(_)`.
97 #[must_use]
98 pub fn unwrap_val(&self) -> bool {
99 match self {
100 Self::Val(b) => *b,
101 _ => panic!("Called unwrap_val on a non-Val expression: {self:?}"),
102 }
103 }
104
105 /// Unwraps the variable identifier if the expression is a `Var` variant.
106 ///
107 /// # Panics
108 ///
109 /// Panics if the expression is not `Expr::Var(_)`.
110 #[must_use]
111 pub fn unwrap_var(&self) -> Variable {
112 match self {
113 Self::Var(i) => *i,
114 _ => panic!("Called unwrap_var on a non-Var expression: {self:?}"),
115 }
116 }
117
118 /// Creates a new expression representing the disjunction (OR) of all expressions in the slice `e`.
119 ///
120 /// `e_1 OR e_2 OR ... OR e_n`.
121 /// The fold starts with `Expr::Val(false)` as the identity for OR (`x OR false == x`).
122 /// If `e` is empty, `Expr::Val(false)` is returned.
123 ///
124 /// # Arguments
125 ///
126 /// * `e`: A slice of `Expr` to be OR-ed together.
127 ///
128 /// # Returns
129 ///
130 /// A new `Expr` representing the OR of all expressions in `e`.
131 ///
132 /// # Panics
133 ///
134 /// Panics if `e` is empty, as the OR of no elements is not defined.
135 #[must_use]
136 pub fn ors(expressions: &[Self]) -> Self {
137 if expressions.is_empty() {
138 return Self::Val(false);
139 }
140 let mut iter = expressions.iter();
141 let first = iter.next().unwrap().clone();
142 iter.fold(first, |acc, x| Self::Or(Box::new(acc), Box::new(x.clone())))
143 }
144
145 /// Creates a new expression representing the conjunction (AND) of all expressions in the slice `e`.
146 ///
147 /// `e_1 AND e_2 AND ... AND e_n`.
148 /// The fold starts with `Expr::Val(true)` as the identity for AND (`x AND true == x`).
149 /// If `e` is empty, `Expr::Val(true)` is returned.
150 ///
151 /// # Arguments
152 ///
153 /// * `e`: A slice of `Expr` to be AND-ed together.
154 ///
155 /// # Returns
156 ///
157 /// A new `Expr` representing the AND of all expressions in `e`.
158 ///
159 /// # Panics
160 ///
161 /// Should not panic, but if `e` is empty, it returns `Expr::Val(true)`.
162 #[must_use]
163 pub fn ands(expressions: &[Self]) -> Self {
164 if expressions.is_empty() {
165 return Self::Val(true);
166 }
167 let mut iter = expressions.iter();
168 // SAFETY: We check that it's not empty above, so `next()` will always return Some.
169 unsafe {
170 let first = iter.next().unwrap_unchecked().clone();
171 iter.fold(first, |acc, x| {
172 Self::And(Box::new(acc), Box::new(x.clone()))
173 })
174 }
175 }
176}
177
178impl From<bool> for Expr {
179 /// Converts a `bool` into an `Expr::Val`.
180 fn from(b: bool) -> Self {
181 Self::Val(b)
182 }
183}
184
185impl From<Variable> for Expr {
186 /// Converts a `Variable` into an `Expr::Var`.
187 fn from(i: Variable) -> Self {
188 Self::Var(i)
189 }
190}
191
192impl From<i32> for Expr {
193 /// Converts an `i32` (DIMACS-style literal) into an `Expr`.
194 /// A positive `i` becomes `Expr::Var(i.abs())`.
195 /// A negative `i` becomes `Expr::Not(Box::new(Expr::Var(i.abs())))`.
196 /// `Variable` is assumed to be constructible from `u32`.
197 fn from(i: i32) -> Self {
198 let var_id = i.unsigned_abs();
199 if i < 0 {
200 Self::Not(Box::new(Self::Var(var_id)))
201 } else {
202 Self::Var(var_id)
203 }
204 }
205}
206
207/// Applies De Morgan's laws to an expression to push negations (`Not`) inwards.
208///
209/// - `Not(And(e1, e2))` becomes `Or(Not(e1), Not(e2))` (after recursive calls).
210/// - `Not(Or(e1, e2))` becomes `And(Not(e1), Not(e2))` (after recursive calls).
211/// - `Not(Not(e))` becomes `e`.
212/// - `Not(Var(i))` remains `Not(Var(i))`.
213/// - `Not(Val(b))` becomes `Val(!b)`.
214///
215/// The function recursively applies these transformations throughout the expression.
216/// Non-`Not` expressions are traversed, and `demorgans_laws` is applied to their sub-expressions.
217///
218/// # Arguments
219///
220/// * `expr`: The expression to transform.
221///
222/// # Returns
223///
224/// A new `Expr` with negations pushed inwards.
225#[must_use]
226pub fn demorgans_laws(expr: &Expr) -> Expr {
227 match expr {
228 Expr::Not(e_boxed) => match e_boxed.as_ref() {
229 Expr::Var(i) => Expr::Not(Box::new(Expr::Var(*i))),
230 Expr::Not(inner_e_boxed) => demorgans_laws(inner_e_boxed.as_ref()),
231 Expr::And(e1, e2) => Expr::Or(
232 Box::new(demorgans_laws(&Expr::Not(e1.clone()))),
233 Box::new(demorgans_laws(&Expr::Not(e2.clone()))),
234 ),
235 Expr::Or(e1, e2) => Expr::And(
236 Box::new(demorgans_laws(&Expr::Not(e1.clone()))),
237 Box::new(demorgans_laws(&Expr::Not(e2.clone()))),
238 ),
239 Expr::Val(b) => Expr::Val(!*b),
240 },
241 Expr::And(e1, e2) => Expr::And(Box::new(demorgans_laws(e1)), Box::new(demorgans_laws(e2))),
242 Expr::Or(e1, e2) => Expr::Or(Box::new(demorgans_laws(e1)), Box::new(demorgans_laws(e2))),
243 Expr::Val(b) => Expr::Val(*b),
244 Expr::Var(i) => Expr::Var(*i),
245 }
246}
247
248/// Applies distributive laws to an expression, primarily `A AND (B OR C) -> (A AND B) OR (A AND C)`
249/// and `(B OR C) AND A -> (B AND A) OR (C AND A)`.
250///
251/// This function aims to transform expressions towards a disjunctive normal form (DNF) if applied
252/// to distribute AND over OR.
253///
254/// It recursively applies these transformations.
255///
256/// # Arguments
257///
258/// * `expr`: The expression to transform.
259///
260/// # Panics
261///
262/// Panics if it encounters an `Expr::Not(_)` variant directly during distribution, as it
263/// expects negations to have been pushed to the literal level (NNF) by `demorgans_laws`
264/// before distribution is applied for typical CNF/DNF conversion steps.
265#[must_use]
266pub fn distributive_laws(expr: &Expr) -> Expr {
267 let current_expr = expr.clone();
268
269 match current_expr {
270 Expr::Or(e1_boxed, e2_boxed) => {
271 let e1_cnf = distributive_laws(&e1_boxed);
272 let e2_cnf = distributive_laws(&e2_boxed);
273
274 match (e1_cnf.clone(), e2_cnf.clone()) {
275 (Expr::And(e11, e12), _) => Expr::And(
276 Box::new(distributive_laws(&Expr::Or(e11, Box::new(e2_cnf.clone())))),
277 Box::new(distributive_laws(&Expr::Or(e12, Box::new(e2_cnf)))),
278 ),
279 (_, Expr::And(e21, e22)) => Expr::And(
280 Box::new(distributive_laws(&Expr::Or(Box::new(e1_cnf.clone()), e21))),
281 Box::new(distributive_laws(&Expr::Or(Box::new(e1_cnf), e22))),
282 ),
283 _ => Expr::Or(Box::new(e1_cnf), Box::new(e2_cnf)),
284 }
285 }
286 Expr::And(e1_boxed, e2_boxed) => Expr::And(
287 Box::new(distributive_laws(&e1_boxed)),
288 Box::new(distributive_laws(&e2_boxed)),
289 ),
290 Expr::Not(inner) => Expr::Not(Box::new(distributive_laws(inner.as_ref()))),
291 Expr::Val(b) => Expr::Val(b),
292 Expr::Var(i) => Expr::Var(i),
293 }
294}
295
296/// Applies De Morgan's laws and then distributive laws repeatedly to an expression
297/// until it no longer changes (reaches a fixed point).
298///
299/// This is a common step in converting an arbitrary boolean expression into a
300/// standard form, often as a preliminary step before full CNF or DNF conversion.
301/// The goal is to achieve Negation Normal Form (NNF) via `demorgans_laws`,
302/// and then apply `distributive_laws` to move towards a
303/// product-of-sums (CNF like) structure.
304///
305/// # Arguments
306///
307/// * `expr`: The initial expression.
308///
309/// # Returns
310///
311/// The transformed `Expr` after applying laws to a fixed point.
312#[must_use]
313pub fn apply_laws(expr: &Expr) -> Expr {
314 let mut current_expr = expr.clone();
315 loop {
316 let nnf_expr = demorgans_laws(¤t_expr);
317 let distributed_expr = distributive_laws(&nnf_expr);
318
319 if distributed_expr == current_expr {
320 break;
321 }
322 current_expr = distributed_expr;
323 }
324 current_expr
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 fn var(id: u32) -> Expr {
332 Expr::Var(id)
333 }
334 fn not(expr: Expr) -> Expr {
335 Expr::Not(Box::new(expr))
336 }
337 fn and(e1: Expr, e2: Expr) -> Expr {
338 Expr::And(Box::new(e1), Box::new(e2))
339 }
340 fn or(e1: Expr, e2: Expr) -> Expr {
341 Expr::Or(Box::new(e1), Box::new(e2))
342 }
343
344 #[test]
345 fn test_demorgans_laws_not_and() {
346 let expr = not(and(var(1), var(2)));
347 let expected = or(not(var(1)), not(var(2)));
348 assert_eq!(demorgans_laws(&expr), expected);
349 }
350
351 #[test]
352 fn test_demorgans_laws_not_or() {
353 let expr = not(or(var(1), var(2)));
354 let expected = and(not(var(1)), not(var(2)));
355 assert_eq!(demorgans_laws(&expr), expected);
356 }
357
358 #[test]
359 fn test_demorgans_laws_double_negation() {
360 let expr = not(not(var(1)));
361 let expected = var(1);
362 assert_eq!(demorgans_laws(&expr), expected);
363 }
364
365 #[test]
366 fn test_demorgans_laws_not_val() {
367 let expr = not(Expr::Val(true));
368 let expected = Expr::Val(false);
369 assert_eq!(demorgans_laws(&expr), expected);
370 }
371
372 #[test]
373 fn test_demorgans_laws_nested() {
374 let expr = not(and(var(1), not(or(var(2), var(3)))));
375 let expected = or(not(var(1)), or(var(2), var(3)));
376 assert_eq!(demorgans_laws(&expr), expected);
377 }
378
379 #[test]
380 fn test_distributive_laws_a_and_or_b_c() {
381 let expr = and(or(var(1), var(2)), var(3));
382 let expected = and(or(var(1), var(2)), var(3));
383 assert_eq!(distributive_laws(&expr), expected);
384 }
385
386 #[test]
387 fn test_distributive_laws_or_a_b_and_c() {
388 let expr = and(or(var(1), var(2)), var(3));
389 let expected = and(or(var(1), var(2)), var(3));
390 assert_eq!(distributive_laws(&expr), expected);
391 }
392
393 #[test]
394 fn test_distributive_laws_no_change() {
395 let expr = and(var(1), var(2));
396 assert_eq!(distributive_laws(&expr), expr.clone());
397
398 let expr_or = or(var(1), var(2));
399 assert_eq!(distributive_laws(&expr_or), expr_or.clone());
400 }
401
402 #[test]
403 fn test_distributive_laws_handles_not_nnf() {
404 let expr = and(not(var(1)), or(var(2), var(3)));
405 let expected = and(not(var(1)), or(var(2), var(3)));
406 assert_eq!(distributive_laws(&expr), expected);
407 }
408
409 #[test]
410 fn test_apply_laws_with_demorgans() {
411 let expr = and(not(and(var(1), var(2))), var(3));
412 let expected = and(or(not(var(1)), not(var(2))), var(3));
413 assert_eq!(apply_laws(&expr), expected);
414 }
415
416 #[test]
417 fn test_ors_and_ands_helpers() {
418 assert_eq!(Expr::ors(&[]), Expr::Val(false));
419 assert_eq!(Expr::ands(&[]), Expr::Val(true));
420
421 let exprs = [var(1), var(2), var(3)];
422 let or_expr = Expr::ors(&exprs);
423 let expected_or = or(or(var(1), var(2)), var(3));
424 assert_eq!(or_expr, expected_or);
425
426 let and_expr = Expr::ands(&exprs);
427 let expected_and = and(and(var(1), var(2)), var(3));
428 assert_eq!(and_expr, expected_and);
429
430 assert_eq!(Expr::ors(&[var(1)]), var(1));
431 assert_eq!(Expr::ands(&[var(1)]), var(1));
432 }
433}