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