CryptoPatrick / agents
Portrait of dilemma, Agent Dilemma #17 of 23

Agents/Uncategorized

Logic Available v0.1.0

dilemma

Agent Dilemma

Generic engine for the dilemma rule of Stalmarck's method: n-saturation over user-defined logic states.

Portrait pending: this agent's prompt is in agents/prompts.md. Its colour is a stand-in until the portrait arrives.

9Downloads, all time
9Last 90 days
1Releases since Oct 2026

Downloads per day

Last 90 days · 9 total · peak 9

Dossier

What dilemma does.

Generic engine for the dilemma rule of Stalmarck's method: n-saturation over user-defined logic states.

From the crate: “Generic engine for the dilemma rule of Stalmarck's method: n-saturation over user-defined logic states.”

Codename
Agent Dilemma
Role
Logic
Character
Quiet, exact, and new here.
Domain
Uncategorized
Enlisted
29 October 2026
Last release
v0.1.0, 29 October 2026
Keywords
logic, sat, solver, stalmarck, verification

Getting started

Call it in.

Add it to a Rust project, then follow the example. The full API is on docs.rs.

Install

cargo add dilemma

Example

use std::fmt;
use dilemma::engine::{DilemmaProcessor, StrategicOutcome};
use dilemma::{LogicState, SaturationStatus};

#[derive(Debug, Clone, PartialEq)]
struct Cnf {
    clauses: Vec<Vec<i32>>,      // DIMACS literals: 2 is x2, -2 is !x2
    values: Vec<Option<bool>>,   // indexed by variable
}

#[derive(Debug)]
struct Conflict;

impl fmt::Display for Conflict {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("conflict")
    }
}

impl std::error::Error for Conflict {}

impl Cnf {
    fn lit(&self, l: i32) -> Option<bool> {
        self.values[l.unsigned_abs() as usize].map(|v| v == (l > 0))
    }
}

impl LogicState for Cnf {
    type Var = usize;
    type Val = bool;
    type Error = Conflict;

    fn saturate_zero(&mut self) -> Result<SaturationStatus, Conflict> {
        for clause in self.clauses.clone() {
            if clause.iter().any(|&l| self.lit(l) == Some(true)) {
                continue;
            }
            let open: Vec<i32> = clause
                .iter()
                .copied()
                .filter(|&l| self.lit(l).is_none())
                .collect();
            match open[..] {
                [] => return Err(Conflict),
                [unit] => {
                    self.assume(unit.unsigned_abs() as usize, unit > 0)?;
                    return Ok(SaturationStatus::Changed);
                }
                _ => {}
            }
        }
        Ok(SaturationStatus::Converged)
    }

    fn assume(&mut self, var: usize, val: bool) -> Result<(), Conflict> {
        match self.values[var] {
            Some(old) if old != val => Err(Conflict),
            _ => {
                self.values[var] = Some(val);
                Ok(())
            }
        }
    }

    fn intersect(&self, other: &Self) -> Self {
        let values = self
            .values
            .iter()
            .zip(&other.values)
            .map(|(a, b)| if a == b { *a } else { None })
            .collect();
        Self { clauses: self.clauses.clone(), values }
    }
}

# fn main() -> Result<(), Conflict> {
// (x1 | x2) & (x1 | !x2): neither clause is a unit, but both
// branches of a split on x2 force x1.
let formula = Cnf {
    clauses: vec![vec![1, 2], vec![1, -2]],
    values: vec![None; 3],
};

let outcome =
    DilemmaProcessor::process_sequential(formula.clone(), [(2, true, false)]);
let StrategicOutcome::Valid(state) = outcome else { unreachable!() };
assert_eq!(state.values[1], Some(true));

// Or saturate to a fixpoint at depth 1 over every variable.
let splits = [(1, true, false), (2, true, false)];
let state = DilemmaProcessor::saturate(formula, 1, &splits)?;
assert_eq!(state.values[1], Some(true));
# Ok(())
# }

Build with a prompt

Add the Rust crate `dilemma` (version 0.1.0) to this project with `cargo add dilemma`. I want to use it for this: Generic engine for the dilemma rule of Stalmarck's method: n-saturation over user-defined logic states. Before writing code, read its README at https://crates.io/crates/dilemma and the API docs at https://docs.rs/dilemma. Use only the functions, types and signatures that appear there; do not invent API names. Write a minimal working example and one test that runs it.

Paste this into Claude Code or another coding agent.

Specs

Version
0.1.0
License
AGPL-3.0-only
Edition
Rust 2024
MSRV
1.85
Size
209 lines of code, 0 of comments
Package
20.2 KB compressed
Dependencies
rayon
Used by
0 crates on crates.io
Features
rayon
Docs
docs.rs, built
Repository
cryptopatrick/dilemma · 0★ · 0 forks · 0 open issues · pushed 29 Oct 2026

Release history

VersionDateDownloads
0.1.029 Oct 20269

Documentation

The manual.

The crate's README as published on crates.io. The full API reference is on docs.rs.

Read the README

dilemma

A generic engine for the dilemma rule of Stålmarck's method.

Overview

dilemma is not a SAT solver. It is the branch-and-merge core of Stålmarck's method, made generic so that constraint solvers, model checkers, and theorem provers can reuse it on their own state types.

You describe your logic by implementing [LogicState]: how to run one pass of local (0-saturation) rules, how to assume a value, and how to intersect two states. The engine then does the rest:

  1. Split the state on a variable v into branches v = a and v = b.
  2. Saturate both branches.
  3. Keep only what both branches agree on. If one branch is contradictory, keep the other; if both are, the state is refuted.

Repeating this to a fixpoint is 1-saturation. Saturating each branch to depth k - 1 gives k-saturation.

Key Features

  • Static dispatch only: your state type is a generic parameter, never a trait object.
  • Sequential and parallel drivers; the parallel driver runs the two branches of each split with rayon::join (feature rayon, on by default).
  • Arbitrary saturation depth through DilemmaProcessor::saturate.

Quick Start

[dependencies]
dilemma = "0.1"

A propositional state where 0-saturation is unit propagation:

use std::fmt;
use dilemma::engine::{DilemmaProcessor, StrategicOutcome};
use dilemma::{LogicState, SaturationStatus};

#[derive(Debug, Clone, PartialEq)]
struct Cnf {
    clauses: Vec<Vec<i32>>,      // DIMACS literals: 2 is x2, -2 is !x2
    values: Vec<Option<bool>>,   // indexed by variable
}

#[derive(Debug)]
struct Conflict;

impl fmt::Display for Conflict {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("conflict")
    }
}

impl std::error::Error for Conflict {}

impl Cnf {
    fn lit(&self, l: i32) -> Option<bool> {
        self.values[l.unsigned_abs() as usize].map(|v| v == (l > 0))
    }
}

impl LogicState for Cnf {
    type Var = usize;
    type Val = bool;
    type Error = Conflict;

    fn saturate_zero(&mut self) -> Result<SaturationStatus, Conflict> {
        for clause in self.clauses.clone() {
            if clause.iter().any(|&l| self.lit(l) == Some(true)) {
                continue;
            }
            let open: Vec<i32> = clause
                .iter()
                .copied()
                .filter(|&l| self.lit(l).is_none())
                .collect();
            match open[..] {
                [] => return Err(Conflict),
                [unit] => {
                    self.assume(unit.unsigned_abs() as usize, unit > 0)?;
                    return Ok(SaturationStatus::Changed);
                }
                _ => {}
            }
        }
        Ok(SaturationStatus::Converged)
    }

    fn assume(&mut self, var: usize, val: bool) -> Result<(), Conflict> {
        match self.values[var] {
            Some(old) if old != val => Err(Conflict),
            _ => {
                self.values[var] = Some(val);
                Ok(())
            }
        }
    }

    fn intersect(&self, other: &Self) -> Self {
        let values = self
            .values
            .iter()
            .zip(&other.values)
            .map(|(a, b)| if a == b { *a } else { None })
            .collect();
        Self { clauses: self.clauses.clone(), values }
    }
}

# fn main() -> Result<(), Conflict> {
// (x1 | x2) & (x1 | !x2): neither clause is a unit, but both
// branches of a split on x2 force x1.
let formula = Cnf {
    clauses: vec![vec![1, 2], vec![1, -2]],
    values: vec![None; 3],
};

let outcome =
    DilemmaProcessor::process_sequential(formula.clone(), [(2, true, false)]);
let StrategicOutcome::Valid(state) = outcome else { unreachable!() };
assert_eq!(state.values[1], Some(true));

// Or saturate to a fixpoint at depth 1 over every variable.
let splits = [(1, true, false), (2, true, false)];
let state = DilemmaProcessor::saturate(formula, 1, &splits)?;
assert_eq!(state.values[1], Some(true));
# Ok(())
# }

A fuller version lives in tests/cnf.rs; run it with cargo run --example basic.

Roadmap

  • Variable-selection heuristics (degree, activity).
  • Persistent, structurally shared state wrappers and benchmarks.
  • LRAT proof-certificate output.

Contributing

Contributions are welcome! See CONTRIBUTING.md.

License

Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only). See LICENSE.

Resources

Same unit

Other agents.