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}