From 39f97fd8f43b705a4fb54e6ed814d67bdf7fdc66 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Wed, 27 May 2026 22:09:21 +1000
Subject: [PATCH 01/11] PropagatatorConstructor::create now returns an
EventRegistration
---
pumpkin-crates/core/src/engine/state.rs | 2 +-
.../core/src/propagation/constructor.rs | 6 +-
.../src/propagation/event_registration.rs | 99 +++++++++++++++++++
pumpkin-crates/core/src/propagation/mod.rs | 3 +
.../hypercube_linear/propagator.rs | 14 ++-
.../propagators/nogoods/nogood_propagator.rs | 20 ++--
.../src/propagators/reified_propagator.rs | 14 ++-
7 files changed, 138 insertions(+), 20 deletions(-)
create mode 100644 pumpkin-crates/core/src/propagation/event_registration.rs
diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs
index 9e358fab6..cb3fe554a 100644
--- a/pumpkin-crates/core/src/engine/state.rs
+++ b/pumpkin-crates/core/src/engine/state.rs
@@ -341,7 +341,7 @@ impl State {
let constructor_context =
PropagatorConstructorContext::new(original_handle.propagator_id(), self);
- let propagator = constructor.create(constructor_context);
+ let (registration, propagator) = constructor.create(constructor_context);
pumpkin_assert_simple!(
propagator.priority() as u8 <= 3,
diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs
index 791627880..e968ee61f 100644
--- a/pumpkin-crates/core/src/propagation/constructor.rs
+++ b/pumpkin-crates/core/src/propagation/constructor.rs
@@ -25,6 +25,7 @@ use crate::proof::InferenceCode;
#[cfg(doc)]
use crate::propagation::DomainEvent;
use crate::propagation::DomainEvents;
+use crate::propagation::EventRegistration;
use crate::propagators::reified_propagator::ReifiedChecker;
use crate::variables::IntegerVariable;
use crate::variables::Literal;
@@ -48,7 +49,10 @@ pub trait PropagatorConstructor {
fn add_inference_checkers(&self, _checkers: InferenceCheckers<'_>) {}
/// Create the propagator instance from `Self`.
- fn create(self, context: PropagatorConstructorContext) -> Self::PropagatorImpl;
+ fn create(
+ self,
+ context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl);
}
/// Interface used to add [`InferenceChecker`]s to the [`State`].
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
new file mode 100644
index 000000000..cc096ea42
--- /dev/null
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -0,0 +1,99 @@
+use crate::propagation::DomainEvents;
+use crate::propagation::LocalId;
+use crate::variables::DomainId;
+
+/// Anything that can subscribe to domain events.
+pub trait EventTarget {
+ /// Add a registration of self for the given domain events with a local id.
+ fn register(
+ &self,
+ registration: &mut EventRegistration,
+ events: DomainEvents,
+ local_id: LocalId,
+ );
+}
+
+/// Contains all the events and domains that a propagator needs to be enqueued for.
+#[derive(Clone, Debug)]
+pub struct EventRegistration(Vec<(DomainId, DomainEvents, LocalId)>);
+
+impl EventRegistration {
+ /// Create an [`EventRegistration`] without any variables.
+ ///
+ /// This is the uncommon case. Without registering for variable events, a propagator will never
+ /// be enqueued.
+ pub fn empty() -> EventRegistration {
+ EventRegistration(vec![])
+ }
+
+ /// Create a new [`EventRegistrationBuilder`].
+ ///
+ /// If no event registrations will be made, use [`EventRegistration::empty`] instead.
+ /// Calling [`EventRegistrationBuilder::build`] without any registrations will cause a panic.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// let registration = EventRegistration::builder()
+ /// .add(LocalId::from(0), &v1)
+ /// .add(LocalId::from(1), &v2)
+ /// .build();
+ /// ```
+ pub fn builder() -> EventRegistrationBuilder {
+ EventRegistrationBuilder {
+ registrations: EventRegistration(vec![]),
+ }
+ }
+
+ /// Add a new event registration.
+ pub fn add(
+ &mut self,
+ target: &impl EventTarget,
+ domain_events: DomainEvents,
+ local_id: LocalId,
+ ) {
+ target.register(self, domain_events, local_id);
+ }
+
+ /// Register the [`DomainId`] with the given [`LocalId`] on the given [`DomainEvents`].
+ ///
+ /// When creating the [`EventRegistration`] in a propagator, prefer to use
+ /// [`EventRegistration::add`] to deal with domain views.
+ pub fn with_domain(&mut self, domain_id: DomainId, events: DomainEvents, local_id: LocalId) {
+ self.0.push((domain_id, events, local_id));
+ }
+}
+
+/// Used to construct an [`EventRegistration`] for heterogeneous [`EventTarget`] implementations.
+///
+/// See [`EventRegistration::builder`] for a usage example.
+#[derive(Clone, Debug)]
+pub struct EventRegistrationBuilder {
+ registrations: EventRegistration,
+}
+
+impl EventRegistrationBuilder {
+ /// Add a new event registration.
+ pub fn add(
+ mut self,
+ target: &impl EventTarget,
+ domain_events: DomainEvents,
+ local_id: LocalId,
+ ) -> Self {
+ self.registrations.add(target, domain_events, local_id);
+ self
+ }
+
+ /// Finish constructing the [`EventRegistration`].
+ ///
+ /// If no variables are registered, then this panics. If no variables can be registered during
+ /// construction, use [`EventRegistration::empty`].
+ pub fn build(self) -> EventRegistration {
+ assert!(
+ !self.registrations.0.is_empty(),
+ "did not register for any events"
+ );
+
+ self.registrations
+ }
+}
diff --git a/pumpkin-crates/core/src/propagation/mod.rs b/pumpkin-crates/core/src/propagation/mod.rs
index 7c4b5b02b..c74ee7106 100644
--- a/pumpkin-crates/core/src/propagation/mod.rs
+++ b/pumpkin-crates/core/src/propagation/mod.rs
@@ -69,6 +69,7 @@
mod constructor;
mod contexts;
mod domains;
+mod event_registration;
mod local_id;
mod propagator;
@@ -76,6 +77,8 @@ pub(crate) mod propagator_id;
pub(crate) mod propagator_var_id;
pub(crate) mod store;
+pub use event_registration::*;
+
mod reexports {
// Re-exports of types not in this module according to the file tree.
// These will probably be be moved at some point, but for now they are simply re-exported here
diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
index fc1e0a3f7..0f6068935 100644
--- a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
+++ b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
@@ -7,6 +7,7 @@ use crate::predicates::PropositionalConjunction;
use crate::proof::ConstraintTag;
use crate::proof::InferenceCode;
use crate::propagation::DomainEvents;
+use crate::propagation::EventRegistration;
use crate::propagation::InferenceCheckers;
use crate::propagation::LocalId;
use crate::propagation::PropagationContext;
@@ -44,7 +45,10 @@ impl PropagatorConstructor for HypercubeLinearConstructor {
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
let HypercubeLinearConstructor {
hypercube,
linear,
@@ -65,13 +69,17 @@ impl PropagatorConstructor for HypercubeLinearConstructor {
]
};
- HypercubeLinearPropagator {
+ let propagator = HypercubeLinearPropagator {
linear,
hypercube_predicates,
watched_predicates,
inference_code: InferenceCode::new(constraint_tag, HypercubeLinear),
- }
+ };
+
+ let registration = EventRegistration::builder().build();
+
+ (registration, propagator)
}
}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
index 4312f65cd..e310d340c 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
@@ -21,6 +21,7 @@ use crate::engine::reason::ReasonStore;
use crate::predicate;
use crate::proof::InferenceCode;
use crate::propagation::EnqueueDecision;
+use crate::propagation::EventRegistration;
use crate::propagation::ExplanationContext;
use crate::propagation::LazyExplanation;
use crate::propagation::NotificationContext;
@@ -101,10 +102,13 @@ impl NogoodPropagatorConstructor {
impl PropagatorConstructor for NogoodPropagatorConstructor {
type PropagatorImpl = NogoodPropagator;
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
context.will_not_register_any_events();
- NogoodPropagator {
+ let propagator = NogoodPropagator {
handle: PropagatorHandle::new(context.propagator_id),
parameters: self.parameters,
nogood_predicates: ArenaAllocator::new(self.capacity),
@@ -117,7 +121,9 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
lbd_helper: Default::default(),
bumped_nogoods: Default::default(),
temp_nogood_reason: Default::default(),
- }
+ };
+
+ (EventRegistration::empty(), propagator)
}
}
@@ -133,14 +139,6 @@ struct Watcher {
cached_predicate: PredicateId,
}
-impl PropagatorConstructor for NogoodPropagator {
- type PropagatorImpl = Self;
-
- fn create(self, _: PropagatorConstructorContext) -> Self::PropagatorImpl {
- self
- }
-}
-
/// Keeps track of three tiers of nogoods:
/// - "low" LBD nogoods
/// - "mid" LBD nogoods
diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs
index 0bb782495..f30044cdd 100644
--- a/pumpkin-crates/core/src/propagators/reified_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs
@@ -9,6 +9,7 @@ use crate::predicates::Predicate;
use crate::propagation::DomainEvents;
use crate::propagation::Domains;
use crate::propagation::EnqueueDecision;
+use crate::propagation::EventRegistration;
use crate::propagation::ExplanationContext;
use crate::propagation::InferenceCheckers;
use crate::propagation::LazyExplanation;
@@ -38,13 +39,16 @@ where
{
type PropagatorImpl = ReifiedPropagator;
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
let ReifiedPropagatorArgs {
propagator,
reification_literal,
} = self;
- let propagator = propagator.create(context.reborrow());
+ let (registration, propagator) = propagator.create(context.reborrow());
let reification_literal_id = context.get_next_local_id();
context.register(
@@ -55,13 +59,15 @@ where
let name = format!("Reified({})", propagator.name());
- ReifiedPropagator {
+ let propagator = ReifiedPropagator {
propagator,
reification_literal,
reification_literal_id,
name,
reason_buffer: vec![],
- }
+ };
+
+ (EventRegistration::builder().build(), propagator)
}
fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
From 4dd42225ba27f6d6758378244848285062142606 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Wed, 27 May 2026 22:26:41 +1000
Subject: [PATCH 02/11] Remove registration from PropagatorConstructorContext
---
pumpkin-crates/core/src/basic_types/mod.rs | 2 -
.../core/src/basic_types/ref_or_owned.rs | 49 ----------
.../core/src/engine/variables/affine_view.rs | 19 ++++
.../core/src/engine/variables/domain_id.rs | 14 +++
.../core/src/engine/variables/literal.rs | 15 +++
.../core/src/propagation/constructor.rs | 93 -------------------
.../src/propagation/event_registration.rs | 21 ++++-
.../core/src/propagation/local_id.rs | 5 +
.../propagators/nogoods/nogood_propagator.rs | 4 +-
.../src/propagators/reified_propagator.rs | 14 ++-
10 files changed, 81 insertions(+), 155 deletions(-)
delete mode 100644 pumpkin-crates/core/src/basic_types/ref_or_owned.rs
diff --git a/pumpkin-crates/core/src/basic_types/mod.rs b/pumpkin-crates/core/src/basic_types/mod.rs
index b87c9a856..b1fd78c9c 100644
--- a/pumpkin-crates/core/src/basic_types/mod.rs
+++ b/pumpkin-crates/core/src/basic_types/mod.rs
@@ -4,7 +4,6 @@ mod function;
mod predicate_id_generators;
mod propositional_conjunction;
mod random;
-mod ref_or_owned;
pub(crate) mod sequence_generators;
mod solution;
mod stored_conflict_info;
@@ -19,7 +18,6 @@ pub use predicate_id_generators::PredicateId;
pub use predicate_id_generators::PredicateIdGenerator;
pub use propositional_conjunction::PropositionalConjunction;
pub use random::*;
-pub(crate) use ref_or_owned::*;
pub use solution::ProblemSolution;
pub use solution::Solution;
pub use solution::SolutionReference;
diff --git a/pumpkin-crates/core/src/basic_types/ref_or_owned.rs b/pumpkin-crates/core/src/basic_types/ref_or_owned.rs
deleted file mode 100644
index f16f3d6c1..000000000
--- a/pumpkin-crates/core/src/basic_types/ref_or_owned.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-use std::ops::Deref;
-use std::ops::DerefMut;
-
-/// Either owns a value or has a mutable reference to a value.
-///
-/// Used to store data in a reborrowed context that needs to be 'shared' with the original context
-/// that was reborrowed from. For example, when dropping a reborrowed context, we want
-/// [`PropagatorConstructorContext::get_next_local_id`] in the original context to 'know' about the
-/// registered local ids in the reborrowed context.
-#[derive(Debug)]
-pub(crate) enum RefOrOwned<'a, T> {
- Ref(&'a mut T),
- Owned(T),
-}
-
-impl RefOrOwned<'_, T> {
- pub(crate) fn reborrow(&mut self) -> RefOrOwned<'_, T> {
- match self {
- RefOrOwned::Ref(ref_to_t) => RefOrOwned::Ref(ref_to_t),
- RefOrOwned::Owned(value) => RefOrOwned::Ref(value),
- }
- }
-}
-
-impl From for RefOrOwned<'_, T> {
- fn from(value: T) -> Self {
- RefOrOwned::Owned(value)
- }
-}
-
-impl Deref for RefOrOwned<'_, T> {
- type Target = T;
-
- fn deref(&self) -> &Self::Target {
- match self {
- RefOrOwned::Ref(reference) => reference,
- RefOrOwned::Owned(value) => value,
- }
- }
-}
-
-impl DerefMut for RefOrOwned<'_, T> {
- fn deref_mut(&mut self) -> &mut Self::Target {
- match self {
- RefOrOwned::Ref(reference) => reference,
- RefOrOwned::Owned(value) => value,
- }
- }
-}
diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs
index 3ce96acfb..c0e41a5ec 100644
--- a/pumpkin-crates/core/src/engine/variables/affine_view.rs
+++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs
@@ -14,6 +14,9 @@ use crate::engine::predicates::predicate_constructor::PredicateConstructor;
use crate::engine::variables::DomainId;
use crate::engine::variables::IntegerVariable;
use crate::math::num_ext::NumExt;
+use crate::propagation::EventRegistration;
+use crate::propagation::EventTarget;
+use crate::propagation::LocalId;
/// Models the constraint `y = ax + b`, by expressing the domain of `y` as a transformation of the
/// domain of `x`.
@@ -54,6 +57,22 @@ impl AffineView {
}
}
+impl EventTarget for AffineView {
+ fn register(
+ &self,
+ registration: &mut EventRegistration,
+ mut events: EnumSet,
+ local_id: LocalId,
+ ) {
+ let bound = DomainEvent::LowerBound | DomainEvent::UpperBound;
+ let intersection = events.intersection(bound);
+ if intersection.len() == 1 && self.scale.is_negative() {
+ events = events.symmetric_difference(bound);
+ }
+ self.inner.register(registration, events, local_id);
+ }
+}
+
impl CheckerVariable for AffineView {
fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool {
self.inner.does_atomic_constrain_self(atomic)
diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs
index 22ede3a40..b384e749f 100644
--- a/pumpkin-crates/core/src/engine/variables/domain_id.rs
+++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs
@@ -12,6 +12,9 @@ use crate::engine::variables::IntegerVariable;
use crate::predicates::Predicate;
use crate::predicates::PredicateConstructor;
use crate::predicates::PredicateType;
+use crate::propagation::EventRegistration;
+use crate::propagation::EventTarget;
+use crate::propagation::LocalId;
use crate::pumpkin_assert_simple;
/// A structure which represents the most basic [`IntegerVariable`]; it is simply the id which links
@@ -32,6 +35,17 @@ impl DomainId {
}
}
+impl EventTarget for DomainId {
+ fn register(
+ &self,
+ registration: &mut EventRegistration,
+ events: EnumSet,
+ local_id: LocalId,
+ ) {
+ registration.with_domain(*self, events, local_id);
+ }
+}
+
impl CheckerVariable for DomainId {
fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool {
atomic.get_domain() == *self
diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs
index 980ffa737..8cc72777f 100644
--- a/pumpkin-crates/core/src/engine/variables/literal.rs
+++ b/pumpkin-crates/core/src/engine/variables/literal.rs
@@ -15,6 +15,9 @@ use crate::engine::notifications::Watchers;
use crate::engine::predicates::predicate::Predicate;
use crate::engine::predicates::predicate_constructor::PredicateConstructor;
use crate::engine::variables::AffineView;
+use crate::propagation::EventRegistration;
+use crate::propagation::EventTarget;
+use crate::propagation::LocalId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Literal {
@@ -73,6 +76,18 @@ macro_rules! forward {
}
}
+impl EventTarget for Literal {
+ fn register(
+ &self,
+ registration: &mut EventRegistration,
+ events: EnumSet,
+ local_id: LocalId,
+ ) {
+ self.integer_variable
+ .register(registration, events, local_id);
+ }
+}
+
impl CheckerVariable for Literal {
forward!(integer_variable, fn does_atomic_constrain_self(&self, atomic: &Predicate) -> bool);
forward!(integer_variable, fn atomic_less_than(&self, value: i32) -> Predicate);
diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs
index e968ee61f..74eb6281d 100644
--- a/pumpkin-crates/core/src/propagation/constructor.rs
+++ b/pumpkin-crates/core/src/propagation/constructor.rs
@@ -1,6 +1,3 @@
-use std::ops::Deref;
-use std::ops::DerefMut;
-
use pumpkin_checking::InferenceChecker;
use super::Domains;
@@ -11,7 +8,6 @@ use super::PropagatorVarId;
#[cfg(doc)]
use crate::Solver;
use crate::basic_types::PredicateId;
-use crate::basic_types::RefOrOwned;
use crate::engine::Assignments;
use crate::engine::State;
use crate::engine::TrailedValues;
@@ -105,15 +101,6 @@ impl InferenceCheckers<'_> {
pub struct PropagatorConstructorContext<'a> {
state: &'a mut State,
pub(crate) propagator_id: PropagatorId,
-
- /// A [`LocalId`] that is guaranteed not to be used to register any variables yet. This is
- /// either a reference or an owned value, to support
- /// [`PropagatorConstructorContext::reborrow`].
- next_local_id: RefOrOwned<'a, LocalId>,
-
- /// Marker to indicate whether the constructor registered for at least one domain event or
- /// predicate becoming assigned. If not, the [`Drop`] implementation will cause a panic.
- did_register: RefOrOwned<'a, bool>,
}
impl PropagatorConstructorContext<'_> {
@@ -122,61 +109,19 @@ impl PropagatorConstructorContext<'_> {
state: &'a mut State,
) -> PropagatorConstructorContext<'a> {
PropagatorConstructorContext {
- next_local_id: RefOrOwned::Owned(LocalId::from(0)),
propagator_id,
state,
- did_register: RefOrOwned::Owned(false),
}
}
- /// Indicate that the constructor is deliberately not registering the propagator to be enqueued
- /// at any time.
- ///
- /// If this is called and later a registration happens, then the registration will still go
- /// through. Calling this function only prevents the crash if no registration happens.
- pub fn will_not_register_any_events(&mut self) {
- *self.did_register = true;
- }
-
/// Get domain information.
pub fn domains(&mut self) -> Domains<'_> {
Domains::new(&self.state.assignments, &mut self.state.trailed_values)
}
- /// Subscribes the propagator to the given [`DomainEvents`].
- ///
- /// The domain events determine when [`Propagator::notify()`] will be called on the propagator.
- /// The [`LocalId`] is internal information related to the propagator,
- /// which is used when calling [`Propagator::notify()`] to identify the variable.
- ///
- /// Each variable *must* have a unique [`LocalId`]. Most often this would be its index of the
- /// variable in the internal array of variables.
- ///
- /// Duplicate registrations are ignored.
- pub fn register(
- &mut self,
- var: impl IntegerVariable,
- domain_events: DomainEvents,
- local_id: LocalId,
- ) {
- self.will_not_register_any_events();
-
- let propagator_var = PropagatorVarId {
- propagator: self.propagator_id,
- variable: local_id,
- };
-
- self.update_next_local_id(local_id);
-
- let mut watchers = Watchers::new(propagator_var, &mut self.state.notification_engine);
- var.watch_all(&mut watchers, domain_events.events());
- }
-
/// Register the propagator to be enqueued when the given [`Predicate`] becomes true.
/// Returns the [`PredicateId`] used by the solver to track the predicate.
pub fn register_predicate(&mut self, predicate: Predicate) -> PredicateId {
- self.will_not_register_any_events();
-
self.state.notification_engine.watch_predicate(
predicate,
self.propagator_id,
@@ -209,25 +154,16 @@ impl PropagatorConstructorContext<'_> {
variable: local_id,
};
- self.update_next_local_id(local_id);
-
let mut watchers = Watchers::new(propagator_var, &mut self.state.notification_engine);
var.watch_all_backtrack(&mut watchers, domain_events.events());
}
- /// Get a new [`LocalId`] which is guaranteed to be unused.
- pub(crate) fn get_next_local_id(&self) -> LocalId {
- *self.next_local_id.deref()
- }
-
/// Reborrow the current context to a new value with a shorter lifetime. Should be used when
/// passing `Self` to another function that takes ownership, but the value is still needed
/// afterwards.
pub fn reborrow(&mut self) -> PropagatorConstructorContext<'_> {
PropagatorConstructorContext {
propagator_id: self.propagator_id,
- next_local_id: self.next_local_id.reborrow(),
- did_register: self.did_register.reborrow(),
state: self.state,
}
}
@@ -243,35 +179,6 @@ impl PropagatorConstructorContext<'_> {
) {
self.state.add_inference_checker(inference_code, checker);
}
-
- /// Set the next local id to be at least one more than the largest encountered local id.
- fn update_next_local_id(&mut self, local_id: LocalId) {
- let next_local_id = (*self.next_local_id.deref()).max(LocalId::from(local_id.unpack() + 1));
-
- *self.next_local_id.deref_mut() = next_local_id;
- }
-}
-
-impl Drop for PropagatorConstructorContext<'_> {
- fn drop(&mut self) {
- if std::thread::panicking() {
- // If we are already unwinding due to a panic, we do not want to trigger another one.
- return;
- }
-
- let did_register = match self.did_register {
- // If we are in a reborrowed context, we do not want to enforce registration.
- RefOrOwned::Ref(_) => return,
-
- RefOrOwned::Owned(did_register) => did_register,
- };
-
- if !did_register {
- panic!(
- "Propagator did not register to be enqueued. If this is intentional, call PropagatorConstructorContext::will_not_register_any_events()."
- );
- }
- }
}
mod private {
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
index cc096ea42..cd115de2b 100644
--- a/pumpkin-crates/core/src/propagation/event_registration.rs
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -1,3 +1,6 @@
+use enumset::EnumSet;
+
+use crate::propagation::DomainEvent;
use crate::propagation::DomainEvents;
use crate::propagation::LocalId;
use crate::variables::DomainId;
@@ -8,14 +11,14 @@ pub trait EventTarget {
fn register(
&self,
registration: &mut EventRegistration,
- events: DomainEvents,
+ events: EnumSet,
local_id: LocalId,
);
}
/// Contains all the events and domains that a propagator needs to be enqueued for.
#[derive(Clone, Debug)]
-pub struct EventRegistration(Vec<(DomainId, DomainEvents, LocalId)>);
+pub struct EventRegistration(Vec<(DomainId, EnumSet, LocalId)>);
impl EventRegistration {
/// Create an [`EventRegistration`] without any variables.
@@ -52,16 +55,26 @@ impl EventRegistration {
domain_events: DomainEvents,
local_id: LocalId,
) {
- target.register(self, domain_events, local_id);
+ target.register(self, domain_events.events(), local_id);
}
/// Register the [`DomainId`] with the given [`LocalId`] on the given [`DomainEvents`].
///
/// When creating the [`EventRegistration`] in a propagator, prefer to use
/// [`EventRegistration::add`] to deal with domain views.
- pub fn with_domain(&mut self, domain_id: DomainId, events: DomainEvents, local_id: LocalId) {
+ pub fn with_domain(
+ &mut self,
+ domain_id: DomainId,
+ events: EnumSet,
+ local_id: LocalId,
+ ) {
self.0.push((domain_id, events, local_id));
}
+
+ /// Iterate the registrations already made.
+ pub fn iter(&self) -> impl ExactSizeIterator- , LocalId)> {
+ self.0.iter().copied()
+ }
}
/// Used to construct an [`EventRegistration`] for heterogeneous [`EventTarget`] implementations.
diff --git a/pumpkin-crates/core/src/propagation/local_id.rs b/pumpkin-crates/core/src/propagation/local_id.rs
index b3a3334df..baff94a32 100644
--- a/pumpkin-crates/core/src/propagation/local_id.rs
+++ b/pumpkin-crates/core/src/propagation/local_id.rs
@@ -10,6 +10,11 @@ impl LocalId {
LocalId(value)
}
+ /// Get the next [`LocalId`].
+ pub const fn successor(self) -> Self {
+ LocalId(self.0 + 1)
+ }
+
pub fn unpack(self) -> u32 {
self.0
}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
index e310d340c..c8b96aeac 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
@@ -104,10 +104,8 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
fn create(
self,
- mut context: PropagatorConstructorContext,
+ context: PropagatorConstructorContext,
) -> (EventRegistration, Self::PropagatorImpl) {
- context.will_not_register_any_events();
-
let propagator = NogoodPropagator {
handle: PropagatorHandle::new(context.propagator_id),
parameters: self.parameters,
diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs
index f30044cdd..d9817753a 100644
--- a/pumpkin-crates/core/src/propagators/reified_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs
@@ -48,11 +48,17 @@ where
reification_literal,
} = self;
- let (registration, propagator) = propagator.create(context.reborrow());
- let reification_literal_id = context.get_next_local_id();
+ let (mut registration, propagator) = propagator.create(context.reborrow());
- context.register(
- self.reification_literal,
+ let reification_literal_id = registration
+ .iter()
+ .map(|(_, _, lid)| lid)
+ .max()
+ .expect("cannot reify propagators that do not register all variables immediately")
+ .successor();
+
+ registration.add(
+ &self.reification_literal,
DomainEvents::BOUNDS,
reification_literal_id,
);
From 27ed751b9f9c3a35c8a1ecfc0b4988ba6187ec34 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Wed, 27 May 2026 22:28:36 +1000
Subject: [PATCH 03/11] Do the registration in the state
---
pumpkin-crates/core/src/engine/state.rs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs
index cb3fe554a..db0d786e0 100644
--- a/pumpkin-crates/core/src/engine/state.rs
+++ b/pumpkin-crates/core/src/engine/state.rs
@@ -36,6 +36,7 @@ use crate::propagation::Propagator;
use crate::propagation::PropagatorConstructor;
use crate::propagation::PropagatorConstructorContext;
use crate::propagation::PropagatorId;
+use crate::propagation::PropagatorVarId;
use crate::propagation::store::PropagatorStore;
use crate::pumpkin_assert_advanced;
use crate::pumpkin_assert_eq_simple;
@@ -343,6 +344,16 @@ impl State {
PropagatorConstructorContext::new(original_handle.propagator_id(), self);
let (registration, propagator) = constructor.create(constructor_context);
+ for (domain_id, events, local_id) in registration.iter() {
+ let propagator_var = PropagatorVarId {
+ propagator: original_handle.propagator_id(),
+ variable: local_id,
+ };
+
+ self.notification_engine
+ .watch_all(domain_id, events, propagator_var);
+ }
+
pumpkin_assert_simple!(
propagator.priority() as u8 <= 3,
"The propagator priority exceeds 3.
From c5290b13ebcd469b051e904782590824b1608c89 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Wed, 27 May 2026 22:46:16 +1000
Subject: [PATCH 04/11] Remove Watchers::watch_all in favor of event target
---
.../domain_event_watch_list.rs | 5 ---
.../core/src/engine/variables/affine_view.rs | 13 +------
.../core/src/engine/variables/constant.rs | 23 ++++++-----
.../core/src/engine/variables/domain_id.rs | 10 ++---
.../src/engine/variables/integer_variable.rs | 5 +--
.../core/src/engine/variables/literal.rs | 8 +---
.../contexts/propagation_context.rs | 38 +++++++++++++++----
.../src/propagation/event_registration.rs | 26 ++++++-------
8 files changed, 63 insertions(+), 65 deletions(-)
diff --git a/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs b/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs
index 9d3220c2c..6646aecad 100644
--- a/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs
+++ b/pumpkin-crates/core/src/engine/notifications/domain_event_notification/domain_event_watch_list.rs
@@ -102,11 +102,6 @@ impl<'a> Watchers<'a> {
}
}
- pub(crate) fn watch_all(&mut self, domain: DomainId, events: EnumSet) {
- self.notification_engine
- .watch_all(domain, events, self.propagator_var);
- }
-
pub(crate) fn unwatch_all(&mut self, domain: DomainId) {
self.notification_engine
.unwatch_all(domain, self.propagator_var);
diff --git a/pumpkin-crates/core/src/engine/variables/affine_view.rs b/pumpkin-crates/core/src/engine/variables/affine_view.rs
index c0e41a5ec..9142f0ba5 100644
--- a/pumpkin-crates/core/src/engine/variables/affine_view.rs
+++ b/pumpkin-crates/core/src/engine/variables/affine_view.rs
@@ -14,7 +14,7 @@ use crate::engine::predicates::predicate_constructor::PredicateConstructor;
use crate::engine::variables::DomainId;
use crate::engine::variables::IntegerVariable;
use crate::math::num_ext::NumExt;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventDispatcher;
use crate::propagation::EventTarget;
use crate::propagation::LocalId;
@@ -60,7 +60,7 @@ impl AffineView {
impl EventTarget for AffineView {
fn register(
&self,
- registration: &mut EventRegistration,
+ registration: &mut impl EventDispatcher,
mut events: EnumSet,
local_id: LocalId,
) {
@@ -284,15 +284,6 @@ where
.map(|value| self.map(value))
}
- fn watch_all(&self, watchers: &mut Watchers<'_>, mut events: EnumSet) {
- let bound = DomainEvent::LowerBound | DomainEvent::UpperBound;
- let intersection = events.intersection(bound);
- if intersection.len() == 1 && self.scale.is_negative() {
- events = events.symmetric_difference(bound);
- }
- self.inner.watch_all(watchers, events);
- }
-
fn unwatch_all(&self, watchers: &mut Watchers<'_>) {
self.inner.unwatch_all(watchers);
}
diff --git a/pumpkin-crates/core/src/engine/variables/constant.rs b/pumpkin-crates/core/src/engine/variables/constant.rs
index 486ea28ab..af328ba3b 100644
--- a/pumpkin-crates/core/src/engine/variables/constant.rs
+++ b/pumpkin-crates/core/src/engine/variables/constant.rs
@@ -1,12 +1,21 @@
+use enumset::EnumSet;
use pumpkin_checking::CheckerVariable;
use pumpkin_checking::IntExt;
use crate::engine::Assignments;
use crate::predicates::Predicate;
use crate::predicates::PredicateConstructor;
+use crate::propagation::DomainEvent;
+use crate::propagation::EventDispatcher;
+use crate::propagation::EventTarget;
+use crate::propagation::LocalId;
use crate::variables::IntegerVariable;
use crate::variables::TransformableVariable;
+impl EventTarget for i32 {
+ fn register(&self, _: &mut impl EventDispatcher, _: EnumSet, _: LocalId) {}
+}
+
impl IntegerVariable for i32 {
type AffineView = i32;
@@ -51,26 +60,16 @@ impl IntegerVariable for i32 {
std::iter::once(*self)
}
- fn watch_all(
- &self,
- _watchers: &mut crate::engine::notifications::Watchers<'_>,
- _events: enumset::EnumSet,
- ) {
- }
-
fn unwatch_all(&self, _watchers: &mut crate::engine::notifications::Watchers<'_>) {}
fn watch_all_backtrack(
&self,
_watchers: &mut crate::engine::notifications::Watchers<'_>,
- _events: enumset::EnumSet,
+ _events: EnumSet,
) {
}
- fn unpack_event(
- &self,
- _event: crate::propagation::OpaqueDomainEvent,
- ) -> crate::propagation::DomainEvent {
+ fn unpack_event(&self, _event: crate::propagation::OpaqueDomainEvent) -> DomainEvent {
unreachable!()
}
diff --git a/pumpkin-crates/core/src/engine/variables/domain_id.rs b/pumpkin-crates/core/src/engine/variables/domain_id.rs
index b384e749f..0d1c42b62 100644
--- a/pumpkin-crates/core/src/engine/variables/domain_id.rs
+++ b/pumpkin-crates/core/src/engine/variables/domain_id.rs
@@ -12,7 +12,7 @@ use crate::engine::variables::IntegerVariable;
use crate::predicates::Predicate;
use crate::predicates::PredicateConstructor;
use crate::predicates::PredicateType;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventDispatcher;
use crate::propagation::EventTarget;
use crate::propagation::LocalId;
use crate::pumpkin_assert_simple;
@@ -38,11 +38,11 @@ impl DomainId {
impl EventTarget for DomainId {
fn register(
&self,
- registration: &mut EventRegistration,
+ registration: &mut impl EventDispatcher,
events: EnumSet,
local_id: LocalId,
) {
- registration.with_domain(*self, events, local_id);
+ registration.register(*self, events, local_id);
}
}
@@ -169,10 +169,6 @@ impl IntegerVariable for DomainId {
assignment.get_domain_iterator(*self)
}
- fn watch_all(&self, watchers: &mut Watchers<'_>, events: EnumSet) {
- watchers.watch_all(*self, events);
- }
-
fn unwatch_all(&self, watchers: &mut Watchers<'_>) {
watchers.unwatch_all(*self);
}
diff --git a/pumpkin-crates/core/src/engine/variables/integer_variable.rs b/pumpkin-crates/core/src/engine/variables/integer_variable.rs
index 09badb92e..34083bbd5 100644
--- a/pumpkin-crates/core/src/engine/variables/integer_variable.rs
+++ b/pumpkin-crates/core/src/engine/variables/integer_variable.rs
@@ -10,6 +10,7 @@ use crate::engine::notifications::OpaqueDomainEvent;
use crate::engine::notifications::Watchers;
use crate::engine::predicates::predicate_constructor::PredicateConstructor;
use crate::predicates::Predicate;
+use crate::propagation::EventTarget;
/// A trait specifying the required behaviour of an integer variable such as retrieving a
/// lower-bound ([`IntegerVariable::lower_bound`]).
@@ -19,6 +20,7 @@ pub trait IntegerVariable:
+ TransformableVariable
+ Debug
+ CheckerVariable
+ + EventTarget
{
type AffineView: IntegerVariable;
@@ -50,9 +52,6 @@ pub trait IntegerVariable:
/// Iterate over the values of the domain.
fn iterate_domain(&self, assignment: &Assignments) -> impl Iterator
- ;
- /// Register a watch for this variable on the given domain events.
- fn watch_all(&self, watchers: &mut Watchers<'_>, events: EnumSet);
-
/// Remove the watcher on this variable.
fn unwatch_all(&self, watchers: &mut Watchers<'_>);
diff --git a/pumpkin-crates/core/src/engine/variables/literal.rs b/pumpkin-crates/core/src/engine/variables/literal.rs
index 8cc72777f..3b358031f 100644
--- a/pumpkin-crates/core/src/engine/variables/literal.rs
+++ b/pumpkin-crates/core/src/engine/variables/literal.rs
@@ -15,7 +15,7 @@ use crate::engine::notifications::Watchers;
use crate::engine::predicates::predicate::Predicate;
use crate::engine::predicates::predicate_constructor::PredicateConstructor;
use crate::engine::variables::AffineView;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventDispatcher;
use crate::propagation::EventTarget;
use crate::propagation::LocalId;
@@ -79,7 +79,7 @@ macro_rules! forward {
impl EventTarget for Literal {
fn register(
&self,
- registration: &mut EventRegistration,
+ registration: &mut impl EventDispatcher,
events: EnumSet,
local_id: LocalId,
) {
@@ -179,10 +179,6 @@ impl IntegerVariable for Literal {
self.integer_variable.iterate_domain(assignment)
}
- fn watch_all(&self, watchers: &mut Watchers<'_>, events: EnumSet) {
- self.integer_variable.watch_all(watchers, events)
- }
-
fn unwatch_all(&self, watchers: &mut Watchers<'_>) {
self.integer_variable.unwatch_all(watchers)
}
diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
index ab3218552..48d21cb67 100644
--- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
+++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
@@ -1,3 +1,5 @@
+use enumset::EnumSet;
+
use crate::basic_types::PredicateId;
use crate::engine::Assignments;
use crate::engine::EmptyDomain;
@@ -10,8 +12,10 @@ use crate::engine::reason::Reason;
use crate::engine::reason::ReasonStore;
use crate::engine::reason::StoredReason;
use crate::engine::variables::Literal;
+use crate::propagation::DomainEvent;
use crate::propagation::DomainEvents;
use crate::propagation::Domains;
+use crate::propagation::EventDispatcher;
use crate::propagation::HasAssignments;
use crate::propagation::LocalId;
#[cfg(doc)]
@@ -23,6 +27,7 @@ use crate::propagation::PropagatorVarId;
#[cfg(doc)]
use crate::propagation::ReadDomains;
use crate::pumpkin_assert_simple;
+use crate::variables::DomainId;
use crate::variables::IntegerVariable;
/// Provided to the propagator when it is notified of a domain event.
@@ -146,13 +151,14 @@ impl<'a> PropagationContext<'a> {
domain_events: DomainEvents,
local_id: LocalId,
) {
- let propagator_var = PropagatorVarId {
- propagator: self.propagator_id,
- variable: local_id,
- };
-
- let mut watchers = Watchers::new(propagator_var, self.notification_engine);
- var.watch_all(&mut watchers, domain_events.events());
+ var.register(
+ &mut NotificationEngineWatchers {
+ notificaton_engine: self.notification_engine,
+ propagator_id: self.propagator_id,
+ },
+ domain_events.events(),
+ local_id,
+ );
}
/// Stop being enqueued for events on the given integer variable.
@@ -290,3 +296,21 @@ pub(crate) fn build_reason(
Reason::DynamicLazy(code) => StoredReason::DynamicLazy(code),
}
}
+
+struct NotificationEngineWatchers<'a> {
+ propagator_id: PropagatorId,
+ notificaton_engine: &'a mut NotificationEngine,
+}
+
+impl EventDispatcher for NotificationEngineWatchers<'_> {
+ fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId) {
+ self.notificaton_engine.watch_all(
+ domain_id,
+ events,
+ PropagatorVarId {
+ propagator: self.propagator_id,
+ variable: local_id,
+ },
+ );
+ }
+}
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
index cd115de2b..733626f17 100644
--- a/pumpkin-crates/core/src/propagation/event_registration.rs
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -10,12 +10,17 @@ pub trait EventTarget {
/// Add a registration of self for the given domain events with a local id.
fn register(
&self,
- registration: &mut EventRegistration,
+ registration: &mut impl EventDispatcher,
events: EnumSet,
local_id: LocalId,
);
}
+pub trait EventDispatcher {
+ /// Register the [`DomainId`] with the given [`LocalId`] on the given [`DomainEvents`].
+ fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId);
+}
+
/// Contains all the events and domains that a propagator needs to be enqueued for.
#[derive(Clone, Debug)]
pub struct EventRegistration(Vec<(DomainId, EnumSet, LocalId)>);
@@ -58,25 +63,18 @@ impl EventRegistration {
target.register(self, domain_events.events(), local_id);
}
- /// Register the [`DomainId`] with the given [`LocalId`] on the given [`DomainEvents`].
- ///
- /// When creating the [`EventRegistration`] in a propagator, prefer to use
- /// [`EventRegistration::add`] to deal with domain views.
- pub fn with_domain(
- &mut self,
- domain_id: DomainId,
- events: EnumSet,
- local_id: LocalId,
- ) {
- self.0.push((domain_id, events, local_id));
- }
-
/// Iterate the registrations already made.
pub fn iter(&self) -> impl ExactSizeIterator
- , LocalId)> {
self.0.iter().copied()
}
}
+impl EventDispatcher for EventRegistration {
+ fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId) {
+ self.0.push((domain_id, events, local_id));
+ }
+}
+
/// Used to construct an [`EventRegistration`] for heterogeneous [`EventTarget`] implementations.
///
/// See [`EventRegistration::builder`] for a usage example.
From b4568923f4ccd9bbf911575cfae9039276a5cfb7 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Wed, 27 May 2026 23:18:14 +1000
Subject: [PATCH 05/11] Implement the new interface in the rest of pumpkin
---
.../core/src/propagation/constructor.rs | 52 -------------------
.../src/propagation/event_registration.rs | 11 +++-
.../hypercube_linear/propagator.rs | 3 +-
.../src/propagators/reified_propagator.rs | 29 +++++++----
.../propagators/arithmetic/absolute_value.rs | 15 ++++--
.../arithmetic/binary/binary_equals.rs | 15 ++++--
.../arithmetic/binary/binary_not_equals.rs | 15 ++++--
.../arithmetic/integer_division.rs | 20 ++++---
.../arithmetic/integer_multiplication.rs | 17 +++---
.../arithmetic/linear_less_or_equal.rs | 20 ++++---
.../arithmetic/linear_not_equal.rs | 11 ++--
.../src/propagators/arithmetic/maximum.rs | 16 +++---
.../time_table_over_interval_incremental.rs | 10 ++--
.../time_table_per_point_incremental.rs | 10 ++--
.../time_table/time_table_over_interval.rs | 10 ++--
.../time_table/time_table_per_point.rs | 10 ++--
.../src/propagators/cumulative/utils/util.rs | 16 ++++--
.../disjunctive/disjunctive_propagator.rs | 16 +++---
.../propagators/src/propagators/element.rs | 18 ++++---
.../src/deduction_propagator.rs | 2 +-
20 files changed, 176 insertions(+), 140 deletions(-)
diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs
index 74eb6281d..da90931c7 100644
--- a/pumpkin-crates/core/src/propagation/constructor.rs
+++ b/pumpkin-crates/core/src/propagation/constructor.rs
@@ -199,55 +199,3 @@ mod private {
}
}
}
-
-#[cfg(test)]
-mod tests {
-
- use super::*;
- use crate::variables::DomainId;
-
- #[test]
- #[should_panic]
- fn panic_when_no_registration_happened() {
- let mut state = State::default();
- state.notification_engine.grow();
-
- let _c1 = PropagatorConstructorContext::new(PropagatorId(0), &mut state);
- }
-
- #[test]
- fn do_not_panic_if_told_no_registration_will_happen() {
- let mut state = State::default();
- state.notification_engine.grow();
-
- let mut ctx = PropagatorConstructorContext::new(PropagatorId(0), &mut state);
- ctx.will_not_register_any_events();
- }
-
- #[test]
- fn do_not_panic_if_no_registration_happens_in_reborrowed() {
- let mut state = State::default();
- state.notification_engine.grow();
-
- let mut ctx = PropagatorConstructorContext::new(PropagatorId(0), &mut state);
- let ctx2 = ctx.reborrow();
- drop(ctx2);
-
- ctx.will_not_register_any_events();
- }
-
- #[test]
- fn reborrowing_remembers_next_local_id() {
- let mut state = State::default();
- state.notification_engine.grow();
-
- let mut c1 = PropagatorConstructorContext::new(PropagatorId(0), &mut state);
- c1.will_not_register_any_events();
-
- let mut c2 = c1.reborrow();
- c2.register(DomainId::new(0), DomainEvents::ANY_INT, LocalId::from(1));
- drop(c2);
-
- assert_eq!(LocalId::from(2), c1.get_next_local_id());
- }
-}
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
index 733626f17..665b92ed5 100644
--- a/pumpkin-crates/core/src/propagation/event_registration.rs
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -42,9 +42,16 @@ impl EventRegistration {
/// # Example
///
/// ```
+ /// use pumpkin_core::propagation::DomainEvents;
+ /// use pumpkin_core::propagation::EventRegistration;
+ /// use pumpkin_core::propagation::LocalId;
+ /// use pumpkin_core::variables::DomainId;
+ ///
+ /// let v1 = DomainId::new(0);
+ /// let v2 = DomainId::new(0);
/// let registration = EventRegistration::builder()
- /// .add(LocalId::from(0), &v1)
- /// .add(LocalId::from(1), &v2)
+ /// .add(&v1, DomainEvents::ANY_INT, LocalId::from(0))
+ /// .add(&v2, DomainEvents::ANY_INT, LocalId::from(1))
/// .build();
/// ```
pub fn builder() -> EventRegistrationBuilder {
diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
index 0f6068935..3b6d87d3a 100644
--- a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
+++ b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
@@ -77,7 +77,8 @@ impl PropagatorConstructor for HypercubeLinearConstructor {
inference_code: InferenceCode::new(constraint_tag, HypercubeLinear),
};
- let registration = EventRegistration::builder().build();
+ // TODO: This will be expanded with registration of predicates.
+ let registration = EventRegistration::empty();
(registration, propagator)
}
diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs
index d9817753a..51e925e7c 100644
--- a/pumpkin-crates/core/src/propagators/reified_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs
@@ -73,7 +73,7 @@ where
reason_buffer: vec![],
};
- (EventRegistration::builder().build(), propagator)
+ (registration, propagator)
}
fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
@@ -308,6 +308,7 @@ mod tests {
let _ = solver
.new_propagator(ReifiedPropagatorArgs {
propagator: GenericPropagator::new(
+ vec![a, b],
move |_: PropagationContext| {
Err(PropagatorConflict {
conjunction: t1.clone(),
@@ -342,6 +343,7 @@ mod tests {
let propagator = solver
.new_propagator(ReifiedPropagatorArgs {
propagator: GenericPropagator::new(
+ vec![var],
move |mut ctx: PropagationContext| {
ctx.post(
predicate![var >= 3],
@@ -385,6 +387,7 @@ mod tests {
let inconsistency = solver
.new_propagator(ReifiedPropagatorArgs {
propagator: GenericPropagator::new(
+ vec![var],
move |_: PropagationContext| {
Err(PropagatorConflict {
conjunction: conjunction!([var >= 1]),
@@ -426,6 +429,7 @@ mod tests {
let propagator = solver
.new_propagator(ReifiedPropagatorArgs {
propagator: GenericPropagator::new(
+ vec![var],
|_: PropagationContext| Ok(()),
move |context: Domains| {
if context.is_fixed(&var) {
@@ -462,16 +466,17 @@ mod tests {
{
type PropagatorImpl = Self;
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ _: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
+ let mut registration = EventRegistration::empty();
+
for (index, variable) in self.variables_to_register.iter().enumerate() {
- context.register(
- *variable,
- DomainEvents::ANY_INT,
- LocalId::from(index as u32),
- );
+ registration.add(variable, DomainEvents::ANY_INT, LocalId::from(index as u32));
}
- self
+ (registration, self)
}
}
@@ -498,11 +503,15 @@ mod tests {
Propagation: Fn(PropagationContext) -> PropagationStatusCP,
ConsistencyCheck: Fn(Domains) -> Option,
{
- pub(crate) fn new(propagation: Propagation, consistency_check: ConsistencyCheck) -> Self {
+ pub(crate) fn new(
+ variables_to_register: Vec,
+ propagation: Propagation,
+ consistency_check: ConsistencyCheck,
+ ) -> Self {
GenericPropagator {
propagation,
consistency_check,
- variables_to_register: vec![],
+ variables_to_register,
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs
index 2881dfa39..bd824a32b 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs
@@ -8,6 +8,7 @@ use pumpkin_core::predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -45,23 +46,27 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
} = self;
- context.register(signed.clone(), DomainEvents::BOUNDS, LocalId::from(0));
- context.register(absolute.clone(), DomainEvents::BOUNDS, LocalId::from(1));
+ let registration = EventRegistration::builder()
+ .add(&signed, DomainEvents::BOUNDS, LocalId::from(0))
+ .add(&absolute, DomainEvents::BOUNDS, LocalId::from(1))
+ .build();
let inference_code = InferenceCode::new(constraint_tag, AbsoluteValue);
- AbsoluteValuePropagator {
+ let propagator = AbsoluteValuePropagator {
signed,
absolute,
inference_code,
- }
+ };
+
+ (registration, propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs
index b95b86b46..adc34e36e 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs
@@ -20,6 +20,7 @@ use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::ExplanationContext;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LazyExplanation;
@@ -64,17 +65,19 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let BinaryEqualsPropagatorArgs {
a,
b,
constraint_tag,
} = self;
- context.register(a.clone(), DomainEvents::ANY_INT, LocalId::from(0));
- context.register(b.clone(), DomainEvents::ANY_INT, LocalId::from(1));
+ let registration = EventRegistration::builder()
+ .add(&a, DomainEvents::ANY_INT, LocalId::from(0))
+ .add(&b, DomainEvents::ANY_INT, LocalId::from(1))
+ .build();
- BinaryEqualsPropagator {
+ let propagator = BinaryEqualsPropagator {
a,
b,
@@ -86,7 +89,9 @@ where
has_backtracked: false,
first_propagation_loop: true,
reason: Predicate::trivially_false(),
- }
+ };
+
+ (registration, propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs
index b33a98543..60f7e49e3 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs
@@ -8,6 +8,7 @@ use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -47,7 +48,7 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let BinaryNotEqualsPropagatorArgs {
a,
b,
@@ -55,15 +56,19 @@ where
} = self;
// We only care about the case where one of the two is assigned
- context.register(a.clone(), DomainEvents::ASSIGN, LocalId::from(0));
- context.register(b.clone(), DomainEvents::ASSIGN, LocalId::from(1));
+ let registration = EventRegistration::builder()
+ .add(&a, DomainEvents::ASSIGN, LocalId::from(0))
+ .add(&b, DomainEvents::ASSIGN, LocalId::from(1))
+ .build();
- BinaryNotEqualsPropagator {
+ let propagator = BinaryNotEqualsPropagator {
a,
b,
inference_code: InferenceCode::new(constraint_tag, BinaryNotEquals),
- }
+ };
+
+ (registration, propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs
index 882495a48..b9dccaf4e 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs
@@ -9,6 +9,7 @@ use pumpkin_core::predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -43,7 +44,10 @@ where
{
type PropagatorImpl = DivisionPropagator;
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
let DivisionArgs {
numerator,
denominator,
@@ -56,18 +60,22 @@ where
"Denominator cannot contain 0"
);
- context.register(numerator.clone(), DomainEvents::BOUNDS, ID_NUMERATOR);
- context.register(denominator.clone(), DomainEvents::BOUNDS, ID_DENOMINATOR);
- context.register(rhs.clone(), DomainEvents::BOUNDS, ID_RHS);
+ let registration = EventRegistration::builder()
+ .add(&numerator, DomainEvents::BOUNDS, ID_NUMERATOR)
+ .add(&denominator, DomainEvents::BOUNDS, ID_DENOMINATOR)
+ .add(&rhs, DomainEvents::BOUNDS, ID_RHS)
+ .build();
let inference_code = InferenceCode::new(constraint_tag, Division);
- DivisionPropagator {
+ let propagator = DivisionPropagator {
numerator,
denominator,
rhs,
inference_code,
- }
+ };
+
+ (registration, propagator)
}
fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs
index 9e7d8d953..65a3519b4 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs
@@ -8,6 +8,7 @@ use pumpkin_core::predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -50,7 +51,7 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let IntegerMultiplicationArgs {
a,
b,
@@ -58,16 +59,20 @@ where
constraint_tag,
} = self;
- context.register(a.clone(), DomainEvents::ANY_INT, ID_A);
- context.register(b.clone(), DomainEvents::ANY_INT, ID_B);
- context.register(c.clone(), DomainEvents::ANY_INT, ID_C);
+ let registration = EventRegistration::builder()
+ .add(&a, DomainEvents::ANY_INT, ID_A)
+ .add(&b, DomainEvents::ANY_INT, ID_B)
+ .add(&c, DomainEvents::ANY_INT, ID_C)
+ .build();
- IntegerMultiplicationPropagator {
+ let propagator = IntegerMultiplicationPropagator {
a,
b,
c,
inference_code: InferenceCode::new(constraint_tag, IntegerMultiplication),
- }
+ };
+
+ (registration, propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs
index 075af43d9..ab244ff96 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs
@@ -13,6 +13,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::ExplanationContext;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LazyExplanation;
@@ -56,7 +57,10 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
let LinearLessOrEqualPropagatorArgs {
x,
c,
@@ -66,26 +70,26 @@ where
let mut lower_bound_left_hand_side = 0_i64;
let mut current_bounds = vec![];
+ let mut registration = EventRegistration::builder();
for (i, x_i) in x.iter().enumerate() {
- context.register(
- x_i.clone(),
- DomainEvents::LOWER_BOUND,
- LocalId::from(i as u32),
- );
+ registration =
+ registration.add(x_i, DomainEvents::LOWER_BOUND, LocalId::from(i as u32));
lower_bound_left_hand_side += context.lower_bound(x_i) as i64;
current_bounds.push(context.new_trailed_integer(context.lower_bound(x_i) as i64));
}
let lower_bound_left_hand_side = context.new_trailed_integer(lower_bound_left_hand_side);
- LinearLessOrEqualPropagator {
+ let propagator = LinearLessOrEqualPropagator {
x,
c,
lower_bound_left_hand_side,
current_bounds: current_bounds.into(),
inference_code: InferenceCode::new(constraint_tag, LinearBounds),
reason_buffer: Vec::default(),
- }
+ };
+
+ (registration.build(), propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs
index 130d2b2d8..23fc766a9 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs
@@ -18,6 +18,7 @@ use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -60,15 +61,19 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
let LinearNotEqualPropagatorArgs {
terms,
rhs,
constraint_tag,
} = self;
+ let mut registration = EventRegistration::builder();
for (i, x_i) in terms.iter().enumerate() {
- context.register(x_i.clone(), DomainEvents::ASSIGN, LocalId::from(i as u32));
+ registration = registration.add(x_i, DomainEvents::ASSIGN, LocalId::from(i as u32));
context.register_backtrack(
x_i.clone(),
DomainEvents::new(enum_set!(DomainEvent::Assign | DomainEvent::Removal)),
@@ -88,7 +93,7 @@ where
propagator.recalculate_fixed_variables(context.domains());
- propagator
+ (registration.build(), propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs
index 2a847bc4c..85761c40c 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs
@@ -9,6 +9,7 @@ use pumpkin_core::predicates::PropositionalConjunction;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -46,30 +47,33 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let MaximumArgs {
array,
rhs,
constraint_tag,
} = self;
+ let mut registration = EventRegistration::builder();
for (idx, var) in array.iter().enumerate() {
- context.register(var.clone(), DomainEvents::BOUNDS, LocalId::from(idx as u32));
+ registration = registration.add(var, DomainEvents::BOUNDS, LocalId::from(idx as u32));
}
- context.register(
- rhs.clone(),
+ registration = registration.add(
+ &rhs,
DomainEvents::BOUNDS,
LocalId::from(array.len() as u32),
);
let inference_code = InferenceCode::new(constraint_tag, Maximum);
- MaximumPropagator {
+ let propagator = MaximumPropagator {
array,
rhs,
inference_code,
- }
+ };
+
+ (registration.build(), propagator)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs
index fc5d108e4..312bd2c83 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs
@@ -11,6 +11,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -129,10 +130,13 @@ impl PropagatorConstruc
);
}
- fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ mut self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
// We only register for notifications of backtrack events if incremental backtracking is
// enabled
- register_tasks(
+ let registration = register_tasks(
&self.parameters.tasks,
context.reborrow(),
self.parameters.options.incremental_backtracking,
@@ -146,7 +150,7 @@ impl PropagatorConstruc
self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable));
- self
+ (registration, self)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs
index 9b4ffa1e5..2428cf98f 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs
@@ -11,6 +11,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -126,8 +127,11 @@ impl Propagator
);
}
- fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
- register_tasks(&self.parameters.tasks, context.reborrow(), true);
+ fn create(
+ mut self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
+ let registration = register_tasks(&self.parameters.tasks, context.reborrow(), true);
self.updatable_structures
.reset_all_bounds_and_remove_fixed(context.domains(), &self.parameters);
@@ -136,7 +140,7 @@ impl Propagator
self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable));
- self
+ (registration, self)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs
index cb8b6926b..cf0792903 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs
@@ -9,6 +9,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -129,14 +130,17 @@ impl PropagatorConstructor
);
}
- fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ mut self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
self.updatable_structures
.initialise_bounds_and_remove_fixed(context.domains(), &self.parameters);
- register_tasks(&self.parameters.tasks, context.reborrow(), false);
+ let registration = register_tasks(&self.parameters.tasks, context.reborrow(), false);
self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable));
- self
+ (registration, self)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs
index 5451cd01d..471f2e26e 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs
@@ -11,6 +11,7 @@ use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::EnqueueDecision;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -119,14 +120,17 @@ impl PropagatorConstructor for TimeTablePerPoint
);
}
- fn create(mut self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(
+ mut self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
self.updatable_structures
.initialise_bounds_and_remove_fixed(context.domains(), &self.parameters);
- register_tasks(&self.parameters.tasks, context.reborrow(), false);
+ let registration = register_tasks(&self.parameters.tasks, context.reborrow(), false);
self.inference_code = Some(InferenceCode::new(self.constraint_tag, TimeTable));
- self
+ (registration, self)
}
}
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs b/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs
index 3b1d3f790..063042abf 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs
@@ -7,6 +7,7 @@ use enumset::enum_set;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::PropagatorConstructorContext;
use pumpkin_core::propagation::ReadDomains;
@@ -52,15 +53,18 @@ pub(crate) fn register_tasks(
tasks: &[Rc>],
mut context: PropagatorConstructorContext<'_>,
register_backtrack: bool,
-) {
- tasks.iter().for_each(|task| {
- context.register(
- task.start_variable.clone(),
+) -> EventRegistration {
+ let mut registration = EventRegistration::builder();
+
+ for task in tasks.iter() {
+ registration = registration.add(
+ &task.start_variable,
DomainEvents::new(enum_set!(
DomainEvent::LowerBound | DomainEvent::UpperBound | DomainEvent::Assign
)),
task.id,
);
+
if register_backtrack {
context.register_backtrack(
task.start_variable.clone(),
@@ -70,7 +74,9 @@ pub(crate) fn register_tasks(
task.id,
);
}
- });
+ }
+
+ registration.build()
}
/// Updates the bounds of the provided [`Task`] to those stored in
diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs
index 0d2da6002..4341ab54b 100644
--- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs
+++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs
@@ -8,6 +8,7 @@ use pumpkin_core::predicates::PropositionalConjunction;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::PropagationContext;
@@ -79,7 +80,7 @@ impl DisjunctiveConstructor {
impl PropagatorConstructor for DisjunctiveConstructor {
type PropagatorImpl = DisjunctivePropagator;
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let tasks = self
.tasks
.into_iter()
@@ -94,17 +95,20 @@ impl PropagatorConstructor for DisjunctiveConstr
let inference_code = InferenceCode::new(self.constraint_tag, DisjunctiveEdgeFinding);
- tasks.iter().for_each(|task| {
- context.register(task.start_time.clone(), DomainEvents::BOUNDS, task.id);
- });
+ let mut registration = EventRegistration::builder();
+ for task in tasks.iter() {
+ registration = registration.add(&task.start_time, DomainEvents::BOUNDS, task.id);
+ }
- DisjunctivePropagator {
+ let propagator = DisjunctivePropagator {
tasks: tasks.clone().into_boxed_slice(),
sorted_tasks: tasks,
theta_lambda_tree,
inference_code,
- }
+ };
+
+ (registration.build(), propagator)
}
fn add_inference_checkers(&self, mut checkers: InferenceCheckers<'_>) {
diff --git a/pumpkin-crates/propagators/src/propagators/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs
index 8874a259c..09f1b9271 100644
--- a/pumpkin-crates/propagators/src/propagators/element.rs
+++ b/pumpkin-crates/propagators/src/propagators/element.rs
@@ -17,6 +17,7 @@ use pumpkin_core::predicates::Predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::ExplanationContext;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LazyExplanation;
@@ -60,7 +61,7 @@ where
);
}
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
let ElementArgs {
array,
index,
@@ -68,26 +69,29 @@ where
constraint_tag,
} = self;
+ let mut registration = EventRegistration::builder();
for (i, x_i) in array.iter().enumerate() {
- context.register(
- x_i.clone(),
+ registration = registration.add(
+ x_i,
DomainEvents::ANY_INT,
LocalId::from(i as u32 + ID_X_OFFSET),
);
}
- context.register(index.clone(), DomainEvents::ANY_INT, ID_INDEX);
- context.register(rhs.clone(), DomainEvents::ANY_INT, ID_RHS);
+ registration = registration.add(&index, DomainEvents::ANY_INT, ID_INDEX);
+ registration = registration.add(&rhs, DomainEvents::ANY_INT, ID_RHS);
let inference_code = InferenceCode::new(constraint_tag, Element);
- ElementPropagator {
+ let propagator = ElementPropagator {
array,
index,
rhs,
inference_code,
rhs_reason_buffer: vec![],
- }
+ };
+
+ (registration.build(), propagator)
}
}
diff --git a/pumpkin-proof-processor/src/deduction_propagator.rs b/pumpkin-proof-processor/src/deduction_propagator.rs
index bba962e49..503c7c59b 100644
--- a/pumpkin-proof-processor/src/deduction_propagator.rs
+++ b/pumpkin-proof-processor/src/deduction_propagator.rs
@@ -24,7 +24,7 @@ pub(crate) struct DeductionPropagatorConstructor {
impl PropagatorConstructor for DeductionPropagatorConstructor {
type PropagatorImpl = DeductionPropagator;
- fn create(self, mut context: PropagatorConstructorContext) -> Self::PropagatorImpl {
+ fn create(self, mut context: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
declare_inference_label!(Nogood);
let DeductionPropagatorConstructor {
From b95ad412dfce9418666814864ef8b83df6952606 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Wed, 27 May 2026 23:29:26 +1000
Subject: [PATCH 06/11] Also update the proof processor
---
pumpkin-proof-processor/src/deduction_propagator.rs | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/pumpkin-proof-processor/src/deduction_propagator.rs b/pumpkin-proof-processor/src/deduction_propagator.rs
index 503c7c59b..80c5bc33c 100644
--- a/pumpkin-proof-processor/src/deduction_propagator.rs
+++ b/pumpkin-proof-processor/src/deduction_propagator.rs
@@ -2,6 +2,7 @@ use pumpkin_core::declare_inference_label;
use pumpkin_core::predicates::PropositionalConjunction;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
+use pumpkin_core::propagation::EventRegistration;
use pumpkin_core::propagation::PredicateId;
use pumpkin_core::propagation::PropagationContext;
use pumpkin_core::propagation::Propagator;
@@ -24,7 +25,10 @@ pub(crate) struct DeductionPropagatorConstructor {
impl PropagatorConstructor for DeductionPropagatorConstructor {
type PropagatorImpl = DeductionPropagator;
- fn create(self, mut context: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(
+ self,
+ mut context: PropagatorConstructorContext,
+ ) -> (EventRegistration, Self::PropagatorImpl) {
declare_inference_label!(Nogood);
let DeductionPropagatorConstructor {
@@ -37,12 +41,14 @@ impl PropagatorConstructor for DeductionPropagatorConstructor {
.map(|&predicate| context.register_predicate(predicate))
.collect();
- DeductionPropagator {
+ let propagator = DeductionPropagator {
nogood,
ids,
inference_code: InferenceCode::new(constraint_tag, Nogood),
active: true,
- }
+ };
+
+ (EventRegistration::empty(), propagator)
}
}
From 6839f510eec9b6255653d9c424cd2ab22dc89b5e Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Thu, 28 May 2026 19:36:36 +1000
Subject: [PATCH 07/11] Rename 'EventRegistration' to 'EventsToRegister'
---
.../core/src/propagation/constructor.rs | 4 ++--
.../core/src/propagation/event_registration.rs | 16 ++++++++--------
.../propagators/hypercube_linear/propagator.rs | 6 +++---
.../src/propagators/nogoods/nogood_propagator.rs | 6 +++---
.../core/src/propagators/reified_propagator.rs | 8 ++++----
.../src/propagators/arithmetic/absolute_value.rs | 6 +++---
.../arithmetic/binary/binary_equals.rs | 6 +++---
.../arithmetic/binary/binary_not_equals.rs | 6 +++---
.../propagators/arithmetic/integer_division.rs | 6 +++---
.../arithmetic/integer_multiplication.rs | 6 +++---
.../arithmetic/linear_less_or_equal.rs | 6 +++---
.../propagators/arithmetic/linear_not_equal.rs | 6 +++---
.../src/propagators/arithmetic/maximum.rs | 6 +++---
.../time_table_over_interval_incremental.rs | 4 ++--
.../time_table_per_point_incremental.rs | 4 ++--
.../time_table/time_table_over_interval.rs | 4 ++--
.../time_table/time_table_per_point.rs | 4 ++--
.../src/propagators/cumulative/utils/util.rs | 6 +++---
.../disjunctive/disjunctive_propagator.rs | 6 +++---
.../propagators/src/propagators/element.rs | 6 +++---
.../src/deduction_propagator.rs | 6 +++---
21 files changed, 64 insertions(+), 64 deletions(-)
diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs
index da90931c7..47d49673b 100644
--- a/pumpkin-crates/core/src/propagation/constructor.rs
+++ b/pumpkin-crates/core/src/propagation/constructor.rs
@@ -21,7 +21,7 @@ use crate::proof::InferenceCode;
#[cfg(doc)]
use crate::propagation::DomainEvent;
use crate::propagation::DomainEvents;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventsToRegister;
use crate::propagators::reified_propagator::ReifiedChecker;
use crate::variables::IntegerVariable;
use crate::variables::Literal;
@@ -48,7 +48,7 @@ pub trait PropagatorConstructor {
fn create(
self,
context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl);
+ ) -> (EventsToRegister, Self::PropagatorImpl);
}
/// Interface used to add [`InferenceChecker`]s to the [`State`].
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
index 665b92ed5..8bc796fd2 100644
--- a/pumpkin-crates/core/src/propagation/event_registration.rs
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -23,15 +23,15 @@ pub trait EventDispatcher {
/// Contains all the events and domains that a propagator needs to be enqueued for.
#[derive(Clone, Debug)]
-pub struct EventRegistration(Vec<(DomainId, EnumSet, LocalId)>);
+pub struct EventsToRegister(Vec<(DomainId, EnumSet, LocalId)>);
-impl EventRegistration {
+impl EventsToRegister {
/// Create an [`EventRegistration`] without any variables.
///
/// This is the uncommon case. Without registering for variable events, a propagator will never
/// be enqueued.
- pub fn empty() -> EventRegistration {
- EventRegistration(vec![])
+ pub fn empty() -> EventsToRegister {
+ EventsToRegister(vec![])
}
/// Create a new [`EventRegistrationBuilder`].
@@ -56,7 +56,7 @@ impl EventRegistration {
/// ```
pub fn builder() -> EventRegistrationBuilder {
EventRegistrationBuilder {
- registrations: EventRegistration(vec![]),
+ registrations: EventsToRegister(vec![]),
}
}
@@ -76,7 +76,7 @@ impl EventRegistration {
}
}
-impl EventDispatcher for EventRegistration {
+impl EventDispatcher for EventsToRegister {
fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId) {
self.0.push((domain_id, events, local_id));
}
@@ -87,7 +87,7 @@ impl EventDispatcher for EventRegistration {
/// See [`EventRegistration::builder`] for a usage example.
#[derive(Clone, Debug)]
pub struct EventRegistrationBuilder {
- registrations: EventRegistration,
+ registrations: EventsToRegister,
}
impl EventRegistrationBuilder {
@@ -106,7 +106,7 @@ impl EventRegistrationBuilder {
///
/// If no variables are registered, then this panics. If no variables can be registered during
/// construction, use [`EventRegistration::empty`].
- pub fn build(self) -> EventRegistration {
+ pub fn build(self) -> EventsToRegister {
assert!(
!self.registrations.0.is_empty(),
"did not register for any events"
diff --git a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
index 3b6d87d3a..11b6fb604 100644
--- a/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
+++ b/pumpkin-crates/core/src/propagators/hypercube_linear/propagator.rs
@@ -7,7 +7,7 @@ use crate::predicates::PropositionalConjunction;
use crate::proof::ConstraintTag;
use crate::proof::InferenceCode;
use crate::propagation::DomainEvents;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventsToRegister;
use crate::propagation::InferenceCheckers;
use crate::propagation::LocalId;
use crate::propagation::PropagationContext;
@@ -48,7 +48,7 @@ impl PropagatorConstructor for HypercubeLinearConstructor {
fn create(
self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let HypercubeLinearConstructor {
hypercube,
linear,
@@ -78,7 +78,7 @@ impl PropagatorConstructor for HypercubeLinearConstructor {
};
// TODO: This will be expanded with registration of predicates.
- let registration = EventRegistration::empty();
+ let registration = EventsToRegister::empty();
(registration, propagator)
}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
index c8b96aeac..b00110535 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
@@ -21,7 +21,7 @@ use crate::engine::reason::ReasonStore;
use crate::predicate;
use crate::proof::InferenceCode;
use crate::propagation::EnqueueDecision;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventsToRegister;
use crate::propagation::ExplanationContext;
use crate::propagation::LazyExplanation;
use crate::propagation::NotificationContext;
@@ -105,7 +105,7 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
fn create(
self,
context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let propagator = NogoodPropagator {
handle: PropagatorHandle::new(context.propagator_id),
parameters: self.parameters,
@@ -121,7 +121,7 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
temp_nogood_reason: Default::default(),
};
- (EventRegistration::empty(), propagator)
+ (EventsToRegister::empty(), propagator)
}
}
diff --git a/pumpkin-crates/core/src/propagators/reified_propagator.rs b/pumpkin-crates/core/src/propagators/reified_propagator.rs
index 51e925e7c..d49059289 100644
--- a/pumpkin-crates/core/src/propagators/reified_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/reified_propagator.rs
@@ -9,7 +9,7 @@ use crate::predicates::Predicate;
use crate::propagation::DomainEvents;
use crate::propagation::Domains;
use crate::propagation::EnqueueDecision;
-use crate::propagation::EventRegistration;
+use crate::propagation::EventsToRegister;
use crate::propagation::ExplanationContext;
use crate::propagation::InferenceCheckers;
use crate::propagation::LazyExplanation;
@@ -42,7 +42,7 @@ where
fn create(
self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let ReifiedPropagatorArgs {
propagator,
reification_literal,
@@ -469,8 +469,8 @@ mod tests {
fn create(
self,
_: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
- let mut registration = EventRegistration::empty();
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
+ let mut registration = EventsToRegister::empty();
for (index, variable) in self.variables_to_register.iter().enumerate() {
registration.add(variable, DomainEvents::ANY_INT, LocalId::from(index as u32));
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs
index bd824a32b..9934e6821 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/absolute_value.rs
@@ -8,7 +8,7 @@ use pumpkin_core::predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -46,14 +46,14 @@ where
);
}
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
} = self;
- let registration = EventRegistration::builder()
+ let registration = EventsToRegister::builder()
.add(&signed, DomainEvents::BOUNDS, LocalId::from(0))
.add(&absolute, DomainEvents::BOUNDS, LocalId::from(1))
.build();
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs
index adc34e36e..b43ccb585 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_equals.rs
@@ -20,7 +20,7 @@ use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::ExplanationContext;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LazyExplanation;
@@ -65,14 +65,14 @@ where
);
}
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let BinaryEqualsPropagatorArgs {
a,
b,
constraint_tag,
} = self;
- let registration = EventRegistration::builder()
+ let registration = EventsToRegister::builder()
.add(&a, DomainEvents::ANY_INT, LocalId::from(0))
.add(&b, DomainEvents::ANY_INT, LocalId::from(1))
.build();
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs
index 60f7e49e3..4ca2c4032 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/binary/binary_not_equals.rs
@@ -8,7 +8,7 @@ use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -48,7 +48,7 @@ where
);
}
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let BinaryNotEqualsPropagatorArgs {
a,
b,
@@ -56,7 +56,7 @@ where
} = self;
// We only care about the case where one of the two is assigned
- let registration = EventRegistration::builder()
+ let registration = EventsToRegister::builder()
.add(&a, DomainEvents::ASSIGN, LocalId::from(0))
.add(&b, DomainEvents::ASSIGN, LocalId::from(1))
.build();
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs
index b9dccaf4e..300b26e96 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_division.rs
@@ -9,7 +9,7 @@ use pumpkin_core::predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -47,7 +47,7 @@ where
fn create(
self,
context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let DivisionArgs {
numerator,
denominator,
@@ -60,7 +60,7 @@ where
"Denominator cannot contain 0"
);
- let registration = EventRegistration::builder()
+ let registration = EventsToRegister::builder()
.add(&numerator, DomainEvents::BOUNDS, ID_NUMERATOR)
.add(&denominator, DomainEvents::BOUNDS, ID_DENOMINATOR)
.add(&rhs, DomainEvents::BOUNDS, ID_RHS)
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs
index 65a3519b4..6811e17bc 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/integer_multiplication.rs
@@ -8,7 +8,7 @@ use pumpkin_core::predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -51,7 +51,7 @@ where
);
}
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let IntegerMultiplicationArgs {
a,
b,
@@ -59,7 +59,7 @@ where
constraint_tag,
} = self;
- let registration = EventRegistration::builder()
+ let registration = EventsToRegister::builder()
.add(&a, DomainEvents::ANY_INT, ID_A)
.add(&b, DomainEvents::ANY_INT, ID_B)
.add(&c, DomainEvents::ANY_INT, ID_C)
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs
index ab244ff96..a3a03be1c 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_less_or_equal.rs
@@ -13,7 +13,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::ExplanationContext;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LazyExplanation;
@@ -60,7 +60,7 @@ where
fn create(
self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let LinearLessOrEqualPropagatorArgs {
x,
c,
@@ -70,7 +70,7 @@ where
let mut lower_bound_left_hand_side = 0_i64;
let mut current_bounds = vec![];
- let mut registration = EventRegistration::builder();
+ let mut registration = EventsToRegister::builder();
for (i, x_i) in x.iter().enumerate() {
registration =
registration.add(x_i, DomainEvents::LOWER_BOUND, LocalId::from(i as u32));
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs
index 23fc766a9..d92290960 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/linear_not_equal.rs
@@ -18,7 +18,7 @@ use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -64,14 +64,14 @@ where
fn create(
self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let LinearNotEqualPropagatorArgs {
terms,
rhs,
constraint_tag,
} = self;
- let mut registration = EventRegistration::builder();
+ let mut registration = EventsToRegister::builder();
for (i, x_i) in terms.iter().enumerate() {
registration = registration.add(x_i, DomainEvents::ASSIGN, LocalId::from(i as u32));
context.register_backtrack(
diff --git a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs
index 85761c40c..6f658ace3 100644
--- a/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs
+++ b/pumpkin-crates/propagators/src/propagators/arithmetic/maximum.rs
@@ -9,7 +9,7 @@ use pumpkin_core::predicates::PropositionalConjunction;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::Priority;
@@ -47,14 +47,14 @@ where
);
}
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let MaximumArgs {
array,
rhs,
constraint_tag,
} = self;
- let mut registration = EventRegistration::builder();
+ let mut registration = EventsToRegister::builder();
for (idx, var) in array.iter().enumerate() {
registration = registration.add(var, DomainEvents::BOUNDS, LocalId::from(idx as u32));
}
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs
index 312bd2c83..ea2af2fda 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/over_interval_incremental_propagator/time_table_over_interval_incremental.rs
@@ -11,7 +11,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -133,7 +133,7 @@ impl PropagatorConstruc
fn create(
mut self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
// We only register for notifications of backtrack events if incremental backtracking is
// enabled
let registration = register_tasks(
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs
index 2428cf98f..661df8e50 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/per_point_incremental_propagator/time_table_per_point_incremental.rs
@@ -11,7 +11,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -130,7 +130,7 @@ impl Propagator
fn create(
mut self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
let registration = register_tasks(&self.parameters.tasks, context.reborrow(), true);
self.updatable_structures
.reset_all_bounds_and_remove_fixed(context.domains(), &self.parameters);
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs
index cf0792903..86b6adfef 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_over_interval.rs
@@ -9,7 +9,7 @@ use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::Domains;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -133,7 +133,7 @@ impl PropagatorConstructor
fn create(
mut self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
self.updatable_structures
.initialise_bounds_and_remove_fixed(context.domains(), &self.parameters);
let registration = register_tasks(&self.parameters.tasks, context.reborrow(), false);
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs
index 471f2e26e..a8d11a955 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/time_table/time_table_per_point.rs
@@ -11,7 +11,7 @@ use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::EnqueueDecision;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::NotificationContext;
@@ -123,7 +123,7 @@ impl PropagatorConstructor for TimeTablePerPoint
fn create(
mut self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
self.updatable_structures
.initialise_bounds_and_remove_fixed(context.domains(), &self.parameters);
let registration = register_tasks(&self.parameters.tasks, context.reborrow(), false);
diff --git a/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs b/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs
index 063042abf..bd0f1c031 100644
--- a/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs
+++ b/pumpkin-crates/propagators/src/propagators/cumulative/utils/util.rs
@@ -7,7 +7,7 @@ use enumset::enum_set;
use pumpkin_core::propagation::DomainEvent;
use pumpkin_core::propagation::DomainEvents;
use pumpkin_core::propagation::Domains;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::PropagatorConstructorContext;
use pumpkin_core::propagation::ReadDomains;
@@ -53,8 +53,8 @@ pub(crate) fn register_tasks(
tasks: &[Rc>],
mut context: PropagatorConstructorContext<'_>,
register_backtrack: bool,
-) -> EventRegistration {
- let mut registration = EventRegistration::builder();
+) -> EventsToRegister {
+ let mut registration = EventsToRegister::builder();
for task in tasks.iter() {
registration = registration.add(
diff --git a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs
index 4341ab54b..753f71494 100644
--- a/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs
+++ b/pumpkin-crates/propagators/src/propagators/disjunctive/disjunctive_propagator.rs
@@ -8,7 +8,7 @@ use pumpkin_core::predicates::PropositionalConjunction;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LocalId;
use pumpkin_core::propagation::PropagationContext;
@@ -80,7 +80,7 @@ impl DisjunctiveConstructor {
impl PropagatorConstructor for DisjunctiveConstructor {
type PropagatorImpl = DisjunctivePropagator;
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let tasks = self
.tasks
.into_iter()
@@ -95,7 +95,7 @@ impl PropagatorConstructor for DisjunctiveConstr
let inference_code = InferenceCode::new(self.constraint_tag, DisjunctiveEdgeFinding);
- let mut registration = EventRegistration::builder();
+ let mut registration = EventsToRegister::builder();
for task in tasks.iter() {
registration = registration.add(&task.start_time, DomainEvents::BOUNDS, task.id);
}
diff --git a/pumpkin-crates/propagators/src/propagators/element.rs b/pumpkin-crates/propagators/src/propagators/element.rs
index 09f1b9271..3ce5516e1 100644
--- a/pumpkin-crates/propagators/src/propagators/element.rs
+++ b/pumpkin-crates/propagators/src/propagators/element.rs
@@ -17,7 +17,7 @@ use pumpkin_core::predicates::Predicate;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
use pumpkin_core::propagation::DomainEvents;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::ExplanationContext;
use pumpkin_core::propagation::InferenceCheckers;
use pumpkin_core::propagation::LazyExplanation;
@@ -61,7 +61,7 @@ where
);
}
- fn create(self, _: PropagatorConstructorContext) -> (EventRegistration, Self::PropagatorImpl) {
+ fn create(self, _: PropagatorConstructorContext) -> (EventsToRegister, Self::PropagatorImpl) {
let ElementArgs {
array,
index,
@@ -69,7 +69,7 @@ where
constraint_tag,
} = self;
- let mut registration = EventRegistration::builder();
+ let mut registration = EventsToRegister::builder();
for (i, x_i) in array.iter().enumerate() {
registration = registration.add(
x_i,
diff --git a/pumpkin-proof-processor/src/deduction_propagator.rs b/pumpkin-proof-processor/src/deduction_propagator.rs
index 80c5bc33c..26d6b86f3 100644
--- a/pumpkin-proof-processor/src/deduction_propagator.rs
+++ b/pumpkin-proof-processor/src/deduction_propagator.rs
@@ -2,7 +2,7 @@ use pumpkin_core::declare_inference_label;
use pumpkin_core::predicates::PropositionalConjunction;
use pumpkin_core::proof::ConstraintTag;
use pumpkin_core::proof::InferenceCode;
-use pumpkin_core::propagation::EventRegistration;
+use pumpkin_core::propagation::EventsToRegister;
use pumpkin_core::propagation::PredicateId;
use pumpkin_core::propagation::PropagationContext;
use pumpkin_core::propagation::Propagator;
@@ -28,7 +28,7 @@ impl PropagatorConstructor for DeductionPropagatorConstructor {
fn create(
self,
mut context: PropagatorConstructorContext,
- ) -> (EventRegistration, Self::PropagatorImpl) {
+ ) -> (EventsToRegister, Self::PropagatorImpl) {
declare_inference_label!(Nogood);
let DeductionPropagatorConstructor {
@@ -48,7 +48,7 @@ impl PropagatorConstructor for DeductionPropagatorConstructor {
active: true,
};
- (EventRegistration::empty(), propagator)
+ (EventsToRegister::empty(), propagator)
}
}
From 9a7d21bdd3b744f9f6e8a731f9f436e7dd6b745c Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Thu, 28 May 2026 19:57:48 +1000
Subject: [PATCH 08/11] Clarify documentation
---
.../src/propagation/event_registration.rs | 72 +++++++++++++------
1 file changed, 51 insertions(+), 21 deletions(-)
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
index 8bc796fd2..143446bbc 100644
--- a/pumpkin-crates/core/src/propagation/event_registration.rs
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -16,8 +16,17 @@ pub trait EventTarget {
);
}
+/// The interface to a component that needs to know which variables care about which events.
pub trait EventDispatcher {
+ // This is a separate trait to isolate the event registration from the rest of the solver.
+ // That isolation is beneficial when writing tests, as individual components can easily be
+ // mocked without needing to set up an entire state/solver.
+
/// Register the [`DomainId`] with the given [`LocalId`] on the given [`DomainEvents`].
+ ///
+ /// It is possible to register the same [`DomainId`] with different [`LocalId`]s. This may
+ /// happen when the propagator uses multiple local IDs for different events for the same
+ /// variable. Or when different views over the same domain are used in a propagator.
fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId);
}
@@ -26,41 +35,46 @@ pub trait EventDispatcher {
pub struct EventsToRegister(Vec<(DomainId, EnumSet, LocalId)>);
impl EventsToRegister {
- /// Create an [`EventRegistration`] without any variables.
+ /// Create an [`EventsToRegister`] without any variables.
///
/// This is the uncommon case. Without registering for variable events, a propagator will never
- /// be enqueued.
+ /// be enqueued automatically. However, certain propagators like, e.g., compound propagators,
+ /// may not be able to register during construction in which case they will be enqueued
+ /// explicitly when a constraint is added to them. The nogood propagator is an example of such a
+ /// propagator.
pub fn empty() -> EventsToRegister {
EventsToRegister(vec![])
}
- /// Create a new [`EventRegistrationBuilder`].
+ /// Create a new [`EventsToRegisterBuilder`].
///
- /// If no event registrations will be made, use [`EventRegistration::empty`] instead.
- /// Calling [`EventRegistrationBuilder::build`] without any registrations will cause a panic.
+ /// If no event registrations will be made, use [`EventsToRegister::empty`] instead.
+ /// Calling [`EventsToRegisterBuilder::build`] without any registrations will cause a panic.
+ pub fn builder() -> EventsToRegisterBuilder {
+ EventsToRegisterBuilder {
+ registrations: EventsToRegister(vec![]),
+ }
+ }
+
+ /// Add a new event registration to an existing instance of self.
///
/// # Example
///
/// ```
/// use pumpkin_core::propagation::DomainEvents;
- /// use pumpkin_core::propagation::EventRegistration;
+ /// use pumpkin_core::propagation::EventsToRegister;
/// use pumpkin_core::propagation::LocalId;
/// use pumpkin_core::variables::DomainId;
///
/// let v1 = DomainId::new(0);
/// let v2 = DomainId::new(0);
- /// let registration = EventRegistration::builder()
+ /// let mut registration = EventsToRegister::builder()
/// .add(&v1, DomainEvents::ANY_INT, LocalId::from(0))
- /// .add(&v2, DomainEvents::ANY_INT, LocalId::from(1))
/// .build();
+ ///
+ /// // Extend the events to register with another variable.
+ /// registration.add(&v2, DomainEvents::ANY_INT, LocalId::from(1));
/// ```
- pub fn builder() -> EventRegistrationBuilder {
- EventRegistrationBuilder {
- registrations: EventsToRegister(vec![]),
- }
- }
-
- /// Add a new event registration.
pub fn add(
&mut self,
target: &impl EventTarget,
@@ -82,16 +96,32 @@ impl EventDispatcher for EventsToRegister {
}
}
-/// Used to construct an [`EventRegistration`] for heterogeneous [`EventTarget`] implementations.
+/// Used to construct an [`EventsToRegister`] for heterogeneous [`EventTarget`] implementations.
///
-/// See [`EventRegistration::builder`] for a usage example.
+/// See [`EventsToRegister::builder`] for a usage example.
#[derive(Clone, Debug)]
-pub struct EventRegistrationBuilder {
+pub struct EventsToRegisterBuilder {
registrations: EventsToRegister,
}
-impl EventRegistrationBuilder {
+impl EventsToRegisterBuilder {
/// Add a new event registration.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use pumpkin_core::propagation::DomainEvents;
+ /// use pumpkin_core::propagation::EventsToRegister;
+ /// use pumpkin_core::propagation::LocalId;
+ /// use pumpkin_core::variables::DomainId;
+ ///
+ /// let v1 = DomainId::new(0);
+ /// let v2 = DomainId::new(0);
+ /// let registration = EventsToRegister::builder()
+ /// .add(&v1, DomainEvents::ANY_INT, LocalId::from(0))
+ /// .add(&v2, DomainEvents::ANY_INT, LocalId::from(1))
+ /// .build();
+ /// ```
pub fn add(
mut self,
target: &impl EventTarget,
@@ -102,10 +132,10 @@ impl EventRegistrationBuilder {
self
}
- /// Finish constructing the [`EventRegistration`].
+ /// Finish constructing the [`EventsToRegister`].
///
/// If no variables are registered, then this panics. If no variables can be registered during
- /// construction, use [`EventRegistration::empty`].
+ /// construction, use [`EventsToRegister::empty`].
pub fn build(self) -> EventsToRegister {
assert!(
!self.registrations.0.is_empty(),
From 994aa45c7f6eb6b4b8ff83f9f9e64d95a5cb3e31 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Tue, 21 Jul 2026 11:12:25 +0100
Subject: [PATCH 09/11] Update documentation
---
pumpkin-crates/core/src/engine/notifications/mod.rs | 2 +-
pumpkin-crates/core/src/engine/state.rs | 2 +-
pumpkin-crates/core/src/propagation/constructor.rs | 3 +++
.../core/src/propagation/contexts/propagation_context.rs | 3 ++-
pumpkin-crates/core/src/propagation/event_registration.rs | 4 +++-
5 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/pumpkin-crates/core/src/engine/notifications/mod.rs b/pumpkin-crates/core/src/engine/notifications/mod.rs
index ba6a5e64e..d4c7d5a8b 100644
--- a/pumpkin-crates/core/src/engine/notifications/mod.rs
+++ b/pumpkin-crates/core/src/engine/notifications/mod.rs
@@ -109,7 +109,7 @@ impl NotificationEngine {
self.predicate_notifier.get_predicate(predicate_id)
}
- pub(crate) fn watch_all(
+ pub(crate) fn register(
&mut self,
domain: DomainId,
events: EnumSet,
diff --git a/pumpkin-crates/core/src/engine/state.rs b/pumpkin-crates/core/src/engine/state.rs
index db0d786e0..177897111 100644
--- a/pumpkin-crates/core/src/engine/state.rs
+++ b/pumpkin-crates/core/src/engine/state.rs
@@ -351,7 +351,7 @@ impl State {
};
self.notification_engine
- .watch_all(domain_id, events, propagator_var);
+ .register(domain_id, events, propagator_var);
}
pumpkin_assert_simple!(
diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs
index 47d49673b..5d79e2890 100644
--- a/pumpkin-crates/core/src/propagation/constructor.rs
+++ b/pumpkin-crates/core/src/propagation/constructor.rs
@@ -45,6 +45,9 @@ pub trait PropagatorConstructor {
fn add_inference_checkers(&self, _checkers: InferenceCheckers<'_>) {}
/// Create the propagator instance from `Self`.
+ ///
+ /// Alongside the propagator instance, this returns the events for which the propagator should
+ /// be enqueued.
fn create(
self,
context: PropagatorConstructorContext,
diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
index 48d21cb67..63d2867e6 100644
--- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
+++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
@@ -297,6 +297,7 @@ pub(crate) fn build_reason(
}
}
+/// A wrapper around the notification engine that implements [`EventDispatcher`].
struct NotificationEngineWatchers<'a> {
propagator_id: PropagatorId,
notificaton_engine: &'a mut NotificationEngine,
@@ -304,7 +305,7 @@ struct NotificationEngineWatchers<'a> {
impl EventDispatcher for NotificationEngineWatchers<'_> {
fn register(&mut self, domain_id: DomainId, events: EnumSet, local_id: LocalId) {
- self.notificaton_engine.watch_all(
+ self.notificaton_engine.register(
domain_id,
events,
PropagatorVarId {
diff --git a/pumpkin-crates/core/src/propagation/event_registration.rs b/pumpkin-crates/core/src/propagation/event_registration.rs
index 143446bbc..13ca1af71 100644
--- a/pumpkin-crates/core/src/propagation/event_registration.rs
+++ b/pumpkin-crates/core/src/propagation/event_registration.rs
@@ -6,8 +6,10 @@ use crate::propagation::LocalId;
use crate::variables::DomainId;
/// Anything that can subscribe to domain events.
+///
+/// Typically these are variables.
pub trait EventTarget {
- /// Add a registration of self for the given domain events with a local id.
+ /// Indicate that `self` should be registered for the given domain events as the given local ID.
fn register(
&self,
registration: &mut impl EventDispatcher,
From 7fa3dfc936bbc9d56a35a6600be243e71ba29aa1 Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Tue, 21 Jul 2026 11:14:24 +0100
Subject: [PATCH 10/11] Squashed commit of the following:
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
commit 285ac94a49ae7ae7f42f73a59626e653baed0163
Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com>
Date: Thu Jul 16 14:22:11 2026 +0200
feat(pumpkin-solver,pumpkin-core): Implement extended nogood propagation and CPIP nogood learning (#454)
Based on the paper "From Literals to Atomic Constraints: Generalising
Conflict-Driven Clause Learning for Constraint Programming - Imko
Marijnissen, Maarten Flippo, and Emir Demirović" (to appear at CP'26), I
have implemented the extended nogood propagation and CPIP nogood
learning.
## Overview
The PR consists of the following:
- Adjusting the `ResolutionResolver` to be able to produce CPIP nogoods;
this involves updating the stopping criterion for resolving and the
structure `LearnedNogood` to ensure certain invariants.
- Adjusting the `NogoodPropagator` in several ways:
- When adding a(n) (asserting) nogood, the watchers need to be placed on
atomic constraints over different variables, and the detection of
propagation, which can take place at the root level, is adjusted.
- When propagating, the watcher structure is different between the two
approaches so this has been adjusted
- I implemented extended nogood propagation based on the algorithm
described in the paper.
This supersedes the branch `feat/extended-conflict-analysis` in several
ways:
- It now uses lazy explanations for the propagation
- It also performs nogood database management; this is based on the
original (SAT-based) strategy. This could be adjusted in the future
since LBD could be uninformative.
## Feedback
The main point I would like feedback on is whether it makes sense to
keep the 1UIP + unit propagation and CPIP + extended nogood propagation
approaches merged or whether it would be preferable to be separated into
their own structs. I have kept it this way to be able to clearly see the
differences between the two, but I can imagine that this does not make
the code clearer.
Additionally, do we want to include testing this feature in the CI or is
that unnecessary?
@maartenflippo I ran into the issue that retrieving from
`unit_nogood_inference_codes` when using CPIP + extended nogood
propagation led to some issues. I have resolved this in _a_ way but it
would be good to hear your opinion on this!
## Experimentation
I tested the extended nogood propagation + CPIP learning using different
priority schemes. The `(Updated)` instances are the ones where
incremental stopping condition calculation is included.
### Overall Results
`1UIP + High Priority`:
```
{'ERROR': 42,
'OPTIMAL': 100,
'SATISFIABLE': 153,
'UNKNOWN': 93,
'UNSATISFIABLE': 5}
```
`CPIP with Extended Nogood Propagation + High Priority`:
```
{'ERROR': 43,
'OPTIMAL': 95,
'SATISFIABLE': 156,
'UNKNOWN': 94,
'UNSATISFIABLE': 5}
```
`CPIP with Extended Nogood Propagation + High Priority (Updated)`:
```
{'ERROR': 48,
'OPTIMAL': 101,
'SATISFIABLE': 146,
'UNKNOWN': 93,
'UNSATISFIABLE': 5}
```
`1UIP + Very Low Priority`:
```
{'ERROR': 43,
'OPTIMAL': 101,
'SATISFIABLE': 151,
'UNKNOWN': 93,
'UNSATISFIABLE': 5}
```
`CPIP with Extended Nogood Propagation + Very Low Priority`:
```
{'ERROR': 43,
'OPTIMAL': 97,
'SATISFIABLE': 154,
'UNKNOWN': 94,
'UNSATISFIABLE': 5}
```
`CPIP with Extended Nogood Propagation + Very Low Priority (Updated)`:
```
{'ERROR': 32,
'OPTIMAL': 99,
'SATISFIABLE': 167,
'UNKNOWN': 90,
'UNSATISFIABLE': 5}
```
### MiniZinc Scoring
```
{
'1UIP + High Priority': 641.0,
'CPIP + High Priority': 670.71,
'CPIP + High Priority (Updated)': 653.61,
'1UIP + Very Low Priority': 652.35,
'CPIP + Very Low Priority': 698.09,
'CPIP + Very Low Priority (Updated)': 708.24,
}
```
### Average Primal Integral
```
{
'1UIP + High Priority': 63.18,
'CPIP + High Priority': 64.09,
'CPIP + High Priority (Updated)': 69.13,
'1UIP + Very Low Priority': 68.44,
'CPIP + Very Low Priority': 63.37,
'CPIP + Very Low Priority (Updated)': 46.45,
}
```
### Overall Conclusions
In terms of the number of instances solved, 1UIP with a Very Low
Priority appears to be best. However, looking at the MiniZinc scoring,
both of the CPIP approaches outperform their 1UIP counterparts, with
CPIP + Very Low priority performing the best. For the primal integral,
the results are more mixed, with CPIP + Very Low Priority having the
best anytime performance. This _appears_ to indicate that CPIP has some
better anytime performance compared to 1UIP learning (when using a very
low priority of the nogood propagator). I think it would be better to
keep the 1UIP as the default since I would _expect_ 1UIP to outperform
when using free search.
**After Update**: It appears that while `1UIP + Very Low Priority` is
generally the best at proving optimality, `CPIP + Very Low Priority
(Updated)` proves optimality on a similar number of instances while
providing solutions on 16 more. Additionally, the MiniZinc score of this
approach is significantly higher, and the primal integral is
significantly lower.
## TODO
- [x] Rerun the experimentation to determine the impact of the changes
- [x] Calculate the stopping condition for learning CPIP nogoods
incrementally rather than recalculating from scratch in every iteration
commit 216c1ec1a0238f88ab8fff485bbef6b57f2dc212
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu Jul 16 09:33:42 2026 +0200
chore(deps): bump clap from 4.6.1 to 4.6.2 (#505)
Bumps [clap](https://github.com/clap-rs/clap) from 4.6.1 to 4.6.2.
Release notes
Sourced from clap's
releases.
v4.6.2
[4.6.2] - 2026-07-15
Fixes
- (help) Say
alias when there is only one
Changelog
Sourced from clap's
changelog.
[4.6.2] - 2026-07-15
Fixes
- (help) Say
alias when there is only one
Commits
0fe0be3
chore: Release
480af9d
docs: Update changelog
2b3ddd0
Merge pull request #6340
from liskin/fix-completion-escape
7ffe739
fix(complete): Do not suggest options after "--"
d47fc4f
test(complete): Options suggested after escape (--)
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 97e670c7bbf5e096f57a5efefef1f66d8d37558a
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu Jul 16 09:33:10 2026 +0200
chore(deps): bump cc from 1.2.66 to 1.2.67 (#504)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.2.66 to 1.2.67.
Release notes
Sourced from cc's
releases.
cc-v1.2.67
Other
- Fix clippy warning (#1788)
- Regenerate target info (#1785)
- Add support for
aarch64-unknown-linux-pauthtest target
(#1713)
- Fix nightly compilation error (#1783)
Changelog
Sourced from cc's
changelog.
1.2.67
- 2026-07-11
Other
- Fix clippy warning (#1788)
- Regenerate target info (#1785)
- Add support for
aarch64-unknown-linux-pauthtest target
(#1713)
- Fix nightly compilation error (#1783)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit c0246ce66ab607d67c47e458120005e7eb27f586
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu Jul 16 09:32:51 2026 +0200
chore(deps): bump regex from 1.12.4 to 1.13.1 (#503)
Bumps [regex](https://github.com/rust-lang/regex) from 1.12.4 to 1.13.1.
Changelog
Sourced from regex's
changelog.
1.13.1 (2026-07-15)
This is a release that fixes a bug where incorrect regex match
offsets could be
reported. Note that this doesn't impact whether a match occurs or not,
just
where it occurs. The match offsets are still valid for slicing, they
just may
not refer to the correct leftmost-first match. See
#1364
for (many) more details.
Bug fixes:
- #1354:
Fixes previously unsound reverse suffix and inner optimizations.
1.13.0 (2026-07-09)
This release includes a new API, a regex! macro, for
lazy compilation of
a regex from a string literal. If you use regexes a lot, it's likely
you've
already written one exactly like it. The new macro can be used like
this:
use regex::regex;
fn is_match(line: &str) -> bool {
// The regex will be compiled approximately once and reused
automatically.
// This avoids the footgun of using Regex::new here, which
would
// guarantee that it would be compiled every time this routine is
called.
// This would likely make this routine much slower than it needs to
be.
regex!(r"bar|baz").is_match(line)
}
let hay = "
path/to/foo:54:Blue Harvest
path/to/bar:90:Something, Something, Something, Dark Side
path/to/baz:3:It's a Trap!
";
let matches = hay.lines().filter(|line| is_match(line)).count();
assert_eq!(matches, 2);
Improvements:
- #709:
Add a new
regex! macro for efficient and automatic reuse of
a compiled regex.
Commits
2b52759
1.13.1, redux
40e9823
1.13.1
75fcb96
changelog: 1.13.1
64ad0b6
automata: fix bug in reverse suffix/inner optimization
fa91c31
automata: fix a bug caught by Codex review
30390ec
automata: formatting tweaks
821a8eb
automata: refactor reverse suffix/inner search slightly
10afd70
automata: expose the extracted literals for inner literal
extraction
8c34f41
automata: avoid reverse suffix optimization for non-leftmost-first
5524f02
test: add regression tests for failed reverse suffix/inner
optimizations
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit fab5ece22db10b95e176b264fa7bec6609bcdd59
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu Jul 16 09:32:03 2026 +0200
chore(deps): bump syn from 2.0.118 to 2.0.119 (#502)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.118 to 2.0.119.
Release notes
Sourced from syn's
releases.
2.0.119
- Preserve attributes on tail-call expressions in statement position
(#1994)
- Parse field-representing types builtin in type position (#1996)
Commits
3295f9e
Release 2.0.119
6ae9c18
Merge pull request #1996
from dtolnay/fieldrepresenting
8ebd963
Parse field-representing types builtin
540ccf8
Drop unneeded lifetime on covariant Cursor in verbatim::between
aa05887
Merge pull request #1995
from dtolnay/cursor
b7160d3
Reduce forking for Verbatim construction
efdc925
Merge pull request #1994
from dtolnay/tailcall
de6424c
Preserve attribute on tail-call expression in statement position
050dd73
Stricter const move closure grammar
c7d514b
Merge pull request #1992
from dtolnay/scanconstmove
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit a6374ce46e3b7f268efee4d978412f5d3429c4c3
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu Jul 16 09:31:48 2026 +0200
chore(deps): bump docker/build-push-action from 3add1637a63aa04f4e5fa1916f9e12090d90780a to cb941d0b895b09c17fa011d41c411b33c752cf28 (#501)
Bumps
[docker/build-push-action](https://github.com/docker/build-push-action)
from 3add1637a63aa04f4e5fa1916f9e12090d90780a to
cb941d0b895b09c17fa011d41c411b33c752cf28.
Commits
cb941d0
Merge pull request #1583
from crazy-max/group-codeql-dependabot-updates
a9855f3
chore: group codeql dependabot updates
- See full diff in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 5ca68ed8365b96b471386b3a85326bbbb4da0684
Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com>
Date: Wed Jul 15 13:15:32 2026 +0200
chore: Add citation file (#473)
Adds the "Cite this repository" button which links to the bib file.
Unfortunately, it does not show the bibtex reference directly. When
using a `.cff` file, certain metadata is lost. Hence, I think this is
the easiest fix.
commit 42894d6640018a5c4ad28e7170a1f993818b3c64
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 09:02:14 2026 +0200
chore(deps): bump actions/cache from 5 to 6 (#477)
Bumps [actions/cache](https://github.com/actions/cache) from 5 to 6.
Release notes
Sourced from actions/cache's
releases.
v6.0.0
What's Changed
Full Changelog: https://github.com/actions/cache/compare/v5...v6.0.0
v5.1.0
What's Changed
Full Changelog: https://github.com/actions/cache/compare/v5...v5.1.0
v5.0.5
What's Changed
Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.5
v5.0.4
What's Changed
New Contributors
Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.4
v5.0.3
What's Changed
Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.3
v.5.0.2
v5.0.2
What's Changed
... (truncated)
Changelog
Sourced from actions/cache's
changelog.
Releases
How to prepare a release
[!NOTE]
Relevant for maintainers with write access only.
- Switch to a new branch from
main.
- Run
npm test to ensure all tests are passing.
- Update the version in
https://github.com/actions/cache/blob/main/package.json.
- Run
npm run build to update the compiled files.
- Update this
https://github.com/actions/cache/blob/main/RELEASES.md
with the new version and changes in the ## Changelog
section.
- Run
licensed cache to update the license report.
- Run
licensed status and resolve any warnings by
updating the https://github.com/actions/cache/blob/main/.licensed.yml
file with the exceptions.
- Commit your changes and push your branch upstream.
- Open a pull request against
main and get it reviewed
and merged.
- Draft a new release https://github.com/actions/cache/releases
use the same version number used in
package.json
- Create a new tag with the version number.
- Auto generate release notes and update them to match the changes you
made in
RELEASES.md.
- Toggle the set as the latest release option.
- Publish the release.
- Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
- There should be a workflow run queued with the same version
number.
- Approve the run to publish the new version and update the major tags
for this action.
Changelog
6.1.0
6.0.0
- Updated
@actions/cache to ^6.0.1,
@actions/core to ^3.0.1, @actions/exec to
^3.0.0, @actions/io to ^3.0.2
- Migrated to ESM module system
- Upgraded Jest to v30 and test infrastructure to be ESM
compatible
5.0.4
- Bump
minimatch to v3.1.5 (fixes ReDoS via globstar
patterns)
- Bump
undici to v6.24.1 (WebSocket decompression bomb
protection, header validation fixes)
- Bump
fast-xml-parser to v5.5.6
5.0.3
5.0.2
... (truncated)
Commits
55cc834
Merge pull request #1768
from jasongin/readonly-cache
d8cd72f
Bump @actions/cache to v6.1.0 - handle cache write error
due to RO token
2c8a9bd
Merge pull request #1760
from actions/samirat/esm_migration_and_package_update
e9b91fd
Prettier fixes
e4884b8
Rebuild dist
10baf01
Fixed licenses
e39b386
Fix test mock return order
b692820
PR feedback
6074912
Rebuild dist bundles as ESM to match type:module
5a912e8
Fix lint and jest issues
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 352b03cfcc45643b2f9fdbfd0ce91fd4713d1e8e
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 09:02:06 2026 +0200
chore(deps): bump env_logger from 0.11.10 to 0.11.11 (#485)
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.11.10
to 0.11.11.
Release notes
Sourced from env_logger's
releases.
v0.11.11
[0.11.11] - 2026-06-25
Internal
Changelog
Sourced from env_logger's
changelog.
[0.11.11] - 2026-06-25
Internal
Commits
b4d3f2b
chore: Release
cc2b2ef
chore: Release
69e27d1
docs: Update changelog
166880d
Merge pull request #411
from epage/parse
0a580d0
fix(filter): Remove 'parse' on no_std
78d8ef1
Merge pull request #404
from cagatay-y/feature/filter-no_std
132fe86
feat(filter): Add support for no_std environments
4feafa4
refactor(env_filter): Fix unreachable pub warning
92f8d8d
Merge pull request #410
from rust-cli/renovate/crate-ci-typos-1.x
4e57784
chore(deps): Update pre-commit hook crate-ci/typos to v1.47.0
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 41a7e71a9977bcc9a255c25f37eebe7503341f57
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 08:55:01 2026 +0200
chore(deps): bump docker/build-push-action from ff26911fd365b0233252dfbd8eede1c0e9ef9a51 to 3add1637a63aa04f4e5fa1916f9e12090d90780a (#493)
Bumps
[docker/build-push-action](https://github.com/docker/build-push-action)
from ff26911fd365b0233252dfbd8eede1c0e9ef9a51 to
3add1637a63aa04f4e5fa1916f9e12090d90780a.
Commits
3add163
Merge pull request #1577
from docker/dependabot/npm_and_yarn/sigstore-4.1.1
6d79e06
chore(deps): Bump sigstore from 4.1.0 to 4.1.1
53b7df9
Merge pull request #1572
from docker/dependabot/npm_and_yarn/docker/actions-t...
154298c
[dependabot skip] chore: update generated content
cb1238b
chore(deps): Bump @docker/actions-toolkit from 0.91.0 to
0.92.0
24f845d
Merge pull request #1566
from docker/dependabot/npm_and_yarn/js-yaml-4.2.0
9c69730
[dependabot skip] chore: update generated content
bc3a3a5
Merge pull request #1574
from docker/dependabot/github_actions/aws-actions/co...
a82c504
chore(deps): Bump js-yaml from 4.1.1 to 4.3.0
0285a75
Merge pull request #1573
from docker/dependabot/github_actions/actions/cache-...
- Additional commits viewable in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit c78512ee66512f5deb307a6d97ce840f80f9f99f
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 08:54:30 2026 +0200
chore(deps): bump docker/metadata-action from 020b7354dd55a28effcab0f3b19639fe24d3f58b to dc802804100637a589fabce1cb79ff13a1411302 (#492)
Bumps
[docker/metadata-action](https://github.com/docker/metadata-action) from
020b7354dd55a28effcab0f3b19639fe24d3f58b to
dc802804100637a589fabce1cb79ff13a1411302.
Commits
dc80280
Merge pull request #696
from docker/dependabot/npm_and_yarn/docker/actions-to...
2b9fe83
[dependabot skip] chore: update generated content
8128ce3
chore(deps): Bump @docker/actions-toolkit from 0.91.0 to
0.92.0
1d1c895
Merge pull request #695
from docker/dependabot/npm_and_yarn/semver-7.8.5
7f0c2dd
Merge pull request #694
from docker/dependabot/npm_and_yarn/sigstore-4.1.1
025f8c5
[dependabot skip] chore: update generated content
e98d63c
chore(deps): Bump semver from 7.8.1 to 7.8.5
37d9379
chore(deps): Bump sigstore from 4.1.0 to 4.1.1
a1b8072
Merge pull request #690
from docker/dependabot/npm_and_yarn/sigstore/core-3.2.1
e0e3381
[dependabot skip] chore: update generated content
- Additional commits viewable in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 10d4c16111518c32283ee1f0ccde2571cd5cd52c
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 08:54:18 2026 +0200
chore(deps): bump docker/login-action from 3864d6aed8ff134b2ed894ce00c87695c709c870 to af1e73f918a031802d376d3c8bbc3fe56130a9b0 (#491)
Bumps [docker/login-action](https://github.com/docker/login-action) from
3864d6aed8ff134b2ed894ce00c87695c709c870 to
af1e73f918a031802d376d3c8bbc3fe56130a9b0.
Commits
af1e73f
Merge pull request #1034
from docker/dependabot/npm_and_yarn/aws-sdk-dependen...
da722bd
[dependabot skip] chore: update generated content
2916ad6
build(deps): bump the aws-sdk-dependencies group across 1 directory with
2 up...
ca0a662
Merge pull request #1035
from crazy-max/fix-registry-auth-empty-mask
c455755
chore: update generated content
4835190
skip empty registry-auth secret mask
992421c
Merge pull request #1033
from docker/dependabot/github_actions/docker/bake-ac...
b249b43
Merge pull request #1032
from docker/dependabot/github_actions/docker/bake-ac...
1b67977
build(deps): bump docker/bake-action from 7.2.0 to 7.3.0
9d49d6a
build(deps): bump docker/bake-action/subaction/matrix
- Additional commits viewable in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 498823fa7866a2b3263f944fde8b15ca49dbef89
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 08:50:08 2026 +0200
chore(deps): bump rand from 0.10.1 to 0.10.2 (#494)
Bumps [rand](https://github.com/rust-random/rand) from 0.10.1 to 0.10.2.
Changelog
Sourced from rand's
changelog.
[0.10.2] — 2026-07-02
Fixes
- Fix possible memory safety violation due to deserialization of
UniformChar from bad source (#1790)
Changes
- Document required output order of fn
partial_shuffle
and apply #[must_use] (#1769)
- Avoid usage of
unsafe in contexts where non-local
memory corruption could invalidate contract (#1791)
#1769:
rust-random/rand#1769
#1790:
rust-random/rand#1790
#1791:
rust-random/rand#1791
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit c775b0cc0ea7fa0372351eec58e831d008a090ba
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed Jul 15 08:49:21 2026 +0200
chore(deps): bump cc from 1.2.65 to 1.2.66 (#495)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.2.65 to 1.2.66.
Release notes
Sourced from cc's
releases.
cc-v1.2.66
Other
- Fix target parsing for aarch64-unknown-linux-pauthtest (#1779)
- Support new QNX targets (#1775)
- Add kache to the supported compiler wrappers (#1770)
Changelog
Sourced from cc's
changelog.
1.2.66
- 2026-07-05
Other
- Fix target parsing for aarch64-unknown-linux-pauthtest (#1779)
- Support new QNX targets (#1775)
- Add kache to the supported compiler wrappers (#1770)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit bd62213e6e281cd33d7680272ed50d082407970d
Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com>
Date: Wed Jul 15 08:29:52 2026 +0200
chore: adding CP papers (#499)
Adds the papers that use Pumpkin from CP'26.
commit bbf0748a4f6ea7cd9fe408194cd174823536044f
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jul 7 15:52:42 2026 +0200
chore(deps): bump anyhow from 1.0.102 to 1.0.103 (#487)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.102 to
1.0.103.
Release notes
Sourced from anyhow's
releases.
1.0.103
- Fix Stacked Borrows violation (UB) in
Error::downcast_mut (#451,
#452)
Commits
5bdb0e2
Release 1.0.103
e621bd3
Merge pull request #452
from dtolnay/downcast
6e8c000
Eliminate pointer->reference->pointer during downcast
67c4abd
Add regression test for issue 451
917a169
Update actions/upload-artifact@v6 -> v7
d9dc3fa
Update actions/checkout@v6 -> v7
841522b
Raise minimum tested compiler to rust 1.85
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit f13524738b913b686d45ab03a745c7114fdc53d9
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:24:49 2026 +0200
chore(deps): bump wasm-bindgen-test from 0.3.75 to 0.3.76 (#476)
Bumps [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen)
from 0.3.75 to 0.3.76.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 00005ef4e3e5e3c19e4f7057e41cc422b3222815
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:24:05 2026 +0200
chore(deps): bump pyo3-build-config from 0.28.3 to 0.29.0 (#478)
Bumps [pyo3-build-config](https://github.com/pyo3/pyo3) from 0.28.3 to
0.29.0.
Release notes
Sourced from pyo3-build-config's
releases.
PyO3 0.29.0
This release is a relatively large release with improvements across
many areas of PyO3's API.
Build and packaging changes
This release brings full support for Python 3.15 beta. We encourage
downstream projects to begin testing and distributing Python 3.15 beta
wheels so that the ecosystem can prepare for the 3.15 final release
later in the year.
Alongside Python 3.15 support comes support for its new
"abi3t" stable ABI which supports both free-threaded and
gil-enabled Python builds. For projects distributing stable ABI wheels,
we recommend distributing (for each OS/architecture) an abi3 wheel built
for your minimum supported Python version, a 3.14t version-specific
wheel for free-threaded Python 3.14, and an abi3t wheel to support
Python 3.15 (and future versions).
Support for Python 3.7 has been dropped. Support for Python 3.13t,
the first experimental free-threaded release of CPython, has also been
dropped. 3.14t (and soon 3.15t) is more stable, performant, and the
starting point for CPython's own declaration of "support" for
the free-threaded build.
The PyO3 build process (via the pyo3-build-config crate)
has been adjusted to reduce the cost of rebuilds when the environment
used to detect the Python interpreter changes;
pyo3-build-config and pyo3-macros will no
longer be rebuilt in such cases (although pyo3-ffi and
crates downstream of it still will be rebuilt). As a consequence the
pyo3_build_config APIs now require crates to have a direct
dependency on pyo3 or pyo3-ffi. We hope to
continue to reduce rebuild frequency and cost in a future PyO3
release.
Security updates
With the recent boom in AI-assisted security scanning, PyO3 has
inevitably had several correctness issues exposed by AI-assisted
scanning.
In particular, PyO3 0.29 fixes two security vulnerabilities we will
be releasing to the RustSec Advisory Database imminently:
- Missing
Sync bound on
PyCFunction::new_closure closures
- Possible out of bounds read in
BoundTupleIterator::nth_back and
BoundListIterator::nth_back
Any code using the above APIs is advised to update as soon as
possible.
This release also contains several other minor breaking changes to
close soundness holes uncovered by AI-assisted scanning. Our assessment
as maintainers was that, excluding the two vulnerability cases listed
above, these correctness issues would likely have crashed immediately
upon user testing rather than leading to attacker-exploitable pathways.
We nevertheless wanted to see them closed without the usual deprecation
cycle. These cases are noted in the migration guide.
Other major themes in this release
New in this release is a CLI in pyo3-introspection to
generate type stubs along with the experimental-inspect
feature. Downstream, maturin has also gained support to
generate type stubs using the feature. The feature is reaching a point
where substantial amount of type stubs can be generated automatically.
We would like to encourage users to begin using this feature and helping
us find what functionality is missing, with a hope we can declare its
API stable given sufficient feedback.
A substantial amount of effort has been invested in
pyo3-ffi as part of the process of extending it with 3.15's
new APIs. There have been many missing APIs from older Python versions
added. There have also been a number of fixes to incorrect definitions
(these are breaking changes, but also necessary for correctness); we
hope there will be far fewer such cases in the future due to more
comprehensive checking added to PyO3's CI. Finally, many private CPython
APIs (those with _Py underscore-named prefix) have been
removed from pyo3-ffi's public API.
In closing
There are also many other incremental improvements, bug fixes and
smaller features; full detail can be found in the CHANGELOG.
Please consult the migration guide for
help upgrading.
Thank you to everyone who contributed code, documentation, design
ideas, bug reports, and feedback. The following contributors' commits
are included in this release:
@Alc-Alc
@alex
@anuraaga
@BD103
@bschoenmaeckers
@Cheukting
@chirizxc
@ChristopherRabotin
@clin1234
@codeguru42
@davidhewitt
... (truncated)
Changelog
Sourced from pyo3-build-config's
changelog.
[0.29.0] - 2026-06-11
Packaging
- Support the new PEP 803 abi3t ABI with new
abi3t and
abi3t-py315 features. #5807
pyo3-macros-backend no longer depends on
pyo3-build-config. #5809
- Drop support for Python 3.13t (3.14t and above continue to be
supported; CPython declared free-threading supported starting with
Python 3.14). #5865
- Drop support for Python 3.7. #5912
- Extend range of supported versions of
hashbrown
optional dependency to include version 0.17. #5973
- Support Python 3.15.0b1. #6014
pyo3-ffi is now no_std. #6022
Added
- Add
PyErr::set_traceback to set the traceback of an
exception object. #5349
- Add
PyUnicodeDecodeError::new_err_from_utf8 to create a
PyErr from a str::Utf8Error. #5668
experimental-inspect: implement INPUT_TYPE
and OUTPUT_TYPE on optional third-party crate conversions.
#5770
experimental-inspect: include doc comments in generated
stubs. #5782
- Add
pyo3_build_config::PythonAbi,
pyo3_build_config::PythonAbiKind,
pyo3_build_config::PythonAbiBuilder,
pyo3_build_config::InterpreterConfig::target_abi, and
pyo3_build_config::InterpreterConfigBuilder::target_abi. #5807
- Add
Borrowed::get as an equivalent to
Bound::get and Py::get. #5849
- Add
PyFrame::new, PyTraceBack::new, and
PyFrameMethods::line_number. #5857
- Add
PyUntypedBuffer::obj to retrieve the Python object
owning the buffer. #5870
- Add
PyCapsule::new_with_value and
PyCapsule::new_with_value_and_destructor. #5881
- Add
PyErr::set_context and PyErr::context.
#5887
- Add a small CLI to
pyo3-introspection to generate
stubs. #5904
- Add
Python::version_str. #5921
- Add
TryFrom<&Bound<T>> for
PyRef<T>, PyRefMut<T>,
PyClassGuard<T> and
PyClassGuardMut<T>. #5922
- Add
From<&Bound<T>> for
Bound<T> and Py<T> #5922
- Add
PyDictMethods::set_default and
PyDictMethods::set_default_ref to allow atomically setting
default values in a PyDict. #5955
- add
PyFrameMethods::outer|code|var|builtins|globals|locals. #5967
- Add
From conversions for PyErr from
std::time::TryFromFloatSecsError,
std::time::SystemTimeError,
std::path::StripPrefixError,
std::env::JoinPathsError,
std::char::ParseCharError, and
std::char::CharTryFromError. #6001
- Add
pyo3_build_config::InterpreterConfigBuilder. #6034
- Add
PyCapsule::import_pointer #6066
- Add
PyClassGuardMapMut. #6073
- Expose
PyListMethods::get_item_unchecked,
PyTupleMethods::get_item_unchecked, and
PyTupleMethods::get_borrowed_item_unchecked on abi3. #6075
- Add
PyClassGuardMapSuper. #6104
- Add
PyClassGuard and PyClassGuardMut to
pyo3::prelude. #6112
- Add
Debug impls for PyClassGuard and
PyClassGuardMut. #6112
- Enable extending
PyDateTime, PyDate,
PyTime, PyDelta and PyTzInfo on
abi3 with python 3.12+. #6115
- Expose
PyFunction available on abi3. #6117
- FFI definitions:
- Added FFI definitions
PyUnstable_Object_IsUniquelyReferenced,
PyUnstable_Object_IsUniquelyReferencedTemporary,
PyUnstable_EnableTryIncref, and
PyUnstable_TryIncref. #5828
- Add FFI definitions
ffi::PyErr_GetHandledException and
ffi::PyErr_SetHandledException. #5887
- Add FFI definition
Py_HASH_SIPHASH13. #5891
- Add FFI definition
PyStructSequence_UnnamedField
constant on Python 3.9 and up (or 3.11 with abi3 features). #5892
- Add FFI definitions
PyUnstable_InterpreterFrame_GetCode,
PyUnstable_InterpreterFrame_GetLasti,
PyUnstable_InterpreterFrame_GetLine, and
PyUnstable_ExecutableKinds. #5932
- Add FFI definitions
PyMarshal_WriteLongToFile,
PyMarshal_WriteObjectToFile,
PyMarshal_ReadLongFromFile,
PyMarshal_ReadShortFromFile,
PyMarshal_ReadObjectFromFile, and
PyMarshal_ReadLastObjectFromFile. #5934
- Add FFI definitions
PyObject_GetAIter,
PyAIter_Check, PyMapping_HasKeyWithError,
PyMapping_HasKeyStringWithError,
PyMapping_GetOptionalItem,
PyMapping_GetOptionalItemString,
PySequence_ITEM, PySequence_Fast_GET_SIZE,
PySequence_Fast_GET_ITEM, and
PySequence_Fast_ITEMS. #5942
- Add FFI definition
compat::PyObject_HasAttrWithError.
#5944
- Add FFI definitions
PyDict_SetDefault,
PyDict_SetDefaultRef, PyDict_ContainsString,
PyDict_Pop, PyDict_PopString,
PyDict_ClearWatcher, PyDict_Watch,
PyDict_Unwatch, and PyFrozenDict_New. #5947
... (truncated)
Commits
0f90242
release: 0.29.0 (#6107)
cd128ed
doc: mention abi3t, python3t.dll, and abi3t_compat folder in FAQ (#6124)
7e2ef18
Avoid type checks in methods where CPython already guarantees the
received ty...
f930199
docs: additional detail in migration guide for 0.29 (#6123)
91ab0d1
Enable Windows abi3t tests (#6106)
fe0fdd5
add PyLong* API (3.14+) (#6016)
f41b1df
Hang when reattaching after detach during shutdown (#6085)
5ae66a8
Fix double import on RustPython (#6122)
ad4a510
PyFunction: enable some extra tests with abi3 (#6118)
c79ac0e
ci: Add test for minimum supported debug build of Python (#5852)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 051e839a3e6ac46eca8e264fff796c684f326bc2
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:23:32 2026 +0200
chore(deps): bump docker/login-action from 65b78e6e13532edd9afa3aa52ac7964289d1a9c1 to 3864d6aed8ff134b2ed894ce00c87695c709c870 (#479)
Bumps [docker/login-action](https://github.com/docker/login-action) from
65b78e6e13532edd9afa3aa52ac7964289d1a9c1 to
3864d6aed8ff134b2ed894ce00c87695c709c870.
Commits
3864d6a
Merge pull request #1018
from docker/sec-cli/npm-ci-20260612-182458
64b2538
fix: use lockfile-aware install commands
37a9a4b
Merge pull request #1016
from docker/ci-ecr-oidc
eb1946f
ci: test AWS ECR with OIDC
946f94d
Merge pull request #1007
from crazy-max/ci-creds-update
f50e5f8
ci: update registry to auth to gar
c5e5fd0
ci: update registry to auth to acr
60e5331
ci: update registry to auth to ecr
6a848e5
ci: update secrets to auth to docker hub
0267638
Merge pull request #1008
from crazy-max/ci-ghcr-dind-test-image
- Additional commits viewable in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit d6d1c4f8a87bdb2b3de2d8ed99f41e2d951c148a
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:23:11 2026 +0200
chore(deps): bump actions/checkout from 6 to 7 (#475)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to
7.
Release notes
Sourced from actions/checkout's
releases.
v7.0.0
What's Changed
New Contributors
Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0
v6.0.3
What's Changed
New Contributors
Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3
v6.0.2
What's Changed
Full Changelog: https://github.com/actions/checkout/compare/v6.0.1...v6.0.2
v6.0.1
What's Changed
Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1
Changelog
Sourced from actions/checkout's
changelog.
Changelog
v7.0.0
v6.0.3
v6.0.2
v6.0.1
v6.0.0
v5.0.1
v5.0.0
v4.3.1
v4.3.0
v4.2.2
v4.2.1
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit faf2dace3f8b4997505b5e4041386d8af6a26eaa
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:22:45 2026 +0200
chore(deps): bump itertools from 0.14.0 to 0.15.0 (#480)
Bumps [itertools](https://github.com/rust-itertools/itertools) from
0.14.0 to 0.15.0.
Changelog
Sourced from itertools's
changelog.
0.15.0
Breaking
- Restructure
Position as struct instead of enum (#1042,
#1043)
- Canonicalize
all_equal_value's error type (#1032)
Added
- Add
*_with_hasher adaptors (#1007)
- Add strip_prefix and strip_prefix_by methods (#1104)
Changed
- Remove
Clone bounds from
tuple_combinations and array_combinations(#1011)
must_use for collect_vec (#1009)
- Make
izip! temporary friendly (#1021)
- Add
array_combinations_with_replacement (#1033)
- Implement
Debug for remaining public types (#1038)
- Specialize
ExactlyOneError::count (#1046)
- Implement
PeekingNext for more types, in particular
vec::IntoIter (#1059,
#1073)
- Fix
PadUsing::next_back (#1082)
- Introduce
[circular_]array_windows, deprecate
tuple_windows (#1086)
- Deprecate
tuple_combinations (replaced by
array_combinations) (#1085)
Notable Internal Changes
Commits
37bd72a
Update CHANGELOG.md: strip_prefix[_by]
86ec635
Use ControlFlow in fold_while
implementation
d5897f7
refactor(strip_prefix): use try_for_each and drop PartialEq, Eq on
StripPrefi...
b2a978a
feat(Itertools): add strip_prefix and strip_prefix_by methods
12b6ec6
Update CHANGELOG.md for all_equal_value_error's error type
121821e
AllEqualValueError implements std::error::Error
adac44e
Introduce AllEqualValueError
5707384
Update CHANGELOG.md
df60ff0
Update CHANGELOG.md
113b850
Update CHANGELOG.md to include with_hasher
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit f801a3cf062c44e0866a861efa1c87f37fd6abef
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:22:18 2026 +0200
chore(deps): bump docker/build-push-action from f2a1d5e99d037542a71f64918e516c093c6f3fc4 to ff26911fd365b0233252dfbd8eede1c0e9ef9a51 (#481)
Bumps
[docker/build-push-action](https://github.com/docker/build-push-action)
from f2a1d5e99d037542a71f64918e516c093c6f3fc4 to
ff26911fd365b0233252dfbd8eede1c0e9ef9a51.
Commits
ff26911
Merge pull request #1562
from docker/sec-cli/npm-ci-20260612-145940
c2245a3
fix: use lockfile-aware install commands
d2aace8
Merge pull request #1561
from docker/e2e-aws-ecr-oidc
ffca515
ci(e2e): use OIDC for AWS ECR
f72b3cf
Merge pull request #1555
from crazy-max/e2e-dockerhub
405b217
ci(e2e): use org-owned Docker Hub credentials for e2e pushes
7b93b2b
Merge pull request #1554
from crazy-max/e2e-ghcr
f55bd08
ci(e2e): use GITHUB_TOKEN for GHCR e2e
1d0c110
Merge pull request #1548
from crazy-max/docs-link-secret-inputs
8db8ba8
Merge pull request #1549
from crazy-max/ci-e2e-dockerhub-push-scope
- Additional commits viewable in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit ee7c80add60b49a3a24b269f9929e5a479c79a50
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 30 13:21:56 2026 +0200
chore(deps): bump docker/metadata-action from 9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 to 020b7354dd55a28effcab0f3b19639fe24d3f58b (#474)
Bumps
[docker/metadata-action](https://github.com/docker/metadata-action) from
9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 to
020b7354dd55a28effcab0f3b19639fe24d3f58b.
Commits
020b735
Merge pull request #682
from docker/sec-cli/npm-ci-20260612-184903
7f842e8
fix: use lockfile-aware install commands
3caf19f
Merge pull request #675
from crazy-max/yarn-update
8016b4f
update yarn to 4.15.0
530a407
Merge pull request #672
from docker/dependabot/npm_and_yarn/docker/actions-to...
afa75d4
chore: update generated content
26a83f6
chore(deps): Bump @docker/actions-toolkit from 0.90.0 to
0.91.0
585dfe4
Merge pull request #663
from docker/dependabot/npm_and_yarn/actions/core-3.0.1
829c7e6
chore: update generated content
246bbe8
chore(deps): Bump @actions/core from 3.0.0 to 3.0.1
- Additional commits viewable in compare
view
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit acf118e8bc8f816b47263683ecdbd62c4d6abb5b
Author: Maarten Flippo
Date: Wed Jun 24 09:19:32 2026 +0200
build: Do not compile with -Ctarget-cpu=native by default
This makes the binaries not transferable to other computers.
commit e19549117d0a2ff3b03564ab67265b36cec683ba
Author: Maarten Flippo
Date: Tue Jun 23 11:32:09 2026 +0200
chore: Fix path in dockerfile of solver binary
commit 9b33d5b1fbcb8339b1d9f8f4e1a2cef1a9feccc0
Author: Imko Marijnissen
Date: Tue Jun 23 11:17:25 2026 +0200
chore: hard code organisation name
commit c4e716eeca60811390f08ee72907f016bea0c1c8
Author: Imko Marijnissen
Date: Tue Jun 23 11:07:40 2026 +0200
chore: fix name in release plz
commit 847fe829c5fcf6b11c948fc125fd30931dfbf6f2
Author: Imko Marijnissen
Date: Tue Jun 23 11:00:43 2026 +0200
chore: add releasing of docker directly in release-plz
commit 7a7052e963a181e9320d9ad02fb704e5d69b17f1
Author: Imko Marijnissen
Date: Tue Jun 23 10:38:34 2026 +0200
chore: add environment for release-plz
commit 8c7732e8f56b450a749ccf2c8f564436fbab2d62
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue Jun 23 10:35:53 2026 +0200
chore: Release packages (#471)
## 🤖 New release
* `fzn-rs-derive`: 0.1.0 -> 0.1.1
* `fzn-rs`: 0.1.0 -> 0.1.1 (✓ API compatible changes)
* `pumpkin-checking`: 0.3.0 -> 0.4.0 (⚠ API breaking changes)
* `pumpkin-core`: 0.3.0 -> 0.4.0 (⚠ API breaking changes)
* `pumpkin-conflict-resolvers`: 0.3.0 -> 0.4.0 (✓ API compatible
changes)
* `pumpkin-propagators`: 0.3.0 -> 0.4.0 (✓ API compatible changes)
* `pumpkin-constraints`: 0.3.0 -> 0.4.0 (✓ API compatible changes)
* `pumpkin-solver`: 0.3.0 -> 0.4.0 (✓ API compatible changes)
* `pumpkin-checker`: 0.3.0 -> 0.4.0 (⚠ API breaking changes)
### ⚠ `pumpkin-checking` breaking changes
```text
--- failure trait_added_supertrait: non-sealed trait added new supertraits ---
Description:
A non-sealed trait added one or more supertraits, which breaks downstream implementations of the trait
ref: https://doc.rust-lang.org/cargo/reference/semver.html#generic-bounds-tighten
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/trait_added_supertrait.ron
Failed in:
trait pumpkin_checking::AtomicConstraint gained Clone in file /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/checking/src/atomic_constraint.rs:11
```
### ⚠ `pumpkin-core` breaking changes
```text
--- failure copy_impl_added: type now implements Copy ---
Description:
A public type now implements Copy, causing non-move closures to capture it by reference instead of moving it.
ref: https://github.com/rust-lang/rust/issues/100905
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/copy_impl_added.ron
Failed in:
pumpkin_core::state::EmptyDomainConflict in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/engine/conflict.rs:53
--- failure enum_repr_variant_discriminant_changed: variant of an enum with explicit repr changed discriminant ---
Description:
An enum variant has changed its discriminant value. The enum has a defined primitive representation, so this breaks downstream code that used the discriminant value via an unsafe pointer cast.
ref: https://doc.rust-lang.org/reference/items/enumerations.html#pointer-casting
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_repr_variant_discriminant_changed.ron
Failed in:
variant PredicateType::UpperBound 1 -> 3 in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/engine/predicates/predicate.rs:149
variant PredicateType::NotEqual 2 -> 1 in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/engine/predicates/predicate.rs:147
variant PredicateType::Equal 3 -> 2 in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/engine/predicates/predicate.rs:148
--- failure enum_tuple_variant_field_added: pub enum tuple variant field added ---
Description:
An enum's exhaustive tuple variant has a new field, which has to be included when constructing or matching on this variant.
ref: https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_tuple_variant_field_added.ron
Failed in:
field 1 of variant Reason::Eager in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/engine/cp/reason.rs:97
--- failure method_parameter_count_changed: pub method parameter count changed ---
Description:
A publicly-visible method now takes a different number of parameters, not counting the receiver (self) parameter.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#fn-change-arity
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/method_parameter_count_changed.ron
Failed in:
pumpkin_core::containers::SparseSet::new takes 2 parameters in /tmp/.tmp4QeO9L/pumpkin-core/src/containers/sparse_set.rs:64, but now takes 1 parameters in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/containers/sparse_set.rs:71
pumpkin_core::propagation::PropagationContext::post takes 3 parameters in /tmp/.tmp4QeO9L/pumpkin-core/src/propagation/contexts/propagation_context.rs:239, but now takes 2 parameters in /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs:238
--- failure trait_added_supertrait: non-sealed trait added new supertraits ---
Description:
A non-sealed trait added one or more supertraits, which breaks downstream implementations of the trait
ref: https://doc.rust-lang.org/cargo/reference/semver.html#generic-bounds-tighten
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/trait_added_supertrait.ron
Failed in:
trait pumpkin_core::branching::Brancher gained Debug in file /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/branching/brancher.rs:38
trait pumpkin_core::branching::value_selection::ValueSelector gained Debug in file /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/branching/value_selection/value_selector.rs:22
trait pumpkin_core::branching::variable_selection::VariableSelector gained Debug in file /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/branching/variable_selection/variable_selector.rs:19
trait pumpkin_core::branching::branchers::alternating::AlternatingStrategy gained Debug in file /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/branching/branchers/alternating/strategies/mod.rs:12
--- failure trait_method_added: pub trait method added ---
Description:
A non-sealed public trait added a new method without a default implementation, which breaks downstream implementations of the trait
ref: https://doc.rust-lang.org/cargo/reference/semver.html#trait-new-item-no-default
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/trait_method_added.ron
Failed in:
trait method pumpkin_core::propagation::ReadDomains::fixed_value in file /tmp/.tmpfQejrY/Pumpkin/pumpkin-crates/core/src/propagation/domains.rs:81
```
### ⚠ `pumpkin-checker` breaking changes
```text
--- failure enum_variant_missing: pub enum variant removed or renamed ---
Description:
A publicly-visible enum has at least one variant that is no longer available under its prior name. It may have been renamed or removed entirely.
ref: https://doc.rust-lang.org/cargo/reference/semver.html#item-remove
impl: https://github.com/obi1kenobi/cargo-semver-checks/tree/v0.48.0/src/lints/enum_variant_missing.ron
Failed in:
variant InvalidDeduction::InconsistentPremises, previously in file /tmp/.tmp4QeO9L/pumpkin-checker/src/deductions.rs:41
```
---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
commit b5787791db59c11cf281fea9de1bf4039f2dc6a7
Author: Imko Marijnissen
Date: Tue Jun 23 10:22:21 2026 +0200
chore: update minizinc image in dockerfile to 2026
commit 347fdb2269d76742d62b433a69acec3c1bf2f184
Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com>
Date: Tue Jun 23 10:18:40 2026 +0200
chore: add docker publish workflow (#472)
Co-authored-by: Maarten Flippo
commit caa594af74a6c612188f7a2efdca26a48b71ff59
Author: Imko Marijnissen
Date: Tue Jun 23 09:50:28 2026 +0200
fix: Release Plz Template
commit 7e284ba9d98848ddd16859c03a7a33979387426e
Author: Maarten Flippo
Date: Tue Jun 23 09:53:05 2026 +0200
chore: Use central version number for all pumpkin-* crates (#470)
commit 87847c361915dd55e94b61c034c08d317cfe5535
Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com>
Date: Tue Jun 23 09:39:18 2026 +0200
chore: Adding release-plz workflow (#469)
commit b0c1d7be01326468bd27b5b863a1c1b2eea07897
Author: Imko Marijnissen <50290518+ImkoMarijnissen@users.noreply.github.com>
Date: Tue Jun 23 09:38:49 2026 +0200
chore: add CPAIOR26 and ICAPS26 papers to README (#465)
commit 6e7385027aaf6e839467064338fc3cf93dde8d77
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue Jun 23 09:29:10 2026 +0200
chore(deps): bump pyo3 from 0.28.3 to 0.29.0 (#466)
Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.28.3 to 0.29.0.
Release notes
Sourced from pyo3's
releases.
PyO3 0.29.0
This release is a relatively large release with improvements across
many areas of PyO3's API.
Build and packaging changes
This release brings full support for Python 3.15 beta. We encourage
downstream projects to begin testing and distributing Python 3.15 beta
wheels so that the ecosystem can prepare for the 3.15 final release
later in the year.
Alongside Python 3.15 support comes support for its new
"abi3t" stable ABI which supports both free-threaded and
gil-enabled Python builds. For projects distributing stable ABI wheels,
we recommend distributing (for each OS/architecture) an abi3 wheel built
for your minimum supported Python version, a 3.14t version-specific
wheel for free-threaded Python 3.14, and an abi3t wheel to support
Python 3.15 (and future versions).
Support for Python 3.7 has been dropped. Support for Python 3.13t,
the first experimental free-threaded release of CPython, has also been
dropped. 3.14t (and soon 3.15t) is more stable, performant, and the
starting point for CPython's own declaration of "support" for
the free-threaded build.
The PyO3 build process (via the pyo3-build-config crate)
has been adjusted to reduce the cost of rebuilds when the environment
used to detect the Python interpreter changes;
pyo3-build-config and pyo3-macros will no
longer be rebuilt in such cases (although pyo3-ffi and
crates downstream of it still will be rebuilt). As a consequence the
pyo3_build_config APIs now require crates to have a direct
dependency on pyo3 or pyo3-ffi. We hope to
continue to reduce rebuild frequency and cost in a future PyO3
release.
Security updates
With the recent boom in AI-assisted security scanning, PyO3 has
inevitably had several correctness issues exposed by AI-assisted
scanning.
In particular, PyO3 0.29 fixes two security vulnerabilities we will
be releasing to the RustSec Advisory Database imminently:
- Missing
Sync bound on
PyCFunction::new_closure closures
- Possible out of bounds read in
BoundTupleIterator::nth_back and
BoundListIterator::nth_back
Any code using the above APIs is advised to update as soon as
possible.
This release also contains several other minor breaking changes to
close soundness holes uncovered by AI-assisted scanning. Our assessment
as maintainers was that, excluding the two vulnerability cases listed
above, these correctness issues would likely have crashed immediately
upon user testing rather than leading to attacker-exploitable pathways.
We nevertheless wanted to see them closed without the usual deprecation
cycle. These cases are noted in the migration guide.
Other major themes in this release
New in this release is a CLI in pyo3-introspection to
generate type stubs along with the experimental-inspect
feature. Downstream, maturin has also gained support to
generate type stubs using the feature. The feature is reaching a point
where substantial amount of type stubs can be generated automatically.
We would like to encourage users to begin using this feature and helping
us find what functionality is missing, with a hope we can declare its
API stable given sufficient feedback.
A substantial amount of effort has been invested in
pyo3-ffi as part of the process of extending it with 3.15's
new APIs. There have been many missing APIs from older Python versions
added. There have also been a number of fixes to incorrect definitions
(these are breaking changes, but also necessary for correctness); we
hope there will be far fewer such cases in the future due to more
comprehensive checking added to PyO3's CI. Finally, many private CPython
APIs (those with _Py underscore-named prefix) have been
removed from pyo3-ffi's public API.
In closing
There are also many other incremental improvements, bug fixes and
smaller features; full detail can be found in the CHANGELOG.
Please consult the migration guide for
help upgrading.
Thank you to everyone who contributed code, documentation, design
ideas, bug reports, and feedback. The following contributors' commits
are included in this release:
@Alc-Alc
@alex
@anuraaga
@BD103
@bschoenmaeckers
@Cheukting
@chirizxc
@ChristopherRabotin
@clin1234
@codeguru42
@davidhewitt
... (truncated)
Changelog
Sourced from pyo3's
changelog.
[0.29.0] - 2026-06-11
Packaging
- Support the new PEP 803 abi3t ABI with new
abi3t and
abi3t-py315 features. #5807
pyo3-macros-backend no longer depends on
pyo3-build-config. #5809
- Drop support for Python 3.13t (3.14t and above continue to be
supported; CPython declared free-threading supported starting with
Python 3.14). #5865
- Drop support for Python 3.7. #5912
- Extend range of supported versions of
hashbrown
optional dependency to include version 0.17. #5973
- Support Python 3.15.0b1. #6014
pyo3-ffi is now no_std. #6022
Added
- Add
PyErr::set_traceback to set the traceback of an
exception object. #5349
- Add
PyUnicodeDecodeError::new_err_from_utf8 to create a
PyErr from a str::Utf8Error. #5668
experimental-inspect: implement INPUT_TYPE
and OUTPUT_TYPE on optional third-party crate conversions.
#5770
experimental-inspect: include doc comments in generated
stubs. #5782
- Add
pyo3_build_config::PythonAbi,
pyo3_build_config::PythonAbiKind,
pyo3_build_config::PythonAbiBuilder,
pyo3_build_config::InterpreterConfig::target_abi, and
pyo3_build_config::InterpreterConfigBuilder::target_abi. #5807
- Add
Borrowed::get as an equivalent to
Bound::get and Py::get. #5849
- Add
PyFrame::new, PyTraceBack::new, and
PyFrameMethods::line_number. #5857
- Add
PyUntypedBuffer::obj to retrieve the Python object
owning the buffer. #5870
- Add
PyCapsule::new_with_value and
PyCapsule::new_with_value_and_destructor. #5881
- Add
PyErr::set_context and PyErr::context.
#5887
- Add a small CLI to
pyo3-introspection to generate
stubs. #5904
- Add
Python::version_str. #5921
- Add
TryFrom<&Bound<T>> for
PyRef<T>, PyRefMut<T>,
PyClassGuard<T> and
PyClassGuardMut<T>. #5922
- Add
From<&Bound<T>> for
Bound<T> and Py<T> #5922
- Add
PyDictMethods::set_default and
PyDictMethods::set_default_ref to allow atomically setting
default values in a PyDict. #5955
- add
PyFrameMethods::outer|code|var|builtins|globals|locals. #5967
- Add
From conversions for PyErr from
std::time::TryFromFloatSecsError,
std::time::SystemTimeError,
std::path::StripPrefixError,
std::env::JoinPathsError,
std::char::ParseCharError, and
std::char::CharTryFromError. #6001
- Add
pyo3_build_config::InterpreterConfigBuilder. #6034
- Add
PyCapsule::import_pointer #6066
- Add
PyClassGuardMapMut. #6073
- Expose
PyListMethods::get_item_unchecked,
PyTupleMethods::get_item_unchecked, and
PyTupleMethods::get_borrowed_item_unchecked on abi3. #6075
- Add
PyClassGuardMapSuper. #6104
- Add
PyClassGuard and PyClassGuardMut to
pyo3::prelude. #6112
- Add
Debug impls for PyClassGuard and
PyClassGuardMut. #6112
- Enable extending
PyDateTime, PyDate,
PyTime, PyDelta and PyTzInfo on
abi3 with python 3.12+. #6115
- Expose
PyFunction available on abi3. #6117
- FFI definitions:
- Added FFI definitions
PyUnstable_Object_IsUniquelyReferenced,
PyUnstable_Object_IsUniquelyReferencedTemporary,
PyUnstable_EnableTryIncref, and
PyUnstable_TryIncref. #5828
- Add FFI definitions
ffi::PyErr_GetHandledException and
ffi::PyErr_SetHandledException. #5887
- Add FFI definition
Py_HASH_SIPHASH13. #5891
- Add FFI definition
PyStructSequence_UnnamedField
constant on Python 3.9 and up (or 3.11 with abi3 features). #5892
- Add FFI definitions
PyUnstable_InterpreterFrame_GetCode,
PyUnstable_InterpreterFrame_GetLasti,
PyUnstable_InterpreterFrame_GetLine, and
PyUnstable_ExecutableKinds. #5932
- Add FFI definitions
PyMarshal_WriteLongToFile,
PyMarshal_WriteObjectToFile,
PyMarshal_ReadLongFromFile,
PyMarshal_ReadShortFromFile,
PyMarshal_ReadObjectFromFile, and
PyMarshal_ReadLastObjectFromFile. #5934
- Add FFI definitions
PyObject_GetAIter,
PyAIter_Check, PyMapping_HasKeyWithError,
PyMapping_HasKeyStringWithError,
PyMapping_GetOptionalItem,
PyMapping_GetOptionalItemString,
PySequence_ITEM, PySequence_Fast_GET_SIZE,
PySequence_Fast_GET_ITEM, and
PySequence_Fast_ITEMS. #5942
- Add FFI definition
compat::PyObject_HasAttrWithError.
#5944
- Add FFI definitions
PyDict_SetDefault,
PyDict_SetDefaultRef, PyDict_ContainsString,
PyDict_Pop, PyDict_PopString,
PyDict_ClearWatcher, PyDict_Watch,
PyDict_Unwatch, and PyFrozenDict_New. #5947
... (truncated)
Commits
0f90242
release: 0.29.0 (#6107)
cd128ed
doc: mention abi3t, python3t.dll, and abi3t_compat folder in FAQ (#6124)
7e2ef18
Avoid type checks in methods where CPython already guarantees the
received ty...
f930199
docs: additional detail in migration guide for 0.29 (#6123)
91ab0d1
Enable Windows abi3t tests (#6106)
fe0fdd5
add PyLong* API (3.14+) (#6016)
f41b1df
Hang when reattaching after detach during shutdown (#6085)
5ae66a8
Fix double import on RustPython (#6122)
ad4a510
PyFunction: enable some extra tests with abi3 (#6118)
c79ac0e
ci: Add test for minimum supported debug build of Python (#5852)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit 96e5064043f8fdb499f02b193b4c6c49475b509c
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu May 28 12:53:39 2026 +1000
chore(deps): bump log from 0.4.29 to 0.4.30 (#458)
Bumps [log](https://github.com/rust-lang/log) from 0.4.29 to 0.4.30.
Release notes
Sourced from log's
releases.
0.4.30
What's Changed
New Contributors
Full Changelog: https://github.com/rust-lang/log/compare/0.4.29...0.4.30
Notable Changes
Changelog
Sourced from log's
changelog.
[0.4.30] - 2026-05-21
What's Changed
New Contributors
Full Changelog: https://github.com/rust-lang/log/compare/0.4.29...0.4.30
Notable Changes
Commits
9c55760
Merge pull request #725
from rust-lang/cargo/0.4.30
d1acb05
update docs on current MSRV and note latest bump in changelog
5068293
prepare for 0.4.30 release
7ccd873
Merge pull request #724
from rust-lang/feat/net-to-value
923dfaa
fix up test cfgs
ecb7de8
gate net value impls on std
67bb4f6
run fmt
25f49fe
rework net type capturing
7087dcb
feat: impl ToValue for core::net types
67bc7e3
Merge pull request #723
from woodruffw-forks/ww/ci
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
commit b3fb89ccd3dddf8601a0ac98fb733a27a054c6ac
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu May 28 12:53:02 2026 +1000
chore(deps): bump wasm-bindgen-test from 0.3.71 to 0.3.72 (#459)
Bumps [wasm-bindgen-test](https://github.com/wasm-bindgen/wasm-bindgen)
from 0.3.71 to 0.3.72.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.cargo/config.toml | 3 -
.github/workflows/ci-python.yml | 10 +-
.github/workflows/ci.yml | 18 +-
.github/workflows/release-plz.yml | 93 ++
CITATION.bib | 38 +
Cargo.lock | 411 +++----
Cargo.toml | 1 +
README.md | 29 +
fzn-rs-derive/CHANGELOG.md | 6 +
fzn-rs-derive/Cargo.toml | 2 +-
fzn-rs/CHANGELOG.md | 7 +
fzn-rs/Cargo.toml | 4 +-
minizinc/Dockerfile | 4 +-
pumpkin-checker/CHANGELOG.md | 12 +
pumpkin-checker/Cargo.toml | 10 +-
pumpkin-crates/checking/CHANGELOG.md | 14 +
pumpkin-crates/checking/Cargo.toml | 2 +-
.../conflict-resolvers/CHANGELOG.md | 14 +
pumpkin-crates/conflict-resolvers/Cargo.toml | 6 +-
.../src/minimisers/semantic_minimiser.rs | 3 +-
.../src/resolvers/resolution_resolver.rs | 102 +-
pumpkin-crates/constraints/CHANGELOG.md | 6 +
pumpkin-crates/constraints/Cargo.toml | 6 +-
pumpkin-crates/core/CHANGELOG.md | 38 +
pumpkin-crates/core/Cargo.toml | 8 +-
pumpkin-crates/core/src/api/solver.rs | 4 +-
.../variable_selection/input_order.rs | 4 +
.../src/conflict_resolving/analysis_mode.rs | 286 +++++
.../conflict_analysis_context.rs | 55 +-
.../src/conflict_resolving/learned_nogood.rs | 75 +-
.../core/src/conflict_resolving/mod.rs | 2 +
.../core/src/containers/key_value_heap.rs | 4 +-
.../engine/constraint_satisfaction_solver.rs | 29 +-
.../core/src/engine/cp/assignments.rs | 78 ++
pumpkin-crates/core/src/engine/cp/reason.rs | 1 +
.../core/src/engine/cp/test_solver.rs | 3 +
.../core/src/engine/notifications/mod.rs | 18 +-
.../predicate_assignments.rs | 15 +
.../contexts/explanation_context.rs | 7 +
.../contexts/propagation_context.rs | 5 +
.../core/src/propagation/domains.rs | 17 +
.../core/src/propagation/propagator.rs | 1 +
.../propagators/nogoods/arena_allocator.rs | 6 +-
.../propagators/nogoods/learning_options.rs | 5 +
.../core/src/propagators/nogoods/mod.rs | 3 +
.../core/src/propagators/nogoods/nogood_id.rs | 10 +
.../propagators/nogoods/nogood_propagator.rs | 1053 ++++++++++++++---
.../propagators/nogoods/propagation_mode.rs | 452 +++++++
.../propagators/nogoods/semantic_minimiser.rs | 267 +++++
pumpkin-crates/propagators/CHANGELOG.md | 22 +
pumpkin-crates/propagators/Cargo.toml | 6 +-
pumpkin-macros/Cargo.toml | 2 +-
pumpkin-proof-processor/Cargo.toml | 11 +-
pumpkin-solver-py/Cargo.toml | 12 +-
pumpkin-solver/CHANGELOG.md | 29 +
pumpkin-solver/Cargo.toml | 12 +-
pumpkin-solver/src/bin/pumpkin-solver/main.rs | 74 +-
pumpkin-solver/tests/cnf/.gitignore | 1 +
.../tests/mzn_infeasible/.gitignore | 1 +
.../tests/mzn_optimization/.gitignore | 1 +
release-plz.toml | 1 -
61 files changed, 2829 insertions(+), 590 deletions(-)
create mode 100644 .github/workflows/release-plz.yml
create mode 100644 CITATION.bib
create mode 100644 pumpkin-crates/core/src/conflict_resolving/analysis_mode.rs
create mode 100644 pumpkin-crates/core/src/propagators/nogoods/propagation_mode.rs
create mode 100644 pumpkin-crates/core/src/propagators/nogoods/semantic_minimiser.rs
diff --git a/.cargo/config.toml b/.cargo/config.toml
index cb1739fa1..2e07606d5 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -1,5 +1,2 @@
-[build]
-rustflags = ["-Ctarget-cpu=native"]
-
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml
index 9451e5d23..6c605104c 100644
--- a/.github/workflows/ci-python.yml
+++ b/.github/workflows/ci-python.yml
@@ -38,7 +38,7 @@ jobs:
- runner: ubuntu-22.04
target: armv7
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -71,7 +71,7 @@ jobs:
- runner: ubuntu-22.04
target: armv7
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -99,7 +99,7 @@ jobs:
- runner: windows-latest
target: x86
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -127,7 +127,7 @@ jobs:
- runner: macos-latest
target: aarch64
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -147,7 +147,7 @@ jobs:
sdist:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Build sdist
uses: PyO3/maturin-action@v1
with:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 48a8e3ab6..69bbfd02e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,8 +15,8 @@ jobs:
name: Test Suite
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/cache@v5
+ - uses: actions/checkout@v7
+ - uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
@@ -32,7 +32,7 @@ jobs:
name: Test Suite for pumpkin-core in WebAssembly
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
@@ -48,7 +48,7 @@ jobs:
runner: [ ubuntu-latest, macos-latest, windows-latest ]
runs-on: ${{ matrix.runner }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: 3.x
@@ -65,8 +65,8 @@ jobs:
name: Documentation
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/cache@v5
+ - uses: actions/checkout@v7
+ - uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
@@ -82,8 +82,8 @@ jobs:
name: Code Style and Lints
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/cache@v5
+ - uses: actions/checkout@v7
+ - uses: actions/cache@v6
with:
path: |
~/.cargo/bin/
@@ -102,5 +102,5 @@ jobs:
name: Dependency Licensing
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- uses: EmbarkStudios/cargo-deny-action@v2
diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml
new file mode 100644
index 000000000..5dc9561ad
--- /dev/null
+++ b/.github/workflows/release-plz.yml
@@ -0,0 +1,93 @@
+name: Release-plz + Docker
+
+on:
+ push:
+ branches:
+ - main
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: consol-lab/pumpkin-solver
+
+jobs:
+ # Release unpublished packages.
+ release-plz-release:
+ name: Release-plz release
+ runs-on: ubuntu-latest
+ environment: crates
+ permissions:
+ id-token: write
+ contents: write
+ pull-requests: read
+ packages: write
+ attestations: write
+ steps:
+ - &checkout
+ name: Checkout repository
+ uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - &install-rust
+ name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+ - name: Run release-plz
+ uses: release-plz/action@v0.5
+ id: release_plz
+ with:
+ command: release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Log in to the Container registry
+ if: contains(steps.release_plz.outputs.packages_released, 'pumpkin-solver')
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Extract metadata (tags, labels) for Docker
+ if: contains(steps.release_plz.outputs.packages_released, 'pumpkin-solver')
+ id: meta
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=match,pattern=pumpkin-solver-(v.*),group=1
+ type=raw,value=latest
+ - name: Build and push Docker image
+ if: contains(steps.release_plz.outputs.packages_released, 'pumpkin-solver')
+ id: push
+ uses: docker/build-push-action@cb941d0b895b09c17fa011d41c411b33c752cf28
+ with:
+ context: .
+ file: minizinc/Dockerfile
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ - name: Generate artifact attestation
+ if: contains(steps.release_plz.outputs.packages_released, 'pumpkin-solver')
+ uses: actions/attest@v4
+ with:
+ subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ subject-digest: ${{ steps.push.outputs.digest }}
+ push-to-registry: true
+
+ # Create a PR with the new versions and changelog, preparing the next release.
+ release-plz-pr:
+ name: Release-plz PR
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ concurrency:
+ group: release-plz-${{ github.ref }}
+ cancel-in-progress: false
+ steps:
+ - *checkout
+ - *install-rust
+ - name: Run release-plz
+ uses: release-plz/action@v0.5
+ with:
+ command: release-pr
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CITATION.bib b/CITATION.bib
new file mode 100644
index 000000000..a150ac39c
--- /dev/null
+++ b/CITATION.bib
@@ -0,0 +1,38 @@
+@inproceedings{marijnissen_et_al:LIPIcs.CP.2026.42,
+ author = {Marijnissen, Imko and Flippo, Maarten and Demirovi\'{c}, Emir},
+ title = {{From Literals to Atomic Constraints: Generalising Conflict-Driven Clause Learning for Constraint Programming}},
+ booktitle = {32nd International Conference on Principles and Practice of Constraint Programming (CP 2026)},
+ pages = {42:1--42:21},
+ series = {Leibniz International Proceedings in Informatics (LIPIcs)},
+ isbn = {978-3-95977-432-1},
+ issn = {1868-8969},
+ year = {2026},
+ volume = {379},
+ editor = {Beldiceanu, Nicolas},
+ publisher = {Schloss Dagstuhl -- Leibniz-Zentrum f{\"u}r Informatik},
+ address = {Dagstuhl, Germany},
+ url = {https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2026.42},
+ urn = {urn:nbn:de:0030-drops-266744},
+ doi = {10.4230/LIPIcs.CP.2026.42},
+ annote = {Keywords: LCG, CP, CDCL, Lazy Literal, Conflict Analysis, Nogood Propagation},
+}
+
+% Please also include the following citation if you are using the proof-logging capabilities of Pumpkin!
+@InProceedings{flippo_et_al:LIPIcs.CP.2024.11,
+ author = {Flippo, Maarten and Sidorov, Konstantin and Marijnissen, Imko and Smits, Jeff and Demirovi\'{c}, Emir},
+ title = {{A Multi-Stage Proof Logging Framework to Certify the Correctness of CP Solvers}},
+ booktitle = {30th International Conference on Principles and Practice of Constraint Programming (CP 2024)},
+ pages = {11:1--11:20},
+ series = {Leibniz International Proceedings in Informatics (LIPIcs)},
+ ISBN = {978-3-95977-336-2},
+ ISSN = {1868-8969},
+ year = {2024},
+ volume = {307},
+ editor = {Shaw, Paul},
+ publisher = {Schloss Dagstuhl -- Leibniz-Zentrum f{\"u}r Informatik},
+ address = {Dagstuhl, Germany},
+ URL = {https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2024.11},
+ URN = {urn:nbn:de:0030-drops-206969},
+ doi = {10.4230/LIPIcs.CP.2024.11},
+ annote = {Keywords: proof logging, formal verification, constraint programming}
+}
diff --git a/Cargo.lock b/Cargo.lock
index 4c7235f20..c7f011429 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -75,15 +75,15 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.102"
+version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "ar_archive_writer"
-version = "0.5.1"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348"
dependencies = [
"object",
]
@@ -116,9 +116,9 @@ dependencies = [
[[package]]
name = "autocfg"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitfield"
@@ -153,9 +153,9 @@ dependencies = [
[[package]]
name = "bitflags"
-version = "2.11.1"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bstr"
@@ -170,9 +170,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.20.2"
+version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cast"
@@ -182,9 +182,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.62"
+version = "1.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
+checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -223,9 +223,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.6.1"
+version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011"
dependencies = [
"clap_builder",
"clap_derive",
@@ -243,9 +243,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.6.0"
+version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
@@ -338,6 +338,38 @@ dependencies = [
"syn",
]
+[[package]]
+name = "defmt"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
+dependencies = [
+ "bitflags",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
+dependencies = [
+ "defmt-parser",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror",
+]
+
[[package]]
name = "derive_more"
version = "2.1.1"
@@ -395,9 +427,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "enum-map"
@@ -442,9 +474,9 @@ dependencies = [
[[package]]
name = "env_filter"
-version = "1.0.1"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
+checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
"log",
"regex",
@@ -452,9 +484,9 @@ dependencies = [
[[package]]
name = "env_logger"
-version = "0.11.10"
+version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
+checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
dependencies = [
"anstream",
"anstyle",
@@ -552,7 +584,7 @@ dependencies = [
[[package]]
name = "fzn-rs"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"chumsky",
"fzn-rs-derive",
@@ -561,7 +593,7 @@ dependencies = [
[[package]]
name = "fzn-rs-derive"
-version = "0.1.0"
+version = "0.1.1"
dependencies = [
"convert_case",
"fzn-rs",
@@ -572,17 +604,15 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.4.2"
+version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"rand_core",
- "wasip2",
- "wasip3",
"wasm-bindgen",
]
@@ -599,9 +629,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.17.0"
+version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
@@ -609,12 +639,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
-[[package]]
-name = "id-arena"
-version = "2.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
-
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -628,9 +652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
- "hashbrown 0.17.0",
- "serde",
- "serde_core",
+ "hashbrown 0.17.1",
]
[[package]]
@@ -641,9 +663,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
-version = "0.14.0"
+version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
dependencies = [
"either",
]
@@ -656,10 +678,11 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jiff"
-version = "0.2.24"
+version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d"
+checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46"
dependencies = [
+ "defmt",
"jiff-static",
"log",
"portable-atomic",
@@ -669,9 +692,9 @@ dependencies = [
[[package]]
name = "jiff-static"
-version = "0.2.24"
+version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7"
+checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f"
dependencies = [
"proc-macro2",
"quote",
@@ -680,22 +703,15 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.98"
+version = "0.3.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
dependencies = [
"cfg-if",
"futures-util",
- "once_cell",
"wasm-bindgen",
]
-[[package]]
-name = "leb128fmt"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
-
[[package]]
name = "libc"
version = "0.2.186"
@@ -710,15 +726,15 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "log"
-version = "0.4.29"
+version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "memchr"
-version = "2.8.0"
+version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "minicov"
@@ -899,12 +915,24 @@ dependencies = [
]
[[package]]
-name = "prettyplease"
-version = "0.2.37"
+name = "proc-macro-error-attr2"
+version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
"syn",
]
@@ -929,7 +957,7 @@ dependencies = [
[[package]]
name = "pumpkin-checker"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"anyhow",
"assert_cmd",
@@ -947,7 +975,7 @@ dependencies = [
[[package]]
name = "pumpkin-checking"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"dyn-clone",
"fnv",
@@ -956,14 +984,16 @@ dependencies = [
[[package]]
name = "pumpkin-conflict-resolvers"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
+ "itertools",
+ "log",
"pumpkin-core",
]
[[package]]
name = "pumpkin-constraints"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"pumpkin-core",
"pumpkin-propagators",
@@ -971,7 +1001,7 @@ dependencies = [
[[package]]
name = "pumpkin-core"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"bitfield",
"bitfield-struct",
@@ -1010,7 +1040,7 @@ dependencies = [
[[package]]
name = "pumpkin-proof-processor"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"anyhow",
"assert_cmd",
@@ -1031,7 +1061,7 @@ dependencies = [
[[package]]
name = "pumpkin-propagators"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"bitfield-struct",
"clap",
@@ -1043,7 +1073,7 @@ dependencies = [
[[package]]
name = "pumpkin-solver"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"cc",
"clap",
@@ -1065,7 +1095,7 @@ dependencies = [
[[package]]
name = "pumpkin-solver-py"
-version = "0.3.0"
+version = "0.4.0"
dependencies = [
"pumpkin-conflict-resolvers",
"pumpkin-constraints",
@@ -1076,9 +1106,9 @@ dependencies = [
[[package]]
name = "pyo3"
-version = "0.28.3"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
+checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
dependencies = [
"libc",
"once_cell",
@@ -1090,18 +1120,18 @@ dependencies = [
[[package]]
name = "pyo3-build-config"
-version = "0.28.3"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
+checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
dependencies = [
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
-version = "0.28.3"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
+checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
dependencies = [
"libc",
"pyo3-build-config",
@@ -1109,9 +1139,9 @@ dependencies = [
[[package]]
name = "pyo3-macros"
-version = "0.28.3"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
+checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
@@ -1121,22 +1151,21 @@ dependencies = [
[[package]]
name = "pyo3-macros-backend"
-version = "0.28.3"
+version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
+checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
dependencies = [
"heck",
"proc-macro2",
- "pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
-version = "1.0.45"
+version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
@@ -1149,9 +1178,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
-version = "0.10.1"
+version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom",
@@ -1166,9 +1195,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "regex"
-version = "1.12.3"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
@@ -1178,9 +1207,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.14"
+version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
dependencies = [
"aho-corasick",
"memchr",
@@ -1189,9 +1218,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.10"
+version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustc_version"
@@ -1255,9 +1284,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.149"
+version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
@@ -1268,9 +1297,9 @@ dependencies = [
[[package]]
name = "shlex"
-version = "1.3.0"
+version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-hook"
@@ -1331,9 +1360,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
-version = "2.0.117"
+version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
@@ -1380,15 +1409,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
-version = "1.13.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
-
-[[package]]
-name = "unicode-xid"
-version = "0.2.6"
+version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "utf8parse"
@@ -1415,29 +1438,11 @@ dependencies = [
"winapi-util",
]
-[[package]]
-name = "wasip2"
-version = "1.0.3+wasi-0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
-dependencies = [
- "wit-bindgen 0.57.1",
-]
-
-[[package]]
-name = "wasip3"
-version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
-dependencies = [
- "wit-bindgen 0.51.0",
-]
-
[[package]]
name = "wasm-bindgen"
-version = "0.2.121"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if",
"once_cell",
@@ -1448,9 +1453,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.71"
+version = "0.4.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -1458,9 +1463,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.121"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1468,9 +1473,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.121"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -1481,18 +1486,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.121"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
-version = "0.3.71"
+version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0"
+checksum = "2a0d555ca874445df8d314f94f5c948a4e74e5418f332c89f660a3d8310a96f4"
dependencies = [
"async-trait",
"cast",
@@ -1512,9 +1517,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
-version = "0.3.71"
+version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb"
+checksum = "94eb68555b95bcea5e8cf4abe280b529049479fa995bfc23734af96a6aedc120"
dependencies = [
"proc-macro2",
"quote",
@@ -1523,43 +1528,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-shared"
-version = "0.2.121"
+version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527"
-
-[[package]]
-name = "wasm-encoder"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
-dependencies = [
- "leb128fmt",
- "wasmparser",
-]
-
-[[package]]
-name = "wasm-metadata"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
-dependencies = [
- "anyhow",
- "indexmap",
- "wasm-encoder",
- "wasmparser",
-]
-
-[[package]]
-name = "wasmparser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
-dependencies = [
- "bitflags",
- "hashbrown 0.15.5",
- "indexmap",
- "semver",
-]
+checksum = "c31d56021e873866c968588ed85ccdf56db5c426e44afdb4618c39895104b920"
[[package]]
name = "web-time"
@@ -1604,100 +1575,6 @@ dependencies = [
"memchr",
]
-[[package]]
-name = "wit-bindgen"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
-dependencies = [
- "wit-bindgen-rust-macro",
-]
-
-[[package]]
-name = "wit-bindgen"
-version = "0.57.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
-
-[[package]]
-name = "wit-bindgen-core"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
-dependencies = [
- "anyhow",
- "heck",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-bindgen-rust"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
-dependencies = [
- "anyhow",
- "heck",
- "indexmap",
- "prettyplease",
- "syn",
- "wasm-metadata",
- "wit-bindgen-core",
- "wit-component",
-]
-
-[[package]]
-name = "wit-bindgen-rust-macro"
-version = "0.51.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
-dependencies = [
- "anyhow",
- "prettyplease",
- "proc-macro2",
- "quote",
- "syn",
- "wit-bindgen-core",
- "wit-bindgen-rust",
-]
-
-[[package]]
-name = "wit-component"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
-dependencies = [
- "anyhow",
- "bitflags",
- "indexmap",
- "log",
- "serde",
- "serde_derive",
- "serde_json",
- "wasm-encoder",
- "wasm-metadata",
- "wasmparser",
- "wit-parser",
-]
-
-[[package]]
-name = "wit-parser"
-version = "0.244.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
-dependencies = [
- "anyhow",
- "id-arena",
- "indexmap",
- "log",
- "semver",
- "serde",
- "serde_derive",
- "serde_json",
- "unicode-xid",
- "wasmparser",
-]
-
[[package]]
name = "zmij"
version = "1.0.21"
diff --git a/Cargo.toml b/Cargo.toml
index 1be6fc8e0..a703ab7b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,6 +26,7 @@ authors = [
"Konstantin Sidorov",
"Jeff Smits"
] # Ordered alphabetically based on last name!
+version = "0.4.0"
[patch.crates-io]
fnv = { git = "https://github.com/servo/rust-fnv", branch = "main" }
diff --git a/README.md b/README.md
index 24aac4e00..2a50e5bde 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,28 @@ We are actively developing Pumpkin and would be happy to hear from you should yo
# Citing
Please cite Pumpkin using the following citation:
```
+@inproceedings{marijnissen_et_al:LIPIcs.CP.2026.42,
+ author = {Marijnissen, Imko and Flippo, Maarten and Demirovi\'{c}, Emir},
+ title = {{From Literals to Atomic Constraints: Generalising Conflict-Driven Clause Learning for Constraint Programming}},
+ booktitle = {32nd International Conference on Principles and Practice of Constraint Programming (CP 2026)},
+ pages = {42:1--42:21},
+ series = {Leibniz International Proceedings in Informatics (LIPIcs)},
+ isbn = {978-3-95977-432-1},
+ issn = {1868-8969},
+ year = {2026},
+ volume = {379},
+ editor = {Beldiceanu, Nicolas},
+ publisher = {Schloss Dagstuhl -- Leibniz-Zentrum f{\"u}r Informatik},
+ address = {Dagstuhl, Germany},
+ url = {https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2026.42},
+ urn = {urn:nbn:de:0030-drops-266744},
+ doi = {10.4230/LIPIcs.CP.2026.42},
+ annote = {Keywords: LCG, CP, CDCL, Lazy Literal, Conflict Analysis, Nogood Propagation},
+}
+```
+
+If you are using the proof-logging capabilities of Pumpkin, then please **additionally** include the following citation:
+```
@InProceedings{flippo_et_al:LIPIcs.CP.2024.11,
author = {Flippo, Maarten and Sidorov, Konstantin and Marijnissen, Imko and Smits, Jeff and Demirovi\'{c}, Emir},
title = {{A Multi-Stage Proof Logging Framework to Certify the Correctness of CP Solvers}},
@@ -62,6 +84,13 @@ Please cite Pumpkin using the following citation:
- R. Baauw, M. Flippo, and E. Demirović, [‘Conflict Analysis Based on Cutting-Planes for Constraint Programming’](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2025.4), in 31st International Conference on Principles and Practice of Constraint Programming (CP 2025), 2025, vol. 340, p. 4:1-4:19.
- K. Sidorov, I. Marijnissen, and E. Demirović, [‘Unite and Lead: Finding Disjunctive Cliques for Scheduling Problems’](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2025.35), in 31st International Conference on Principles and Practice of Constraint Programming (CP 2025), 2025, vol. 340, p. 35:1-35:24.
- I. Bleukx, M. Flippo, B. Bogaerts, E. Demirović, and T. Guns, [‘Using Certifying Constraint Solvers for Generating Step-wise Explanations’](https://ojs.aaai.org/index.php/AAAI/article/view/38432), Proceedings of the AAAI Conference on Artificial Intelligence, vol. 40, no. 17, pp. 14192–14200, Mar. 2026.
+- M. Flippo, P. J. Stuckey, and E. Demirović, [‘Resolution Meets Cutting Planes: Introducing Hypercube Linear Resolution’](https://link.springer.com/chapter/10.1007/978-3-032-27242-3_10), in Integration of Constraint Programming, Artificial Intelligence, and Operations Research, 2026, pp. 155–172.
+- I. Marijnissen, J. C. Beck, E. Demirović, and R. Kuroiwa, [‘Domain-Independent Dynamic Programming with Constraint Propagation’](https://ojs.aaai.org/index.php/ICAPS/article/view/42826), Proceedings of the International Conference on Automated Planning and Scheduling, vol. 36, no. 1, pp. 171–180, June 2026.
+- I. Marijnissen, M. Flippo, and E. Demirović, [‘From Literals to Atomic Constraints: Generalising Conflict-Driven Clause Learning for Constraint Programming’](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2026.42), in 32nd International Conference on Principles and Practice of Constraint Programming (CP 2026), 2026, vol. 379, p. 42:1-42:21.
+- M. Flippo, K. Sidorov, T. ten Brink, C. Pit-Claudel, and E. Demirović, [‘Formally Verified Certification of Constraint Programming Proofs’](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2026.24), in 32nd International Conference on Principles and Practice of Constraint Programming (CP 2026), 2026, vol. 379, p. 24:1-24:23.
+- K. Sidorov, [‘On Inferring Cumulative Constraints’](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2026.50), in 32nd International Conference on Principles and Practice of Constraint Programming (CP 2026), 2026, vol. 379, p. 50:1-50:23.
+- I. Bleukx, P. J. Stuckey, and T. Guns, [‘Towards Step-Wise Explanations of Large Search Trees’](https://drops.dagstuhl.de/entities/document/10.4230/LIPIcs.CP.2026.62), in 32nd International Conference on Principles and Practice of Constraint Programming (CP 2026), 2026, vol. 379, p. 62:1-62:11.
+
# Usage
diff --git a/fzn-rs-derive/CHANGELOG.md b/fzn-rs-derive/CHANGELOG.md
index 854966dae..4e55e3627 100644
--- a/fzn-rs-derive/CHANGELOG.md
+++ b/fzn-rs-derive/CHANGELOG.md
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.1.1](https://github.com/ConSol-Lab/Pumpkin/compare/fzn-rs-derive-v0.1.0...fzn-rs-derive-v0.1.1) - 2026-06-23
+
+### Other
+
+- *(deps)* bump convert_case from 0.8.0 to 0.11.0 ([#428](https://github.com/ConSol-Lab/Pumpkin/pull/428))
+
## [0.1.0](https://github.com/consol-lab/pumpkin/releases/tag/fzn-rs-derive-v0.1.0) - 2026-02-10
### Added
diff --git a/fzn-rs-derive/Cargo.toml b/fzn-rs-derive/Cargo.toml
index df88c76dd..3eb9dffd3 100644
--- a/fzn-rs-derive/Cargo.toml
+++ b/fzn-rs-derive/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fzn-rs-derive"
-version = "0.1.0"
+version = "0.1.1"
description = "Derive macros for fzn-rs."
repository.workspace = true
edition.workspace = true
diff --git a/fzn-rs/CHANGELOG.md b/fzn-rs/CHANGELOG.md
index 4bdeec303..086fb9167 100644
--- a/fzn-rs/CHANGELOG.md
+++ b/fzn-rs/CHANGELOG.md
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.1.1](https://github.com/ConSol-Lab/Pumpkin/compare/fzn-rs-v0.1.0...fzn-rs-v0.1.1) - 2026-06-23
+
+### Other
+
+- *(deps)* bump chumsky from 0.12.0 to 0.13.0 ([#442](https://github.com/ConSol-Lab/Pumpkin/pull/442))
+- *(deps)* bump chumsky from 0.10.1 to 0.12.0 ([#420](https://github.com/ConSol-Lab/Pumpkin/pull/420))
+
## [0.1.0](https://github.com/consol-lab/pumpkin/releases/tag/fzn-rs-v0.1.0) - 2026-02-10
### Added
diff --git a/fzn-rs/Cargo.toml b/fzn-rs/Cargo.toml
index 2eb31ab23..cba31312d 100644
--- a/fzn-rs/Cargo.toml
+++ b/fzn-rs/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "fzn-rs"
-version = "0.1.0"
+version = "0.1.1"
description = "A FlatZinc parser written in Rust."
repository.workspace = true
edition.workspace = true
@@ -10,7 +10,7 @@ authors.workspace = true
[dependencies]
chumsky = { version = "0.13.0" }
thiserror = "2.0.12"
-fzn-rs-derive = { version = "0.1.0", path = "../fzn-rs-derive/" }
+fzn-rs-derive = { version = "0.1.1", path = "../fzn-rs-derive/" }
[lints]
workspace = true
diff --git a/minizinc/Dockerfile b/minizinc/Dockerfile
index 7952ce446..8de9972a4 100644
--- a/minizinc/Dockerfile
+++ b/minizinc/Dockerfile
@@ -16,7 +16,7 @@ WORKDIR /pumpkin-src
RUN cargo build -p pumpkin-solver --release
# Create the MiniZinc image
-FROM minizinc/mznc2025:latest
+FROM minizinc/mznc2026:latest
# Copy the solver executable
COPY --from=builder /pumpkin-src/target/release/pumpkin-solver /pumpkin/pumpkin-solver
@@ -25,7 +25,7 @@ COPY --from=builder /pumpkin-src/target/release/pumpkin-solver /pumpkin/pumpkin-
COPY --from=builder /pumpkin-src/minizinc/ /pumpkin/
# Change the path to the solver executable to be the correct one.
-RUN sed -i 's/..\/target\/release\/pumpkin-solver/pumpkin-solver/' /pumpkin/pumpkin.msc
+RUN sed -i 's/..\/target\/release\/pumpkin-solver/.\/pumpkin-solver/' /pumpkin/pumpkin.msc
# Add Pumpkin to the MiniZinc search path and set it as the default solver.
#
diff --git a/pumpkin-checker/CHANGELOG.md b/pumpkin-checker/CHANGELOG.md
index 37a899774..ad42d6825 100644
--- a/pumpkin-checker/CHANGELOG.md
+++ b/pumpkin-checker/CHANGELOG.md
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-checker-v0.3.0...pumpkin-checker-v0.4.0) - 2026-06-23
+
+### Added
+
+- *(pumpkin-solver)* Check derived nogoods during search ([#373](https://github.com/ConSol-Lab/Pumpkin/pull/373))
+- *(pumpkin-proof-processor)* Introduce the proof processor in the main branch ([#371](https://github.com/ConSol-Lab/Pumpkin/pull/371))
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+- *(deps)* bump assert_cmd from 2.2.1 to 2.2.2 ([#445](https://github.com/ConSol-Lab/Pumpkin/pull/445))
+
## [0.1.0](https://github.com/consol-lab/pumpkin/releases/tag/pumpkin-checker-v0.1.0) - 2026-02-10
### Added
diff --git a/pumpkin-checker/Cargo.toml b/pumpkin-checker/Cargo.toml
index 9bdfc1065..dd882dac3 100644
--- a/pumpkin-checker/Cargo.toml
+++ b/pumpkin-checker/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "pumpkin-checker"
-version = "0.3.0"
description = "A proof-checker for DRCP proofs generated for FlatZinc models."
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
@@ -17,14 +17,14 @@ exclude = [
]
[dependencies]
-pumpkin-core = { version = "0.3.0", path = "../pumpkin-crates/core/" }
-pumpkin-checking = { version = "0.3.0", path = "../pumpkin-crates/checking/" }
-pumpkin-propagators = { version = "0.3.0", path = "../pumpkin-crates/propagators/" }
+pumpkin-core = { version = "0.4.0", path = "../pumpkin-crates/core/" }
+pumpkin-checking = { version = "0.4.0", path = "../pumpkin-crates/checking/" }
+pumpkin-propagators = { version = "0.4.0", path = "../pumpkin-crates/propagators/" }
anyhow = "1.0.99"
clap = { version = "4.5.47", features = ["derive"] }
drcp-format = { version = "0.3.1", path = "../drcp-format" }
flate2 = "1.1.2"
-fzn-rs = { version = "0.1.0", path = "../fzn-rs" }
+fzn-rs = { version = "0.1.1", path = "../fzn-rs" }
thiserror = "2.0.16"
fnv = "1.0.7"
derive_more = { version = "2.1.1", features = ["from"] }
diff --git a/pumpkin-crates/checking/CHANGELOG.md b/pumpkin-crates/checking/CHANGELOG.md
index 4af1c4bd4..25327c823 100644
--- a/pumpkin-crates/checking/CHANGELOG.md
+++ b/pumpkin-crates/checking/CHANGELOG.md
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-checking-v0.3.0...pumpkin-checking-v0.4.0) - 2026-06-23
+
+### Added
+
+- *(pumpkin-solver)* Check derived nogoods during search ([#373](https://github.com/ConSol-Lab/Pumpkin/pull/373))
+
+### Fixed
+
+- Off-by-one error in is_true of VariableState ([#389](https://github.com/ConSol-Lab/Pumpkin/pull/389))
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+
## [0.3.0](https://github.com/consol-lab/pumpkin/releases/tag/pumpkin-checking-v0.3.0) - 2026-02-10
### Added
diff --git a/pumpkin-crates/checking/Cargo.toml b/pumpkin-crates/checking/Cargo.toml
index 2bda5514a..4638ad280 100644
--- a/pumpkin-crates/checking/Cargo.toml
+++ b/pumpkin-crates/checking/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "pumpkin-checking"
-version = "0.3.0"
description = "Types used by both pumpkin-core and pumpkin-checker"
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
diff --git a/pumpkin-crates/conflict-resolvers/CHANGELOG.md b/pumpkin-crates/conflict-resolvers/CHANGELOG.md
index a11785ec6..1b89a9fcf 100644
--- a/pumpkin-crates/conflict-resolvers/CHANGELOG.md
+++ b/pumpkin-crates/conflict-resolvers/CHANGELOG.md
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-conflict-resolvers-v0.3.0...pumpkin-conflict-resolvers-v0.4.0) - 2026-06-23
+
+### Added
+
+- *(pumpkin-solver)* Check derived nogoods during search ([#373](https://github.com/ConSol-Lab/Pumpkin/pull/373))
+
+### Fixed
+
+- *(pumpkin-core)* Creation and Insertion in Sparse Set ([#395](https://github.com/ConSol-Lab/Pumpkin/pull/395))
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+
## [0.3.0](https://github.com/consol-lab/pumpkin/releases/tag/pumpkin-conflict-resolvers-v0.3.0) - 2026-02-10
### Added
diff --git a/pumpkin-crates/conflict-resolvers/Cargo.toml b/pumpkin-crates/conflict-resolvers/Cargo.toml
index de3438062..fefd283fb 100644
--- a/pumpkin-crates/conflict-resolvers/Cargo.toml
+++ b/pumpkin-crates/conflict-resolvers/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pumpkin-conflict-resolvers"
-version = "0.3.0"
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
@@ -11,4 +11,6 @@ description = "The conflict resolvers of the Pumpkin constraint programming solv
workspace = true
[dependencies]
-pumpkin-core = { version = "0.3.0", path = "../core" }
+itertools = "0.15.0"
+log = "0.4.27"
+pumpkin-core = { version = "0.4.0", path = "../core" }
diff --git a/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs b/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs
index 6bf99af68..d6c1da9c0 100644
--- a/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs
+++ b/pumpkin-crates/conflict-resolvers/src/minimisers/semantic_minimiser.rs
@@ -9,6 +9,7 @@ use pumpkin_core::predicate;
use pumpkin_core::predicates::Predicate;
use pumpkin_core::predicates::PredicateType;
use pumpkin_core::propagation::ReadDomains;
+use pumpkin_core::results::ProblemSolution;
use pumpkin_core::statistics::moving_averages::CumulativeMovingAverage;
use pumpkin_core::statistics::moving_averages::MovingAverage;
use pumpkin_core::variables::DomainId;
@@ -138,7 +139,7 @@ impl SemanticMinimiser {
fn accommodate(&mut self, context: &ConflictAnalysisContext) {
assert!(self.domains.len() == self.original_domains.len());
- while (self.domains.len() as u32) < context.number_of_domains() {
+ while (self.domains.len() as u32) < context.num_domains() as u32 {
let domain_id = DomainId::new(self.domains.len() as u32);
let lower_bound = context.initial_lower_bound(domain_id);
let upper_bound = context.initial_upper_bound(domain_id);
diff --git a/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs b/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs
index cd2c763f8..a1a352129 100644
--- a/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs
+++ b/pumpkin-crates/conflict-resolvers/src/resolvers/resolution_resolver.rs
@@ -1,8 +1,9 @@
use pumpkin_core::asserts::pumpkin_assert_advanced;
use pumpkin_core::asserts::pumpkin_assert_moderate;
-use pumpkin_core::asserts::pumpkin_assert_simple;
+use pumpkin_core::conflict_resolving::AnalysisMode;
use pumpkin_core::conflict_resolving::ConflictAnalysisContext;
use pumpkin_core::conflict_resolving::ConflictResolver;
+use pumpkin_core::containers::HashMap;
use pumpkin_core::containers::KeyValueHeap;
use pumpkin_core::containers::StorageKey;
use pumpkin_core::create_statistics_struct;
@@ -16,6 +17,7 @@ use pumpkin_core::statistics::Statistic;
use pumpkin_core::statistics::StatisticLogger;
use pumpkin_core::statistics::moving_averages::CumulativeMovingAverage;
use pumpkin_core::statistics::moving_averages::MovingAverage;
+use pumpkin_core::variables::DomainId;
use crate::minimisers::NogoodMinimiser;
use crate::minimisers::RecursiveMinimiser;
@@ -28,9 +30,7 @@ use crate::minimisers::SemanticMinimiser;
/// in the solver. This new nogood is added as a constraint to the solver, and the solver
/// backtracks to the decision level at which the new constraint propagates.
///
-/// The [`ResolutionResolver`] can be used in two [`AnalysisMode`]s:
-/// - [`AnalysisMode::OneUIP`] - Resolves until finding the unit implication point.
-/// - [AnalysisMode::AllDecision] - Resolves until the learned nogood contains only decisions.
+/// The [`ResolutionResolver`] learns the nogoods specified by the provided [`AnalysisMode`].
///
/// For an in-depth explanation and overview of CDCL and UIP, see \[1\].
///
@@ -51,19 +51,31 @@ pub struct ResolutionResolver {
/// Note that this structure may contain duplicates which are removed at the end by semantic
/// minimisation.
processed_nogood_predicates: Vec,
- /// Whether the resolver employs 1-UIP or all-decision learning.
+ /// The type of learning that the resolver employs (e.g., 1UIP, All-decision).
mode: AnalysisMode,
/// Re-usable buffer which reasons are written into.
reason_buffer: Vec,
/// Computes the LBD for nogoods.
lbd_helper: Lbd,
+ /// A helper for keeping track of how many [`Predicate`]s concerning a specific [`DomainId`]
+ /// are present in the working nogood.
+ ///
+ /// This is used when determining when to stop resolving when using CPIP learning (see
+ /// [`AnalysisMode::CPIP`]).
+ unique_variable_helper: HashMap,
- /// A minimiser which recursively determines whether a predicate is redundant in the nogood
+ /// A minimiser which recursively determines whether a predicate is redundant in the nogood.
recursive_minimiser: RecursiveMinimiser,
+ /// A minimiser which determines whether a predicate is redundant in the nogood based on its
+ /// semantic meaning.
semantic_minimiser: SemanticMinimiser,
+ /// The statistics of the learned nogoods.
statistics: LearnedNogoodStatistics,
+ /// Whether nogood minimisation should be applied.
+ ///
+ /// Note that semantic minimisation is always applied to remove duplicates.
should_minimise: bool,
}
@@ -86,20 +98,15 @@ create_statistics_struct!(
average_backtrack_amount: CumulativeMovingAverage,
/// The average literal-block distance (LBD) metric for newly added learned nogoods
average_lbd: CumulativeMovingAverage,
-});
+ /// The average number of predicates which describe the domain of the propagating variable when
+ /// using CPIP learning.
+ average_number_of_predicates_describing_domain_cpip: CumulativeMovingAverage,
+ /// The number of nogoods which have more than one predicate concerning the propagating variable (i.e., CPIP nogoods).
+ num_cpip_nogood_learned: usize,
+ /// The number of nogoods which one predicate concerning the propagating variable.
+ num_regular_nogood_learned: usize,
-#[derive(Debug, Clone, Copy, Default)]
-/// Determines which type of learning is performed by the [`ResolutionResolver`].
-pub enum AnalysisMode {
- #[default]
- /// Standard conflict analysis which returns as soon as the first unit implication point is
- /// found (i.e. when a nogood is created which only contains a single predicate from the
- /// current decision level)
- OneUIP,
- /// An alternative to 1-UIP which stops as soon as the learned nogood only creates decision
- /// predicates.
- AllDecision,
-}
+});
impl ConflictResolver for ResolutionResolver {
fn resolve_conflict(&mut self, context: &mut ConflictAnalysisContext) {
@@ -117,8 +124,11 @@ impl ConflictResolver for ResolutionResolver {
.average_learned_nogood_length
.add_term(self.processed_nogood_predicates.len() as u64);
- let backtrack_level =
- context.process_learned_nogood(self.processed_nogood_predicates.clone(), lbd);
+ let backtrack_level = context.process_learned_nogood(
+ self.processed_nogood_predicates.clone(),
+ lbd,
+ self.mode.uses_cpip(),
+ );
self.statistics
.average_backtrack_amount
@@ -146,6 +156,7 @@ impl ResolutionResolver {
semantic_minimiser: Default::default(),
statistics: Default::default(),
should_minimise,
+ unique_variable_helper: Default::default(),
}
}
@@ -177,12 +188,11 @@ impl ResolutionResolver {
// When posting the decision [x = v], it gets decomposed into two decisions ([x >= v] & [x
// <= v]). In this case there will be two predicates left from the current decision
// level, and both will be decisions. This is accounted for below.
- while {
- match self.mode {
- AnalysisMode::OneUIP => self.to_process_heap.num_nonremoved_elements() > 1,
- AnalysisMode::AllDecision => self.to_process_heap.num_nonremoved_elements() > 0,
- }
- } {
+ while self.mode.should_continue_resolving(
+ &self.to_process_heap,
+ &mut self.predicate_id_generator,
+ &mut self.unique_variable_helper,
+ ) {
// Replace the predicate from the nogood that has been assigned last on the trail.
//
// This is done in two steps:
@@ -215,6 +225,7 @@ impl ResolutionResolver {
self.processed_nogood_predicates.clear();
self.predicate_id_generator.clear();
self.to_process_heap.clear();
+ self.unique_variable_helper.clear();
}
/// Add the predicate to the current conflict nogood if we know it needs to be added.
@@ -247,10 +258,7 @@ impl ResolutionResolver {
// All-decision Learning
// If the variables are not decisions then we want to potentially add them to the heap,
// otherwise we add it to the decision predicates which have been discovered previously
- else if match mode {
- AnalysisMode::OneUIP => dec_level == context.get_checkpoint(),
- AnalysisMode::AllDecision => !context.is_decision_predicate(predicate),
- } {
+ else if mode.predicate_should_be_processed(predicate, dec_level, context) {
let predicate_id = self.predicate_id_generator.get_id(predicate);
// The first time we encounter the predicate, we initialise its value in the
// heap.
@@ -312,6 +320,7 @@ impl ResolutionResolver {
self.to_process_heap.restore_key(predicate_id);
self.to_process_heap
.increment(predicate_id, heap_value as u32);
+ mode.add_predicate_to_nogood(predicate, &mut self.unique_variable_helper);
pumpkin_assert_moderate!(
*self.to_process_heap.get_value(predicate_id) == heap_value.try_into().unwrap(),
@@ -327,24 +336,31 @@ impl ResolutionResolver {
fn pop_predicate_from_conflict_nogood(&mut self) -> Predicate {
let next_predicate_id = self.to_process_heap.pop_max().unwrap();
- self.predicate_id_generator.get_predicate(next_predicate_id)
+ let predicate = self.predicate_id_generator.get_predicate(next_predicate_id);
+ self.mode
+ .remove_predicate_from_nogood(predicate, &mut self.unique_variable_helper);
+ predicate
}
fn extract_final_nogood(&mut self, context: &mut ConflictAnalysisContext) {
// The final nogood is composed of the predicates encountered from the lower decision
- // levels, plus the predicate remaining in the heap.
+ // levels, plus the predicate(s) remaining in the heap.
+
+ // Depending on what mode we are in, we first remove the elements which are remaining in
+ // the heap.
+ let num_removed = self.mode.remove_final_predicates(
+ &mut self.to_process_heap,
+ &mut self.predicate_id_generator,
+ &mut self.processed_nogood_predicates,
+ );
- // First we obtain a semantically minimised nogood.
- //
- // We reuse the vector with lower decision levels for simplicity.
- if self.to_process_heap.num_nonremoved_elements() > 0 {
- let last_predicate = self.pop_predicate_from_conflict_nogood();
- self.processed_nogood_predicates.push(last_predicate);
+ self.statistics
+ .average_number_of_predicates_describing_domain_cpip
+ .add_term(num_removed);
+ if num_removed == 1 {
+ self.statistics.num_regular_nogood_learned += 1;
} else {
- pumpkin_assert_simple!(
- matches!(self.mode, AnalysisMode::AllDecision),
- "If the heap is empty when extracting the final nogood then we should be performing all decision learning"
- )
+ self.statistics.num_cpip_nogood_learned += 1;
}
// First we minimise the nogood using semantic minimisation to remove duplicates but we
diff --git a/pumpkin-crates/constraints/CHANGELOG.md b/pumpkin-crates/constraints/CHANGELOG.md
index ee9b027c8..7e724ee5c 100644
--- a/pumpkin-crates/constraints/CHANGELOG.md
+++ b/pumpkin-crates/constraints/CHANGELOG.md
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-constraints-v0.3.0...pumpkin-constraints-v0.4.0) - 2026-06-23
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+
## [0.3.0](https://github.com/consol-lab/pumpkin/releases/tag/pumpkin-constraints-v0.3.0) - 2026-02-10
### Added
diff --git a/pumpkin-crates/constraints/Cargo.toml b/pumpkin-crates/constraints/Cargo.toml
index 6713b2d66..7763519fa 100644
--- a/pumpkin-crates/constraints/Cargo.toml
+++ b/pumpkin-crates/constraints/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pumpkin-constraints"
-version = "0.3.0"
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
@@ -11,8 +11,8 @@ description = "The constraints of the Pumpkin constraint programming solver."
workspace = true
[dependencies]
-pumpkin-core = { version = "0.3.0", path = "../core" }
-pumpkin-propagators = { version = "0.3.0", path="../propagators"}
+pumpkin-core = { version = "0.4.0", path = "../core" }
+pumpkin-propagators = { version = "0.4.0", path="../propagators"}
[features]
clap = ["pumpkin-core/clap", "pumpkin-propagators/clap"]
diff --git a/pumpkin-crates/core/CHANGELOG.md b/pumpkin-crates/core/CHANGELOG.md
index f2e20c1d1..a222e33ab 100644
--- a/pumpkin-crates/core/CHANGELOG.md
+++ b/pumpkin-crates/core/CHANGELOG.md
@@ -1,5 +1,43 @@
# Changelog
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-core-v0.3.0...pumpkin-core-v0.4.0) - 2026-06-23
+
+### Added
+
+- *(pumpkin-core)* Define predicate order and add Predicate::implies ([#426](https://github.com/ConSol-Lab/Pumpkin/pull/426))
+- *(pumpkin-core)* Brancher implements Debug ([#409](https://github.com/ConSol-Lab/Pumpkin/pull/409))
+- *(pumpkin-solver)* Check derived nogoods during search ([#373](https://github.com/ConSol-Lab/Pumpkin/pull/373))
+- Adding IntegerVariable for i32 ([#388](https://github.com/ConSol-Lab/Pumpkin/pull/388))
+- *(pumpkin-proof-processor)* Introduce the proof processor in the main branch ([#371](https://github.com/ConSol-Lab/Pumpkin/pull/371))
+
+### Fixed
+
+- *(pumpkin-core)* Creation and Insertion in Sparse Set ([#395](https://github.com/ConSol-Lab/Pumpkin/pull/395))
+- *(pumpkin-solver)* Update packages and fix duplicate dependencies ([#412](https://github.com/ConSol-Lab/Pumpkin/pull/412))
+- *(pumpkin-core)* Upgrade to rand 0.10 ([#411](https://github.com/ConSol-Lab/Pumpkin/pull/411))
+- *(pumpkin-solver)* Correctly detect when to write zipped proofs ([#372](https://github.com/ConSol-Lab/Pumpkin/pull/372))
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+- *(deps)* bump log from 0.4.29 to 0.4.30 ([#458](https://github.com/ConSol-Lab/Pumpkin/pull/458))
+- *(deps)* bump enumset from 1.1.12 to 1.1.13 ([#452](https://github.com/ConSol-Lab/Pumpkin/pull/452))
+- *(deps)* bump enumset from 1.1.11 to 1.1.12 ([#446](https://github.com/ConSol-Lab/Pumpkin/pull/446))
+- *(deps)* bump enumset from 1.1.10 to 1.1.11 ([#441](https://github.com/ConSol-Lab/Pumpkin/pull/441))
+- *(pumpkin-solver)* Remove vergen as a dependency ([#438](https://github.com/ConSol-Lab/Pumpkin/pull/438))
+- Do not collect unnecessarily into solution in `Solver` ([#439](https://github.com/ConSol-Lab/Pumpkin/pull/439))
+- *(pumpkin-core)* Attach `InferenceCode` instead of `Predicate` ([#433](https://github.com/ConSol-Lab/Pumpkin/pull/433))
+- *(deps)* bump convert_case from 0.8.0 to 0.11.0 ([#428](https://github.com/ConSol-Lab/Pumpkin/pull/428))
+- *(deps)* bump downcast-rs from 1.2.1 to 2.0.2 ([#429](https://github.com/ConSol-Lab/Pumpkin/pull/429))
+- *(deps)* bump bitfield from 0.14.0 to 0.19.4 ([#430](https://github.com/ConSol-Lab/Pumpkin/pull/430))
+- *(deps)* bump itertools from 0.13.0 to 0.14.0 ([#427](https://github.com/ConSol-Lab/Pumpkin/pull/427))
+- *(deps)* bump bitfield-struct from 0.9.5 to 0.13.0 ([#421](https://github.com/ConSol-Lab/Pumpkin/pull/421))
+- *(pumpkin-core)* Clarify get_propagation_reason does not clear buffer ([#402](https://github.com/ConSol-Lab/Pumpkin/pull/402))
+- *(pumpkin-core)* Describe unnamed variables in state ([#403](https://github.com/ConSol-Lab/Pumpkin/pull/403))
+- *(pumpkin-core)* Cleanup creation of propagator conflict ([#399](https://github.com/ConSol-Lab/Pumpkin/pull/399))
+- add utility method fixed value and replace is_fixed wherever possible ([#393](https://github.com/ConSol-Lab/Pumpkin/pull/393))
+- clippy warning ([#376](https://github.com/ConSol-Lab/Pumpkin/pull/376))
+
## [0.3.0](https://github.com/consol-lab/pumpkin/compare/pumpkin-core-v0.2.2...pumpkin-core-v0.3.0) - 2026-02-10
### Added
diff --git a/pumpkin-crates/core/Cargo.toml b/pumpkin-crates/core/Cargo.toml
index 7774eb67d..f0da66ff1 100644
--- a/pumpkin-crates/core/Cargo.toml
+++ b/pumpkin-crates/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pumpkin-core"
-version = "0.3.0"
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
@@ -11,9 +11,9 @@ description = "The core of the Pumpkin constraint programming solver."
workspace = true
[dependencies]
-pumpkin-checking = { version = "0.3.0", path = "../checking" }
+pumpkin-checking = { version = "0.4.0", path = "../checking" }
thiserror = "2.0.12"
-log = "0.4.17"
+log = "0.4.30"
bitfield = "0.19.4"
enumset = "1.1.13"
fnv = "1.0.7" # We require features which are on the `main` branch of the repository but are not on crates.io
@@ -22,7 +22,7 @@ once_cell = "1.19.0"
downcast-rs = "2.0.2"
drcp-format = { version = "0.3.1", path = "../../drcp-format" }
convert_case = "0.11.0"
-itertools = "0.14.0"
+itertools = "0.15.0"
bitfield-struct = "0.13.0"
num = "0.4.3"
enum-map = "2.7.3"
diff --git a/pumpkin-crates/core/src/api/solver.rs b/pumpkin-crates/core/src/api/solver.rs
index 4e53fbe49..997c5d7f9 100644
--- a/pumpkin-crates/core/src/api/solver.rs
+++ b/pumpkin-crates/core/src/api/solver.rs
@@ -611,9 +611,7 @@ impl Solver {
/// A brancher which makes use of VSIDS \[1\] and solution-based phase saving (both adapted for CP).
///
/// If VSIDS does not contain any (unfixed) predicates then it will default to the
-/// [`IndependentVariableValueBrancher`] using [`RandomSelector`] for variable selection
-/// (over the variables in the order in which they were defined) and [`RandomSplitter`] for
-/// value selection.
+/// [`IndependentVariableValueBrancher`].
///
/// # Bibliography
/// \[1\] M. W. Moskewicz, C. F. Madigan, Y. Zhao, L. Zhang, and S. Malik, ‘Chaff: Engineering an
diff --git a/pumpkin-crates/core/src/branching/variable_selection/input_order.rs b/pumpkin-crates/core/src/branching/variable_selection/input_order.rs
index 59928a46f..a8ccadf0c 100644
--- a/pumpkin-crates/core/src/branching/variable_selection/input_order.rs
+++ b/pumpkin-crates/core/src/branching/variable_selection/input_order.rs
@@ -22,6 +22,10 @@ impl InputOrder {
variables: variables.to_vec(),
}
}
+
+ pub fn add_domain(&mut self, var: Var) {
+ self.variables.push(var)
+ }
}
impl VariableSelector for InputOrder {
diff --git a/pumpkin-crates/core/src/conflict_resolving/analysis_mode.rs b/pumpkin-crates/core/src/conflict_resolving/analysis_mode.rs
new file mode 100644
index 000000000..750c84f6e
--- /dev/null
+++ b/pumpkin-crates/core/src/conflict_resolving/analysis_mode.rs
@@ -0,0 +1,286 @@
+use std::collections::hash_map::Entry;
+
+use itertools::Itertools;
+
+use crate::basic_types::PredicateId;
+use crate::conflict_resolving::ConflictAnalysisContext;
+use crate::containers::HashMap;
+use crate::containers::KeyValueHeap;
+use crate::predicates::Predicate;
+use crate::predicates::PredicateIdGenerator;
+use crate::predicates::PredicateType;
+use crate::propagation::ReadDomains;
+use crate::pumpkin_assert_moderate;
+use crate::pumpkin_assert_simple;
+use crate::variables::DomainId;
+
+/// Determines the different type of resolution-based analysis modes that are supported.
+#[derive(Debug, Clone, Copy)]
+pub enum AnalysisMode {
+ /// Standard conflict analysis which returns as soon as the first unit implication point is
+ /// found (i.e. when a nogood is created which only contains a single predicate from the
+ /// current decision level).
+ OneUIP,
+ /// An alternative to 1-UIP which stops as soon as the learned nogood only creates decision
+ /// predicates.
+ AllDecision,
+ /// Learns CPIP nogoods \[1\] (i.e., nogoods which only have predicates from the current
+ /// decision level which reason over a single variable when learning).
+ ///
+ /// # Bibliography
+ /// - \[1\] I. Marijnissen, M. Flippo, and E. Demirović, ‘From Literals to Atomic Constraints:
+ /// Generalising Conflict-Driven Clause Learning for Constraint Programming’, in 32nd
+ /// International Conference on Principles and Practice of Constraint Programming (CP 2026),
+ /// 2026, vol. 379, p. 42:1-42:21.
+ CPIP,
+ /// Learns CPIP nogoods \[1\] but rather than stopping at the first point where extended nogood
+ /// propagation can take place, it stops when extended nogood propagation can adjust a bound
+ /// upon learning.
+ ///
+ /// # Bibliography
+ /// - \[1\] I. Marijnissen, M. Flippo, and E. Demirović, ‘From Literals to Atomic Constraints:
+ /// Generalising Conflict-Driven Clause Learning for Constraint Programming’, in 32nd
+ /// International Conference on Principles and Practice of Constraint Programming (CP 2026),
+ /// 2026, vol. 379, p. 42:1-42:21.
+ BoundsCPIP,
+}
+
+impl AnalysisMode {
+ /// Returns whether the provided [`Predicate`] (which became true at `decision_level`) should
+ /// be processed further.
+ ///
+ /// If false is returned, then the provided [`Predicate`] is added directly to the nogood.
+ pub fn predicate_should_be_processed(
+ &self,
+ predicate: Predicate,
+ decision_level: usize,
+ context: &ConflictAnalysisContext,
+ ) -> bool {
+ match self {
+ AnalysisMode::OneUIP | AnalysisMode::CPIP | AnalysisMode::BoundsCPIP => {
+ // The predicate should be processed further if it is not from the current decision
+ // level
+ decision_level == context.get_checkpoint()
+ }
+ AnalysisMode::AllDecision => {
+ // The predicate should be processed further if it is not a decision
+ !context.is_decision_predicate(predicate)
+ }
+ }
+ }
+
+ /// Returns whether to continue resolving.
+ pub fn should_continue_resolving(
+ &self,
+ to_process_heap: &KeyValueHeap,
+ predicate_id_generator: &mut PredicateIdGenerator,
+ unique_variable_helper: &mut HashMap,
+ ) -> bool {
+ match self {
+ AnalysisMode::OneUIP => {
+ // We wait until there is only a single element from the current decision level
+ // left.
+ to_process_heap.num_nonremoved_elements() > 1
+ }
+ AnalysisMode::AllDecision => {
+ // We wait until there are only decisions left.
+ to_process_heap.num_nonremoved_elements() > 0
+ }
+ AnalysisMode::CPIP => {
+ // We wait until there are only elements over a single variable left.
+ unique_variable_helper.len() > 1
+ }
+ AnalysisMode::BoundsCPIP => {
+ // We wait until extended nogood propagation can propagate a bound.
+ //
+ // Firstly, there should be only elements over a single element.
+ // Secondly, one of the following should hold:
+ // - There is a lower-bound present but no upper-bound OR there is an upper-bound
+ // present but no lower-bound
+ // - There are only holes present
+ // - There is an equality present (would necessarily lead to a single predicate due
+ // to semantic minimisation)
+ let present_domain_ids = to_process_heap
+ .keys()
+ .map(|predicate_id| {
+ predicate_id_generator
+ .get_predicate(predicate_id)
+ .get_domain()
+ })
+ .unique()
+ .collect::>();
+ if present_domain_ids.len() > 1 {
+ true
+ } else {
+ // We calculate the number of predicate types from the current decision
+ // level (note that they are necessarily over a
+ // single variable) to determine when bound
+ // propagation can take place.
+ let (mut lower_bounds, mut upper_bounds, mut _disequalities, mut equalities) =
+ (0, 0, 0, 0);
+ for predicate_id in to_process_heap.keys() {
+ let predicate = predicate_id_generator.get_predicate(predicate_id);
+ match predicate.get_predicate_type() {
+ PredicateType::LowerBound => lower_bounds += 1,
+ PredicateType::NotEqual => _disequalities += 1,
+ PredicateType::Equal => equalities += 1,
+ PredicateType::UpperBound => upper_bounds += 1,
+ }
+ }
+ // We return true if we cannot propagate any bounds
+ //
+ // We can propagate bounds in the following situations:
+ // - There is a lower-bound present but no upper-bound OR there is an
+ // upper-bound present but no lower-bound
+ // - There are only holes present
+ // - There is an equality present (would necessarily lead to a single
+ // predicate due to semantic minimisation)
+ !((lower_bounds > 0 && upper_bounds == 0)
+ || (lower_bounds == 0 && upper_bounds > 0)
+ || (lower_bounds == 0 && upper_bounds == 0)
+ || equalities > 0)
+ }
+ }
+ }
+ }
+
+ /// Removes the left-over predicates in `to_process_heap` after
+ /// [`AnalysisMode::should_continue_resolving`] returned false (e.g., when finding the 1UIP, the
+ /// `to_process_heap` will contain the asserting predicate) and returns the number of
+ /// elements which were left in the `to_process_heap`.
+ pub fn remove_final_predicates(
+ &self,
+ to_process_heap: &mut KeyValueHeap,
+ predicate_id_generator: &mut PredicateIdGenerator,
+ processed_nogood_predicates: &mut Vec,
+ ) -> usize {
+ let num_removed = to_process_heap.num_nonremoved_elements();
+
+ match self {
+ AnalysisMode::CPIP | AnalysisMode::BoundsCPIP => {
+ // When using extended UIP, we need to ensure that all of the remaining predicates
+ // are added to the domain.
+ pumpkin_assert_simple!(
+ to_process_heap.num_nonremoved_elements() > 0,
+ "There should be at least one element in the final nogood"
+ );
+ pumpkin_assert_moderate!(
+ to_process_heap
+ .keys()
+ .map(|predicate_id| predicate_id_generator
+ .get_predicate(predicate_id)
+ .get_domain())
+ .unique()
+ .count()
+ == 1,
+ "There should be only one variable in the final nogood from teh current decision level"
+ );
+
+ let propagating_domain = predicate_id_generator
+ .get_predicate(*to_process_heap.peek_max().unwrap().0)
+ .get_domain();
+
+ // We need to add all of the remaining predicates to the nogood; due to the way in
+ // which the extended UIP is calculated, this could be multiple elements.
+ while to_process_heap.num_nonremoved_elements() > 0 {
+ let predicate = Self::pop_predicate_from_conflict_nogood(
+ to_process_heap,
+ predicate_id_generator,
+ );
+ pumpkin_assert_simple!(predicate.get_domain() == propagating_domain);
+ processed_nogood_predicates.push(predicate);
+ }
+ }
+ AnalysisMode::OneUIP | AnalysisMode::AllDecision => {
+ if to_process_heap.num_nonremoved_elements() > 0 {
+ let last_predicate = Self::pop_predicate_from_conflict_nogood(
+ to_process_heap,
+ predicate_id_generator,
+ );
+ processed_nogood_predicates.push(last_predicate);
+ } else {
+ pumpkin_assert_simple!(
+ matches!(self, AnalysisMode::AllDecision),
+ "If the heap is empty when extracting the final nogood then we should be performing all decision learning"
+ )
+ }
+ }
+ }
+
+ num_removed
+ }
+
+ /// Removes the element with the highest value from `to_process_heap` and returns the
+ /// corresponding [`Predicate`].
+ pub fn pop_predicate_from_conflict_nogood(
+ to_process_heap: &mut KeyValueHeap,
+ predicate_id_generator: &mut PredicateIdGenerator,
+ ) -> Predicate {
+ let next_predicate_id = to_process_heap.pop_max().unwrap();
+ predicate_id_generator.get_predicate(next_predicate_id)
+ }
+
+ /// Whether the analysis mode learns CPIP nogoods.
+ pub fn uses_cpip(&self) -> bool {
+ matches!(self, AnalysisMode::CPIP | AnalysisMode::BoundsCPIP)
+ }
+
+ /// Called whenever a [`Predicate`] is added to the nogood by the resolution resolver.
+ ///
+ /// A helper is passed which contains how many times a [`DomainId`] appears in the current
+ /// nogood.
+ pub fn add_predicate_to_nogood(
+ &self,
+ predicate: Predicate,
+ unique_variable_helper: &mut HashMap,
+ ) {
+ match self {
+ AnalysisMode::CPIP | AnalysisMode::BoundsCPIP => {
+ // We get the current count for the domain, or insert it if it does not exist
+ let entry = unique_variable_helper
+ .entry(predicate.get_domain())
+ .or_default();
+
+ *entry += 1
+ }
+ AnalysisMode::OneUIP | AnalysisMode::AllDecision => {}
+ }
+ }
+
+ /// Called whenever a [`Predicate`] is removed from the nogood by the resolution resolver.
+ ///
+ /// A helper is passed which contains how many times a [`DomainId`] appears in the current
+ /// nogood.
+ pub fn remove_predicate_from_nogood(
+ &self,
+ predicate: Predicate,
+ unique_variable_helper: &mut HashMap,
+ ) {
+ match self {
+ AnalysisMode::CPIP | AnalysisMode::BoundsCPIP => {
+ // First, we find the entry
+ let entry = unique_variable_helper.entry(predicate.get_domain());
+
+ match entry {
+ Entry::Occupied(mut occupied_entry) => {
+ let value = occupied_entry.get_mut();
+
+ pumpkin_assert_simple!(*value > 0);
+
+ if *value == 1 {
+ // We remove the entry in its entirety if the count ever reaches 0.
+ let _ = occupied_entry.remove();
+ } else {
+ // Otherwise, we simply reduce the count by 1.
+ *value -= 1;
+ }
+ }
+ Entry::Vacant(_) => {
+ panic!("Whne removing a predicate from a nogood, it should exist.")
+ }
+ }
+ }
+ AnalysisMode::OneUIP | AnalysisMode::AllDecision => {}
+ }
+ }
+}
diff --git a/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs b/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs
index ad5f02016..ffebd61e5 100644
--- a/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs
+++ b/pumpkin-crates/core/src/conflict_resolving/conflict_analysis_context.rs
@@ -269,6 +269,7 @@ impl ConflictAnalysisContext<'_> {
&mut self,
learned_nogood_predicates: Vec,
lbd: u32,
+ uses_cpip: bool,
) -> usize {
// important to notify about the conflict _before_ backtracking removes literals from
// the trail -> although in the current version this does nothing but notify that a
@@ -276,7 +277,8 @@ impl ConflictAnalysisContext<'_> {
self.restart_strategy
.notify_conflict(lbd, self.state.assignments.get_pruned_value_count());
- let learned_nogood = LearnedNogood::create_from_vec(learned_nogood_predicates, self);
+ let learned_nogood =
+ LearnedNogood::create_from_vec(learned_nogood_predicates, self, uses_cpip);
let constraint_tag = self.log_deduction(learned_nogood.predicates.iter().copied());
let inference_code = InferenceCode::new(constraint_tag, NogoodLabel);
@@ -355,27 +357,36 @@ impl ConflictAnalysisContext<'_> {
//
// It could be that the predicate is implied by another unit nogood
- let unit_ic = unit_nogood_inference_codes
- .get(&predicate)
- .or_else(|| {
- // It could be the case that we attempt to get the reason for the predicate
- // [x >= v] but that the corresponding unit nogood idea is the one for the
- // predicate [x == v]
- let domain_id = predicate.get_domain();
- let right_hand_side = predicate.get_right_hand_side();
-
- unit_nogood_inference_codes.get(&predicate!(domain_id == right_hand_side))
- })
- .expect("Expected to be able to retrieve step id for unit nogood");
-
- let _ = proof_log.log_inference(
- &mut state.constraint_tags,
- unit_ic.clone(),
- [],
- Some(predicate),
- &state.variable_names,
- &state.assignments,
- );
+ let unit_ic = unit_nogood_inference_codes.get(&predicate).or_else(|| {
+ // It could be the case that we attempt to get the reason for the predicate
+ // [x >= v] but that the corresponding unit nogood idea is the one for the
+ // predicate [x == v]
+ let domain_id = predicate.get_domain();
+ let right_hand_side = predicate.get_right_hand_side();
+
+ unit_nogood_inference_codes.get(&predicate!(domain_id == right_hand_side))
+ });
+
+ if let Some(unit_ic) = unit_ic {
+ let _ = proof_log.log_inference(
+ &mut state.constraint_tags,
+ unit_ic.clone(),
+ [],
+ Some(predicate),
+ &state.variable_names,
+ &state.assignments,
+ );
+ } else {
+ // Otherwise we log the inference which was used to derive the nogood
+ let _ = proof_log.log_inference(
+ &mut state.constraint_tags,
+ ic.clone(),
+ reason_buffer.as_ref().iter().copied(),
+ Some(predicate),
+ &state.variable_names,
+ &state.assignments,
+ );
+ }
} else {
// Otherwise we log the inference which was used to derive the nogood
let _ = proof_log.log_inference(
diff --git a/pumpkin-crates/core/src/conflict_resolving/learned_nogood.rs b/pumpkin-crates/core/src/conflict_resolving/learned_nogood.rs
index fd0dc2fbc..11583a572 100644
--- a/pumpkin-crates/core/src/conflict_resolving/learned_nogood.rs
+++ b/pumpkin-crates/core/src/conflict_resolving/learned_nogood.rs
@@ -6,10 +6,10 @@ use crate::predicates::Predicate;
/// A structure which stores a learned nogood
///
/// There are two assumptions:
-/// - The asserting [`Predicate`] (i.e. the predicate of the current decision level) is placed at
-/// the `0`th index of [`LearnedNogood::predicates`].
-/// - A [`Predicate`] from the second-highest decision level is placed at the `1`st index of
-/// [`LearnedNogood::predicates`].
+/// - The asserting literal (i.e. the literal of the current decision level) is placed at the `0`th
+/// index of [`LearnedNogood::literals`].
+/// - A literal from the second-highest decision level is placed at the `1`st index of
+/// [`LearnedNogood::literals`].
#[derive(Clone, Debug)]
pub(crate) struct LearnedNogood {
pub(crate) predicates: Vec,
@@ -29,13 +29,45 @@ impl LearnedNogood {
///
/// This method automatically ensures that the invariants of nogoods hold; see [`LearnedNogood`]
/// for more details on these invariants.
+ ///
+ /// When using [`ConflictResolverType::ExtendedUIP`], it is ensured that the [`Predicate`] at
+ /// the the 1-st position is the one with the highest decision level concerning a different
+ /// domain than the propagating domain
pub(crate) fn create_from_vec(
mut clean_nogood: Vec,
context: &ConflictAnalysisContext,
+ uses_cpip: bool,
) -> Self {
+ if clean_nogood.is_empty() {
+ return Self {
+ predicates: vec![],
+ backtrack_level: 0,
+ };
+ }
+
// We perform a linear scan to maintain the two invariants:
// - The predicate from the current decision level is placed at index 0
// - The predicate from the highest decision level below the current is placed at index 1
+
+ // If we are performing extended conflict analysis, then we find the domain that is
+ // currently propagating; this is to correctly put the predicates in the right place.
+ let propagating_domain = if uses_cpip {
+ Some(
+ clean_nogood
+ .iter()
+ .find(|predicate| {
+ context
+ .state
+ .get_checkpoint_for_predicate(**predicate)
+ .is_some_and(|checkpoint| checkpoint == context.state.get_checkpoint())
+ })
+ .copied()
+ .expect("Expected at least one element to be from the current decision level"),
+ )
+ } else {
+ None
+ };
+
let mut index = 1;
let mut highest_level_below_current = 0;
while index < clean_nogood.len() {
@@ -47,8 +79,17 @@ impl LearnedNogood {
if dl == context.state.get_checkpoint() {
clean_nogood.swap(0, index);
- index -= 1;
- } else if dl > highest_level_below_current {
+ if propagating_domain.is_none()
+ || clean_nogood[0].get_domain() != clean_nogood[index].get_domain()
+ {
+ index -= 1;
+ }
+ } else if dl > highest_level_below_current
+ && (propagating_domain.is_none()
+ || predicate.get_domain() != propagating_domain.unwrap().get_domain())
+ {
+ // The extra condition is to ensure that the first two predicates in the nogood
+ // are over different variables.
highest_level_below_current = dl;
clean_nogood.swap(1, index);
}
@@ -58,7 +99,7 @@ impl LearnedNogood {
// The second highest decision level predicate is at position one.
// This is the backjump level.
- let backjump_level = if clean_nogood.len() > 1 {
+ let mut backjump_level = if clean_nogood.len() > 1 {
context
.state
.get_checkpoint_for_predicate(clean_nogood[1])
@@ -68,6 +109,26 @@ impl LearnedNogood {
0
};
+ if clean_nogood.len() > 1 && propagating_domain.is_some() {
+ // In this case, the order of the predicates is irrelevant (for now)
+ //
+ // We first find the propagating domain
+ let propagating_domain = clean_nogood[0].get_domain();
+ // Then we calculate the backjump level as the level of the highest present predicate
+ // in the nogood which is not concerning the propagating domain
+ backjump_level = clean_nogood
+ .iter()
+ .filter(|predicate| predicate.get_domain() != propagating_domain)
+ .map(|predicate| {
+ context
+ .state
+ .get_checkpoint_for_predicate(*predicate)
+ .unwrap()
+ })
+ .max()
+ .unwrap_or(0);
+ }
+
Self {
predicates: clean_nogood,
backtrack_level: backjump_level,
diff --git a/pumpkin-crates/core/src/conflict_resolving/mod.rs b/pumpkin-crates/core/src/conflict_resolving/mod.rs
index 9829b52f0..cd798158a 100644
--- a/pumpkin-crates/core/src/conflict_resolving/mod.rs
+++ b/pumpkin-crates/core/src/conflict_resolving/mod.rs
@@ -1,9 +1,11 @@
//! Contains algorithms for conflict analysis, core extraction, and clause minimisation.
//! The algorithms use resolution and implement the 1uip and all decision literal learning schemes
+mod analysis_mode;
mod conflict_analysis_context;
mod conflict_resolver;
mod learned_nogood;
+pub use analysis_mode::AnalysisMode;
pub use conflict_analysis_context::ConflictAnalysisContext;
pub use conflict_resolver::ConflictResolver;
pub(crate) use learned_nogood::LearnedNogood;
diff --git a/pumpkin-crates/core/src/containers/key_value_heap.rs b/pumpkin-crates/core/src/containers/key_value_heap.rs
index 12334728f..6de026025 100644
--- a/pumpkin-crates/core/src/containers/key_value_heap.rs
+++ b/pumpkin-crates/core/src/containers/key_value_heap.rs
@@ -62,7 +62,7 @@ where
/// Get the keys in the heap.
///
/// The order in which the keys are yielded is unspecified.
- pub(crate) fn keys(&self) -> impl Iterator- + '_ {
+ pub fn keys(&self) -> impl Iterator
- + '_ {
self.map_position_to_key[..self.end_position]
.iter()
.copied()
@@ -72,7 +72,7 @@ where
/// this does not delete the key (see [`KeyValueHeap::pop_max`] to get and delete).
///
/// The time-complexity of this operation is O(1)
- pub(crate) fn peek_max(&self) -> Option<(&Key, &Value)> {
+ pub fn peek_max(&self) -> Option<(&Key, &Value)> {
if self.has_no_nonremoved_elements() {
None
} else {
diff --git a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs
index 36e33a1aa..eabeed1ff 100644
--- a/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs
+++ b/pumpkin-crates/core/src/engine/constraint_satisfaction_solver.rs
@@ -50,6 +50,7 @@ use crate::propagation::store::PropagatorHandle;
use crate::propagators::nogoods::NogoodChecker;
use crate::propagators::nogoods::NogoodPropagator;
use crate::propagators::nogoods::NogoodPropagatorConstructor;
+use crate::propagators::nogoods::PropagationMode;
use crate::pumpkin_assert_eq_simple;
use crate::pumpkin_assert_moderate;
use crate::pumpkin_assert_ne_moderate;
@@ -143,8 +144,22 @@ pub enum CoreExtractionResult {
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum ConflictResolverType {
NoLearning,
+ /// Standard conflict analysis which returns as soon as the first unit implication point is
+ /// found (i.e. when a nogood is created which only contains a single predicate from the
+ /// current decision level).
#[default]
- UIP,
+ OneUIP,
+ /// An alternative to 1-UIP which stops as soon as the learned nogood only creates decision
+ /// predicates.
+ AllDecision,
+ /// Learns CPIP nogoods (i.e., nogoods which only have predicates from the current decision
+ /// level which reason over a single variable when learning) in combination with extended nogood
+ /// propagation.
+ ExtendedCPIP,
+ /// Learns CPIP nogoods in combination with extended nogood propagation but rather than stopping
+ /// at the first point where extended nogood propagation can take place, it stops when
+ /// extended nogood propagation can adjust a bound upon learning.
+ BoundsExtendedCPIP,
}
/// Options for the [`Solver`] which determine how it behaves.
@@ -162,6 +177,7 @@ pub struct SatisfactionSolverOptions {
pub learning_options: LearningOptions,
/// The number of MBs which are preallocated by the nogood propagator.
pub memory_preallocated: usize,
+ pub analysis_mode: ConflictResolverType,
}
impl Default for SatisfactionSolverOptions {
@@ -173,6 +189,7 @@ impl Default for SatisfactionSolverOptions {
proof_log: ProofLog::default(),
learning_options: LearningOptions::default(),
memory_preallocated: 50,
+ analysis_mode: ConflictResolverType::default(),
}
}
}
@@ -254,6 +271,16 @@ impl ConstraintSatisfactionSolver {
let handle = state.add_propagator(NogoodPropagatorConstructor::new(
(solver_options.memory_preallocated * 1_000_000) / size_of::(),
solver_options.learning_options,
+ match solver_options.analysis_mode {
+ ConflictResolverType::OneUIP | ConflictResolverType::AllDecision => {
+ PropagationMode::UnitPropagation
+ }
+ ConflictResolverType::ExtendedCPIP | ConflictResolverType::BoundsExtendedCPIP => {
+ PropagationMode::ExtendedNogoodPropagation
+ }
+ ConflictResolverType::NoLearning => PropagationMode::default(),
+ },
+ solver_options.learning_options.nogood_propagator_priority,
));
ConstraintSatisfactionSolver {
diff --git a/pumpkin-crates/core/src/engine/cp/assignments.rs b/pumpkin-crates/core/src/engine/cp/assignments.rs
index 5ccbbb455..2ff549110 100644
--- a/pumpkin-crates/core/src/engine/cp/assignments.rs
+++ b/pumpkin-crates/core/src/engine/cp/assignments.rs
@@ -663,6 +663,84 @@ impl Assignments {
}
}
+ /// Determines whether the provided [`Predicate`] holds at the provided trail position. In case
+ /// the predicate is not assigned yet (neither true nor false), returns None.
+ pub(crate) fn evaluate_predicate_at_trail_position(
+ &self,
+ predicate: Predicate,
+ trail_position: usize,
+ ) -> Option {
+ let domain_id = predicate.get_domain();
+ let value = predicate.get_right_hand_side();
+
+ match predicate.get_predicate_type() {
+ PredicateType::LowerBound => {
+ if self.get_lower_bound_at_trail_position(domain_id, trail_position) >= value {
+ Some(true)
+ } else if self.get_upper_bound_at_trail_position(domain_id, trail_position) < value
+ {
+ Some(false)
+ } else {
+ None
+ }
+ }
+ PredicateType::UpperBound => {
+ if self.get_upper_bound_at_trail_position(domain_id, trail_position) <= value {
+ Some(true)
+ } else if self.get_lower_bound_at_trail_position(domain_id, trail_position) > value
+ {
+ Some(false)
+ } else {
+ None
+ }
+ }
+ PredicateType::NotEqual => {
+ if !self.is_value_in_domain_at_trail_position(domain_id, value, trail_position) {
+ Some(true)
+ } else if let Some(assigned_value) =
+ self.get_assigned_value_at_trail_position(&domain_id, trail_position)
+ {
+ // Previous branch concluded the value is not in the domain, so if the variable
+ // is assigned, then it is assigned to the not equals value.
+ pumpkin_assert_simple!(assigned_value == value);
+ Some(false)
+ } else {
+ None
+ }
+ }
+ PredicateType::Equal => {
+ if !self.is_value_in_domain_at_trail_position(domain_id, value, trail_position) {
+ Some(false)
+ } else if let Some(assigned_value) =
+ self.get_assigned_value_at_trail_position(&domain_id, trail_position)
+ {
+ pumpkin_assert_moderate!(assigned_value == value);
+ Some(true)
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub(crate) fn get_assigned_value_at_trail_position(
+ &self,
+ var: &Var,
+ trail_position: usize,
+ ) -> Option {
+ self.is_domain_assigned_at_trail_position(var, trail_position)
+ .then(|| var.lower_bound(self))
+ }
+
+ pub(crate) fn is_domain_assigned_at_trail_position(
+ &self,
+ var: &Var,
+ trail_position: usize,
+ ) -> bool {
+ var.lower_bound_at_trail_position(self, trail_position)
+ == var.upper_bound_at_trail_position(self, trail_position)
+ }
+
pub(crate) fn is_predicate_satisfied(&self, predicate: Predicate) -> bool {
self.evaluate_predicate(predicate)
.is_some_and(|truth_value| truth_value)
diff --git a/pumpkin-crates/core/src/engine/cp/reason.rs b/pumpkin-crates/core/src/engine/cp/reason.rs
index 627c6a5a8..11b7badce 100644
--- a/pumpkin-crates/core/src/engine/cp/reason.rs
+++ b/pumpkin-crates/core/src/engine/cp/reason.rs
@@ -56,6 +56,7 @@ impl ReasonStore {
.compute(context, reason.0, propagators, destination_buffer)
}
+ #[allow(unused, reason = "Will be reintroduced with database management")]
pub(crate) fn get_lazy_code(&self, reference: ReasonRef) -> Option<&u64> {
match self.trail.get(reference.0 as usize) {
Some(reason) => match &reason.1 {
diff --git a/pumpkin-crates/core/src/engine/cp/test_solver.rs b/pumpkin-crates/core/src/engine/cp/test_solver.rs
index 4bbcdf795..7a9ecc7cb 100644
--- a/pumpkin-crates/core/src/engine/cp/test_solver.rs
+++ b/pumpkin-crates/core/src/engine/cp/test_solver.rs
@@ -25,6 +25,7 @@ use crate::propagation::PropagatorConstructor;
use crate::propagation::PropagatorId;
use crate::propagators::nogoods::NogoodPropagator;
use crate::propagators::nogoods::NogoodPropagatorConstructor;
+use crate::propagators::nogoods::PropagationMode;
use crate::state::Conflict;
use crate::state::EmptyDomainConflict;
use crate::state::PropagatorHandle;
@@ -43,6 +44,8 @@ impl Default for TestSolver {
let handle = state.add_propagator(NogoodPropagatorConstructor::new(
0,
LearningOptions::default(),
+ PropagationMode::UnitPropagation,
+ crate::propagation::Priority::High,
));
let mut solver = Self {
state,
diff --git a/pumpkin-crates/core/src/engine/notifications/mod.rs b/pumpkin-crates/core/src/engine/notifications/mod.rs
index d4c7d5a8b..335888bf4 100644
--- a/pumpkin-crates/core/src/engine/notifications/mod.rs
+++ b/pumpkin-crates/core/src/engine/notifications/mod.rs
@@ -187,7 +187,9 @@ impl NotificationEngine {
let index = watch_list
.iter()
.position(|&watched_propagator| watched_propagator == propagator_to_unwatch)
- .expect("cannot unwatch a (predicate, propagator) pair if it was not watched");
+ .unwrap_or_else(|| {
+ panic!("cannot unwatch ({predicate_id:?}, {propagator_to_unwatch:?}) pair if it was not watched")
+ });
let _ = watch_list.swap_remove(index);
@@ -537,6 +539,20 @@ impl NotificationEngine {
result
}
+ /// Returns whether the [`Predicate`] corresponding to the provided [`PredicateId`] is
+ /// satisfied.
+ pub(crate) fn evaluate_predicate_id(
+ &mut self,
+ predicate_id: PredicateId,
+ assignments: &Assignments,
+ ) -> Option {
+ self.predicate_notifier.predicate_id_assignments.evaluate(
+ predicate_id,
+ assignments,
+ &mut self.predicate_notifier.predicate_to_id,
+ )
+ }
+
/// Returns whether the [`Predicate`] corresponding to the provided [`PredicateId`] is
/// falsified.
pub(crate) fn is_predicate_id_falsified(
diff --git a/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_assignments.rs b/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_assignments.rs
index 331456def..6729cea32 100644
--- a/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_assignments.rs
+++ b/pumpkin-crates/core/src/engine/notifications/predicate_notification/predicate_assignments.rs
@@ -162,6 +162,21 @@ impl PredicateIdAssignments {
self.predicate_values[predicate_id].is_falsified()
}
+ pub(crate) fn evaluate(
+ &mut self,
+ predicate_id: PredicateId,
+ assignments: &Assignments,
+ predicate_id_generator: &mut PredicateIdGenerator,
+ ) -> Option {
+ self.update_if_unknown(predicate_id, assignments, predicate_id_generator);
+
+ match self.predicate_values[predicate_id] {
+ PredicateValue::AssignedTrue => Some(true),
+ PredicateValue::AssignedFalse => Some(false),
+ PredicateValue::Unknown => None,
+ }
+ }
+
pub(crate) fn synchronise(&mut self, new_checkpoint: usize) {
// We also need to clear the stored updated predicates; if this is not done, then it can be
// the case that a predicate is erroneously said to be satisfied/falsified while it is not
diff --git a/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs b/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs
index 1158a6887..419825f89 100644
--- a/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs
+++ b/pumpkin-crates/core/src/propagation/contexts/explanation_context.rs
@@ -83,6 +83,13 @@ impl<'a> ExplanationContext<'a> {
pub fn get_trail_position(&self) -> usize {
self.trail_position - 1
}
+
+ /// Returns the [`Predicate`] which is being explained.
+ pub fn get_predicate_to_be_explained(&self) -> Predicate {
+ self.assignments
+ .get_trail_entry(self.trail_position)
+ .predicate
+ }
}
impl HasAssignments for ExplanationContext<'_> {
diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
index 63d2867e6..1606b0dce 100644
--- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
+++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
@@ -217,6 +217,11 @@ impl<'a> PropagationContext<'a> {
.is_predicate_id_satisfied(predicate_id, self.assignments)
}
+ pub(crate) fn evaluate_predicate_id(&mut self, predicate_id: PredicateId) -> Option {
+ self.notification_engine
+ .evaluate_predicate_id(predicate_id, self.assignments)
+ }
+
/// Returns the number of [`PredicateId`]s.
pub(crate) fn num_predicate_ids(&self) -> usize {
self.notification_engine.num_predicate_ids()
diff --git a/pumpkin-crates/core/src/propagation/domains.rs b/pumpkin-crates/core/src/propagation/domains.rs
index c79f0aed5..521ad75ec 100644
--- a/pumpkin-crates/core/src/propagation/domains.rs
+++ b/pumpkin-crates/core/src/propagation/domains.rs
@@ -58,6 +58,14 @@ pub trait ReadDomains {
/// currently unassigned.
fn evaluate_predicate(&self, predicate: Predicate) -> Option;
+ /// Returns whether the provided [`Predicate`] is assigned (either true or false) or is
+ /// assigned at the provided trail position.
+ fn evaluate_predicate_at_trail_position(
+ &self,
+ predicate: Predicate,
+ trail_position: usize,
+ ) -> Option;
+
/// Returns whether the provided [`Literal`] is assigned (either true or false) or is
/// currently unassigned.
fn evaluate_literal(&self, literal: Literal) -> Option;
@@ -274,4 +282,13 @@ impl ReadDomains for T {
fn number_of_domains(&self) -> u32 {
self.assignments().num_domains()
}
+
+ fn evaluate_predicate_at_trail_position(
+ &self,
+ predicate: Predicate,
+ trail_position: usize,
+ ) -> Option {
+ self.assignments()
+ .evaluate_predicate_at_trail_position(predicate, trail_position)
+ }
}
diff --git a/pumpkin-crates/core/src/propagation/propagator.rs b/pumpkin-crates/core/src/propagation/propagator.rs
index 426757163..2ccffce40 100644
--- a/pumpkin-crates/core/src/propagation/propagator.rs
+++ b/pumpkin-crates/core/src/propagation/propagator.rs
@@ -245,6 +245,7 @@ pub enum EnqueueDecision {
/// priority (i.e., should be propagated before computationally expensive propagators).
#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[repr(u8)]
+#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
pub enum Priority {
High = 0,
Medium = 1,
diff --git a/pumpkin-crates/core/src/propagators/nogoods/arena_allocator.rs b/pumpkin-crates/core/src/propagators/nogoods/arena_allocator.rs
index d8e6c7b29..a4c522f11 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/arena_allocator.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/arena_allocator.rs
@@ -19,11 +19,11 @@ pub(crate) struct ArenaAllocator {
/// If there is a [`NogoodId`] with value `i`, then the [`PredicateId`] at position `i` will
/// contain the length `x` of the nogood. The next `i + 1 + x` elements are then the nogood
/// pointed to by the [`NogoodId`] with value `i`.
- nogoods: Vec,
+ pub(crate) nogoods: Vec,
/// Maps each [`NogoodId`] to an index; this is to prevent unnecessary allocations for other
/// structures such as the [`NogoodInfo`] which use direct hashing for storing information
/// about nogoods.
- nogood_id_to_index: HashMap,
+ pub(crate) nogood_id_to_index: HashMap,
/// The current index for the next [`NogoodId`] which is entered; see
/// [`ArenaAllocator::nogood_id_to_index`].
current_index: u32,
@@ -106,7 +106,7 @@ impl ArenaAllocator {
}
/// Calculates the range of the nogood spanned by the nogood with ID [`NogoodId`].
- fn calculate_range_of_nogood(&self, nogood_id: NogoodId) -> Range {
+ pub(crate) fn calculate_range_of_nogood(&self, nogood_id: NogoodId) -> Range {
let len = self.len_of_nogood(nogood_id);
nogood_id.index() + 1..nogood_id.index() + 1 + len
}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/learning_options.rs b/pumpkin-crates/core/src/propagators/nogoods/learning_options.rs
index f431ed52c..931eb2abc 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/learning_options.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/learning_options.rs
@@ -1,3 +1,5 @@
+use crate::propagation::Priority;
+
/// Options related to nogood management, i.e., how and when to remove learned nogoods from the
/// database.
#[derive(Debug, Copy, Clone)]
@@ -36,6 +38,8 @@ pub struct LearningOptions {
pub lbd_threshold_low: u32,
/// Specifies by how much the activity is increased when a nogood is bumped.
pub activity_bump_increment: f32,
+ /// The priority of the nogood propagator.
+ pub nogood_propagator_priority: Priority,
}
impl Default for LearningOptions {
fn default() -> Self {
@@ -48,6 +52,7 @@ impl Default for LearningOptions {
lbd_threshold_high: 7,
lbd_threshold_low: 3,
activity_bump_increment: 1.0,
+ nogood_propagator_priority: Priority::High,
}
}
}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/mod.rs b/pumpkin-crates/core/src/propagators/nogoods/mod.rs
index e04fd791a..f49574a08 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/mod.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/mod.rs
@@ -4,9 +4,12 @@ mod learning_options;
mod nogood_id;
mod nogood_info;
mod nogood_propagator;
+mod propagation_mode;
+mod semantic_minimiser;
pub use checker::*;
pub use learning_options::*;
pub(crate) use nogood_id::*;
pub(crate) use nogood_info::*;
pub(crate) use nogood_propagator::*;
+pub use propagation_mode::*;
diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_id.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_id.rs
index 184a6ea4a..514e02675 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/nogood_id.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_id.rs
@@ -14,3 +14,13 @@ impl StorageKey for NogoodId {
NogoodId { id: index as u32 }
}
}
+
+impl NogoodId {
+ pub(crate) const fn into_bits(self) -> u32 {
+ self.id
+ }
+
+ pub(crate) const fn from_bits(value: u32) -> Self {
+ Self { id: value }
+ }
+}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
index b00110535..82936e569 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
@@ -1,6 +1,7 @@
use std::cmp::max;
use std::ops::Not;
+use bitfield_struct::bitfield;
use log::warn;
use super::LearningOptions;
@@ -8,8 +9,10 @@ use super::NogoodId;
use super::NogoodInfo;
use crate::basic_types::PredicateId;
use crate::basic_types::PropositionalConjunction;
+use crate::containers::HashSet;
use crate::containers::KeyedVec;
use crate::containers::StorageKey;
+use crate::create_statistics_struct;
use crate::engine::Assignments;
use crate::engine::Lbd;
use crate::engine::PropagationStatusCP;
@@ -19,10 +22,12 @@ use crate::engine::predicates::predicate::Predicate;
use crate::engine::reason::Reason;
use crate::engine::reason::ReasonStore;
use crate::predicate;
+use crate::predicates::PredicateType;
use crate::proof::InferenceCode;
use crate::propagation::EnqueueDecision;
use crate::propagation::EventsToRegister;
use crate::propagation::ExplanationContext;
+use crate::propagation::HasAssignments;
use crate::propagation::LazyExplanation;
use crate::propagation::NotificationContext;
use crate::propagation::Priority;
@@ -31,24 +36,27 @@ use crate::propagation::Propagator;
use crate::propagation::PropagatorConstructor;
use crate::propagation::PropagatorConstructorContext;
use crate::propagation::ReadDomains;
+use crate::propagators::nogoods::PropagationMode;
+use crate::propagators::nogoods::WatcherProcessingStatus;
use crate::propagators::nogoods::arena_allocator::ArenaAllocator;
use crate::propagators::nogoods::arena_allocator::NogoodIndex;
-use crate::pumpkin_assert_advanced;
+use crate::propagators::nogoods::semantic_minimiser::SemanticMinimiser;
use crate::pumpkin_assert_eq_simple;
use crate::pumpkin_assert_extreme;
use crate::pumpkin_assert_moderate;
use crate::pumpkin_assert_simple;
use crate::state::Conflict;
use crate::state::PropagatorHandle;
+use crate::statistics::Statistic;
+use crate::statistics::StatisticLogger;
+use crate::statistics::moving_averages::CumulativeMovingAverage;
+use crate::statistics::moving_averages::MovingAverage;
+use crate::variables::DomainId;
/// A propagator which propagates nogoods (i.e. a list of [`Predicate`]s which cannot all be true
/// at the same time).
///
-/// It should be noted that this propagator is notified about each event which occurs in the solver
-/// (since the propagator does not know which IDs will be present in its learnt clauses).
-///
-/// The idea for propagation is the two-watcher scheme; this is achieved by internally keeping
-/// track of watch lists.
+/// The propagation used is based on the provided [`PropagationMode`].
#[derive(Clone, Debug)]
pub struct NogoodPropagator {
/// The [`PredicateId`]s of the nogoods.
@@ -79,7 +87,58 @@ pub struct NogoodPropagator {
/// Used during nogood cleanup. A nogood can only be removed if it is not propagated in the
/// current subtree. To test for that, we compare this handle with the propagator ID of a
/// proapgated literal to see if this propagator propagated a predicate.
+ #[allow(unused, reason = "Will be reintroduced with database management")]
handle: PropagatorHandle,
+ /// What form of propagation is performed (e.g., unit propagation, or extended nogood
+ /// propagation).
+ ///
+ /// This, among other components, influences how watchers are placed.
+ propagation_mode: PropagationMode,
+ /// The statistics kept by the [`NogoodPropagator`].
+ statistics: NogoodPropagatorStatistics,
+ /// A [`SemanticMinimiser`] used for preprocessing nogoods when added to the database.
+ semantic_minimiser: SemanticMinimiser,
+ /// The priority of the nogood propagator.
+ priority: Priority,
+}
+
+create_statistics_struct!(NogoodPropagatorStatistics {
+ /// Records the number of unit propagations.
+ num_unit_propagations: usize,
+ /// Records the number of calls to the extended nogood propagation algorithm.
+ num_extended_propagation_calls: usize,
+ /// Records the number of variables propagated by extended nogood propagation.
+ num_variables_propagated: usize,
+ /// Records the number of lower-bounds propagated by extended nogood propagation.
+ num_extended_lower_bound_propagations: usize,
+ /// Records the number of upper-bounds propagated by extended nogood propagation.
+ num_extended_upper_bound_propagations: usize,
+ /// Records the number of disequalities propagated by extended nogood propagation.
+ num_extended_disequality_propagations: usize,
+ /// The average number of [`Predicate`]s describing the propagated domain when performing
+ /// extended nogood propagation.
+ average_num_predicates_describing_domain_when_propagating_extended: CumulativeMovingAverage
+});
+
+/// The information necessary to calculate the explanation for a propagation.
+#[bitfield(u64)]
+struct LazyNogoodExplanation {
+ /// The [`NogoodId`] of the nogood which caused the propagation.
+ #[bits(32)]
+ nogood_id: NogoodId,
+ /// Whether extended nogood propagation takes place; if this is the case, then a different
+ /// explanation than when using unit propagation is generated.
+ #[bits(1)]
+ explains_extended_propagation: bool,
+ /// Whether the extended nogood propagation was a unit propagation.
+ #[bits(1)]
+ is_extended_unit_propagation: bool,
+ /// When extended nogood propagation performs unit propagation, the lazy explanation expects
+ /// the propagated atomic constraint at the 0-th index. However, this invariant is not
+ /// maintained when performing extended nogood propagation, so this index is used to indicate
+ /// the [`PredicateId`] of the propagating atomic constraint.
+ #[bits(30)]
+ unit_propagation_index: u32,
}
/// [`PropagatorConstructor`] for constructing a new instance of the [`NogoodPropagator`] with the
@@ -88,13 +147,22 @@ pub(crate) struct NogoodPropagatorConstructor {
/// How many [`PredicateId`]s to preallocate to the [`ArenaAllocator`].
capacity: usize,
parameters: LearningOptions,
+ propagation_mode: PropagationMode,
+ priority: Priority,
}
impl NogoodPropagatorConstructor {
- pub(crate) fn new(capacity: usize, parameters: LearningOptions) -> Self {
+ pub(crate) fn new(
+ capacity: usize,
+ parameters: LearningOptions,
+ propagation_mode: PropagationMode,
+ priority: Priority,
+ ) -> Self {
Self {
capacity,
parameters,
+ propagation_mode,
+ priority,
}
}
}
@@ -107,6 +175,7 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
context: PropagatorConstructorContext,
) -> (EventsToRegister, Self::PropagatorImpl) {
let propagator = NogoodPropagator {
+ statistics: NogoodPropagatorStatistics::default(),
handle: PropagatorHandle::new(context.propagator_id),
parameters: self.parameters,
nogood_predicates: ArenaAllocator::new(self.capacity),
@@ -119,6 +188,9 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
lbd_helper: Default::default(),
bumped_nogoods: Default::default(),
temp_nogood_reason: Default::default(),
+ propagation_mode: self.propagation_mode,
+ semantic_minimiser: Default::default(),
+ priority: self.priority,
};
(EventsToRegister::empty(), propagator)
@@ -131,10 +203,10 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
/// that is observed to be `false`, it will be made the cached predicate. That way, whenever the
/// watcher is triggered, the propagator may be able to quickly determine if the nogood can be
/// skipped by looking at the cached predicate.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-struct Watcher {
- nogood_id: NogoodId,
- cached_predicate: PredicateId,
+#[derive(Clone, Copy, Debug)]
+pub(crate) struct Watcher {
+ pub(crate) nogood_id: NogoodId,
+ pub(crate) cached_predicate: PredicateId,
}
/// Keeps track of three tiers of nogoods:
@@ -154,38 +226,49 @@ struct LearnedNogoodIds {
}
impl NogoodPropagator {
- /// Determines whether the nogood (pointed to by `id`) is propagating using the following
- /// reasoning:
+ /// Replace the watcher at `watcher_to_replace` with `i`.
///
- /// - The predicate at position 0 is falsified; this is one of the conventions of the nogood
- /// propagator
- /// - The reason for the predicate is the nogood propagator
- fn is_nogood_propagating(
- handle: PropagatorHandle,
- nogood: &[PredicateId],
- assignments: &Assignments,
- reason_store: &ReasonStore,
- id: NogoodId,
- notification_engine: &mut NotificationEngine,
- ) -> bool {
- if notification_engine.is_predicate_id_falsified(nogood[0], assignments) {
- let trail_position = assignments
- .get_trail_position(&!notification_engine.get_predicate(nogood[0]))
- .unwrap();
- let trail_entry = assignments.get_trail_entry(trail_position);
- if let Some(reason_ref) = trail_entry.reason {
- let propagator_id = reason_store.get_propagator(reason_ref);
- let code = reason_store.get_lazy_code(reason_ref);
-
- // We check whether the predicate was propagated by the nogood propagator first
- let propagated_by_nogood_propagator = propagator_id == handle.propagator_id();
- // Then we check whether the lazy reason for the propagation was this particular
- // nogood
- let code_matches_id = code.is_none() || *code.unwrap() == id.id as u64;
- return propagated_by_nogood_propagator && code_matches_id;
- }
+ /// Note that this method does not remove any watchers but only adds a watcher to
+ /// `watcher_to_replace`.
+ fn replace_watcher(
+ context: &mut PropagationContext<'_>,
+ watcher: Watcher,
+ nogood_predicates: &mut [PredicateId],
+ i: usize,
+ watcher_to_replace: usize,
+ watch_lists: &mut KeyedVec>,
+ ) {
+ // Replace the current watcher with the new predicate watcher.
+ nogood_predicates.swap(watcher_to_replace, i);
+ // Add this nogood to the watch list of the new watcher.
+ Self::add_watcher(
+ context,
+ nogood_predicates[watcher_to_replace],
+ watcher,
+ watch_lists,
+ );
+ }
+
+ /// Removes the provided `watcher` in the watchlist of `predicate_id`.
+ ///
+ /// Note that this method removes the watcher by iterating through the watchers of
+ /// `predicate_id` and finding the one that matches it nogood id. If the index of the watcher
+ /// in the watchlist of `predicate_id` is known, then this method should not be used.
+ fn remove_watcher(
+ context: &mut PropagationContext<'_>,
+ watcher: Watcher,
+ predicate_id: PredicateId,
+ watch_lists: &mut KeyedVec>,
+ ) {
+ let index_in_zeroth_watchlist = watch_lists[predicate_id]
+ .iter()
+ .position(|other_watcher| other_watcher.nogood_id == watcher.nogood_id)
+ .expect("Expected to be able to retrieve watcher");
+ let _ = watch_lists[predicate_id].swap_remove(index_in_zeroth_watchlist);
+
+ if watch_lists[predicate_id].is_empty() {
+ context.unregister_predicate(predicate_id);
}
- false
}
}
@@ -197,7 +280,7 @@ impl Propagator for NogoodPropagator {
}
fn priority(&self) -> Priority {
- Priority::High
+ self.priority
}
fn notify_predicate_id_satisfied(
@@ -209,8 +292,16 @@ impl Propagator for NogoodPropagator {
EnqueueDecision::Enqueue
}
+ fn log_statistics(&self, statistic_logger: StatisticLogger) {
+ self.statistics.log(statistic_logger);
+ }
+
+ #[allow(
+ clippy::filter_map_bool_then,
+ reason = "Will run into borrow issues otherwise"
+ )]
fn propagate(&mut self, mut context: PropagationContext) -> Result<(), Conflict> {
- pumpkin_assert_advanced!(self.debug_is_properly_watched());
+ pumpkin_assert_moderate!(self.debug_is_properly_watched());
// First we perform nogood management to ensure that the database does not grow excessively
// large with "bad" nogoods
@@ -224,8 +315,6 @@ impl Propagator for NogoodPropagator {
self.watch_lists
.resize(context.num_predicate_ids() + 1, Vec::default());
}
-
- // TODO: should drop all elements afterwards
for predicate_id in self.updated_predicate_ids.drain(..) {
pumpkin_assert_moderate!(
{
@@ -235,19 +324,20 @@ impl Propagator for NogoodPropagator {
"The predicate {} with id {predicate_id:?} should be satisfied but was not",
context.get_predicate(predicate_id),
);
-
let mut index = 0;
while index < self.watch_lists[predicate_id].len() {
let watcher = self.watch_lists[predicate_id][index];
-
// We first check whether the cached predicate might already make the nogood
// satisfied
if context.is_predicate_id_falsified(watcher.cached_predicate) {
index += 1;
continue;
}
-
- let nogood_predicates = &mut self.nogood_predicates[watcher.nogood_id];
+ let calculate_range_of_nogood = self
+ .nogood_predicates
+ .calculate_range_of_nogood(watcher.nogood_id);
+ let nogood_predicates =
+ &mut self.nogood_predicates.nogoods[calculate_range_of_nogood];
// Place the watched predicate at position 1 for simplicity.
if nogood_predicates[0] == predicate_id {
@@ -265,65 +355,119 @@ impl Propagator for NogoodPropagator {
continue;
}
- // Look for another nonsatisfied predicate
- // to replace the watched predicate.
+ // We start updating the watchers.
+ //
+ // Look for another nonsatisfied predicate to replace the watched predicate.
let mut found_new_watch = false;
+
+ // In the case of extended nogood propagation, we need to track more information.
+ //
+ // If there is a falsified predicate over the same variable as the 0th
+ // predicate, then we need to replace it.
+ //
+ // Similarly, we need to keep the invariant for lazy unit propagation explanations
+ // that the propagated predicate is at the 0th position. Hence, if we are looking
+ // for a new predicate over a different variable but find an unassinged predicate
+ // over the same variable, then we replace it (this Boolean ensures that we only do
+ // that once).
+ let mut falsified_zeroth = None;
+
// Start from index 2 since we are skipping watched predicates.
for i in 2..nogood_predicates.len() {
- // Find a predicate that is either false or unassigned,
- // i.e., not assigned true.
- if !context.is_predicate_id_satisfied(nogood_predicates[i]) {
- // Found another predicate that can be the watcher.
- found_new_watch = true;
- // todo: does it make sense to replace the cached predicate with
- // this new predicate?
-
- // Replace the current watcher with the new predicate watcher.
- nogood_predicates.swap(1, i);
- // Add this nogood to the watch list of the new watcher.
- Self::add_watcher(
- &mut context,
- nogood_predicates[1],
- watcher,
- &mut self.watch_lists,
- );
-
- // No propagation is taking place, go to the next nogood.
- break;
+ // We process the watcher based on the analysis mode that we are in
+ match self.propagation_mode.process_potential_watcher(
+ &mut context,
+ nogood_predicates,
+ i,
+ ) {
+ WatcherProcessingStatus::Continue => continue,
+ WatcherProcessingStatus::FoundNewWatch => {
+ // Found another predicate that can be the watcher.
+ found_new_watch = true;
+
+ Self::replace_watcher(
+ &mut context,
+ watcher,
+ nogood_predicates,
+ i,
+ 1,
+ &mut self.watch_lists,
+ );
+
+ // No propagation is taking place, go to the next nogood.
+ break;
+ }
+ WatcherProcessingStatus::FalsifiedZeroth => {
+ // We have found a predicate which reasons over the same
+ // variable as the 0-th predicate *and* is falsified
+ //
+ // We swap the two predicates and mark that the current
+ // nogood id should be removed from the watchlist of the 0-th
+ // predicate (before swapping)
+ falsified_zeroth = Some(nogood_predicates[0]);
+
+ Self::replace_watcher(
+ &mut context,
+ watcher,
+ nogood_predicates,
+ i,
+ 0,
+ &mut self.watch_lists,
+ );
+
+ // Note that we do not break, since we still want to find a new
+ // watcher for the other predicate
+ }
}
- } // end iterating through the nogood
+ }
if found_new_watch {
// We remove the current watcher
let _ = self.watch_lists[predicate_id].swap_remove(index);
+
if self.watch_lists[predicate_id].is_empty() {
context.unregister_predicate(predicate_id);
}
+ }
+ if let Some(to_remove) = falsified_zeroth {
+ // We have replaced `to_remove` with a predicate that has been
+ // falsified; we now remove this nogood from the watchlist of
+ // `to_remove`
+ Self::remove_watcher(&mut context, watcher, to_remove, &mut self.watch_lists);
+ }
+ if found_new_watch || falsified_zeroth.is_some() {
+ // We have either found a new watcher, or we have found a falsified
+ // predicate; no propagation can take place in either case, so we
+ // continue
+ pumpkin_assert_moderate!(nogood_predicates.iter().skip(2).all(
+ |predicate_id| {
+ !self.watch_lists[predicate_id]
+ .iter()
+ .any(|other_watcher| watcher.nogood_id == other_watcher.nogood_id)
+ }
+ ),);
continue;
}
- // At this point, nonwatched predicates and nogood[1] are falsified.
- pumpkin_assert_advanced!(nogood_predicates.iter().skip(1).all(|p| {
- let predicate = context.get_predicate(*p);
- context.evaluate_predicate(predicate) == Some(true)
- }));
-
- // There are two scenarios:
- // nogood[0] is unassigned -> propagate the predicate to false
- // nogood[0] is assigned true -> conflict.
- let reason = Reason::DynamicLazy(watcher.nogood_id.id as u64);
-
- let predicate = !context.get_predicate(nogood_predicates[0]);
- let result = context.post(predicate, reason);
- // If the propagation lead to a conflict.
- if let Err(e) = result {
- return Err(e.into());
- }
+ // Now we perform the propagation
+ let nogood_index = self
+ .nogood_predicates
+ .nogood_id_to_index
+ .get(&watcher.nogood_id)
+ .expect("Expected nogood predicate to exist");
+ self.propagation_mode.perform_propagation(
+ &mut context,
+ nogood_predicates,
+ &self.inference_codes[nogood_index],
+ watcher.nogood_id,
+ &mut self.statistics,
+ )?;
+
index += 1;
}
}
- pumpkin_assert_advanced!(self.debug_is_properly_watched());
+ pumpkin_assert_moderate!(self.debug_is_properly_watched());
Ok(())
}
@@ -354,12 +498,96 @@ impl Propagator for NogoodPropagator {
code: u64,
mut context: ExplanationContext,
) -> LazyExplanation<'_> {
- let id = NogoodId { id: code as u32 };
+ let reason = LazyNogoodExplanation::from_bits(code);
+ let id = reason.nogood_id();
+ let result = if reason.explains_extended_propagation() {
+ // The lazy explanations explains a propagation using extended nogood propagation.
+ let nogood = &self.nogood_predicates[id];
+ let info_id = self.nogood_predicates.get_nogood_index(&id);
- self.temp_nogood_reason = self.nogood_predicates[id][1..]
- .iter()
- .map(|predicate_id| context.get_predicate(*predicate_id))
- .collect::>();
+ // We retrieve the predicate which is being explained.
+ let predicate_to_be_explained = context.get_predicate_to_be_explained();
+ let rhs = predicate_to_be_explained.get_right_hand_side();
+ let propagated_domain = predicate_to_be_explained.get_domain();
+
+ if reason.is_extended_unit_propagation() {
+ let propagating_predicate_id =
+ PredicateId::create_from_index(reason.unit_propagation_index() as usize);
+
+ self.temp_nogood_reason = self.nogood_predicates[id]
+ .iter()
+ .filter(|&&predicate_id| predicate_id != propagating_predicate_id)
+ .map(|&predicate_id| context.get_predicate(predicate_id))
+ .collect::>();
+ } else {
+ match predicate_to_be_explained.get_predicate_type() {
+ PredicateType::UpperBound => {
+ self.temp_nogood_reason = nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ let predicate = context.get_predicate(*predicate_id);
+
+ (context.evaluate_predicate_at_trail_position(
+ predicate,
+ context.get_trail_position(),
+ ) == Some(true)
+ && (predicate.get_domain() != propagated_domain
+ || predicate.is_upper_bound_predicate()
+ || (predicate.is_not_equal_predicate()
+ && predicate.get_right_hand_side() > rhs)))
+ .then_some(predicate)
+ })
+ .collect();
+ }
+ PredicateType::LowerBound => {
+ self.temp_nogood_reason = nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ let predicate = context.get_predicate(*predicate_id);
+
+ (context.evaluate_predicate_at_trail_position(
+ predicate,
+ context.get_trail_position(),
+ ) == Some(true)
+ && (predicate.get_domain() != propagated_domain
+ || predicate.is_lower_bound_predicate()
+ || (predicate.is_not_equal_predicate()
+ && predicate.get_right_hand_side() < rhs)))
+ .then_some(predicate)
+ })
+ .collect();
+ }
+ PredicateType::NotEqual => {
+ self.temp_nogood_reason = nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ let predicate = context.get_predicate(*predicate_id);
+
+ (predicate.get_domain() != propagated_domain).then_some(predicate)
+ })
+ .collect();
+ }
+ PredicateType::Equal => unreachable!(),
+ }
+ }
+
+ LazyExplanation {
+ predicates: &self.temp_nogood_reason,
+ inference_code: self.inference_codes[info_id].clone(),
+ }
+ } else {
+ self.temp_nogood_reason = self.nogood_predicates[id][1..]
+ .iter()
+ .map(|predicate_id| context.get_predicate(*predicate_id))
+ .collect::>();
+
+ let info_id = self.nogood_predicates.get_nogood_index(&id);
+
+ LazyExplanation {
+ predicates: self.temp_nogood_reason.as_slice(),
+ inference_code: self.inference_codes[info_id].clone(),
+ }
+ };
let info_id = self.nogood_predicates.get_nogood_index(&id);
@@ -372,9 +600,10 @@ impl Propagator for NogoodPropagator {
{
self.nogood_info[info_id].block_bumps = true;
self.bumped_nogoods.push(id);
- // Note that we do not need to take into account the propagated predicate (in position
- // zero), since it will share a decision level with one of the other predicates (if it
- // did not then it should have propagated earlier).
+ // Note that we do not need to take into account the propagated predicate (in
+ // position zero), since it will share a decision level with one of
+ // the other predicates (if it did not then it should have
+ // propagated earlier).
let current_lbd = self
.lbd_helper
.compute_lbd(&self.temp_nogood_reason, &context);
@@ -386,17 +615,19 @@ impl Propagator for NogoodPropagator {
// Nogood activity update.
//
- // Rescale the nogood activity if bumping would lead to a (too) large activity value.
+ // Rescale the nogood activity if bumping would lead to a (too) large activity
+ // value.
if self.nogood_info[info_id].activity + self.parameters.activity_bump_increment
> self.parameters.max_activity
{
// Rescale the activity of the "mid" and "high" LBD learned nogoods (recall that
// "low" LBD nogoods do not have their LBD scaled).
//
- // TODO: we could consider having separate activity bump values for each tier, so
- // that we can do rescaling only within the same tier.
- // This would lead to less rescaling, and anyway we are (probably) only interested
- // in the relative order of nogoods within a tier.
+ // TODO: we could consider having separate activity bump values for each tier,
+ // so that we can do rescaling only within the same tier.
+ // This would lead to less rescaling, and anyway we are (probably) only
+ // interested in the relative order of nogoods within a
+ // tier.
self.learned_nogood_ids
.high_lbd
.iter()
@@ -411,42 +642,401 @@ impl Propagator for NogoodPropagator {
// At this point, it is safe to increase the activity value
self.nogood_info[info_id].activity += self.parameters.activity_bump_increment;
}
- LazyExplanation {
- predicates: self.temp_nogood_reason.as_slice(),
- inference_code: self.inference_codes[info_id].clone(),
- }
+
+ result
}
}
/// Functions for adding nogoods
impl NogoodPropagator {
+ /// Propagates a nogood using "extended" reasoning.
+ ///
+ /// If the nogood only contains unassigned predicates over a single variable, then the nogood
+ /// can be seen as a domain description of that variable.
+ ///
+ /// We assume that the nogood has been semantically minimised beforehand.
+ ///
+ /// For example, let's say that we have the nogood:
+ /// `[x >= 6] /\ [x <= 15] /\ [x != 12] /\ ... -> false`
+ /// For this nogood to be satisfied, we can see that [x <= 5] \/ [x >= 16] \/ [x == 12].
+ /// Based on this reasoning, we can remove the values {6, 7, 8, 9, 10, 11, 13, 14, 15} from the
+ /// domain of `x`.
+ #[allow(
+ clippy::filter_map_bool_then,
+ reason = "Otherwise leads to borrow issues."
+ )]
+ pub(crate) fn extended_nogood_propagation(
+ context: &mut PropagationContext,
+ nogood: &[PredicateId],
+ propagated_domain: DomainId,
+ inference_code: &InferenceCode,
+ statistics: &mut NogoodPropagatorStatistics,
+ nogood_id: Option,
+ ) -> Result<(), Conflict> {
+ statistics.num_extended_propagation_calls += 1;
+
+ let (
+ exceptions,
+ lower_bound,
+ upper_bound,
+ num_describing_domain,
+ last_describing_predicate_id,
+ is_falsified,
+ ) = get_domain_info(context, nogood, propagated_domain);
+
+ if is_falsified {
+ // The nogood is already falsified
+ return Ok(());
+ }
+
+ if num_describing_domain == 0 {
+ // We could not find another watcher (i.e., all predicats over other variables are
+ // satisfied). However, we could not find a predicate in the nogood which is over
+ // `propgating_domain` *and* unsatisfied.
+ //
+ // This means that there is a conflict which should be reported.
+ let reason = nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ (context.is_predicate_id_satisfied(*predicate_id))
+ .then(|| context.get_predicate(*predicate_id))
+ })
+ .collect::();
+
+ return Err(Conflict::Propagator(PropagatorConflict {
+ conjunction: reason,
+ inference_code: inference_code.clone(),
+ }));
+ }
+
+ statistics
+ .average_num_predicates_describing_domain_when_propagating_extended
+ .add_term(num_describing_domain);
+
+ // We perform the standard unit propagation if possible
+ //
+ // TODO: Could use a lazy explanation here
+ if num_describing_domain == 1 {
+ statistics.num_unit_propagations += 1;
+
+ let last_describing_predicate = context.get_predicate(last_describing_predicate_id);
+
+ let reason = if let Some(nogood_id) = nogood_id {
+ Reason::DynamicLazy(
+ LazyNogoodExplanation::new()
+ .with_nogood_id(nogood_id)
+ .with_explains_extended_propagation(true)
+ .with_is_extended_unit_propagation(true)
+ .with_unit_propagation_index(last_describing_predicate_id.id)
+ .into(),
+ )
+ } else {
+ (
+ nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ (context.is_predicate_id_satisfied(*predicate_id))
+ .then(|| context.get_predicate(*predicate_id))
+ })
+ .collect::(),
+ inference_code,
+ )
+ .into()
+ };
+
+ return context
+ .post(!last_describing_predicate, reason)
+ .map_err(|e| e.into());
+ }
+
+ // Now we get the minimum and maximum exception values for propagation
+ let (min_exception, max_exception) =
+ exceptions
+ .iter()
+ .fold((None, None), |(min_exception, max_exception), exception| {
+ (
+ min_exception.map_or_else(
+ || Some(*exception),
+ |min_exception: i32| Some(min_exception.min(*exception)),
+ ),
+ max_exception.map_or_else(
+ || Some(*exception),
+ |max_exception: i32| Some(max_exception.max(*exception)),
+ ),
+ )
+ });
+
+ // Now we propagate our nogood
+ //
+ // We store the bound of the lower-bound predicate in `lb` e.g., if we have the
+ // predicate [x >= 5], then we store 5
+ //
+ // If there is no such predicate in the nogood then there must be one or more holes, and we
+ // store the minimum value of this.
+ let lb = lower_bound.map_or_else(
+ || min_exception.unwrap(),
+ |predicate| predicate.get_right_hand_side(),
+ );
+
+ // We store the bound of the upper-bound predicate in `ub`; e.g., if we have the
+ // predicate [x <= 10], then we store 10
+ //
+ // If there is no such predicate in the nogood then there must be one or more holes, and we
+ // store the maximum value of this.
+ let ub = upper_bound.map_or_else(
+ || max_exception.unwrap(),
+ |predicate| predicate.get_right_hand_side(),
+ );
+ pumpkin_assert_simple!(lb <= ub);
+
+ // We keep track of whether propagation took place.
+ let mut propagated = false;
+
+ // Now we check whether we can do any bound propagation
+ if upper_bound.is_none() {
+ pumpkin_assert_simple!(
+ exceptions.len() > 1
+ || (!exceptions.is_empty()
+ && lower_bound.is_some()
+ && ub > lower_bound.unwrap().get_right_hand_side()),
+ );
+ // First, if there is no upper-bound predicate ([x <= v]), then we can propagate the
+ // upper-bound based on the lower-bound predicate and/or the inequality predicates (note
+ // that there must be one or more holes due to semantic minimisation and unit
+ // propagation taking place previously).
+ //
+ // For example, if we have the predicates [x >= 5] /\ [x != 10], then we know that it
+ // should either hold that [x <= 4] or [x = 10]. In either case, we know that [x <=
+ // 10].
+ //
+ // Thus, we calculate the new maximum value as either the maximum value of the
+ // inequality predicates (note that it can never be the case that there is an inequality
+ // predicate with a value lower than the lower-bound predicate due to semantic
+ // minimisation).
+
+ if ub < context.upper_bound(&propagated_domain) {
+ propagated = true;
+
+ // The reason consists of:
+ // 1) All predicates which reason over a different domain than the propagated
+ // predicate (which are all guaranteed to be satisfied)
+ // 2) All predicates which reason over the same domain as the propagated predicate,
+ // but are *not* disequality predicates with a right-hand side larger than the
+ // max exception
+ //
+ // For example, if we have the nogood [x >= 5] /\ [x != 10] /\ [x != 7] /\ [x !=
+ // 15], where the first two predicates are unassigned but the
+ // last two are satisfied; then the fact that [x != 7] is true,
+ // does not matter for the propagation of [x <= 10] to hold, but
+ // [x != 15] is required in the explanation
+ statistics.num_extended_upper_bound_propagations += 1;
+ let reason = if let Some(nogood_id) = nogood_id {
+ Reason::DynamicLazy(
+ LazyNogoodExplanation::new()
+ .with_nogood_id(nogood_id)
+ .with_explains_extended_propagation(true)
+ .into(),
+ )
+ } else {
+ (
+ nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ let predicate = context.get_predicate(*predicate_id);
+
+ (context.is_predicate_id_satisfied(*predicate_id)
+ && (predicate.get_domain() != propagated_domain
+ || predicate.is_upper_bound_predicate()
+ || (predicate.is_not_equal_predicate()
+ && predicate.get_right_hand_side() > ub)))
+ .then_some(predicate)
+ })
+ .collect::(),
+ inference_code,
+ )
+ .into()
+ };
+
+ let result = context.post(predicate!(propagated_domain <= ub), reason);
+
+ if result.is_err() {
+ statistics.num_variables_propagated += 1;
+ }
+ result?
+ }
+ }
+
+ if lower_bound.is_none() {
+ // First, if there is no lower-bound predicate ([x >= v]), then we can propagate the
+ // lower-bound based on the upper-bound predicate and/or the inequality predicates (note
+ // that there must be one or more holes due to semantic minimisation and unit
+ // propagation taking place previously).
+ //
+ // For example, if we have the predicates [x <= 15] /\ [x != 10], then we know that it
+ // should either hold that [x >= 16] or [x = 10]. In either case, we know that [x >= 10]
+ //
+ // Thus, we calculate the new minimum value as the minimum value of the
+ // inequality predicates (note that it can never be the case that there is an inequality
+ // predicate with a value higher than the upper-bound predicate due to
+ // semantic minimisation).
+ if lb > context.lower_bound(&propagated_domain) {
+ propagated = true;
+
+ // The reason consists of:
+ // 1) All predicates which reason over a different domain than the propagated
+ // predicate (which are all guaranteed to be satisfied)
+ // 2) All predicates which reason over the same domain as the propagated predicate,
+ // but are *not* disequality predicates with a right-hand side smaller than the
+ // min exception
+ //
+ // For example, if we have the nogood [x <= 15] /\ [x != 7] /\ [x != 10] /\ [x !=
+ // 5], where the first two predicates are unassigned but the
+ // last two are satisfied; then the fact that [x != 10] is true,
+ // does not matter for the propagation of [x >= 7] to hold, but
+ // [x != 5] is required in the explanation
+ statistics.num_extended_lower_bound_propagations += 1;
+ let reason = if let Some(nogood_id) = nogood_id {
+ Reason::DynamicLazy(
+ LazyNogoodExplanation::new()
+ .with_nogood_id(nogood_id)
+ .with_explains_extended_propagation(true)
+ .into(),
+ )
+ } else {
+ (
+ nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ let predicate = context.get_predicate(*predicate_id);
+
+ (context.is_predicate_id_satisfied(*predicate_id)
+ && (predicate.get_domain() != propagated_domain
+ || predicate.is_lower_bound_predicate()
+ || (predicate.is_not_equal_predicate()
+ && predicate.get_right_hand_side() < lb)))
+ .then_some(predicate)
+ })
+ .collect::(),
+ inference_code,
+ )
+ .into()
+ };
+
+ let result = context.post(predicate!(propagated_domain >= lb), reason);
+
+ if result.is_err() {
+ statistics.num_variables_propagated += 1;
+ }
+ result?
+ }
+ }
+
+ // Now we still need to create the holes in the domain which are infeasible.
+ //
+ // Let's look at some scenarios:
+ //
+ // We have all three types of predicates; for example, [x >= 5] /\ [x <= 15] /\ [x !=
+ // 10]. In this case we should remove all values between [5, 15] with the exception of 10.
+ //
+ // We have two types of predicates; for example, [x >= 5] /\ [x != 10]. In this case, we
+ // have previously propagated [x <= 10] and we need to remove the range [5, 9].
+ // Another example is the case where [x >= 5] /\ [x <= 15]. In this case, we have not
+ // previously propagated anything and we need to remove the range [5, 15].
+ //
+ // We only have one type of predicate (necessarily inequalities due to semantic
+ // minimisation); for example, [x != 10] /\ [x != 12]. In this case, we have previous
+ // propagated [x >= 10] and [x <= 12] and we need to remove 11 from the domain.
+ //
+ //
+ // In all of these scenarios, we need to determine the range over which to iterate by
+ // looking at the values which should be removed after updating the bounds (where the new
+ // bounds are stored in `min_range` and `max_range`).
+ //
+ // We do this by traversing the following range:
+ // - If the lower-bound was propagated, then we use the new value as the lower-bound of our
+ // range.
+ //
+ // If it was not (i.e. because [x <= v] is in the nogood), then we use either the
+ // right-hand side of the lower-bound predicate or the lower-bound of the variable as the
+ // lower-bound of the range to start iterating over.
+ // - If the upper-bound was propagated, then we use the new value as the upper-bound of our
+ // range.
+ //
+ // If it was not (i.e. because [x >= v] is in the nogood), then we use either the
+ // right-hand side of the upper-bound predicate or the upper-bound of the variable as the
+ // upper-bound of the range to start iterating over.
+ //
+ //
+ // Hence, we iterate over the range that we have created, removing the values while *not*
+ // removing the values for which there is an inequality predicate.
+
+ // The reason consists of:
+ // 1) All predicates which reason over a different domain than the propagated predicate
+ // (which are all guaranteed to be satisfied)
+ for value_in_domain in lb..=ub {
+ if !exceptions.contains(&value_in_domain)
+ && context.contains(&propagated_domain, value_in_domain)
+ {
+ propagated = true;
+ statistics.num_extended_disequality_propagations += 1;
+ let reason = if let Some(nogood_id) = nogood_id {
+ Reason::DynamicLazy(
+ LazyNogoodExplanation::new()
+ .with_nogood_id(nogood_id)
+ .with_explains_extended_propagation(true)
+ .into(),
+ )
+ } else {
+ (
+ nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ let predicate = context.get_predicate(*predicate_id);
+
+ (predicate.get_domain() != propagated_domain).then_some(predicate)
+ })
+ .collect::(),
+ inference_code,
+ )
+ .into()
+ };
+ let result = context.post(predicate!(propagated_domain != value_in_domain), reason);
+ if result.is_err() {
+ statistics.num_variables_propagated += 1;
+ }
+ result?
+ }
+ }
+
+ if propagated {
+ statistics.num_variables_propagated += 1;
+ }
+
+ Ok(())
+ }
+
/// Adds a nogood which has been learned during search.
///
/// The first predicate should be asserting and the second predicate should contain the
- /// predicte with the next highest decision level.
+ /// predicate with the next highest decision level.
pub(crate) fn add_asserting_nogood(
&mut self,
nogood: Vec,
inference_code: InferenceCode,
context: &mut PropagationContext,
) {
- // We treat unit nogoods in a special way by adding it as a permanent nogood at the
- // root-level; this is essentially the same as adding a predicate at the root level
- if nogood.len() == 1 {
- pumpkin_assert_moderate!(
- context.get_checkpoint() == 0,
- "A unit nogood should have backtracked to the root-level"
- );
+ if self
+ .propagation_mode
+ .can_be_added_as_permanent(context, &nogood)
+ {
self.add_permanent_nogood(nogood, inference_code, context)
.expect("Unit learned nogoods cannot fail.");
return;
}
- // Skip the zero-th predicate since it is unassigned,
- // but will be assigned at the level of the predicate at index one.
let lbd = self
- .lbd_helper
- .compute_lbd(&nogood.as_slice()[1..], context);
+ .propagation_mode
+ .calculate_lbd(context, &nogood, &mut self.lbd_helper);
let nogood = nogood
.iter()
@@ -467,7 +1057,8 @@ impl NogoodPropagator {
cached_predicate: self.nogood_predicates[nogood_id][0],
};
- // Now we add two watchers to the first two predicates in the nogood
+ // Now we add two watchers to the first two predicates in the nogood; we are
+ // guaranteed that these are different predicates
NogoodPropagator::add_watcher(
context,
self.nogood_predicates[nogood_id][0],
@@ -481,16 +1072,18 @@ impl NogoodPropagator {
&mut self.watch_lists,
);
- // Then we propagate the asserting predicate and as the reason we give the index to the
- // asserting nogood such that we can re-create the reason when asked for it
- let reason = Reason::DynamicLazy(nogood_id.id as u64);
+ let inference_code =
+ &self.inference_codes[self.nogood_predicates.get_nogood_index(&nogood_id)];
- let predicate = !context
- .notification_engine
- .get_predicate(self.nogood_predicates[nogood_id][0]);
- context
- .post(predicate, reason)
- .expect("Cannot fail to add the asserting predicate.");
+ self.propagation_mode
+ .perform_propagation(
+ context,
+ &self.nogood_predicates[nogood_id],
+ inference_code,
+ nogood_id,
+ &mut self.statistics,
+ )
+ .expect("Asserting nogood cannot fail");
// We then assign the nogood to the correct tier based on its LBD
if lbd >= self.parameters.lbd_threshold_high {
@@ -537,7 +1130,7 @@ impl NogoodPropagator {
let mut input_nogood = nogood.clone();
// Then we pre-process the nogood such that (among others) it does not contain duplicates
- Self::preprocess_nogood(&mut nogood, context);
+ Self::preprocess_nogood(&mut nogood, context, &mut self.semantic_minimiser);
// Unit nogoods are added as root assignments rather than as nogoods.
if nogood.len() == 1 {
@@ -586,7 +1179,7 @@ impl NogoodPropagator {
}
} else {
// Otherwise, we just check whether they are not the same, if they are not
- // then keep the predicte
+ // then keep the predicate
(p != nogood[0]).then_some(p)
}
})
@@ -615,62 +1208,112 @@ impl NogoodPropagator {
//
// The preprocessing ensures that all predicates are unassigned.
else {
- #[cfg(feature = "check-propagations")]
- let nogood = input_nogood
- .iter()
- .map(|predicate| context.get_id(*predicate))
- .collect::>();
-
- #[cfg(not(feature = "check-propagations"))]
- let nogood = nogood
- .iter()
- .map(|predicate| context.get_id(*predicate))
- .collect::>();
-
- // Add the nogood to the database.
- //
- // Currently we always allocate a fresh ID
- let nogood_id = self.nogood_predicates.insert(nogood);
- let _ = self
- .nogood_info
- .push(NogoodInfo::new_permanent_nogood_info());
- let _ = self.inference_codes.push(inference_code);
-
- self.permanent_nogood_ids.push(nogood_id);
-
- let watcher = Watcher {
- nogood_id,
- cached_predicate: self.nogood_predicates[nogood_id][0],
- };
-
- NogoodPropagator::add_watcher(
- context,
- self.nogood_predicates[nogood_id][0],
- watcher,
- &mut self.watch_lists,
- );
- NogoodPropagator::add_watcher(
+ self.propagation_mode.add_permanent_nogood_non_unit(
+ nogood,
+ &input_nogood,
+ inference_code,
context,
- self.nogood_predicates[nogood_id][1],
- watcher,
+ &mut self.nogood_predicates,
+ &mut self.nogood_info,
+ &mut self.inference_codes,
&mut self.watch_lists,
- );
+ &mut self.permanent_nogood_ids,
+ &mut self.statistics,
+ )
+ }
+ }
+}
- Ok(())
+/// Returns the following information about the unsatisfied predicates concerning the provided
+/// domain:
+/// 1. The values disequality values which are present in the nogood.
+/// 2. The lower-bound predicate, if present.
+/// 3. The upper-bound predicate, if present
+/// 4. The number of elements describing the domain.
+/// 5. The index of the last processed predicate of the domain.
+/// 6. Whether any predicates in the nogood are satisfied.
+fn get_domain_info(
+ context: &mut PropagationContext<'_>,
+ nogood: &[PredicateId],
+ propagated_domain: DomainId,
+) -> (
+ HashSet,
+ Option,
+ Option,
+ usize,
+ PredicateId,
+ bool,
+) {
+ // We keep track of the holes in the domains which are posted; these can be seen as
+ // "exceptions" to the removals of the domain
+ let mut exceptions: HashSet = Default::default();
+
+ // We need to keep track of whether there is a lower-bound and/or upper-bound predicate in
+ // the nogood.
+ let mut lower_bound = None;
+ let mut upper_bound = None;
+
+ let mut num_describing_domain = 0;
+ let mut last_describing_predicate_id = PredicateId::create_from_index(u32::MAX as usize);
+ let mut is_falsified = false;
+
+ for predicate_id in nogood.iter().copied() {
+ let predicate = context.get_predicate(predicate_id);
+
+ is_falsified |= context.is_predicate_id_falsified(predicate_id);
+
+ pumpkin_assert_moderate!(
+ predicate.get_domain() == propagated_domain
+ || context.is_predicate_id_satisfied(predicate_id),
+ "Expected {predicate} to be unassigned with {propagated_domain:?}",
+ );
+
+ // First, we filter out all of the predicates which are currently satisfied and
+ // which are not concerning the propagated domain id
+ if context.is_predicate_id_satisfied(predicate_id)
+ || predicate.get_domain() != propagated_domain
+ {
+ continue;
+ }
+
+ // Then we add the information of the predicates to our structures
+ num_describing_domain += 1;
+ last_describing_predicate_id = predicate_id;
+ match predicate.get_predicate_type() {
+ PredicateType::UpperBound => {
+ pumpkin_assert_simple!(upper_bound.is_none());
+ upper_bound = Some(predicate)
+ }
+ PredicateType::LowerBound => {
+ pumpkin_assert_simple!(lower_bound.is_none());
+ lower_bound = Some(predicate)
+ }
+ PredicateType::NotEqual => {
+ let _ = exceptions.insert(predicate.get_right_hand_side());
+ }
+ PredicateType::Equal => {}
}
}
+ (
+ exceptions,
+ lower_bound,
+ upper_bound,
+ num_describing_domain,
+ last_describing_predicate_id,
+ is_falsified,
+ )
}
/// Methods concerning the watchers and watch lists
impl NogoodPropagator {
/// Adds a watcher to the predicate.
- fn add_watcher(
+ pub(crate) fn add_watcher(
context: &mut PropagationContext,
predicate: PredicateId,
watcher: Watcher,
watch_lists: &mut KeyedVec>,
) {
- // First we resize the watch list to accomodate the new nogood
+ // First we resize the watch list to accommodate the new nogood
if predicate.id as usize >= watch_lists.len() {
watch_lists.resize((predicate.id + 1) as usize, Vec::default());
}
@@ -758,6 +1401,7 @@ impl NogoodPropagator {
assignments,
reason_store,
notification_engine,
+ self.propagation_mode,
);
}
@@ -781,6 +1425,7 @@ impl NogoodPropagator {
assignments,
reason_store,
notification_engine,
+ self.propagation_mode,
);
}
@@ -802,6 +1447,7 @@ impl NogoodPropagator {
assignments,
reason_store,
notification_engine,
+ self.propagation_mode,
);
}
@@ -947,6 +1593,10 @@ impl NogoodPropagator {
// A nogood is not removed if it is currently propagating at a non-root level.
// This means that the function may remove some nogoods from the first half if
// some of the bottom nogoods are currently propagating.
+ #[allow(
+ clippy::too_many_arguments,
+ reason = "Will run into borrow-issues when passing it by itself"
+ )]
fn remove_roughly_worst_half_nogood_ids(
handle: PropagatorHandle,
nogood_ids: &mut Vec,
@@ -955,6 +1605,7 @@ impl NogoodPropagator {
assignments: &Assignments,
reason_store: &mut ReasonStore,
notification_engine: &mut NotificationEngine,
+ propagation_mode: PropagationMode,
) -> bool {
// The removal is done in two phases.
// 1. Nogoods are deleted in the database, but the IDs are not removed from `nogood_ids`.
@@ -977,17 +1628,20 @@ impl NogoodPropagator {
}
// Skip nogoods which are propagating at a non-root level.
- if NogoodPropagator::is_nogood_propagating(
+ if propagation_mode.is_nogood_propagating(
handle,
&nogoods[id],
assignments,
reason_store,
id,
notification_engine,
- ) && assignments
- .get_checkpoint_for_predicate(&!notification_engine.get_predicate(nogoods[id][0]))
- .expect("A propagating predicate must have a decision level.")
- > 0
+ ) && (matches!(propagation_mode, PropagationMode::ExtendedNogoodPropagation)
+ || assignments
+ .get_checkpoint_for_predicate(
+ &!notification_engine.get_predicate(nogoods[id][0]),
+ )
+ .expect("A propagating predicate must have a decision level.")
+ > 0)
{
continue;
}
@@ -1111,7 +1765,11 @@ impl NogoodPropagator {
/// 3. Detecting predicates falsified at the root. In that case, the nogood is preprocessed
/// to the empty nogood.
/// 4. Conflicting predicates?
- fn preprocess_nogood(nogood: &mut Vec, context: &mut PropagationContext) {
+ fn preprocess_nogood(
+ nogood: &mut Vec,
+ context: &mut PropagationContext,
+ semantic_minimiser: &mut SemanticMinimiser,
+ ) {
pumpkin_assert_simple!(context.get_checkpoint() == 0);
// The code below is broken down into several parts
@@ -1119,6 +1777,7 @@ impl NogoodPropagator {
// assigned predicates in the final nogood. This could happen since the root bound can
// change since the initial time the semantic minimiser recorded it, so it would not know
// that a previously nonroot bound is now actually a root bound.
+ semantic_minimiser.minimise_internal(context.assignments(), nogood);
// We assume that duplicate predicates have been removed
@@ -1175,6 +1834,44 @@ impl NogoodPropagator {
return Ok(());
}
+ match self.propagation_mode {
+ PropagationMode::ExtendedNogoodPropagation => {
+ // We find all of the unasssigned predicates and get their domains
+ //
+ // If there is a falsified predicate then we do not propagate; also, if
+ // the nogood can be unit propagated, then
+ // we do not propagate
+ let mut is_falsified = false;
+ let mut num_unassigned = 0;
+ let unassigned_predicate_ids = nogood
+ .iter()
+ .filter_map(|predicate_id| {
+ if context.is_predicate_id_falsified(*predicate_id) {
+ is_falsified = true;
+ None
+ } else if context.is_predicate_id_satisfied(*predicate_id) {
+ None
+ } else {
+ num_unassigned += 1;
+ let predicate = context.get_predicate(*predicate_id);
+ Some(predicate.get_domain())
+ }
+ })
+ .collect::>();
+ if num_unassigned > 1 && !is_falsified && unassigned_predicate_ids.len() == 1 {
+ NogoodPropagator::extended_nogood_propagation(
+ context,
+ nogood,
+ *unassigned_predicate_ids.iter().next().unwrap(),
+ inference_code,
+ &mut NogoodPropagatorStatistics::default(),
+ Some(nogood_id),
+ )?;
+ }
+ }
+ PropagationMode::UnitPropagation => {}
+ }
+
let num_satisfied_predicates = nogood
.iter()
.filter(|predicate| {
diff --git a/pumpkin-crates/core/src/propagators/nogoods/propagation_mode.rs b/pumpkin-crates/core/src/propagators/nogoods/propagation_mode.rs
new file mode 100644
index 000000000..d9d7a3b9f
--- /dev/null
+++ b/pumpkin-crates/core/src/propagators/nogoods/propagation_mode.rs
@@ -0,0 +1,452 @@
+use crate::basic_types::PredicateId;
+use crate::containers::HashSet;
+use crate::containers::KeyedVec;
+use crate::engine::Assignments;
+use crate::engine::Lbd;
+use crate::engine::Reason;
+use crate::engine::notifications::NotificationEngine;
+use crate::engine::reason::ReasonStore;
+use crate::predicates::Predicate;
+use crate::proof::InferenceCode;
+use crate::propagation::PropagationContext;
+use crate::propagation::ReadDomains;
+use crate::propagators::nogoods::NogoodId;
+use crate::propagators::nogoods::NogoodInfo;
+use crate::propagators::nogoods::NogoodPropagator;
+use crate::propagators::nogoods::NogoodPropagatorStatistics;
+use crate::propagators::nogoods::Watcher;
+use crate::propagators::nogoods::arena_allocator::ArenaAllocator;
+use crate::propagators::nogoods::arena_allocator::NogoodIndex;
+use crate::pumpkin_assert_moderate;
+use crate::state::Conflict;
+use crate::state::PropagationStatusCP;
+use crate::state::PropagatorHandle;
+use crate::variables::DomainId;
+
+/// The type of propagation performed by the nogood propagator.
+#[derive(Clone, Copy, Debug, Default)]
+pub enum PropagationMode {
+ /// Uses the standard unit propagation.
+ ///
+ /// Unit propagation occurs under the following two conditions:
+ /// - There are no falsified predicates in the nogood.
+ /// - There is only a single unassigned predicate.
+ ///
+ /// If both of these conditions hold, then the unassigned predicate is propagated to be false.
+ #[default]
+ UnitPropagation,
+ /// Uses the extended nogood propagation algorithm \[1\].
+ ///
+ /// Extended nogood propagation occurs under the following two conditions:
+ /// - There are no falsified predicates in the nogood.
+ /// - The only unassigned predicates reason over the same variable `x`.
+ ///
+ /// If both of these conditions hold, then the nogood represents a domain description of `x`.
+ /// All of the values which would lead to all predicates in the nogood being false can be
+ /// removed.
+ ///
+ /// Note that this approach subsumes unit propagation.
+ ///
+ /// # Bibliography
+ /// - \[1\] I. Marijnissen, M. Flippo, and E. Demirović, ‘From Literals to Atomic Constraints:
+ /// Generalising Conflict-Driven Clause Learning for Constraint Programming’, in 32nd
+ /// International Conference on Principles and Practice of Constraint Programming (CP 2026),
+ /// 2026, vol. 379, p. 42:1-42:21.
+ ExtendedNogoodPropagation,
+}
+
+impl PropagationMode {
+ /// Returns a [`WatcherProcessingStatus`] based on the [`Predicate`] pointed to by `index` in
+ /// `nogood_predicates`.
+ pub(crate) fn process_potential_watcher(
+ &self,
+ context: &mut PropagationContext,
+ nogood_predicates: &[PredicateId],
+ index: usize,
+ ) -> WatcherProcessingStatus {
+ match self {
+ PropagationMode::ExtendedNogoodPropagation => {
+ // In the case of CPIP nogoods, we split into cases dependent on whether the
+ // predicate which we are processing reasons over the same domain.
+ //
+ // First, we store whether the current predicate reasons over the same domain as
+ // the predicate for which we are finding a new watcher.
+ let reasons_over_same_domain =
+ context.get_predicate(nogood_predicates[index]).get_domain()
+ == context.get_predicate(nogood_predicates[0]).get_domain();
+
+ // Next we split into several cases depending on the states of the to process
+ // predicate.
+ match context.evaluate_predicate_id(nogood_predicates[index]) {
+ None | Some(false) if !reasons_over_same_domain => {
+ // If the predicate is unassigned and does not reason over the same
+ // domain as the 0-th predicate, then we have found a new watch.
+ WatcherProcessingStatus::FoundNewWatch
+ }
+ Some(false) => {
+ assert!(reasons_over_same_domain);
+ // Otherwise, we have found a falsified predicate, and we need to
+ // update the zero-th predicate with this predicate.
+ WatcherProcessingStatus::FalsifiedZeroth
+ }
+ _ => WatcherProcessingStatus::Continue,
+ }
+ }
+ PropagationMode::UnitPropagation => {
+ // Standard case, we check whether the atomic constraint is not satisfied and
+ // replace it if we have found such a predicate.
+ if !context.is_predicate_id_satisfied(nogood_predicates[index]) {
+ WatcherProcessingStatus::FoundNewWatch
+ } else {
+ WatcherProcessingStatus::Continue
+ }
+ }
+ }
+ }
+
+ /// Computes from scratch whether extended nogood propagation can take place.
+ pub fn can_perform_extended_nogood_propagation(
+ &self,
+ context: &mut PropagationContext,
+ nogood_predicates: &[PredicateId],
+ ) -> Option {
+ // We find all of the unasssigned predicates and get their domains
+ //
+ // If there is a falsified predicate then we do not propagate; also,
+ // if the nogood can be unit
+ // propagated, then
+ // we do not propagate
+ let mut is_falsified = false;
+ let mut num_unassigned = 0;
+ let mut unassigned_domains = HashSet::new();
+ for predicate_id in nogood_predicates.iter() {
+ if context.is_predicate_id_falsified(*predicate_id) {
+ is_falsified = true;
+ break;
+ } else if context.is_predicate_id_satisfied(*predicate_id) {
+ continue;
+ } else {
+ num_unassigned += 1;
+ let predicate = context.get_predicate(*predicate_id);
+ let _ = unassigned_domains.insert(predicate.get_domain());
+ }
+ }
+
+ (num_unassigned > 1 && !is_falsified && unassigned_domains.len() == 1)
+ .then(|| *unassigned_domains.iter().next().unwrap())
+ }
+
+ /// Performs unit propagation or extended nogood propagation depending on what types of nogoods
+ /// are being learned.
+ ///
+ /// Note that this method does *not* check whether the propagation conditions have been met.
+ pub(crate) fn perform_propagation(
+ &self,
+ context: &mut PropagationContext,
+ nogood_predicates: &[PredicateId],
+ inference_code: &InferenceCode,
+ nogood_id: NogoodId,
+ statistics: &mut NogoodPropagatorStatistics,
+ ) -> PropagationStatusCP {
+ match self {
+ PropagationMode::ExtendedNogoodPropagation => {
+ let propagated_domain = context.get_predicate(nogood_predicates[0]).get_domain();
+ NogoodPropagator::extended_nogood_propagation(
+ context,
+ nogood_predicates,
+ propagated_domain,
+ inference_code,
+ statistics,
+ Some(nogood_id),
+ )?;
+ }
+ PropagationMode::UnitPropagation => {
+ statistics.num_unit_propagations += 1;
+
+ // There are two scenarios:
+ // nogood[0] is unassigned -> propagate the predicate to false
+ // nogood[0] is assigned true -> conflict.
+ let reason = Reason::DynamicLazy(nogood_id.id as u64);
+
+ let predicate = !context.get_predicate(nogood_predicates[0]);
+ let result = context.post(predicate, reason);
+ // If the propagation lead to a conflict.
+ if let Err(e) = result {
+ return Err(e.into());
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// Returns whether the provided `nogood` can be added as a permanent nogood (i.e., whether it
+ /// would propagate at the root level).
+ pub(crate) fn can_be_added_as_permanent(
+ &self,
+ context: &PropagationContext,
+ nogood: &[Predicate],
+ ) -> bool {
+ // We treat unit nogoods in a special way by adding it as a permanent nogood at the
+ // root-level; this is essentially the same as adding a predicate at the root level
+ if nogood.len() == 1 {
+ pumpkin_assert_moderate!(
+ context.get_checkpoint() == 0,
+ "A unit nogood should have backtracked to the root-level"
+ );
+ return true;
+ }
+ match self {
+ PropagationMode::ExtendedNogoodPropagation => {
+ // We maintain the invariant that the first two predicates in a learned clause
+ // point to different variables; if this does not hold, then it is a "unit" nogood
+ pumpkin_assert_moderate!(
+ context.get_checkpoint_for_predicate(nogood[1]).unwrap()
+ >= nogood
+ .iter()
+ .skip(2)
+ .filter(|predicate| predicate.get_domain() != nogood[0].get_domain())
+ .map(|predicate| context
+ .get_checkpoint_for_predicate(*predicate)
+ .unwrap())
+ .max()
+ .unwrap_or(0),
+ );
+ if nogood[0].get_domain() == nogood[1].get_domain() {
+ pumpkin_assert_moderate!(
+ context.get_checkpoint() == 0,
+ "A unit nogood should have backtracked to the root-level"
+ );
+ return true;
+ }
+ }
+ PropagationMode::UnitPropagation => {}
+ }
+
+ false
+ }
+
+ /// Calculates the LBD.
+ pub(crate) fn calculate_lbd(
+ &self,
+ context: &PropagationContext,
+ nogood: &[Predicate],
+ lbd_helper: &mut Lbd,
+ ) -> u32 {
+ match self {
+ PropagationMode::ExtendedNogoodPropagation => lbd_helper.compute_lbd(
+ &nogood
+ .iter()
+ .filter(|predicate| context.evaluate_predicate(**predicate).is_some())
+ .copied()
+ .collect::>(),
+ context,
+ ),
+ PropagationMode::UnitPropagation => {
+ // Skip the zero-th predicate since it is unassigned,
+ // but will be assigned at the level of the predicate at index one.
+ lbd_helper.compute_lbd(&nogood[1..], context)
+ }
+ }
+ }
+
+ /// Determines whether the nogood (pointed to by `id`) is propagating using the following
+ /// reasoning:
+ ///
+ /// - The predicate at position 0 is falsified; this is one of the conventions of the nogood
+ /// propagator
+ /// - The reason for the predicate is the nogood propagator
+ pub(crate) fn is_nogood_propagating(
+ &self,
+ handle: PropagatorHandle,
+ nogood: &[PredicateId],
+ assignments: &Assignments,
+ reason_store: &ReasonStore,
+ id: NogoodId,
+ notification_engine: &mut NotificationEngine,
+ ) -> bool {
+ match self {
+ PropagationMode::ExtendedNogoodPropagation => {
+ let potential_domain = notification_engine.get_predicate(nogood[0]).get_domain();
+ for predicate_id in nogood {
+ let predicate = notification_engine.get_predicate(*predicate_id);
+ if predicate.get_domain() == potential_domain {
+ continue;
+ }
+
+ if notification_engine.evaluate_predicate_id(*predicate_id, assignments)
+ != Some(true)
+ {
+ return false;
+ }
+ }
+ true
+ }
+ PropagationMode::UnitPropagation => {
+ if notification_engine.is_predicate_id_falsified(nogood[0], assignments) {
+ let trail_position = assignments
+ .get_trail_position(&!notification_engine.get_predicate(nogood[0]))
+ .unwrap();
+ let trail_entry = assignments.get_trail_entry(trail_position);
+ if let Some(reason_ref) = trail_entry.reason {
+ let propagator_id = reason_store.get_propagator(reason_ref);
+ let code = reason_store.get_lazy_code(reason_ref);
+
+ // We check whether the predicate was propagated by the nogood propagator
+ // first
+ let propagated_by_nogood_propagator =
+ propagator_id == handle.propagator_id();
+ // Then we check whether the lazy reason for the propagation was this
+ // particular nogood
+ let code_matches_id = code.is_none() || *code.unwrap() == id.id as u64;
+ return propagated_by_nogood_propagator && code_matches_id;
+ }
+ }
+ false
+ }
+ }
+ }
+
+ /// Adds the provided nogood to the nogood database as a permanent nogood (i.e., it cannot be
+ /// removed by nogood database management).
+ #[allow(
+ clippy::too_many_arguments,
+ reason = "Cannot take the nogood propagator; could be refactored in the future"
+ )]
+ #[allow(unused, reason = "Used when using feature flag")]
+ pub(crate) fn add_permanent_nogood_non_unit(
+ &mut self,
+ nogood: Vec,
+ input_nogood: &[Predicate],
+ inference_code: InferenceCode,
+ context: &mut PropagationContext<'_>,
+ nogood_predicates: &mut ArenaAllocator,
+ nogood_info: &mut KeyedVec,
+ inference_codes: &mut KeyedVec,
+ watch_lists: &mut KeyedVec>,
+ permanent_nogood_ids: &mut Vec,
+ statistics: &mut NogoodPropagatorStatistics,
+ ) -> Result<(), Conflict> {
+ #[cfg(feature = "check-propagations")]
+ let mut nogood = input_nogood
+ .iter()
+ .map(|predicate| context.get_id(*predicate))
+ .collect::>();
+
+ #[cfg(not(feature = "check-propagations"))]
+ let mut nogood = nogood
+ .iter()
+ .map(|predicate| context.get_id(*predicate))
+ .collect::>();
+
+ match self {
+ PropagationMode::ExtendedNogoodPropagation => {
+ // We try to find a predicate with a different domain than the 0-th predicate;
+ // this is the invariant that we maintain for the watchers
+ let other = nogood.iter().position(|&predicate_id| {
+ context.get_predicate(predicate_id).get_domain()
+ != context.get_predicate(nogood[0]).get_domain()
+ });
+
+ let first_domain = context.get_predicate(nogood[0]).get_domain();
+
+ if let Some(position) = other {
+ // If we can find predicate which reasons over a different domain than the
+ // 0th, then we proceed to add watchers
+ nogood.swap(1, position);
+
+ // Add the nogood to the database.
+ //
+ // Currently we always allocate a fresh ID
+ let nogood_id = nogood_predicates.insert(nogood);
+ let _ = nogood_info.push(NogoodInfo::new_permanent_nogood_info());
+ let _ = inference_codes.push(inference_code);
+
+ let watcher = Watcher {
+ nogood_id,
+ cached_predicate: nogood_predicates[nogood_id][0],
+ };
+
+ NogoodPropagator::add_watcher(
+ context,
+ nogood_predicates[nogood_id][0],
+ watcher,
+ watch_lists,
+ );
+
+ NogoodPropagator::add_watcher(
+ context,
+ nogood_predicates[nogood_id][1],
+ watcher,
+ watch_lists,
+ );
+
+ permanent_nogood_ids.push(nogood_id);
+
+ Ok(())
+ } else {
+ // Otherwise, we treat it as a "unit" nogood and we perform propagation and
+ // then do not add the nogood to the database.
+
+ NogoodPropagator::extended_nogood_propagation(
+ context,
+ &nogood,
+ first_domain,
+ &inference_code,
+ statistics,
+ None,
+ )?;
+
+ Ok(())
+ }
+ }
+ PropagationMode::UnitPropagation => {
+ // Add the nogood to the database.
+ //
+ // Currently we always allocate a fresh ID
+ let nogood_id = nogood_predicates.insert(nogood);
+ let _ = nogood_info.push(NogoodInfo::new_permanent_nogood_info());
+ let _ = inference_codes.push(inference_code);
+
+ permanent_nogood_ids.push(nogood_id);
+
+ let watcher = Watcher {
+ nogood_id,
+ cached_predicate: nogood_predicates[nogood_id][0],
+ };
+
+ NogoodPropagator::add_watcher(
+ context,
+ nogood_predicates[nogood_id][0],
+ watcher,
+ watch_lists,
+ );
+ NogoodPropagator::add_watcher(
+ context,
+ nogood_predicates[nogood_id][1],
+ watcher,
+ watch_lists,
+ );
+
+ Ok(())
+ }
+ }
+ }
+}
+
+/// The result of [`PropagationMode::process_potential_watcher`] indicating what should happen to
+/// the watchers of the nogood.
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum WatcherProcessingStatus {
+ /// No new watcher has been found, we should simply move to the next potential watcher.
+ Continue,
+ /// A new watcher has been found and it can replace the satisfied watcher.
+ FoundNewWatch,
+ /// **Only applicable when learning CPIP nogoods** - Indicates that a [`Predicate`] reasoning
+ /// over the same variable as the other watcher (i.e., the watcher for which a new watcher
+ /// is currently *not* being looked for) has been found which is falsified.
+ ///
+ /// This return value ensures that the watcher at index 0 is replaced with the currently
+ /// processed predicate.
+ FalsifiedZeroth,
+}
diff --git a/pumpkin-crates/core/src/propagators/nogoods/semantic_minimiser.rs b/pumpkin-crates/core/src/propagators/nogoods/semantic_minimiser.rs
new file mode 100644
index 000000000..ea9c4a7e4
--- /dev/null
+++ b/pumpkin-crates/core/src/propagators/nogoods/semantic_minimiser.rs
@@ -0,0 +1,267 @@
+use std::cmp;
+
+use crate::containers::HashSet;
+use crate::containers::KeyedVec;
+use crate::containers::SparseSet;
+use crate::create_statistics_struct;
+use crate::engine::Assignments;
+use crate::predicate;
+use crate::predicates::Predicate;
+use crate::predicates::PredicateType;
+use crate::statistics::moving_averages::CumulativeMovingAverage;
+use crate::statistics::moving_averages::MovingAverage;
+use crate::variables::DomainId;
+
+/// Minimiser that removes redundant [`Predicate`]s by analysing the semantic meaning of
+/// the predicates.
+///
+/// A [`Predicate`] is redundant if it is implied by another predicate in the nogood.
+///
+/// For example, if there is a nogood `[x >= 5] /\ [x >= 7] /\ [...] -> ⊥`, then the predicate [x
+/// >= 5] is redundant.
+#[derive(Clone, Debug)]
+pub(super) struct SemanticMinimiser {
+ original_domains: KeyedVec,
+ domains: KeyedVec,
+ present_ids: SparseSet,
+ helper: Vec,
+
+ statistics: SemanticMinimiserStatistics,
+}
+
+create_statistics_struct!(SemanticMinimiserStatistics {
+ /// The average number of atomic constraints removed by semantic minimisation during conflict analysis
+ average_number_of_removed_atomic_constraints_semantic: CumulativeMovingAverage,
+});
+
+impl Default for SemanticMinimiser {
+ fn default() -> Self {
+ Self {
+ original_domains: Default::default(),
+ domains: Default::default(),
+ present_ids: SparseSet::new(vec![]),
+ helper: Vec::default(),
+ statistics: SemanticMinimiserStatistics::default(),
+ }
+ }
+}
+
+impl SemanticMinimiser {
+ pub(crate) fn minimise_internal(
+ &mut self,
+ assignments: &Assignments,
+ nogood: &mut Vec,
+ ) {
+ self.accommodate(assignments);
+ self.clean_up();
+ self.apply_predicates(nogood);
+
+ let len_before = nogood.len();
+
+ // Compile the nogood based on the internal state.
+ // Add domain description to the helper.
+ for domain_id in self.present_ids.iter() {
+ // If at least one domain is inconsistent, we can stop.
+ if self.domains[domain_id].inconsistent {
+ *nogood = vec![Predicate::trivially_false()];
+ return;
+ }
+ self.domains[domain_id].add_domain_description_to_vector(
+ *domain_id,
+ &self.original_domains[domain_id],
+ &mut self.helper,
+ );
+ }
+ *nogood = self.helper.clone();
+
+ self.statistics
+ .average_number_of_removed_atomic_constraints_semantic
+ .add_term((len_before - nogood.len()) as u64);
+ }
+}
+
+impl SemanticMinimiser {
+ fn apply_predicates(&mut self, nogood: &Vec) {
+ // Apply the predicates to the domains in a straight-forward way.
+ // Later we will take into account the effect of holes on the domain.
+ for predicate in nogood {
+ self.present_ids.insert(predicate.get_domain());
+
+ let domain_id = predicate.get_domain();
+ let value = predicate.get_right_hand_side();
+
+ match predicate.get_predicate_type() {
+ PredicateType::LowerBound => {
+ self.domains[domain_id].tighten_lower_bound(value);
+ }
+ PredicateType::UpperBound => {
+ self.domains[domain_id].tighten_upper_bound(value);
+ }
+ PredicateType::NotEqual => {
+ self.domains[domain_id].add_hole(value);
+ }
+ PredicateType::Equal => {
+ self.domains[domain_id].assign(value);
+ }
+ }
+ }
+ for domain_id in self.present_ids.iter() {
+ self.domains[*domain_id].propagate_holes_on_lower_bound();
+ self.domains[*domain_id].propagate_holes_on_upper_bound();
+ self.domains[*domain_id].remove_redundant_holes();
+ self.domains[*domain_id].update_consistency();
+ }
+ }
+
+ fn accommodate(&mut self, context: &Assignments) {
+ assert!(self.domains.len() == self.original_domains.len());
+
+ while (self.domains.len() as u32) < context.num_domains() {
+ let domain_id = DomainId::new(self.domains.len() as u32);
+ let lower_bound = context.get_initial_lower_bound(domain_id);
+ let upper_bound = context.get_initial_upper_bound(domain_id);
+ let holes = context.get_initial_holes(domain_id);
+ self.grow(lower_bound, upper_bound, holes);
+ }
+ }
+
+ fn grow(&mut self, lower_bound: i32, upper_bound: i32, holes: Vec) {
+ let mut initial_domain = SimpleIntegerDomain {
+ lower_bound,
+ upper_bound,
+ holes: HashSet::from_iter(holes.iter().cloned()),
+ inconsistent: false,
+ };
+
+ initial_domain.propagate_holes_on_lower_bound();
+ initial_domain.propagate_holes_on_upper_bound();
+ initial_domain.remove_redundant_holes();
+ initial_domain.update_consistency();
+
+ let _ = self.original_domains.push(initial_domain.clone());
+ let _ = self.domains.push(initial_domain);
+ }
+
+ pub(crate) fn clean_up(&mut self) {
+ // Remove the domain ids from the present domain ids.
+ let vals: Vec = self.present_ids.iter().copied().collect();
+ for domain_id in vals {
+ self.present_ids.remove(&domain_id);
+ self.domains[domain_id] = self.original_domains[domain_id].clone();
+ }
+ self.helper.clear();
+ }
+}
+
+#[derive(Clone, Default, Debug)]
+struct SimpleIntegerDomain {
+ lower_bound: i32,
+ upper_bound: i32,
+ holes: HashSet,
+ inconsistent: bool,
+}
+
+impl SimpleIntegerDomain {
+ fn tighten_lower_bound(&mut self, lower_bound: i32) {
+ self.lower_bound = cmp::max(self.lower_bound, lower_bound);
+ }
+
+ fn tighten_upper_bound(&mut self, upper_bound: i32) {
+ self.upper_bound = cmp::min(self.upper_bound, upper_bound);
+ }
+
+ fn add_hole(&mut self, hole: i32) {
+ // Add the hole if it is within the domain.
+ // Note that we do not adjust bounds due to holes being at the border. This is taken care of
+ // by other functions (propagate bounds based on holes).
+ if self.lower_bound <= hole && hole <= self.upper_bound {
+ let _ = self.holes.insert(hole);
+ }
+ }
+
+ fn assign(&mut self, value: i32) {
+ // If the domains are inconsistent, or if the assigned value would make the domain
+ // inconsistent, declare inconsistency and stop.
+ if self.lower_bound > self.upper_bound
+ || self.lower_bound > value
+ || self.upper_bound < value
+ {
+ self.inconsistent = true;
+ }
+ // Otherwise, it is safe to apply the predicate.
+ // Note that we do not take into account holes here.
+ else {
+ self.lower_bound = value;
+ self.upper_bound = value;
+ }
+ }
+
+ fn propagate_holes_on_lower_bound(&mut self) {
+ while self.holes.contains(&self.lower_bound) && self.lower_bound <= self.upper_bound {
+ self.lower_bound += 1;
+ }
+ }
+
+ fn propagate_holes_on_upper_bound(&mut self) {
+ while self.holes.contains(&self.upper_bound) && self.lower_bound <= self.upper_bound {
+ self.upper_bound -= 1;
+ }
+ }
+
+ fn update_consistency(&mut self) {
+ // The domain may have already gotten in an inconsistent state due to equality predicates.
+ // Make sure not to make any changes if already inconsistent.
+ if !self.inconsistent {
+ self.inconsistent = self.lower_bound > self.upper_bound;
+ }
+ }
+
+ fn remove_redundant_holes(&mut self) {
+ // Do nothing if inconsistent.
+ if self.inconsistent {
+ return;
+ }
+ // Only keep holes that fall within the current bounds.
+ self.holes
+ .retain(|hole| self.lower_bound < *hole && *hole < self.upper_bound);
+ }
+
+ fn add_domain_description_to_vector(
+ &self,
+ domain_id: DomainId,
+ original_domain: &SimpleIntegerDomain,
+ description: &mut Vec,
+ ) {
+ // If the domain assigned at a nonroot level, this is just one predicate.
+ if self.lower_bound == self.upper_bound
+ && self.lower_bound != original_domain.lower_bound
+ && self.upper_bound != original_domain.upper_bound
+ {
+ description.push(predicate![domain_id == self.lower_bound]);
+ return;
+ }
+
+ // Add bounds but avoid root assignments.
+ if self.lower_bound != original_domain.lower_bound {
+ description.push(predicate![domain_id >= self.lower_bound]);
+ }
+
+ if self.upper_bound != original_domain.upper_bound {
+ description.push(predicate![domain_id <= self.upper_bound]);
+ }
+
+ // Add nonroot holes.
+ for hole in self.holes.iter() {
+ // Only record holes that are within the lower and upper bound,
+ // that are not root assignments.
+ // Since bound values cannot be in the holes,
+ // we can use '<' or '>'.
+ if self.lower_bound < *hole
+ && *hole < self.upper_bound
+ && !original_domain.holes.contains(hole)
+ {
+ description.push(predicate![domain_id != *hole])
+ }
+ }
+ }
+}
diff --git a/pumpkin-crates/propagators/CHANGELOG.md b/pumpkin-crates/propagators/CHANGELOG.md
index 9f5744d50..b16378a04 100644
--- a/pumpkin-crates/propagators/CHANGELOG.md
+++ b/pumpkin-crates/propagators/CHANGELOG.md
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-propagators-v0.3.0...pumpkin-propagators-v0.4.0) - 2026-06-23
+
+### Fixed
+
+- *(pumpkin-core)* Creation and Insertion in Sparse Set ([#395](https://github.com/ConSol-Lab/Pumpkin/pull/395))
+- *(pumpkin-solver)* Update packages and fix duplicate dependencies ([#412](https://github.com/ConSol-Lab/Pumpkin/pull/412))
+- Time-table checker not detecting conflicts properly ([#405](https://github.com/ConSol-Lab/Pumpkin/pull/405))
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+- *(deps)* bump enumset from 1.1.12 to 1.1.13 ([#452](https://github.com/ConSol-Lab/Pumpkin/pull/452))
+- *(deps)* bump enumset from 1.1.11 to 1.1.12 ([#446](https://github.com/ConSol-Lab/Pumpkin/pull/446))
+- *(deps)* bump enumset from 1.1.10 to 1.1.11 ([#441](https://github.com/ConSol-Lab/Pumpkin/pull/441))
+- *(pumpkin-solver)* Remove vergen as a dependency ([#438](https://github.com/ConSol-Lab/Pumpkin/pull/438))
+- *(pumpkin-core)* Attach `InferenceCode` instead of `Predicate` ([#433](https://github.com/ConSol-Lab/Pumpkin/pull/433))
+- *(deps)* bump convert_case from 0.8.0 to 0.11.0 ([#428](https://github.com/ConSol-Lab/Pumpkin/pull/428))
+- *(deps)* bump bitfield-struct from 0.9.5 to 0.13.0 ([#421](https://github.com/ConSol-Lab/Pumpkin/pull/421))
+- *(pumpkin-propagators)* Move away from TestSolver in propagator tests ([#387](https://github.com/ConSol-Lab/Pumpkin/pull/387))
+- *(pumpkin-core)* Cleanup creation of propagator conflict ([#399](https://github.com/ConSol-Lab/Pumpkin/pull/399))
+- add utility method fixed value and replace is_fixed wherever possible ([#393](https://github.com/ConSol-Lab/Pumpkin/pull/393))
+
## [0.3.0](https://github.com/consol-lab/pumpkin/releases/tag/pumpkin-propagators-v0.3.0) - 2026-02-10
### Added
diff --git a/pumpkin-crates/propagators/Cargo.toml b/pumpkin-crates/propagators/Cargo.toml
index c7794e235..4648bce09 100644
--- a/pumpkin-crates/propagators/Cargo.toml
+++ b/pumpkin-crates/propagators/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pumpkin-propagators"
-version = "0.3.0"
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
@@ -11,8 +11,8 @@ description = "The propagators of the Pumpkin constraint programming solver."
workspace = true
[dependencies]
-pumpkin-core = { version = "0.3.0", path = "../core" }
-pumpkin-checking = { version = "0.3.0", path = "../checking" }
+pumpkin-core = { version = "0.4.0", path = "../core" }
+pumpkin-checking = { version = "0.4.0", path = "../checking" }
enumset = "1.1.13"
bitfield-struct = "0.13.0"
convert_case = "0.11.0"
diff --git a/pumpkin-macros/Cargo.toml b/pumpkin-macros/Cargo.toml
index 58651bef4..bfc6b2881 100644
--- a/pumpkin-macros/Cargo.toml
+++ b/pumpkin-macros/Cargo.toml
@@ -14,7 +14,7 @@ path = "src/lib.rs"
proc-macro = true
[dependencies]
-itertools = "0.14.0"
+itertools = "0.15.0"
proc-macro2 = "1.0.89"
quote = "1.0.37"
stringcase = "0.4.0"
diff --git a/pumpkin-proof-processor/Cargo.toml b/pumpkin-proof-processor/Cargo.toml
index 43b3ba945..ca16594a7 100644
--- a/pumpkin-proof-processor/Cargo.toml
+++ b/pumpkin-proof-processor/Cargo.toml
@@ -1,22 +1,23 @@
[package]
name = "pumpkin-proof-processor"
-version = "0.3.0"
+version.workspace = true
repository.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
+publish = false
[dependencies]
anyhow = "1.0.99"
clap = { version = "4.5.47", features = ["derive"] }
clap-verbosity-flag = "3.0.4"
drcp-format = { version = "0.3.1", path = "../drcp-format" }
-fzn-rs = { version = "0.1.0", path = "../fzn-rs" }
+fzn-rs = { version = "0.1.1", path = "../fzn-rs" }
flate2 = { version = "1.1.2" }
env_logger = "0.11.10"
-pumpkin-core = { version = "0.3.0", path = "../pumpkin-crates/core" }
-pumpkin-propagators = { version = "0.3.0", path = "../pumpkin-crates/propagators" }
-log = "0.4.29"
+pumpkin-core = { version = "0.4.0", path = "../pumpkin-crates/core" }
+pumpkin-propagators = { version = "0.4.0", path = "../pumpkin-crates/propagators" }
+log = "0.4.30"
thiserror = "2.0.18"
[dev-dependencies]
diff --git a/pumpkin-solver-py/Cargo.toml b/pumpkin-solver-py/Cargo.toml
index 897e4772f..b340d4987 100644
--- a/pumpkin-solver-py/Cargo.toml
+++ b/pumpkin-solver-py/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "pumpkin-solver-py"
-version = "0.3.0"
+version.workspace = true
description = "The Python interface for the Pumpkin solver library."
authors.workspace = true
license.workspace = true
@@ -16,10 +16,10 @@ crate-type = ["cdylib"]
doc = false
[dependencies]
-pyo3 = { version = "0.28.3", features= ["extension-module"] }
-pumpkin-solver = { version = "0.3.0", path = "../pumpkin-solver" }
-pumpkin-constraints = { version = "0.3.0", path = "../pumpkin-crates/constraints", features=["clap"] }
-pumpkin-conflict-resolvers = { version = "0.3.0", path = "../pumpkin-crates/conflict-resolvers/"}
+pyo3 = { version = "0.29.0", features= ["extension-module"] }
+pumpkin-solver = { version = "0.4.0", path = "../pumpkin-solver" }
+pumpkin-constraints = { version = "0.4.0", path = "../pumpkin-crates/constraints", features=["clap"] }
+pumpkin-conflict-resolvers = { version = "0.4.0", path = "../pumpkin-crates/conflict-resolvers/"}
[build-dependencies]
-pyo3-build-config = "0.28.3"
+pyo3-build-config = "0.29.0"
diff --git a/pumpkin-solver/CHANGELOG.md b/pumpkin-solver/CHANGELOG.md
index 2891a6851..02d407892 100644
--- a/pumpkin-solver/CHANGELOG.md
+++ b/pumpkin-solver/CHANGELOG.md
@@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.4.0](https://github.com/ConSol-Lab/Pumpkin/compare/pumpkin-solver-v0.3.0...pumpkin-solver-v0.4.0) - 2026-06-23
+
+### Added
+
+- *(pumpkin-solver)* Check derived nogoods during search ([#373](https://github.com/ConSol-Lab/Pumpkin/pull/373))
+- *(pumpkin-solver)* Expose version and git commit in crate ([#404](https://github.com/ConSol-Lab/Pumpkin/pull/404))
+- *(pumpkin-proof-processor)* Introduce the proof processor in the main branch ([#371](https://github.com/ConSol-Lab/Pumpkin/pull/371))
+
+### Fixed
+
+- README links + updating component overview ([#453](https://github.com/ConSol-Lab/Pumpkin/pull/453))
+- *(pumpkin-solver)* Update packages and fix duplicate dependencies ([#412](https://github.com/ConSol-Lab/Pumpkin/pull/412))
+- *(pumpkin-solver)* New clippy suggestion ([#410](https://github.com/ConSol-Lab/Pumpkin/pull/410))
+
+### Other
+
+- Use central version number for all pumpkin-* crates ([#470](https://github.com/ConSol-Lab/Pumpkin/pull/470))
+- add CPAIOR26 and ICAPS26 papers to README ([#465](https://github.com/ConSol-Lab/Pumpkin/pull/465))
+- *(deps)* bump log from 0.4.29 to 0.4.30 ([#458](https://github.com/ConSol-Lab/Pumpkin/pull/458))
+- avoid rebuilding unnecessarily ([#450](https://github.com/ConSol-Lab/Pumpkin/pull/450))
+- *(deps)* bump cc from 1.2.61 to 1.2.62 ([#447](https://github.com/ConSol-Lab/Pumpkin/pull/447))
+- *(pumpkin-solver)* Remove vergen as a dependency ([#438](https://github.com/ConSol-Lab/Pumpkin/pull/438))
+- *(deps)* bump cc from 1.2.60 to 1.2.61 ([#437](https://github.com/ConSol-Lab/Pumpkin/pull/437))
+- *(deps)* bump signal-hook from 0.3.18 to 0.4.4 ([#431](https://github.com/ConSol-Lab/Pumpkin/pull/431))
+- *(deps)* bump stringcase from 0.3.0 to 0.4.0 ([#422](https://github.com/ConSol-Lab/Pumpkin/pull/422))
+- *(deps)* bump env_logger from 0.10.2 to 0.11.10 ([#419](https://github.com/ConSol-Lab/Pumpkin/pull/419))
+- update README papers ([#391](https://github.com/ConSol-Lab/Pumpkin/pull/391))
+- *(pumpkin-solver)* Don't pin a checker version for tests
+
## [0.3.0](https://github.com/consol-lab/pumpkin/compare/pumpkin-solver-v0.2.2...pumpkin-solver-v0.3.0) - 2026-02-10
### Fixed
diff --git a/pumpkin-solver/Cargo.toml b/pumpkin-solver/Cargo.toml
index d1060ef7f..b60166aa8 100644
--- a/pumpkin-solver/Cargo.toml
+++ b/pumpkin-solver/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "pumpkin-solver"
-version = "0.3.0"
description = "The Pumpkin combinatorial optimisation solver library."
readme = "../README.md"
+version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
@@ -12,11 +12,11 @@ repository.workspace = true
clap = { version = "4.5.17", features = ["derive"] }
env_logger = "0.11.10"
flatzinc = "0.3.21"
-log = "0.4.27"
-pumpkin-core = { version = "0.3.0", path = "../pumpkin-crates/core/", features = ["clap"] }
-pumpkin-constraints = { version = "0.3.0", path = "../pumpkin-crates/constraints/"}
-pumpkin-propagators = { version = "0.3.0", path = "../pumpkin-crates/propagators/", features=["clap"]}
-pumpkin-conflict-resolvers = { version = "0.3.0", path = "../pumpkin-crates/conflict-resolvers/"}
+log = "0.4.30"
+pumpkin-core = { version = "0.4.0", path = "../pumpkin-crates/core/", features = ["clap"] }
+pumpkin-constraints = { version = "0.4.0", path = "../pumpkin-crates/constraints/"}
+pumpkin-propagators = { version = "0.4.0", path = "../pumpkin-crates/propagators/", features=["clap"]}
+pumpkin-conflict-resolvers = { version = "0.4.0", path = "../pumpkin-crates/conflict-resolvers/"}
signal-hook = "0.4.4"
thiserror = "2.0.12"
diff --git a/pumpkin-solver/src/bin/pumpkin-solver/main.rs b/pumpkin-solver/src/bin/pumpkin-solver/main.rs
index 68719848f..eec9f2526 100644
--- a/pumpkin-solver/src/bin/pumpkin-solver/main.rs
+++ b/pumpkin-solver/src/bin/pumpkin-solver/main.rs
@@ -24,9 +24,10 @@ use maxsat::PseudoBooleanEncoding;
use parsers::dimacs::SolverArgs;
use parsers::dimacs::SolverDimacsSink;
use parsers::dimacs::parse_cnf;
-use pumpkin_conflict_resolvers::resolvers::AnalysisMode;
use pumpkin_conflict_resolvers::resolvers::NoLearningResolver;
use pumpkin_conflict_resolvers::resolvers::ResolutionResolver;
+use pumpkin_core::conflict_resolving::AnalysisMode;
+use pumpkin_core::propagation::Priority;
use pumpkin_propagators::cumulative::options::CumulativeOptions;
use pumpkin_propagators::cumulative::options::CumulativePropagationMethod;
use pumpkin_propagators::cumulative::time_table::CumulativeExplanationType;
@@ -401,6 +402,10 @@ struct Args {
/// The amount of memory (in MB) that is preallocated for storing nogoods.
#[arg(long = "memory-preallocated", default_value_t = 50)]
memory_preallocated: usize,
+
+ /// The priority of the nogood propagator.
+ #[arg(long = "nogood-priority", value_enum, default_value_t)]
+ nogood_propagator_priority: Priority,
}
fn configure_logging(
@@ -565,16 +570,19 @@ fn run() -> PumpkinResult<()> {
lbd_threshold_low: args.learning_low_lbd_threshold,
lbd_threshold_high: args.learning_high_lbd_threshold,
activity_bump_increment: 1.0,
+ nogood_propagator_priority: args.nogood_propagator_priority,
};
+ let should_minimise_nogoods = !args.no_learning_clause_minimisation;
let solver_options = SolverOptions {
// 1 MB is 1_000_000 bytes
memory_preallocated: args.memory_preallocated,
restart_options,
- should_minimise_nogoods: !args.no_learning_clause_minimisation,
+ should_minimise_nogoods,
random_generator: SmallRng::seed_from_u64(args.random_seed),
proof_log,
learning_options,
+ analysis_mode: args.conflict_resolver,
};
let time_limit = args.time_limit.map(Duration::from_millis);
@@ -612,7 +620,7 @@ fn run() -> PumpkinResult<()> {
},
NoLearningResolver,
)?,
- ConflictResolverType::UIP => flatzinc::solve(
+ ConflictResolverType::OneUIP => flatzinc::solve(
Solver::with_options(solver_options),
instance_path,
time_limit,
@@ -635,6 +643,66 @@ fn run() -> PumpkinResult<()> {
!args.no_learning_clause_minimisation,
),
)?,
+ ConflictResolverType::ExtendedCPIP => flatzinc::solve(
+ Solver::with_options(solver_options),
+ instance_path,
+ time_limit,
+ FlatZincOptions {
+ free_search: args.free_search,
+ all_solutions: args.all_solutions,
+ cumulative_options: CumulativeOptions::new(
+ args.cumulative_allow_holes,
+ args.cumulative_explanation_type,
+ !args.cumulative_single_profiles,
+ args.cumulative_propagation_method,
+ args.cumulative_incremental_backtracking,
+ ),
+ optimisation_strategy: args.optimisation_strategy,
+ proof_type: args.proof_path.map(|_| args.proof_type),
+ verbose: args.verbose,
+ },
+ ResolutionResolver::new(AnalysisMode::CPIP, should_minimise_nogoods),
+ )?,
+ ConflictResolverType::BoundsExtendedCPIP => flatzinc::solve(
+ Solver::with_options(solver_options),
+ instance_path,
+ time_limit,
+ FlatZincOptions {
+ free_search: args.free_search,
+ all_solutions: args.all_solutions,
+ cumulative_options: CumulativeOptions::new(
+ args.cumulative_allow_holes,
+ args.cumulative_explanation_type,
+ !args.cumulative_single_profiles,
+ args.cumulative_propagation_method,
+ args.cumulative_incremental_backtracking,
+ ),
+ optimisation_strategy: args.optimisation_strategy,
+ proof_type: args.proof_path.map(|_| args.proof_type),
+ verbose: args.verbose,
+ },
+ ResolutionResolver::new(AnalysisMode::BoundsCPIP, should_minimise_nogoods),
+ )?,
+ ConflictResolverType::AllDecision => flatzinc::solve(
+ Solver::with_options(solver_options),
+ instance_path,
+ time_limit,
+ FlatZincOptions {
+ free_search: args.free_search,
+ all_solutions: args.all_solutions,
+ cumulative_options: CumulativeOptions::new(
+ args.cumulative_allow_holes,
+ args.cumulative_explanation_type,
+ !args.cumulative_single_profiles,
+ args.cumulative_propagation_method,
+ args.cumulative_incremental_backtracking,
+ ),
+ optimisation_strategy: args.optimisation_strategy,
+ proof_type: args.proof_path.map(|_| args.proof_type),
+ verbose: args.verbose,
+ },
+ ResolutionResolver::new(AnalysisMode::AllDecision, should_minimise_nogoods),
+ )?,
},
}
diff --git a/pumpkin-solver/tests/cnf/.gitignore b/pumpkin-solver/tests/cnf/.gitignore
index 9e9116c58..2e114d5da 100644
--- a/pumpkin-solver/tests/cnf/.gitignore
+++ b/pumpkin-solver/tests/cnf/.gitignore
@@ -3,3 +3,4 @@
*.drcp
*.err
*.log
+*.drcp
diff --git a/pumpkin-solver/tests/mzn_infeasible/.gitignore b/pumpkin-solver/tests/mzn_infeasible/.gitignore
index 311556b7d..69bf5d579 100644
--- a/pumpkin-solver/tests/mzn_infeasible/.gitignore
+++ b/pumpkin-solver/tests/mzn_infeasible/.gitignore
@@ -4,3 +4,4 @@
*.drcp.gz
*.drcp
*.lits
+*.drcp
diff --git a/pumpkin-solver/tests/mzn_optimization/.gitignore b/pumpkin-solver/tests/mzn_optimization/.gitignore
index 311556b7d..69bf5d579 100644
--- a/pumpkin-solver/tests/mzn_optimization/.gitignore
+++ b/pumpkin-solver/tests/mzn_optimization/.gitignore
@@ -4,3 +4,4 @@
*.drcp.gz
*.drcp
*.lits
+*.drcp
diff --git a/release-plz.toml b/release-plz.toml
index b70f841fa..b0d7e798f 100644
--- a/release-plz.toml
+++ b/release-plz.toml
@@ -23,7 +23,6 @@ pr_body = """
```text
{{ release.breaking_changes }}
```{% endif %}{% endfor %}
-{% endif %}
---
This PR was generated with [release-plz](https://github.com/release-plz/release-plz/).
"""
From 52bbd1e8865258dc2d7dced5e3485c2981c239bf Mon Sep 17 00:00:00 2001
From: Maarten Flippo
Date: Tue, 21 Jul 2026 11:24:56 +0100
Subject: [PATCH 11/11] Update docs
---
pumpkin-crates/core/src/propagation/constructor.rs | 3 +--
.../core/src/propagation/contexts/propagation_context.rs | 2 --
pumpkin-crates/core/src/propagation/mod.rs | 2 +-
pumpkin-crates/core/src/propagation/propagator.rs | 2 +-
.../core/src/propagators/nogoods/nogood_propagator.rs | 3 ++-
5 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/pumpkin-crates/core/src/propagation/constructor.rs b/pumpkin-crates/core/src/propagation/constructor.rs
index 5d79e2890..6aa22776b 100644
--- a/pumpkin-crates/core/src/propagation/constructor.rs
+++ b/pumpkin-crates/core/src/propagation/constructor.rs
@@ -134,8 +134,7 @@ impl PropagatorConstructorContext<'_> {
}
/// Subscribes the propagator to the given [`DomainEvents`] when they are undone during
- /// backtracking. This method is complementary to [`PropagatorConstructorContext::register`],
- /// the [`LocalId`]s provided to both of these method should be the same for the same variable.
+ /// backtracking. The [`LocalId`]s used to register the variable should be the same here.
///
/// The domain events determine when [`Propagator::notify_backtrack()`] will be called on the
/// propagator. The [`LocalId`] is internal information related to the propagator,
diff --git a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
index 1606b0dce..57187434c 100644
--- a/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
+++ b/pumpkin-crates/core/src/propagation/contexts/propagation_context.rs
@@ -143,8 +143,6 @@ impl<'a> PropagationContext<'a> {
}
/// Subscribes the propagator to the given [`DomainEvents`].
- ///
- /// See [`PropagatorConstructorContext::register`] for more information.
pub fn register_domain_event(
&mut self,
var: impl IntegerVariable,
diff --git a/pumpkin-crates/core/src/propagation/mod.rs b/pumpkin-crates/core/src/propagation/mod.rs
index c74ee7106..1646dda63 100644
--- a/pumpkin-crates/core/src/propagation/mod.rs
+++ b/pumpkin-crates/core/src/propagation/mod.rs
@@ -30,7 +30,7 @@
//! implement for this trait is [`Propagator::propagate`], which performs the domain reduction.
//!
//! A propagator is created by a [`PropagatorConstructor`]. The constructor is responsible for
-//! registering to [`DomainEvents`] (using [`PropagatorConstructorContext::register`]), and setting
+//! registering to [`DomainEvents`] and setting
//! up the state of the propagator. The constructor is provided a [`PropagatorConstructorContext`],
//! which has all the available functions allowing the propagator to hook into the solver state.
//!
diff --git a/pumpkin-crates/core/src/propagation/propagator.rs b/pumpkin-crates/core/src/propagation/propagator.rs
index 2ccffce40..9cb6690c6 100644
--- a/pumpkin-crates/core/src/propagation/propagator.rs
+++ b/pumpkin-crates/core/src/propagation/propagator.rs
@@ -113,7 +113,7 @@ pub trait Propagator: Downcast + DynClone {
/// Returns whether the propagator should be enqueued for propagation when a [`DomainEvent`]
/// happens to one of the variables the propagator is subscribed to (as registered during
- /// creation with [`PropagatorConstructor`] using [`PropagatorConstructorContext::register`]).
+ /// creation with [`PropagatorConstructor`].
///
/// This can be used to incrementally maintain data structures or perform propagations, and
/// should only be used for computationally cheap logic. Expensive computation should be
diff --git a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
index 95e415dd4..82936e569 100644
--- a/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
+++ b/pumpkin-crates/core/src/propagators/nogoods/nogood_propagator.rs
@@ -206,7 +206,8 @@ impl PropagatorConstructor for NogoodPropagatorConstructor {
#[derive(Clone, Copy, Debug)]
pub(crate) struct Watcher {
pub(crate) nogood_id: NogoodId,
- pub(crate) cached_predicate: PredicateId,}
+ pub(crate) cached_predicate: PredicateId,
+}
/// Keeps track of three tiers of nogoods:
/// - "low" LBD nogoods