From b45fbbfcf7981c47a217767f74d38c1258f1b065 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 23 Jul 2026 21:43:31 +0800 Subject: [PATCH 1/2] Add persistent CDCL-assisted cube generation --- Cargo.lock | 287 +- Cargo.toml | 4 + benchmarks/cnc/README.md | 53 +- benchmarks/cnc/trace_mechanism.py | 72 +- src/adapter.rs | 88 +- src/bin/cnc_cuber.rs | 184 +- src/cdcl.rs | 419 + src/conquer.rs | 77 +- src/cube.rs | 781 +- src/lib.rs | 2 + src/selector.rs | 15 +- src/solver.rs | 2 + src/table.rs | 87 +- src/termination.rs | 27 + tests/cnc_cuber_trace.rs | 244 +- tests/cnc_streaming.rs | 150 +- tests/test_cnc_trace_mechanism.py | 21 +- vendor/rustsat-cadical/.cargo_vcs_info.json | 6 + vendor/rustsat-cadical/CHANGELOG.md | 287 + vendor/rustsat-cadical/Cargo.toml | 154 + vendor/rustsat-cadical/Cargo.toml.orig | 98 + vendor/rustsat-cadical/README.md | 108 + vendor/rustsat-cadical/VENDORED.md | 17 + vendor/rustsat-cadical/build.rs | 715 ++ .../cpp-extension/cadical_extension.hpp | 16 + .../cpp-extension/ccadical_extension.cpp | 176 + .../cpp-extension/ccadical_extension.h | 49 + .../rustsat-cadical/cpp-extension/ctracer.cpp | 155 + .../rustsat-cadical/cpp-extension/ctracer.h | 145 + .../cpp-extension/solver_extension.cpp | 164 + vendor/rustsat-cadical/cppsrc/LICENSE | 28 + vendor/rustsat-cadical/cppsrc/README.md | 65 + vendor/rustsat-cadical/cppsrc/VERSION | 1 + .../rustsat-cadical/cppsrc/scripts/README.md | 42 + vendor/rustsat-cadical/cppsrc/src/README.md | 12 + vendor/rustsat-cadical/cppsrc/src/analyze.cpp | 1360 +++ vendor/rustsat-cadical/cppsrc/src/arena.cpp | 30 + vendor/rustsat-cadical/cppsrc/src/arena.hpp | 105 + vendor/rustsat-cadical/cppsrc/src/assume.cpp | 616 ++ .../rustsat-cadical/cppsrc/src/averages.cpp | 34 + .../rustsat-cadical/cppsrc/src/averages.hpp | 37 + .../rustsat-cadical/cppsrc/src/backbone.cpp | 631 ++ .../rustsat-cadical/cppsrc/src/backtrack.cpp | 177 + .../rustsat-cadical/cppsrc/src/backward.cpp | 231 + vendor/rustsat-cadical/cppsrc/src/bins.cpp | 22 + vendor/rustsat-cadical/cppsrc/src/bins.hpp | 22 + vendor/rustsat-cadical/cppsrc/src/block.cpp | 824 ++ vendor/rustsat-cadical/cppsrc/src/block.hpp | 37 + vendor/rustsat-cadical/cppsrc/src/cadical.cpp | 1026 +++ vendor/rustsat-cadical/cppsrc/src/cadical.hpp | 1448 +++ .../rustsat-cadical/cppsrc/src/ccadical.cpp | 210 + vendor/rustsat-cadical/cppsrc/src/ccadical.h | 76 + vendor/rustsat-cadical/cppsrc/src/checker.cpp | 649 ++ vendor/rustsat-cadical/cppsrc/src/checker.hpp | 173 + vendor/rustsat-cadical/cppsrc/src/clause.cpp | 689 ++ vendor/rustsat-cadical/cppsrc/src/clause.hpp | 190 + vendor/rustsat-cadical/cppsrc/src/collect.cpp | 545 ++ vendor/rustsat-cadical/cppsrc/src/compact.cpp | 550 ++ .../rustsat-cadical/cppsrc/src/condition.cpp | 940 ++ vendor/rustsat-cadical/cppsrc/src/config.cpp | 101 + vendor/rustsat-cadical/cppsrc/src/config.hpp | 20 + vendor/rustsat-cadical/cppsrc/src/configure | 688 ++ .../rustsat-cadical/cppsrc/src/congruence.cpp | 7881 +++++++++++++++++ .../rustsat-cadical/cppsrc/src/congruence.hpp | 762 ++ .../rustsat-cadical/cppsrc/src/constrain.cpp | 64 + .../rustsat-cadical/cppsrc/src/contract.cpp | 27 + .../rustsat-cadical/cppsrc/src/contract.hpp | 138 + vendor/rustsat-cadical/cppsrc/src/cover.cpp | 704 ++ vendor/rustsat-cadical/cppsrc/src/cover.hpp | 34 + vendor/rustsat-cadical/cppsrc/src/decide.cpp | 347 + .../rustsat-cadical/cppsrc/src/decompose.cpp | 738 ++ .../rustsat-cadical/cppsrc/src/decompose.hpp | 23 + .../cppsrc/src/deduplicate.cpp | 275 + .../rustsat-cadical/cppsrc/src/definition.cpp | 283 + vendor/rustsat-cadical/cppsrc/src/delay.hpp | 38 + .../rustsat-cadical/cppsrc/src/drattracer.cpp | 153 + .../rustsat-cadical/cppsrc/src/drattracer.hpp | 55 + vendor/rustsat-cadical/cppsrc/src/elim.cpp | 1171 +++ vendor/rustsat-cadical/cppsrc/src/elim.hpp | 53 + .../rustsat-cadical/cppsrc/src/elimfast.cpp | 570 ++ vendor/rustsat-cadical/cppsrc/src/ema.cpp | 95 + vendor/rustsat-cadical/cppsrc/src/ema.hpp | 68 + vendor/rustsat-cadical/cppsrc/src/extend.cpp | 289 + .../rustsat-cadical/cppsrc/src/external.cpp | 1034 +++ .../rustsat-cadical/cppsrc/src/external.hpp | 487 + .../cppsrc/src/external_propagate.cpp | 1316 +++ vendor/rustsat-cadical/cppsrc/src/factor.cpp | 1011 +++ vendor/rustsat-cadical/cppsrc/src/factor.hpp | 54 + vendor/rustsat-cadical/cppsrc/src/file.cpp | 506 ++ vendor/rustsat-cadical/cppsrc/src/file.hpp | 210 + vendor/rustsat-cadical/cppsrc/src/flags.cpp | 135 + vendor/rustsat-cadical/cppsrc/src/flags.hpp | 93 + vendor/rustsat-cadical/cppsrc/src/flip.cpp | 269 + vendor/rustsat-cadical/cppsrc/src/format.cpp | 89 + vendor/rustsat-cadical/cppsrc/src/format.hpp | 36 + .../rustsat-cadical/cppsrc/src/frattracer.cpp | 277 + .../rustsat-cadical/cppsrc/src/frattracer.hpp | 67 + vendor/rustsat-cadical/cppsrc/src/gates.cpp | 766 ++ vendor/rustsat-cadical/cppsrc/src/heap.hpp | 212 + .../cppsrc/src/idruptracer.cpp | 566 ++ .../cppsrc/src/idruptracer.hpp | 112 + .../cppsrc/src/instantiate.cpp | 365 + .../cppsrc/src/instantiate.hpp | 45 + .../rustsat-cadical/cppsrc/src/internal.cpp | 1265 +++ .../rustsat-cadical/cppsrc/src/internal.hpp | 1941 ++++ .../rustsat-cadical/cppsrc/src/inttypes.hpp | 34 + vendor/rustsat-cadical/cppsrc/src/ipasir.cpp | 47 + vendor/rustsat-cadical/cppsrc/src/ipasir.h | 37 + vendor/rustsat-cadical/cppsrc/src/kitten.c | 2599 ++++++ vendor/rustsat-cadical/cppsrc/src/kitten.h | 97 + vendor/rustsat-cadical/cppsrc/src/level.hpp | 33 + .../cppsrc/src/lidruptracer.cpp | 657 ++ .../cppsrc/src/lidruptracer.hpp | 122 + vendor/rustsat-cadical/cppsrc/src/limit.cpp | 146 + vendor/rustsat-cadical/cppsrc/src/limit.hpp | 168 + vendor/rustsat-cadical/cppsrc/src/logging.cpp | 214 + vendor/rustsat-cadical/cppsrc/src/logging.hpp | 98 + .../rustsat-cadical/cppsrc/src/lookahead.cpp | 520 ++ .../cppsrc/src/lratchecker.cpp | 833 ++ .../cppsrc/src/lratchecker.hpp | 168 + .../rustsat-cadical/cppsrc/src/lrattracer.cpp | 200 + .../rustsat-cadical/cppsrc/src/lrattracer.hpp | 61 + vendor/rustsat-cadical/cppsrc/src/lucky.cpp | 506 ++ vendor/rustsat-cadical/cppsrc/src/message.cpp | 218 + vendor/rustsat-cadical/cppsrc/src/message.hpp | 65 + .../rustsat-cadical/cppsrc/src/minimize.cpp | 224 + vendor/rustsat-cadical/cppsrc/src/mobical.cpp | 5588 ++++++++++++ vendor/rustsat-cadical/cppsrc/src/occs.cpp | 52 + vendor/rustsat-cadical/cppsrc/src/occs.hpp | 36 + vendor/rustsat-cadical/cppsrc/src/options.cpp | 359 + vendor/rustsat-cadical/cppsrc/src/options.hpp | 442 + vendor/rustsat-cadical/cppsrc/src/parse.cpp | 454 + vendor/rustsat-cadical/cppsrc/src/parse.hpp | 75 + vendor/rustsat-cadical/cppsrc/src/phases.cpp | 56 + vendor/rustsat-cadical/cppsrc/src/phases.hpp | 19 + vendor/rustsat-cadical/cppsrc/src/probe.cpp | 990 +++ vendor/rustsat-cadical/cppsrc/src/profile.cpp | 107 + vendor/rustsat-cadical/cppsrc/src/profile.hpp | 283 + vendor/rustsat-cadical/cppsrc/src/proof.cpp | 691 ++ vendor/rustsat-cadical/cppsrc/src/proof.hpp | 125 + .../rustsat-cadical/cppsrc/src/propagate.cpp | 584 ++ vendor/rustsat-cadical/cppsrc/src/queue.cpp | 90 + vendor/rustsat-cadical/cppsrc/src/queue.hpp | 74 + vendor/rustsat-cadical/cppsrc/src/radix.hpp | 180 + vendor/rustsat-cadical/cppsrc/src/random.cpp | 207 + vendor/rustsat-cadical/cppsrc/src/random.h | 44 + vendor/rustsat-cadical/cppsrc/src/random.hpp | 98 + vendor/rustsat-cadical/cppsrc/src/range.hpp | 125 + vendor/rustsat-cadical/cppsrc/src/reap.cpp | 127 + vendor/rustsat-cadical/cppsrc/src/reap.hpp | 28 + vendor/rustsat-cadical/cppsrc/src/reduce.cpp | 278 + .../rustsat-cadical/cppsrc/src/reluctant.hpp | 82 + vendor/rustsat-cadical/cppsrc/src/rephase.cpp | 402 + vendor/rustsat-cadical/cppsrc/src/report.cpp | 313 + .../rustsat-cadical/cppsrc/src/resources.cpp | 160 + .../rustsat-cadical/cppsrc/src/resources.hpp | 16 + vendor/rustsat-cadical/cppsrc/src/restart.cpp | 178 + vendor/rustsat-cadical/cppsrc/src/restore.cpp | 267 + vendor/rustsat-cadical/cppsrc/src/score.cpp | 51 + vendor/rustsat-cadical/cppsrc/src/score.hpp | 16 + vendor/rustsat-cadical/cppsrc/src/shrink.cpp | 507 ++ vendor/rustsat-cadical/cppsrc/src/signal.cpp | 136 + vendor/rustsat-cadical/cppsrc/src/signal.hpp | 33 + .../rustsat-cadical/cppsrc/src/solution.cpp | 50 + vendor/rustsat-cadical/cppsrc/src/solver.cpp | 1867 ++++ vendor/rustsat-cadical/cppsrc/src/stable.cpp | 31 + vendor/rustsat-cadical/cppsrc/src/stack.h | 110 + vendor/rustsat-cadical/cppsrc/src/stats.cpp | 920 ++ vendor/rustsat-cadical/cppsrc/src/stats.hpp | 418 + vendor/rustsat-cadical/cppsrc/src/subsume.cpp | 645 ++ vendor/rustsat-cadical/cppsrc/src/sweep.cpp | 1960 ++++ vendor/rustsat-cadical/cppsrc/src/sweep.hpp | 61 + .../rustsat-cadical/cppsrc/src/terminal.cpp | 38 + .../rustsat-cadical/cppsrc/src/terminal.hpp | 96 + vendor/rustsat-cadical/cppsrc/src/ternary.cpp | 450 + vendor/rustsat-cadical/cppsrc/src/testing.hpp | 24 + vendor/rustsat-cadical/cppsrc/src/tier.cpp | 189 + vendor/rustsat-cadical/cppsrc/src/tracer.hpp | 186 + .../rustsat-cadical/cppsrc/src/transred.cpp | 253 + .../rustsat-cadical/cppsrc/src/unstable.cpp | 29 + vendor/rustsat-cadical/cppsrc/src/util.cpp | 129 + vendor/rustsat-cadical/cppsrc/src/util.hpp | 181 + vendor/rustsat-cadical/cppsrc/src/var.cpp | 39 + vendor/rustsat-cadical/cppsrc/src/var.hpp | 22 + .../cppsrc/src/veripbtracer.cpp | 427 + .../cppsrc/src/veripbtracer.hpp | 106 + vendor/rustsat-cadical/cppsrc/src/version.cpp | 107 + vendor/rustsat-cadical/cppsrc/src/version.hpp | 13 + vendor/rustsat-cadical/cppsrc/src/vivify.cpp | 1893 ++++ vendor/rustsat-cadical/cppsrc/src/vivify.hpp | 50 + vendor/rustsat-cadical/cppsrc/src/walk.cpp | 1088 +++ vendor/rustsat-cadical/cppsrc/src/walk.hpp | 91 + .../cppsrc/src/walk_full_occs.cpp | 966 ++ vendor/rustsat-cadical/cppsrc/src/warmup.cpp | 403 + vendor/rustsat-cadical/cppsrc/src/watch.cpp | 126 + vendor/rustsat-cadical/cppsrc/src/watch.hpp | 77 + vendor/rustsat-cadical/cppsrc/test/README.md | 55 + .../rustsat-cadical/cppsrc/test/api/README.md | 8 + .../rustsat-cadical/cppsrc/test/cnf/README.md | 23 + .../cppsrc/test/contrib/README.md | 8 + .../rustsat-cadical/cppsrc/test/mbt/README.md | 7 + .../cppsrc/test/trace/README.md | 6 + .../cppsrc/test/usage/README.md | 8 + .../rustsat-cadical/examples/cadical-cli.rs | 132 + vendor/rustsat-cadical/patches/v150.patch | 63 + vendor/rustsat-cadical/patches/v154.patch | 63 + vendor/rustsat-cadical/patches/v156.patch | 61 + vendor/rustsat-cadical/patches/v160.patch | 61 + vendor/rustsat-cadical/patches/v170.patch | 61 + vendor/rustsat-cadical/patches/v171.patch | 61 + vendor/rustsat-cadical/patches/v180.patch | 61 + vendor/rustsat-cadical/patches/v190.patch | 61 + vendor/rustsat-cadical/patches/v192.patch | 61 + vendor/rustsat-cadical/patches/v200.patch | 61 + vendor/rustsat-cadical/patches/v210.patch | 61 + vendor/rustsat-cadical/patches/v211.patch | 61 + vendor/rustsat-cadical/patches/v213.patch | 61 + vendor/rustsat-cadical/patches/v220.patch | 119 + vendor/rustsat-cadical/patches/v221.patch | 98 + vendor/rustsat-cadical/src/ffi.rs | 256 + vendor/rustsat-cadical/src/lib.rs | 1522 ++++ vendor/rustsat-cadical/src/prooftracer.rs | 358 + 222 files changed, 77436 insertions(+), 132 deletions(-) create mode 100644 src/cdcl.rs create mode 100644 src/termination.rs create mode 100644 vendor/rustsat-cadical/.cargo_vcs_info.json create mode 100644 vendor/rustsat-cadical/CHANGELOG.md create mode 100644 vendor/rustsat-cadical/Cargo.toml create mode 100644 vendor/rustsat-cadical/Cargo.toml.orig create mode 100644 vendor/rustsat-cadical/README.md create mode 100644 vendor/rustsat-cadical/VENDORED.md create mode 100644 vendor/rustsat-cadical/build.rs create mode 100644 vendor/rustsat-cadical/cpp-extension/cadical_extension.hpp create mode 100644 vendor/rustsat-cadical/cpp-extension/ccadical_extension.cpp create mode 100644 vendor/rustsat-cadical/cpp-extension/ccadical_extension.h create mode 100644 vendor/rustsat-cadical/cpp-extension/ctracer.cpp create mode 100644 vendor/rustsat-cadical/cpp-extension/ctracer.h create mode 100644 vendor/rustsat-cadical/cpp-extension/solver_extension.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/LICENSE create mode 100644 vendor/rustsat-cadical/cppsrc/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/VERSION create mode 100644 vendor/rustsat-cadical/cppsrc/scripts/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/src/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/src/analyze.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/arena.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/arena.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/assume.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/averages.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/averages.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/backbone.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/backtrack.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/backward.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/bins.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/bins.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/block.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/block.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/cadical.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/cadical.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ccadical.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ccadical.h create mode 100644 vendor/rustsat-cadical/cppsrc/src/checker.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/checker.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/clause.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/clause.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/collect.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/compact.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/condition.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/config.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/config.hpp create mode 100755 vendor/rustsat-cadical/cppsrc/src/configure create mode 100644 vendor/rustsat-cadical/cppsrc/src/congruence.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/congruence.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/constrain.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/contract.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/contract.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/cover.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/cover.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/decide.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/decompose.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/decompose.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/deduplicate.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/definition.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/delay.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/drattracer.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/drattracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/elim.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/elim.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/elimfast.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ema.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ema.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/extend.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/external.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/external.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/external_propagate.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/factor.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/factor.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/file.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/file.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/flags.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/flags.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/flip.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/format.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/format.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/frattracer.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/frattracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/gates.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/heap.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/idruptracer.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/idruptracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/instantiate.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/instantiate.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/internal.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/internal.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/inttypes.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ipasir.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ipasir.h create mode 100644 vendor/rustsat-cadical/cppsrc/src/kitten.c create mode 100644 vendor/rustsat-cadical/cppsrc/src/kitten.h create mode 100644 vendor/rustsat-cadical/cppsrc/src/level.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lidruptracer.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lidruptracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/limit.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/limit.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/logging.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/logging.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lookahead.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lratchecker.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lratchecker.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lrattracer.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lrattracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/lucky.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/message.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/message.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/minimize.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/mobical.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/occs.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/occs.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/options.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/options.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/parse.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/parse.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/phases.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/phases.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/probe.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/profile.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/profile.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/proof.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/proof.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/propagate.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/queue.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/queue.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/radix.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/random.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/random.h create mode 100644 vendor/rustsat-cadical/cppsrc/src/random.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/range.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/reap.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/reap.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/reduce.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/reluctant.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/rephase.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/report.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/resources.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/resources.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/restart.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/restore.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/score.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/score.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/shrink.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/signal.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/signal.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/solution.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/solver.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/stable.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/stack.h create mode 100644 vendor/rustsat-cadical/cppsrc/src/stats.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/stats.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/subsume.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/sweep.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/sweep.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/terminal.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/terminal.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/ternary.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/testing.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/tier.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/tracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/transred.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/unstable.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/util.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/util.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/var.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/var.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/veripbtracer.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/veripbtracer.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/version.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/version.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/vivify.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/vivify.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/walk.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/walk.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/walk_full_occs.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/warmup.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/watch.cpp create mode 100644 vendor/rustsat-cadical/cppsrc/src/watch.hpp create mode 100644 vendor/rustsat-cadical/cppsrc/test/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/test/api/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/test/cnf/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/test/contrib/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/test/mbt/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/test/trace/README.md create mode 100644 vendor/rustsat-cadical/cppsrc/test/usage/README.md create mode 100644 vendor/rustsat-cadical/examples/cadical-cli.rs create mode 100644 vendor/rustsat-cadical/patches/v150.patch create mode 100644 vendor/rustsat-cadical/patches/v154.patch create mode 100644 vendor/rustsat-cadical/patches/v156.patch create mode 100644 vendor/rustsat-cadical/patches/v160.patch create mode 100644 vendor/rustsat-cadical/patches/v170.patch create mode 100644 vendor/rustsat-cadical/patches/v171.patch create mode 100644 vendor/rustsat-cadical/patches/v180.patch create mode 100644 vendor/rustsat-cadical/patches/v190.patch create mode 100644 vendor/rustsat-cadical/patches/v192.patch create mode 100644 vendor/rustsat-cadical/patches/v200.patch create mode 100644 vendor/rustsat-cadical/patches/v210.patch create mode 100644 vendor/rustsat-cadical/patches/v211.patch create mode 100644 vendor/rustsat-cadical/patches/v213.patch create mode 100644 vendor/rustsat-cadical/patches/v220.patch create mode 100644 vendor/rustsat-cadical/patches/v221.patch create mode 100644 vendor/rustsat-cadical/src/ffi.rs create mode 100644 vendor/rustsat-cadical/src/lib.rs create mode 100644 vendor/rustsat-cadical/src/prooftracer.rs diff --git a/Cargo.lock b/Cargo.lock index ee4c9b8..c74c439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,15 @@ dependencies = [ "cc", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -32,6 +41,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "autocfg" version = "1.5.1" @@ -47,7 +62,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -72,6 +87,8 @@ dependencies = [ "optimal-branching", "rayon", "rustc-hash", + "rustsat", + "rustsat-cadical", "serde", "serde_json", "smallvec", @@ -104,6 +121,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex 2.0.1", ] @@ -122,6 +141,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "ciborium" version = "0.2.2" @@ -194,6 +226,22 @@ dependencies = [ "cc", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpu-time" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e393a7668fe1fad3075085b86c781883000b4ede868f43627b34a87c8b7ded" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "criterion" version = "0.8.2" @@ -206,7 +254,7 @@ dependencies = [ "ciborium", "clap", "criterion-plot", - "itertools", + "itertools 0.13.0", "num-traits", "oorandom", "page_size", @@ -226,7 +274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", - "itertools", + "itertools 0.13.0", ] [[package]] @@ -266,6 +314,22 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -310,10 +374,21 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + [[package]] name = "glob" version = "0.3.3" @@ -361,6 +436,30 @@ dependencies = [ "cmake", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itertools" version = "0.13.0" @@ -370,20 +469,40 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", + "once_cell", "wasm-bindgen", ] @@ -403,6 +522,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "log" version = "0.4.33" @@ -550,6 +675,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.9.4" @@ -576,7 +707,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -634,6 +765,48 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustsat" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ca3e44fb555707f07747514c1575a656c5913627f44e988e728f2e40b32977" +dependencies = [ + "anyhow", + "cpu-time", + "itertools 0.14.0", + "nom", + "rustc-hash", + "tempfile", + "thiserror", + "web-time", +] + +[[package]] +name = "rustsat-cadical" +version = "0.7.5" +dependencies = [ + "anyhow", + "bindgen", + "cc", + "chrono", + "glob", + "rustsat", + "thiserror", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -736,6 +909,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -784,18 +970,18 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.4+wasi-0.2.12" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -806,9 +992,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -816,9 +1002,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -829,18 +1015,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -887,12 +1083,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index db97513..1b021f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ thiserror = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" rayon = { version = "1", optional = true } +rustsat = "0.7.5" +# The crates.io wrapper emits `dbg!` output on every CaDiCaL 2.2 propagation. +# `VENDORED.md` records the one-line wrapper-only patch and upstream revision. +rustsat-cadical = { path = "vendor/rustsat-cadical", features = ["quiet"] } [features] default = [] diff --git a/benchmarks/cnc/README.md b/benchmarks/cnc/README.md index 56e751e..cb9ba27 100644 --- a/benchmarks/cnc/README.md +++ b/benchmarks/cnc/README.md @@ -59,16 +59,65 @@ DIMACS. The `.csp` format retains each ` : ` line as one relation tensor; it is intended for transfer tests where the structure-aware cuber must see semantics that a flattened CNF does not expose. +Add `--propagation cdcl --propagate-cnf INSTANCE.cnf` to retain native regions +while using one persistent CaDiCaL 2.2.1 instance. Each branch query is expressed +using only the current cube's decision literals as assumptions and invokes +CaDiCaL's standard assumptions-propagation path. Native implications are not +reintroduced as artificial assumptions: CaDiCaL reconstructs their reasons, +BCP runs to a fixpoint, a conflict is analyzed, and globally valid learned +clauses remain available to later queries. Because this propagation entry point +sits outside CaDiCaL's normal search loop, the wrapper invokes CaDiCaL's own +scheduled learned-clause reduction after conflicts. CaDiCaL stops after applying +the assumptions. The cuber never invokes `solve`/`solve_assumps`, never searches +beyond the branch assumptions, and never publishes a SAT model. + +With `--propagation cdcl`, the same propagation-and-learning path is also used +for the many hypothetical branches evaluated by the region-rule optimizer. +Those clauses are sound consequences of the base CNF, so they may safely help +later candidates and committed nodes. + +`--propagation hybrid --propagate-cnf INSTANCE.cnf` is the production hybrid: +region construction, feasibility probes, and the many hypothetical +branch-candidate evaluations use the native CT engine, while the persistent +CaDiCaL companion is called only after a selected branch is applied. This keeps +candidate scoring on CT while retaining conflict learning across committed +branches. + +Before descending, the cuber converts the optimizer's potentially overlapping +DNF cover into an equivalent pairwise-disjoint DNF. Consequently the emitted +frontier is a true CnC partition rather than a collection that can submit the +same residual assignment through multiple branches. + +An open decision-only cube is submitted to Kissat only after the online cutoff +fires. Kissat is the conquer solver and performs unrestricted modern CDCL. +A cuber-side propagation conflict closes that branch without submission. In +streaming `--solve-cnf` mode, SAT from any Kissat worker stops cubing and the +other in-flight workers through the shared first-answer signal. Global UNSAT is +reported only after cubing finishes and every submitted cube is UNSAT (except +for a root contradiction proved by propagation). + +For DIMACS input, `--propagate-cnf` is optional; in streaming mode, +`--solve-cnf` is also reused automatically. The native and CNF files must +describe the same formula, with native variables occupying the corresponding +leading DIMACS ids. The trace records the CDCL search mode, and stderr reports +internal conflicts/decisions/propagations, cumulative learned clauses, and the +current redundant-clause database size. + Both `--branch-solver` and `--measure` are mandatory so an artifact cannot silently inherit a changed default. `--branch-solver tail-greedy` starts from the full-row branches and rejects any GreedyMerge whose measured reduction is worse than the weakest initial child. Measures are selected as `vars`, `tensors`, or `hard-tensors`. -Trace schema v2 records the selected `measure` and `rule_diagnostics` for every -structure-aware branch: +The trace records propagation/CDCL provenance, the selected `measure`, and +`rule_diagnostics` for every structure-aware branch: the focus variable, region tensor/variable/boundary counts, joined and probe-surviving row counts, closed-region status, branching vector, and gamma. +`optimized_rule_clauses` is the cover to which those optimizer diagnostics +belong; `rule_clauses` is the disjoint CnC partition actually traversed, with +`rule_partition_sources` linking each partition branch back to its optimizer +clause. Trace producers and analysis tools use this current format directly; +older layouts are not accepted. It declares `search_semantics: "sat-decision"`. Ordinary open-region rules are configuration covers; closed regions may select one representative witness, so the full frontier is satisfiability-preserving but is neither a model-space diff --git a/benchmarks/cnc/trace_mechanism.py b/benchmarks/cnc/trace_mechanism.py index 70761f8..70f94d9 100644 --- a/benchmarks/cnc/trace_mechanism.py +++ b/benchmarks/cnc/trace_mechanism.py @@ -1,4 +1,4 @@ -"""Validate and summarize cnc_cuber mechanism traces (schema v2). +"""Validate and summarize current cnc_cuber mechanism traces. This deliberately aggregates raw local evidence without claiming that local gamma predicts conquer cost. Join the output to per-cube residual/conquer data @@ -169,10 +169,18 @@ def _clauses_may_overlap(left: dict[str, Any], right: dict[str, Any]) -> bool: def _validate_record(record: dict[str, Any], index: int) -> None: - if record.get("schema_version") != 2: - raise TraceError(f"record {index}: expected schema_version 2") if record.get("search_semantics") != "sat-decision": raise TraceError(f"record {index}: expected sat-decision semantics") + propagation = record.get("propagation") + cdcl_mode = record.get("cdcl_mode") + if propagation not in {"ct", "cdcl", "hybrid"}: + raise TraceError(f"record {index}: invalid propagation provenance") + if (propagation, cdcl_mode) not in { + ("ct", "off"), + ("cdcl", "branch-learning"), + ("hybrid", "branch-learning"), + }: + raise TraceError(f"record {index}: invalid CDCL search provenance") if record.get("selector") not in (None, "region", "structure-blind"): raise TraceError(f"record {index}: invalid selector provenance") if record.get("branch_solver") not in (None, "greedy", "tail-greedy", "naive"): @@ -202,6 +210,34 @@ def _validate_record(record: dict[str, Any], index: int) -> None: raise TraceError(f"record {index}: invalid rule clause mask/value") if value & ~mask: raise TraceError(f"record {index}: rule clause value exceeds its mask") + optimized_clauses = record.get("optimized_rule_clauses") + if not isinstance(optimized_clauses, list) or not all( + isinstance(clause, dict) for clause in optimized_clauses + ): + raise TraceError(f"record {index}: optimized_rule_clauses must be an array") + for clause in optimized_clauses: + mask = clause.get("mask") + value = clause.get("value") + if ( + type(mask) is not int + or type(value) is not int + or mask < 0 + or value < 0 + or value & ~mask + ): + raise TraceError(f"record {index}: invalid optimized rule clause") + partition_sources = record.get("rule_partition_sources") + if ( + not isinstance(partition_sources, list) + or len(partition_sources) != len(clauses) + or any( + type(source) is not int + or source < 0 + or source >= len(optimized_clauses) + for source in partition_sources + ) + ): + raise TraceError(f"record {index}: invalid rule_partition_sources") diagnostics = record.get("rule_diagnostics") if diagnostics is None: @@ -252,7 +288,7 @@ def _validate_record(record: dict[str, Any], index: int) -> None: if semantics == "cover": if closed or feasible_rows == 0 or record["kind"] != "branch": raise TraceError(f"record {index}: inconsistent cover semantics") - if len(clauses) != len(vector) or not clauses: + if len(optimized_clauses) != len(vector) or not optimized_clauses: raise TraceError(f"record {index}: selected branch/vector count mismatch") _validate_gamma(gamma, vector, "gamma", index) elif semantics == "closed-witness": @@ -260,7 +296,7 @@ def _validate_record(record: dict[str, Any], index: int) -> None: not closed or feasible_rows == 0 or record["kind"] != "branch" - or len(clauses) != 1 + or len(optimized_clauses) != 1 or vector or gamma != 1.0 ): @@ -268,7 +304,7 @@ def _validate_record(record: dict[str, Any], index: int) -> None: elif ( feasible_rows != 0 or record["kind"] != "refuted" - or clauses + or optimized_clauses or vector or gamma is not None ): @@ -360,14 +396,18 @@ def summarize( if unverified: raise TraceError(f"cover not verified at nodes {unverified[:8]}") - selected_branches = sum(len(record.get("rule_clauses", [])) for record, _ in rule_nodes) + selected_branches = sum( + len(record["optimized_rule_clauses"]) + for record, _ in rule_nodes + ) selected_literals = sum( int(clause["mask"]).bit_count() for record, _ in rule_nodes - for clause in record.get("rule_clauses", []) + for clause in record["optimized_rule_clauses"] ) single_branch_nodes = sum( - len(record.get("rule_clauses", [])) == 1 for record, _ in rule_nodes + len(record["optimized_rule_clauses"]) == 1 + for record, _ in rule_nodes ) sibling_pairs = 0 potentially_overlapping_pairs = 0 @@ -446,8 +486,6 @@ def summarize( } return { - "schema_version": 1, - "trace_schema_version": 2, "nodes": len(records), "rule_nodes": len(rule_nodes), "cover_nodes": len(cover_nodes), @@ -614,24 +652,26 @@ def link_conquer( "rule_diagnostics.branching_vector", int(node["node_id"]), ) + partition_sources = node["rule_partition_sources"] + source_index = partition_sources[child_index] if not vector and diagnostics.get("rule_semantics") == "closed-witness": - if child_index != 0: + if source_index != 0: raise TraceError( f"cube {cube_index}: closed witness has a nonzero child_index" ) selected_reductions.append(0.0) - elif child_index < 0 or child_index >= len(vector): + elif source_index < 0 or source_index >= len(vector): raise TraceError( - f"cube {cube_index}: child_index exceeds branching vector" + f"cube {cube_index}: partition source exceeds branching vector" ) else: - selected_reductions.append(vector[child_index]) + selected_reductions.append(vector[source_index]) replay = diagnostics.get("same_state_replay") if isinstance(replay, dict) and selected is not None and selected > 0: naive = _finite_gamma(replay["naive"].get("gamma")) if naive is not None and naive > 0: gamma_advantage_naive += math.log(naive) - math.log(selected) - if len(node.get("rule_clauses", [])) == 1: + if len(node["optimized_rule_clauses"]) == 1: single_branch_nodes += 1 root_node, root_child_index = path_edges[0] root_reduction = selected_reductions[0] diff --git a/src/adapter.rs b/src/adapter.rs index 42ca0ce..1be8bf9 100644 --- a/src/adapter.rs +++ b/src/adapter.rs @@ -23,6 +23,7 @@ use optimal_branching_core::{ IPSolver, LPSolver, Measure as ObMeasure, NaiveBranch, OptimalBranchingResult, }; +use crate::cdcl::CdclPropagator; use crate::ct::{RSparseBitSet, TableMasks}; use crate::domain::DomainMask; use crate::measure::{measure_core, Measure}; @@ -81,13 +82,20 @@ pub(crate) fn with_measure_scratch( /// A clone-cheap view of the SAT problem at one search node, sized to feed /// `optimal_branching_rule`. Cloning bumps the network `Arc` refcount and -/// deep-copies only `doms`. CT tables are shared via `masks` so `apply_branch` -/// can propagate with CT via the thread-local measure scratch. +/// deep-copies only `doms`. Candidate propagation uses either the optional +/// shared CDCL engine or CT via the thread-local measure scratch. #[derive(Clone)] pub struct RuleProblem { pub cn: Arc, pub masks: Arc>, pub doms: Vec, + /// Optional flattened-CNF propagation engine. Candidate evaluation uses + /// assumption-only BCP, so cloned rule problems share one clause database + /// while keeping their own projected native-domain snapshots. + pub cdcl: Option, + /// Actual cube decisions leading to this node. Native implications in + /// `doms` are intentionally excluded from the CaDiCaL assumption prefix. + pub decisions: Vec<(usize, bool)>, } impl RuleProblem { @@ -96,7 +104,19 @@ impl RuleProblem { masks: Arc>, doms: Vec, ) -> RuleProblem { - RuleProblem { cn, masks, doms } + RuleProblem { + cn, + masks, + doms, + cdcl: None, + decisions: Vec::new(), + } + } + + pub fn with_cdcl(mut self, cdcl: CdclPropagator, decisions: Vec<(usize, bool)>) -> RuleProblem { + self.cdcl = Some(cdcl); + self.decisions = decisions; + self } } @@ -111,13 +131,11 @@ impl BranchAndReduceProblem for RuleProblem { self.doms.iter().all(|d| d.is_fixed()) } - /// Apply `clause` over `variables` on the thread-local measure scratch (the - /// node's live CT store, at base), run CT to a fixpoint, snapshot the - /// resulting domains as the returned sub-problem, and restore the scratch to - /// base. Behavior-identical to the old clone-doms + rescan path (CT and rescan - /// reach the same GAC fixpoint) but ~2-3x faster and allocation-free. - /// Precondition (ob-core guarantee): called only single-level from the root, - /// with the scratch primed by `with_measure_scratch`. + /// Apply `clause` over `variables`, propagate with assumption-only CDCL when + /// configured, otherwise use the node's live CT store in the thread-local + /// measure scratch. Return the projected domain snapshot without changing + /// the base node. Precondition (ob-core guarantee): called only single-level + /// from the root, with CT scratch primed by `with_measure_scratch`. /// /// No per-node memo here: `GreedyMerge` (the only rule solver that re-evaluates /// the same clause) now memoizes `size_reduction` by `(mask, val)` in ob-core, @@ -125,15 +143,22 @@ impl BranchAndReduceProblem for RuleProblem { /// would never be hit. `IPSolver`/`LPSolver`/`NaiveBranch` evaluate each /// candidate clause exactly once, so they never needed one. fn apply_branch(&self, clause: &Clause, variables: &[usize]) -> (RuleProblem, f64) { - let snapshot = MEASURE_SCRATCH.with(|s| { - let s = &mut *s.borrow_mut(); - apply_branch_fresh(&self.cn, &self.masks, s, clause, variables) - }); + let snapshot = match &self.cdcl { + Some(cdcl) => cdcl + .propagate_clause(&self.doms, &self.decisions, clause, variables) + .expect("CDCL candidate propagation failed"), + None => MEASURE_SCRATCH.with(|s| { + let s = &mut *s.borrow_mut(); + apply_branch_fresh(&self.cn, &self.masks, s, clause, variables) + }), + }; ( RuleProblem { cn: Arc::clone(&self.cn), masks: Arc::clone(&self.masks), doms: snapshot, + cdcl: self.cdcl.clone(), + decisions: self.decisions.clone(), }, 0.0, ) @@ -226,7 +251,10 @@ impl BranchSolver { #[cfg(test)] mod tests { + use std::io::Cursor; + use super::*; + use crate::cdcl::CdclPropagator; use crate::ct::build_tables; use crate::network::setup_problem; use crate::problem::SolverBuffer; @@ -240,7 +268,7 @@ mod tests { setup_problem(3, vec![vec![0, 1], vec![1, 2]], vec![or2.clone(), or2]) } - /// Build a `RuleProblem` at `doms` with CT masks (apply_branch uses CT scratch). + /// Build a CT-scored `RuleProblem` at `doms`. fn rule_problem(cn: &ConstraintNetwork, doms: Vec) -> RuleProblem { let (masks, _tables) = build_tables(cn); RuleProblem::new(Arc::new(cn.clone()), Arc::new(masks), doms) @@ -305,6 +333,36 @@ mod tests { assert!(Arc::ptr_eq(&p.cn, &sub.cn)); } + #[test] + fn cdcl_apply_branch_matches_ct_and_is_order_independent_on_cnf() { + let cn = or_chain(); + let base = vec![DomainMask::BOTH; 3]; + let (masks, mut tables) = build_tables(&cn); + let masks = Arc::new(masks); + let mut buf = SolverBuffer::new(&cn); + let mut trail = Trail::new(); + let ct = RuleProblem::new(Arc::new(cn.clone()), Arc::clone(&masks), base.clone()); + let cdcl = CdclPropagator::from_dimacs( + &mut Cursor::new(b"p cnf 3 2\n1 2 0\n2 3 0\n"), + vec![0, 1, 2], + ) + .unwrap(); + let hybrid = RuleProblem::new(Arc::new(cn), Arc::clone(&masks), base.clone()) + .with_cdcl(cdcl, Vec::new()); + let variables = [0, 1, 2]; + let first = Clause::new(0b001, 0); // x0=0 => x1=1 + let other = Clause::new(0b100, 0); // x2=0 => x1=1 + + let ct_result = with_measure_scratch(&base, &mut tables, &mut buf, &mut trail, || { + ct.apply_branch(&first, &variables).0.doms + }); + let hybrid_first = hybrid.apply_branch(&first, &variables).0.doms; + let _ = hybrid.apply_branch(&other, &variables); + let hybrid_repeated = hybrid.apply_branch(&first, &variables).0.doms; + assert_eq!(hybrid_first, ct_result); + assert_eq!(hybrid_repeated, hybrid_first); + } + #[test] fn is_empty_tracks_unfixed_vars() { let cn = or_chain(); diff --git a/src/bin/cnc_cuber.rs b/src/bin/cnc_cuber.rs index 8dd488a..60f0265 100644 --- a/src/bin/cnc_cuber.rs +++ b/src/bin/cnc_cuber.rs @@ -10,12 +10,14 @@ use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use boolean_inference::adapter::BranchSolver; +use boolean_inference::cdcl::CdclPropagator; use boolean_inference::circuit::network_from_circuit_sat; use boolean_inference::conquer::{ConquerResult, StreamingConquer}; use boolean_inference::csp::network_from_csp; use boolean_inference::cube::{ - generate_cubes_with_cutoff, generate_cubes_with_cutoff_trace, CubeCutoff, CubeNodeKind, - CubeNodeTrace, CubeRefutationReason, + generate_cubes_configured, generate_cubes_configured_with_trace, CdclIntegrationMode, + CncSatPolicy, CubeCdclOptions, CubeCutoff, CubeGenerationOptions, CubeNodeKind, CubeNodeTrace, + CubeRefutationReason, }; use boolean_inference::dimacs::network_from_dimacs; use boolean_inference::measure::Measure; @@ -30,9 +32,9 @@ const USAGE: &str = (-o | --solve-cnf --kissat --workers ) \ --branch-solver \ --measure \ + [--propagation ] [--propagate-cnf ] \ [--selector ] \ [--max-rows ] [--trace ] [--trace-replay]"; -const SOLVED: &str = "streaming-conquer-found-sat"; #[derive(Clone, Copy, Debug)] enum SelectorKind { @@ -40,6 +42,41 @@ enum SelectorKind { StructureBlind, } +#[derive(Clone, Copy, Debug)] +enum PropagationKind { + Ct, + Cdcl, + Hybrid, +} + +impl PropagationKind { + fn parse(value: &str) -> Result { + match value { + "ct" => Ok(Self::Ct), + "cdcl" => Ok(Self::Cdcl), + "hybrid" => Ok(Self::Hybrid), + _ => Err(format!( + "invalid --propagation value: {value}; expected ct, cdcl, or hybrid" + )), + } + } + + fn label(self) -> &'static str { + match self { + Self::Ct => "ct", + Self::Cdcl => "cdcl", + Self::Hybrid => "hybrid", + } + } +} + +fn cdcl_mode_label(propagation: PropagationKind) -> &'static str { + match propagation { + PropagationKind::Ct => "off", + PropagationKind::Cdcl | PropagationKind::Hybrid => "branch-learning", + } +} + impl SelectorKind { fn parse(value: &str) -> Result { match value { @@ -91,12 +128,14 @@ struct Args { input: PathBuf, output: Option, solve_cnf: Option, + propagate_cnf: Option, kissat: Option, workers: Option, cutoff: CubeCutoff, selector: SelectorKind, branch_solver: BranchSolverKind, measure: Measure, + propagation: PropagationKind, max_rows: usize, trace: Option, trace_replay: bool, @@ -104,7 +143,7 @@ struct Args { enum Command { Help, - Run(Args), + Run(Box), } fn take_value(args: &[String], index: &mut usize, option: &str) -> Result { @@ -119,6 +158,7 @@ fn parse_args() -> Result { let mut input = None; let mut output = None; let mut solve_cnf = None; + let mut propagate_cnf = None; let mut kissat = None; let mut workers = None; let mut cutoff_vars = None; @@ -127,6 +167,7 @@ fn parse_args() -> Result { let mut selector = SelectorKind::Region; let mut branch_solver = None; let mut measure = None; + let mut propagation = PropagationKind::Ct; let mut trace = None; let mut trace_replay = false; let mut i = 0usize; @@ -154,6 +195,7 @@ fn parse_args() -> Result { } "-o" => output = Some(take_value(&raw, &mut i, "-o")?), "--solve-cnf" => solve_cnf = Some(take_value(&raw, &mut i, "--solve-cnf")?), + "--propagate-cnf" => propagate_cnf = Some(take_value(&raw, &mut i, "--propagate-cnf")?), "--kissat" => kissat = Some(take_value(&raw, &mut i, "--kissat")?), "--workers" => { let value = take_value(&raw, &mut i, "--workers")?; @@ -180,6 +222,9 @@ fn parse_args() -> Result { "--measure" => { measure = Some(Measure::parse(&take_value(&raw, &mut i, "--measure")?)?); } + "--propagation" => { + propagation = PropagationKind::parse(&take_value(&raw, &mut i, "--propagation")?)?; + } "--max-rows" => { let value = take_value(&raw, &mut i, "--max-rows")?; max_rows = value @@ -217,10 +262,22 @@ fn parse_args() -> Result { if trace_replay && matches!(selector, SelectorKind::StructureBlind) { return Err("--trace-replay requires --selector region".to_string()); } - Ok(Command::Run(Args { - input: PathBuf::from(input.ok_or_else(|| "missing input instance".to_string())?), + let input = PathBuf::from(input.ok_or_else(|| "missing input instance".to_string())?); + if matches!(propagation, PropagationKind::Cdcl | PropagationKind::Hybrid) + && solve_cnf.is_none() + && propagate_cnf.is_none() + && input.extension().and_then(|extension| extension.to_str()) != Some("cnf") + { + return Err(format!( + "--propagation {} requires --propagate-cnf, --solve-cnf, or a DIMACS input instance", + propagation.label() + )); + } + Ok(Command::Run(Box::new(Args { + input, output: output.map(PathBuf::from), solve_cnf: solve_cnf.map(PathBuf::from), + propagate_cnf: propagate_cnf.map(PathBuf::from), kissat: kissat.map(PathBuf::from), workers, cutoff, @@ -231,10 +288,11 @@ fn parse_args() -> Result { measure: measure.ok_or_else(|| { "missing --measure (experiments must select it explicitly)".to_string() })?, + propagation, max_rows, trace: trace.map(PathBuf::from), trace_replay, - })) + }))) } fn load_network(path: &Path) -> Result { @@ -277,9 +335,11 @@ fn refutation_reason(reason: CubeRefutationReason) -> &'static str { CubeRefutationReason::RootPropagation => "root-propagation-contradiction", CubeRefutationReason::SelectorNoFeasibleConfig => "selector-no-feasible-config", CubeRefutationReason::BranchPropagation => "branch-propagation-contradiction", + CubeRefutationReason::CdclPropagationConflict => "cdcl-propagation-conflict", } } +#[allow(clippy::too_many_arguments)] fn write_trace_node( writer: &mut dyn Write, node: CubeNodeTrace, @@ -287,6 +347,8 @@ fn write_trace_node( selector: &str, branch_solver: &str, measure: &str, + propagation: &str, + cdcl_mode: &str, input_kind: &str, ) -> Result<(), String> { let literals: Vec = node @@ -311,6 +373,11 @@ fn write_trace_node( .iter() .map(|clause| serde_json::json!({"mask": clause.mask, "value": clause.value})) .collect(); + let optimized_clauses: Vec<_> = node + .optimized_clauses + .iter() + .map(|clause| serde_json::json!({"mask": clause.mask, "value": clause.value})) + .collect(); let rule_diagnostics = node.rule_diagnostics.as_ref().map(|diagnostics| { let rule_semantics = if diagnostics.feasible_rows == 0 { "local-refutation" @@ -356,11 +423,12 @@ fn write_trace_node( }) }); let record = serde_json::json!({ - "schema_version": 2, "search_semantics": "sat-decision", "selector": selector, "branch_solver": branch_solver, "measure": measure, + "propagation": propagation, + "cdcl_mode": cdcl_mode, "input_kind": input_kind, "node_id": node.node_id, "parent_id": node.parent_id, @@ -374,7 +442,9 @@ fn write_trace_node( "freevars": node.freevars, "rule_diagnostics": rule_diagnostics, "rule_variables": variables, + "optimized_rule_clauses": optimized_clauses, "rule_clauses": clauses, + "rule_partition_sources": node.partition_sources, }); serde_json::to_writer(&mut *writer, &record) .map_err(|error| format!("serialize trace: {error}"))?; @@ -458,12 +528,16 @@ fn run(args: Args) -> Result { freevars: nvars, rule_diagnostics: None, variables: Vec::new(), + optimized_clauses: Vec::new(), clauses: Vec::new(), + partition_sources: Vec::new(), }, &new_to_orig, args.selector.label(), args.branch_solver.label(), args.measure.label(), + args.propagation.label(), + cdcl_mode_label(args.propagation), input_kind, )?; trace_writer @@ -473,11 +547,14 @@ fn run(args: Args) -> Result { writer.flush().map_err(|e| format!("flush output: {e}"))?; eprintln!( "status=UNSAT_AT_ROOT cubes=0 refuted=1 sat_leaves=0 cutoff={:?} \ - selector={} branch_solver={} measure={} max_rows={}", + selector={} branch_solver={} measure={} propagation={} cdcl_mode={} \ + max_rows={}", args.cutoff, args.selector.label(), args.branch_solver.label(), args.measure.label(), + args.propagation.label(), + cdcl_mode_label(args.propagation), args.max_rows ); if let Some(conquer) = conquer.take() { @@ -490,6 +567,27 @@ fn run(args: Args) -> Result { } }; let root_unfixed = problem.count_unfixed(); + let cdcl = match args.propagation { + PropagationKind::Ct => None, + PropagationKind::Cdcl | PropagationKind::Hybrid => { + let cnf = args + .propagate_cnf + .as_ref() + .or(args.solve_cnf.as_ref()) + .unwrap_or(&args.input); + Some(CdclPropagator::from_dimacs_path(cnf, new_to_orig.clone())?) + } + }; + let cdcl_integration = match args.propagation { + PropagationKind::Hybrid => CdclIntegrationMode::HybridCtCandidates, + PropagationKind::Ct | PropagationKind::Cdcl => CdclIntegrationMode::FullPropagation, + }; + let sat_policy = if conquer.is_some() { + CncSatPolicy::StopDecision + } else { + CncSatPolicy::CompleteFrontier + }; + let termination = conquer.as_ref().map(StreamingConquer::termination_signal); let mut emitted = 0usize; let mut min_remaining = usize::MAX; @@ -518,19 +616,12 @@ fn run(args: Args) -> Result { if leaf_sat { if let Some(conquer) = conquer.as_ref() { conquer.mark_sat(); - return Err(SOLVED.to_string()); } + return Ok(()); } let remaining = nvars - cube.sigma_all; - let stopped = match args.cutoff { - CubeCutoff::RemainingVars(n) => remaining < n.get(), - CubeCutoff::CcDifficulty(threshold) => { - (cube.sigma_dec as u128).pow(2) * (cube.sigma_all as u128) - > threshold * (nvars as u128) - } - }; - if !leaf_sat && !stopped { + if !args.cutoff.stops(cube.sigma_dec, cube.sigma_all, remaining) { return Err(format!( "internal cutoff error: emitted cube does not satisfy {:?}", args.cutoff @@ -548,7 +639,7 @@ fn run(args: Args) -> Result { .submit(literals) .map_err(|error| error.to_string())? { - return Err(SOLVED.to_string()); + return Ok(()); } } else { writer @@ -567,13 +658,22 @@ fn run(args: Args) -> Result { max_remaining = max_remaining.max(remaining); Ok(()) }; + let generation_options = CubeGenerationOptions { + cutoff: args.cutoff, + cdcl: cdcl.as_ref().map(|cdcl| CubeCdclOptions { + propagator: cdcl.clone(), + integration: cdcl_integration, + }), + sat_policy, + termination, + }; let generated = match trace_writer.as_mut() { - Some(trace_writer) => generate_cubes_with_cutoff_trace( + Some(trace_writer) => generate_cubes_configured_with_trace( &mut problem, selector, args.measure, &solver, - args.cutoff, + generation_options, &mut emit, |node| { write_trace_node( @@ -583,23 +683,27 @@ fn run(args: Args) -> Result { args.selector.label(), args.branch_solver.label(), args.measure.label(), + args.propagation.label(), + cdcl_mode_label(args.propagation), input_kind, ) }, ), - None => generate_cubes_with_cutoff( + None => generate_cubes_configured( &mut problem, selector, args.measure, &solver, - args.cutoff, + generation_options, &mut emit, ), }; - let stopped_on_sat = matches!(&generated, Err(error) if error == SOLVED); + let stopped_during_generation = generated.as_ref().is_ok_and(|stats| stats.stopped_early); + let cdcl_stats = cdcl.as_ref().map(CdclPropagator::stats); + let stopped_on_sat = stopped_during_generation; let stats = match generated { + Ok(stats) if stats.stopped_early => None, Ok(stats) => Some(stats), - Err(error) if error == SOLVED => None, Err(error) => { if let Some(conquer) = conquer.take() { let _ = conquer.finish(false); @@ -622,7 +726,8 @@ fn run(args: Args) -> Result { if let Some(stats) = stats { eprintln!( "status=OK cubes={} refuted={} sat_leaves={} visited={} cutoff={:?} \ - root_unfixed={} remaining_range={} selector={} branch_solver={} measure={} max_rows={}", + root_unfixed={} remaining_range={} selector={} branch_solver={} measure={} \ + propagation={} cdcl_mode={} max_rows={}", stats.cubes, stats.refuted, stats.sat_leaves, @@ -633,6 +738,8 @@ fn run(args: Args) -> Result { args.selector.label(), args.branch_solver.label(), args.measure.label(), + args.propagation.label(), + cdcl_mode_label(args.propagation), args.max_rows ); let expected = stats.cubes + stats.sat_leaves; @@ -644,12 +751,31 @@ fn run(args: Args) -> Result { } } else { eprintln!( - "status=SAT_EARLY cubes_submitted={} cutoff={:?} selector={} branch_solver={} measure={}", + "status=SAT_EARLY cubes_submitted={} cutoff={:?} selector={} branch_solver={} \ + measure={} propagation={} cdcl_mode={}", emitted, args.cutoff, args.selector.label(), args.branch_solver.label(), - args.measure.label() + args.measure.label(), + args.propagation.label(), + cdcl_mode_label(args.propagation) + ); + } + if let Some(stats) = cdcl_stats { + eprintln!( + "cdcl propagation_calls={} propagation_conflicts={} assumption_literals={} \ + full_search_calls={} conflicts={} decisions={} propagations={} \ + learned_total={} redundant_current={}", + stats.propagation_calls, + stats.propagation_conflicts, + stats.assumption_literals, + stats.full_search_calls, + stats.conflicts, + stats.decisions, + stats.propagations, + stats.total_learned_clauses, + stats.current_redundant_clauses ); } if let Some(conquer) = conquer.take() { @@ -682,7 +808,7 @@ fn run(args: Args) -> Result { fn main() { match parse_args() { Ok(Command::Help) => println!("{USAGE}"), - Ok(Command::Run(args)) => match run(args) { + Ok(Command::Run(args)) => match run(*args) { Ok(0) => {} Ok(code) => std::process::exit(code), Err(message) => { diff --git a/src/cdcl.rs b/src/cdcl.rs new file mode 100644 index 0000000..3dfc253 --- /dev/null +++ b/src/cdcl.rs @@ -0,0 +1,419 @@ +//! Persistent CDCL propagation for cube generation. +//! +//! The cuber owns one CaDiCaL instance for the whole run. Every query applies +//! the current cube decisions as assumptions and invokes CaDiCaL's standard +//! assumptions-propagation entry point. Native implications are deliberately +//! not promoted to assumptions: CaDiCaL must reconstruct their reasons so +//! conflict analysis learns clauses over the actual cube decisions. The entry +//! point performs BCP and conflict analysis, so globally valid learned clauses +//! remain in the solver for later nodes. It stops after the assumptions: the +//! cuber never calls `solve` or lets CaDiCaL make search decisions beyond the +//! cube. + +use std::cell::{Cell, RefCell}; +use std::io::BufRead; +use std::path::Path; +use std::rc::Rc; + +use optimal_branching_core::Clause as BranchClause; +use rustsat::instances::SatInstance; +use rustsat::solvers::{FreezeVar, GetInternalStats, Learn, Propagate, Solve}; +use rustsat::types::{Lit, TernaryVal, Var}; +use rustsat_cadical::{CaDiCaL, Config}; + +use crate::domain::DomainMask; + +/// Aggregate counters from the persistent CaDiCaL instance. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CdclStats { + /// Number of assumption-propagation queries issued by the cuber. + pub propagation_calls: u64, + /// Queries whose BCP/conflict-analysis result refuted the assumptions. + pub propagation_conflicts: u64, + /// Total number of literals supplied as assumptions across all queries. + pub assumption_literals: u64, + /// Full CDCL searches started inside the cuber. This is an architectural + /// invariant and is always zero. + pub full_search_calls: u64, + pub conflicts: u64, + pub decisions: u64, + pub propagations: u64, + /// Clauses reported by CaDiCaL's learner callback over the whole run. + pub total_learned_clauses: u64, + /// Redundant clauses currently retained in CaDiCaL's clause database. + pub current_redundant_clauses: usize, +} + +/// Cloneable handle to one persistent, single-threaded CaDiCaL instance. +/// +/// Region scoring and cube generation are single-threaded. Clones therefore +/// share the solver through `Rc>`; every learned clause immediately +/// benefits later committed branches and, when enabled, later candidate probes. +#[derive(Clone)] +pub struct CdclPropagator { + inner: Rc, +} + +struct CdclInner { + solver: RefCell>, + native_to_cnf: Vec, + cnf_to_native: Vec>, + propagation_calls: Cell, + propagation_conflicts: Cell, + assumption_literals: Cell, + total_learned_clauses: Rc>, +} + +impl CdclPropagator { + /// Load a DIMACS formula. `native_to_cnf[v]` is the zero-based DIMACS + /// variable corresponding to compressed native variable `v`. + pub fn from_dimacs_path(path: &Path, native_to_cnf: Vec) -> Result { + let instance = SatInstance::from_dimacs_path(path) + .map_err(|error| format!("parse CDCL CNF {}: {error}", path.display()))?; + Self::from_instance(instance, native_to_cnf) + } + + /// Reader form used by tests and embedders. + pub fn from_dimacs( + reader: &mut R, + native_to_cnf: Vec, + ) -> Result { + let instance = + SatInstance::from_dimacs(reader).map_err(|error| format!("parse CDCL CNF: {error}"))?; + Self::from_instance(instance, native_to_cnf) + } + + fn from_instance(instance: SatInstance, native_to_cnf: Vec) -> Result { + let formula_vars = instance.max_var().map_or(0, |var| var.idx() + 1); + let mapped_vars = native_to_cnf.iter().copied().max().map_or(0, |var| var + 1); + let n_cnf_vars = formula_vars.max(mapped_vars); + let mut seen = vec![false; n_cnf_vars]; + for &cnf_var in &native_to_cnf { + if cnf_var > Var::MAX_IDX as usize { + return Err(format!( + "flattened CNF variable {} exceeds the RustSAT limit", + cnf_var + 1 + )); + } + if std::mem::replace(&mut seen[cnf_var], true) { + return Err(format!( + "two native variables map to flattened CNF variable {}", + cnf_var + 1 + )); + } + } + + let mut solver = CaDiCaL::default(); + solver + .set_configuration(Config::Default) + .map_err(|error| format!("configure CaDiCaL: {error}"))?; + if n_cnf_vars > 0 { + solver + .reserve(Var::new((n_cnf_vars - 1) as u32)) + .map_err(|error| format!("reserve CaDiCaL variables: {error}"))?; + } + for clause in instance.cnf() { + solver + .add_clause_ref(clause) + .map_err(|error| format!("load CaDiCaL clause: {error}"))?; + } + // Native variables recur as assumptions and must keep stable external + // identities across CaDiCaL inprocessing rounds. + for &cnf_var in &native_to_cnf { + solver + .freeze_var(Var::new(cnf_var as u32)) + .map_err(|error| format!("freeze CaDiCaL variable {}: {error}", cnf_var + 1))?; + } + let total_learned_clauses = Rc::new(Cell::new(0u64)); + let learner_count = Rc::clone(&total_learned_clauses); + solver.attach_learner( + move |_| learner_count.set(learner_count.get().saturating_add(1)), + n_cnf_vars, + ); + let mut cnf_to_native = vec![None; n_cnf_vars]; + for (native, &cnf_var) in native_to_cnf.iter().enumerate() { + cnf_to_native[cnf_var] = Some(native); + } + + Ok(Self { + inner: Rc::new(CdclInner { + solver: RefCell::new(solver), + native_to_cnf, + cnf_to_native, + propagation_calls: Cell::new(0), + propagation_conflicts: Cell::new(0), + assumption_literals: Cell::new(0), + total_learned_clauses, + }), + }) + } + + /// Propagate a hypothetical optimal-branching clause from `base`. + pub fn propagate_clause( + &self, + base: &[DomainMask], + prefix: &[(usize, bool)], + clause: &BranchClause, + variables: &[usize], + ) -> Result, String> { + let mut decisions = Vec::with_capacity(prefix.len() + clause.mask.count_ones() as usize); + decisions.extend_from_slice(prefix); + for (index, &var) in variables.iter().enumerate() { + if (clause.mask >> index) & 1 != 0 { + decisions.push((var, (clause.val >> index) & 1 != 0)); + } + } + self.propagate_decisions(base, &decisions) + } + + /// Propagate the explicit cube `decisions` and overlay the resulting native + /// implications on `base`. + /// + /// Only `decisions` are passed to CaDiCaL as assumptions. Fixed values in + /// `base` are a projection maintained by native propagation and are kept in + /// the returned snapshot, but are not turned into artificial decision + /// levels. This distinction is essential for useful first-UIP learning. + /// + /// A conflict is represented by the existing solver convention + /// `snapshot[0] == DomainMask::NONE`. CaDiCaL's propagation call analyzes + /// such a conflict before returning, retaining the learned clause globally. + pub fn propagate_decisions( + &self, + base: &[DomainMask], + decisions: &[(usize, bool)], + ) -> Result, String> { + if base.len() != self.inner.native_to_cnf.len() { + return Err(format!( + "native domain length {} does not match CDCL map length {}", + base.len(), + self.inner.native_to_cnf.len() + )); + } + + let mut snapshot = base.to_vec(); + for &(var, value) in decisions { + let requested = fixed_domain(value); + match snapshot.get_mut(var) { + Some(domain) if *domain == DomainMask::BOTH || *domain == requested => { + *domain = requested; + } + Some(_) => { + mark_conflict(&mut snapshot); + return Ok(snapshot); + } + None => return Err(format!("native branch variable {var} is out of range")), + } + } + + let assumptions = assumptions_from_decisions(&self.inner, decisions)?; + self.inner + .propagation_calls + .set(self.inner.propagation_calls.get() + 1); + self.inner.assumption_literals.set( + self.inner.assumption_literals.get() + + u64::try_from(assumptions.len()).unwrap_or(u64::MAX), + ); + + let mut solver = self.inner.solver.borrow_mut(); + let result = solver + .propagate(&assumptions, false) + .map_err(|error| format!("CaDiCaL branch propagation failed: {error}"))?; + if result.conflict { + self.inner + .propagation_conflicts + .set(self.inner.propagation_conflicts.get() + 1); + // Assumption propagation analyzes conflicts outside CaDiCaL's + // normal search loop. Run its own scheduled reduction policy at + // this reset-to-root boundary so retained clauses stay managed. + solver.maintain_learned_clauses(); + mark_conflict(&mut snapshot); + return Ok(snapshot); + } + + // `propagated` contains the assumption trail. `current_lit_val` also + // exposes root-fixed projected variables that predate the first + // assumption and therefore may not appear in that returned suffix. + for (native, &cnf_var) in self.inner.native_to_cnf.iter().enumerate() { + let literal = Var::new(cnf_var as u32).pos_lit(); + let implied = match solver.current_lit_val(literal) { + TernaryVal::True => Some(DomainMask::D1), + TernaryVal::False => Some(DomainMask::D0), + TernaryVal::DontCare => None, + }; + if let Some(implied) = implied { + let domain = &mut snapshot[native]; + if *domain == DomainMask::BOTH || *domain == implied { + *domain = implied; + } else { + mark_conflict(&mut snapshot); + return Ok(snapshot); + } + } + } + + for literal in result.propagated { + let Some(Some(native)) = self.inner.cnf_to_native.get(literal.vidx()) else { + continue; + }; + let implied = fixed_domain(literal.is_pos()); + let domain = &mut snapshot[*native]; + if *domain == DomainMask::BOTH || *domain == implied { + *domain = implied; + } else { + mark_conflict(&mut snapshot); + break; + } + } + Ok(snapshot) + } + + pub fn stats(&self) -> CdclStats { + let solver = self.inner.solver.borrow(); + CdclStats { + propagation_calls: self.inner.propagation_calls.get(), + propagation_conflicts: self.inner.propagation_conflicts.get(), + assumption_literals: self.inner.assumption_literals.get(), + full_search_calls: 0, + conflicts: solver.conflicts().try_into().unwrap_or(u64::MAX), + decisions: solver.decisions().try_into().unwrap_or(u64::MAX), + propagations: solver.propagations().try_into().unwrap_or(u64::MAX), + total_learned_clauses: self.inner.total_learned_clauses.get(), + current_redundant_clauses: solver.get_redundant().max(0) as usize, + } + } +} + +fn assumptions_from_decisions( + inner: &CdclInner, + decisions: &[(usize, bool)], +) -> Result, String> { + let mut assumptions = Vec::with_capacity(decisions.len()); + for &(native, value) in decisions { + if native >= inner.native_to_cnf.len() { + return Err(format!("native decision variable {native} is out of range")); + } + let cnf_var: u32 = inner.native_to_cnf[native] + .try_into() + .map_err(|_| "flattened CNF variable exceeds u32".to_string())?; + assumptions.push(Lit::new(cnf_var, !value)); + } + Ok(assumptions) +} + +fn mark_conflict(doms: &mut [DomainMask]) { + if let Some(sentinel) = doms.first_mut() { + *sentinel = DomainMask::NONE; + } +} + +#[inline] +fn fixed_domain(value: bool) -> DomainMask { + if value { + DomainMask::D1 + } else { + DomainMask::D0 + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use super::*; + + fn solver(cnf: &str, variables: usize) -> CdclPropagator { + CdclPropagator::from_dimacs(&mut Cursor::new(cnf.as_bytes()), (0..variables).collect()) + .unwrap() + } + + #[test] + fn projects_gate_implications_to_native_domains() { + // z <-> (a AND b): + // (¬z∨a)(¬z∨b)(z∨¬a∨¬b) + let cdcl = solver("p cnf 3 3\n-3 1 0\n-3 2 0\n3 -1 -2 0\n", 3); + let base = vec![DomainMask::BOTH; 3]; + + let z_true = cdcl.propagate_decisions(&base, &[(2, true)]).unwrap(); + assert_eq!(z_true, vec![DomainMask::D1, DomainMask::D1, DomainMask::D1]); + + let a_false = cdcl.propagate_decisions(&base, &[(0, false)]).unwrap(); + assert_eq!(a_false[0], DomainMask::D0); + assert_eq!(a_false[2], DomainMask::D0); + } + + #[test] + fn repeated_probes_do_not_leak_assumptions() { + let cdcl = solver("p cnf 2 1\n1 2 0\n", 2); + let base = vec![DomainMask::BOTH; 2]; + let x0_false = cdcl.propagate_decisions(&base, &[(0, false)]).unwrap(); + assert_eq!(x0_false, vec![DomainMask::D0, DomainMask::D1]); + + let x0_true = cdcl.propagate_decisions(&base, &[(0, true)]).unwrap(); + assert_eq!(x0_true[0], DomainMask::D1); + assert_eq!(x0_true[1], DomainMask::BOTH); + } + + #[test] + fn contradictory_assumptions_return_the_native_sentinel() { + let cdcl = solver("p cnf 1 1\n1 0\n", 1); + let result = cdcl + .propagate_decisions(&[DomainMask::BOTH], &[(0, false)]) + .unwrap(); + assert_eq!(result, vec![DomainMask::NONE]); + } + + #[test] + fn auxiliary_implications_are_projected_but_auxiliaries_stay_hidden() { + // Native variables are a,b,c (CNF 1,2,3); variable 4 is a Tseitin + // auxiliary. a -> aux -> c, while b is unrelated. + let cdcl = solver("p cnf 4 4\n-1 4 0\n1 -4 0\n-4 3 0\n4 -3 0\n", 3); + let result = cdcl + .propagate_decisions(&[DomainMask::BOTH; 3], &[(0, true)]) + .unwrap(); + assert_eq!(result[0], DomainMask::D1); + assert_eq!(result[1], DomainMask::BOTH); + assert_eq!(result[2], DomainMask::D1); + } + + #[test] + fn propagation_conflict_learns_for_later_parent_bcp() { + // Resolving the three clauses shows that the formula entails a. Root + // BCP initially cannot see it. Under a=0 it forces both b and c before + // conflicting, producing the learned unit a. + let cdcl = solver("p cnf 3 3\n1 2 0\n1 3 0\n1 -2 -3 0\n", 3); + let parent = vec![DomainMask::BOTH; 3]; + assert_eq!(cdcl.propagate_decisions(&parent, &[]).unwrap(), parent); + + let child = cdcl.propagate_decisions(&parent, &[(0, false)]).unwrap(); + assert_eq!(child[0], DomainMask::NONE); + + let after = cdcl.propagate_decisions(&parent, &[]).unwrap(); + assert_eq!(after[0], DomainMask::D1); + let stats = cdcl.stats(); + assert_eq!(stats.propagation_conflicts, 1); + assert_eq!(stats.full_search_calls, 0); + assert!(stats.conflicts >= 1); + assert!(stats.total_learned_clauses >= 1); + } + + #[test] + fn open_formula_is_not_solved_by_the_cuber_cdcl() { + // No root implication. A full CDCL solve could return SAT immediately, + // but branch propagation must leave the formula open. + let cdcl = solver("p cnf 3 2\n1 2 0\n-1 2 0\n", 3); + let base = vec![DomainMask::BOTH; 3]; + assert_eq!(cdcl.propagate_decisions(&base, &[]).unwrap(), base); + let stats = cdcl.stats(); + assert_eq!(stats.full_search_calls, 0); + assert_eq!(stats.propagation_calls, 1); + } + + #[test] + fn projected_implications_are_not_reintroduced_as_assumptions() { + let cdcl = solver("p cnf 3 2\n-1 2 0\n-2 3 0\n", 3); + let projected = vec![DomainMask::D1, DomainMask::D1, DomainMask::D1]; + let result = cdcl.propagate_decisions(&projected, &[(0, true)]).unwrap(); + assert_eq!(result, projected); + assert_eq!(cdcl.stats().assumption_literals, 1); + } +} diff --git a/src/conquer.rs b/src/conquer.rs index 09d2eda..c53588c 100644 --- a/src/conquer.rs +++ b/src/conquer.rs @@ -4,10 +4,12 @@ use std::fs; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{mpsc, Arc, Mutex}; use std::thread::{self, JoinHandle}; +use crate::termination::TerminationSignal; + #[derive(Debug, thiserror::Error)] pub enum ConquerError { #[error("read CNF {path}: {source}")] @@ -86,7 +88,7 @@ pub struct ConquerSummary { } struct Shared { - stopped: AtomicBool, + stopped: TerminationSignal, submitted: AtomicUsize, sat: AtomicUsize, unsat: AtomicUsize, @@ -97,7 +99,7 @@ struct Shared { impl Shared { fn new() -> Self { Self { - stopped: AtomicBool::new(false), + stopped: TerminationSignal::new(), submitted: AtomicUsize::new(0), sat: AtomicUsize::new(0), unsat: AtomicUsize::new(0), @@ -140,7 +142,7 @@ impl StreamingConquer { /// Submit one open cube. Returns `false` once another cube has proved SAT. pub fn submit(&self, cube: Vec) -> Result { - if self.shared.stopped.load(Ordering::Acquire) { + if self.shared.stopped.is_requested() { return Ok(false); } let sent = self @@ -149,27 +151,36 @@ impl StreamingConquer { .ok_or(ConquerError::Disconnected)? .send(cube); if sent.is_err() { - return if self.shared.stopped.load(Ordering::Acquire) { + return if self.shared.stopped.is_requested() { Ok(false) } else { Err(ConquerError::Disconnected) }; } self.shared.submitted.fetch_add(1, Ordering::Relaxed); - Ok(!self.shared.stopped.load(Ordering::Acquire)) + Ok(!self.shared.stopped.is_requested()) + } + + /// Clone the global first-answer signal so the cuber can stop at its own + /// safe boundaries when a conquer worker wins. + pub fn termination_signal(&self) -> TerminationSignal { + self.shared.stopped.clone() } /// Record a satisfying leaf found by the cuber itself. pub fn mark_sat(&self) { self.shared.sat.fetch_add(1, Ordering::Relaxed); - self.shared.stopped.store(true, Ordering::Release); + self.shared.stopped.request(); + } + + /// Stop all conquer workers after an integrated solver found a model. + pub fn mark_sat_with_witness(&self, witness: String) { + *self.shared.witness.lock().expect("witness lock") = Some(witness); + self.mark_sat(); } pub fn finish(mut self, cubing_complete: bool) -> Result { - self.sender.take(); - for worker in self.workers.drain(..) { - worker.join().map_err(|_| ConquerError::WorkerPanicked)?; - } + self.close_and_join(false)?; let submitted = self.shared.submitted.load(Ordering::Relaxed); let sat = self.shared.sat.load(Ordering::Relaxed); let unsat = self.shared.unsat.load(Ordering::Relaxed); @@ -191,6 +202,31 @@ impl StreamingConquer { witness, }) } + + fn close_and_join(&mut self, request_stop: bool) -> Result<(), ConquerError> { + if request_stop { + self.shared.stopped.request(); + } + self.sender.take(); + let mut worker_panicked = false; + for worker in self.workers.drain(..) { + worker_panicked |= worker.join().is_err(); + } + if worker_panicked { + Err(ConquerError::WorkerPanicked) + } else { + Ok(()) + } + } +} + +impl Drop for StreamingConquer { + fn drop(&mut self) { + // Error paths must not detach workers or leave their Kissat children + // behind. Each worker observes this signal in its polling loop, kills + // its active child, and is joined here before the pool disappears. + let _ = self.close_and_join(true); + } } fn worker_loop( @@ -200,21 +236,21 @@ fn worker_loop( kissat: PathBuf, ) { loop { - if shared.stopped.load(Ordering::Acquire) { + if shared.stopped.is_requested() { break; } let cube = match receiver.lock().expect("cube receiver lock").recv() { Ok(cube) => cube, Err(_) => break, }; - if shared.stopped.load(Ordering::Acquire) { + if shared.stopped.is_requested() { break; } match solve_cube(&template, &kissat, &shared, &cube) { Ok(CubeResult::Sat(output)) => { shared.sat.fetch_add(1, Ordering::Relaxed); *shared.witness.lock().expect("witness lock") = Some(output); - shared.stopped.store(true, Ordering::Release); + shared.stopped.request(); } Ok(CubeResult::Unsat) => { shared.unsat.fetch_add(1, Ordering::Relaxed); @@ -281,14 +317,21 @@ fn solve_cube( } let status = loop { - if shared.stopped.load(Ordering::Acquire) { + if shared.stopped.is_requested() { let _ = child.kill(); let _ = child.wait(); reader.join().expect("Kissat output reader panicked")?; return Ok(CubeResult::Cancelled); } - if let Some(status) = child.try_wait().map_err(ConquerError::StartKissat)? { - break status; + match child.try_wait() { + Ok(Some(status)) => break status, + Ok(None) => {} + Err(error) => { + let _ = child.kill(); + let _ = child.wait(); + let _ = reader.join(); + return Err(ConquerError::StartKissat(error)); + } } thread::sleep(std::time::Duration::from_millis(5)); }; diff --git a/src/cube.rs b/src/cube.rs index bdbd0ad..20b4147 100644 --- a/src/cube.rs +++ b/src/cube.rs @@ -21,7 +21,10 @@ use std::num::NonZeroUsize; use std::sync::Arc; use crate::adapter::BranchSolver; -use crate::ct::{apply_masked_assignment, ct_propagate, RSparseBitSet, TableMasks}; +use crate::cdcl::CdclPropagator; +use crate::ct::{ + apply_masked_assignment, ct_propagate, enqueue_var_change, RSparseBitSet, TableMasks, +}; use crate::domain::DomainMask; use crate::measure::Measure; use crate::network::ConstraintNetwork; @@ -29,6 +32,7 @@ use crate::problem::{SolverBuffer, Stats, TnProblem}; use crate::propagate::{dominate_fixpoint, failed_literal_fixpoint}; use crate::selector::{occurrence_pool, Selector, FAILED_LITERAL_POOL}; use crate::table::RegionRuleDiagnostics; +use crate::termination::TerminationSignal; use crate::trail::Trail; use crate::util::count_unfixed; @@ -49,6 +53,8 @@ pub struct CubeStats { pub cubes: usize, pub refuted: usize, pub sat_leaves: usize, + /// A decision-mode component found SAT before the frontier was complete. + pub stopped_early: bool, /// Nodes visited (branch decisions applied). pub visited: u64, } @@ -68,6 +74,7 @@ pub enum CubeRefutationReason { RootPropagation, SelectorNoFeasibleConfig, BranchPropagation, + CdclPropagationConflict, } /// One branching clause in the bit encoding over `CubeNodeTrace::variables`. @@ -96,7 +103,13 @@ pub struct CubeNodeTrace { /// control arm, which deliberately does not run the region machinery. pub rule_diagnostics: Option, pub variables: Vec, + /// Clauses selected by the branching-rule optimizer. Diagnostics such as + /// `branching_vector` and `gamma` describe this cover. + pub optimized_clauses: Vec, + /// Pairwise-disjoint clauses actually traversed by the CnC search. pub clauses: Vec, + /// For each traversed clause, the optimizer clause from which it was split. + pub partition_sources: Vec, } struct CubeCtx<'a> { @@ -105,6 +118,28 @@ struct CubeCtx<'a> { measure: Measure, solver: &'a BranchSolver, cutoff: CubeCutoff, + cdcl: Option, + cdcl_integration: CdclIntegrationMode, + sat_policy: CncSatPolicy, + termination: Option, +} + +impl CubeCtx<'_> { + fn candidate_cdcl(&self) -> Option<&CdclPropagator> { + match self.cdcl_integration { + CdclIntegrationMode::FullPropagation => self.cdcl.as_ref(), + CdclIntegrationMode::HybridCtCandidates => None, + } + } + + fn should_stop_for_sat(&self) -> bool { + if self.sat_policy != CncSatPolicy::StopDecision { + return false; + } + self.termination + .as_ref() + .is_some_and(TerminationSignal::is_requested) + } } /// Online stopping rule evaluated at each post-reduction search node. @@ -116,8 +151,58 @@ pub enum CubeCutoff { CcDifficulty(u128), } +/// Which propagation work is delegated to the persistent CDCL companion. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum CdclIntegrationMode { + /// Use CaDiCaL for real-node fixpoints and repeated branching-candidate BCP. + #[default] + FullPropagation, + /// Keep repeated candidate scoring on native CT while CaDiCaL propagates + /// selected branches and retains clauses learned from their conflicts. + HybridCtCandidates, +} + +/// Whether cube generation is exhaustive or participates in first-answer CnC. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum CncSatPolicy { + /// Preserve a complete exported frontier even when a solver finds SAT. + #[default] + CompleteFrontier, + /// Stop when the native cuber or any conquer worker proves SAT. + StopDecision, +} + +/// Optional persistent-CDCL integration for one cube-generation run. +#[derive(Clone)] +pub struct CubeCdclOptions { + pub propagator: CdclPropagator, + pub integration: CdclIntegrationMode, +} + +/// Orthogonal generation policies collected in one value to avoid a public +/// function for every cutoff/CDCL/termination combination. +#[derive(Clone)] +pub struct CubeGenerationOptions { + pub cutoff: CubeCutoff, + pub cdcl: Option, + pub sat_policy: CncSatPolicy, + pub termination: Option, +} + +impl CubeGenerationOptions { + pub fn new(cutoff: CubeCutoff) -> Self { + Self { + cutoff, + cdcl: None, + sat_policy: CncSatPolicy::CompleteFrontier, + termination: None, + } + } +} + impl CubeCutoff { - fn stops(self, sigma_dec: usize, sigma_all: usize, freevars: usize) -> bool { + /// Return whether a post-reduction node satisfies this cutoff. + pub fn stops(self, sigma_dec: usize, sigma_all: usize, freevars: usize) -> bool { match self { Self::RemainingVars(n) => freevars < n.get(), Self::CcDifficulty(threshold) => { @@ -193,6 +278,29 @@ pub fn generate_cubes_with_cutoff( cutoff: CubeCutoff, emit: F, ) -> Result +where + F: FnMut(Cube) -> Result<(), E>, +{ + generate_cubes_configured( + problem, + selector, + measure, + solver, + CubeGenerationOptions::new(cutoff), + emit, + ) +} + +/// Primary streaming entry point. This also covers CT-only cubing: a conquer +/// worker can stop the cuber even when no companion CDCL solver is configured. +pub fn generate_cubes_configured( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + options: CubeGenerationOptions, + emit: F, +) -> Result where F: FnMut(Cube) -> Result<(), E>, { @@ -201,12 +309,135 @@ where selector, measure, solver, - cutoff, + options, emit, None::<&mut fn(CubeNodeTrace) -> Result<(), E>>, ) } +/// Compatibility wrapper for callers that configure only SAT termination. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_policy( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + sat_policy: CncSatPolicy, + termination: Option, + emit: F, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, +{ + generate_cubes_configured( + problem, + selector, + measure, + solver, + CubeGenerationOptions { + cutoff, + cdcl: None, + sat_policy, + termination, + }, + emit, + ) +} + +/// CDCL-propagated form of [`generate_cubes_with_cutoff`]. The native network +/// still grows regions and maintains CT tables, while one persistent CaDiCaL +/// instance performs assumption propagation and retains conflict clauses. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_cdcl( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + cdcl: CdclPropagator, + emit: F, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, +{ + generate_cubes_with_cutoff_cdcl_mode( + problem, + selector, + measure, + solver, + cutoff, + cdcl, + CdclIntegrationMode::FullPropagation, + emit, + ) +} + +/// CDCL-assisted generation with an explicit propagation-integration policy. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_cdcl_mode( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + cdcl: CdclPropagator, + integration: CdclIntegrationMode, + emit: F, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, +{ + generate_cubes_with_cutoff_cdcl_policy( + problem, + selector, + measure, + solver, + cutoff, + cdcl, + integration, + CncSatPolicy::CompleteFrontier, + None, + emit, + ) +} + +/// CDCL-assisted generation with explicit propagation and SAT termination +/// policies. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_cdcl_policy( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + cdcl: CdclPropagator, + integration: CdclIntegrationMode, + sat_policy: CncSatPolicy, + termination: Option, + emit: F, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, +{ + generate_cubes_configured( + problem, + selector, + measure, + solver, + CubeGenerationOptions { + cutoff, + cdcl: Some(CubeCdclOptions { + propagator: cdcl, + integration, + }), + sat_policy, + termination, + }, + emit, + ) +} + /// Streaming cube generation with an additional callback for every tree node. /// The trace callback observes data already computed by the normal search and /// must not mutate solver state, so enabling it does not alter the frontier. @@ -242,18 +473,178 @@ pub fn generate_cubes_with_cutoff_trace( solver: &BranchSolver, cutoff: CubeCutoff, emit: F, + trace: T, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, + T: FnMut(CubeNodeTrace) -> Result<(), E>, +{ + generate_cubes_with_cutoff_trace_policy( + problem, + selector, + measure, + solver, + cutoff, + CncSatPolicy::CompleteFrontier, + None, + emit, + trace, + ) +} + +/// Traced generation with a shared first-answer signal. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_trace_policy( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + sat_policy: CncSatPolicy, + termination: Option, + emit: F, mut trace: T, ) -> Result where F: FnMut(Cube) -> Result<(), E>, T: FnMut(CubeNodeTrace) -> Result<(), E>, { - generate_cubes_impl( + generate_cubes_configured_with_trace( + problem, + selector, + measure, + solver, + CubeGenerationOptions { + cutoff, + cdcl: None, + sat_policy, + termination, + }, + emit, + &mut trace, + ) +} + +/// Traced counterpart of [`generate_cubes_with_cutoff_cdcl`]. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_trace_cdcl( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + cdcl: CdclPropagator, + emit: F, + trace: T, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, + T: FnMut(CubeNodeTrace) -> Result<(), E>, +{ + generate_cubes_with_cutoff_trace_cdcl_mode( + problem, + selector, + measure, + solver, + cutoff, + cdcl, + CdclIntegrationMode::FullPropagation, + emit, + trace, + ) +} + +/// Traced CDCL-assisted generation with an explicit integration policy. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_trace_cdcl_mode( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + cdcl: CdclPropagator, + integration: CdclIntegrationMode, + emit: F, + trace: T, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, + T: FnMut(CubeNodeTrace) -> Result<(), E>, +{ + generate_cubes_with_cutoff_trace_cdcl_policy( problem, selector, measure, solver, cutoff, + cdcl, + integration, + CncSatPolicy::CompleteFrontier, + None, + emit, + trace, + ) +} + +/// Traced CDCL-assisted generation with explicit propagation and SAT +/// termination policies. +#[allow(clippy::too_many_arguments)] +pub fn generate_cubes_with_cutoff_trace_cdcl_policy( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + cutoff: CubeCutoff, + cdcl: CdclPropagator, + integration: CdclIntegrationMode, + sat_policy: CncSatPolicy, + termination: Option, + emit: F, + mut trace: T, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, + T: FnMut(CubeNodeTrace) -> Result<(), E>, +{ + generate_cubes_configured_with_trace( + problem, + selector, + measure, + solver, + CubeGenerationOptions { + cutoff, + cdcl: Some(CubeCdclOptions { + propagator: cdcl, + integration, + }), + sat_policy, + termination, + }, + emit, + &mut trace, + ) +} + +/// Traced form of [`generate_cubes_configured`]. +pub fn generate_cubes_configured_with_trace( + problem: &mut TnProblem, + selector: Selector, + measure: Measure, + solver: &BranchSolver, + options: CubeGenerationOptions, + emit: F, + mut trace: T, +) -> Result +where + F: FnMut(Cube) -> Result<(), E>, + T: FnMut(CubeNodeTrace) -> Result<(), E>, +{ + generate_cubes_impl( + problem, + selector, + measure, + solver, + options, emit, Some(&mut trace), ) @@ -264,7 +655,7 @@ fn generate_cubes_impl( selector: Selector, measure: Measure, solver: &BranchSolver, - cutoff: CubeCutoff, + options: CubeGenerationOptions, mut emit: F, mut trace: Option<&mut T>, ) -> Result @@ -273,12 +664,24 @@ where T: FnMut(CubeNodeTrace) -> Result<(), E>, { problem.stats.reset(); + let (cdcl, cdcl_integration) = options + .cdcl + .map(|options| (Some(options.propagator), options.integration)) + .unwrap_or((None, CdclIntegrationMode::FullPropagation)); + let termination = match (options.sat_policy, options.termination) { + (CncSatPolicy::StopDecision, None) => Some(TerminationSignal::new()), + (_, termination) => termination, + }; let ctx = CubeCtx { cn: &problem.static_cn, selector, measure, solver, - cutoff, + cutoff: options.cutoff, + cdcl, + cdcl_integration, + sat_policy: options.sat_policy, + termination, }; let masks = &problem.masks; let stats = &mut problem.stats; @@ -291,9 +694,13 @@ where let mut decisions: Vec<(usize, bool)> = Vec::new(); let mut next_node_id = 0u64; let mark = trail.mark(); + let root_cdcl_refuted = + cdcl_propagate_then_ct(&ctx, doms, masks, tables, buffer, trail, &decisions); // Root already propagated; if it is already solved or refuted, that is a // single (degenerate) cube. - let result = if doms[0] == DomainMask::NONE { + let result = if ctx.should_stop_for_sat() { + Ok(()) + } else if doms[0] == DomainMask::NONE { if let Some(trace) = trace.as_deref_mut() { trace(CubeNodeTrace { node_id: 0, @@ -301,14 +708,20 @@ where child_index: None, depth: 0, kind: CubeNodeKind::Refuted, - refutation_reason: Some(CubeRefutationReason::RootPropagation), + refutation_reason: Some(if root_cdcl_refuted { + CubeRefutationReason::CdclPropagationConflict + } else { + CubeRefutationReason::RootPropagation + }), decisions: Vec::new(), sigma_dec: 0, sigma_all: 0, freevars: doms.len(), rule_diagnostics: None, variables: Vec::new(), + optimized_clauses: Vec::new(), clauses: Vec::new(), + partition_sources: Vec::new(), })?; } emit_cube( @@ -344,6 +757,7 @@ where trail.restore_to(mark, doms, tables); result?; + cube_stats.stopped_early = ctx.should_stop_for_sat(); cube_stats.visited = stats.total_visited_nodes; Ok(cube_stats) } @@ -384,6 +798,9 @@ where F: FnMut(Cube) -> Result<(), E>, T: FnMut(CubeNodeTrace) -> Result<(), E>, { + if ctx.should_stop_for_sat() { + return Ok(()); + } let node_id = *next_node_id; *next_node_id += 1; // march_cu -n cutoff on the current post-reduction node. The reductions run @@ -407,7 +824,9 @@ where freevars, rule_diagnostics: None, variables: Vec::new(), + optimized_clauses: Vec::new(), clauses: Vec::new(), + partition_sources: Vec::new(), })?; } return emit_cube( @@ -442,9 +861,17 @@ where freevars, rule_diagnostics: None, variables: Vec::new(), + optimized_clauses: Vec::new(), clauses: Vec::new(), + partition_sources: Vec::new(), })?; } + if ctx.sat_policy == CncSatPolicy::StopDecision { + ctx.termination + .as_ref() + .expect("decision mode always has a termination signal") + .request(); + } return emit_cube( cube_stats, emit, @@ -468,8 +895,13 @@ where tables, trail, &scope, + ctx.candidate_cdcl(), + decisions, trace.is_some(), ); + if ctx.should_stop_for_sat() { + return Ok(()); + } let clauses = match selection.clauses { // No rule (region proved locally UNSAT): refuted cube. None => { @@ -487,7 +919,9 @@ where freevars, rule_diagnostics: selection.diagnostics, variables: selection.variables, + optimized_clauses: Vec::new(), clauses: Vec::new(), + partition_sources: Vec::new(), })?; } return emit_cube( @@ -504,10 +938,32 @@ where } Some(clauses) => clauses, }; + // Optimal-branching rules are set covers: their conjunctions may overlap. + // That is acceptable for branch-and-reduce, but a CnC frontier must be a + // partition or the same residual search space can be submitted repeatedly. + // Subtract earlier cubes from each later cube to obtain an equivalent + // disjoint DNF before descending. + let optimized_clauses = clauses; + let partition = disjointize_clauses_with_sources(&optimized_clauses); + let clauses = partition + .iter() + .map(|(clause, _)| *clause) + .collect::>(); + let partition_sources = partition + .iter() + .map(|(_, source)| *source) + .collect::>(); let variables = selection.variables; let rule_diagnostics = selection.diagnostics; if let Some(trace) = trace.as_deref_mut() { + let trace_optimized_clauses = optimized_clauses + .iter() + .map(|clause| TraceClause { + mask: clause.mask, + value: clause.val, + }) + .collect(); let trace_clauses = clauses .iter() .map(|clause| TraceClause { @@ -528,11 +984,16 @@ where freevars, rule_diagnostics, variables: variables.clone(), + optimized_clauses: trace_optimized_clauses, clauses: trace_clauses, + partition_sources: partition_sources.clone(), })?; } for (branch_index, cl) in clauses.iter().enumerate() { + if ctx.should_stop_for_sat() { + break; + } stats.record_visit(); trail.open(); let mark = trail.mark(); @@ -545,15 +1006,24 @@ where } } apply_masked_assignment(ctx.cn, doms, buffer, trail, &variables, cl.mask, cl.val); - ct_propagate(ctx.cn, doms, masks, tables, buffer, trail); - if doms[0] != DomainMask::NONE { + // The selected branch reaches CaDiCaL before native CT propagation. + // Thus an immediate branch conflict is analyzed and learned by CDCL + // instead of being consumed first by the native propagator. + let cdcl_refuted = + cdcl_propagate_then_ct(ctx, doms, masks, tables, buffer, trail, decisions); + let mut stop_for_sat = ctx.should_stop_for_sat(); + if !stop_for_sat && doms[0] != DomainMask::NONE { dominate_fixpoint(ctx.cn, doms, masks, tables, buffer, trail); } - if doms[0] != DomainMask::NONE { + stop_for_sat |= ctx.should_stop_for_sat(); + if !stop_for_sat && doms[0] != DomainMask::NONE { let pool = occurrence_pool(ctx.cn, doms, buffer, masks, FAILED_LITERAL_POOL); failed_literal_fixpoint(ctx.cn, doms, masks, tables, buffer, trail, &pool); } - let branch_result = if doms[0] == DomainMask::NONE { + stop_for_sat |= ctx.should_stop_for_sat(); + let branch_result = if stop_for_sat { + Ok(()) + } else if doms[0] == DomainMask::NONE { // Branch closed by propagation: refuted cube (no conquer needed). let branch_freevars = count_unfixed(doms); let child_node_id = *next_node_id; @@ -565,14 +1035,20 @@ where child_index: Some(branch_index), depth: depth + 1, kind: CubeNodeKind::Refuted, - refutation_reason: Some(CubeRefutationReason::BranchPropagation), + refutation_reason: Some(if cdcl_refuted { + CubeRefutationReason::CdclPropagationConflict + } else { + CubeRefutationReason::BranchPropagation + }), decisions: decisions.clone(), sigma_dec: decisions.len(), sigma_all: doms.len() - branch_freevars, freevars: branch_freevars, rule_diagnostics: None, variables: Vec::new(), + optimized_clauses: Vec::new(), clauses: Vec::new(), + partition_sources: Vec::new(), })?; } emit_cube( @@ -608,15 +1084,144 @@ where decisions.truncate(decision_base); trail.restore_to(mark, doms, tables); branch_result?; + if stop_for_sat || ctx.should_stop_for_sat() { + break; + } } Ok(()) } +/// Convert a DNF cube cover into an equivalent pairwise-disjoint cube cover. +/// +/// Clauses are processed in order. Each new cube has the union of all earlier +/// output cubes subtracted from it; subtraction of one conjunction from another +/// uses the standard prefix split of `A ∧ ¬B`. +#[cfg(test)] +fn disjointize_clauses( + clauses: &[optimal_branching_core::Clause], +) -> Vec { + disjointize_clauses_with_sources(clauses) + .into_iter() + .map(|(clause, _)| clause) + .collect() +} + +fn disjointize_clauses_with_sources( + clauses: &[optimal_branching_core::Clause], +) -> Vec<(optimal_branching_core::Clause, usize)> { + let mut disjoint = Vec::new(); + for (source, &clause) in clauses.iter().enumerate() { + let mut pieces = vec![clause]; + for &(covered, _) in &disjoint { + pieces = pieces + .into_iter() + .flat_map(|piece| subtract_clause(piece, covered)) + .collect(); + if pieces.is_empty() { + break; + } + } + disjoint.extend(pieces.into_iter().map(|piece| (piece, source))); + } + disjoint +} + +fn subtract_clause( + minuend: optimal_branching_core::Clause, + subtrahend: optimal_branching_core::Clause, +) -> Vec { + let shared = minuend.mask & subtrahend.mask; + if ((minuend.val ^ subtrahend.val) & shared) != 0 { + return vec![minuend]; + } + + let mut remaining = subtrahend.mask & !minuend.mask; + if remaining == 0 { + return Vec::new(); + } + + let mut prefix = minuend; + let mut pieces = Vec::with_capacity(remaining.count_ones() as usize); + while remaining != 0 { + let bit = remaining & remaining.wrapping_neg(); + let required = subtrahend.val & bit; + pieces.push(optimal_branching_core::Clause::new( + prefix.mask | bit, + prefix.val | (required ^ bit), + )); + prefix = optimal_branching_core::Clause::new(prefix.mask | bit, prefix.val | required); + remaining &= !bit; + } + pieces +} + +/// Apply the committed decision path to persistent CaDiCaL exactly once, then +/// project its native implications into one native CT fixpoint. CDCL auxiliaries +/// stay private to CaDiCaL; every newly fixed native variable is trailed and +/// sent through CT so later region work sees a coherent native store. +/// +/// Returns true exactly when CaDiCaL's assumption propagation found the +/// conflict. A native CT conflict returns false so traces preserve provenance. +fn cdcl_propagate_then_ct( + ctx: &CubeCtx<'_>, + doms: &mut [DomainMask], + masks: &[TableMasks], + tables: &mut [RSparseBitSet], + buffer: &mut SolverBuffer, + trail: &mut Trail, + decisions: &[(usize, bool)], +) -> bool { + let Some(cdcl) = &ctx.cdcl else { + ct_propagate(ctx.cn, doms, masks, tables, buffer, trail); + return false; + }; + if doms.first() == Some(&DomainMask::NONE) { + return false; + } + let projected = cdcl + .propagate_decisions(doms, decisions) + .expect("CDCL node propagation failed"); + if projected.first() == Some(&DomainMask::NONE) { + set_contradiction(doms, trail); + return true; + } + for (var, &implied) in projected.iter().enumerate() { + if !implied.is_fixed() { + continue; + } + match doms[var] { + DomainMask::BOTH => { + trail.record_dom(var, doms[var]); + doms[var] = implied; + enqueue_var_change(ctx.cn, buffer, var); + } + current if current == implied => {} + _ => { + set_contradiction(doms, trail); + return true; + } + } + } + ct_propagate(ctx.cn, doms, masks, tables, buffer, trail); + false +} + +fn set_contradiction(doms: &mut [DomainMask], trail: &mut Trail) { + if let Some(sentinel) = doms.first_mut() { + if *sentinel != DomainMask::NONE { + trail.record_dom(0, *sentinel); + *sentinel = DomainMask::NONE; + } + } +} + #[cfg(test)] mod tests { use super::*; + use crate::cdcl::CdclPropagator; use crate::dimacs::network_from_dimacs; use optimal_branching_core::GreedyMerge; + use std::io::Cursor; fn xor_chain() -> TnProblem { let cnf = "p cnf 3 4\n1 2 0\n-1 -2 0\n2 3 0\n-2 -3 0\n"; @@ -628,6 +1233,28 @@ mod tests { NonZeroUsize::new(value).expect("test cutoff must be nonzero") } + #[test] + fn overlapping_branch_cover_is_disjointized_without_changing_union() { + use optimal_branching_core::Clause; + + // x0=0 and x1=0 overlap on 00*. The third cube also overlaps both. + let cover = vec![ + Clause::new(0b001, 0), + Clause::new(0b010, 0), + Clause::new(0b100, 0b100), + ]; + let partition = disjointize_clauses(&cover); + + for assignment in 0..8 { + let covered = cover.iter().any(|clause| clause.covered_by(assignment)); + let partition_count = partition + .iter() + .filter(|clause| clause.covered_by(assignment)) + .count(); + assert_eq!(partition_count, usize::from(covered)); + } + } + /// A cutoff larger than the root residual emits the empty decision path, /// while a cutoff equal to the residual must branch because `-n` is strict. #[test] @@ -659,6 +1286,134 @@ mod tests { .all(|c| !c.decisions.is_empty())); } + #[test] + fn hybrid_keeps_cdcl_at_committed_nodes_not_candidate_probes() { + const CNF: &str = "p cnf 3 4\n1 2 0\n-1 -2 0\n2 3 0\n-2 -3 0\n"; + + fn run(integration: CdclIntegrationMode) -> (Vec, CubeStats, crate::cdcl::CdclStats) { + let mut problem = xor_chain(); + let mut reader = Cursor::new(CNF.as_bytes()); + let cdcl = CdclPropagator::from_dimacs(&mut reader, vec![0, 1, 2]) + .expect("create CaDiCaL companion"); + let mut cubes = Vec::new(); + let stats = generate_cubes_with_cutoff_cdcl_mode( + &mut problem, + Selector::MostOccurrence { max_rows: 1 }, + Measure::NumUnfixedVars, + &BranchSolver::Greedy(GreedyMerge), + CubeCutoff::RemainingVars(n(3)), + cdcl.clone(), + integration, + |cube| { + cubes.push(cube); + Ok::<(), Infallible>(()) + }, + ) + .expect("infallible callback"); + let cdcl_stats = cdcl.stats(); + (cubes, stats, cdcl_stats) + } + + let (full_cubes, full_stats, full_cdcl) = run(CdclIntegrationMode::FullPropagation); + let (hybrid_cubes, hybrid_stats, hybrid_cdcl) = + run(CdclIntegrationMode::HybridCtCandidates); + + assert_eq!(full_stats.cubes, hybrid_stats.cubes); + assert_eq!(full_stats.refuted, hybrid_stats.refuted); + assert_eq!(full_stats.sat_leaves, hybrid_stats.sat_leaves); + assert_eq!(full_stats.visited, hybrid_stats.visited); + assert_eq!(full_cubes.len(), hybrid_cubes.len()); + for (full, hybrid) in full_cubes.iter().zip(&hybrid_cubes) { + assert_eq!(full.decisions, hybrid.decisions); + assert_eq!(full.sigma_dec, hybrid.sigma_dec); + assert_eq!(full.sigma_all, hybrid.sigma_all); + assert_eq!(full.refuted, hybrid.refuted); + assert_eq!(full.sat, hybrid.sat); + } + assert!( + hybrid_cdcl.propagation_calls < full_cdcl.propagation_calls, + "hybrid should eliminate candidate BCP calls: full={}, hybrid={}", + full_cdcl.propagation_calls, + hybrid_cdcl.propagation_calls + ); + assert!( + hybrid_cdcl.propagation_calls > 0, + "hybrid must still propagate at committed nodes" + ); + assert_eq!( + hybrid_cdcl.propagation_calls, + hybrid_stats.visited + 1, + "hybrid performs one root query and one query per committed branch" + ); + } + + #[test] + fn cdcl_only_emits_after_cutoff_and_never_starts_a_full_search() { + const CNF: &str = "p cnf 3 4\n1 2 0\n-1 -2 0\n2 3 0\n-2 -3 0\n"; + let mut reader = Cursor::new(CNF.as_bytes()); + let cdcl = CdclPropagator::from_dimacs(&mut reader, vec![0, 1, 2]) + .expect("create CaDiCaL companion"); + let mut problem = xor_chain(); + let mut cubes = Vec::new(); + let mut nodes = Vec::new(); + let stats = generate_cubes_with_cutoff_trace_cdcl_policy( + &mut problem, + Selector::MostOccurrence { max_rows: 1 }, + Measure::NumUnfixedVars, + &BranchSolver::Greedy(GreedyMerge), + CubeCutoff::RemainingVars(n(3)), + cdcl.clone(), + CdclIntegrationMode::HybridCtCandidates, + CncSatPolicy::StopDecision, + None, + |cube| { + cubes.push(cube); + Ok::<(), Infallible>(()) + }, + |node| { + nodes.push(node); + Ok::<(), Infallible>(()) + }, + ) + .expect("infallible callbacks"); + + assert!(!stats.stopped_early); + assert!(!cubes.is_empty()); + assert!(cubes.iter().all(|cube| cube.refuted || cube.sat || { + let freevars = 3 - cube.sigma_all; + freevars < 3 && !cube.decisions.is_empty() + })); + assert!(nodes.iter().any(|node| node.kind == CubeNodeKind::Branch)); + assert!(nodes.iter().any(|node| node.kind == CubeNodeKind::Cutoff)); + assert_eq!(cdcl.stats().full_search_calls, 0); + } + + #[test] + fn decision_policy_honors_a_conquer_stop_without_cdcl() { + let signal = TerminationSignal::new(); + signal.request(); + let mut problem = xor_chain(); + let mut cubes = Vec::new(); + + let stats = generate_cubes_with_cutoff_policy( + &mut problem, + Selector::MostOccurrence { max_rows: 1 }, + Measure::NumUnfixedVars, + &BranchSolver::Greedy(GreedyMerge), + CubeCutoff::RemainingVars(n(3)), + CncSatPolicy::StopDecision, + Some(signal), + |cube| { + cubes.push(cube); + Ok::<(), Infallible>(()) + }, + ) + .expect("infallible callback"); + + assert!(stats.stopped_early); + assert!(cubes.is_empty()); + } + #[test] fn cc_difficulty_cutoff_is_evaluated_online() { let mut problem = xor_chain(); diff --git a/src/lib.rs b/src/lib.rs index 542e365..4fd38fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod adapter; pub mod api; pub mod canonicalize; +pub mod cdcl; pub mod circuit; pub mod conquer; pub mod contract; @@ -19,5 +20,6 @@ pub mod selector; pub mod solver; pub mod table; pub mod tail_greedy; +pub mod termination; pub mod trail; pub mod util; diff --git a/src/selector.rs b/src/selector.rs index fcdf571..4955dc8 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -3,12 +3,13 @@ use std::sync::Arc; use optimal_branching_core::Clause; use crate::adapter::BranchSolver; +use crate::cdcl::CdclPropagator; use crate::ct::{RSparseBitSet, TableMasks}; use crate::domain::DomainMask; use crate::measure::Measure; use crate::network::ConstraintNetwork; use crate::problem::SolverBuffer; -use crate::table::{compute_branching_result, BranchingResult}; +use crate::table::BranchingResult; use crate::trail::Trail; use crate::util::{active_tensors, is_entailed}; @@ -148,6 +149,8 @@ impl Selector { tables: &mut Vec, trail: &mut Trail, scope: &[usize], + cdcl: Option<&CdclPropagator>, + cdcl_decisions: &[(usize, bool)], collect_diagnostics: bool, ) -> BranchingResult { let var_id = select_var_most_occurrence(cn, doms, buffer, scope, masks); @@ -195,7 +198,7 @@ impl Selector { diagnostics: None, }; } - compute_branching_result( + crate::table::compute_branching_result_with_cdcl( cn, doms, buffer, @@ -206,6 +209,8 @@ impl Selector { masks, tables, trail, + cdcl, + cdcl_decisions, collect_diagnostics || self.replays_same_state(), self.replays_same_state(), ) @@ -295,6 +300,8 @@ mod tests { &mut tables, &mut trail, &[0, 1], + None, + &[], false, ); assert_eq!(result.variables, vec![0, 1]); @@ -328,6 +335,8 @@ mod tests { &mut tables, &mut trail, &[0, 1, 2, 3], + None, + &[], false, ); assert!(result.clauses.is_some()); @@ -344,6 +353,8 @@ mod tests { &mut tables, &mut trail, &[0, 1, 2, 3], + None, + &[], true, ); assert_eq!(traced.clauses, result.clauses); diff --git a/src/solver.rs b/src/solver.rs index 9fe9667..08ad747 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -104,6 +104,8 @@ fn bbsat_rec( tables, trail, scope, + None, + &[], false, ); let clauses = match selection.clauses { diff --git a/src/table.rs b/src/table.rs index 2bcc18c..4b47534 100644 --- a/src/table.rs +++ b/src/table.rs @@ -4,6 +4,7 @@ use std::time::Instant; use optimal_branching_core::{BranchingTable, Clause, NaiveBranch, OptimalBranchingResult, DNF}; use crate::adapter::{with_measure_scratch, BranchSolver, MeasureAdapter, RuleProblem}; +use crate::cdcl::CdclPropagator; use crate::ct::{RSparseBitSet, TableMasks}; use crate::domain::DomainMask; use crate::measure::Measure; @@ -160,6 +161,44 @@ pub fn compute_branching_result( trail: &mut Trail, collect_diagnostics: bool, replay_diagnostics: bool, +) -> BranchingResult { + compute_branching_result_with_cdcl( + cn, + doms, + buffer, + var_id, + max_rows, + measure, + solver, + masks, + tables, + trail, + None, + &[], + collect_diagnostics, + replay_diagnostics, + ) +} + +/// CDCL-scored form of [`compute_branching_result`]. Region growth and global +/// feasibility remain native/CT; only the many hypothetical `apply_branch` +/// probes performed by the rule optimizer use assumption-only CDCL BCP. +#[allow(clippy::too_many_arguments)] +pub fn compute_branching_result_with_cdcl( + cn: &Arc, + doms: &mut [DomainMask], + buffer: &mut SolverBuffer, + var_id: usize, + max_rows: usize, + measure: Measure, + solver: &BranchSolver, + masks: &Arc>, + tables: &mut Vec, + trail: &mut Trail, + cdcl: Option<&CdclPropagator>, + cdcl_decisions: &[(usize, bool)], + collect_diagnostics: bool, + replay_diagnostics: bool, ) -> BranchingResult { debug_assert!(!replay_diagnostics || collect_diagnostics); // 1. Grow the region and keep only its GAC-feasible configs, decided with @@ -170,9 +209,11 @@ pub fn compute_branching_result( // Growth already knows whether the live frontier is empty. Enumerate the // exact boundary only for trace diagnostics; production search pays no // second incidence scan and allocates no boundary vector. - let boundary_variables = collect_diagnostics - .then(|| boundary_vars(cn, ®ion, doms, masks).len()) - .unwrap_or(0); + let boundary_variables = if collect_diagnostics { + boundary_vars(cn, ®ion, doms, masks).len() + } else { + 0 + }; debug_assert!(!collect_diagnostics || closed == (boundary_variables == 0)); let region_tensors = region.tensors.len(); let region_vars = region.vars; @@ -235,7 +276,7 @@ pub fn compute_branching_result( let same_state_replay = if replay_diagnostics { let groups: Vec> = feasible.iter().map(|&c| vec![c]).collect(); let table = BranchingTable::new(region_vars.len(), groups); - let problem = RuleProblem::new(Arc::clone(cn), Arc::clone(masks), doms.to_vec()); + let problem = rule_problem(cn, masks, doms, cdcl, cdcl_decisions); Some(with_measure_scratch(doms, tables, buffer, trail, || { replay_same_state(&problem, &table, ®ion_vars, var_id, measure) })) @@ -273,12 +314,11 @@ pub fn compute_branching_result( // framework computes each candidate's measure reduction itself // (apply_branch + measure) and applies the literal-count fallback when the // measure is degenerate, so IPSolver/LPSolver/GreedyMerge/NaiveBranch all - // produce the rule through this one call. `apply_branch` uses CT via the - // thread-local measure scratch primed here. - let problem = RuleProblem::new(Arc::clone(cn), Arc::clone(masks), doms.to_vec()); - // Lend the live CT state to the measure scratch so apply_branch propagates - // with CT instead of the linear rescan. apply_branch restores it to base - // after every candidate, so `doms`/`tables`/`buffer`/`trail` are unchanged here. + // produce the rule through this one call. `apply_branch` uses the selected + // CDCL or CT propagation backend. + let problem = rule_problem(cn, masks, doms, cdcl, cdcl_decisions); + // Keep CT scratch primed for the CT backend and replay path. CDCL candidate + // calls ignore it. Either way, `doms`/`tables`/`buffer`/`trail` are unchanged. let (result, rule_solver_ns, same_state_replay) = with_measure_scratch(doms, tables, buffer, trail, || { let rule_start = collect_diagnostics.then(Instant::now); @@ -301,9 +341,8 @@ pub fn compute_branching_result( ); true }); - BranchingResult { - clauses: Some(result.optimal_rule.clauses), - diagnostics: collect_diagnostics.then(|| RegionRuleDiagnostics { + let diagnostics = if collect_diagnostics { + Some(RegionRuleDiagnostics { focus_var: var_id, region_tensors, region_variables: region_vars.len(), @@ -319,11 +358,31 @@ pub fn compute_branching_result( feasibility_probe_ns, rule_solver_ns, same_state_replay, - }), + }) + } else { + None + }; + BranchingResult { + clauses: Some(result.optimal_rule.clauses), + diagnostics, variables: region_vars, } } +fn rule_problem( + cn: &Arc, + masks: &Arc>, + doms: &[DomainMask], + cdcl: Option<&CdclPropagator>, + cdcl_decisions: &[(usize, bool)], +) -> RuleProblem { + let problem = RuleProblem::new(Arc::clone(cn), Arc::clone(masks), doms.to_vec()); + match cdcl { + Some(cdcl) => problem.with_cdcl(cdcl.clone(), cdcl_decisions.to_vec()), + None => problem, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/termination.rs b/src/termination.rs new file mode 100644 index 0000000..fb72a62 --- /dev/null +++ b/src/termination.rs @@ -0,0 +1,27 @@ +//! Shared first-answer termination for Cube-and-Conquer components. + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +/// A clone-cheap stop signal shared by the cuber, its CDCL companion, and all +/// conquer workers. +#[derive(Clone, Debug, Default)] +pub struct TerminationSignal { + requested: Arc, +} + +impl TerminationSignal { + pub fn new() -> Self { + Self::default() + } + + /// Publish that one sound component has decided the instance. + pub fn request(&self) { + self.requested.store(true, Ordering::Release); + } + + /// Observe a previously published terminal result. + pub fn is_requested(&self) -> bool { + self.requested.load(Ordering::Acquire) + } +} diff --git a/tests/cnc_cuber_trace.rs b/tests/cnc_cuber_trace.rs index ee058fd..ba8ee23 100644 --- a/tests/cnc_cuber_trace.rs +++ b/tests/cnc_cuber_trace.rs @@ -115,7 +115,9 @@ fn trace_flag_preserves_cubes_and_writes_original_variable_ids() { .map(|line| serde_json::from_str(line).expect("valid trace JSON")) .collect(); assert!(!records.is_empty()); - assert!(records.iter().all(|record| record["schema_version"] == 2)); + assert!(records + .iter() + .all(|record| record.get("schema_version").is_none())); assert!(records .iter() .all(|record| record["search_semantics"] == "sat-decision")); @@ -124,6 +126,8 @@ fn trace_flag_preserves_cubes_and_writes_original_variable_ids() { .iter() .all(|record| record["branch_solver"] == "greedy")); assert!(records.iter().all(|record| record["measure"] == "vars")); + assert!(records.iter().all(|record| record["propagation"] == "ct")); + assert!(records.iter().all(|record| record["cdcl_mode"] == "off")); assert!(records .iter() .all(|record| record["input_kind"] == "dimacs")); @@ -207,6 +211,242 @@ fn trace_flag_preserves_cubes_and_writes_original_variable_ids() { fs::remove_dir_all(dir).expect("remove temp directory"); } +#[test] +fn cdcl_propagation_matches_ct_on_a_clause_network() { + let dir = temp_dir(); + fs::create_dir_all(&dir).expect("create temp directory"); + let input = dir.join("input.cnf"); + let ct_cubes = dir.join("ct.cubes"); + let cdcl_cubes = dir.join("cdcl.cubes"); + let cdcl_trace = dir.join("cdcl.jsonl"); + fs::write( + &input, + "p cnf 6 8\n\ + 1 2 0\n-1 -2 0\n2 3 0\n-2 -3 0\n\ + 4 5 0\n-4 -5 0\n5 6 0\n-5 -6 0\n", + ) + .expect("write CNF"); + + let binary = env!("CARGO_BIN_EXE_cnc_cuber"); + let common = [ + "-n", + "4", + "--branch-solver", + "greedy", + "--measure", + "vars", + "--max-rows", + "1", + ]; + let ct = Command::new(binary) + .arg(&input) + .args(["-o"]) + .arg(&ct_cubes) + .args(common) + .output() + .expect("run CT cuber"); + assert!(ct.status.success(), "{ct:?}"); + + let cdcl = Command::new(binary) + .arg(&input) + .args(["-o"]) + .arg(&cdcl_cubes) + .args(common) + .args(["--propagation", "cdcl", "--trace"]) + .arg(&cdcl_trace) + .output() + .expect("run CDCL cuber"); + assert!(cdcl.status.success(), "{cdcl:?}"); + assert_eq!(fs::read(&ct_cubes).unwrap(), fs::read(&cdcl_cubes).unwrap()); + assert!( + String::from_utf8_lossy(&cdcl.stderr).contains("propagation=cdcl"), + "{cdcl:?}" + ); + let records: Vec = fs::read_to_string(&cdcl_trace) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).expect("valid trace JSON")) + .collect(); + assert!(!records.is_empty()); + assert!(records.iter().all(|record| record["propagation"] == "cdcl")); + assert!(records + .iter() + .all(|record| record["cdcl_mode"] == "branch-learning")); + + fs::remove_dir_all(dir).expect("remove temp directory"); +} + +#[test] +fn branch_learning_handles_a_non_bcp_unsat_formula_without_full_search() { + let dir = temp_dir(); + fs::create_dir_all(&dir).expect("create temp directory"); + let input = dir.join("learning-unsat.cnf"); + let cubes = dir.join("learning-unsat.cubes"); + let trace = dir.join("learning-unsat.jsonl"); + // PHP(4,3): root BCP stays open. Repeated branch propagation retains + // conflict clauses, but the cuber-side CaDiCaL never starts a full solve. + fs::write( + &input, + "p cnf 12 22\n\ + 1 2 3 0\n4 5 6 0\n7 8 9 0\n10 11 12 0\n\ + -1 -4 0\n-1 -7 0\n-1 -10 0\n-4 -7 0\n-4 -10 0\n-7 -10 0\n\ + -2 -5 0\n-2 -8 0\n-2 -11 0\n-5 -8 0\n-5 -11 0\n-8 -11 0\n\ + -3 -6 0\n-3 -9 0\n-3 -12 0\n-6 -9 0\n-6 -12 0\n-9 -12 0\n", + ) + .expect("write non-BCP UNSAT CNF"); + + let run = Command::new(env!("CARGO_BIN_EXE_cnc_cuber")) + .arg(&input) + .args(["-n", "1", "-o"]) + .arg(&cubes) + .args([ + "--branch-solver", + "greedy", + "--measure", + "vars", + "--propagation", + "cdcl", + "--trace", + ]) + .arg(&trace) + .output() + .expect("run learning CDCL cuber"); + assert!(run.status.success(), "{run:?}"); + assert!(fs::read_to_string(&cubes).unwrap().is_empty()); + let records: Vec = fs::read_to_string(&trace) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).expect("valid trace JSON")) + .collect(); + assert!(!records.is_empty()); + assert!(records + .iter() + .all(|record| record["cdcl_mode"] == "branch-learning")); + let stderr = String::from_utf8_lossy(&run.stderr); + assert!(stderr.contains("cdcl_mode=branch-learning"), "{stderr}"); + assert!(stderr.contains("full_search_calls=0"), "{stderr}"); + assert!(stderr.contains("learned_total="), "{stderr}"); + + fs::remove_dir_all(dir).expect("remove temp directory"); +} + +#[test] +fn hybrid_uses_ct_candidates_and_committed_branch_cdcl_learning() { + let dir = temp_dir(); + fs::create_dir_all(&dir).expect("create temp directory"); + let input = dir.join("learning-unsat.cnf"); + let cubes = dir.join("hybrid.cubes"); + let trace = dir.join("hybrid.jsonl"); + fs::write( + &input, + "p cnf 12 22\n\ + 1 2 3 0\n4 5 6 0\n7 8 9 0\n10 11 12 0\n\ + -1 -4 0\n-1 -7 0\n-1 -10 0\n-4 -7 0\n-4 -10 0\n-7 -10 0\n\ + -2 -5 0\n-2 -8 0\n-2 -11 0\n-5 -8 0\n-5 -11 0\n-8 -11 0\n\ + -3 -6 0\n-3 -9 0\n-3 -12 0\n-6 -9 0\n-6 -12 0\n-9 -12 0\n", + ) + .expect("write non-BCP UNSAT CNF"); + + let binary = env!("CARGO_BIN_EXE_cnc_cuber"); + let run = Command::new(binary) + .arg(&input) + .args(["-n", "1", "-o"]) + .arg(&cubes) + .args([ + "--branch-solver", + "greedy", + "--measure", + "vars", + "--propagation", + "hybrid", + "--trace", + ]) + .arg(&trace) + .output() + .expect("run hybrid cuber"); + assert!(run.status.success(), "{run:?}"); + assert!(fs::read_to_string(&cubes).unwrap().is_empty()); + let records: Vec = fs::read_to_string(&trace) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).expect("valid trace JSON")) + .collect(); + assert!(!records.is_empty()); + assert!(records + .iter() + .all(|record| record["propagation"] == "hybrid")); + assert!(records + .iter() + .all(|record| record["cdcl_mode"] == "branch-learning")); + let stderr = String::from_utf8_lossy(&run.stderr); + assert!(stderr.contains("propagation=hybrid"), "{stderr}"); + assert!(stderr.contains("full_search_calls=0"), "{stderr}"); + assert!(stderr.contains("learned_total="), "{stderr}"); + + fs::remove_dir_all(dir).expect("remove temp directory"); +} + +#[test] +fn native_regions_can_use_a_matching_flattened_cnf_for_cdcl_propagation() { + let dir = temp_dir(); + fs::create_dir_all(&dir).expect("create temp directory"); + let input = dir.join("xor.json"); + let cnf = dir.join("xor.cnf"); + let ct_cubes = dir.join("ct.cubes"); + let cdcl_cubes = dir.join("cdcl.cubes"); + fs::write( + &input, + r#"{ + "variables": ["a", "b", "c"], + "circuit": {"assignments": [ + {"outputs": ["c"], "expr": {"op": {"Xor": [ + {"op": {"Var": "a"}}, {"op": {"Var": "b"}} + ]}}} + ]} + }"#, + ) + .expect("write CircuitSAT"); + fs::write( + &cnf, + "p cnf 3 4\n-1 -2 -3 0\n1 2 -3 0\n1 -2 3 0\n-1 2 3 0\n", + ) + .expect("write matching Tseitin CNF"); + + let binary = env!("CARGO_BIN_EXE_cnc_cuber"); + let common = [ + "-n", + "3", + "--branch-solver", + "greedy", + "--measure", + "vars", + "--max-rows", + "1", + ]; + let ct = Command::new(binary) + .arg(&input) + .args(["-o"]) + .arg(&ct_cubes) + .args(common) + .output() + .expect("run native CT cuber"); + assert!(ct.status.success(), "{ct:?}"); + + let cdcl = Command::new(binary) + .arg(&input) + .args(["-o"]) + .arg(&cdcl_cubes) + .args(common) + .args(["--propagation", "cdcl", "--propagate-cnf"]) + .arg(&cnf) + .output() + .expect("run native/CDCL cuber"); + assert!(cdcl.status.success(), "{cdcl:?}"); + assert_eq!(fs::read(&ct_cubes).unwrap(), fs::read(&cdcl_cubes).unwrap()); + + fs::remove_dir_all(dir).expect("remove temp directory"); +} + #[test] fn structure_blind_selector_is_auditable_binary_control() { let dir = temp_dir(); @@ -347,7 +587,7 @@ fn root_refutation_trace_records_a_semantic_closure_reason() { let record: serde_json::Value = serde_json::from_str(fs::read_to_string(&trace).unwrap().trim()).unwrap(); assert_eq!(record["kind"], "refuted"); - assert_eq!(record["schema_version"], 2); + assert!(record.get("schema_version").is_none()); assert!(record["rule_diagnostics"].is_null()); assert_eq!( record["refutation_reason"], diff --git a/tests/cnc_streaming.rs b/tests/cnc_streaming.rs index ed08b21..e17e4fa 100644 --- a/tests/cnc_streaming.rs +++ b/tests/cnc_streaming.rs @@ -4,17 +4,21 @@ use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::{Path, PathBuf}; use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use boolean_inference::conquer::{ConquerResult, StreamingConquer}; +static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(0); + fn temp_dir() -> PathBuf { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock after epoch") .as_nanos(); + let sequence = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!( - "boolean-inference-streaming-{}-{nonce}", + "boolean-inference-streaming-{}-{nonce}-{sequence}", std::process::id() )) } @@ -96,6 +100,150 @@ fn streaming_mode_stops_after_a_sat_cube() { fs::remove_dir_all(dir).unwrap(); } +#[test] +fn conquer_first_answer_interrupts_the_ct_cuber() { + let dir = temp_dir(); + fs::create_dir_all(&dir).unwrap(); + let cnf = dir.join("many-cubes.cnf"); + let mut formula = String::from("p cnf 12 12\n"); + for pair in 0..6 { + let left = pair * 2 + 1; + let right = left + 1; + formula.push_str(&format!("{left} {right} 0\n-{left} -{right} 0\n")); + } + fs::write(&cnf, formula).unwrap(); + + let kissat = dir.join("kissat-first-answer"); + fs::write( + &kissat, + "#!/bin/sh\n\ + [ \"$#\" -eq 1 ] && [ \"$1\" = --relaxed ] || exit 3\n\ + cat >/dev/null\n\ + echo 's SATISFIABLE'\n\ + exit 10\n", + ) + .unwrap(); + let mut permissions = fs::metadata(&kissat).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&kissat, permissions).unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_cnc_cuber")) + .arg(&cnf) + .args(["-n", "1", "--solve-cnf"]) + .arg(&cnf) + .args([ + "--kissat", + kissat.to_str().unwrap(), + "--workers", + "1", + "--selector", + "structure-blind", + "--branch-solver", + "greedy", + "--measure", + "vars", + ]) + .output() + .expect("run first-answer CnC"); + + assert_eq!(output.status.code(), Some(10), "{output:?}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("status=SAT_EARLY cubes_submitted="), + "{stderr}" + ); + assert!(stderr.contains("sat=1"), "{stderr}"); + fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn cuber_cdcl_does_not_solve_sat_before_a_cutoff_cube_reaches_conquer() { + let dir = temp_dir(); + fs::create_dir_all(&dir).unwrap(); + let cnf = dir.join("sat.cnf"); + // PHP(3,4) is SAT without a root unit. The cuber-side CaDiCaL may only + // propagate selected branches; a cutoff cube must reach the conquer solver. + fs::write( + &cnf, + "p cnf 12 33\n\ + 1 2 3 4 0\n5 6 7 8 0\n9 10 11 12 0\n\ + -1 -2 0\n-1 -3 0\n-1 -4 0\n-2 -3 0\n-2 -4 0\n-3 -4 0\n\ + -5 -6 0\n-5 -7 0\n-5 -8 0\n-6 -7 0\n-6 -8 0\n-7 -8 0\n\ + -9 -10 0\n-9 -11 0\n-9 -12 0\n-10 -11 0\n-10 -12 0\n-11 -12 0\n\ + -1 -5 0\n-1 -9 0\n-5 -9 0\n-2 -6 0\n-2 -10 0\n-6 -10 0\n\ + -3 -7 0\n-3 -11 0\n-7 -11 0\n-4 -8 0\n-4 -12 0\n-8 -12 0\n", + ) + .unwrap(); + let kissat = dir.join("kissat-sat"); + fs::write( + &kissat, + "#!/bin/sh\n\ + [ \"$#\" -eq 1 ] && [ \"$1\" = --relaxed ] || exit 3\n\ + cat >/dev/null\n\ + echo 's SATISFIABLE'\n\ + exit 10\n", + ) + .unwrap(); + let mut permissions = fs::metadata(&kissat).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&kissat, permissions).unwrap(); + + let output = Command::new(env!("CARGO_BIN_EXE_cnc_cuber")) + .arg(&cnf) + .args(["-n", "1", "--solve-cnf"]) + .arg(&cnf) + .args([ + "--kissat", + kissat.to_str().unwrap(), + "--workers", + "1", + "--branch-solver", + "tail-greedy", + "--measure", + "vars", + "--propagation", + "hybrid", + ]) + .output() + .expect("run branch-learning CnC solver"); + + assert_eq!(output.status.code(), Some(10), "{output:?}"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("s SATISFIABLE"), "{stdout}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("status=OK cubes=1") + || (stderr.contains("status=SAT_EARLY cubes_submitted=") + && !stderr.contains("status=SAT_EARLY cubes_submitted=0")), + "{stderr}" + ); + assert!(stderr.contains("full_search_calls=0"), "{stderr}"); + assert!( + stderr.contains("streaming submitted=1 sat=1 unsat=0 errors=0"), + "{stderr}" + ); + fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn externally_reported_sat_model_stops_workers_and_preserves_the_witness() { + let dir = temp_dir(); + fs::create_dir_all(&dir).unwrap(); + let cnf = dir.join("input.cnf"); + fs::write(&cnf, "p cnf 2 1\n1 2 0\n").unwrap(); + let conquer = StreamingConquer::start(&cnf, &dir.join("must-not-run"), 2).unwrap(); + let witness = "s SATISFIABLE\nv 1 -2 0\n".to_string(); + + conquer.mark_sat_with_witness(witness.clone()); + let summary = conquer.finish(false).unwrap(); + + assert_eq!(summary.result, ConquerResult::Sat); + assert_eq!(summary.sat, 1); + assert_eq!(summary.submitted, 0); + assert_eq!(summary.witness.as_deref(), Some(witness.as_str())); + fs::remove_dir_all(dir).unwrap(); +} + #[test] fn streaming_mode_kills_an_inflight_solver_after_sat() { let dir = temp_dir(); diff --git a/tests/test_cnc_trace_mechanism.py b/tests/test_cnc_trace_mechanism.py index 7e4ab0d..7599fe9 100644 --- a/tests/test_cnc_trace_mechanism.py +++ b/tests/test_cnc_trace_mechanism.py @@ -43,14 +43,17 @@ def rule_record( vector = [] gamma = 1.0 return { - "schema_version": 2, "search_semantics": "sat-decision", + "propagation": "ct", + "cdcl_mode": "off", "node_id": node_id, "parent_id": parent_id, "child_index": child_index, "depth": depth, "kind": "branch", + "optimized_rule_clauses": [{"mask": 0b0011, "value": 0}] * branches, "rule_clauses": [{"mask": 0b0011, "value": 0}] * branches, + "rule_partition_sources": list(range(branches)), "rule_diagnostics": { "rule_semantics": semantics, "region_tensors": 3, @@ -298,18 +301,32 @@ def test_rejects_semantic_contract_corruption(self): with self.assertRaisesRegex(TraceError, "sat-decision"): summarize([record]) + def test_accepts_only_branch_learning_cdcl_for_hybrid_provenance(self): + record = rule_record(0, replay_value=replay()) + record["propagation"] = "hybrid" + record["cdcl_mode"] = "branch-learning" + self.assertEqual(summarize([record])["rule_nodes"], 1) + + record["cdcl_mode"] = "off" + with self.assertRaisesRegex(TraceError, "invalid CDCL search provenance"): + summarize([record]) + def test_links_cutoff_paths_without_treating_cubes_as_instances(self): root = rule_record(0, replay_value=replay()) leaves = [ { - "schema_version": 2, "search_semantics": "sat-decision", + "propagation": "ct", + "cdcl_mode": "off", "node_id": index + 1, "parent_id": 0, "child_index": index, "depth": 1, "kind": "cutoff", "literals": [-1, index + 2], + "optimized_rule_clauses": [], + "rule_clauses": [], + "rule_partition_sources": [], "rule_diagnostics": None, } for index in range(2) diff --git a/vendor/rustsat-cadical/.cargo_vcs_info.json b/vendor/rustsat-cadical/.cargo_vcs_info.json new file mode 100644 index 0000000..9de08ae --- /dev/null +++ b/vendor/rustsat-cadical/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "457d6d7bf27998947edc45fa2200d6a5fef6c389" + }, + "path_in_vcs": "cadical" +} \ No newline at end of file diff --git a/vendor/rustsat-cadical/CHANGELOG.md b/vendor/rustsat-cadical/CHANGELOG.md new file mode 100644 index 0000000..a33607d --- /dev/null +++ b/vendor/rustsat-cadical/CHANGELOG.md @@ -0,0 +1,287 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## 0.7.5 - 2026-01-30 + +### Features + +- Version 2.2.0 ([1e26774](1e26774e9130132bc8c2c5a912b2752f6ab618df)) +- Version 2.2.1 ([53c85cc](53c85ccd7bcf71f3b0cf416399fdf5721fe5a3e2)) +- Environment variables to control cxx-compiler features ([807cfaa](807cfaa61a90a479602def5fbf7a9951e5b1563f)) + +### Bug Fixes + +- Properly mark `rerun-if` in build scripts ([8352e99](8352e99504c0784bbba696d86d780258ec267ba0)) +- Proper config for building docs ([2da9620](2da9620a352e8bcabea169bb912ecc30b13d7a8a)) +- Don't silently fail on environment variable error ([d2a0c02](d2a0c02626f7ae140cd4f08e3583a5efdb974773)) + +### Documentation + +- Document windows cross compilation ([7c730a5](7c730a5a9f69fcfc363025eb897d46d0f18de4cd)) + +### Refactor + +- Version config switches ([7c71967](7c719678595eee989541e4fba0c5824cdafeee42)) + +### Miscellaneous Tasks + +- Update to rel-2.2.0 ([178379c](178379cbfa94f512a8204398d6213a1822a51de8)) +- Update subtree to rel-2.2.1 ([fd30336](fd303365f54ab3c0d66930bba0ec9fa0de7db125)) + + + +## 0.7.4 - 2025-10-18 + +### Bug Fixes + +- Abstract timer and use `std::time::Instant` in `wasm` ([aadef69](aadef69472923d57e99a541c64376dbccaf3a855)), fixes #455 +- Auto cfg in documentation ([#497](https://github.com/chrjabs/rustsat/pull/497)) ([c6623bf](c6623bf230c4e21218dd374ada9d8dada45cf0e2)) + +### Miscellaneous Tasks + +- Clippy ([844a209](844a2097c9eea05375d32f0c178f83cf3ac8d633)) +- Include keep-sorted formatter ([46e505c](46e505cca3e50b7743c47288b2fb2610da3f1952)) + + + +## 0.7.3 - 2025-08-07 + +### Bug Fixes + +- Handle null pointers in empty ranges from solvers ([#418](https://github.com/chrjabs/rustsat/pull/418)) ([1a3f8f4](1a3f8f4ba8b2ae125b018a051906b018ab370697)) +- Clause conversion in learner callback ([#425](https://github.com/chrjabs/rustsat/pull/425)) ([c09ad05](c09ad058870abcf831dd0025ffc2ce473867308f)) + + +## [0.7.2] - 2025-05-30 + +### Bug Fixes + +- Re-enable runtime feature flag for bindgen (#373) + +### Testing + +- Increase test coverage for solvers (#382) + + +## [0.7.1] - 2025-05-01 + +### Bug Fixes + +- Include `ctracer.cpp` in C++ build (#356) + +### Documentation + +- Update badges and docs link (#366) + +### Features + +- Make `git2` optional + +### Miscellaneous Tasks + +- Migrate to mainly self-hosted runners +- Update rust version + + +## [0.7.0] - 2025-04-03 + +All `rustsat-` crates now follow the version number of the main crate. + +### Documentation + +- Update shield style + +### Features + +- CaDiCaL API tracing +- Write proofs to file +- Add `Debug` implementations for everything +- Implementable proof tracers + +### Miscellaneous Tasks + +- Documentation generation with all features +- Bump MSRV to `1.75.0` +- Avoid `expect` attribute because of MSRV + + +## [0.4.6] - 2025-03-27 + +### Miscellaneous Tasks + +- Updated the following local packages: rustsat + + +## [0.4.5] - 2025-03-25 + +### Features + +- Include source for newest version +- Properly detect c++ features + +### Bug Fixes + +- Index out of bounds error in old propagate +- Fix all warnings + +### Miscellaneous Tasks + +- Prepare for subtree inclusion +- Include source as subtree +- Remove deprecated CI badges + + +## [0.4.4] - 2025-02-18 + +### Features + +- Version 2.1.3 with native propagate + + +## [0.4.3] - 2024-12-20 + +### Features + +- Version 2.1.1 + +### Miscellaneous Tasks + +- Exclude unnecessary files from release + + +## [0.4.2] - 2024-12-13 + +### Documentation + +- Spellchecking + + +## [0.4.1] - 2024-10-16 + +### Documentation + +- Fix docsrs build + + +## [0.4.0] - 2024-10-16 + +### Bug Fixes + +- Ensure solvers are always linked statically +- Don't unnecessarily rebuild cadical +- Accidental double init +- Use `cargo:` syntax in build script for backwards compatibility + +### Documentation + +- Describe CPP source customization + +### Features + +- CaDiCal Versions 2.0.0 and 2.1.0 +- `Propagate` trait +- Allow applying custom patches +- Allow specifying custom cpp src directory + +### Miscellaneous Tasks + +- Fix stray feature reference +- Pedantic clippy +- [**breaking**] Breaking clippy suggestions + +### Refactor + +- [**breaking**] Make reading functions take reader by reference +- Use bindgen to generate solver bindings +- Keep patch code in separate files + +### Testing + +- Add tests for `FlipLit` trait + + +## [0.3.1] - 2024-06-12 + +### Miscellaneous Tasks + +- Updated the following local packages: rustsat + + +## [0.3.0] - 2024-04-30 + +The corresponding RustSAT release contains breaking changes. For detailed +instructions on how to handle migration, please refer to the [migration +guide](https://github.com/chrjabs/rustsat/blob/main/docs/0-5-0-migration-guide.md). + +### Documentation + +- Add missing documentation + +### Features + +- Cadical version 1.9.5 +- Migrate error handling to `anyhow` create +- Exclude `ipasir.cpp` to avoid conflicts with other linked ipasir libs +- Add `add_clause_ref` method to `Solve` trait +- `Extend<&Clause>` for solvers +- Catch memory out in solvers +- Catch memory outs in clause collector + +### Miscellaneous Tasks + +- Cleanup feature-dependent compilation + +### Refactor + +- Clean up control flow in solver methods +- Factor out solver integration tests +- Factor out solver unit tests +- Solver build system + +### Testing + +- Minisat segfault tests + +### Example + +- `cadical-cli` tool + + +## [0.2.4] - 2024-02-22 + +### Miscellaneous Tasks + +- Updated the following local packages: rustsat + + + +## [0.2.3] - 2024-01-11 + +### Documentation + +- Fix [docs.rs](https://docs.rs/rustsat-cadical) build + +## [0.2.2] - 2024-01-11 + +### Bug Fixes + +- Specify c++ std version in cadical build + +### Documentation + +- Mention broken windows build +- Add shields to READMEs + +### Features + +- Cadical versions 1.9.[3-4] + + +## [0.2.1] - 2023-12-18 + +### Features + +- Cadical v1.9.0 - v1.9.2 + + diff --git a/vendor/rustsat-cadical/Cargo.toml b/vendor/rustsat-cadical/Cargo.toml new file mode 100644 index 0000000..1e90439 --- /dev/null +++ b/vendor/rustsat-cadical/Cargo.toml @@ -0,0 +1,154 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.77.0" +name = "rustsat-cadical" +version = "0.7.5" +authors = ["Christoph Jabs "] +build = "build.rs" +include = [ + "build.rs", + "CHANGELOG.md", + "README.md", + "/src/", + "/patches/", + "/examples/", + "/cpp-extension/", + "/cppsrc/src/", + "/cppsrc/README.md", + "/cppsrc/LICENSE", + "/cppsrc/VERSION", +] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Interface to the SAT solver CaDiCaL for the RustSAT library." +readme = "README.md" +keywords = [ + "sat-solver", + "rustsat", +] +license = "MIT" +repository = "https://github.com/chrjabs/rustsat" + +[package.metadata.docs.rs] +features = ["_docs"] +rustdoc-args = [ + "--cfg", + "docsrs", +] +cargo-args = [ + "-Zunstable-options", + "-Zrustdoc-scrape-examples", +] + +[features] +_docs = [ + "pigeons", + "tracing", +] +_test = [ + "tracing", + "debug", +] +debug = [] +default = ["quiet"] +git = ["dep:git2"] +logging = [] +pigeons = [ + "dep:pigeons", + "rustsat/proof-logging", +] +quiet = [] +tracing = [] +v1-5-0 = ["git"] +v1-5-1 = ["git"] +v1-5-2 = ["git"] +v1-5-3 = ["git"] +v1-5-4 = ["git"] +v1-5-5 = ["git"] +v1-5-6 = ["git"] +v1-6-0 = ["git"] +v1-7-0 = ["git"] +v1-7-1 = ["git"] +v1-7-2 = ["git"] +v1-7-3 = ["git"] +v1-7-4 = ["git"] +v1-7-5 = ["git"] +v1-8-0 = ["git"] +v1-9-0 = ["git"] +v1-9-1 = ["git"] +v1-9-2 = ["git"] +v1-9-3 = ["git"] +v1-9-4 = ["git"] +v1-9-5 = ["git"] +v2-0-0 = ["git"] +v2-1-0 = ["git"] +v2-1-1 = ["git"] +v2-1-2 = ["git"] +v2-1-3 = ["git"] +v2-2-0 = ["git"] +v2-2-1 = [] + +[lib] +name = "rustsat_cadical" +path = "src/lib.rs" + +[[example]] +name = "cadical-cli" +path = "examples/cadical-cli.rs" + +[dependencies.anyhow] +version = "1.0.100" + +[dependencies.pigeons] +version = "0.2.3" +optional = true + +[dependencies.rustsat] +version = "0.7.5" +default-features = false + +[dependencies.thiserror] +version = "2.0.18" + +[dev-dependencies.clap] +version = "4.5.54" +features = [ + "derive", + "cargo", +] + +[dev-dependencies.signal-hook] +version = "0.4.3" + +[build-dependencies.bindgen] +version = "0.72.1" +features = ["runtime"] +default-features = false + +[build-dependencies.cc] +version = "1.2.54" +features = ["parallel"] + +[build-dependencies.chrono] +version = "0.4.43" + +[build-dependencies.git2] +version = "0.20.3" +optional = true + +[build-dependencies.glob] +version = "0.3.3" diff --git a/vendor/rustsat-cadical/Cargo.toml.orig b/vendor/rustsat-cadical/Cargo.toml.orig new file mode 100644 index 0000000..78ab90b --- /dev/null +++ b/vendor/rustsat-cadical/Cargo.toml.orig @@ -0,0 +1,98 @@ +[package] +name = "rustsat-cadical" +version.workspace = true +edition.workspace = true +authors = ["Christoph Jabs "] +license.workspace = true +description = "Interface to the SAT solver CaDiCaL for the RustSAT library." +keywords = ["sat-solver", "rustsat"] +repository = "https://github.com/chrjabs/rustsat" +readme = "README.md" +include = [ + "build.rs", + "CHANGELOG.md", + "README.md", + "/src/", + "/patches/", + "/examples/", + "/cpp-extension/", + "/cppsrc/src/", + "/cppsrc/README.md", + "/cppsrc/LICENSE", + "/cppsrc/VERSION", +] +rust-version = "1.77.0" # When changing, update crate documentation, build script, and tools MSRV + +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["quiet"] +debug = [] +quiet = [] +logging = [] +tracing = [] +pigeons = ["dep:pigeons", "rustsat/proof-logging"] +git = ["dep:git2"] +_test = ["tracing", "debug"] +_docs = ["pigeons", "tracing"] +# keep-sorted start +v1-5-0 = ["git"] +v1-5-1 = ["git"] +v1-5-2 = ["git"] +v1-5-3 = ["git"] +v1-5-4 = ["git"] +v1-5-5 = ["git"] +v1-5-6 = ["git"] +v1-6-0 = ["git"] +v1-7-0 = ["git"] +v1-7-1 = ["git"] +v1-7-2 = ["git"] +v1-7-3 = ["git"] +v1-7-4 = ["git"] +v1-7-5 = ["git"] +v1-8-0 = ["git"] +v1-9-0 = ["git"] +v1-9-1 = ["git"] +v1-9-2 = ["git"] +v1-9-3 = ["git"] +v1-9-4 = ["git"] +v1-9-5 = ["git"] +v2-0-0 = ["git"] +v2-1-0 = ["git"] +v2-1-1 = ["git"] +v2-1-2 = ["git"] +v2-1-3 = ["git"] +v2-2-0 = ["git"] +v2-2-1 = [] +# keep-sorted end + +[dependencies] +# keep-sorted start +anyhow.workspace = true +pigeons = { workspace = true, optional = true } +rustsat.workspace = true +thiserror.workspace = true +# keep-sorted end + +[build-dependencies] +# keep-sorted start +bindgen.workspace = true +cc.workspace = true +chrono.workspace = true +git2 = { workspace = true, optional = true } +glob.workspace = true +# keep-sorted end + +[dev-dependencies] +# keep-sorted start +clap.workspace = true +rustsat-solvertests.workspace = true +signal-hook.workspace = true +# keep-sorted end + +[package.metadata.docs.rs] +features = ["_docs"] +rustdoc-args = ["--cfg", "docsrs"] +cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] diff --git a/vendor/rustsat-cadical/README.md b/vendor/rustsat-cadical/README.md new file mode 100644 index 0000000..dcaea67 --- /dev/null +++ b/vendor/rustsat-cadical/README.md @@ -0,0 +1,108 @@ +[![crates.io](https://img.shields.io/crates/v/rustsat-cadical?style=for-the-badge&logo=rust)](https://crates.io/crates/rustsat-cadical) +[![docs.rs](https://img.shields.io/docsrs/rustsat-cadical?style=for-the-badge&logo=docsdotrs)](https://docs.rs/rustsat-cadical) +[![License](https://img.shields.io/crates/l/rustsat-cadical?style=for-the-badge)](../LICENSE) + + + +# rustsat-cadical - Interface to the CaDiCaL SAT Solver for RustSAT + +Armin Biere's SAT solver [CaDiCaL](https://github.com/arminbiere/cadical) to be used with the [RustSAT](https://github.com/chrjabs/rustsat) library. + +**Note**: at the moment this crate is known to not work on Windows since CaDiCaL is non-trivial to get to work on Windows. + +## Features + +- `debug`: if this feature is enabled, the Cpp library will be built with debug and check + functionality if the Rust project is built in debug mode. API tracing via the + `CADICAL_API_TRACE` environment variable is also enabled in debug mode. +- `safe`: disable writing through `popen` for more safe usage of the library in applications +- `quiet`: exclude message and profiling code (logging too) +- `logging`: include logging code (but disabled by default) +- `tracing`: always include CaDiCaL API tracing via the `CADICAL_API_TRACE` environment + variable and the [`CaDiCaL::trace_api_calls`] method + +## CaDiCaL Versions + +CaDiCaL versions can be selected via cargo crate features. +All CaDiCaL versions from +[Version 1.5.0](https://github.com/arminbiere/cadical/releases/tag/rel-1.5.0) +up to +[Version 2.2.1](https://github.com/arminbiere/cadical/releases/tag/rel-2.2.1) +are available. For the full list of versions and the changelog see +[the CaDiCaL releases](https://github.com/arminbiere/cadical/releases). + +Without any features selected, the newest version will be used. +If conflicting CaDiCaL versions are requested, the newest requested version will be selected. + +If the determined version is _not_ the newest available, and no custom source directory is +specified (see customization below), the CaDiCaL source code is downloaded at compile time, +which requires network access. + +## Customization + +In order to build a custom version of CaDiCaL, this crate supports two environment variables to +customize the Cpp source code that CaDiCaL is built from. + +- `CADICAL_PATCHES` allows to specify a list of colon-separated paths to patch files that will + be applied to the CaDiCaL source repository before building it. These patches are applied + in order of appearance _after_ the patches of this crate have been applied. +- `CADICAL_SRC_DIR` allows for overriding where the Cpp library is built from. By default this + crate fetches the appropriate code from [the GitHub + repository](https://github.com/arminbiere/cadical). If this variable is set, the directory specified + there is used instead. Note that when using this variable, the crate will not apply any + patches, the user is responsible for applying the appropriate and necessary patches from the + [`patches/`](https://github.com/chrjabs/rustsat/tree/main/cadical/patches) directory. + +## Cpp Compiler Features + +By default, the build script of this crate uses the same compiler/platform tests as CaDiCaL's +`configure` script to detect whether certain Cpp compiler features are available. +The following environment variables allow for manually modifying the logic for detecting +compiler features. + +- `CADICAL_RUN_CPP_TESTS`: By default we only consider a compiler/platform feature available if + the test compiles _and runs_. + With this environment variable set to `0` or `false` a feature is considered available if the + test compiles correctly, without trying to execute it. + This is useful for cross-compilation settings where executing the compiled binary is not + possible. +- `CADICAL_FLEXIBLE_ARRAY_MEMBERS` enables (`1` or `true`) or disables (`0` or `false`) or + automatically checks (`auto`) the availability of the flexible array members compiler + feature. + Disabling the feature is equivalent to `--no-flexible` in CaDiCaL's `configure` script. +- `CADICAL_CLOSEFROM` marks `closefrom` as available (`1` or `true`) or unavailable (`0` or + `false`) or automatically checks (`auto`) the availability. + Disabling the feature is equivalent to `--no-closefrom` in CaDiCaL's `configure` script. +- `CADICAL_UNLOCKED_IO` enables (`1` or `true`) or disables (`0` or `false`) or automatically + checks (`auto`) the availability of the unlocked IO platform feature. + Disabling the feature is equivalent to `--no-unlocked` in CaDiCaL's `configure` script. + +By default, the availability of all compiler/platform features is automatically checked. + +## Minimum Supported Rust Version (MSRV) + +Currently, the MSRV is 1.77.0, the plan is to always support an MSRV that is at least a year +old. + +Bumps in the MSRV will _not_ be considered breaking changes. If you need a specific MSRV, make +sure to pin a precise version of RustSAT. + +Note that the specified minimum-supported Rust version only applies if the _newest_ version of +CaDiCaL is build. +Older versions are pulled down via the [`git2`](https://crates.io/crates/git2) crate, which has +transitive dependencies that have a higher MSRV. + +## Compiling on Windows / Cross Compilation + +Compiling this crate under Windows can be a bit challenging, but it can be done. +The challenges come from CaDiCaL relying on unix-specific features. +For starters, make sure to use the `x86_64-pc-windows-gnu` target triple. + +Cross compilation for Windows can be achieved with the help of [`cargo +zigbuild`](https://github.com/rust-cross/cargo-zigbuild). +During cross compilation, make sure to set `CADICAL_RUN_CPP_TESTS=0`, since compiler tests +(compiled for Windows) can't be run on the non-Windows host system. +For a working Windows cross compilation setup example, see [this CI +configuration](https://github.com/marceline-cramer/saturn-v/blob/f8bcdc24857bce232981518bb4bec2c8cfcc6acf/.github/workflows/release.yml). + + diff --git a/vendor/rustsat-cadical/VENDORED.md b/vendor/rustsat-cadical/VENDORED.md new file mode 100644 index 0000000..50536e5 --- /dev/null +++ b/vendor/rustsat-cadical/VENDORED.md @@ -0,0 +1,17 @@ +# Vendored rustsat-cadical + +This is the crates.io source for `rustsat-cadical` 0.7.5 (upstream RustSAT +commit `457d6d7bf27998947edc45fa2200d6a5fef6c389`) with its bundled CaDiCaL +2.2.1 source. + +Local changes: + +- remove an accidental `dbg!(res)` call from the CaDiCaL 2.1+ `Propagate` + implementation, which otherwise writes one line to stderr per query; +- expose a narrow `maintain_learned_clauses` hook that runs CaDiCaL's own + scheduled `reducing()`/`reduce()` policy after assumption conflicts. The + assumption-propagation entry point performs conflict analysis outside the + normal search loop, so without this hook its persistent learned database is + never reduced. + +No branching, restart, or clause-quality policy is replaced locally. diff --git a/vendor/rustsat-cadical/build.rs b/vendor/rustsat-cadical/build.rs new file mode 100644 index 0000000..f19261a --- /dev/null +++ b/vendor/rustsat-cadical/build.rs @@ -0,0 +1,715 @@ +#![warn(clippy::pedantic)] + +use glob::glob; +use std::{ + env, + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, + process::Command, + str, +}; + +macro_rules! check_env_var { + ($var:expr) => { + match env::var($var) { + Err(env::VarError::NotPresent) => None, + Ok(val) => Some(val), + Err(err) => panic!("`{}` variable error: {err}", $var), + } + }; +} + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +enum Version { + // Note: derived order makes top < bottom + V150, + V151, + V152, + V153, + V154, + V155, + V156, + V160, + V170, + V171, + V172, + V173, + V174, + V175, + V180, + V190, + V191, + V192, + V193, + V194, + V195, + V200, + V210, + V211, + V212, + V213, + V220, + #[default] + V221, + // Don't forget to update the crate documentation when adding a newer version +} + +/// Checks if the version was set manually via a feature +macro_rules! version_set_manually { + () => { + cfg!(any( + feature = "v2-2-1", + feature = "v2-2-0", + feature = "v2-1-3", + feature = "v2-1-2", + feature = "v2-1-1", + feature = "v2-1-0", + feature = "v2-0-0", + feature = "v1-9-5", + feature = "v1-9-4", + feature = "v1-9-3", + feature = "v1-9-2", + feature = "v1-9-1", + feature = "v1-9-0", + feature = "v1-8-0", + feature = "v1-7-5", + feature = "v1-7-4", + feature = "v1-7-3", + feature = "v1-7-2", + feature = "v1-7-1", + feature = "v1-7-0", + feature = "v1-6-0", + feature = "v1-5-6", + feature = "v1-5-5", + feature = "v1-5-4", + feature = "v1-5-3", + feature = "v1-5-2", + feature = "v1-5-1", + feature = "v1-5-0", + )) + }; +} + +impl Version { + fn determine() -> Self { + if cfg!(feature = "v2-2-1") { + Version::V221 + } else if cfg!(feature = "v2-2-0") { + Version::V220 + } else if cfg!(feature = "v2-1-3") { + Version::V213 + } else if cfg!(feature = "v2-1-2") { + Version::V212 + } else if cfg!(feature = "v2-1-1") { + Version::V211 + } else if cfg!(feature = "v2-1-0") { + Version::V210 + } else if cfg!(feature = "v2-0-0") { + Version::V200 + } else if cfg!(feature = "v1-9-5") { + Version::V195 + } else if cfg!(feature = "v1-9-4") { + Version::V194 + } else if cfg!(feature = "v1-9-3") { + Version::V193 + } else if cfg!(feature = "v1-9-2") { + Version::V192 + } else if cfg!(feature = "v1-9-1") { + Version::V191 + } else if cfg!(feature = "v1-9-0") { + Version::V190 + } else if cfg!(feature = "v1-8-0") { + Version::V180 + } else if cfg!(feature = "v1-7-5") { + Version::V175 + } else if cfg!(feature = "v1-7-4") { + Version::V174 + } else if cfg!(feature = "v1-7-3") { + Version::V173 + } else if cfg!(feature = "v1-7-2") { + Version::V172 + } else if cfg!(feature = "v1-7-1") { + Version::V171 + } else if cfg!(feature = "v1-7-0") { + Version::V170 + } else if cfg!(feature = "v1-6-0") { + Version::V160 + } else if cfg!(feature = "v1-5-6") { + Version::V156 + } else if cfg!(feature = "v1-5-5") { + Version::V155 + } else if cfg!(feature = "v1-5-4") { + Version::V154 + } else if cfg!(feature = "v1-5-3") { + Version::V153 + } else if cfg!(feature = "v1-5-2") { + Version::V152 + } else if cfg!(feature = "v1-5-1") { + Version::V151 + } else if cfg!(feature = "v1-5-0") { + Version::V150 + } else { + // default to newest version + Version::default() + } + } + + fn reference(self) -> &'static str { + match self { + Version::V150 => "refs/tags/rel-1.5.0", + Version::V151 => "refs/tags/rel-1.5.1", + Version::V152 => "refs/tags/rel-1.5.2", + Version::V153 => "refs/tags/rel-1.5.3", + Version::V154 => "refs/tags/rel-1.5.4", + Version::V155 => "refs/tags/rel-1.5.5", + Version::V156 => "refs/tags/rel-1.5.6", + Version::V160 => "refs/tags/rel-1.6.0", + Version::V170 => "refs/tags/rel-1.7.0", + Version::V171 => "refs/tags/rel-1.7.1", + Version::V172 => "refs/tags/rel-1.7.2", + Version::V173 => "refs/tags/rel-1.7.3", + Version::V174 => "refs/tags/rel-1.7.4", + Version::V175 => "refs/tags/rel-1.7.5", + Version::V180 => "refs/tags/rel-1.8.0", + Version::V190 => "refs/tags/rel-1.9.0", + Version::V191 => "refs/tags/rel-1.9.1", + Version::V192 => "refs/tags/rel-1.9.2", + Version::V193 => "refs/tags/rel-1.9.3", + Version::V194 => "refs/tags/rel-1.9.4", + Version::V195 => "refs/tags/rel-1.9.5", + Version::V200 => "refs/tags/rel-2.0.0", + Version::V210 => "refs/tags/rel-2.1.0", + Version::V211 => "refs/tags/rel-2.1.1", + Version::V212 => "refs/tags/rel-2.1.2", + Version::V213 => "refs/tags/rel-2.1.3", + Version::V220 => "refs/tags/rel-2.2.0", + Version::V221 => "refs/tags/rel-2.2.1", + } + } + + #[cfg(feature = "git")] + fn patch(self) -> &'static str { + #![allow(clippy::enum_glob_use)] + use Version::*; + match self { + V150 | V151 | V152 | V153 => "v150.patch", + V154 | V155 => "v154.patch", + V156 => "v156.patch", + V160 => "v160.patch", + V170 => "v170.patch", + V171 | V172 | V173 | V174 | V175 => "v171.patch", + V180 => "v180.patch", + V190 | V191 => "v190.patch", + V192 | V193 | V194 | V195 => "v192.patch", + V200 => "v200.patch", + V210 => "v210.patch", + V211 | V212 => "v211.patch", + V213 => "v213.patch", + V220 => "v220.patch", + V221 => "v221.patch", + } + } + + fn has_proof_tracer(self) -> bool { + self >= Version::V200 + } + + fn set_defines(self, build: &mut cc::Build) { + let run_cpp_tests = check_env_var!("CADICAL_RUN_CPP_TESTS").map_or(true, |val| { + let val_lower = val.to_lowercase(); + match val_lower.trim() { + "1" | "true" => true, + "0" | "false" => false, + _ => panic!("`CADICAL_RUN_CPP_TESTS` variable invalid value: {val}"), + } + }); + if !has_cpp_feature(CppFeature::FlexibleArrayMembers, run_cpp_tests) { + build.define("NFLEXIBLE", None); + } + if !has_cpp_feature(CppFeature::UnlockedIo, run_cpp_tests) { + build.define("NUNLOCKED", None); + } + if self >= Version::V211 && !has_cpp_feature(CppFeature::Closefrom, run_cpp_tests) { + build.define("NCLOSEFROM", None); + } + if self >= Version::V154 { + build.define("V154", None); + } + if self >= Version::V160 { + build.define("V160", None); + } + if self >= Version::V190 { + build.define("V190", None); + } + if self >= Version::V194 { + build.define("V194", None); + } + if self >= Version::V200 { + build.define("V200", None); + } + if self >= Version::V213 { + build.define("V213", None); + } + if self >= Version::V220 { + build.define("V220", None); + } + } + + fn set_bindings_defines(self, mut bindings: bindgen::Builder) -> bindgen::Builder { + if self >= Version::V154 { + bindings = bindings.clang_arg("-DV154"); + } + if self >= Version::V213 { + bindings = bindings.clang_arg("-DV213"); + } + if self >= Version::V220 { + bindings = bindings.clang_arg("-DV220"); + } + bindings + } + + /// Sets custom `rustc` `--cfg` arguments for features only present in some version + fn set_cfgs(self) { + println!("cargo:rustc-check-cfg=cfg(cadical_version, values(\"v1.5.4\", \"v1.7.0\", \"v1.9.0\", \"v2.0.0\", \"v2.1.3\", \"v2.2.0\"))"); + if self >= Version::V154 { + println!("cargo:rustc-cfg=cadical_version=\"v1.5.4\""); + } + if self >= Version::V170 { + println!("cargo:rustc-cfg=cadical_version=\"v1.7.0\""); + } + if self >= Version::V190 { + println!("cargo:rustc-cfg=cadical_version=\"v1.9.0\""); + } + if self >= Version::V200 { + println!("cargo:rustc-cfg=cadical_version=\"v2.0.0\""); + } + if self >= Version::V213 { + println!("cargo:rustc-cfg=cadical_version=\"v2.1.3\""); + } + if self >= Version::V220 { + println!("cargo:rustc-cfg=cadical_version=\"v2.2.0\""); + } + } +} + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + + let version = Version::determine(); + + #[cfg(all(feature = "quiet", feature = "logging"))] + compile_error!("cannot combine cadical features quiet and logging"); + + // Build C++ library + build( + "https://github.com/arminbiere/cadical.git", + "master", + version, + ); + + // Built solver is in out_dir + println!("cargo:rustc-link-search={out_dir}"); + println!("cargo:rustc-link-search={out_dir}/lib"); + println!("cargo:rustc-link-lib=static=cadical"); + if version >= Version::V220 { + println!("cargo:rustc-link-lib=static=kitten"); + } + + // Link c++ std lib + // Note: this should be _after_ linking the solver itself so that it is actually pulled in + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-lib=dylib=c++"); + #[cfg(not(any(target_os = "macos", target_os = "windows")))] + println!("cargo:rustc-link-lib=dylib=stdc++"); + + let cadical_dir = get_cadical_dir(version, None); + + // Mark when to rerun the build script + println!("cargo:rerun-if-changed={cadical_dir}/src/"); + println!("cargo:rerun-if-changed=cpp-extension/"); + println!("cargo:rerun-if-env-changed=CADICAL_SRC_DIR"); + println!("cargo:rerun-if-env-changed=CADICAL_PATCHES"); + println!("cargo:rerun-if-env-changed=CADICAL_RUN_CPP_TESTS"); + println!("cargo:rerun-if-env-changed=CADICAL_FLEXIBLE_ARRAY_MEMBERS"); + println!("cargo:rerun-if-env-changed=CADICAL_CLOSEFROM"); + println!("cargo:rerun-if-env-changed=CADICAL_UNLOCKED_IO"); + + generate_bindings(&cadical_dir, version, &out_dir); + + version.set_cfgs(); +} + +/// Generates Rust FFI bindings +fn generate_bindings(cadical_dir: &str, version: Version, out_dir: &str) { + let header_path = format!("{cadical_dir}/src/ccadical.h"); + + let bindings = bindgen::Builder::default() + .rust_target("1.77.0".parse().unwrap()) // Set MSRV + .clang_arg(format!("-I{cadical_dir}/src")) + .clang_arg("-Icpp-extension") + .allowlist_file(&header_path) + .allowlist_file("cpp-extension/ccadical_extension.h") + .blocklist_item("FILE") + .blocklist_item("_IO_FILE") + .blocklist_item("__sFILE") + .blocklist_item("fpos_t") + .blocklist_function("ccadical_add") + .blocklist_function("ccadical_assume") + .blocklist_function("ccadical_solve") + .blocklist_function("ccadical_constrain") + .blocklist_function("ccadical_set_option") + .blocklist_function("ccadical_limit") + .blocklist_function("ccadical_trace_proof") + .blocklist_function("ccadical_close_proof") + .blocklist_function("ccadical_conclude") + .blocklist_function("ccadical_simplify") + .blocklist_function("ccadical_declare_one_more_variable"); + let bindings = if version.has_proof_tracer() { + // in this case, `ccadical.h` is included from `ctracer.h` + bindings + .header("cpp-extension/ctracer.h") + .allowlist_file("cpp-extension/ctracer.h") + } else { + bindings.header(&header_path) + }; + #[cfg(not(feature = "tracing"))] + let bindings = bindings.blocklist_function("ccadical_trace_api_calls"); + let bindings = version.set_bindings_defines(bindings); + let bindings = if cfg!(feature = "tracing") + || cfg!(feature = "debug") && env::var("PROFILE").unwrap() == "debug" + { + bindings + } else { + bindings.clang_arg("-DNTRACING") + }; + let bindings = bindings + .generate() + .expect("Unable to generate ffi bindings"); + bindings + .write_to_file(PathBuf::from(out_dir).join("bindings.rs")) + .expect("Could not write ffi bindings"); +} + +fn get_cadical_dir(version: Version, _remote: Option<(&str, &str)>) -> String { + if let Some(src_dir) = check_env_var!("CADICAL_SRC_DIR") { + if version_set_manually!() { + println!("cargo:warning=Both version feature and CADICAL_SRC_DIR. It is your responsibility to ensure that they make sense together."); + } + return src_dir; + } + + if version == Version::default() { + // the sources for the default version are included with the crate and do not need to be + // cloned + return String::from("cppsrc"); + } + + #[cfg(feature = "git")] + { + let mut src_dir = env::var("OUT_DIR").unwrap(); + src_dir.push_str("/cadical"); + if let Some((repo, branch)) = _remote { + update_repo( + Path::new(&src_dir), + repo, + branch, + version.reference(), + Path::new("patches").join(version.patch()), + ); + } + src_dir + } + #[cfg(not(feature = "git"))] + unreachable!("non-default features enable the git feature") +} + +fn build(repo: &str, branch: &str, version: Version) { + let cadical_dir_str = get_cadical_dir(version, Some((repo, branch))); + let cadical_dir = Path::new(&cadical_dir_str); + // We specify the build manually here instead of calling make for better portability + let src_files = glob(&format!("{cadical_dir_str}/src/*.cpp")) + .unwrap() + .filter_map(|res| { + if let Ok(p) = res { + if let Some(name) = p.file_name() { + if name == "cadical.cpp" || name == "mobical.cpp" || name == "ipasir.cpp" { + return None; // Filter out application files and IPASIR interface + } + } + Some(p) + } else { + None + } + }); + // Setup build configuration + let mut cadical_build = default_build(); + if cfg!(feature = "debug") && env::var("PROFILE").unwrap() == "debug" { + cadical_build + .opt_level(0) + .define("DEBUG", None) + .warnings(true) + .debug(true); + } else { + cadical_build + .opt_level(3) + .define("NDEBUG", None) + .define("NCONTRACTS", None) // --no-contracts + .warnings(false); + #[cfg(not(feature = "tracing"))] + cadical_build.define("NTRACING", None); // --no-tracing + } + #[cfg(feature = "quiet")] + cadical_build.define("QUIET", None); // --quiet + #[cfg(feature = "logging")] + cadical_build.define("LOGGING", None); // --log + version.set_defines(&mut cadical_build); + + let out_dir = std::env::var("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + // Generate build header + let mut build_header = + File::create(out_dir.join("build.hpp")).expect("Could not create CaDiCaL header"); + let mut cadical_version = + fs::read_to_string(cadical_dir.join("VERSION")).expect("Cannot read CaDiCaL version"); + cadical_version.retain(|c| c != '\n'); + let (compiler_desc, compiler_flags) = get_compiler_description(&cadical_build.get_compiler()); + write!( + build_header, + "#define VERSION \"{}\"\n#define IDENTIFIER \"{}\"\n#define COMPILER \"{}\"\n#define FLAGS \"{}\"\n#define DATE \"{}\"", + cadical_version, version.reference(), compiler_desc, compiler_flags, chrono::Utc::now() + ).expect("Failed to write CaDiCaL build.hpp"); + // Build Kitten + if version >= Version::V220 { + let mut kitten_build = cadical_build.clone(); + kitten_build + .cpp(false) + .std("c99") + .include(cadical_dir.join("src")) + .file(format!("{cadical_dir_str}/src/kitten.c")) + .compile("kitten"); + } + // Build CaDiCaL + cadical_build + .include(out_dir) + .include(cadical_dir.join("src")) + .include("cpp-extension") + .files(src_files) + .compile("cadical"); +} + +#[cfg(feature = "git")] +fn update_repo(repo_path: &Path, url: &str, branch: &str, reference: &str, patch: PathBuf) { + let repo = if let Ok(repo) = git2::Repository::open(repo_path) { + if repo.find_reference(reference).is_err() { + // Fetch repo + let mut remote = repo.find_remote("origin").unwrap_or_else(|e| { + panic!("Expected remote \"origin\" in git repo {repo_path:?}: {e}",) + }); + remote.fetch(&[branch], None, None).unwrap_or_else(|e| { + panic!("Could not fetch \"origin/{branch}\" for git repo {repo_path:?}: {e}") + }); + drop(remote); + } + repo + } else { + if repo_path.exists() { + fs::remove_dir_all(repo_path).unwrap_or_else(|e| { + panic!( + "Could not delete directory {}: {}", + repo_path.to_str().unwrap(), + e + ) + }); + }; + git2::Repository::clone(url, repo_path) + .unwrap_or_else(|e| panic!("Could not clone repository {url}: {e}")) + }; + let target_commit = repo + .find_reference(reference) + .expect("could not find specified reference") + .peel_to_commit() + .expect("could not peel to commit"); + repo.checkout_tree( + target_commit.as_object(), + Some(git2::build::CheckoutBuilder::new().force()), + ) + .expect("could not checkout commit"); + repo.set_head_detached(target_commit.id()) + .expect("could not detach head"); + + apply_patch(&repo, patch); + + // Allow for manually applying patches + if let Some(patches) = check_env_var!("CADICAL_PATCHES") { + for patch in patches.split(':') { + apply_patch(&repo, patch); + } + } +} + +/// Applies a patch to the repository +#[cfg(feature = "git")] +fn apply_patch>(repo: &git2::Repository, patch: P) { + use std::io::Read; + + let mut f = File::open(patch).unwrap(); + let mut buffer = Vec::new(); + f.read_to_end(&mut buffer).unwrap(); + let patch = git2::Diff::from_buffer(&buffer).unwrap(); + repo.apply(&patch, git2::ApplyLocation::WorkDir, None) + .unwrap(); +} + +/// Gets a description of the C(pp) compiler used and the used flags +fn get_compiler_description(compiler: &cc::Tool) -> (String, String) { + let compiler_command = compiler.to_command(); + let mut first_line = true; + let compiler_version = match Command::new(compiler_command.get_program()) + .arg("--version") + .output() + { + Ok(output) => { + let mut version = String::from_utf8(output.stdout).unwrap(); + version.retain(|c| { + if first_line && c == '\n' { + first_line = false; + false + } else { + first_line + } + }); + version + } + Err(_) => String::from(compiler_command.get_program().to_str().unwrap()), + }; + let compiler_flags = compiler.cflags_env(); + ( + compiler_version, + String::from(compiler_flags.to_str().unwrap()), + ) +} + +/// Gets a [`cc::Build`] with the default configuration applied +/// (used in main build and when checking Cpp features) +fn default_build() -> cc::Build { + let mut build = cc::Build::new(); + build.cpp(true).std("c++11"); + build +} + +#[derive(Clone, Copy, Debug)] +enum CppFeature { + FlexibleArrayMembers, + UnlockedIo, + Closefrom, +} + +impl CppFeature { + fn env_var(self) -> &'static str { + match self { + CppFeature::FlexibleArrayMembers => "CADICAL_FLEXIBLE_ARRAY_MEMBERS", + CppFeature::UnlockedIo => "CADICAL_UNLOCKED_IO", + CppFeature::Closefrom => "CADICAL_CLOSEFROM", + } + } + + fn name_content(self) -> (&'static str, &'static str) { + match self { + CppFeature::FlexibleArrayMembers => { + ("has-flexible-array-members", FLEXIBLE_ARRAY_MEMBERS_TEST) + } + CppFeature::UnlockedIo => ("has-unlocked-io", UNLOCKED_IO_TEST), + CppFeature::Closefrom => ("has-closefrom", CLOSEFROM_TEST), + } + } +} + +const FLEXIBLE_ARRAY_MEMBERS_TEST: &str = r" +#include +struct S { + int size; + int flexible_array_member[]; +}; +int main () { + struct S * s = (struct S*) malloc (12); + s->size = 2; + s->flexible_array_member[0] = 1; + s->flexible_array_member[1] = -1; + int res = 0; + for (int i = 0; i != s->size; i++) + res += s->flexible_array_member[i]; + return res; +} +"; + +const UNLOCKED_IO_TEST: &str = r#" +#include +int main () { + FILE * file = stdout; + if (!file) return 1; + if (putc_unlocked (42, file) != 42) return 1; + if (fclose (file)) return 1; + file = fopen (path, "r"); + if (!file) return 1; + if (getc_unlocked (file) != 42) return 1; + if (fclose (file)) return 1; + return 0; +} +"#; + +const CLOSEFROM_TEST: &str = r#" +extern "C" { +#include +}; +int main () { + closefrom (0); + return 0; +} +"#; + +/// Checks whether a Cpp feature is available +/// +/// The actual checks are taken from CaDiCaL's `configure` script +fn has_cpp_feature(feature: CppFeature, run: bool) -> bool { + if let Some(val) = check_env_var!(feature.env_var()) { + let val_lower = val.to_lowercase(); + match val_lower.trim() { + "1" | "true" => return true, + "0" | "false" => return false, + "auto" => (), + _ => panic!("`{}` variable invalid value: {val}", feature.env_var()), + } + } + + let out_dir = env::var("OUT_DIR").unwrap(); + let (name, content) = feature.name_content(); + + // write test to file + let test_file = format!("{out_dir}/{name}.cpp"); + { + let mut test_file = fs::File::create(&test_file).expect("cannot open test file"); + write!(test_file, "{content}").expect("failed to write test file"); + } + + // compile and run test + let out_file = format!("{out_dir}/{name}.out"); + let mut compile = default_build().get_compiler().to_command(); + let compile = compile + .current_dir(out_dir) + .args([&test_file, "-o", &out_file]) + .output() + .expect("failed to run test compilation"); + if !compile.status.success() { + return false; + } + if run { + let output = Command::new(out_file) + .output() + .expect("failed to execute compiled test"); + output.status.success() + } else { + true + } +} diff --git a/vendor/rustsat-cadical/cpp-extension/cadical_extension.hpp b/vendor/rustsat-cadical/cpp-extension/cadical_extension.hpp new file mode 100644 index 0000000..a59787f --- /dev/null +++ b/vendor/rustsat-cadical/cpp-extension/cadical_extension.hpp @@ -0,0 +1,16 @@ +// CaDiCaL Solver API Extension (Christoph Jabs) +// To be included in the public interface of `Solver` in `cadical.hpp` + +#ifndef V220 +int64_t propagations() const; +int64_t decisions() const; +int64_t conflicts() const; +#endif +#ifdef V220 +void maintain_learned_clauses(); +#endif + +#ifndef V213 +bool prop_check(const int *assumps, size_t assumps_len, bool psaving, + void (*prop_cb)(void *, int), void *cb_data); +#endif diff --git a/vendor/rustsat-cadical/cpp-extension/ccadical_extension.cpp b/vendor/rustsat-cadical/cpp-extension/ccadical_extension.cpp new file mode 100644 index 0000000..0374454 --- /dev/null +++ b/vendor/rustsat-cadical/cpp-extension/ccadical_extension.cpp @@ -0,0 +1,176 @@ +// CaDiCaL C API Extension (Christoph Jabs) +// To be included at the bottom of `ccadical.cpp` + +extern "C" { + +int ccadical_add_mem(CCaDiCaL *wrapper, int lit) { + try { + ((Wrapper *)wrapper)->solver->add(lit); + return 0; + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} + +int ccadical_assume_mem(CCaDiCaL *wrapper, int lit) { + try { + ((Wrapper *)wrapper)->solver->assume(lit); + return 0; + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} + +int ccadical_constrain_mem(CCaDiCaL *wrapper, int lit) { + try { + ((Wrapper *)wrapper)->solver->constrain(lit); + return 0; + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} + +int ccadical_solve_mem(CCaDiCaL *wrapper) { + try { + return ((Wrapper *)wrapper)->solver->solve(); + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} + +int ccadical_configure(CCaDiCaL *ptr, const char *name) { + return ((Wrapper *)ptr)->solver->configure(name); +} + +#ifndef V220 +void ccadical_phase(CCaDiCaL *ptr, int lit) { + ((Wrapper *)ptr)->solver->phase(lit); +} + +void ccadical_unphase(CCaDiCaL *ptr, int lit) { + ((Wrapper *)ptr)->solver->unphase(lit); +} + +int ccadical_vars(CCaDiCaL *ptr) { return ((Wrapper *)ptr)->solver->vars(); } +#endif + +int ccadical_set_option_ret(CCaDiCaL *wrapper, const char *name, int val) { + return ((Wrapper *)wrapper)->solver->set(name, val); +} + +int ccadical_limit_ret(CCaDiCaL *wrapper, const char *name, int val) { + return ((Wrapper *)wrapper)->solver->limit(name, val); +} + +int64_t ccadical_redundant(CCaDiCaL *wrapper) { + return ((Wrapper *)wrapper)->solver->redundant(); +} + +int ccadical_simplify_rounds(CCaDiCaL *wrapper, int rounds) { + return ((Wrapper *)wrapper)->solver->simplify(rounds); +} + +int ccadical_resize(CCaDiCaL *wrapper, int min_max_var) { + try { +#ifdef V220 + ((Wrapper *)wrapper)->solver->resize(min_max_var); +#else + ((Wrapper *)wrapper)->solver->reserve(min_max_var); +#endif + return 0; + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} + +#ifndef V220 +int64_t ccadical_propagations(CCaDiCaL *wrapper) { + return ((Wrapper *)wrapper)->solver->propagations(); +} + +int64_t ccadical_decisions(CCaDiCaL *wrapper) { + return ((Wrapper *)wrapper)->solver->decisions(); +} + +int64_t ccadical_conflicts(CCaDiCaL *wrapper) { + return ((Wrapper *)wrapper)->solver->conflicts(); +} +#endif + +#ifdef V154 +int ccadical_flip(CCaDiCaL *wrapper, int lit) { + return ((Wrapper *)wrapper)->solver->flip(lit); +} + +int ccadical_flippable(CCaDiCaL *wrapper, int lit) { + return ((Wrapper *)wrapper)->solver->flippable(lit); +} +#endif + +#ifndef V213 +int ccadical_propcheck(CCaDiCaL *wrapper, const int *assumps, + size_t assumps_len, int psaving, + void (*prop_cb)(void *, int), void *cb_data) { + try { + if (((Wrapper *)wrapper) + ->solver->prop_check(assumps, assumps_len, psaving, prop_cb, + cb_data)) { + return 10; + } + return 20; + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} +#else +int ccadical_propagate(CCaDiCaL *wrapper) { + try { + return ((Wrapper *)wrapper)->solver->propagate(); + } catch (std::bad_alloc &) { + return OUT_OF_MEM; + } +} + +void ccadical_implied(CCaDiCaL *wrapper, void (*implied_cb)(void *, int), + void *cb_data) { + std::vector implied{}; +#ifdef V220 + ((Wrapper *)wrapper)->solver->implied(implied); +#else + ((Wrapper *)wrapper)->solver->get_entrailed_literals(implied); +#endif + for (int lit : implied) { + implied_cb(cb_data, lit); + } +} +#endif + +#ifndef NTRACING +int ccadical_trace_api_calls(CCaDiCaL *wrapper, const char *const path) { + FILE *trace_file = fopen(path, "w"); + if (!trace_file) + return 1; + ((Wrapper *)wrapper)->solver->trace_api_calls(trace_file); + return 0; +} +#endif + +int ccadical_trace_proof_path(CCaDiCaL *wrapper, const char *const path) { + return ((Wrapper *)wrapper)->solver->trace_proof(path); +} +} + +#ifdef V220 +int64_t ccadical_get_statistic_value(const CCaDiCaL *wrapper, + const char *const opt) { + return ((Wrapper *)wrapper)->solver->get_statistic_value(opt); +} + +void ccadical_maintain_learned_clauses(CCaDiCaL *wrapper) { + ((Wrapper *)wrapper)->solver->maintain_learned_clauses(); +} +#endif + +#ifdef V200 +#include "ctracer.cpp" +#endif diff --git a/vendor/rustsat-cadical/cpp-extension/ccadical_extension.h b/vendor/rustsat-cadical/cpp-extension/ccadical_extension.h new file mode 100644 index 0000000..c5a0f5e --- /dev/null +++ b/vendor/rustsat-cadical/cpp-extension/ccadical_extension.h @@ -0,0 +1,49 @@ +// CaDiCaL C API Extension (Christoph Jabs) +// To be included at the bottom of `ccadical.h` + +#include + +const int OUT_OF_MEM = 50; + +int ccadical_add_mem(CCaDiCaL *wrapper, int lit); +int ccadical_assume_mem(CCaDiCaL *wrapper, int lit); +int ccadical_constrain_mem(CCaDiCaL *wrapper, int lit); +int ccadical_solve_mem(CCaDiCaL *wrapper); +int ccadical_configure(CCaDiCaL *ptr, const char *name); +#ifndef V220 +void ccadical_phase(CCaDiCaL *ptr, int lit); +void ccadical_unphase(CCaDiCaL *ptr, int lit); +int ccadical_vars(CCaDiCaL *ptr); +#endif +int ccadical_set_option_ret(CCaDiCaL *wrapper, const char *name, int val); +int ccadical_limit_ret(CCaDiCaL *wrapper, const char *name, int val); +int64_t ccadical_redundant(CCaDiCaL *wrapper); +int ccadical_simplify_rounds(CCaDiCaL *wrapper, int rounds); +int ccadical_resize(CCaDiCaL *wrapper, int min_max_var); +#ifndef V220 +int64_t ccadical_propagations(CCaDiCaL *wrapper); +int64_t ccadical_decisions(CCaDiCaL *wrapper); +int64_t ccadical_conflicts(CCaDiCaL *wrapper); +#endif +#ifdef V154 +int ccadical_flip(CCaDiCaL *wrapper, int lit); +int ccadical_flippable(CCaDiCaL *wrapper, int lit); +#endif +#ifndef V213 +int ccadical_propcheck(CCaDiCaL *wrapper, const int *assumps, + size_t assumps_len, int psaving, + void (*prop_cb)(void *, int), void *cb_data); +#else +int ccadical_propagate(CCaDiCaL *wrapper); +void ccadical_implied(CCaDiCaL *wrapper, void (*implied_cb)(void *, int), + void *cb_data); +#endif +#ifndef NTRACING +int ccadical_trace_api_calls(CCaDiCaL *wrapper, const char *const path); +#endif +int ccadical_trace_proof_path(CCaDiCaL *wrapper, const char *const path); +#ifdef V220 +int64_t ccadical_get_statistic_value(const CCaDiCaL *wrapper, + const char *const); +void ccadical_maintain_learned_clauses(CCaDiCaL *wrapper); +#endif diff --git a/vendor/rustsat-cadical/cpp-extension/ctracer.cpp b/vendor/rustsat-cadical/cpp-extension/ctracer.cpp new file mode 100644 index 0000000..1907799 --- /dev/null +++ b/vendor/rustsat-cadical/cpp-extension/ctracer.cpp @@ -0,0 +1,155 @@ +// CaDiCaL C API Extension For Proof Tracing (Christoph Jabs) + +#include + +#include "ctracer.h" +#include "tracer.hpp" + +namespace CaDiCaL { + +#ifdef V220 +#define ID int64_t +#else +#define ID uint64_t +#endif + +class CTracer : public Tracer { + void *data; + CCaDiCaLTraceCallbacks callbacks; + +public: + CTracer(void *data, CCaDiCaLTraceCallbacks callbacks) + : data(data), callbacks(callbacks) { + assert(callbacks.add_original_clause); + assert(callbacks.add_derived_clause); + assert(callbacks.delete_clause); + assert(callbacks.weaken_minus); + assert(callbacks.strengthen); + assert(callbacks.report_status); + assert(callbacks.finalize_clause); + assert(callbacks.begin_proof); + assert(callbacks.solve_query); + assert(callbacks.add_assumption); + assert(callbacks.add_constraint); + assert(callbacks.reset_assumptions); + assert(callbacks.add_assumption_clause); + assert(callbacks.conclude_unsat); + assert(callbacks.conclude_sat); + } + ~CTracer() {}; + + void add_original_clause(ID id, bool redundant, + const std::vector &clause, + bool restored = false) override { + callbacks.add_original_clause(data, id, redundant, clause.size(), + clause.data(), restored); + } + + void add_derived_clause(ID id, bool redundant, +#ifdef V220 + int, +#endif + const std::vector &clause, + const std::vector &antecedents) override { + callbacks.add_derived_clause(data, id, redundant, clause.size(), + clause.data(), antecedents.size(), + (int64_t *)antecedents.data()); + } + + void delete_clause(ID id, bool redundant, + const std::vector &clause) override { + callbacks.delete_clause(data, id, redundant, clause.size(), clause.data()); + } + + void weaken_minus(ID id, const std::vector &clause) override { + callbacks.weaken_minus(data, id, clause.size(), clause.data()); + } + + void strengthen(ID id) override { callbacks.strengthen(data, id); } + + void report_status(int status, ID id) override { + callbacks.report_status(data, status, id); + } + + void finalize_clause(ID id, const std::vector &clause) override { + callbacks.finalize_clause(data, id, clause.size(), clause.data()); + } + + void begin_proof(ID id) override { callbacks.begin_proof(data, id); } + + void solve_query() override { callbacks.solve_query(data); } + + void add_assumption(int assumption_literal) override { + callbacks.add_assumption(data, assumption_literal); + } + + void add_constraint(const std::vector &constraint_clause) override { + callbacks.add_constraint(data, constraint_clause.size(), + constraint_clause.data()); + } + + void reset_assumptions() override { callbacks.reset_assumptions(data); } + + void add_assumption_clause(ID id, const std::vector &clause, + const std::vector &antecedents) override { + callbacks.add_assumption_clause(data, id, clause.size(), clause.data(), + antecedents.size(), + (int64_t *)antecedents.data()); + } + + void conclude_unsat(ConclusionType conclusion_type, + const std::vector &clause_ids) override { + CCaDiCaLConclusionType conclusion_out; + switch (conclusion_type) { + case ConclusionType::CONFLICT: + conclusion_out = CCaDiCaLConclusionType::CONFLICT; + break; + case ConclusionType::ASSUMPTIONS: + conclusion_out = CCaDiCaLConclusionType::ASSUMPTIONS; + break; + case ConclusionType::CONSTRAINT: + conclusion_out = CCaDiCaLConclusionType::CONSTRAINT; + break; + } + callbacks.conclude_unsat(data, conclusion_out, clause_ids.size(), + (int64_t *)clause_ids.data()); + } + + void conclude_sat(const std::vector &model) override { + callbacks.conclude_sat(data, model.size(), model.data()); + } + +#ifdef V220 + void conclude_unknown(const std::vector &model) override { + callbacks.conclude_unknown(data, model.size(), model.data()); + } + + void notify_equivalence(int first, int second) override { + callbacks.notify_equivalence(data, first, second); + } +#endif + + void *get_data() { return data; } +}; + +} // namespace CaDiCaL + +extern "C" { + +CCaDiCaLTracer *ccadical_connect_proof_tracer(CCaDiCaL *wrapper, void *data, + CCaDiCaLTraceCallbacks callbacks, + bool antecedents) { + CaDiCaL::CTracer *tracer = new CaDiCaL::CTracer(data, callbacks); + ((Wrapper *)wrapper) + ->solver->connect_proof_tracer((Tracer *)tracer, antecedents); + return (CCaDiCaLTracer *)tracer; +} + +bool ccadical_disconnect_proof_tracer(CCaDiCaL *wrapper, + CCaDiCaLTracer *tracer) { + bool ret = + ((Wrapper *)wrapper)->solver->disconnect_proof_tracer((Tracer *)tracer); + delete (CaDiCaL::CTracer *)tracer; + return ret; +} +} diff --git a/vendor/rustsat-cadical/cpp-extension/ctracer.h b/vendor/rustsat-cadical/cpp-extension/ctracer.h new file mode 100644 index 0000000..5117893 --- /dev/null +++ b/vendor/rustsat-cadical/cpp-extension/ctracer.h @@ -0,0 +1,145 @@ +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#include "ccadical.h" + +typedef struct CCaDiCaLTracer CCaDiCaLTracer; +typedef enum { + CONFLICT = 1, + ASSUMPTIONS = 2, + CONSTRAINT = 4 +} CCaDiCaLConclusionType; + +// Struct defining all necessary callbacks for a tracer +// If any of the callback is set to `NULL`, it will be ignored +// For all callbacks, the first void pointer argument is what was passed to the +// init function +// All clauses and other vectors are passed as a length and a read-only array of +// integers +typedef struct { + /*------------------------------------------------------------------------*/ + /* */ + /* Basic Events */ + /* */ + /*------------------------------------------------------------------------*/ + + // Notify the tracer that a original clause has been added. + // Includes ID and whether the clause is redundant or irredundant + // Arguments: Data, ID, redundant, length, clause, restored + void (*add_original_clause)(void *, int64_t, bool, size_t, const int *, bool); + + // Notify the observer that a new clause has been derived. + // Includes ID and whether the clause is redundant or irredundant + // If antecedents are derived they will be included here. + // Arguments: Data, ID, redundant, clause length, clause, antecedents length, + // antecedents + void (*add_derived_clause)(void *, int64_t, bool, size_t, const int *, size_t, + const int64_t *); + + // Notify the observer that a clause is deleted. + // Includes ID and redundant/irredundant + // Arguments: Data, ID, redundant, length, clause + void (*delete_clause)(void *, int64_t, bool, size_t, const int *); + + // Notify the observer to remember that the clause might be restored later + // Arguments: Data, ID, length, clause + void (*weaken_minus)(void *, int64_t, size_t, const int *); + + // Notify the observer that a clause is strengthened + // Arguments: Data, ID + void (*strengthen)(void *, int64_t); + + // Notify the observer that the solve call ends with status StatusType + // If the status is UNSAT and an empty clause has been derived, the second + // argument will contain its id. + // Note that the empty clause is already added through add_derived_clause + // and finalized with finalize_clause + // Arguments: Data, int, ID + void (*report_status)(void *, int, int64_t); + + /*------------------------------------------------------------------------*/ + /* */ + /* Specifically non-incremental */ + /* */ + /*------------------------------------------------------------------------*/ + + // Notify the observer that a clause is finalized. + // Arguments: Data, ID, length, clause + void (*finalize_clause)(void *, int64_t, size_t, const int *); + + // Notify the observer that the proof begins with a set of reserved ids + // for original clauses. Given ID is the first derived clause ID. + // Arguments: Data, ID + void (*begin_proof)(void *, int64_t); + + /*------------------------------------------------------------------------*/ + /* */ + /* Specifically incremental */ + /* */ + /*------------------------------------------------------------------------*/ + + // Notify the observer that an assumption has been added + // Arguments: Data + void (*solve_query)(void *); + + // Notify the observer that an assumption has been added + // Arguments: Data, assumption_literal + void (*add_assumption)(void *, int); + + // Notify the observer that a constraint has been added + // Arguments: Data, length, constraint_clause + void (*add_constraint)(void *, size_t, const int *); + + // Notify the observer that assumptions and constraints are reset + // Arguments: Data + void (*reset_assumptions)(void *); + + // Notify the observer that this clause could be derived, which + // is the negation of a core of failing assumptions/constraints. + // If antecedents are derived they will be included here. + // Arguments: Data, ID, clause length, clause, antecedents length, antecedents + void (*add_assumption_clause)(void *, int64_t, size_t, const int *, size_t, + const int64_t *); + + // Notify the observer that conclude unsat was requested. + // will give either the id of the empty clause, the id of a failing + // assumption clause or the ids of the failing constrain clauses + // Arguments: Data, conclusion_type, length, clause_ids + void (*conclude_unsat)(void *, CCaDiCaLConclusionType, size_t, + const int64_t *); + + // Notify the observer that conclude sat was requested. + // will give the complete model as a vector. + // Arguments: Data, model length, model + void (*conclude_sat)(void *, size_t, const int *); + + // Notify the observer that conclude unknown was requested. + // will give the current trail as a vector. + void (*conclude_unknown)(void *, size_t, const int *); + + // Notify the observer that two literals are equivalent + // + // You receive literals, not variables. You can also get notified + // multiple times. You can also get notified of BVA variables, aka + // variables you did not declare. + void (*notify_equivalence)(void *, int, int); +} CCaDiCaLTraceCallbacks; + +// Connects a proof tracer to an instance of CaDiCaL +// Arguments: CaDiCaL, data, callbacks (all non-null), antecedents +CCaDiCaLTracer *ccadical_connect_proof_tracer(CCaDiCaL *, void *, + CCaDiCaLTraceCallbacks, bool); + +// Disconnects a proof tracer from an instance of CaDiCaL +// Arguments: CaDiCaL, tracer +// Returns false if the tracer was not found to be connected +bool ccadical_disconnect_proof_tracer(CCaDiCaL *, CCaDiCaLTracer *); + +#ifdef __cplusplus +} +#endif diff --git a/vendor/rustsat-cadical/cpp-extension/solver_extension.cpp b/vendor/rustsat-cadical/cpp-extension/solver_extension.cpp new file mode 100644 index 0000000..f0bd538 --- /dev/null +++ b/vendor/rustsat-cadical/cpp-extension/solver_extension.cpp @@ -0,0 +1,164 @@ +namespace CaDiCaL { + +#ifndef V220 +int64_t Solver::propagations() const { + TRACE("propagations"); + REQUIRE_VALID_STATE(); + int64_t res = internal->stats.propagations.search; + LOG_API_CALL_RETURNS("propagations", res); + return res; +} + +int64_t Solver::decisions() const { + TRACE("decisions"); + REQUIRE_VALID_STATE(); + int64_t res = internal->stats.decisions; + LOG_API_CALL_RETURNS("decisions", res); + return res; +} + +int64_t Solver::conflicts() const { + TRACE("conflicts"); + REQUIRE_VALID_STATE(); + int64_t res = internal->stats.conflicts; + LOG_API_CALL_RETURNS("conflicts", res); + return res; +} +#endif + +#ifdef V220 +void Solver::maintain_learned_clauses() { + TRACE("maintain_learned_clauses"); + REQUIRE_VALID_STATE(); + if (internal->reducing()) + internal->reduce(); +} +#endif + +#ifndef V213 +// Propagate and check +// This is based on the implementation in PySat +// https://github.com/pysathq/pysat/blob/master/solvers/patches/cadical195.patch +bool Solver::prop_check(const int *assumps, size_t assumps_len, bool psaving, + void (*prop_cb)(void *, int), void *cb_data) { + if (internal->unsat || internal->unsat_constraint) { + return false; + } + + // saving default options +#ifdef V190 + int old_ilb = internal->opts.ilb; +#ifndef V194 + int old_reimply = internal->opts.reimply; +#endif +#endif + int old_psave = internal->opts.rephase; + int old_lucky = internal->opts.lucky; + int old_resall = internal->opts.restoreall; + + // resetting the above options +#ifdef V190 + internal->opts.ilb = 0; +#ifndef V194 + internal->opts.reimply = 0; +#endif +#endif + internal->opts.lucky = psaving; + internal->opts.rephase = psaving; + internal->opts.restoreall = 2; + + int tmp = internal->already_solved(); + if (!tmp) + tmp = internal->restore_clauses(); + if (tmp) { + // restoring default option values +#ifdef V190 + internal->opts.ilb = old_ilb; +#ifndef V194 + internal->opts.reimply = old_reimply; +#endif +#endif + internal->opts.lucky = old_lucky; + internal->opts.rephase = old_psave; + internal->opts.restoreall = old_resall; + internal->reset_solving(); + internal->report_solving(tmp); + return false; + } + internal->opts.restoreall = old_resall; + + bool unsat = false; + int level = internal->level; + bool noconfl = true; + Clause *old_conflict = internal->conflict; + + // propagate each assumption at a new decision level + for (size_t i = 0; !unsat && noconfl && i < assumps_len; ++i) { + int p = assumps[i]; + + // deciding + const signed char tmp = internal->val(p); + if (tmp < 0) // if assumption is already set to false + unsat = true; + else { +#ifdef V160 + if (tmp > 0) { + // add pseudo decision level +#ifdef V190 + internal->new_trail_level(0); +#else + internal->level++; + internal->control.push_back(Level(0, internal->trail.size())); +#endif + internal->notify_decision(); + } else + internal->search_assume_decision(p); + + noconfl = internal->propagate(); + if (noconfl) + noconfl = internal->external_propagate(); +#else + if (tmp == 0) { + internal->search_assume_decision(p); + noconfl = internal->propagate(); + } +#endif + } + } + + // copy results + if (internal->level > level) { + for (size_t i = internal->control[level + 1].trail; + i < internal->trail.size(); ++i) { + prop_cb(cb_data, internal->trail[i]); + } + // if there is a conflict, push the conflicting literal as well + if (!noconfl) { + literal_iterator conflict_ptr = internal->conflict->begin(); + int conflict_val = *conflict_ptr; + prop_cb(cb_data, conflict_val); + } + // backtrack + internal->backtrack(level); + } + +#ifdef V190 + internal->opts.ilb = old_ilb; +#ifndef V194 + internal->opts.reimply = old_reimply; +#endif +#endif + + // restore phase saving + internal->opts.rephase = old_psave; + internal->opts.lucky = old_lucky; + // reset conflict + internal->conflict = old_conflict; + internal->reset_solving(); + internal->report_solving(tmp); + + return !unsat && noconfl; +} +#endif + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/LICENSE b/vendor/rustsat-cadical/cppsrc/LICENSE new file mode 100644 index 0000000..fd52947 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/LICENSE @@ -0,0 +1,28 @@ +MIT License + +Copyright (c) 2016-2021 Armin Biere, Johannes Kepler University Linz, Austria +Copyright (c) 2020-2021 Mathias Fleury, Johannes Kepler University Linz, Austria +Copyright (c) 2020-2021 Nils Froleyks, Johannes Kepler University Linz, Austria +Copyright (c) 2022-2025 Katalin Fazekas, Vienna University of Technology, Austria +Copyright (c) 2021-2025 Armin Biere, University of Freiburg, Germany +Copyright (c) 2021-2025 Mathias Fleury, University of Freiburg, Germany +Copyright (c) 2023-2025 Florian Pollitt, University of Freiburg, Germany +Copyright (c) 2024-2024 Tobias Faller, University of Freiburg, Germany + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/rustsat-cadical/cppsrc/README.md b/vendor/rustsat-cadical/cppsrc/README.md new file mode 100644 index 0000000..6ebe4db --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/README.md @@ -0,0 +1,65 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + + +CaDiCaL Simplified Satisfiability Solver +=============================================================================== + +The goal of the development of CaDiCaL was to obtain a CDCL solver, +which is easy to understand and change, while at the same time not being +much slower than other state-of-the-art CDCL solvers. + +Originally we wanted to also radically simplify the design and internal data +structures, but that goal was only achieved partially, at least for instance +compared to Lingeling. + +However, the code is much better documented and CaDiCaL actually became in +general faster than Lingeling even though it is missing some preprocessors +(mostly parity and cardinality constraint reasoning), which would be crucial +to solve certain instances. + +Use `./configure && make` to configure and build `cadical` and the library +`libcadical.a` in the default `build` sub-directory. The header file of +the library is [`src/cadical.hpp`](src/cadical.hpp) and includes an example +for API usage. + +See [`BUILD.md`](BUILD.md) for options and more details related to the build +process and [`test/README.md`](test/README.md) for testing the library and +the solver. Since release 1.5.1 we have a [`NEWS.md`](NEWS.md) file. +You might also want to check out [`CONTRIBUTING.md`](CONTRIBUTING.md) on +if you want to contribute. + +The solver has the following usage `cadical [ dimacs [ proof ] ]`. +See `cadical -h` for more options. + +If you want to cite CaDiCaL please use as reference our CaDiCaL 2.0 tool +paper which appeared at CAV'24: + +

+CaDiCaL +2.0 +
+Armin Biere, +Tobias Faller, +Katalin Fazekas, +Mathias Fleury, +Nils Froleyks and +Florian Pollitt +
+ +Proc. Computer Aidded Verification - 26th Intl. Conf. (CAV'24) +
+Lecture Notes in Computer Science (LNCS) +
+vol. 14681, +pages 133-152, +Springer 2024 +
+[ paper +| bibtex +| official +| artifact +| github +| doi +] +

diff --git a/vendor/rustsat-cadical/cppsrc/VERSION b/vendor/rustsat-cadical/cppsrc/VERSION new file mode 100644 index 0000000..c043eea --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/VERSION @@ -0,0 +1 @@ +2.2.1 diff --git a/vendor/rustsat-cadical/cppsrc/scripts/README.md b/vendor/rustsat-cadical/cppsrc/scripts/README.md new file mode 100644 index 0000000..bc7bb5a --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/scripts/README.md @@ -0,0 +1,42 @@ +# CaDiCaL Scripts + +Scripts needed for the build process + + ./make-build-header.sh # generates 'build.hpp' + ./get-git-id.sh # get GIT id (needed by 'make-build-header.sh') + ./update-version.sh # synchronizes VERSION in '../src/version.cpp' + +and a script which builds and tests all configurations + + ./build-and-test-all-configurations.sh + CXX=clang++ ./build-and-test-all-configurations.sh + CXX=g++-4.8 ./build-and-test-all-configurations.sh + +where as the code shows the compiler (default `g++`) is specified through +the environment variable `CXX` (as for `../configure`). Then there are +scripts for producing a source release + + ./make-src-release.sh # archive as 'cadical-VERSION-GITID.tar.xz' + ./prepare-sc2021-submission.sh # star-exec format for SAT competition + +and scripts for testing and debugging + + ./generate-embedded-options-default-list.sh # in 'c --opt=val' format + ./generate-options-range-list.sh # 'cnfuzz' option file format + ./run-cadical-and-check-proof.sh # wrapper to check proofs too + ./run-simplifier-and-extend-solution.sh # to check simplifier + ./extend-solution.sh # called by previous script + +a script to check whether all options are actually used + + ./check-options-occur.sh + +a script to update the example in the `../src/cadical.hpp` header + + ./update-example-in-cadical-header-file.sh + +and finally a script to normalize white space of the source code + + ./normalize-white-space.sh + +The `color.sh` script is used by the `run.sh` scripts in `test`. diff --git a/vendor/rustsat-cadical/cppsrc/src/README.md b/vendor/rustsat-cadical/cppsrc/src/README.md new file mode 100644 index 0000000..f17395f --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/README.md @@ -0,0 +1,12 @@ +This is the source code of the library `libcadical.a` with header +`cadical.hpp`, the stand-alone solver `cadical` (in `cadical.cpp`) and the +model based tester `mobical` (in `mobical.app`). + +The `configure` script and link to the `makefile` in the root directory +can be used from within the `src` sub-directory too and then will just work +as if used from the root directory. For instance + + ./configure && make test + +will configure and build in `../build` the default (optimizing) +configuration and if successful then run the test suite. diff --git a/vendor/rustsat-cadical/cppsrc/src/analyze.cpp b/vendor/rustsat-cadical/cppsrc/src/analyze.cpp new file mode 100644 index 0000000..13b9cff --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/analyze.cpp @@ -0,0 +1,1360 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +/*------------------------------------------------------------------------*/ + +// Code for conflict analysis, i.e., to generate the first UIP clause. The +// main function is 'analyze' below. It further uses 'minimize' to minimize +// the first UIP clause, which is in 'minimize.cpp'. An important side +// effect of conflict analysis is to update the decision queue by bumping +// variables. Similarly analyzed clauses are bumped to mark them as active. + +/*------------------------------------------------------------------------*/ + +void Internal::learn_empty_clause () { + assert (!unsat); + build_chain_for_empty (); + LOG ("learned empty clause"); + external->check_learned_empty_clause (); + int64_t id = ++clause_id; + if (proof) { + proof->add_derived_empty_clause (id, lrat_chain); + } + unsat = true; + conflict_id = id; + marked_failed = true; + conclusion.push_back (id); + lrat_chain.clear (); +} + +void Internal::learn_unit_clause (int lit) { + assert (!unsat); + LOG ("learned unit clause %d, stored at position %d", lit, vlit (lit)); + external->check_learned_unit_clause (lit); + int64_t id = ++clause_id; + if (lrat || frat) { + const unsigned uidx = vlit (lit); + unit_clauses (uidx) = id; + } + if (proof) { + proof->add_derived_unit_clause (id, lit, lrat_chain); + } + mark_fixed (lit); +} + +/*------------------------------------------------------------------------*/ + +// Move bumped variables to the front of the (VMTF) decision queue. The +// 'bumped' time stamp is updated accordingly. It is used to determine +// whether the 'queue.assigned' pointer has to be moved in 'unassign'. + +void Internal::bump_queue (int lit) { + assert (opts.bump); + const int idx = vidx (lit); + if (!links[idx].next) + return; + queue.dequeue (links, idx); + queue.enqueue (links, idx); + assert (stats.bumped != INT64_MAX); + btab[idx] = ++stats.bumped; + LOG ("moved to front variable %d and bumped to %" PRId64 "", idx, + btab[idx]); + if (!vals[idx]) + update_queue_unassigned (idx); +} + +/*------------------------------------------------------------------------*/ + +// It would be better to use 'isinf' but there are some historical issues +// with this function. On some platforms it is a macro and even for C++ it +// changed the scope (in pre 5.0 gcc) from '::isinf' to 'std::isinf'. I do +// not want to worry about these strange incompatibilities and thus use the +// same trick as in older solvers (since the MiniSAT team invented EVSIDS) +// and simply put a hard limit here. It is less elegant but easy to port. + +static inline bool evsids_limit_hit (double score) { + assert (sizeof (score) == 8); // assume IEEE 754 64-bit double + return score > 1e150; // MAX_DOUBLE is around 1.8e308 +} + +/*------------------------------------------------------------------------*/ + +// Classical exponential VSIDS as pioneered by MiniSAT. + +void Internal::rescale_variable_scores () { + stats.rescored++; + double divider = score_inc; + for (auto idx : vars) { + const double tmp = stab[idx]; + if (tmp > divider) + divider = tmp; + } + PHASE ("rescore", stats.rescored, "rescoring %d variable scores by 1/%g", + max_var, divider); + assert (divider > 0); + double factor = 1.0 / divider; + for (auto idx : vars) + stab[idx] *= factor; + score_inc *= factor; + PHASE ("rescore", stats.rescored, + "new score increment %g after %" PRId64 " conflicts", score_inc, + stats.conflicts); +} + +void Internal::bump_variable_score (int lit) { + assert (opts.bump); + int idx = vidx (lit); + double old_score = score (idx); + assert (!evsids_limit_hit (old_score)); + double new_score = old_score + score_inc; + if (evsids_limit_hit (new_score)) { + LOG ("bumping %g score of %d hits EVSIDS score limit", old_score, idx); + rescale_variable_scores (); + old_score = score (idx); + assert (!evsids_limit_hit (old_score)); + new_score = old_score + score_inc; + } + assert (!evsids_limit_hit (new_score)); + LOG ("new %g score of %d", new_score, idx); + score (idx) = new_score; + if (scores.contains (idx)) + scores.update (idx); +} + +// Important variables recently used in conflict analysis are 'bumped', + +void Internal::bump_variable (int lit) { + if (use_scores ()) + bump_variable_score (lit); + else + bump_queue (lit); +} + +// After every conflict the variable score increment is increased by a +// factor (if we are currently using scores). + +void Internal::bump_variable_score_inc () { + assert (use_scores ()); + assert (!evsids_limit_hit (score_inc)); + double f = 1e3 / opts.scorefactor; + double new_score_inc = score_inc * f; + if (evsids_limit_hit (new_score_inc)) { + LOG ("bumping %g increment by %g hits EVSIDS score limit", score_inc, + f); + rescale_variable_scores (); + new_score_inc = score_inc * f; + } + assert (!evsids_limit_hit (new_score_inc)); + LOG ("bumped score increment from %g to %g with factor %g", score_inc, + new_score_inc, f); + score_inc = new_score_inc; +} + +/*------------------------------------------------------------------------*/ + +struct analyze_bumped_rank { + Internal *internal; + analyze_bumped_rank (Internal *i) : internal (i) {} + typedef uint64_t Type; + Type operator() (const int &a) const { return internal->bumped (a); } +}; + +struct analyze_bumped_smaller { + Internal *internal; + analyze_bumped_smaller (Internal *i) : internal (i) {} + bool operator() (const int &a, const int &b) const { + const auto s = analyze_bumped_rank (internal) (a); + const auto t = analyze_bumped_rank (internal) (b); + return s < t; + } +}; + +/*------------------------------------------------------------------------*/ + +void Internal::bump_variables () { + + assert (opts.bump); + + START (bump); + + if (!use_scores ()) { + + // Variables are bumped in the order they are in the current decision + // queue. This maintains relative order between bumped variables in + // the queue and seems to work best. We also experimented with + // focusing on variables of the last decision level, but results were + // mixed. + + MSORT (opts.radixsortlim, analyzed.begin (), analyzed.end (), + analyze_bumped_rank (this), analyze_bumped_smaller (this)); + } + + for (const auto &lit : analyzed) + bump_variable (lit); + + if (use_scores ()) + bump_variable_score_inc (); + + STOP (bump); +} + +/*------------------------------------------------------------------------*/ + +// We use the glue time stamp table 'gtab' for fast glue computation. + +int Internal::recompute_glue (Clause *c) { + int res = 0; + const int64_t stamp = ++stats.recomputed; + for (const auto &lit : *c) { + assert (val (lit)); + int level = var (lit).level; + assert (gtab[level] <= stamp); + if (gtab[level] == stamp) + continue; + gtab[level] = stamp; + res++; + } + return res; +} + +// Clauses resolved since the last reduction are marked as 'used', their +// glue is recomputed and they are promoted if the glue shrinks. Note that +// promotion from 'tier3' to 'tier2' will set 'used' to '2'. + +inline void Internal::bump_clause (Clause *c) { + LOG (c, "bumping"); + c->used = max_used; + if (c->hyper) + return; + if (!c->redundant) + return; + int new_glue = recompute_glue (c); + if (new_glue < c->glue) + promote_clause (c, new_glue); + + const size_t glue = + std::min ((size_t) c->glue, stats.used[stable].size () - 1); + ++stats.used[stable][glue]; + ++stats.bump_used[stable]; +} + +void Internal::bump_clause2 (Clause *c) { bump_clause (c); } +/*------------------------------------------------------------------------*/ + +// During conflict analysis literals not seen yet either become part of the +// first unique implication point (UIP) clause (if on lower decision level), +// are dropped (if fixed), or are resolved away (if on the current decision +// level and different from the first UIP). At the same time we update the +// number of seen literals on a decision level. This helps conflict clause +// minimization. The number of seen levels is the glucose level (also +// called 'glue', or 'LBD'). + +inline void Internal::analyze_literal (int lit, int &open, + int &resolvent_size, + int &antecedent_size) { + assert (lit); + Var &v = var (lit); + Flags &f = flags (lit); + + if (!v.level) { + if (f.seen || !lrat) + return; + f.seen = true; + unit_analyzed.push_back (lit); + assert (val (lit) < 0); + int64_t id = unit_id (-lit); + unit_chain.push_back (id); + return; + } + ++antecedent_size; + if (f.seen) + return; + + // before marking as seen, get reason and check for missed unit + + assert (val (lit) < 0); + assert (v.level <= level); + if (v.reason == external_reason) { + assert (!opts.exteagerreasons); + v.reason = learn_external_reason_clause (-lit, 0, true); + if (!v.reason) { // actually a unit + --antecedent_size; + LOG ("%d unit after explanation", -lit); + if (f.seen || !lrat) + return; + f.seen = true; + unit_analyzed.push_back (lit); + assert (val (lit) < 0); + const unsigned uidx = vlit (-lit); + int64_t id = unit_clauses (uidx); + assert (id); + unit_chain.push_back (id); + return; + } + } + + f.seen = true; + analyzed.push_back (lit); + + assert (v.reason != external_reason); + if (v.level < level) + clause.push_back (lit); + Level &l = control[v.level]; + if (!l.seen.count++) { + LOG ("found new level %d contributing to conflict", v.level); + levels.push_back (v.level); + } + if (v.trail < l.seen.trail) + l.seen.trail = v.trail; + ++resolvent_size; + LOG ("analyzed literal %d assigned at level %d", lit, v.level); + if (v.level == level) + open++; +} + +inline void Internal::analyze_reason (int lit, Clause *reason, int &open, + int &resolvent_size, + int &antecedent_size) { + assert (reason); + assert (reason != external_reason); + bump_clause (reason); + if (lrat) + lrat_chain.push_back (reason->id); + for (const auto &other : *reason) + if (other != lit) + analyze_literal (other, open, resolvent_size, antecedent_size); +} + +/*------------------------------------------------------------------------*/ + +// This is an idea which was implicit in MapleCOMSPS 2016 for 'limit = 1'. +// See also the paragraph on 'bumping reason side literals' in their SAT'16 +// paper [LiangGaneshPoupartCzarnecki-SAT'16]. Reason side bumping was +// performed exactly when 'LRB' based decision heuristics was used, which in +// the original version was enabled after 10000 conflicts until a time limit +// of 2500 seconds was reached (half of the competition time limit). The +// Maple / Glucose / MiniSAT evolution winning the SAT race in 2019 made +// the schedule of reason side bumping deterministic, i.e., avoiding a time +// limit, by switching between 'LRB' and 'VSIDS' in an interval of initially +// 30 million propagations, which then is increased geometrically by 10%. + +inline bool Internal::bump_also_reason_literal (int lit) { + assert (lit); + assert (val (lit) < 0); + Flags &f = flags (lit); + if (f.seen) + return false; + const Var &v = var (lit); + if (!v.level) + return false; + f.seen = true; + analyzed.push_back (lit); + LOG ("bumping also reason literal %d assigned at level %d", lit, v.level); + return true; +} + +// We experimented with deeper reason bumping without much success though. + +inline void Internal::bump_also_reason_literals (int lit, int depth_limit, + size_t analyzed_limit) { + assert (lit); + assert (depth_limit > 0); + const Var &v = var (lit); + assert (val (lit)); + if (!v.level) + return; + Clause *reason = v.reason; + if (!reason || reason == external_reason) + return; + stats.ticks.search[stable]++; + for (const auto &other : *reason) { + if (other == lit) + continue; + if (!bump_also_reason_literal (other)) + continue; + if (depth_limit < 2) + continue; + bump_also_reason_literals (-other, depth_limit - 1, analyzed_limit); + if (analyzed.size () > analyzed_limit) + break; + } +} + +inline void Internal::bump_also_all_reason_literals () { + assert (opts.bump); + if (!opts.bumpreason) + return; + if (averages.current.decisions > opts.bumpreasonrate) { + LOG ("decisions per conflict rate %g > limit %d", + (double) averages.current.decisions, opts.bumpreasonrate); + return; + } + if (delay[stable].bumpreasons.limit) { + LOG ("delaying reason bumping %" PRId64 " more times", + delay[stable].bumpreasons.limit); + delay[stable].bumpreasons.limit--; + return; + } + assert (opts.bumpreasondepth > 0); + const int depth_limit = opts.bumpreasondepth + stable; + size_t saved_analyzed = analyzed.size (); + size_t analyzed_limit = saved_analyzed * opts.bumpreasonlimit; + for (const auto &lit : clause) + if (analyzed.size () <= analyzed_limit) + bump_also_reason_literals (-lit, depth_limit, analyzed_limit); + else + break; + if (analyzed.size () > analyzed_limit) { + LOG ("not bumping reason side literals as limit exhausted"); + for (size_t i = saved_analyzed; i != analyzed.size (); i++) { + const int lit = analyzed[i]; + Flags &f = flags (lit); + assert (f.seen); + f.seen = false; + } + delay[stable].bumpreasons.interval++; + analyzed.resize (saved_analyzed); + } else { + LOG ("bumping reasons up to depth %d", opts.bumpreasondepth); + delay[stable].bumpreasons.interval /= 2; + } + LOG ("delay internal %" PRId64, delay[stable].bumpreasons.interval); + delay[stable].bumpreasons.limit = delay[stable].bumpreasons.interval; +} + +/*------------------------------------------------------------------------*/ + +void Internal::clear_unit_analyzed_literals () { + LOG ("clearing %zd unit analyzed literals", unit_analyzed.size ()); + for (const auto &lit : unit_analyzed) { + Flags &f = flags (lit); + assert (f.seen); + assert (!var (lit).level); + f.seen = false; + assert (!f.keep); + assert (!f.poison); + assert (!f.removable); + } + unit_analyzed.clear (); +} + +void Internal::clear_analyzed_literals () { + LOG ("clearing %zd analyzed literals", analyzed.size ()); + for (const auto &lit : analyzed) { + Flags &f = flags (lit); + assert (f.seen); + f.seen = false; + assert (!f.keep); + assert (!f.poison); + assert (!f.removable); + } + analyzed.clear (); +#if 0 // to expensive, even for debugging mode + if (unit_analyzed.size ()) + return; + for (auto idx : vars) { + Flags &f = flags (idx); + assert (!f.seen); + } +#endif +} + +void Internal::clear_analyzed_levels () { + LOG ("clearing %zd analyzed levels", levels.size ()); + for (const auto &l : levels) + if (l < (int) control.size ()) + control[l].reset (); + levels.clear (); +} + +/*------------------------------------------------------------------------*/ + +// Smaller level and trail. Comparing literals on their level is necessary +// for chronological backtracking, since trail order might in this case not +// respect level order. + +struct analyze_trail_negative_rank { + Internal *internal; + analyze_trail_negative_rank (Internal *s) : internal (s) {} + typedef uint64_t Type; + Type operator() (int a) { + Var &v = internal->var (a); + uint64_t res = v.level; + res <<= 32; + res |= v.trail; + return ~res; + } +}; + +struct analyze_trail_larger { + Internal *internal; + analyze_trail_larger (Internal *s) : internal (s) {} + bool operator() (const int &a, const int &b) const { + return analyze_trail_negative_rank (internal) (a) < + analyze_trail_negative_rank (internal) (b); + } +}; + +/*------------------------------------------------------------------------*/ + +// Generate new driving clause and compute jump level. + +Clause *Internal::new_driving_clause (const int glue, int &jump) { + + const size_t size = clause.size (); + Clause *res; + + if (!size) { + + jump = 0; + res = 0; + + } else if (size == 1) { + + iterating = true; + jump = 0; + res = 0; + + } else { + + assert (clause.size () > 1); + + // We have to get the last assigned literals into the watch position. + // Sorting all literals with respect to reverse assignment order is + // overkill but seems to get slightly faster run-time. For 'minimize' + // we sort the literals too heuristically along the trail order (so in + // the opposite order) with the hope to hit the recursion limit less + // frequently. Thus sorting effort is doubled here. + // + MSORT (opts.radixsortlim, clause.begin (), clause.end (), + analyze_trail_negative_rank (this), analyze_trail_larger (this)); + + jump = var (clause[1]).level; + res = new_learned_redundant_clause (glue); + res->used = max_used; + } + + LOG ("jump level %d", jump); + + return res; +} + +/*------------------------------------------------------------------------*/ + +// determine the OTFS level for OTFS. Unlike the find_conflict_level, we do +// not have to fix the clause + +inline int Internal::otfs_find_backtrack_level (int &forced) { + assert (opts.otfs); + int res = 0; + + for (const auto &lit : *conflict) { + const int tmp = var (lit).level; + if (tmp == level) { + forced = lit; + } else if (tmp > res) { + res = tmp; + LOG ("bt level is now %d due to %d", res, lit); + } + } + return res; +} + +/*------------------------------------------------------------------------*/ + +// If chronological backtracking is enabled we need to find the actual +// conflict level and then potentially can also reuse the conflict clause +// as driving clause instead of deriving a redundant new driving clause +// (forcing 'forced') if the number 'count' of literals in conflict assigned +// at the conflict level is exactly one. + +inline int Internal::find_conflict_level (int &forced) { + + assert (conflict); + assert (opts.chrono || opts.otfs || external_prop); + + int res = 0, count = 0; + + forced = 0; + + for (const auto &lit : *conflict) { + const int tmp = var (lit).level; + if (tmp > res) { + res = tmp; + forced = lit; + count = 1; + } else if (tmp == res) { + count++; + if (res == level && count > 1) + break; + } + } + + LOG ("%d literals on actual conflict level %d", count, res); + + const int size = conflict->size; + int *lits = conflict->literals; + + // Move the two highest level literals to the front. + // + for (int i = 0; i < 2; i++) { + + const int lit = lits[i]; + + int highest_position = i; + int highest_literal = lit; + int highest_level = var (highest_literal).level; + + for (int j = i + 1; j < size; j++) { + const int other = lits[j]; + const int tmp = var (other).level; + if (highest_level >= tmp) + continue; + highest_literal = other; + highest_position = j; + highest_level = tmp; + if (highest_level == res) + break; + } + + // No unwatched higher assignment level literal. + // + if (highest_position == i) + continue; + + if (highest_position > 1) { + LOG (conflict, "unwatch %d in", lit); + remove_watch (watches (lit), conflict); + } + + lits[highest_position] = lit; + lits[i] = highest_literal; + + if (highest_position > 1) + watch_literal (highest_literal, lits[!i], conflict); + } + + // Only if the number of highest level literals in the conflict is one + // then we can reuse the conflict clause as driving clause for 'forced'. + // + if (count != 1) + forced = 0; + + return res; +} + +/*------------------------------------------------------------------------*/ + +inline int Internal::determine_actual_backtrack_level (int jump) { + + int res; + + assert (level > jump); + + if (!opts.chrono) { + res = jump; + LOG ("chronological backtracking disabled using jump level %d", res); + } else if (opts.chronoalways) { + stats.chrono++; + res = level - 1; + LOG ("forced chronological backtracking to level %d", res); + } else if (jump >= level - 1) { + res = jump; + LOG ("jump level identical to chronological backtrack level %d", res); + } else if ((size_t) jump < assumptions.size ()) { + res = jump; + LOG ("using jump level %d since it is lower than assumption level %zd", + res, assumptions.size ()); + } else if (level - jump > opts.chronolevelim) { + stats.chrono++; + res = level - 1; + LOG ("back-jumping over %d > %d levels prohibited" + "thus backtracking chronologically to level %d", + level - jump, opts.chronolevelim, res); + } else if (opts.chronoreusetrail) { + int best_idx = 0, best_pos = 0; + + if (use_scores ()) { + for (size_t i = control[jump + 1].trail; i < trail.size (); i++) { + const int idx = abs (trail[i]); + if (best_idx && !score_smaller (this) (best_idx, idx)) + continue; + best_idx = idx; + best_pos = i; + } + LOG ("best variable score %g", score (best_idx)); + } else { + for (size_t i = control[jump + 1].trail; i < trail.size (); i++) { + const int idx = abs (trail[i]); + if (best_idx && bumped (best_idx) >= bumped (idx)) + continue; + best_idx = idx; + best_pos = i; + } + LOG ("best variable bumped %" PRId64 "", bumped (best_idx)); + } + assert (best_idx); + LOG ("best variable %d at trail position %d", best_idx, best_pos); + + // Now find the frame and decision level in the control stack of that + // best variable index. Note that, as in 'reuse_trail', the frame + // 'control[i]' for decision level 'i' contains the trail before that + // decision level, i.e., the decision 'control[i].decision' sits at + // 'control[i].trail' in the trail and we thus have to check the level + // of the control frame one higher than at the result level. + // + res = jump; + while (res < level - 1 && control[res + 1].trail <= best_pos) + res++; + + if (res == jump) + LOG ("default non-chronological back-jumping to level %d", res); + else { + stats.chrono++; + LOG ("chronological backtracking to level %d to reuse trail", res); + } + + } else { + res = jump; + LOG ("non-chronological back-jumping to level %d", res); + } + + return res; +} + +/*------------------------------------------------------------------------*/ + +void Internal::eagerly_subsume_recently_learned_clauses (Clause *c) { + assert (opts.eagersubsume); + LOG (c, "trying eager subsumption with"); + mark (c); + int64_t lim = stats.eagertried + opts.eagersubsumelim; + const auto begin = clauses.begin (); + auto it = clauses.end (); +#ifdef LOGGING + int64_t before = stats.eagersub; +#endif + while (it != begin && stats.eagertried++ <= lim) { + Clause *d = *--it; + if (c == d) + continue; + if (d->garbage) + continue; + if (!d->redundant) + continue; + int needed = c->size; + for (auto &lit : *d) { + if (marked (lit) <= 0) + continue; + if (!--needed) + break; + } + if (needed) + continue; + LOG (d, "eager subsumed"); + stats.eagersub++; + stats.subsumed++; + mark_garbage (d); + } + unmark (c); +#ifdef LOGGING + uint64_t subsumed = stats.eagersub - before; + if (subsumed) + LOG ("eagerly subsumed %" PRIu64 " clauses", subsumed); +#endif +} + +/*------------------------------------------------------------------------*/ + +Clause *Internal::on_the_fly_strengthen (Clause *new_conflict, int uip) { + assert (new_conflict); + assert (new_conflict->size > 2); + LOG (new_conflict, "applying OTFS on lit %d", uip); + auto sorted = std::vector (); + sorted.reserve (new_conflict->size); + assert (sorted.empty ()); + ++stats.otfs.strengthened; + + int *lits = new_conflict->literals; + + assert (lits[0] == uip || lits[1] == uip); + const int other_init = lits[0] ^ lits[1] ^ uip; + + assert (mini_chain.empty ()); + + const int old_size = new_conflict->size; + int new_size = 0; + for (int i = 0; i < old_size; ++i) { + const int other = lits[i]; + sorted.push_back (other); + if (var (other).level) + lits[new_size++] = other; + } + + LOG (new_conflict, "removing all units in"); + + assert (lits[0] == uip || lits[1] == uip); + const int other = lits[0] ^ lits[1] ^ uip; + lits[0] = other; + lits[1] = lits[--new_size]; + LOG (new_conflict, "putting uip at pos 1"); + + if (other_init != other) + remove_watch (watches (other_init), new_conflict); + remove_watch (watches (uip), new_conflict); + + assert (!lrat || lrat_chain.back () == new_conflict->id); + if (lrat) { + assert (!lrat_chain.empty ()); + for (const auto &id : unit_chain) { + mini_chain.push_back (id); + } + const auto end = lrat_chain.rend (); + const auto begin = lrat_chain.rbegin (); + for (auto i = begin; i != end; i++) { + const auto id = *i; + mini_chain.push_back (id); + } + lrat_chain.clear (); + clear_unit_analyzed_literals (); + unit_chain.clear (); + } + assert (unit_analyzed.empty ()); + // sort the clause + { + int highest_pos = 0; + int highest_level = 0; + for (int i = 1; i < new_size; i++) { + const unsigned other = lits[i]; + assert (val (other) < 0); + const int level = var (other).level; + assert (level); + LOG ("checking %d", other); + if (level <= highest_level) + continue; + highest_pos = i; + highest_level = level; + } + LOG ("highest lit is %d", lits[highest_pos]); + if (highest_pos != 1) + swap (lits[1], lits[highest_pos]); + LOG ("removing %d literals", new_conflict->size - new_size); + + if (new_size == 1) { + LOG (new_conflict, "new size = 1, so interrupting"); + assert (!opts.exteagerreasons); + return 0; + } else { + otfs_strengthen_clause (new_conflict, uip, new_size, sorted); + assert (new_size == new_conflict->size); + } + } + + if (other_init != other) + watch_literal (other, lits[1], new_conflict); + else { + update_watch_size (watches (other), lits[1], new_conflict); + } + watch_literal (lits[1], other, new_conflict); + + LOG (new_conflict, "strengthened clause by OTFS"); + sorted.clear (); + + return new_conflict; +} + +/*------------------------------------------------------------------------*/ +inline void Internal::otfs_subsume_clause (Clause *subsuming, + Clause *subsumed) { + stats.subsumed++; + assert (subsuming->size <= subsumed->size); + LOG (subsumed, "subsumed"); + if (subsumed->redundant) + stats.subred++; + else + stats.subirr++; + if (subsumed->redundant || !subsuming->redundant) { + mark_garbage (subsumed); + return; + } + LOG ("turning redundant subsuming clause into irredundant clause"); + subsuming->redundant = false; + if (proof) + proof->strengthen (subsuming->id); + mark_garbage (subsumed); + stats.current.irredundant++; + stats.added.irredundant++; + stats.irrlits += subsuming->size; + assert (stats.current.redundant > 0); + stats.current.redundant--; + assert (stats.added.redundant > 0); + stats.added.redundant--; + // ... and keep 'stats.added.total'. +} + +/*------------------------------------------------------------------------*/ + +// Candidate clause 'c' is strengthened by removing 'lit' and units. +// +void Internal::otfs_strengthen_clause (Clause *c, int lit, int new_size, + const std::vector &old) { + stats.strengthened++; + assert (c->size > 2); + (void) shrink_clause (c, new_size); + if (proof) { + proof->otfs_strengthen_clause (c, old, mini_chain); + } + if (!c->redundant) { + mark_removed (lit); + } + mini_chain.clear (); + c->used = max_used; + LOG (c, "strengthened"); + external->check_shrunken_clause (c); +} + +/*------------------------------------------------------------------------*/ + +// If the average number of decisions per conflict (analysis actually so not +// taking OTFS conflicts into account) is high we do not bump reasons. This +// is the function which updates the exponential moving decision rate +// average. + +void Internal::update_decision_rate_average () { + int64_t current = stats.decisions; + int64_t decisions = current - saved_decisions; + UPDATE_AVERAGE (averages.current.decisions, decisions); + saved_decisions = current; +} + +/*------------------------------------------------------------------------*/ + +// This is the main conflict analysis routine. It assumes that a conflict +// was found. Then we derive the 1st UIP clause, optionally minimize it, +// add it as learned clause, and then uses the clause for conflict directed +// back-jumping and flipping the 1st UIP literal. In combination with +// chronological backtracking (see discussion above) the algorithm becomes +// slightly more involved. + +void Internal::analyze () { + + START (analyze); + + assert (conflict); + assert (lrat_chain.empty ()); + assert (unit_chain.empty ()); + assert (unit_analyzed.empty ()); + assert (clause.empty ()); + + // First update moving averages of trail height at conflict. + // + UPDATE_AVERAGE (averages.current.trail.fast, num_assigned); + UPDATE_AVERAGE (averages.current.trail.slow, num_assigned); + update_decision_rate_average (); + + /*----------------------------------------------------------------------*/ + + if (external_prop && !external_prop_is_lazy && opts.exteagerreasons) { + explain_external_propagations (); + } + + if (opts.chrono || external_prop) { + + int forced; + + const int conflict_level = find_conflict_level (forced); + + // In principle we can perform conflict analysis as in non-chronological + // backtracking except if there is only one literal with the maximum + // assignment level in the clause. Then standard conflict analysis is + // unnecessary and we can use the conflict as a driving clause. In the + // pseudo code of the SAT'18 paper on chronological backtracking this + // corresponds to the situation handled in line 4-6 in Alg. 1, except + // that the pseudo code in the paper only backtracks while we eagerly + // assign the single literal on the highest decision level. + + if (forced) { + + assert (forced); + assert (conflict_level > 0); + LOG ("single highest level literal %d", forced); + + // The pseudo code in the SAT'18 paper actually backtracks to the + // 'second highest decision' level, while their code backtracks + // to 'conflict_level-1', which is more in the spirit of chronological + // backtracking anyhow and thus we also do the latter. + // + backtrack (conflict_level - 1); + + // if we are on decision level 0 search assign will learn unit + // so we need a valid chain here (of course if we are not on decision + // level 0 this will not result in a valid chain). + // we can just use build_chain_for_units in propagate + // + build_chain_for_units (forced, conflict, 0); + + LOG ("forcing %d", forced); + search_assign_driving (forced, conflict); + + conflict = 0; + if (!opts.chrono) + did_external_prop = true; + STOP (analyze); + return; + } + + // Backtracking to the conflict level is in the pseudo code in the + // SAT'18 chronological backtracking paper, but not in their actual + // implementation. In principle we do not need to backtrack here. + // However, as a side effect of backtracking to the conflict level we + // set 'level' to the conflict level which then allows us to reuse the + // old 'analyze' code as is. The alternative (which we also tried but + // then abandoned) is to use 'conflict_level' instead of 'level' in the + // analysis, which however requires to pass it to the 'analyze_reason' + // and 'analyze_literal' functions. + // + backtrack (conflict_level); + } + + // Actual conflict on root level, thus formula unsatisfiable. + // + if (!level) { + learn_empty_clause (); + if (external->learner) + external->export_learned_empty_clause (); + STOP (analyze); + return; + } + + /*----------------------------------------------------------------------*/ + + // First derive the 1st UIP clause by going over literals assigned on the + // current decision level. Literals in the conflict are marked as 'seen' + // as well as all literals in reason clauses of already 'seen' literals on + // the current decision level. Thus the outer loop starts with the + // conflict clause as 'reason' and then uses the 'reason' of the next + // seen literal on the trail assigned on the current decision level. + // During this process maintain the number 'open' of seen literals on the + // current decision level with not yet processed 'reason'. As soon 'open' + // drops to one, we have found the first unique implication point. This + // is sound because the topological order in which literals are processed + // follows the assignment order and a more complex algorithm to find + // articulation points is not necessary. + // + Clause *reason = conflict; + LOG (reason, "analyzing conflict"); + + assert (clause.empty ()); + assert (lrat_chain.empty ()); + + const auto &t = &trail; + int i = t->size (); // Start at end-of-trail. + int open = 0; // Seen but not processed on this level. + int uip = 0; // The first UIP literal. + int resolvent_size = 0; // without the uip + int antecedent_size = 1; // with the uip and without unit literals + int conflict_size = 0; // without the uip and without unit literals + int resolved = 0; // number of resolution (0 = clause in CNF) + const bool otfs = opts.otfs; + + for (;;) { + antecedent_size = 1; // for uip + analyze_reason (uip, reason, open, resolvent_size, antecedent_size); + if (resolved == 0) + conflict_size = antecedent_size - 1; + assert (resolvent_size == open + (int) clause.size ()); + + if (otfs && resolved > 0 && antecedent_size > 2 && + resolvent_size < antecedent_size) { + assert (reason != conflict); + LOG (analyzed, "found candidate for OTFS conflict"); + LOG (clause, "found candidate for OTFS conflict"); + LOG (reason, "found candidate (size %d) for OTFS resolvent", + antecedent_size); + const int other = reason->literals[0] ^ reason->literals[1] ^ uip; + assert (other != uip); + reason = on_the_fly_strengthen (reason, uip); + if (opts.bump) + bump_variables (); + + assert (conflict_size); + if (!reason) { + uip = -other; + assert (open == 1); + LOG ("clause is actually unit %d, stopping", -uip); + reverse (begin (mini_chain), end (mini_chain)); + for (auto id : mini_chain) + lrat_chain.push_back (id); + mini_chain.clear (); + clear_analyzed_levels (); + assert (!opts.exteagerreasons); + clause.clear (); + break; + } + assert (conflict_size >= 2); + + if (resolved == 1 && resolvent_size < conflict_size) { + // here both clauses are part of the CNF, so one subsumes the other + otfs_subsume_clause (reason, conflict); + LOG (reason, "changing conflict to"); + --conflict_size; + assert (conflict_size == reason->size); + ++stats.otfs.subsumed; + ++stats.subsumed; + } + + LOG (reason, "changing conflict to"); + conflict = reason; + if (open == 1) { + int forced = 0; + const int conflict_level = otfs_find_backtrack_level (forced); + int new_level = determine_actual_backtrack_level (conflict_level); + UPDATE_AVERAGE (averages.current.level, new_level); + backtrack (new_level); + + LOG ("forcing %d", forced); + search_assign_driving (forced, conflict); + + // Clean up. + // + conflict = 0; + clear_analyzed_literals (); + clear_analyzed_levels (); + clause.clear (); + STOP (analyze); + return; + } + + stats.conflicts++; + + clear_analyzed_literals (); + clear_analyzed_levels (); + clause.clear (); + resolvent_size = 0; + antecedent_size = 1; + resolved = 0; + open = 0; + analyze_reason (0, reason, open, resolvent_size, antecedent_size); + conflict_size = antecedent_size - 1; + assert (open > 1); + } + + ++resolved; + + uip = 0; + while (!uip) { + if (!i) { + lazy_external_propagator_out_of_order_clause (uip); + if (unsat) + return; + else if (uip) { + open = 1; + break; + } else { + LOG (reason, "restarting the analysis on the new conflict"); + ++stats.conflicts; + reason = conflict; + resolvent_size = 0; + antecedent_size = 1; + resolved = 0; + open = 0; + analyze_reason (0, reason, open, resolvent_size, antecedent_size); + conflict_size = antecedent_size - 1; + assert (open > 1); + } + } + assert (i > 0); + const int lit = (*t)[--i]; + if (!flags (lit).seen) + continue; + if (var (lit).level == level) + uip = lit; + } + if (!--open) + break; + reason = var (uip).reason; + if (reason == external_reason) { + assert (!opts.exteagerreasons); + reason = learn_external_reason_clause (-uip, 0, true); + var (uip).reason = reason; + } + assert (reason != external_reason); + LOG (reason, "analyzing %d reason", uip); + assert (resolvent_size); + --resolvent_size; + } + LOG ("first UIP %d", uip); + clause.push_back (-uip); + + // Update glue and learned (1st UIP literals) statistics. + // + int size = (int) clause.size (); + const int glue = (int) levels.size () - 1; + LOG (clause, "1st UIP size %d and glue %d clause", size, glue); + UPDATE_AVERAGE (averages.current.glue.fast, glue); + UPDATE_AVERAGE (averages.current.glue.slow, glue); + stats.learned.literals += size; + stats.learned.clauses++; + assert (glue < size); + + // up to this point lrat_chain contains the proof for current clause in + // reversed order. in minimize and shrink the clause is changed and + // therefore lrat_chain has to be extended. Unfortunately we cannot create + // the chain directly during minimization (or shrinking) but afterwards we + // can calculate it pretty easily and even better the same algorithm works + // for both shrinking and minimization. + + // Minimize the 1st UIP clause as pioneered by Niklas Soerensson in + // MiniSAT and described in our joint SAT'09 paper. + // + if (size > 1) { + if (opts.shrink) + shrink_and_minimize_clause (); + else if (opts.minimize) + minimize_clause (); + + size = (int) clause.size (); + + // Update decision heuristics. + // + if (opts.bump) { + bump_also_all_reason_literals (); + bump_variables (); + } + + if (external->learner) + external->export_learned_large_clause (clause); + } else if (external->learner) + external->export_learned_unit_clause (-uip); + + // Update actual size statistics. + // + stats.units += (size == 1); + stats.binaries += (size == 2); + UPDATE_AVERAGE (averages.current.size, size); + + // reverse lrat_chain. We could probably work with reversed iterators + // (views) to be more efficient but we would have to distinguish in proof + // + if (lrat) { + LOG (unit_chain, "unit chain: "); + for (auto id : unit_chain) + lrat_chain.push_back (id); + unit_chain.clear (); + reverse (lrat_chain.begin (), lrat_chain.end ()); + } + + // Determine back-jump level, learn driving clause, backtrack and assign + // flipped 1st UIP literal. + // + int jump; + Clause *driving_clause = new_driving_clause (glue, jump); + UPDATE_AVERAGE (averages.current.jump, jump); + + int new_level = determine_actual_backtrack_level (jump); + UPDATE_AVERAGE (averages.current.level, new_level); + backtrack (new_level); + + // It should hold that (!level <=> size == 1) + // and (!uip <=> size == 0) + // this means either we have already learned a clause => size >= 2 + // in this case we will not learn empty clause or unit here + // or we haven't actually learned a clause in new_driving_clause + // then lrat_chain is still valid and we will learn a unit or empty clause + // + if (uip) { + search_assign_driving (-uip, driving_clause); + } else + learn_empty_clause (); + + if (stable) + reluctant.tick (); // Reluctant has its own 'conflict' counter. + + // Clean up. + // + clear_analyzed_literals (); + clear_unit_analyzed_literals (); + clear_analyzed_levels (); + clause.clear (); + conflict = 0; + + lrat_chain.clear (); + STOP (analyze); + + if (driving_clause && opts.eagersubsume) + eagerly_subsume_recently_learned_clauses (driving_clause); + + if (lim.recompute_tier <= stats.conflicts) + recompute_tier (); +} + +// In the special case where the external propagator is lazy, the same +// invariants as OTFS break (but even more complicated). There are three +// possible cases: +// - the clause becomes empty (unsat must be answered) +// - the clause is a unit (backtrack and set the clause) +// - the clause is a new conflict on lower level and we restart the +// analysis +// +// TODO: we do not really need to keep the clause longer than the conflict +// analysis. +void Internal::lazy_external_propagator_out_of_order_clause (int &uip) { + assert (!opts.exteagerreasons); + assert (external_prop); + LOG (clause, "out-of-order conflict"); + if (clause.empty ()) { + LOG (lrat_chain, "lrat_chain:"); + LOG (clause, "clause:"); + LOG (unit_chain, "units:"); + if (lrat) { + LOG (unit_chain, "unit chain: "); + for (auto id : unit_chain) + lrat_chain.push_back (id); + unit_chain.clear (); + reverse (lrat_chain.begin (), lrat_chain.end ()); + } + LOG (lrat_chain, "lrat_chain:"); + learn_empty_clause (); + if (external->learner) + external->export_learned_empty_clause (); + conflict = 0; + } else if (clause.size () == 1) { + LOG ("found out-of-order unit"); + uip = -clause[0]; + assert (uip); + backtrack (var (uip).level); + assert (val (uip) > 0); + clause.clear (); + } else { + int jump; + const int glue = clause.size () - 1; + conflict = new_driving_clause (glue, jump); + UPDATE_AVERAGE (averages.current.level, jump); + backtrack (jump); + LOG (conflict, "new conflict"); + } + // Clean up. + // + clear_analyzed_literals (); + clear_unit_analyzed_literals (); + clear_analyzed_levels (); + clause.clear (); + + if (unsat) { + lrat_chain.clear (); + STOP (analyze); + } +} + +// We wait reporting a learned unit until propagation of that unit is +// completed. Otherwise the 'i' report gives the number of remaining +// variables before propagating the unit (and hides the actual remaining +// variables after propagating it). + +void Internal::iterate () { + iterating = false; + report ('i'); +} + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/arena.cpp b/vendor/rustsat-cadical/cppsrc/src/arena.cpp new file mode 100644 index 0000000..b00a44f --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/arena.cpp @@ -0,0 +1,30 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +Arena::Arena (Internal *i) { + memset ((void *) this, 0, sizeof *this); + internal = i; +} + +Arena::~Arena () { + delete[] from.start; + delete[] to.start; +} + +void Arena::prepare (size_t bytes) { + LOG ("preparing 'to' space of arena with %zd bytes", bytes); + assert (!to.start); + to.top = to.start = new char[bytes]; + to.end = to.start + bytes; +} + +void Arena::swap () { + delete[] from.start; + LOG ("delete 'from' space of arena with %zd bytes", + (size_t) (from.end - from.start)); + from = to; + to.start = to.top = to.end = 0; +} + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/arena.hpp b/vendor/rustsat-cadical/cppsrc/src/arena.hpp new file mode 100644 index 0000000..6dbd511 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/arena.hpp @@ -0,0 +1,105 @@ +#ifndef _arena_hpp_INCLUDED +#define _arena_hpp_INCLUDED + +namespace CaDiCaL { + +// This memory allocation arena provides fixed size pre-allocated memory for +// the moving garbage collector 'copy_non_garbage_clauses' in 'collect.cpp' +// to hold clauses which should survive garbage collection. + +// The advantage of using a pre-allocated arena is that the allocation order +// of the clauses can be adapted in such a way that clauses watched by the +// same literal are allocated consecutively. This improves locality during +// propagation and thus is more cache friendly. A similar technique is +// implemented in MiniSAT and Glucose and gives substantial speed-up in +// propagations per second even though it might even almost double peek +// memory usage. Note that in MiniSAT this arena is actually required for +// MiniSAT to be able to use 32 bit clauses references instead of 64 bit +// pointers. This would restrict the maximum number of clauses and thus is +// a restriction we do not want to use anymore. + +// New learned clauses are allocated in CaDiCaL outside of this arena and +// moved to the arena during garbage collection. The additional 'to' space +// required for such a moving garbage collector is only allocated for those +// clauses surviving garbage collection, which usually needs much less +// memory than all clauses. The net effect is that in our implementation +// the moving garbage collector using this arena only needs roughly 50% more +// memory than allocating the clauses directly. Both implementations can be +// compared by varying the 'opts.arenatype' option (which also controls the +// allocation order of clauses during moving them). + +// The standard sequence of using the arena is as follows: +// +// Arena arena; +// ... +// arena.prepare (bytes); +// q1 = arena.copy (p1, bytes1); +// ... +// qn = arena.copy (pn, bytesn); +// assert (bytes1 + ... + bytesn <= bytes); +// arena.swap (); +// ... +// if (!arena.contains (q)) delete q; +// ... +// arena.prepare (bytes); +// q1 = arena.copy (p1, bytes1); +// ... +// qn = arena.copy (pn, bytesn); +// assert (bytes1 + ... + bytesn <= bytes); +// arena.swap (); +// ... +// +// One has to be really careful with 'qi' references to arena memory. + +struct Internal; + +class Arena { + + Internal *internal; + + struct { + char *start, *top, *end; + } from, to; + +public: + Arena (Internal *); + ~Arena (); + + // Prepare 'to' space to hold that amount of memory. Precondition is that + // the 'to' space is empty. The following sequence of 'copy' operations + // can use as much memory in sum as pre-allocated here. + // + void prepare (size_t bytes); + + // Does the memory pointed to by 'p' belong to this arena? More precisely + // to the 'from' space, since that is the only one remaining after 'swap'. + // + bool contains (void *p) const { + char *c = (char *) p; + return (from.start <= c && c < from.top) || + (to.start <= c && c < to.top); + } + + // Allocate that amount of memory in 'to' space. This assumes the 'to' + // space has been prepared to hold enough memory with 'prepare'. Then + // copy the memory pointed to by 'p' of size 'bytes'. Note that it does + // not matter whether 'p' is in 'from' or allocated outside of the arena. + // + char *copy (const char *p, size_t bytes) { + char *res = to.top; + to.top += bytes; + assert (to.top <= to.end); + memcpy (res, p, bytes); + return res; + } + + // Completely delete 'from' space and then replace 'from' by 'to' (by + // pointer swapping). Everything previously allocated (in 'from') and not + // explicitly copied to 'to' with 'copy' becomes invalid. + // + void swap (); +}; + +} // namespace CaDiCaL + +#endif diff --git a/vendor/rustsat-cadical/cppsrc/src/assume.cpp b/vendor/rustsat-cadical/cppsrc/src/assume.cpp new file mode 100644 index 0000000..5c7bd4a --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/assume.cpp @@ -0,0 +1,616 @@ +#include "internal.hpp" +#include "options.hpp" + +namespace CaDiCaL { + +// Failed literal handling as pioneered by MiniSAT. This first function +// adds an assumption literal onto the assumption stack. + +void Internal::assume (int lit) { + if (level && !opts.ilb) + backtrack (); + else if (val (lit) < 0) + backtrack (max (0, var (lit).level - 1)); + Flags &f = flags (lit); + const unsigned char bit = bign (lit); + if (f.assumed & bit) { + LOG ("ignoring already assumed %d", lit); + return; + } + LOG ("assume %d", lit); + f.assumed |= bit; + assumptions.push_back (lit); + freeze (lit); +} + +// for LRAT we actually need to implement recursive DFS +// for non-lrat use BFS. TODO: maybe derecursify to avoid stack overflow +// +void Internal::assume_analyze_literal (int lit) { + assert (lit); + Flags &f = flags (lit); + if (f.seen) + return; + f.seen = true; + analyzed.push_back (lit); + Var &v = var (lit); + assert (val (lit) < 0); + if (v.reason == external_reason) { + v.reason = wrapped_learn_external_reason_clause (-lit); + assert (v.reason || !v.level); + } + assert (v.reason != external_reason); + if (!v.level) { + int64_t id = unit_id (-lit); + lrat_chain.push_back (id); + return; + } + if (v.reason) { + assert (v.level); + LOG (v.reason, "analyze reason"); + for (const auto &other : *v.reason) { + assume_analyze_literal (other); + } + lrat_chain.push_back (v.reason->id); + return; + } + assert (assumed (-lit)); + LOG ("failed assumption %d", -lit); + clause.push_back (lit); +} + +void Internal::assume_analyze_reason (int lit, Clause *reason) { + assert (reason); + assert (lrat_chain.empty ()); + assert (reason != external_reason); + assert (lrat); + for (const auto &other : *reason) + if (other != lit) + assume_analyze_literal (other); + lrat_chain.push_back (reason->id); +} + +// Find all failing assumptions starting from the one on the assumption +// stack with the lowest decision level. This goes back to MiniSAT and is +// called 'analyze_final' there. + +void Internal::failing () { + + START (analyze); + + LOG ("analyzing failing assumptions"); + + assert (analyzed.empty ()); + assert (clause.empty ()); + assert (lrat_chain.empty ()); + assert (!marked_failed); + assert (!conflict_id); + + if (!unsat_constraint) { + // Search for failing assumptions in the (internal) assumption stack. + + // There are in essence three cases: (1) An assumption is falsified on + // the root-level and then 'failed_unit' is set to that assumption, (2) + // two clashing assumptions are assumed and then 'failed_clashing' is + // set to the second assumed one, or otherwise (3) there is a failing + // assumption 'first_failed' with minimum (non-zero) decision level + // 'failed_level'. + + int failed_unit = 0; + int failed_clashing = 0; + int first_failed = 0; + int failed_level = INT_MAX; + int efailed = 0; + + for (auto &elit : external->assumptions) { + int lit = external->e2i[abs (elit)]; + if (elit < 0) + lit = -lit; + if (val (lit) >= 0) + continue; + const Var &v = var (lit); + if (!v.level) { + failed_unit = lit; + efailed = elit; + break; + } + if (failed_clashing) + continue; + if (v.reason == external_reason) { + Var &ev = var (lit); + ev.reason = learn_external_reason_clause (-lit); + if (!ev.reason) { + ev.level = 0; + failed_unit = lit; + efailed = elit; + break; + } + ev.level = 0; + // Recalculate assignment level + for (const auto &other : *ev.reason) { + if (other == -lit) + continue; + assert (val (other)); + int tmp = var (other).level; + if (tmp > ev.level) + ev.level = tmp; + } + if (!ev.level) { + failed_unit = lit; + efailed = elit; + break; + } + } + assert (v.reason != external_reason); + if (!v.reason) { + failed_clashing = lit; + efailed = elit; + } else if (!first_failed || v.level < failed_level) { + first_failed = lit; + efailed = elit; + failed_level = v.level; + } + } + + assert (clause.empty ()); + + // Get the 'failed' assumption from one of the three cases. + int failed; + if (failed_unit) + failed = failed_unit; + else if (failed_clashing) + failed = failed_clashing; + else + failed = first_failed; + assert (failed); + assert (efailed); + + // In any case mark literal 'failed' as failed assumption. + { + Flags &f = flags (failed); + const unsigned bit = bign (failed); + assert (!(f.failed & bit)); + f.failed |= bit; + } + + // First case (1). + if (failed_unit) { + assert (failed == failed_unit); + LOG ("root-level falsified assumption %d", failed); + if (proof) { + if (lrat) { + unsigned eidx = (efailed > 0) + 2u * (unsigned) abs (efailed); + assert ((size_t) eidx < external->ext_units.size ()); + const int64_t id = external->ext_units[eidx]; + if (id) { + lrat_chain.push_back (id); + } else { + int64_t id = unit_id (-failed_unit); + lrat_chain.push_back (id); + } + } + proof->add_assumption_clause (++clause_id, -efailed, lrat_chain); + conclusion.push_back (clause_id); + lrat_chain.clear (); + } + goto DONE; + } + + // Second case (2). + if (failed_clashing) { + assert (failed == failed_clashing); + LOG ("clashing assumptions %d and %d", failed, -failed); + Flags &f = flags (-failed); + const unsigned bit = bign (-failed); + assert (!(f.failed & bit)); + f.failed |= bit; + if (proof) { + vector clash = {externalize (failed), externalize (-failed)}; + proof->add_assumption_clause (++clause_id, clash, lrat_chain); + conclusion.push_back (clause_id); + } + goto DONE; + } + + // Fall through to third case (3). + LOG ("starting with assumption %d falsified on minimum decision level " + "%d", + first_failed, failed_level); + + assert (first_failed); + assert (failed_level > 0); + + // The 'analyzed' stack serves as working stack for a BFS through the + // implication graph until decisions, which are all assumptions, or + // units are reached. This is simpler than corresponding code in + // 'analyze'. + { + LOG ("failed assumption %d", first_failed); + Flags &f = flags (first_failed); + assert (!f.seen); + f.seen = true; + assert (f.failed & bign (first_failed)); + analyzed.push_back (-first_failed); + clause.push_back (-first_failed); + } + } else { + // unsat_constraint + // The assumptions necessary to fail each literal in the constraint are + // collected. + for (auto lit : constraint) { + lit *= -1; + assert (lit != INT_MIN); + flags (lit).seen = true; + analyzed.push_back (lit); + } + } + + { + // used for unsat_constraint lrat + vector> constraint_chains; + vector> constraint_clauses; + vector sum_constraints; + vector econstraints; + for (auto &elit : external->constraint) { + int lit = external->e2i[abs (elit)]; + if (elit < 0) + lit = -lit; + if (!lit) + continue; + Flags &f = flags (lit); + if (f.seen) + continue; + if (std::find (econstraints.begin (), econstraints.end (), elit) != + econstraints.end ()) + continue; + econstraints.push_back (elit); + } + + // no LRAT do bfs as it was before + if (!lrat) { + size_t next = 0; + while (next < analyzed.size ()) { + const int lit = analyzed[next++]; + assert (val (lit) > 0); + Var &v = var (lit); + if (!v.level) + continue; + if (v.reason == external_reason) { + v.reason = wrapped_learn_external_reason_clause (lit); + if (!v.reason) { + v.level = 0; + continue; + } + } + assert (v.reason != external_reason); + if (v.reason) { + assert (v.level); + LOG (v.reason, "analyze reason"); + for (const auto &other : *v.reason) { + Flags &f = flags (other); + if (f.seen) + continue; + f.seen = true; + assert (val (other) < 0); + analyzed.push_back (-other); + } + } else { + assert (assumed (lit)); + LOG ("failed assumption %d", lit); + clause.push_back (-lit); + Flags &f = flags (lit); + const unsigned bit = bign (lit); + assert (!(f.failed & bit)); + f.failed |= bit; + } + } + clear_analyzed_literals (); + } else if (!unsat_constraint) { // LRAT for case (3) + assert (clause.size () == 1); + const int lit = clause[0]; + Var &v = var (lit); + assert (v.reason); + if (v.reason == external_reason) { // does this even happen? + v.reason = wrapped_learn_external_reason_clause (lit); + } + assert (v.reason != external_reason); + if (v.reason) + assume_analyze_reason (lit, v.reason); + else { + int64_t id = unit_id (lit); + lrat_chain.push_back (id); + } + for (auto &lit : clause) { + Flags &f = flags (lit); + const unsigned bit = bign (-lit); + if (!(f.failed & bit)) + f.failed |= bit; + } + clear_analyzed_literals (); + } else { // LRAT for unsat_constraint + assert (clause.empty ()); + clear_analyzed_literals (); + for (auto lit : constraint) { + // make sure nothing gets marked failed twice + // also might shortcut the case where + // lrat_chain is empty because clause is tautological + assert (lit != INT_MIN); + assume_analyze_literal (lit); + vector empty; + vector empty2; + constraint_chains.push_back (empty); + constraint_clauses.push_back (empty2); + for (auto ign : clause) { + constraint_clauses.back ().push_back (ign); + Flags &f = flags (ign); + const unsigned bit = bign (-ign); + if (!(f.failed & bit)) { + sum_constraints.push_back (ign); + assert (!(f.failed & bit)); + f.failed |= bit; + } + } + clause.clear (); + clear_analyzed_literals (); + for (auto p : lrat_chain) { + constraint_chains.back ().push_back (p); + } + lrat_chain.clear (); + } + for (auto &lit : sum_constraints) + clause.push_back (lit); + } + clear_analyzed_literals (); + + // Doing clause minimization here does not do anything because + // the clause already contains only one literal of each level + // and minimization can never reduce the number of levels + + VERBOSE (1, "found %zd failed assumptions %.0f%%", clause.size (), + percent (clause.size (), assumptions.size ())); + + // We do not actually need to learn this clause, since the conflict is + // forced already by some other clauses. There is also no bumping + // of variables nor clauses necessary. But we still want to check + // correctness of the claim that the determined subset of failing + // assumptions are a high-level core or equivalently their negations + // form a unit-implied clause. + // + if (!unsat_constraint) { + external->check_learned_clause (); + if (proof) { + vector eclause; + for (auto &lit : clause) + eclause.push_back (externalize (lit)); + proof->add_assumption_clause (++clause_id, eclause, lrat_chain); + conclusion.push_back (clause_id); + } + } else { + assert (!lrat || (constraint.size () == constraint_clauses.size () && + constraint.size () == constraint_chains.size ())); + for (auto p = constraint.rbegin (); p != constraint.rend (); p++) { + const auto &lit = *p; + if (lrat) { + clause.clear (); + for (auto &ign : constraint_clauses.back ()) + clause.push_back (ign); + constraint_clauses.pop_back (); + } + clause.push_back (-lit); + external->check_learned_clause (); + if (proof) { + if (lrat) { + for (auto p : constraint_chains.back ()) { + lrat_chain.push_back (p); + } + constraint_chains.pop_back (); + LOG (lrat_chain, "assume proof chain with constraints"); + } + vector eclause; + for (auto &lit : clause) + eclause.push_back (externalize (lit)); + proof->add_assumption_clause (++clause_id, eclause, lrat_chain); + conclusion.push_back (clause_id); + lrat_chain.clear (); + } + clause.pop_back (); + } + if (proof) { + for (auto &elit : econstraints) { + if (lrat) { + unsigned eidx = (elit > 0) + 2u * (unsigned) abs (elit); + assert ((size_t) eidx < external->ext_units.size ()); + const int64_t id = external->ext_units[eidx]; + if (id) { + lrat_chain.push_back (id); + } else { + int lit = external->e2i[abs (elit)]; + if (elit < 0) + lit = -lit; + int64_t id = unit_id (-lit); + lrat_chain.push_back (id); + } + } + proof->add_assumption_clause (++clause_id, -elit, lrat_chain); + conclusion.push_back (clause_id); + lrat_chain.clear (); + } + } + } + lrat_chain.clear (); + clause.clear (); + } + +DONE: + + STOP (analyze); +} + +bool Internal::failed (int lit) { + if (!marked_failed) { + if (!conflict_id) + failing (); + marked_failed = true; + } + conclude_unsat (); + Flags &f = flags (lit); + const unsigned bit = bign (lit); + return (f.failed & bit) != 0; +} + +void Internal::conclude_unsat () { + if (!proof || concluded) + return; + concluded = true; + if (!marked_failed) { + assert (conclusion.empty ()); + if (!conflict_id) + failing (); + marked_failed = true; + } + ConclusionType con; + if (conflict_id) + con = CONFLICT; + else if (unsat_constraint) + con = CONSTRAINT; + else + con = ASSUMPTIONS; + proof->conclude_unsat (con, conclusion); +} + +void Internal::reset_concluded () { + if (proof) + proof->reset_assumptions (); + if (concluded) { + LOG ("reset concluded"); + concluded = false; + } + if (conflict_id) { + assert (conclusion.size () == 1); + return; + } + conclusion.clear (); +} + +// Add the start of each incremental phase (leaving the state +// 'UNSATISFIABLE' actually) we reset all assumptions. + +void Internal::reset_assumptions () { + for (const auto &lit : assumptions) { + Flags &f = flags (lit); + const unsigned char bit = bign (lit); + f.assumed &= ~bit; + f.failed &= ~bit; + melt (lit); + } + LOG ("cleared %zd assumptions", assumptions.size ()); + assumptions.clear (); + marked_failed = true; +} + +struct sort_assumptions_positive_rank { + Internal *internal; + + // Decision level could be 'INT_MAX' and thus 'level + 1' could overflow. + // Therefore we carefully have to use 'unsigned' for levels below. + + const unsigned max_level; + + sort_assumptions_positive_rank (Internal *s) + : internal (s), max_level (s->level + 1u) {} + + typedef uint64_t Type; + + // Set assumptions first, then sorted by position on the trail + // unset literals are sorted by literal value. + + Type operator() (const int &a) const { + const int val = internal->val (a); + const bool assigned = (val != 0); + const Var &v = internal->var (a); + uint64_t res = (assigned ? (unsigned) v.level : max_level); + res <<= 32; + res |= (assigned ? v.trail : abs (a)); + return res; + } +}; + +struct sort_assumptions_smaller { + Internal *internal; + sort_assumptions_smaller (Internal *s) : internal (s) {} + bool operator() (const int &a, const int &b) const { + return sort_assumptions_positive_rank (internal) (a) < + sort_assumptions_positive_rank (internal) (b); + } +}; + +// Sort the assumptions by the current position on the trail and backtrack +// to the first place where the assumptions and the current trail differ. + +void Internal::sort_and_reuse_assumptions () { + assert (opts.ilb >= 1); + if (assumptions.empty ()) { + if (opts.ilb == 1) { + LOG ("no assumptions, reusing nothing (ilb == 1)"); + backtrack (0); + } else { // reuse full trail + LOG ("no assumptions, reusing everything (ilb == 2)"); + return; + } + } + MSORT (opts.radixsortlim, assumptions.begin (), assumptions.end (), + sort_assumptions_positive_rank (this), + sort_assumptions_smaller (this)); + + unsigned max_level = 0; + // assumptions are sorted by level, with unset at the end + for (auto lit : assumptions) { + if (val (lit)) + max_level = var (lit).level; + else + break; + } + + const unsigned size = min (level + 1u, max_level + 1); + assert ((size_t) level == control.size () - 1); + LOG (assumptions, "sorted assumptions"); + int target = 0; + for (unsigned i = 1, j = 0; i < size;) { + const Level &l = control[i]; + const int lit = l.decision; + const int alit = assumptions[j]; + const int lev = i; + target = lev; + if (val (alit) > 0 && + var (alit).level < lev) { // we can ignore propagated assumptions + LOG ("ILB skipping propagation %d", alit); + ++j; + continue; + } + if (!lit) { // skip fake decisions + target = lev - 1; + break; + } + ++i, ++j; + assert (var (lit).level == lev); + if (l.decision == alit) { + continue; + } + target = lev - 1; + LOG ("first different literal %d on the trail and %d from the " + "assumptions", + lit, alit); + break; + } + if (opts.ilb == 1 && + (size_t) target > assumptions.size ()) // reusing only assumptions + target = assumptions.size (); + if (target < level) + backtrack_without_updating_phases (target); + LOG ("assumptions allow for reuse of trail up to level %d", level); + if ((size_t) level > assumptions.size ()) + stats.assumptionsreused += assumptions.size (); + else + stats.assumptionsreused += level; +} +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/averages.cpp b/vendor/rustsat-cadical/cppsrc/src/averages.cpp new file mode 100644 index 0000000..971598b --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/averages.cpp @@ -0,0 +1,34 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +void Internal::init_averages () { + + LOG ("initializing averages"); + + INIT_EMA (averages.current.jump, opts.emajump); + INIT_EMA (averages.current.level, opts.emalevel); + INIT_EMA (averages.current.size, opts.emasize); + + INIT_EMA (averages.current.glue.fast, opts.emagluefast); + INIT_EMA (averages.current.glue.slow, opts.emaglueslow); + + INIT_EMA (averages.current.decisions, opts.emadecisions); + + INIT_EMA (averages.current.trail.fast, opts.ematrailfast); + INIT_EMA (averages.current.trail.slow, opts.ematrailslow); + + assert (!averages.swapped); +} + +void Internal::swap_averages () { + LOG ("saving current averages"); + swap (averages.current, averages.saved); + if (!averages.swapped) + init_averages (); + else + LOG ("swapping in previously saved averages"); + averages.swapped++; +} + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/averages.hpp b/vendor/rustsat-cadical/cppsrc/src/averages.hpp new file mode 100644 index 0000000..9f25950 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/averages.hpp @@ -0,0 +1,37 @@ +#ifndef _averages_hpp_INCLUDED +#define _averages_hpp_INCLUDED + +#include "ema.hpp" // alphabetically after 'averages.hpp' + +namespace CaDiCaL { + +struct Averages { + + int64_t swapped; + + struct { + + struct { + EMA fast; // average fast (small window) moving glucose level + EMA slow; // average slow (large window) moving glucose level + } glue; + + struct { + EMA fast; // average fast (small window) moving trail level + EMA slow; // average slow (large window) moving trail level + } trail; + + EMA decisions; + + EMA size; // average learned clause size + EMA jump; // average (potential non-chronological) back-jump level + EMA level; // average back track level after conflict + + } current, saved; + + Averages () : swapped (0) {} +}; + +} // namespace CaDiCaL + +#endif diff --git a/vendor/rustsat-cadical/cppsrc/src/backbone.cpp b/vendor/rustsat-cadical/cppsrc/src/backbone.cpp new file mode 100644 index 0000000..ad87109 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/backbone.cpp @@ -0,0 +1,631 @@ +#include "internal.hpp" +#include "message.hpp" +#include "util.hpp" + +namespace CaDiCaL { + +inline void Internal::backbone_lrat_for_units (int lit, Clause *reason) { + if (!lrat) + return; + if (level) + return; // not decision level 0 + LOG ("backbone building chain for units"); + assert (lrat_chain.empty ()); + assert (reason); + for (auto &reason_lit : *reason) { + if (lit == reason_lit) + continue; + assert (val (reason_lit)); + if (!val (reason_lit)) + continue; + const int signed_reason_lit = val (reason_lit) * reason_lit; + int64_t id = unit_id (signed_reason_lit); + lrat_chain.push_back (id); + } + lrat_chain.push_back (reason->id); +} + +inline bool Internal::backbone_propagate (int64_t &ticks) { + require_mode (BACKBONE); + assert (!unsat); + START (propagate); + int64_t before = propagated2 = propagated; + for (;;) { + if (propagated2 != trail.size ()) { + const int lit = -trail[propagated2++]; + LOG ("backbone propagating %d over binary clauses", -lit); + Watches &ws = watches (lit); + ticks += + 1 + cache_lines (ws.size (), sizeof (const_watch_iterator *)); + for (const auto &w : ws) { + if (!w.binary ()) + continue; + const signed char b = val (w.blit); + if (b > 0) + continue; + if (b < 0) + conflict = w.clause; // but continue + else { + ticks++; + build_chain_for_units (w.blit, w.clause, 0); + backbone_assign (w.blit, w.clause); + lrat_chain.clear (); + } + } + } else if (!conflict && propagated != trail.size ()) { + const int lit = -trail[propagated++]; + LOG ("backbone propagating %d over large clauses", -lit); + Watches &ws = watches (lit); + const const_watch_iterator eow = ws.end (); + const_watch_iterator i = ws.begin (); + ticks += 1 + cache_lines (ws.size (), sizeof (*i)); + watch_iterator j = ws.begin (); + while (i != eow) { + const Watch w = *j++ = *i++; + if (w.binary ()) + continue; + if (val (w.blit) > 0) + continue; + ticks++; + if (w.clause->garbage) { + j--; + continue; + } + literal_iterator lits = w.clause->begin (); + const int other = lits[0] ^ lits[1] ^ lit; + const signed char u = val (other); + if (u > 0) + j[-1].blit = other; + else { + const int size = w.clause->size; + const const_literal_iterator end = lits + size; + const literal_iterator middle = lits + w.clause->pos; + literal_iterator k = middle; + signed char v = -1; + int r = 0; + while (k != end && (v = val (r = *k)) < 0) + k++; + if (v < 0) { + k = lits + 2; + assert (w.clause->pos <= size); + while (k != middle && (v = val (r = *k)) < 0) + k++; + } + w.clause->pos = k - lits; + assert (lits + 2 <= k), assert (k <= w.clause->end ()); + if (v > 0) + j[-1].blit = r; + else if (!v) { + LOG (w.clause, "unwatch %d in", r); + lits[0] = other; + lits[1] = r; + *k = lit; + ticks++; + watch_literal (r, lit, w.clause); + j--; + } else if (!u) { + ticks++; + assert (v < 0); + build_chain_for_units (other, w.clause, 0); + backbone_assign_any (other, w.clause); + lrat_chain.clear (); + } else { + if (w.clause == ignore) { + LOG ("ignoring conflict due to clause to vivify"); + continue; + } + assert (u < 0); + assert (v < 0); + conflict = w.clause; + break; + } + } + } + if (j != i) { + while (i != eow) + *j++ = *i++; + ws.resize (j - ws.begin ()); + } + } else + break; + } + int64_t delta = propagated2 - before; + stats.propagations.backbone += delta; + if (conflict) + LOG (conflict, "conflict"); + STOP (propagate); + return !conflict; +} + +inline void Internal::backbone_propagate2 (int64_t &ticks) { + require_mode (BACKBONE); + assert (propagated2 <= trail.size ()); + int64_t before = propagated2; + while (propagated2 != trail.size ()) { + const int lit = -trail[propagated2++]; + LOG ("probe propagating %d over binary clauses", -lit); + Watches &ws = watches (lit); + ticks += 1 + cache_lines (ws.size (), sizeof (const_watch_iterator *)); + for (const auto &w : ws) { + if (!w.binary ()) + break; + const signed char b = val (w.blit); + if (b > 0) + continue; + ticks++; + if (b < 0) { + conflict = w.clause; // no need to continue + break; + } else { + assert (lrat_chain.empty ()); + backbone_lrat_for_units (w.blit, w.clause); + backbone_assign (w.blit, w.clause); + lrat_chain.clear (); + } + } + } + + int64_t delta = propagated2 - before; + stats.propagations.backbone += delta; +} + +void Internal::schedule_backbone_cands (std::vector &candidates) { + + unsigned not_rescheduled = 0; + for (auto v : vars) { + const Flags f = flags (v); + if (!f.active ()) + continue; + if (f.backbone0) { + LOG ("scheduling backbone literal candidate %s", LOGLIT (v)); + candidates.push_back (v); + } else + ++not_rescheduled; + if (f.backbone1) { + LOG ("scheduling backbone literal candidate %s", LOGLIT (-v)); + candidates.push_back (-v); + } else + ++not_rescheduled; + } + + if (not_rescheduled) { + for (auto v : vars) { + const Flags f = flags (v); + if (!f.active ()) + continue; + if (!f.backbone0) { + LOG ("scheduling backbone literal candidate %s", LOGLIT (v)); + candidates.push_back (v); + } + if (!f.backbone1) { + LOG ("scheduling backbone literal candidate %s", LOGLIT (-v)); + candidates.push_back (-v); + } + } + } + assert (candidates.size () <= 2 * (size_t) max_var); + + VERBOSE (3, + "backbone schedule %zu backbone candidates in total %f " + "(rescheduled: %f%%)", + candidates.size (), percent (candidates.size (), 2 * max_var), + percent (not_rescheduled, 2 * max_var)); +} + +int Internal::backbone_analyze (Clause *, int64_t &ticks) { + assert (conflict); + assert (conflict->size == 2); + analyzed.push_back (std::abs (conflict->literals[0])); + flags (conflict->literals[0]).seen = true; + analyzed.push_back (std::abs (conflict->literals[1])); + flags (conflict->literals[1]).seen = true; + LOG (conflict, "analyzing conflict"); + if (lrat) + lrat_chain.push_back (conflict->id); + conflict = nullptr; + + for (auto t = trail.rbegin ();;) { + assert (t < trail.rend ()); + int lit = *t++; + LOG ("analyzing %s", LOGLIT (lit)); + if (!flags (lit).seen) + continue; + Clause *reason = var (lit).reason; + LOG (reason, "resolving with reason of %s", LOGLIT (lit)); + assert (reason), assert (reason != decision_reason); + ++ticks; + const int other = reason->literals[0] ^ reason->literals[1] ^ lit; + Flags &f_o = flags (other); + if (lrat) + lrat_chain.push_back (reason->id); + if (!f_o.seen) { + f_o.seen = true; + analyzed.push_back (other); + } else { + LOG ("backbone UIP %s", LOGLIT (other)); + for (auto lit : analyzed) + flags (lit).seen = false; + analyzed.clear (); + if (lrat) + reverse (begin (lrat_chain), end (lrat_chain)); + return other; + } + } +} + +inline void Internal::backbone_unit_reassign (int lit) { +#ifdef LOGGING + LOG ("reassigning %s to level 0", LOGLIT (lit)); + assert (val (lit) > 0); + assert (val (-lit) < 0); +#else + (void) lit; +#endif + return; +} + +inline void Internal::backbone_unit_assign (int lit) { + LOG ("assigning %s to level 0", LOGLIT (lit)); + require_mode (BACKBONE); + const int idx = vidx (lit); + assert (!vals[idx]); + Var &v = var (idx); + v.level = 0; // required to reuse decisions + v.trail = (int) trail.size (); // used in 'vivify_better_watch' + assert ((int) num_assigned < max_var); + num_assigned++; + v.reason = 0; // for conflict analysis + learn_unit_clause (lit); + lrat_chain.clear (); + const signed char tmp = sign (lit); + vals[idx] = tmp; + vals[-idx] = -tmp; + assert (val (lit) > 0); + assert (val (-lit) < 0); + trail.push_back (lit); + LOG ("backbone assign %d to level 0", lit); +} + +inline void Internal::backbone_assign_any (int lit, Clause *reason) { + require_mode (BACKBONE); + const int idx = vidx (lit); + assert (!vals[idx]); + assert (!flags (idx).eliminated () || !reason); + assert (reason == decision_reason || !reason || reason->size >= 2); + Var &v = var (idx); + v.level = level; // required to reuse decisions + v.trail = (int) trail.size (); // used in 'vivify_better_watch' + assert ((int) num_assigned < max_var); + num_assigned++; + v.reason = level ? reason : 0; // for conflict analysis + if (!level) + learn_unit_clause (lit); + const signed char tmp = sign (lit); + vals[idx] = tmp; + vals[-idx] = -tmp; + assert (val (lit) > 0); + assert (val (-lit) < 0); + trail.push_back (lit); + LOG (reason, "backbone assign %d", lit); +} + +inline void Internal::backbone_assign (int lit, Clause *reason) { + require_mode (BACKBONE); + const int idx = vidx (lit); + assert (!vals[idx]); + assert (!flags (idx).eliminated () || !reason); + assert (reason == decision_reason || !reason || reason->size == 2); + Var &v = var (idx); + v.level = level; // required to reuse decisions + v.trail = (int) trail.size (); // used in 'vivify_better_watch' + assert ((int) num_assigned < max_var); + num_assigned++; + v.reason = level ? reason : 0; // for conflict analysis + if (!level) + learn_unit_clause (lit); + const signed char tmp = sign (lit); + vals[idx] = tmp; + vals[-idx] = -tmp; + assert (val (lit) > 0); + assert (val (-lit) < 0); + trail.push_back (lit); + LOG (reason, "backbone assign %d", lit); +} + +void Internal::backbone_decision (int lit) { + require_mode (BACKBONE); + assert (propagated2 == trail.size ()); + new_trail_level (lit); + notify_decision (); + LOG ("search decide %d", lit); + backbone_assign (lit, decision_reason); +} + +unsigned Internal::compute_backbone_round (std::vector &candidates, + std::vector &units, + const int64_t ticks_limit, + int64_t &ticks, + unsigned inconsistent) { + assert (!conflict); + auto p = begin (candidates); + auto q = p; + const auto end = std::end (candidates); + size_t failed = 0; + ++stats.backbone.rounds; + + LOG (candidates, "candidates: "); + ticks += 1 + cache_lines (candidates.size (), + sizeof (std::vector::iterator *)); + while (p != end) { + assert (p < end); + assert (q <= p); + assert (!conflict); + const int probe = (*q = *p); + ++stats.backbone.probes; + + ++p, ++q; + const signed char v = val (probe); + if (v > 0) { + q--; + LOG ("removing satisfied backbone probe %s", LOGLIT (probe)); + if (probe < 0) + flags (probe).backbone1 = false; + else + flags (probe).backbone0 = false; + continue; + } + + if (v < 0) { + if (var (probe).level) + LOG ("skipping falsified backbone probe %s", LOGLIT (probe)); + else { + LOG ("removing root-level falsified backbone probe %s", + LOGLIT (probe)); + q--; + } + continue; + } + if (ticks >= ticks_limit) + break; + backbone_decision (probe); + backbone_propagate2 (ticks); + if (!conflict) { + LOG (candidates, + "propagating backbone probe %s successful; candidates:", + LOGLIT (probe)); + continue; + } + + ++failed; + ++stats.backbone.units; + int uip = backbone_analyze (conflict, ticks); + backtrack_without_updating_phases (level - 1); + backbone_unit_assign (uip); + ++stats.units; + assert (!conflict); + if (external->learner) + external->export_learned_unit_clause (uip); + + backbone_propagate2 (ticks); + if (conflict) { + LOG ("propagating backbone forced %s failed", LOGLIT (uip)); + inconsistent = uip; + // we have to give up on the current conflict + conflict = nullptr; + break; + } + units.push_back (uip); + } + while (p != end) + *q++ = *p++; + assert (q <= end); + candidates.resize (q - begin (candidates)); + LOG (candidates, "candidates: "); + + if (!inconsistent) { + LOG ("flushing satisfied probe candidates"); + auto p = begin (candidates); + auto q = p; + const auto end = std::end (candidates); + + while (p != end) { + const int probe = (*q++ = *p++); + const signed char v = val (probe); + if (v > 0) { + q--; + LOG ("removing satisfied backbone probe %s", LOGLIT (probe)); + if (probe < 0) + flags (probe).backbone1 = false; + else + flags (probe).backbone0 = false; + continue; + } + + if (v < 0) { + LOG ("keeping falsified probe %s", LOGLIT (probe)); + continue; + } + assert (!v); + LOG ("keeping unassigned probe %s", LOGLIT (probe)); + } + candidates.resize (q - begin (candidates)); + } + LOG (candidates, "candidates after !inconsistent: "); + if (level) + backtrack_without_updating_phases (); + if (!inconsistent && !units.empty ()) { + for (auto l : units) { + backbone_unit_reassign (l); + } + units.clear (); + if (!backbone_propagate (ticks)) { + LOG (conflict, "final repropagation yielded conflict"); + learn_empty_clause (); + } + } else { + if (!backbone_propagate (ticks)) { + learn_empty_clause (); + } + } + LOG (candidates, "candidates end of loop: "); + + LOG (candidates, "candidates end of backbone_round: "); + return failed; +} + +void Internal::keep_backbone_candidates ( + const std::vector &candidates) { + size_t remain = 0; + size_t prioritized = 0; + for (auto v : candidates) { + const Flags &f = flags (v); + if (!f.active ()) + continue; + ++remain; + if (v < 0) + prioritized += f.backbone1; + else + prioritized += f.backbone0; + } + assert (prioritized <= remain); + if (!remain) { +#ifndef NDEBUG + for (auto v : candidates) { + const Flags &f = flags (v); + if (!f.active ()) + continue; + assert (!f.backbone0); + assert (!f.backbone1); + } +#endif + return; + } + if (prioritized == remain) { + LOG ("keeping all remaining backbones"); + } else if (!prioritized) { + for (auto v : candidates) { + Flags &f = flags (v); + if (!f.active ()) + continue; + ++remain; + if (v < 0) { + assert (!f.backbone1); + f.backbone1 = true; + } else { + assert (!f.backbone0); + f.backbone0 = true; + } + } + } +} + +unsigned Internal::compute_backbone () { + size_t failed = 0; + + int64_t ticks = 0; + backbone_propagate2 (ticks); + assert (!conflict); + + std::vector candidates, units; + unsigned inconsistent = 0; + assert (!conflict); + + ++stats.backbone.phases; + schedule_backbone_cands (candidates); + + const size_t max_rounds = opts.backbonemaxrounds; + size_t round_limit = opts.backbonerounds; + round_limit *= stats.backbone.phases; + if (round_limit > max_rounds) + round_limit = max_rounds; + + SET_EFFORT_LIMIT (totalticks, backbone, false); + int64_t ticks_limit = totalticks - stats.ticks.backbone; + PHASE ("backbone", stats.backbone.phases, + "backbone limit of %" PRId64 " ticks", ticks_limit); + size_t rounds = 0; + for (; ++rounds;) { + if (rounds >= round_limit) { + LOG ("backround round limit %zu rounds", rounds); + break; + } + if (ticks >= ticks_limit) { + LOG ("backround round limit %" PRIu64 " ticks", ticks); + break; + } + VERBOSE (3, + "backbone round %zu of %zu with %" PRId64 + " ticks (%f %% done) with %zu failed so far", + rounds, max_rounds, ticks, percent (ticks, ticks_limit), + failed); + size_t new_failed = compute_backbone_round ( + candidates, units, ticks_limit, ticks, inconsistent); + failed += new_failed; + if (inconsistent) + break; + if (candidates.empty ()) + break; + if (unsat) + break; + } + + if (inconsistent && !unsat) { + LOG ("using forced unit %s by repropagating at level 0", + LOGLIT (inconsistent)); + backtrack_without_updating_phases (); + propagate (); + learn_empty_clause (); + } + if (unsat) { + PHASE ("backbone", stats.backbone.phases, + "inconsistent binary clauses"); + } else { + PHASE ("backbone", stats.backbone.phases, + "found %zu backbone literals %zu round in %" PRId64 " ticks", + failed, rounds, ticks); + } + + keep_backbone_candidates (candidates); + if (level) { + backtrack_without_updating_phases (); + if (!backbone_propagate (ticks)) { + learn_empty_clause (); + } + } + stats.ticks.backbone += ticks; + return failed; +} + +void Internal::binary_clauses_backbone () { + if (unsat) + return; + if (!opts.backbone) + return; + if (level) + backtrack_without_updating_phases (); + propagated2 = 0; // TODO: why? + if (!propagate ()) { + LOG ("propagation after connecting watches in inconsistency"); + learn_empty_clause (); + return; + } + + for (auto lit : lits) { + Watches &w = watches (lit); + std::stable_partition (begin (w), end (w), + [] (Watch w) { return w.binary (); }); + } + assert (propagated2 <= trail.size ()); + private_steps = true; + + assert (watching ()); + START_SIMPLIFIER (backbone, BACKBONE); + int failed = compute_backbone (); + assert (!level); + private_steps = false; + + report ('k', !failed); + STOP_SIMPLIFIER (backbone, BACKBONE); +} + +} // namespace CaDiCaL \ No newline at end of file diff --git a/vendor/rustsat-cadical/cppsrc/src/backtrack.cpp b/vendor/rustsat-cadical/cppsrc/src/backtrack.cpp new file mode 100644 index 0000000..2fb89a2 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/backtrack.cpp @@ -0,0 +1,177 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +// The global assignment stack can only be (partially) reset through +// 'backtrack' which is the only function using 'unassign' (inlined and thus +// local to this file). It turns out that 'unassign' does not need a +// specialization for 'probe' nor 'vivify' and thus it is shared. + +inline void Internal::unassign (int lit) { + assert (val (lit) > 0); + set_val (lit, 0); + + int idx = vidx (lit); + LOG ("unassign %d @ %d", lit, var (idx).level); + num_assigned--; + + // In the standard EVSIDS variable decision heuristic of MiniSAT, we need + // to push variables which become unassigned back to the heap. + // + if (!scores.contains (idx)) + scores.push_back (idx); + + // For VMTF we need to update the 'queue.unassigned' pointer in case this + // variable sits after the variable to which 'queue.unassigned' currently + // points. See our SAT'15 paper for more details on this aspect. + // + if (queue.bumped < btab[idx]) + update_queue_unassigned (idx); +} + +/*------------------------------------------------------------------------*/ + +// Update the current target maximum assignment and also the very best +// assignment. Whether a trail produces a conflict is determined during +// propagation. Thus that all functions in the 'search' loop after +// propagation can assume that 'no_conflict_until' is valid. If a conflict +// is found then the trail before the last decision is used (see the end of +// 'propagate'). During backtracking we can then save this largest +// propagation conflict free assignment. It is saved as both 'target' +// assignment for picking decisions in 'stable' mode and if it is the +// largest ever such assignment also as 'best' assignment. This 'best' +// assignment can then be used in future stable decisions after the next +// 'rephase_best' overwrites saved phases with it. + +void Internal::update_target_and_best () { + + if (opts.rephase == 2 && !stable) + return; + + bool reset = (rephased && stats.conflicts > last.rephase.conflicts); + + if (reset) { + target_assigned = 0; + if (rephased == 'B') + best_assigned = 0; // update it again + } + + if (no_conflict_until > target_assigned) { + copy_phases (phases.target); + target_assigned = no_conflict_until; + LOG ("new target trail level %zu", target_assigned); + } + + if (no_conflict_until > best_assigned) { + copy_phases (phases.best); + best_assigned = no_conflict_until; + LOG ("new best trail level %zu", best_assigned); + } + + if (reset) { + report (rephased); + rephased = 0; + } +} + +/*------------------------------------------------------------------------*/ + +void Internal::backtrack (int new_level) { + assert (new_level <= level); + if (new_level == level) + return; + + update_target_and_best (); + backtrack_without_updating_phases (new_level); +} + +void Internal::backtrack_without_updating_phases (int new_level) { + + assert (new_level <= level); + if (new_level == level) + return; + + stats.backtracks++; + + assert (num_assigned == trail.size ()); + + const size_t assigned = control[new_level + 1].trail; + + LOG ("backtracking to decision level %d with decision %d and trail %zd", + new_level, control[new_level].decision, assigned); + + const size_t end_of_trail = trail.size (); + size_t i = assigned, j = i; + +#ifdef LOGGING + int unassigned = 0; +#endif + int reassigned = 0; + + notify_backtrack (new_level); + if (external_prop && !external_prop_is_lazy && !private_steps && + notified > assigned) { + LOG ("external propagator is notified about some unassignments (trail: " + "%zd, notified: %zd).", + trail.size (), notified); + notified = assigned; + } + + while (i < end_of_trail) { + int lit = trail[i++]; + Var &v = var (lit); + if (v.level > new_level) { + unassign (lit); +#ifdef LOGGING + unassigned++; +#endif + } else { + // This is the essence of the SAT'18 paper on chronological + // backtracking. It is possible to just keep out-of-order assigned + // literals on the trail without breaking the solver (after some + // modifications to 'analyze' - see 'opts.chrono' guarded code there). + assert ((in_mode (BACKBONE)) || opts.chrono || external_prop || + did_external_prop); +#ifdef LOGGING + if (!v.level) + LOG ("reassign %d @ 0 unit clause %d", lit, lit); + else + LOG (v.reason, "reassign %d @ %d", lit, v.level); +#endif + trail[j] = lit; + v.trail = j++; + reassigned++; + } + } + trail.resize (j); + LOG ("unassigned %d literals %.0f%%", unassigned, + percent (unassigned, unassigned + reassigned)); + LOG ("reassigned %d literals %.0f%%", reassigned, + percent (reassigned, unassigned + reassigned)); + + if (propagated > assigned) + propagated = assigned; + if (propagated2 > assigned) + propagated2 = assigned; + if (no_conflict_until > assigned) + no_conflict_until = assigned; + + propergated = 0; // Always go back to root-level. + + assert (notified <= assigned + reassigned); + if (reassigned) { + notify_assignments (); + } + + control.resize (new_level + 1); + level = new_level; + if (changed_val) { + assert (opts.ilb); + if (!val (changed_val)) { + changed_val = 0; + } + } + assert (num_assigned == trail.size ()); +} + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/backward.cpp b/vendor/rustsat-cadical/cppsrc/src/backward.cpp new file mode 100644 index 0000000..e7228c5 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/backward.cpp @@ -0,0 +1,231 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +/*------------------------------------------------------------------------*/ + +// Provide eager backward subsumption for resolved clauses. + +// The eliminator maintains a queue of clauses that are new and have to be +// checked to subsume or strengthen other (longer or same size) clauses. + +void Eliminator::enqueue (Clause *c) { + if (!internal->opts.elimbackward) + return; + if (c->enqueued) + return; + LOG (c, "backward enqueue"); + backward.push (c); + c->enqueued = true; +} + +Clause *Eliminator::dequeue () { + if (backward.empty ()) + return 0; + Clause *res = backward.front (); + backward.pop (); + assert (res->enqueued); + res->enqueued = false; + LOG (res, "backward dequeue"); + return res; +} + +Eliminator::~Eliminator () { + while (dequeue ()) + ; +} + +/*------------------------------------------------------------------------*/ + +void Internal::elim_backward_clause (Eliminator &eliminator, Clause *c) { + assert (opts.elimbackward); + assert (!c->redundant); + if (c->garbage) + return; + LOG (c, "attempting backward subsumption and strengthening with"); + size_t len = UINT_MAX; + unsigned size = 0; + int best = 0; + bool satisfied = false; + assert (mini_chain.empty ()); + for (const auto &lit : *c) { + const signed char tmp = val (lit); + if (tmp > 0) { + satisfied = true; + break; + } + if (tmp < 0) + continue; + size_t l = occs (lit).size (); + LOG ("literal %d occurs %zd times", lit, l); + if (l < len) + best = lit, len = l; + mark (lit); + size++; + } + if (satisfied) { + LOG ("clause actually already satisfied"); + elim_update_removed_clause (eliminator, c); + mark_garbage (c); + } else if (len > (size_t) opts.elimocclim) { + LOG ("skipping backward subsumption due to too many occurrences"); + } else { + assert (len); + LOG ("literal %d has smallest number of occurrences %zd", best, len); + LOG ("marked %d literals in clause of size %d", size, c->size); + for (auto &d : occs (best)) { + if (d == c) + continue; + if (d->garbage) + continue; + if ((unsigned) d->size < size) + continue; + int negated = 0; + unsigned found = 0; + satisfied = false; + for (const auto &lit : *d) { + signed char tmp = val (lit); + if (tmp > 0) { + satisfied = true; + break; + } + if (tmp < 0) + continue; + tmp = marked (lit); + if (!tmp) + continue; + if (tmp < 0) { + if (negated) { + size = UINT_MAX; + break; + } else + negated = lit; + } + if (++found == size) + break; + } + if (satisfied) { + LOG (d, "found satisfied clause"); + elim_update_removed_clause (eliminator, d); + mark_garbage (d); + } else if (found == size) { + if (!negated) { + LOG (d, "found subsumed clause"); + elim_update_removed_clause (eliminator, d); + mark_garbage (d); + stats.subsumed++; + stats.elimbwsub++; + } else { + int unit = 0; + assert (minimize_chain.empty ()); + assert (analyzed.empty ()); + assert (lrat_chain.empty ()); + // figure out wether we strengthen c or get a new unit + for (const auto &lit : *d) { + const signed char tmp = val (lit); + if (tmp < 0) { + if (!lrat) + continue; + Flags &f = flags (lit); + assert (!f.seen); + if (f.seen) + continue; + f.seen = true; + analyzed.push_back (lit); + continue; + } + if (tmp > 0) { + satisfied = true; + break; + } + if (lit == negated) + continue; + if (unit) { + unit = INT_MIN; + continue; // needed to guarantee d is not satsified + } else + unit = lit; + } + if (lrat && !satisfied) { + // if we found a unit we need to add all unit ids from + // {c\d}U{d\c} otherwise just the unit ids from {c\d} + for (const auto &lit : *c) { + const signed char tmp = val (lit); + assert (tmp <= 0); + if (tmp >= 0) + continue; + Flags &f = flags (lit); + if (f.seen && unit && unit == INT_MIN) { + f.seen = false; + continue; + } else if (!f.seen) { + f.seen = true; + analyzed.push_back (lit); + } + } + if (unit == INT_MIN) { // we do not need units from {d\c} + for (const auto &lit : *d) { + flags (lit).seen = false; + } + } + for (const auto &lit : analyzed) { + Flags &f = flags (lit); + if (!f.seen) { + f.seen = true; + continue; + } + int64_t id = unit_id (-lit); + lrat_chain.push_back (id); + } + clear_analyzed_literals (); + lrat_chain.push_back (d->id); + lrat_chain.push_back (c->id); + } else if (lrat) + clear_analyzed_literals (); + if (satisfied) { + assert (lrat_chain.empty ()); + mark_garbage (d); + elim_update_removed_clause (eliminator, d); + } else if (unit && unit != INT_MIN) { + assert (unit); + LOG (d, "unit %d through hyper unary resolution with", unit); + assign_unit (unit); + elim_propagate (eliminator, unit); + lrat_chain.clear (); + break; + } else if (occs (negated).size () <= (size_t) opts.elimocclim) { + strengthen_clause (d, negated); + remove_occs (occs (negated), d); + elim_update_removed_lit (eliminator, negated); + stats.elimbwstr++; + assert (negated != best); + eliminator.enqueue (d); + } + lrat_chain.clear (); + } + } + } + } + mini_chain.clear (); + unmark (c); +} + +/*------------------------------------------------------------------------*/ + +void Internal::elim_backward_clauses (Eliminator &eliminator) { + if (!opts.elimbackward) { + assert (eliminator.backward.empty ()); + return; + } + START (backward); + LOG ("attempting backward subsumption and strengthening with %zd clauses", + eliminator.backward.size ()); + Clause *c; + while (!unsat && (c = eliminator.dequeue ())) + elim_backward_clause (eliminator, c); + STOP (backward); +} + +/*------------------------------------------------------------------------*/ + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/bins.cpp b/vendor/rustsat-cadical/cppsrc/src/bins.cpp new file mode 100644 index 0000000..406074e --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/bins.cpp @@ -0,0 +1,22 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +/*------------------------------------------------------------------------*/ + +// Binary implication graph lists. + +void Internal::init_bins () { + assert (big.empty ()); + if (big.size () < 2 * vsize) + big.resize (2 * vsize, Bins ()); + LOG ("initialized binary implication graph"); +} + +void Internal::reset_bins () { + assert (!big.empty ()); + erase_vector (big); + LOG ("reset binary implication graph"); +} + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/bins.hpp b/vendor/rustsat-cadical/cppsrc/src/bins.hpp new file mode 100644 index 0000000..ec0f0f1 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/bins.hpp @@ -0,0 +1,22 @@ +#ifndef _bins_hpp_INCLUDED +#define _bins_hpp_INCLUDED + +#include "util.hpp" // Alphabetically after 'bins'. + +namespace CaDiCaL { + +using namespace std; + +struct Bin { + int lit; + int64_t id; +}; + +typedef vector Bins; + +inline void shrink_bins (Bins &bs) { shrink_vector (bs); } +inline void erase_bins (Bins &bs) { erase_vector (bs); } + +} // namespace CaDiCaL + +#endif diff --git a/vendor/rustsat-cadical/cppsrc/src/block.cpp b/vendor/rustsat-cadical/cppsrc/src/block.cpp new file mode 100644 index 0000000..11721d8 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/block.cpp @@ -0,0 +1,824 @@ +#include "internal.hpp" + +namespace CaDiCaL { + +/*------------------------------------------------------------------------*/ + +// This implements an inprocessing version of blocked clause elimination and +// is assumed to be triggered just before bounded variable elimination. It +// has a separate 'block' flag while variable elimination uses 'elim'. +// Thus it only tries to block clauses on a literal which was removed in an +// irredundant clause in negated form before and has not been tried to use +// as blocking literal since then. + +/*------------------------------------------------------------------------*/ + +inline bool block_more_occs_size::operator() (unsigned a, unsigned b) { + size_t s = internal->noccs (-internal->u2i (a)); + size_t t = internal->noccs (-internal->u2i (b)); + if (s > t) + return true; + if (s < t) + return false; + s = internal->noccs (internal->u2i (a)); + t = internal->noccs (internal->u2i (b)); + if (s > t) + return true; + if (s < t) + return false; + return a > b; +} + +/*------------------------------------------------------------------------*/ + +// Determine whether 'c' is blocked on 'lit', by first marking all its +// literals and then checking all resolvents with negative clauses (with +// '-lit') are tautological. We use a move-to-front scheme for both the +// occurrence list of negative clauses (with '-lit') and then for literals +// within each such clause. The clause move-to-front scheme has the goal to +// find non-tautological clauses faster in the future, while the literal +// move-to-front scheme has the goal to faster find the matching literal, +// which makes the resolvent tautological (again in the future). + +bool Internal::is_blocked_clause (Clause *c, int lit) { + + LOG (c, "trying to block on %d", lit); + + assert (c->size >= opts.blockminclslim); + assert (c->size <= opts.blockmaxclslim); + assert (active (lit)); + assert (!val (lit)); + assert (!c->garbage); + assert (!c->redundant); + assert (!level); + + mark (c); // First mark all literals in 'c'. + + Occs &os = occs (-lit); + LOG ("resolving against at most %zd clauses with %d", os.size (), -lit); + + bool res = true; // Result is true if all resolvents tautological. + + // Can not use 'auto' here since we update 'os' during traversal. + // + const auto end_of_os = os.end (); + auto i = os.begin (); + + Clause *prev_d = 0; // Previous non-tautological clause. + + for (; i != end_of_os; i++) { + // Move the first clause with non-tautological resolvent to the front of + // the occurrence list to improve finding it faster later. + // + Clause *d = *i; + + assert (!d->garbage); + assert (!d->redundant); + assert (d->size <= opts.blockmaxclslim); + + *i = prev_d; // Move previous non-tautological clause + prev_d = d; // backwards but remember clause at this position. + + LOG (d, "resolving on %d against", lit); + stats.blockres++; + + int prev_other = 0; // Previous non-tautological literal. + + // No 'auto' since we update literals of 'd' during traversal. + // + const const_literal_iterator end_of_d = d->end (); + literal_iterator l; + + for (l = d->begin (); l != end_of_d; l++) { + // Same move-to-front mechanism for literals within a clause. It + // moves the first negatively marked literal to the front to find it + // faster in the future. + // + const int other = *l; + *l = prev_other; + prev_other = other; + if (other == -lit) + continue; + assert (other != lit); + assert (active (other)); + assert (!val (other)); + if (marked (other) < 0) { + LOG ("found tautological literal %d", other); + d->literals[0] = other; // Move to front of 'd'. + break; + } + } + + if (l == end_of_d) { + LOG ("no tautological literal found"); + // + // Since we did not find a tautological literal we restore the old + // order of literals in the clause. + // + const const_literal_iterator begin_of_d = d->begin (); + while (l-- != begin_of_d) { + const int other = *l; + *l = prev_other; + prev_other = other; + } + res = false; // Now 'd' is a witness that 'c' is not blocked. + os[0] = d; // Move it to the front of the occurrence list. + break; + } + } + unmark (c); // ... all literals of the candidate clause. + + // If all resolvents are tautological and thus the clause is blocked we + // restore the old order of clauses in the occurrence list of '-lit'. + // + if (res) { + assert (i == end_of_os); + const auto boc = os.begin (); + while (i != boc) { + Clause *d = *--i; + *i = prev_d; + prev_d = d; + } + } + + return res; +} + +/*------------------------------------------------------------------------*/ + +void Internal::block_schedule (Blocker &blocker) { + // Set skip flags for all literals in too large clauses. + // + for (const auto &c : clauses) { + + if (c->garbage) + continue; + if (c->redundant) + continue; + if (c->size <= opts.blockmaxclslim) + continue; + + for (const auto &lit : *c) + mark_skip (-lit); + } + + // Connect all literal occurrences in irredundant clauses. + // + for (const auto &c : clauses) { + + if (c->garbage) + continue; + if (c->redundant) + continue; + + for (const auto &lit : *c) { + assert (active (lit)); + assert (!val (lit)); + occs (lit).push_back (c); + } + } + + // We establish the invariant that 'noccs' gives the number of actual + // occurrences of 'lit' in non-garbage clauses, while 'occs' might still + // refer to garbage clauses, thus 'noccs (lit) <= occs (lit).size ()'. It + // is expensive to remove references to garbage clauses from 'occs' during + // blocked clause elimination, but decrementing 'noccs' is cheap. + + for (auto lit : lits) { + if (!active (lit)) + continue; + assert (!val (lit)); + Occs &os = occs (lit); + noccs (lit) = os.size (); + } + + // Now we fill the schedule (priority queue) of candidate literals to be + // tried as blocking literals. It is probably slightly faster to do this + // in one go after all occurrences have been determined, instead of + // filling the priority queue during pushing occurrences. Filling the + // schedule can not be fused with the previous loop (easily) since we + // first have to initialize 'noccs' for both 'lit' and '-lit'. + +#ifndef QUIET + int skipped = 0; +#endif + + for (auto idx : vars) { + if (!active (idx)) + continue; + if (frozen (idx)) { +#ifndef QUIET + skipped += 2; +#endif + continue; + } + assert (!val (idx)); + for (int sign = -1; sign <= 1; sign += 2) { + const int lit = sign * idx; + if (marked_skip (lit)) { +#ifndef QUIET + skipped++; +#endif + continue; + } + if (!marked_block (lit)) + continue; + unmark_block (lit); + LOG ("scheduling %d with %" PRId64 " positive and %" PRId64 + " negative occurrences", + lit, noccs (lit), noccs (-lit)); + blocker.schedule.push_back (vlit (lit)); + } + } + + PHASE ("block", stats.blockings, + "scheduled %zd candidate literals %.2f%% (%d skipped %.2f%%)", + blocker.schedule.size (), + percent (blocker.schedule.size (), 2.0 * active ()), skipped, + percent (skipped, 2.0 * active ())); +} + +/*------------------------------------------------------------------------*/ + +// A literal is pure if it only occurs positive. Then all clauses in which +// it occurs are blocked on it. This special case can be implemented faster +// than trying to block literals with at least one negative occurrence and +// is thus handled separately. It also allows to avoid pushing blocked +// clauses onto the extension stack. + +void Internal::block_pure_literal (Blocker &blocker, int lit) { + if (frozen (lit)) + return; + assert (active (lit)); + + Occs &pos = occs (lit); + Occs &nos = occs (-lit); + + assert (!noccs (-lit)); +#ifndef NDEBUG + for (const auto &c : nos) + assert (c->garbage); +#endif + stats.blockpurelits++; + LOG ("found pure literal %d", lit); +#ifdef LOGGING + int64_t pured = 0; +#endif + for (const auto &c : pos) { + if (c->garbage) + continue; + assert (!c->redundant); + LOG (c, "pure literal %d in", lit); + blocker.reschedule.push_back (c); + if (proof) { + proof->weaken_minus (c); + } + external->push_clause_on_extension_stack (c, lit); + stats.blockpured++; + mark_garbage (c); +#ifdef LOGGING + pured++; +#endif + } + + erase_vector (pos); + erase_vector (nos); + + mark_pure (lit); + stats.blockpured++; + LOG ("blocking %" PRId64 " clauses on pure literal %d", pured, lit); +} + +/*------------------------------------------------------------------------*/ + +// If there is only one negative clause with '-lit' it is faster to mark it +// instead of marking all the positive clauses with 'lit' one after the +// other and then resolving against the negative clause. + +void Internal::block_literal_with_one_negative_occ (Blocker &blocker, + int lit) { + assert (active (lit)); + assert (!frozen (lit)); + assert (noccs (lit) > 0); + assert (noccs (-lit) == 1); + + Occs &nos = occs (-lit); + assert (nos.size () >= 1); + + Clause *d = 0; + for (const auto &c : nos) { + if (c->garbage) + continue; + assert (!d); + d = c; +#ifndef NDEBUG + break; +#endif + } + assert (d); + nos.resize (1); + nos[0] = d; + + if (d && d->size > opts.blockmaxclslim) { + LOG (d, "skipped common antecedent"); + return; + } + + assert (!d->garbage); + assert (!d->redundant); + assert (d->size <= opts.blockmaxclslim); + + LOG (d, "common %d antecedent", lit); + mark (d); + int64_t blocked = 0; +#ifdef LOGGING + int64_t skipped = 0; +#endif + Occs &pos = occs (lit); + + // Again no 'auto' since 'pos' is update during traversal. + // + const auto eop = pos.end (); + auto j = pos.begin (), i = j; + + for (; i != eop; i++) { + Clause *c = *j++ = *i; + + if (c->garbage) { + j--; + continue; + } + if (c->size > opts.blockmaxclslim) { +#ifdef LOGGING + skipped++; +#endif + continue; + } + if (c->size < opts.blockminclslim) { +#ifdef LOGGING + skipped++; +#endif + continue; + } + + LOG (c, "trying to block on %d", lit); + + // We use the same literal move-to-front strategy as in + // 'is_blocked_clause'. See there for more explanations. + + int prev_other = 0; // Previous non-tautological literal. + + // No 'auto' since literals of 'c' are updated during traversal. + // + const const_literal_iterator end_of_c = c->end (); + literal_iterator l; + + for (l = c->begin (); l != end_of_c; l++) { + const int other = *l; + *l = prev_other; + prev_other = other; + if (other == lit) + continue; + assert (other != -lit); + assert (active (other)); + assert (!val (other)); + if (marked (other) < 0) { + LOG ("found tautological literal %d", other); + c->literals[0] = other; // Move to front of 'c'. + break; + } + } + + if (l == end_of_c) { + LOG ("no tautological literal found"); + + // Restore old literal order in the clause because. + + const const_literal_iterator begin_of_c = c->begin (); + while (l-- != begin_of_c) { + const int other = *l; + *l = prev_other; + prev_other = other; + } + + continue; // ... with next candidate 'c' in 'pos'. + } + + blocked++; + LOG (c, "blocked"); + if (proof) { + proof->weaken_minus (c); + } + external->push_clause_on_extension_stack (c, lit); + blocker.reschedule.push_back (c); + mark_garbage (c); + j--; + } + if (j == pos.begin ()) + erase_vector (pos); + else + pos.resize (j - pos.begin ()); + + stats.blocked += blocked; + LOG ("blocked %" PRId64 " clauses on %d (skipped %" PRId64 ")", blocked, + lit, skipped); + + unmark (d); +} + +/*------------------------------------------------------------------------*/ + +// Determine the set of candidate clauses with 'lit', which are checked to +// be blocked by 'lit'. Filter out too large and small clauses and which do +// not have any negated other literal in any of the clauses with '-lit'. + +size_t Internal::block_candidates (Blocker &blocker, int lit) { + + assert (blocker.candidates.empty ()); + + Occs &pos = occs (lit); // Positive occurrences of 'lit'. + Occs &nos = occs (-lit); + + assert ((size_t) noccs (lit) <= pos.size ()); + assert ((size_t) noccs (-lit) == nos.size ()); // Already flushed. + + // Mark all literals in clauses with '-lit'. Note that 'mark2' uses + // separate bits for 'lit' and '-lit'. + // + for (const auto &c : nos) + mark2 (c); + + const auto eop = pos.end (); + auto j = pos.begin (), i = j; + + for (; i != eop; i++) { + Clause *c = *j++ = *i; + if (c->garbage) { + j--; + continue; + } + assert (!c->redundant); + if (c->size > opts.blockmaxclslim) + continue; + if (c->size < opts.blockminclslim) + continue; + const const_literal_iterator eoc = c->end (); + const_literal_iterator l; + for (l = c->begin (); l != eoc; l++) { + const int other = *l; + if (other == lit) + continue; + assert (other != -lit); + assert (active (other)); + assert (!val (other)); + if (marked2 (-other)) + break; + } + if (l != eoc) + blocker.candidates.push_back (c); + } + if (j == pos.begin ()) + erase_vector (pos); + else + pos.resize (j - pos.begin ()); + + assert (pos.size () == (size_t) noccs (lit)); // Now also flushed. + + for (const auto &c : nos) + unmark (c); + + return blocker.candidates.size (); +} + +/*------------------------------------------------------------------------*/ + +// Try to find a clause with '-lit' which does not have any literal in +// clauses with 'lit'. If such a clause exists no candidate clause can be +// blocked on 'lit' since all candidates would produce a non-tautological +// resolvent with that clause. + +Clause *Internal::block_impossible (Blocker &blocker, int lit) { + assert (noccs (-lit) > 1); + assert (blocker.candidates.size () > 1); + + for (const auto &c : blocker.candidates) + mark2 (c); + + Occs &nos = occs (-lit); + Clause *res = 0; + + for (const auto &c : nos) { + assert (!c->garbage); + assert (!c->redundant); + assert (c->size <= opts.blockmaxclslim); + const const_literal_iterator eoc = c->end (); + const_literal_iterator l; + for (l = c->begin (); l != eoc; l++) { + const int other = *l; + if (other == -lit) + continue; + assert (other != lit); + assert (active (other)); + assert (!val (other)); + if (marked2 (-other)) + break; + } + if (l == eoc) + res = c; + } + + for (const auto &c : blocker.candidates) + unmark (c); + + if (res) { + LOG (res, "common non-tautological resolvent producing"); + blocker.candidates.clear (); + } + + return res; +} + +/*------------------------------------------------------------------------*/ + +// In the general case we have at least two negative occurrences. + +void Internal::block_literal_with_at_least_two_negative_occs ( + Blocker &blocker, int lit) { + assert (active (lit)); + assert (!frozen (lit)); + assert (noccs (lit) > 0); + assert (noccs (-lit) > 1); + + Occs &nos = occs (-lit); + assert ((size_t) noccs (-lit) <= nos.size ()); + + int max_size = 0; + + // Flush all garbage clauses in occurrence list 'nos' of '-lit' and + // determine the maximum size of negative clauses (with '-lit'). + // + const auto eon = nos.end (); + auto j = nos.begin (), i = j; + for (; i != eon; i++) { + Clause *c = *j++ = *i; + if (c->garbage) + j--; + else if (c->size > max_size) + max_size = c->size; + } + if (j == nos.begin ()) + erase_vector (nos); + else + nos.resize (j - nos.begin ()); + + assert (nos.size () == (size_t) noccs (-lit)); + assert (nos.size () > 1); + + // If the maximum size of a negative clause (with '-lit') exceeds the + // maximum clause size limit ignore this candidate literal. + // + if (max_size > opts.blockmaxclslim) { + LOG ("maximum size %d of clauses with %d exceeds clause size limit %d", + max_size, -lit, opts.blockmaxclslim); + return; + } + + LOG ("maximum size %d of clauses with %d", max_size, -lit); + + // We filter candidate clauses with positive occurrence of 'lit' in + // 'blocker.candidates' and return if no candidate clause remains. + // Candidates should be small enough and should have at least one literal + // which occurs negated in one of the clauses with '-lit'. + // + size_t candidates = block_candidates (blocker, lit); + if (!candidates) { + LOG ("no candidate clauses found"); + return; + } + + LOG ("found %zd candidate clauses", candidates); + + // We further search for a clause with '-lit' that has no literal + // negated in any of the candidate clauses (except 'lit'). If such a + // clause exists, we know that none of the candidates is blocked. + // + if (candidates > 1 && block_impossible (blocker, lit)) { + LOG ("impossible to block any candidate clause on %d", lit); + assert (blocker.candidates.empty ()); + return; + } + + LOG ("trying to block %zd clauses out of %" PRId64 " with literal %d", + candidates, noccs (lit), lit); + + int64_t blocked = 0; + + // Go over all remaining candidates and try to block them on 'lit'. + // + for (const auto &c : blocker.candidates) { + assert (!c->garbage); + assert (!c->redundant); + if (!is_blocked_clause (c, lit)) + continue; + blocked++; + LOG (c, "blocked"); + if (proof) { + proof->weaken_minus (c); + } + external->push_clause_on_extension_stack (c, lit); + blocker.reschedule.push_back (c); + mark_garbage (c); + } + + LOG ("blocked %" PRId64 + " clauses on %d out of %zd candidates in %zd occurrences", + blocked, lit, blocker.candidates.size (), occs (lit).size ()); + + blocker.candidates.clear (); + stats.blocked += blocked; + if (blocked) + flush_occs (lit); +} + +/*------------------------------------------------------------------------*/ + +// Reschedule literals in a clause (except 'lit') which was blocked. + +void Internal::block_reschedule_clause (Blocker &blocker, int lit, + Clause *c) { +#ifdef NDEBUG + (void) lit; +#endif + assert (c->garbage); + + for (const auto &other : *c) { + + int64_t &n = noccs (other); + assert (n > 0); + n--; + + LOG ("updating %d with %" PRId64 " positive and %" PRId64 + " negative occurrences", + other, noccs (other), noccs (-other)); + + if (blocker.schedule.contains (vlit (-other))) + blocker.schedule.update (vlit (-other)); + else if (active (other) && !frozen (other) && !marked_skip (-other)) { + LOG ("rescheduling to block clauses on %d", -other); + blocker.schedule.push_back (vlit (-other)); + } + + if (blocker.schedule.contains (vlit (other))) { + assert (other != lit); + blocker.schedule.update (vlit (other)); + } + } +} + +// Reschedule all literals in clauses blocked by 'lit' (except 'lit'). + +void Internal::block_reschedule (Blocker &blocker, int lit) { + while (!blocker.reschedule.empty ()) { + Clause *c = blocker.reschedule.back (); + blocker.reschedule.pop_back (); + block_reschedule_clause (blocker, lit, c); + } +} + +/*------------------------------------------------------------------------*/ + +void Internal::block_literal (Blocker &blocker, int lit) { + assert (!marked_skip (lit)); + + if (!active (lit)) + return; // Pure literal '-lit'. + if (frozen (lit)) + return; + + assert (!val (lit)); + + // If the maximum number of a negative clauses (with '-lit') exceeds the + // occurrence limit ignore this candidate literal. + // + if (noccs (-lit) > opts.blockocclim) + return; + + LOG ("blocking literal candidate %d " + "with %" PRId64 " positive and %" PRId64 " negative occurrences", + lit, noccs (lit), noccs (-lit)); + + stats.blockcands++; + + assert (blocker.reschedule.empty ()); + assert (blocker.candidates.empty ()); + + if (!noccs (-lit)) + block_pure_literal (blocker, lit); + else if (!noccs (lit)) { + // Rare situation, where the clause length limit was hit for 'lit' and + // '-lit' is skipped and then it becomes pure. Can be ignored. We also + // so it once happening for a 'elimboundmin=-1' and zero positive and + // one negative occurrence. + } else if (noccs (-lit) == 1) + block_literal_with_one_negative_occ (blocker, lit); + else + block_literal_with_at_least_two_negative_occs (blocker, lit); + + // Done with blocked clause elimination on this literal and we do not + // have to try blocked clause elimination on it again until irredundant + // clauses with its negation are removed. + // + assert (!frozen (lit)); // just to be sure ... + unmark_block (lit); +} + +/*------------------------------------------------------------------------*/ + +bool Internal::block () { + + if (!opts.block) + return false; + if (unsat) + return false; + if (!stats.current.irredundant) + return false; + if (terminated_asynchronously ()) + return false; + + if (propagated < trail.size ()) { + LOG ("need to propagate %zd units first", trail.size () - propagated); + init_watches (); + connect_watches (); + if (!propagate ()) { + LOG ("propagating units results in empty clause"); + learn_empty_clause (); + assert (unsat); + } + clear_watches (); + reset_watches (); + if (unsat) + return false; + } + + START_SIMPLIFIER (block, BLOCK); + + stats.blockings++; + + LOG ("block-%" PRId64 "", stats.blockings); + + assert (!level); + assert (!watching ()); + assert (!occurring ()); + + mark_satisfied_clauses_as_garbage (); + + init_occs (); // Occurrence lists for all literals. + init_noccs (); // Number of occurrences to avoid flushing garbage clauses. + + Blocker blocker (this); + block_schedule (blocker); + + int64_t blocked = stats.blocked; + int64_t resolutions = stats.blockres; + int64_t purelits = stats.blockpurelits; + int64_t pured = stats.blockpured; + + while (!terminated_asynchronously () && !blocker.schedule.empty ()) { + int lit = u2i (blocker.schedule.front ()); + blocker.schedule.pop_front (); + block_literal (blocker, lit); + block_reschedule (blocker, lit); + } + + blocker.erase (); + reset_noccs (); + reset_occs (); + + resolutions = stats.blockres - resolutions; + blocked = stats.blocked - blocked; + + PHASE ("block", stats.blockings, + "blocked %" PRId64 " clauses in %" PRId64 " resolutions", blocked, + resolutions); + + pured = stats.blockpured - pured; + purelits = stats.blockpurelits - purelits; + + if (pured) + mark_redundant_clauses_with_eliminated_variables_as_garbage (); + + if (purelits) + PHASE ("block", stats.blockings, + "found %" PRId64 " pure literals in %" PRId64 " clauses", + purelits, pured); + else + PHASE ("block", stats.blockings, "no pure literals found"); + + report ('b', !opts.reportall && !blocked); + + STOP_SIMPLIFIER (block, BLOCK); + + return blocked; +} + +} // namespace CaDiCaL diff --git a/vendor/rustsat-cadical/cppsrc/src/block.hpp b/vendor/rustsat-cadical/cppsrc/src/block.hpp new file mode 100644 index 0000000..c567075 --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/block.hpp @@ -0,0 +1,37 @@ +#ifndef _block_hpp_INCLUDED +#define _block_hpp_INCLUDED + +#include "heap.hpp" // Alphabetically after 'block.hpp'. + +namespace CaDiCaL { + +struct Internal; + +struct block_more_occs_size { + Internal *internal; + block_more_occs_size (Internal *i) : internal (i) {} + bool operator() (unsigned a, unsigned b); +}; + +typedef heap BlockSchedule; + +class Blocker { + + friend struct Internal; + + vector candidates; + vector reschedule; + BlockSchedule schedule; + + Blocker (Internal *i) : schedule (block_more_occs_size (i)) {} + + void erase () { + erase_vector (candidates); + erase_vector (reschedule); + schedule.erase (); + } +}; + +} // namespace CaDiCaL + +#endif diff --git a/vendor/rustsat-cadical/cppsrc/src/cadical.cpp b/vendor/rustsat-cadical/cppsrc/src/cadical.cpp new file mode 100644 index 0000000..046d0ca --- /dev/null +++ b/vendor/rustsat-cadical/cppsrc/src/cadical.cpp @@ -0,0 +1,1026 @@ +/*------------------------------------------------------------------------*/ + +// Do include 'internal.hpp' but try to minimize internal dependencies. + +#include "internal.hpp" +#include "signal.hpp" // Separate, only need for apps. + +/*------------------------------------------------------------------------*/ + +namespace CaDiCaL { + +// A wrapper app which makes up the CaDiCaL stand alone solver. It in +// essence only consists of the 'App::main' function. So this class +// contains code, which is not required if only the library interface in +// the class 'Solver' is used (defined in 'cadical.hpp'). It further uses +// static data structures in order to have a signal handler catch signals. +// +// It is thus neither thread-safe nor reentrant. If you want to use +// multiple instances of the solver use the 'Solver' interface directly +// which is thread-safe and reentrant among different solver instances. + +/*------------------------------------------------------------------------*/ + +class App : public Handler, public Terminator { + + Solver *solver; // Global solver. + +#ifndef _WIN32 + // Command line options. + // + int time_limit; // '-t ' +#endif + + // Strictness of (DIMACS) parsing: + // + // 0 = force parsing and completely ignore header + // 1 = relaxed header handling (default) + // 2 = strict header handling + // + // To use '0' use '-f' of '--force'. + // To use '2' use '--strict'. + // + int force_strict_parsing; + + bool force_writing; + static bool most_likely_existing_cnf_file (const char *path); + + // Internal variables. + // + int max_var; // Set after parsing. + volatile bool timesup; // Asynchronous termination. + + // Printing. + // + void print_usage (bool all = false); + void print_witness (FILE *); + +#ifndef QUIET + void signal_message (const char *msg, int sig); +#endif + + // Option handling. + // + bool set (const char *); + bool set (const char *, int); + int get (const char *); + bool verbose () { return get ("verbose") && !get ("quiet"); } + + /*----------------------------------------------------------------------*/ + + // The actual initialization. + // + void init (); + + // Terminator interface. + // + bool terminate () { return timesup; } + + // Handler interface. + // + void catch_signal (int sig); + void catch_alarm (); + +public: + App (); + ~App (); + + // Parse the arguments and run the solver. + // + int main (int arg, char **argv); +}; + +/*------------------------------------------------------------------------*/ + +// clang-format off + +void App::print_usage (bool all) { + printf ( + +"usage: cadical [