Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2be6e8d
Started reworking multiplication for consistency checkers.
maartenflippo May 18, 2026
4ca596a
feat(pumpkin-solver): Implement consistency checker infrastructure
maartenflippo May 20, 2026
f7c9b69
Fix multiline test command syntax
maartenflippo May 20, 2026
11511e2
Implement domain consistency checker
maartenflippo May 20, 2026
70a2c31
Avoid copying domains when doing domain consistency checks
maartenflippo May 20, 2026
1d32565
Refactor PropagatorConstructor to remove `add_inference_checkers`
maartenflippo May 22, 2026
a9324b9
Fix conditional compilation errors and split up reified propagator
maartenflippo May 22, 2026
5fc57f9
Fix formatting
maartenflippo May 22, 2026
dee4887
Implement the checkers for nogood propagator
maartenflippo May 22, 2026
1c9f258
Cleanup code
maartenflippo May 22, 2026
893989d
Various cleanup
maartenflippo May 23, 2026
2d999b7
Rename 'ConsistencyChecker' to 'RetentionChecker'
maartenflippo May 24, 2026
2bbdd59
Explicitly introduce the concept of 'PropagationChecker'
maartenflippo May 24, 2026
12e8f2b
refactor: split up maximum propagator
ImkoMarijnissen May 26, 2026
5dbb7e1
feat: adding consistency checker for maximum
ImkoMarijnissen May 26, 2026
d220a64
feat: adding weak consistency checker
ImkoMarijnissen May 26, 2026
11713ff
feat: adding consistency checker for time-tabling
ImkoMarijnissen May 26, 2026
ca935d0
chore: add proper error logging to weak retention checker
ImkoMarijnissen May 26, 2026
4d22b09
fix: test cases
ImkoMarijnissen May 26, 2026
c676757
Squashed commit of the following:
maartenflippo May 27, 2026
f2169ee
refactor: simplify disjunctive propagation checker
ImkoMarijnissen May 28, 2026
a5b2548
feat: add consistency checker for disjunctive
ImkoMarijnissen May 28, 2026
2d8f33b
chore: add back commented out lines
ImkoMarijnissen May 28, 2026
86c42a2
merging main, step 1
EmirDe Sep 18, 2026
0d57a2f
main merged
EmirDe Sep 18, 2026
2348e1a
refactored retention checking, part 1
EmirDe Sep 18, 2026
c2a3d02
added retention checkers for the linear propagators
EmirDe Sep 18, 2026
94c2137
added a retention checker for the maximum propagator
EmirDe Sep 18, 2026
361f80a
added a retention checker for the binary equals propagator
EmirDe Sep 18, 2026
f017f69
added a retention checker for extended nogood propagation
EmirDe Sep 18, 2026
8a3eb99
added a retention checker for the absolute value propagator
EmirDe Sep 18, 2026
27d880d
added a retention checker for the binary not-equals propagator
EmirDe Sep 18, 2026
9b646e5
added comments to retention checkers
EmirDe Sep 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ jobs:
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --release --no-fail-fast --features pumpkin-solver/check-propagations --features pumpkin-core/check-deductions
- run: |
cargo test \
--release \
--no-fail-fast \
--features pumpkin-solver/check-propagations \
--features pumpkin-core/check-consistency \
--features pumpkin-core/check-deductions

wasm-test:
name: Test Suite for pumpkin-core in WebAssembly
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pumpkin-checker/src/inferences/linear.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use pumpkin_checking::InferenceChecker;
use pumpkin_checking::VariableState;
use pumpkin_propagators::arithmetic::LinearLessOrEqualInferenceChecker;
use pumpkin_propagators::arithmetic::LinearLessOrEqualChecker;

use crate::inferences::Fact;
use crate::inferences::InvalidInference;
Expand Down Expand Up @@ -52,7 +52,7 @@ fn verify_linear_inference(
fact: &Fact,
state: VariableState<Atomic>,
) -> Result<(), InvalidInference> {
let checker = LinearLessOrEqualInferenceChecker::new(linear.terms.clone().into(), linear.bound);
let checker = LinearLessOrEqualChecker::new(linear.terms.clone().into(), linear.bound);

if checker.check(state, &fact.premises, fact.consequent.as_ref()) {
Ok(())
Expand Down
2 changes: 2 additions & 0 deletions pumpkin-crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ clap = { version = "4.5.40", optional = true, features=["derive"] }
indexmap = "2.10.0"
dyn-clone = "1.0.20"
flate2 = { version = "1.1.2" }
bit-set = "0.10.0"

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-time = "1.1"
Expand All @@ -39,6 +40,7 @@ getrandom = { version = "0.4.2", features = ["wasm_js"] }
wasm-bindgen-test = "0.3"

[features]
check-consistency = []
check-propagations = []
check-deductions = []
debug-checks = []
Expand Down
2 changes: 1 addition & 1 deletion pumpkin-crates/core/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ pub mod options {
pub use crate::engine::ConflictResolverType;
pub use crate::engine::RestartOptions;
pub use crate::engine::SatisfactionSolverOptions as SolverOptions;
pub use crate::propagators::ReifiedPropagatorArgs;
pub use crate::propagators::nogoods::LearningOptions;
pub use crate::propagators::reified_propagator::ReifiedPropagatorArgs;
}

pub mod termination {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ impl Deref for PropositionalConjunction {
}
}

impl From<PropositionalConjunction> for Box<[Predicate]> {
fn from(val: PropositionalConjunction) -> Self {
val.predicates_in_conjunction.into()
}
}

impl PropositionalConjunction {
pub fn new(predicates_in_conjunction: Vec<Predicate>) -> Self {
PropositionalConjunction {
Expand Down
10 changes: 10 additions & 0 deletions pumpkin-crates/core/src/checkers/mod.rs
Comment thread
maartenflippo marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
mod propagation_checker;
mod retention_checker;
mod retention_store;
mod scope;
mod self_disabling;
mod store;

pub use propagation_checker::*;
pub use retention_checker::*;
pub use retention_store::*;
pub use scope::*;
pub use self_disabling::*;
pub use store::*;
64 changes: 64 additions & 0 deletions pumpkin-crates/core/src/checkers/propagation_checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use pumpkin_checking::BoxedChecker;
use pumpkin_checking::VariableState;

use crate::predicates::Predicate;
use crate::propagation::Domains;
use crate::propagation::ReadDomains;
use crate::variables::DomainId;

/// Tests whether an inference is correct given the solver state.
///
/// An inference is correct when:
/// 1. All premises are satisfied.
/// 2. The conjunction of the premises and negation of the consequent is consistent.
/// 3. The consequent is logically entailed given the inference code.
#[derive(Clone, Debug)]
pub struct PropagationChecker {
inference_checker: BoxedChecker<Predicate>,
}

impl PropagationChecker {
/// Create a new propagation checker given an inference checker and inference code.
pub fn new(inference_checker: BoxedChecker<Predicate>) -> PropagationChecker {
PropagationChecker { inference_checker }
}

/// Run the propagation checker for the given inference.
pub fn check(
&self,
premises: &[Predicate],
consequent: Option<Predicate>,
domains: Domains<'_>,
) -> Result<(), InvalidInference> {
let premises_satisfied = premises
.iter()
.all(|&premise| domains.evaluate_predicate(premise) == Some(true));

if !premises_satisfied {
return Err(InvalidInference::UnsatisfiedPremises);
}

let variable_state =
VariableState::prepare_for_conflict_check(premises.iter().copied(), consequent)
.map_err(InvalidInference::InconsistentPredicates)?;

if self
.inference_checker
.check(variable_state, premises, consequent.as_ref())
{
Ok(())
} else {
Err(InvalidInference::Unsound)
}
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InvalidInference {
/// Not all premises are true given the current state.
UnsatisfiedPremises,
/// The predicates that make up the inference are trivially inconsistent.
InconsistentPredicates(DomainId),
/// Cannot establish that the inference is sound.
Unsound,
}
43 changes: 43 additions & 0 deletions pumpkin-crates/core/src/checkers/retention_checker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::fmt::Debug;

use dyn_clone::DynClone;

use crate::checkers::Scope;
use crate::propagation::Domains;

/// A runtime verifier that determines whether a propagator has nothing left to propagate.
///
/// The contract mirrors the retention conditions of the formally verified proof checker: in the
/// current domains no inference of the propagator's rule applies, i.e. giving any variable in
/// the scope any value of its domain does not let the rule report a conflict. Each propagator
/// supplies its own checker, which may exploit the structure of its rule to decide this cheaply.
pub trait RetentionChecker: Debug + DynClone {
/// Returns `true` if the propagator has nothing left to propagate in `domains`, and `false`
/// if some inference of its rule still applies.
fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal preference but an enum specifying the return type would be a bit nicer I think

}

/// Wrapper around `Box<dyn RetentionChecker>` that implements [`Clone`].
#[derive(Debug)]
pub struct BoxedRetentionChecker(Box<dyn RetentionChecker>);

impl Clone for BoxedRetentionChecker {
fn clone(&self) -> Self {
BoxedRetentionChecker(dyn_clone::clone_box(&*self.0))
}
}
Comment on lines +24 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be implemented using the dyn_clone::clone_trait_object! macro?


impl<T> From<T> for BoxedRetentionChecker
where
T: RetentionChecker + 'static,
{
fn from(value: T) -> Self {
BoxedRetentionChecker(Box::new(value))
}
}

impl BoxedRetentionChecker {
pub fn check_retention(&mut self, scope: &Scope, domains: Domains<'_>) -> bool {
self.0.check_retention(scope, domains)
}
}
90 changes: 90 additions & 0 deletions pumpkin-crates/core/src/checkers/retention_store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::checkers::BoxedRetentionChecker;
use crate::checkers::Scope;
use crate::containers::KeyedBitSet;
use crate::containers::KeyedVec;
use crate::containers::StorageKey;
use crate::propagation::Domains;
use crate::variables::DomainId;

/// Holds the retention checkers in the solver.
///
/// Also responsible for enqueueing the checkers and dispatching them when instructed via
/// [`RetentionCheckerStore::run_enqueued`].
#[derive(Clone, Debug, Default)]
pub struct RetentionCheckerStore {
/// The checkers in the store.
store: KeyedVec<CheckerId, (Scope, BoxedRetentionChecker)>,
/// Map from [`DomainId`] to the relevant checkers via their ID.
watch_list: KeyedVec<DomainId, Vec<CheckerId>>,
/// The checkers to run the next time.
queue: Vec<CheckerId>,
/// Marks which checkers are enqueued to prevent duplicate checkers in
/// [`RetentionCheckerStore::queue`].
enqueued: KeyedBitSet<CheckerId>,
}

impl RetentionCheckerStore {
/// Add a new `checker` to the store with the given `scope`.
pub fn register(&mut self, scope: Scope, checker: BoxedRetentionChecker) {
let checker_slot = self.store.new_slot();

for (_, domain) in scope.domains() {
self.watch_list.accomodate(domain, vec![]);
self.watch_list[domain].push(checker_slot.key());
}

let _ = checker_slot.populate((scope, checker));
}

/// Called when the domain is modified.
///
/// Causes the checkers for this domain to be enqueued.
pub fn on_domain_event(&mut self, domain_id: DomainId) {
let Some(list) = self.watch_list.get(domain_id) else {
return;
};

for &checker_id in list {
if !self.enqueued.insert(checker_id) {
continue;
}

self.queue.push(checker_id);
}
}

/// Run the enqueued retention checkers.
pub fn run_enqueued(&mut self, mut domains: Domains<'_>) -> bool {
for checker_id in self.queue.drain(..) {
assert!(self.enqueued.remove(checker_id));

let (scope, checker) = &mut self.store[checker_id];

if !checker.check_retention(scope, domains.reborrow()) {
return false;
}
}

true
}

/// Clear the queue of retention checkers.
pub fn clear_queue(&mut self) {
self.queue.clear();
self.enqueued.clear();
}
}

/// An identifier for added checkers.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct CheckerId(u32);

impl StorageKey for CheckerId {
fn index(&self) -> usize {
self.0 as usize
}

fn create_from_index(index: usize) -> Self {
CheckerId(index as u32)
}
}
Loading
Loading