Lattice Land Core Library
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 
7 namespace lala {
8 namespace impl {
9 
10 template <class F, class Env>
11 class Ternarizer
12 {
13 public:
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 
29 private:
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 
39 public:
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 
54 private:
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 
374 public:
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  */
422 template <class F, class Env = VarEnv<battery::standard_allocator>>
423 F 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
::lala::B<::battery::local_memory > B
Definition: b.hpp:12
Definition: abstract_deps.hpp:14
CUDA constexpr NI bool is_predicate(Sig sig)
Definition: ast.hpp:217
CUDA NI const char * string_of_sig(Sig sig)
Definition: ast.hpp:121
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 constexpr NI bool is_logical(Sig sig)
Definition: ast.hpp:213
CUDA NI std::optional< F > negate(const F &f)
Definition: algorithm.hpp:284
CUDA constexpr NI bool is_associative(Sig sig)
Definition: ast.hpp:204
CUDA NI int num_vars(const F &f)
Definition: algorithm.hpp:153
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