sat_solver/sat/
literal.rs

1#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
2//! Defines literals, the fundamental building blocks of clauses in SAT formulas.
3//!
4//! A literal is either a propositional variable (e.g. `x`) or its negation (e.g. `!x`).
5//! This module provides:
6//! - A `Variable` type alias (typically `u32`) for identifying propositional variables.
7//! - The `Literal` trait, which defines the common interface for literal representations.
8//!   This allows different data structures to represent literals while adhering to a
9//!   consistent API for accessing their variable, polarity, and performing operations
10//!   like negation.
11//! - Several concrete implementations of the `Literal` trait:
12//!   - `PackedLiteral`: A memory-efficient representation storing the variable and polarity
13//!     within a single `u32` using bit manipulation.
14//!   - `StructLiteral`: A straightforward representation using a struct with separate fields
15//!     for variable and polarity.
16//!   - `DoubleLiteral`: Represents a literal `L` as `2*variable_index(L) + polarity_bit(L)`.
17//!     This is a common encoding for mapping literals to unique `usize` indices.
18//!   - `NegativeLiteral`: Represents literals using signed integers, similar to the DIMACS format
19//!     (e.g. `x` as `variable_id`, `!x` as `-variable_id`).
20//! - A utility function `convert` to transform a literal of one type into another.
21
22use clap::ValueEnum;
23use std::fmt::{Debug, Display};
24use std::hash::Hash;
25
26/// Type alias for representing a propositional variable identifier.
27///
28/// Variables are typically identified by non-negative integers.
29/// `u32` provides a large range, suitable for most practical SAT instances.
30pub type Variable = u32;
31
32/// Trait defining the common interface for literal representations.
33///
34/// A literal is a propositional variable or its negation.
35/// This trait allows for different underlying data structures while providing
36/// consistent methods for:
37/// - Creating a new literal from a variable ID and its polarity.
38/// - Accessing the variable ID of the literal.
39/// - Accessing the polarity of the literal (true for positive, false for negative).
40/// - Negating the literal.
41/// - Converting to/from `i32` (DIMACS-like representation).
42/// - Converting to/from a `usize` index (useful for array lookups).
43pub trait Literal:
44    Copy + Debug + Eq + Hash + Default + Ord + PartialOrd + PartialEq + Send + Sync
45{
46    /// Creates a new literal.
47    ///
48    /// # Arguments
49    ///
50    /// * `var`: The `Variable` identifier.
51    /// * `polarity`: The polarity of the literal. Conventionally:
52    ///   - `true` indicates a positive literal (e.g. `x_var`).
53    ///   - `false` indicates a negative literal (e.g. `!x_var`).
54    fn new(var: Variable, polarity: bool) -> Self;
55
56    /// Returns the `Variable` identifier of this literal.
57    fn variable(self) -> Variable;
58
59    /// Returns the polarity of this literal.
60    ///
61    /// Convention:
62    /// - `true` for a positive literal (e.g. `x_i`).
63    /// - `false` for a negative literal (e.g. `!x_i`).
64    fn polarity(self) -> bool;
65
66    /// Returns the negated version of this literal.
67    /// If `self` is `x_i`, returns `!x_i`.
68    /// If `self` is `!x_i`, returns `x_i`.
69    #[must_use]
70    fn negated(self) -> Self;
71
72    /// Checks if the literal is negated (i.e. has negative polarity).
73    /// This is equivalent to `!self.polarity()`.
74    fn is_negated(self) -> bool {
75        !self.polarity()
76    }
77
78    /// Checks if the literal is positive (i.e. has positive polarity).
79    /// This is equivalent to `self.polarity()`.
80    fn is_positive(self) -> bool {
81        self.polarity()
82    }
83
84    /// Creates a literal from an `i32` value (DIMACS-style).
85    ///
86    /// A positive `value` `v` becomes a positive literal for variable `v`.
87    /// A negative `value` `-v` becomes a negative literal for variable `v`.
88    /// Variable `0` is not part of the DIMACS standard for literals and its handling
89    /// here depends on `Self::new` if `value.unsigned_abs()` is 0.
90    ///
91    /// # Arguments
92    ///
93    /// * `value`: The `i32` representing the literal.
94    #[must_use]
95    fn from_i32(value: i32) -> Self {
96        let polarity = value.is_positive();
97        let var = value.unsigned_abs();
98        Self::new(var, polarity)
99    }
100
101    /// Converts the literal to its `i32` (DIMACS-style) representation.
102    /// Positive literal `x_v` becomes `v` (as `i32`).
103    /// Negative literal `!x_v` becomes `-v` (as `i32`).
104    ///
105    /// # Panics
106    ///
107    /// `self.variable() as i32` may panic if `self.variable()` (a `u32`) is too large
108    /// to fit in an `i32` (i.e. > `i32::MAX`). `clippy::cast_possible_wrap` is allowed.
109    fn to_i32(&self) -> i32 {
110        #[allow(clippy::cast_possible_wrap)]
111        let var_signed = self.variable() as i32;
112        if self.polarity() {
113            var_signed
114        } else {
115            -var_signed
116        }
117    }
118
119    /// Converts the literal to a `usize` index.
120    ///
121    /// This is used for direct array indexing (e.g. in watch lists or vsids lists).
122    /// A common mapping is `2*variable_id + polarity_bit`, where `polarity_bit` is
123    /// 0 for one polarity and 1 for the other.
124    /// This default implementation uses `polarity_bit = 1` for positive (`polarity() == true`)
125    /// and `0` for negative (`polarity() == false`).
126    /// So, for variable `v`:
127    /// - `!v` (polarity=false) -> `2*v + 0`
128    /// - ` v` (polarity=true)  -> `2*v + 1`
129    fn index(self) -> usize {
130        let polarity_bit = usize::from(self.polarity());
131        let var_usize = self.variable() as usize;
132        var_usize.wrapping_mul(2).wrapping_add(polarity_bit)
133    }
134
135    /// Creates a literal from a `usize` index.
136    /// This is the inverse of `self.index()`.
137    ///
138    /// # Arguments
139    ///
140    /// * `index`: The `usize` index representing the literal.
141    ///
142    /// # Panics
143    ///
144    /// `var as Variable` (where `var` is `usize`) may panic if `var` is too large to fit
145    /// in `Variable` (a `u32`). `clippy::cast_possible_truncation` is allowed.
146    #[must_use]
147    fn from_index(index: usize) -> Self {
148        let polarity = (index % 2) != 0;
149        let var_usize = index / 2;
150        #[allow(clippy::cast_possible_truncation)]
151        Self::new(var_usize as Variable, polarity)
152    }
153}
154
155/// A memory-efficient literal representation using bit packing within a `u32`.
156///
157/// The lowest 31 bits store the variable ID, and the most significant bit (MSB)
158/// stores the polarity.
159/// - Variable ID: `self.0 & 0x7FFF_FFFF` (masks out the MSB). Max variable ID is `2^31 - 1`.
160/// - Polarity: `(self.0 >> 31) != 0`. `1` for positive, `0` for negative.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
162pub struct PackedLiteral(u32);
163
164/// Mask to extract the variable ID (lowest 31 bits).
165const VAR_MASK: u32 = 0x7FFF_FFFF;
166/// Left shift amount to move the polarity bit to the MSB position.
167const LSHIFT: u32 = 31;
168
169impl Literal for PackedLiteral {
170    /// Creates a new `PackedLiteral`. O(1) complexity.
171    /// `var` is masked to fit in 31 bits.
172    /// `polarity` (bool) is converted to `u32` (0 or 1) and shifted to the MSB.
173    #[inline]
174    fn new(var: Variable, polarity: bool) -> Self {
175        Self((var & VAR_MASK) | (u32::from(polarity) << LSHIFT))
176    }
177
178    /// Extracts the variable ID. O(1) complexity.
179    #[inline]
180    fn variable(self) -> Variable {
181        self.0 & VAR_MASK
182    }
183
184    /// Extracts the polarity. O(1) complexity.
185    /// Returns `true` if MSB is 1 (positive), `false` if MSB is 0 (negative).
186    #[inline]
187    fn polarity(self) -> bool {
188        (self.0 >> LSHIFT) != 0
189    }
190
191    /// Negates the literal by flipping the polarity bit (MSB). O(1) complexity.
192    #[inline]
193    fn negated(self) -> Self {
194        Self(self.0 ^ (1 << LSHIFT))
195    }
196}
197
198/// A straightforward literal representation using a struct.
199///
200/// Stores the variable ID and polarity in separate fields.
201/// Less memory-efficient than `PackedLiteral` if not optimised well by the compiler,
202/// but very clear.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
204pub struct StructLiteral {
205    /// The variable identifier.
206    value: u32,
207    /// The polarity (true for positive, false for negative).
208    polarity: bool,
209}
210
211impl Literal for StructLiteral {
212    #[inline]
213    fn new(var: Variable, polarity: bool) -> Self {
214        Self {
215            value: var,
216            polarity,
217        }
218    }
219    #[inline]
220    fn variable(self) -> Variable {
221        self.value
222    }
223
224    #[inline]
225    fn polarity(self) -> bool {
226        self.polarity
227    }
228
229    #[inline]
230    fn negated(self) -> Self {
231        Self {
232            value: self.value,
233            polarity: !self.polarity,
234        }
235    }
236}
237
238/// A literal representation where the literal is encoded as `2*var_id + polarity_bit`.
239///
240/// This encoding directly matches the `index()` method's logic.
241/// Polarity bit: `1` for positive, `0` for negative (if `polarity()` is true for positive).
242/// So, `polarity()` `true` -> `val % 2 == 1`.
243/// `polarity()` `false` -> `val % 2 == 0`.
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
245pub struct DoubleLiteral(u32);
246
247impl Literal for DoubleLiteral {
248    /// Creates a new `DoubleLiteral`.
249    /// `var * 2 + (1 if polarity is true else 0)`.
250    #[inline]
251    fn new(var: Variable, polarity: bool) -> Self {
252        Self(var.wrapping_mul(2).wrapping_add(u32::from(polarity)))
253    }
254
255    /// Extracts the variable ID: `self.0 / 2`.
256    #[inline]
257    fn variable(self) -> Variable {
258        self.0 / 2
259    }
260
261    /// Extracts the polarity: `self.0 % 2 != 0`.
262    /// (True if odd, meaning polarity bit was 1).
263    #[inline]
264    fn polarity(self) -> bool {
265        (self.0 % 2) != 0
266    }
267
268    /// Negates the literal by flipping the LSB (polarity bit).
269    /// `(2v+p) XOR 1` results in `2v + (p XOR 1)`.
270    #[inline]
271    fn negated(self) -> Self {
272        Self(self.0 ^ 1)
273    }
274
275    /// The internal `u32` value is already the index.
276    #[inline]
277    fn index(self) -> usize {
278        self.0 as usize
279    }
280}
281
282/// A literal representation using a signed `i32`, similar to DIMACS format.
283///
284/// Positive `var_id` for `x_var_id`.
285/// Negative `-var_id` for `!x_var_id`.
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
287pub struct NegativeLiteral(i32);
288
289impl Literal for NegativeLiteral {
290    /// Creates a new `NegativeLiteral`.
291    /// If `polarity` is true (positive literal), stores `var` as positive `i32`.
292    /// If `polarity` is false (negative literal), stores `var` as negative `i32`.
293    #[inline]
294    fn new(var: Variable, polarity: bool) -> Self {
295        #[allow(clippy::cast_possible_wrap)]
296        let var = var as i32;
297        let p = i32::from(!polarity); // 1 if polarity is false, 0 if true.
298        let var = var * (1 - 2 * p);
299        Self(var)
300    }
301
302    /// Extracts the variable ID (always positive).
303    #[inline]
304    fn variable(self) -> Variable {
305        self.0.unsigned_abs() // Returns u32.
306    }
307
308    /// Extracts the polarity. True if `self.0` is positive.
309    #[inline]
310    fn polarity(self) -> bool {
311        self.0.is_positive()
312    }
313
314    /// Negates the literal by flipping the sign of the stored `i32`.
315    #[inline]
316    fn negated(self) -> Self {
317        Self(-self.0)
318    }
319}
320
321/// Converts a literal of one type (`T`) into a literal of another type (`U`).
322///
323/// This function extracts the variable and polarity from the source literal `lit`
324/// and uses them to construct a new literal of the target type `U`.
325///
326/// # Type Parameters
327///
328/// * `T`: The source `Literal` type.
329/// * `U`: The target `Literal` type.
330///
331/// # Arguments
332///
333/// * `lit`: A reference to the source literal of type `T`.
334///
335/// # Returns
336///
337/// A new literal of type `U` with the same variable and polarity as `lit`.
338pub fn convert<T: Literal, U: Literal>(lit: &T) -> U {
339    let var = lit.variable();
340    let polarity = lit.polarity();
341    U::new(var, polarity)
342}
343
344/// Represents a literal that can be one of several implementations.
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
346pub enum LiteralImpls {
347    /// A packed literal using bit manipulation for memory efficiency.
348    Packed(PackedLiteral),
349    /// A struct-based literal with separate fields for variable and polarity.
350    Struct(StructLiteral),
351    /// A double literal represented as `2*variable + polarity_bit`.
352    Double(DoubleLiteral),
353    /// A negative literal represented as a signed integer (DIMACS-style).
354    Negative(NegativeLiteral),
355}
356
357impl Default for LiteralImpls {
358    fn default() -> Self {
359        Self::Double(DoubleLiteral::default())
360    }
361}
362
363impl Literal for LiteralImpls {
364    fn new(var: Variable, polarity: bool) -> Self {
365        Self::Packed(PackedLiteral::new(var, polarity))
366    }
367
368    fn variable(self) -> Variable {
369        match self {
370            Self::Packed(lit) => lit.variable(),
371            Self::Struct(lit) => lit.variable(),
372            Self::Double(lit) => lit.variable(),
373            Self::Negative(lit) => lit.variable(),
374        }
375    }
376
377    fn polarity(self) -> bool {
378        match self {
379            Self::Packed(lit) => lit.polarity(),
380            Self::Struct(lit) => lit.polarity(),
381            Self::Double(lit) => lit.polarity(),
382            Self::Negative(lit) => lit.polarity(),
383        }
384    }
385
386    fn negated(self) -> Self {
387        match self {
388            Self::Packed(lit) => Self::Packed(lit.negated()),
389            Self::Struct(lit) => Self::Struct(lit.negated()),
390            Self::Double(lit) => Self::Double(lit.negated()),
391            Self::Negative(lit) => Self::Negative(lit.negated()),
392        }
393    }
394}
395
396/// Represents the type of literal implementation.
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, ValueEnum)]
398pub enum LiteralType {
399    /// A packed literal using bit manipulation for memory efficiency.
400    Packed,
401    /// A struct-based literal with separate fields for variable and polarity.
402    Struct,
403    /// A double literal represented as `2*variable + polarity_bit`.
404    #[default]
405    Double,
406    /// A negative literal represented as a signed integer (DIMACS-style).
407    Negative,
408}
409
410impl Display for LiteralType {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        match self {
413            Self::Packed => write!(f, "packed"),
414            Self::Struct => write!(f, "struct"),
415            Self::Double => write!(f, "double"),
416            Self::Negative => write!(f, "negative"),
417        }
418    }
419}
420
421impl LiteralType {
422    /// Converts a `LiteralType` to its corresponding `Literal` implementation.
423    ///
424    /// # Arguments
425    ///
426    /// * `var`: The variable identifier for the literal.
427    /// * `polarity`: The polarity of the literal (true for positive, false for negative).
428    ///
429    /// # Returns
430    ///
431    /// A new literal of the specified type.
432    #[allow(dead_code)]
433    #[must_use]
434    pub fn to_impl(self, var: Variable, polarity: bool) -> LiteralImpls {
435        match self {
436            Self::Packed => LiteralImpls::Packed(PackedLiteral::new(var, polarity)),
437            Self::Struct => LiteralImpls::Struct(StructLiteral::new(var, polarity)),
438            Self::Double => LiteralImpls::Double(DoubleLiteral::new(var, polarity)),
439            Self::Negative => LiteralImpls::Negative(NegativeLiteral::new(var, polarity)),
440        }
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    fn test_literal_implementation<L: Literal + 'static>(var_id: Variable, initial_polarity: bool) {
449        let lit = L::new(var_id, initial_polarity);
450        assert_eq!(lit.variable(), var_id, "Variable ID mismatch");
451        assert_eq!(
452            lit.polarity(),
453            initial_polarity,
454            "Initial polarity mismatch"
455        );
456
457        if initial_polarity {
458            assert!(lit.is_positive(), "Should be positive");
459            assert!(!lit.is_negated(), "Should not be negated");
460        } else {
461            assert!(!lit.is_positive(), "Should not be positive");
462            assert!(lit.is_negated(), "Should be negated");
463        }
464
465        let neg_lit = lit.negated();
466        assert_eq!(
467            neg_lit.variable(),
468            var_id,
469            "Variable ID mismatch after negation"
470        );
471        assert_eq!(
472            neg_lit.polarity(),
473            !initial_polarity,
474            "Polarity should flip after negation"
475        );
476
477        let double_neg_lit = neg_lit.negated();
478        assert_eq!(
479            double_neg_lit.variable(),
480            var_id,
481            "Variable ID mismatch after double negation"
482        );
483        assert_eq!(
484            double_neg_lit.polarity(),
485            initial_polarity,
486            "Polarity should revert after double negation"
487        );
488        assert_eq!(
489            double_neg_lit, lit,
490            "Double negation should return original literal"
491        );
492
493        let i32_val = lit.to_i32();
494        let lit_from_i32 = L::from_i32(i32_val);
495        assert_eq!(
496            lit_from_i32, lit,
497            "from_i32(to_i32(lit)) should be lit. Got: L={lit:?}, i32={i32_val}, L'={lit_from_i32:?}"
498        );
499
500        #[allow(
501            clippy::cast_possible_truncation,
502            clippy::cast_sign_loss,
503            clippy::cast_possible_wrap
504        )]
505        let expected_i32_val = if initial_polarity {
506            var_id as i32
507        } else {
508            -(var_id as i32)
509        };
510        if var_id == 0 && !initial_polarity {
511            if var_id != 0 {
512                assert_eq!(
513                    i32_val, expected_i32_val,
514                    "to_i32() value incorrect. Expected {expected_i32_val}, Got {i32_val}"
515                );
516            }
517        } else {
518            assert_eq!(
519                i32_val, expected_i32_val,
520                "to_i32() value incorrect. Expected {expected_i32_val}, Got {i32_val}"
521            );
522        }
523
524        if std::any::TypeId::of::<L>() != std::any::TypeId::of::<DoubleLiteral>() {
525            let idx = lit.index();
526            let lit_from_idx = L::from_index(idx);
527            assert_eq!(lit_from_idx, lit, "from_index(index(lit)) should be lit");
528        }
529    }
530
531    #[test]
532    fn test_all_literal_implementations() {
533        let test_cases = [
534            (1u32, true),
535            (1u32, false),
536            (VAR_MASK, true),
537            (VAR_MASK, false),
538            (12345u32, true),
539            (67890u32, false),
540        ];
541
542        for &(var_id, polarity) in &test_cases {
543            let packed_var_id = var_id & VAR_MASK;
544
545            test_literal_implementation::<PackedLiteral>(packed_var_id, polarity);
546            test_literal_implementation::<StructLiteral>(var_id, polarity);
547            test_literal_implementation::<DoubleLiteral>(var_id, polarity);
548            if var_id != 0 {
549                test_literal_implementation::<NegativeLiteral>(var_id, polarity);
550            }
551        }
552    }
553
554    #[test]
555    fn test_double_literal_index() {
556        let lit_pos = DoubleLiteral::new(5, true); // 2*5 + 1 = 11
557        assert_eq!(lit_pos.index(), 11);
558        assert_eq!(DoubleLiteral::from_index(11), lit_pos);
559
560        let lit_neg = DoubleLiteral::new(5, false); // 2*5 + 0 = 10
561        assert_eq!(lit_neg.index(), 10);
562        assert_eq!(DoubleLiteral::from_index(10), lit_neg);
563    }
564
565    #[test]
566    fn test_literal_negation_consistency() {
567        assert_eq!(
568            PackedLiteral::new(1, false).negated().negated(),
569            PackedLiteral::new(1, false)
570        );
571        assert_eq!(
572            PackedLiteral::new(1, true).negated().negated(),
573            PackedLiteral::new(1, true)
574        );
575
576        assert_eq!(
577            StructLiteral::new(1, false).negated().negated(),
578            StructLiteral::new(1, false)
579        );
580        assert_eq!(
581            StructLiteral::new(1, true).negated().negated(),
582            StructLiteral::new(1, true)
583        );
584
585        assert_eq!(
586            DoubleLiteral::new(1, false).negated().negated(),
587            DoubleLiteral::new(1, false)
588        );
589        assert_eq!(
590            DoubleLiteral::new(1, true).negated().negated(),
591            DoubleLiteral::new(1, true)
592        );
593
594        assert_eq!(
595            NegativeLiteral::new(1, false).negated().negated(),
596            NegativeLiteral::new(1, false)
597        );
598        assert_eq!(
599            NegativeLiteral::new(1, true).negated().negated(),
600            NegativeLiteral::new(1, true)
601        );
602    }
603
604    #[test]
605    fn test_conversion_function() {
606        let p_lit = PackedLiteral::new(10, true);
607        let s_lit: StructLiteral = convert(&p_lit);
608        assert_eq!(s_lit.variable(), 10);
609        assert!(s_lit.polarity());
610
611        let d_lit: DoubleLiteral = convert(&s_lit);
612        assert_eq!(d_lit.variable(), 10);
613        assert!(d_lit.polarity());
614
615        let n_lit: NegativeLiteral = convert(&d_lit);
616        assert_eq!(n_lit.variable(), 10);
617        assert!(n_lit.polarity());
618
619        let p_lit_again: PackedLiteral = convert(&n_lit);
620        assert_eq!(p_lit_again, p_lit);
621    }
622}