Lattice Land Core Library
Loading...
Searching...
No Matches
ternarize.hpp
Go to the documentation of this file.
1#ifndef LALA_CORE_TERNARIZE_HPP
2#define LALA_CORE_TERNARIZE_HPP
3
4#include "ast.hpp"
5#include "env.hpp"
6
7namespace lala {
8
9template <class F>
10bool is_constant_var(const F& x) {
11 if(x.is(F::LV)) {
12 std::string varname(x.lv().data());
13 return varname.starts_with("__CONSTANT_");
14 }
15 return false;
16}
17
18template <class F>
19int value_of_constant(const F& x) {
20 assert(is_constant_var(x));
21 std::string varname(x.lv().data());
22 varname = varname.substr(11);
23 varname[0] = varname[0] == 'm' ? '-' : varname[0];
24 return std::stoi(varname);
25}
26
27template <class F>
28CUDA bool is_tnf(const F& f) {
29 return f.is_binary() && f.seq(0).is_variable() &&
30 (f.sig() == EQ || f.sig() == EQUIV) &&
31 f.seq(1).is_binary() && f.seq(1).seq(0).is_variable() &&
32 f.seq(1).seq(1).is_variable();
33}
34
35namespace impl {
36
37template <class F, class Env>
38class Ternarizer
39{
40public:
41 using allocator_type = battery::standard_allocator;
42
43 /** A constraint is in extended ternary form if it is either unary (without NEQ, IN) or of the form `x = (y <op> z)`. */
44 static bool is_extended_ternary_form(const F& f) {
45 int vars = num_vars(f);
46 return (vars == 1 && (f.is(F::E) || (f.is_binary() && f.sig() != NEQ && f.sig() != IN && (f.seq(0).is_variable() || f.seq(1).is_variable()) && (f.seq(0).is_constant() || f.seq(1).is_constant())))) ||
47 (vars == 3 && f.is_binary() && f.sig() == EQ &&
48 ((f.seq(0).is_variable() && f.seq(1).is_binary() && is_ternary_op(f.seq(1).sig()) && f.seq(1).seq(0).is_variable() && f.seq(1).seq(1).is_variable())
49 || (f.seq(1).is_variable() && f.seq(0).is_binary() && is_ternary_op(f.seq(0).sig()) && f.seq(0).seq(0).is_variable() && f.seq(0).seq(1).is_variable())));
50 }
51
52 static bool is_ternary_op(Sig sig) {
53 return sig == MAX || sig == MIN || sig == EQ || sig == LEQ || (!is_logical(sig) && !is_predicate(sig));
54 }
55
56private:
57 void introduce_existing_var(const std::string& varname) {
58 if(varname.starts_with("__VAR_Z_")) {
59 introduced_int_vars = std::max((unsigned int)std::stoul(varname.substr(8))+1, introduced_int_vars);
60 }
61 else if(varname.starts_with("__VAR_B_")) {
62 introduced_bool_vars = std::max((unsigned int)std::stoul(varname.substr(8))+1, introduced_bool_vars);
63 }
64 }
65
66public:
67 /** The environment is helpful to recover the sort of the free variables. */
68 Ternarizer(const Env& env):
69 introduced_int_vars(0),
70 introduced_bool_vars(0),
71 introduced_constants(0),
72 env(env)
73 {
74 /** We skip all the temporary variables already created in the environment. */
75 for(int i = 0; i < env.num_vars(); ++i) {
76 introduce_existing_var(std::string(env[i].name.data()));
77 }
78 }
79
80private:
81 const Env& env;
82 battery::vector<F, allocator_type> conjunction;
83 battery::vector<F, allocator_type> existentials;
84 std::unordered_map<std::string, int> name2exists;
85 unsigned int introduced_int_vars;
86 unsigned int introduced_bool_vars;
87 unsigned int introduced_constants;
88
89 F introduce_var(const std::string& name, auto sort, bool constant) {
90 auto var_name = LVar<allocator_type>(name.data());
91 assert(!env.contains(name.data()));
92 existentials.push_back(F::make_exists(UNTYPED, var_name, sort));
93 assert(!name2exists.contains(name));
94 name2exists[name] = existentials.size() - 1;
95 if(constant) { introduced_constants++; }
96 else if(sort.is_int()) { introduced_int_vars++; }
97 else if(sort.is_bool()) { introduced_bool_vars++; }
98 return F::make_lvar(UNTYPED, var_name);
99 }
100
101 F introduce_int_var() {
102 std::string name = "__VAR_Z_" + std::to_string(introduced_int_vars);
103 return introduce_var(name, Sort<allocator_type>(Sort<allocator_type>::Int), false);
104 }
105
106 F introduce_bool_var() {
107 std::string name = "__VAR_B_" + std::to_string(introduced_bool_vars);
108 return introduce_var(name, Sort<allocator_type>(Sort<allocator_type>::Bool), false);
109 }
110
111public:
112 F ternarize_constant(const F& f) {
113 assert(f.is(F::Z) || f.is(F::B));
114 auto index = f.to_z();
115 std::string name = "__CONSTANT_" + (index < 0 ? std::string("m") : std::string("")) + std::to_string(abs(index));
116 // if the constant is already a logical variable, we return it.
117 if (name2exists.contains(name) || env.contains(name.data())) {
118 return F::make_lvar(UNTYPED, LVar<allocator_type>(name.data()));
119 }
120 auto var = introduce_var(name, f.sort().value(), true);
121 conjunction.push_back(F::make_binary(var, EQ, f));
122 return var;
123 }
124
125private:
126 /** Create a unary formula if the ternary formula can be simplified. */
127 bool simplify_to_unary(F x, F y, Sig sig, F z) {
128 /** Unary constraint of the form `1 <=> x <= 5`, `0 <=> x <= 5` or `1 <=> x == 5` */
129 if(is_constant_var(x) && (is_constant_var(y) || is_constant_var(z)) &&
130 (sig == LEQ || (sig == EQ && value_of_constant(x) == 1)))
131 {
132 auto yv = is_constant_var(y) ? F::make_z(value_of_constant(y)) : y;
133 auto zv = is_constant_var(z) ? F::make_z(value_of_constant(z)) : z;
134 if(value_of_constant(x) == 0) {
135 conjunction.push_back(F::make_binary(yv, GT, zv));
136 }
137 else {
138 conjunction.push_back(F::make_binary(yv, sig, zv));
139 }
140 return true;
141 }
142 return false;
143 }
144
145 /** Create the ternary formula `x = y <sig> z`. */
146 F push_ternary(F x, F y, Sig sig, F z) {
147 if(simplify_to_unary(x,y,sig,z)) {
148 return x;
149 }
150 if(sig == SUB) {
151 /** We simplify x = y - z into y = x + z. */
152 conjunction.push_back(F::make_binary(y, EQ, F::make_binary(x, ADD, z)));
153 return x;
154 }
155 /** If the simplification was not possible, we add the ternary constraint. */
156 conjunction.push_back(F::make_binary(x, EQ, F::make_binary(y, sig, z)));
157 return x;
158 }
159
160 F ternarize_unary(const F& f, bool toplevel = false) {
161 F x = ternarize(f.seq(0));
162 switch(f.sig()) {
163 /** -x ~~> t = 0 - x */
164 case NEG: {
165 F t = push_ternary(introduce_int_var(), ternarize(F::make_z(0)), SUB, x);
166 if(toplevel) {
167 return ternarize(F::make_binary(t, NEQ, F::make_z(0)), true);
168 }
169 return t;
170 }
171 /** |x| ~~> t1 = 0 - x /\ t2 = max(x, t1) /\ t2 >= 0 */
172 case ABS: {
173 F t1 = ternarize(F::make_unary(NEG, x));
174 F t2 = introduce_int_var();
175 compute(F::make_binary(t2, EQ, F::make_binary(x, MAX, t1)));
176 compute(F::make_binary(t2, GEQ, F::make_z(0)));
177 if(toplevel) {
178 return ternarize(F::make_binary(t2, NEQ, F::make_z(0)), true);
179 }
180 return t2;
181 }
182 /** NOT x ~~> ternarize(x = 0) ~~> t = (x = 0) */
183 case NOT: return ternarize(F::make_binary(x, EQ, F::make_z(0)), toplevel);
184 case MINIMIZE:
185 case MAXIMIZE: {
186 conjunction.push_back(F::make_unary(f.sig(), x));
187 return x;
188 }
189 default: {
190 printf("Unary operator %s not supported\n", string_of_sig(f.sig()));
191 printf("In formula: "); f.print(); printf("\n");
192 // assert(false);
193 return f;
194 }
195 }
196 }
197
198 bool is_boolean(const F& f) {
199 assert(f.is(F::LV));
200 std::string varname(f.lv().data());
201 if(name2exists.contains(varname)) {
202 return battery::get<1>(existentials[name2exists[varname]].exists()).is_bool();
203 }
204 else {
205 auto var_opt = env.variable_of(varname.data());
206 if(var_opt.has_value()) {
207 return var_opt->get().sort.is_bool();
208 }
209 }
210 assert(false); // undeclared variable.
211 return false;
212 }
213
214 /** Let `t` be a variable in a logical context, e.g. X OR Y.
215 * If `t` is an integer, the semantics is that `t` is true whenever `t != 0`, and not only when `t == 1`.
216 */
217 F booleanize(const F& t, Sig sig) {
218 if(is_logical(sig) && !is_boolean(t)) {
219 return ternarize(F::make_binary(t, NEQ, F::make_z(0)));
220 }
221 return t;
222 }
223
224 F ternarize_binary(F f, bool toplevel) {
225 if(f.sig() == IN && f.is_binary() && f.seq(0).is_variable() && f.seq(1).is(F::S)) {
226 if(toplevel) {
227 compute(decompose_in_constraint(f));
228 /** The decomposition of x in S does not capture the approximation x >= min(S) /\ x <= max(S).
229 * Therefore, we still give this unary constraint to the interval store in order to over-approximate it.
230 * We avoid adding the over-approximation if the decomposition is also a unary constraint. */
231 if(f.seq(1).s().size() > 1) {
232 conjunction.push_back(f);
233 }
234 return F::make_true(); /* unused anyways. */
235 }
236 else {
237 return ternarize(decompose_in_constraint(f), toplevel);
238 }
239 }
240 /** We introduce a new temporary variable `t0`.
241 * The type of `t0` is decided by the return type of the operator and whether we are at toplevel or not.
242 */
243 F t0;
244 F t1;
245 F t2;
246 bool almost_ternary = false;
247 /** We first handle "almost ternarized" constraint.
248 * If the symbol is already an equality with a variable on one side, we only need to ternarize the other half.
249 * We set t0 to be the variable and proceeds. */
250 if((((f.seq(0).is_variable() || f.seq(0).is_constant()) && f.seq(1).is_binary())
251 ||((f.seq(1).is_variable() || f.seq(1).is_constant()) && f.seq(0).is_binary()))
252 && (f.sig() == EQUIV || f.sig() == EQ))
253 {
254 int left = f.seq(0).is_binary();
255 int right = f.seq(1).is_binary();
256 /** If a IN constraint appears on the right side, we decompose it here and immediately call ternarize again. */
257 if(f.seq(right).sig() == IN) {
258 return ternarize(F::make_binary(f.seq(left), EQ, decompose_in_constraint(f.seq(right))), toplevel);
259 }
260 t0 = ternarize(f.seq(left));
261 f = f.seq(right);
262 toplevel = false;
263 almost_ternary = true;
264 }
265 t1 = ternarize(f.seq(0));
266 t1 = booleanize(t1, f.sig());
267 t2 = ternarize(f.seq(1));
268 t2 = booleanize(t2, f.sig());
269 if(!almost_ternary) {
270 /** We don't need to create t0 for these formulas at toplevel. */
271 if(toplevel && (f.sig() == NEQ || f.sig() == XOR || f.sig() == IMPLY || f.sig() == GT || f.sig() == LT)) {}
272 else if(is_logical(f.sig()) || is_predicate(f.sig())
273 || ((f.sig() == MIN || f.sig() == MAX) && is_boolean(t1) && is_boolean(t2)))
274 {
275 t0 = toplevel ? ternarize_constant(F::make_z(1)) : introduce_bool_var();
276 }
277 else {
278 t0 = toplevel ? ternarize_constant(F::make_z(1)) : introduce_int_var();
279 }
280 }
281 switch(f.sig()) {
282 case AND:
283 case MIN: return push_ternary(t0, t1, MIN, t2);
284 case OR:
285 case MAX: return push_ternary(t0, t1, MAX, t2);
286 case EQ:
287 case EQUIV: return push_ternary(t0, t1, EQ, t2);
288 // x xor y ~~> t1 = not t2 /\ t1 = (x = y)
289 case NEQ:
290 case XOR: {
291 if(toplevel) {
292 return push_ternary(ternarize_constant(F::make_z(0)), t1, EQ, t2);
293 }
294 push_ternary(ternarize(F::make_unary(NOT, t0)), t1, EQ, t2);
295 return t0;
296 }
297 case IMPLY: return ternarize(F::make_binary(F::make_unary(NOT, t1), OR, t2), toplevel);
298 case LEQ: return push_ternary(t0, t1, LEQ, t2);
299 // x >= y ~~> y <= x
300 case GEQ: return push_ternary(t0, t2, LEQ, t1);
301 // x > y ~~> !(x <= y)
302 case GT: {
303 if(toplevel) {
304 return push_ternary(ternarize_constant(F::make_z(0)), t1, LEQ, t2);
305 }
306 push_ternary(ternarize(F::make_unary(NOT, t0)), t1, LEQ, t2);
307 return t0;
308 }
309 // x < y ~~> y > x ~~> !(y <= x)
310 case LT: {
311 if(toplevel) {
312 return push_ternary(ternarize_constant(F::make_z(0)), t2, LEQ, t1);
313 }
314 push_ternary(ternarize(F::make_unary(NOT, t0)), t2, LEQ, t1);
315 return t0;
316 }
317 default: {
318 return push_ternary(t0, t1, f.sig(), t2);
319 }
320 }
321 }
322
323 std::pair<F, F> binarize_middle(const F& f) {
324 assert(f.is(F::Seq) && f.seq().size() > 2);
325 battery::vector<F, allocator_type> left;
326 battery::vector<F, allocator_type> right;
327 int i;
328 for(i = 0; i < f.seq().size() / 2; ++i) {
329 left.push_back(f.seq(i));
330 }
331 for(; i < f.seq().size(); ++i) {
332 right.push_back(f.seq(i));
333 }
334 return {
335 ternarize(left.size() == 1 ? left.back() : F::make_nary(f.sig(), std::move(left))),
336 ternarize(right.size() == 1 ? right.back() : F::make_nary(f.sig(), std::move(right)))};
337 }
338
339 F ternarize_nary(const F& f, bool toplevel) {
340 if(is_associative(f.sig())) {
341 auto [t1, t2] = binarize_middle(f);
342 return ternarize(F::make_binary(t1, f.sig(), t2), toplevel);
343 }
344 else {
345 F tmp = ternarize(F::make_binary(f.seq(0), f.sig(), f.seq(1)));
346 for (int i = 2; i < f.seq().size() - 1; i++) {
347 tmp = ternarize(F::make_binary(tmp, f.sig(), f.seq(i)));
348 }
349 tmp = ternarize(F::make_binary(tmp, f.sig(), f.seq(f.seq().size() - 1)), toplevel);
350 return tmp;
351 }
352 }
353
354 F ternarize(const F& f, bool toplevel = false) {
355 if (f.is_variable()) {
356 if(toplevel) {
357 return ternarize(F::make_binary(f, NEQ, F::make_z(0)), true);
358 }
359 return f;
360 }
361 else if (f.is(F::Z) || f.is(F::B)) {
362 if(toplevel) {
363 return f.to_z() != 0 ? F::make_true() : F::make_false();
364 }
365 return ternarize_constant(f);
366 }
367 else if (f.is(F::S)) {
368 return f;
369 }
370 else if (f.is_unary()) {
371 return ternarize_unary(f, toplevel);
372 }
373 else if (f.is_binary()) {
374 return ternarize_binary(f, toplevel);
375 }
376 else if (f.is(F::Seq) && f.seq().size() > 2) {
377 return ternarize_nary(f, toplevel);
378 }
379 printf("Unsupported formula: "); f.print(false); printf("\n");
380 assert(false);
381 return F::make_false();
382 }
383
384public:
385 void compute(const F& f) {
386 if (f.is(F::Seq) && f.sig() == AND) {
387 const auto& seq = f.seq();
388 for (int i = 0; i < seq.size(); ++i) {
389 compute(f.seq(i));
390 }
391 }
392 else if(f.is(F::E)) {
393 existentials.push_back(f);
394 std::string varname(battery::get<0>(f.exists()).data());
395 name2exists[varname] = existentials.size() - 1;
396 /** If ternarize has been called before, some temporary variables __VAR_Z_* and __VAR_B_* might already have been created.
397 * In that case, we need to update the counters to avoid conflicts.
398 * This is not perfect, because it requires existential quantifier to be in the beginning of the formula (before we introduce any variable).
399 * For more robustness, we should rename the variables in the formula, if introduced_int_vars >= X in __VAR_Z_X.
400 */
401 introduce_existing_var(varname);
402 }
403 else if(!f.is(F::ESeq) && !is_extended_ternary_form(f)) {
404 ternarize(f, true);
405 }
406 // Either it is unary, an extended formula or already in ternary form.
407 else {
408 conjunction.push_back(f);
409 }
410 }
411
412 F create() && {
413 auto ternarized_formula = std::move(existentials);
414 for (int i = 0; i < conjunction.size(); ++i) {
415 ternarized_formula.push_back(std::move(conjunction[i]));
416 }
417 if(ternarized_formula.size() == 1) {
418 return std::move(ternarized_formula[0]);
419 }
420 return F::make_nary(AND, std::move(ternarized_formula), UNTYPED, false);
421 }
422};
423
424} // namespace impl
425
426/**
427 * Given a formula `f`, we transform it into a conjunction of formulas of this form:
428 * 1. `x <op> c` where `c` is a constant.
429 * 2. `x = (y <op> z)` where `<op>` is a binary operator, either arithmetic or a comparison (`=`, `<=`).
430 * This ternary form is used by the lala-pc/PIR solver.
431 */
432template <class F, class Env = VarEnv<battery::standard_allocator>>
433F ternarize(const F& f, const Env& env = Env(), const std::vector<int>& constants = {}) {
434 impl::Ternarizer<F, Env> ternarizer(env);
435 for(int c : constants) {
436 ternarizer.ternarize_constant(F::make_z(c));
437 }
438 ternarizer.compute(f);
439 return std::move(ternarizer).create();
440}
441
442} // namespace lala
443
444#endif // LALA_CORE_TERNARIZE_HPP
Definition abstract_deps.hpp:14
bool is_constant_var(const F &x)
Definition ternarize.hpp:10
int value_of_constant(const F &x)
Definition ternarize.hpp:19
CUDA NI constexpr bool is_associative(Sig sig)
Definition ast.hpp:238
Sig
Definition ast.hpp:94
@ XOR
Logical connector.
Definition ast.hpp:114
@ NEQ
Equality relations.
Definition ast.hpp:112
@ LEQ
Unary arithmetic function symbols.
Definition ast.hpp:113
@ NOT
Unary arithmetic function symbols.
Definition ast.hpp:114
@ IMPLY
Unary arithmetic function symbols.
Definition ast.hpp:114
@ OR
Unary arithmetic function symbols.
Definition ast.hpp:114
@ EQ
Unary arithmetic function symbols.
Definition ast.hpp:112
@ GEQ
Unary arithmetic function symbols.
Definition ast.hpp:113
@ ADD
Unary arithmetic function symbols.
Definition ast.hpp:97
@ IN
Set membership predicate.
Definition ast.hpp:108
@ MIN
Unary arithmetic function symbols.
Definition ast.hpp:97
@ GT
Arithmetic comparison predicates. When applied to set, it corresponds to the lexicographic ordering o...
Definition ast.hpp:113
@ MAX
Binary arithmetic function symbols.
Definition ast.hpp:97
@ LT
Unary arithmetic function symbols.
Definition ast.hpp:113
@ AND
Unary arithmetic function symbols.
Definition ast.hpp:114
@ MAXIMIZE
Unary "meta-predicate" indicating that its argument must be maximized, according to the increasing or...
Definition ast.hpp:116
@ MINIMIZE
Same as MAXIMIZE, but for minimization.
Definition ast.hpp:117
@ EQUIV
Unary arithmetic function symbols.
Definition ast.hpp:114
@ ABS
Unary arithmetic function symbols.
Definition ast.hpp:96
@ SUB
Unary arithmetic function symbols.
Definition ast.hpp:97
@ NEG
Unary arithmetic function symbols.
Definition ast.hpp:96
CUDA NI constexpr bool is_predicate(Sig sig)
Definition ast.hpp:256
CUDA NI const char * string_of_sig(Sig sig)
Definition ast.hpp:121
CUDA NI int num_vars(const F &f)
Definition algorithm.hpp:154
F ternarize(const F &f, const Env &env=Env(), const std::vector< int > &constants={})
Definition ternarize.hpp:433
CUDA NI constexpr bool is_logical(Sig sig)
Definition ast.hpp:252
CUDA bool is_tnf(const F &f)
Definition ternarize.hpp:28
CUDA F decompose_in_constraint(const F &f, const typename F::allocator_type &alloc=typename F::allocator_type())
Definition algorithm.hpp:944
#define UNTYPED
Definition sort.hpp:21
@ Bool
Definition sort.hpp:28
@ Int
Definition sort.hpp:29