Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

Pumpkin is a constraint programming (CP) solver written in pure Rust, based on the lazy clause generation paradigm. It supports proof logging via DRAT (SAT) and DRCP (CP) certificates.

## Build & Development Commands

```bash
# Build
cargo build
cargo build --release

# Test (standard CI command)
cargo test --release --no-fail-fast --features pumpkin-solver/check-propagations

# Run a single test
cargo test --release --features pumpkin-solver/check-propagations <test_name>

# Format (requires nightly)
cargo +nightly fmt
cargo +nightly fmt --check # check only

# Lint (requires nightly)
cargo +nightly clippy --all-targets -- -Dwarnings

# Docs
cargo doc --no-deps

# License check
cargo deny check

# WASM tests (for pumpkin-crates/core)
wasm-pack test --release --node

# Python tests
cd pumpkin-solver-py && pytest
```

Scripts in `scripts/` (`fmt.sh`, `clippy.sh`, `documentation.sh`, `deny.sh`) mirror the CI commands and are also run by the pre-commit hook (`.githooks/pre-commit`).

## Workspace Architecture

The project is a Cargo workspace (edition 2024, resolver 2). Crates are grouped by role:

### Core Engine (`pumpkin-crates/`)
- **`core`** — The main solver engine. Contains the CDCL loop, propagation engine, nogood learning, branching heuristics, and proof logging infrastructure. This is the heart of the solver.
- **`checking`** — Shared types used by both `core` and `pumpkin-checker` (avoids circular deps).
- **`propagators`** — Implementations of CP propagators (arithmetic, cumulative, disjunctive, element, etc.).
- **`conflict-resolvers`** — Pluggable conflict analysis strategies for nogood derivation.
- **`constraints`** — High-level constraint API built on top of `core`.

### Interfaces
- **`pumpkin-solver`** — CLI binary. Accepts CNF, WCNF (MaxSAT), and FlatZinc input formats.
- **`pumpkin-solver-py`** — Python bindings via PyO3.

### Proof Infrastructure
- **`drcp-format`** — Reading/writing the DRCP proof certificate format.
- **`pumpkin-checker`** — Standalone proof verification tool.
- **`pumpkin-proof-processor`** — Proof transformation/preprocessing utility.
- **`drcp-debugger`** — Debugging tool for DRCP proofs.

### Parsing & Utilities
- **`fzn-rs`** / **`fzn-rs-derive`** — FlatZinc parser and derive macros.
- **`pumpkin-macros`** — Procedural macros used across the workspace.
- **`minizinc/`** — MiniZinc integration (solver plugin).

## Key Design Concepts

- **Lazy Clause Generation (LCG)**: The solver operates on a hybrid SAT/CP model. CP propagators generate explanations (clauses/nogoods) on demand during conflict analysis.
- **Proof Logging**: Every inference can be certified. The `core` crate threads proof-logging through propagators via a `Proof` type. The `check-propagations` feature enables runtime validation of propagator explanations during tests.
- **Propagator Interface**: Custom propagators implement the `Propagator` trait in `pumpkin-crates/core`. They must provide both `propagate` and `explain` methods for proof soundness.
- **Feature Flags**: `pumpkin-solver/check-propagations` enables expensive correctness assertions — always enable this when running tests.

## Code Style

- **Rust edition 2024**, stable toolchain (see `rust-toolchain.toml`), but formatting/linting requires nightly.
- Line length: 120 chars for comments (`rustfmt.toml`).
- Imports: grouped (std / external / crate), single-line style enforced.
- Workspace-level lints are configured in the root `Cargo.toml` — check there before suppressing warnings locally.
26 changes: 26 additions & 0 deletions pumpkin-crates/propagators/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,29 @@
//! [`pumpkin_core::propagation`].
mod propagators;
pub use propagators::*;
#[cfg(test)]
use pumpkin_core::state::State;
#[cfg(test)]
use pumpkin_core::variables::DomainId;

/// Utilities that simplify test code using the [`State`].
#[cfg(test)]
pub(crate) trait StateExt {
/// Assert that the bounds of the given `domain_id` match the provided `lower_bound` and
/// `upper_bound`.
fn assert_bounds(&self, domain_id: DomainId, lower_bound: i32, upper_bound: i32);
}

#[cfg(test)]
impl StateExt for State {
fn assert_bounds(&self, domain_id: DomainId, lower_bound: i32, upper_bound: i32) {
let actual_lb = self.lower_bound(domain_id);
let actual_ub = self.upper_bound(domain_id);

assert_eq!(
(lower_bound, upper_bound),
(actual_lb, actual_ub),
"The expected bounds [{lower_bound}..{upper_bound}] did not match the actual bounds [{actual_lb}..{actual_ub}]"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,124 +205,118 @@ where
}
}

#[allow(deprecated, reason = "Will be refactored")]
#[cfg(test)]
mod tests {
use pumpkin_core::TestSolver;
use pumpkin_core::state::State;

use super::*;
use crate::StateExt;

#[test]
fn absolute_bounds_are_propagated_at_initialise() {
let mut solver = TestSolver::default();
let mut state = State::default();

let signed = solver.new_variable(-3, 4);
let absolute = solver.new_variable(-2, 10);
let constraint_tag = solver.new_constraint_tag();
let signed = state.new_interval_variable(-3, 4, None);
let absolute = state.new_interval_variable(-2, 10, None);
let constraint_tag = state.new_constraint_tag();

let _ = solver
.new_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
})
.expect("no empty domains");
let _ = state.add_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
});
state.propagate_to_fixed_point().expect("no empty domains");

solver.assert_bounds(absolute, 0, 4);
state.assert_bounds(absolute, 0, 4);
}

#[test]
fn signed_bounds_are_propagated_at_initialise() {
let mut solver = TestSolver::default();
let mut state = State::default();

let signed = solver.new_variable(-5, 5);
let absolute = solver.new_variable(0, 3);
let constraint_tag = solver.new_constraint_tag();
let signed = state.new_interval_variable(-5, 5, None);
let absolute = state.new_interval_variable(0, 3, None);
let constraint_tag = state.new_constraint_tag();

let _ = solver
.new_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
})
.expect("no empty domains");
let _ = state.add_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
});
state.propagate_to_fixed_point().expect("no empty domains");

solver.assert_bounds(signed, -3, 3);
state.assert_bounds(signed, -3, 3);
}

#[test]
fn absolute_lower_bound_can_be_strictly_positive() {
let mut solver = TestSolver::default();
let mut state = State::default();

let signed = solver.new_variable(3, 6);
let absolute = solver.new_variable(0, 10);
let constraint_tag = solver.new_constraint_tag();
let signed = state.new_interval_variable(3, 6, None);
let absolute = state.new_interval_variable(0, 10, None);
let constraint_tag = state.new_constraint_tag();

let _ = solver
.new_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
})
.expect("no empty domains");
let _ = state.add_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
});
state.propagate_to_fixed_point().expect("no empty domains");

solver.assert_bounds(absolute, 3, 6);
state.assert_bounds(absolute, 3, 6);
}

#[test]
fn strictly_negative_signed_value_can_propagate_lower_bound_on_absolute() {
let mut solver = TestSolver::default();
let mut state = State::default();

let signed = solver.new_variable(-5, -3);
let absolute = solver.new_variable(1, 5);
let constraint_tag = solver.new_constraint_tag();
let signed = state.new_interval_variable(-5, -3, None);
let absolute = state.new_interval_variable(1, 5, None);
let constraint_tag = state.new_constraint_tag();

let _ = solver
.new_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
})
.expect("no empty domains");
let _ = state.add_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
});
state.propagate_to_fixed_point().expect("no empty domains");

solver.assert_bounds(absolute, 3, 5);
state.assert_bounds(absolute, 3, 5);
}

#[test]
fn lower_bound_on_absolute_can_propagate_negative_upper_bound_on_signed() {
let mut solver = TestSolver::default();
let mut state = State::default();

let signed = solver.new_variable(-5, 0);
let absolute = solver.new_variable(1, 5);
let constraint_tag = solver.new_constraint_tag();
let signed = state.new_interval_variable(-5, 0, None);
let absolute = state.new_interval_variable(1, 5, None);
let constraint_tag = state.new_constraint_tag();

let _ = solver
.new_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
})
.expect("no empty domains");
let _ = state.add_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
});
state.propagate_to_fixed_point().expect("no empty domains");

solver.assert_bounds(signed, -5, -1);
state.assert_bounds(signed, -5, -1);
}

#[test]
fn lower_bound_on_absolute_can_propagate_positive_lower_bound_on_signed() {
let mut solver = TestSolver::default();
let mut state = State::default();

let signed = solver.new_variable(1, 5);
let absolute = solver.new_variable(3, 5);
let constraint_tag = solver.new_constraint_tag();
let signed = state.new_interval_variable(1, 5, None);
let absolute = state.new_interval_variable(3, 5, None);
let constraint_tag = state.new_constraint_tag();

let _ = solver
.new_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
})
.expect("no empty domains");
let _ = state.add_propagator(AbsoluteValueArgs {
signed,
absolute,
constraint_tag,
});
state.propagate_to_fixed_point().expect("no empty domains");

solver.assert_bounds(signed, 3, 5);
state.assert_bounds(signed, 3, 5);
}
}
Loading
Loading