Skip to content

Commit 5c740fc

Browse files
caballaclaude
andcommitted
Fix exporting abstract states as linear constraint systems
Three related defects, all reachable from any domain built as array_adaptive(flat_boolean_numerical(...)). 1. flat_boolean_numerical_domain::to_disjunctive_linear_constraint_system added its two components to a disjunctive system with operator+=, which is *disjunction* ("c1 or ... or cn += d ==> c1 or ... or cn or d"). A product denotes the conjunction of its components, so this returned a much weaker system than the state it was exporting -- and terminated the process with "cannot add true" whenever a component was top, which is the common case since most program points carry no boolean information. The boolean component is a conjunction, so it is now distributed into each disjunct of the numerical component. The sibling to_linear_constraint_system was already correct; only the disjunctive version was wrong. 2. flat_boolean_domain::to_linear_constraint_system returned a system *containing a tautology* for top, whereas the convention elsewhere (e.g. split_dbm) is that top is the empty system and only bottom is represented by a constraint. Consumers therefore never recognized it as top, and conjoining it contributed a redundant `true`. 3. array_adaptive and array_smashing range-iterated the base domain's disjunctive system without checking for bottom, and begin() raises an error on a bottom system. This was unreachable only while (1) never reported bottom. powerset_domain and value_partitioning_domain already guarded it. tests/domains/unittests-lincst-export.cc covers all three. Against the unfixed code the suite reports two assertion failures and then dies with "cannot add true". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6fbadde commit 5c740fc

4 files changed

Lines changed: 305 additions & 4 deletions

File tree

include/crab/domains/array_adaptive.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2980,6 +2980,11 @@ class array_adaptive_domain final
29802980
to_disjunctive_linear_constraint_system() const override {
29812981
disjunctive_linear_constraint_system_t res;
29822982
auto disj_csts = m_base_dom.to_disjunctive_linear_constraint_system();
2983+
if (disj_csts.is_false()) {
2984+
// begin() raises an error on a bottom system, so bottom must be
2985+
// propagated rather than iterated.
2986+
return disjunctive_linear_constraint_system_t(true /*is_false*/);
2987+
}
29832988
for (auto &csts : disj_csts) {
29842989
auto filtered_csts = filter_nonscalar_vars(std::move(csts));
29852990
if (!filtered_csts.is_true()) {

include/crab/domains/array_smashing.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,11 @@ class array_smashing final
724724
disjunctive_linear_constraint_system_t res;
725725

726726
auto disj_csts = m_base_dom.to_disjunctive_linear_constraint_system();
727+
if (disj_csts.is_false()) {
728+
// begin() raises an error on a bottom system, so bottom must be
729+
// propagated rather than iterated.
730+
return disjunctive_linear_constraint_system_t(true /*is_false*/);
731+
}
727732
for (auto &csts : disj_csts) {
728733
auto filtered_csts = filter_ghost_vars(std::move(csts));
729734
if (!filtered_csts.is_true()) {

include/crab/domains/flat_boolean_domain.hpp

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,11 @@ class flat_boolean_domain final
400400
return linear_constraint_t::get_false();
401401

402402
if (is_top())
403-
return linear_constraint_t::get_true();
403+
// Top is the empty system: linear_constraint_system::is_true() is
404+
// "no constraints". Returning a system holding a tautology instead would
405+
// not be recognized as top by consumers, and would add a redundant `true`
406+
// to any conjunction this is combined with.
407+
return linear_constraint_system_t();
404408

405409
linear_constraint_system_t res;
406410
for (auto kv : m_env) {
@@ -1751,9 +1755,42 @@ class flat_boolean_numerical_domain final
17511755

17521756
disjunctive_linear_constraint_system_t
17531757
to_disjunctive_linear_constraint_system() const override {
1754-
disjunctive_linear_constraint_system_t res;
1755-
res += m_product.first().to_disjunctive_linear_constraint_system();
1756-
res += m_product.second().to_disjunctive_linear_constraint_system();
1758+
// A product denotes the *conjunction* of its components, but operator+= on
1759+
// a disjunctive constraint system is disjunction (see its documentation:
1760+
// "c1 or ... or cn += d ==> c1 or ... or cn or d"). Adding each component
1761+
// in turn would therefore compute `bool_part OR num_part`, which is much
1762+
// weaker than the product, and is an outright error ("cannot add true")
1763+
// whenever a component is top.
1764+
//
1765+
// The boolean component is always a conjunction of constraints over 0/1
1766+
// variables, so the product is obtained by distributing those constraints
1767+
// into every disjunct of the numerical component.
1768+
linear_constraint_system_t bool_csts =
1769+
m_product.first().to_linear_constraint_system();
1770+
disjunctive_linear_constraint_system_t num_csts =
1771+
m_product.second().to_disjunctive_linear_constraint_system();
1772+
1773+
if (bool_csts.is_false() || num_csts.is_false()) {
1774+
return disjunctive_linear_constraint_system_t(true /*is_false*/);
1775+
}
1776+
if (num_csts.is_true()) {
1777+
// No numerical information: the product is just the boolean part.
1778+
if (bool_csts.is_true()) {
1779+
return disjunctive_linear_constraint_system_t(false /*is_false*/);
1780+
}
1781+
return disjunctive_linear_constraint_system_t(bool_csts);
1782+
}
1783+
1784+
disjunctive_linear_constraint_system_t res(true /*is_false*/);
1785+
for (const linear_constraint_system_t &disjunct : num_csts) {
1786+
linear_constraint_system_t conj(disjunct);
1787+
conj += bool_csts;
1788+
if (conj.is_true()) {
1789+
// A top disjunct makes the whole disjunction top.
1790+
return disjunctive_linear_constraint_system_t(false /*is_false*/);
1791+
}
1792+
res += conj;
1793+
}
17571794
return res;
17581795
}
17591796

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Unit tests for exporting abstract states as linear constraint systems,
2+
// organized as Boost.Test suites:
3+
//
4+
// flat_bool_export to_linear_constraint_system on the 3-valued boolean
5+
// domain: top is the empty system, bottom carries a
6+
// contradiction, known values become 0/1 equalities
7+
// product_export to_disjunctive_linear_constraint_system on
8+
// flat_boolean_numerical_domain: a product denotes the
9+
// *conjunction* of its components
10+
// array_export to_disjunctive_linear_constraint_system on the array
11+
// wrappers, which must propagate a bottom base domain
12+
// rather than iterating it
13+
//
14+
// The product and array suites are regression tests: exporting a state whose
15+
// boolean component was top used to terminate the process ("cannot add true"),
16+
// and a state with both boolean and numerical information used to be exported
17+
// as their disjunction instead of their conjunction.
18+
//
19+
// Nothing is printed to standard output: the whole Boost.Test log is routed
20+
// to stderr (tests/run_tests.sh diffs stdout against a golden file) and
21+
// ctest checks the exit code.
22+
#define BOOST_TEST_MODULE unittests_lincst_export
23+
#define BOOST_TEST_ALTERNATIVE_INIT_API
24+
#define BOOST_TEST_NO_MAIN
25+
#include <boost/test/included/unit_test.hpp>
26+
27+
#include "../common.hpp"
28+
29+
#include <crab/support/debug.hpp>
30+
31+
#include <set>
32+
#include <string>
33+
#include <vector>
34+
35+
using namespace crab::cfg_impl;
36+
using namespace crab::domain_impl;
37+
38+
using flat_bool_domain_t =
39+
crab::domains::flat_boolean_domain<ikos::z_number, varname_t>;
40+
using product_domain_t = z_bool_interval_domain_t;
41+
using z_lin_cst_sys_t = ikos::linear_constraint_system<ikos::z_number, varname_t>;
42+
using z_disj_lin_cst_sys_t =
43+
ikos::disjunctive_linear_constraint_system<ikos::z_number, varname_t>;
44+
45+
namespace {
46+
47+
/** The constraints of a system, as strings, so they can be compared by set. */
48+
std::set<std::string> constraints_of(const z_lin_cst_sys_t &csts) {
49+
std::set<std::string> res;
50+
for (auto const &c : csts) {
51+
crab::crab_string_os os;
52+
os << c;
53+
res.insert(os.str());
54+
}
55+
return res;
56+
}
57+
58+
/** The disjuncts of a disjunctive system. Must not be called on bottom. */
59+
std::vector<std::set<std::string>>
60+
disjuncts_of(const z_disj_lin_cst_sys_t &dcsts) {
61+
std::vector<std::set<std::string>> res;
62+
for (auto const &csts : dcsts) {
63+
res.push_back(constraints_of(csts));
64+
}
65+
return res;
66+
}
67+
68+
} // namespace
69+
70+
// ---------------------------------------------------------------------------
71+
BOOST_AUTO_TEST_SUITE(flat_bool_export)
72+
73+
// Top is the empty system, matching the convention of the numerical domains
74+
// (e.g. split_dbm): is_true() is "no constraints". Returning a system that
75+
// *contains* a tautology instead would not be recognized as top, and would add
76+
// a redundant `true` to any conjunction it took part in.
77+
BOOST_AUTO_TEST_CASE(top_exports_the_empty_system) {
78+
flat_bool_domain_t dom;
79+
auto csts = dom.to_linear_constraint_system();
80+
BOOST_CHECK_MESSAGE(csts.is_true(), "top must export as the empty system");
81+
BOOST_CHECK_EQUAL(csts.size(), 0u);
82+
}
83+
84+
// Bottom, by contrast, *is* represented by a constraint, so that is_false()
85+
// can detect it.
86+
BOOST_AUTO_TEST_CASE(bottom_exports_a_contradiction) {
87+
flat_bool_domain_t dom;
88+
dom.set_to_bottom();
89+
auto csts = dom.to_linear_constraint_system();
90+
BOOST_CHECK(csts.is_false());
91+
}
92+
93+
BOOST_AUTO_TEST_CASE(known_booleans_export_as_zero_one_equalities) {
94+
variable_factory_t vfac;
95+
z_var b(vfac["b"], crab::BOOL_TYPE, 1);
96+
z_var c(vfac["c"], crab::BOOL_TYPE, 1);
97+
98+
flat_bool_domain_t dom;
99+
dom.assume_bool(b, false /*not negated*/); // b is true
100+
dom.assume_bool(c, true /*negated*/); // c is false
101+
102+
auto csts = constraints_of(dom.to_linear_constraint_system());
103+
BOOST_CHECK_EQUAL(csts.count("b = 1"), 1u);
104+
BOOST_CHECK_EQUAL(csts.count("c = 0"), 1u);
105+
}
106+
107+
BOOST_AUTO_TEST_SUITE_END()
108+
109+
// ---------------------------------------------------------------------------
110+
BOOST_AUTO_TEST_SUITE(product_export)
111+
112+
BOOST_AUTO_TEST_CASE(top_exports_as_top) {
113+
product_domain_t dom;
114+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
115+
BOOST_CHECK(!dcsts.is_false());
116+
BOOST_CHECK_MESSAGE(dcsts.is_true(), "a top product must export as top");
117+
}
118+
119+
BOOST_AUTO_TEST_CASE(bottom_exports_as_bottom) {
120+
product_domain_t dom;
121+
dom.set_to_bottom();
122+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
123+
BOOST_CHECK_MESSAGE(dcsts.is_false(),
124+
"a bottom product must export as bottom");
125+
}
126+
127+
// Regression: with a top boolean component, exporting used to reach
128+
// `operator+=(true)` on a disjunctive system, which terminates the process.
129+
// Most program points have no boolean information, so this was the common case.
130+
BOOST_AUTO_TEST_CASE(numerical_only_survives_a_top_boolean_component) {
131+
variable_factory_t vfac;
132+
z_var y(vfac["y"], crab::INT_TYPE, 32);
133+
134+
product_domain_t dom;
135+
dom += z_lin_cst_t(z_lin_exp_t(y) >= ikos::z_number(1));
136+
dom += z_lin_cst_t(z_lin_exp_t(y) <= ikos::z_number(10));
137+
138+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
139+
BOOST_REQUIRE(!dcsts.is_false());
140+
BOOST_CHECK_MESSAGE(!dcsts.is_true(),
141+
"numerical information must not be exported as top");
142+
143+
auto disjuncts = disjuncts_of(dcsts);
144+
BOOST_REQUIRE_EQUAL(disjuncts.size(), 1u);
145+
BOOST_CHECK_EQUAL(disjuncts[0].count("-y <= -1"), 1u);
146+
BOOST_CHECK_EQUAL(disjuncts[0].count("y <= 10"), 1u);
147+
// No stray tautology from the top boolean component.
148+
BOOST_CHECK_EQUAL(disjuncts[0].count("true"), 0u);
149+
}
150+
151+
BOOST_AUTO_TEST_CASE(boolean_only_is_exported) {
152+
variable_factory_t vfac;
153+
z_var b(vfac["b"], crab::BOOL_TYPE, 1);
154+
155+
product_domain_t dom;
156+
dom.assume_bool(b, false /*not negated*/);
157+
158+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
159+
BOOST_REQUIRE(!dcsts.is_false());
160+
BOOST_REQUIRE(!dcsts.is_true());
161+
auto disjuncts = disjuncts_of(dcsts);
162+
BOOST_REQUIRE_EQUAL(disjuncts.size(), 1u);
163+
BOOST_CHECK_EQUAL(disjuncts[0].count("b = 1"), 1u);
164+
}
165+
166+
// Regression, and the reason this file exists: a product denotes the
167+
// conjunction of its components. Adding each component to a disjunctive system
168+
// in turn computes their *disjunction* instead, which is far weaker -- here it
169+
// would yield `(b = 1) OR (1 <= y <= 10)` as two disjuncts rather than one
170+
// disjunct holding both facts.
171+
BOOST_AUTO_TEST_CASE(components_are_conjoined_not_disjoined) {
172+
variable_factory_t vfac;
173+
z_var b(vfac["b"], crab::BOOL_TYPE, 1);
174+
z_var y(vfac["y"], crab::INT_TYPE, 32);
175+
176+
product_domain_t dom;
177+
dom.assume_bool(b, false /*not negated*/);
178+
dom += z_lin_cst_t(z_lin_exp_t(y) >= ikos::z_number(1));
179+
dom += z_lin_cst_t(z_lin_exp_t(y) <= ikos::z_number(10));
180+
181+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
182+
BOOST_REQUIRE(!dcsts.is_false());
183+
BOOST_REQUIRE(!dcsts.is_true());
184+
185+
auto disjuncts = disjuncts_of(dcsts);
186+
BOOST_REQUIRE_MESSAGE(disjuncts.size() == 1u,
187+
"the two components must be conjoined into a single "
188+
"disjunct, not unioned into two");
189+
BOOST_CHECK_EQUAL(disjuncts[0].count("b = 1"), 1u);
190+
BOOST_CHECK_EQUAL(disjuncts[0].count("-y <= -1"), 1u);
191+
BOOST_CHECK_EQUAL(disjuncts[0].count("y <= 10"), 1u);
192+
}
193+
194+
BOOST_AUTO_TEST_SUITE_END()
195+
196+
// ---------------------------------------------------------------------------
197+
BOOST_AUTO_TEST_SUITE(array_export)
198+
199+
// Regression: the array wrappers range-iterate the base domain's disjunctive
200+
// system, and begin() raises an error on a bottom system. This was unreachable
201+
// only while the product never reported bottom.
202+
BOOST_AUTO_TEST_CASE(array_adaptive_propagates_bottom) {
203+
z_aa_bool_int_t dom;
204+
dom.set_to_bottom();
205+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
206+
BOOST_CHECK(dcsts.is_false());
207+
}
208+
209+
BOOST_AUTO_TEST_CASE(array_smashing_propagates_bottom) {
210+
z_as_bool_num_t dom;
211+
dom.set_to_bottom();
212+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
213+
BOOST_CHECK(dcsts.is_false());
214+
}
215+
216+
BOOST_AUTO_TEST_CASE(array_adaptive_exports_scalar_facts) {
217+
variable_factory_t vfac;
218+
z_var y(vfac["y"], crab::INT_TYPE, 32);
219+
220+
z_aa_bool_int_t dom;
221+
dom += z_lin_cst_t(z_lin_exp_t(y) >= ikos::z_number(1));
222+
dom += z_lin_cst_t(z_lin_exp_t(y) <= ikos::z_number(10));
223+
224+
auto dcsts = dom.to_disjunctive_linear_constraint_system();
225+
BOOST_REQUIRE(!dcsts.is_false());
226+
BOOST_REQUIRE(!dcsts.is_true());
227+
auto disjuncts = disjuncts_of(dcsts);
228+
BOOST_REQUIRE_EQUAL(disjuncts.size(), 1u);
229+
BOOST_CHECK_EQUAL(disjuncts[0].count("-y <= -1"), 1u);
230+
BOOST_CHECK_EQUAL(disjuncts[0].count("y <= 10"), 1u);
231+
}
232+
233+
BOOST_AUTO_TEST_SUITE_END()
234+
235+
// Keep stdout empty: the golden-output harness (tests/run_tests.sh) diffs
236+
// the standard output of every test binary, so the whole Boost.Test log is
237+
// routed to stderr. ctest passes --disable-warnings, which Boost.Test would
238+
// reject, so it is translated into the crab flag it stands for.
239+
int main(int argc, char **argv) {
240+
std::vector<char *> args;
241+
for (int idx = 0; idx < argc; ++idx) {
242+
if (std::string(argv[idx]) == "--disable-warnings") {
243+
crab::CrabEnableWarningMsg(false);
244+
continue;
245+
}
246+
args.push_back(argv[idx]);
247+
}
248+
char log_sink[] = "--log_sink=stderr";
249+
char report_sink[] = "--report_sink=stderr";
250+
args.push_back(log_sink);
251+
args.push_back(report_sink);
252+
return boost::unit_test::unit_test_main(
253+
&init_unit_test, static_cast<int>(args.size()), args.data());
254+
}

0 commit comments

Comments
 (0)