ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
GenericCircuit.hpp
Go to the documentation of this file.
1/**
2 * @file GenericCircuit.hpp
3 * @brief Template implementation of @c GenericCircuit::evaluate().
4 *
5 * Provides the out-of-line definition of the @c evaluate() template method
6 * declared in @c GenericCircuit.h. This file must be included (directly
7 * or transitively) by any translation unit that instantiates
8 * @c GenericCircuit::evaluate<S>() for a specific semiring type @c S.
9 *
10 * The @c evaluate() method performs a post-order traversal of the sub-circuit
11 * rooted at gate @p g, looking up input-gate values from @p provenance_mapping
12 * and combining them using the semiring operations:
13 *
14 * | Gate type | Semiring operation |
15 * |-------------|-------------------------------|
16 * | gate_input | lookup in @p provenance_mapping (else @c unmapped_input) |
17 * | gate_plus | @c semiring.plus(children) |
18 * | gate_times | @c semiring.times(children) |
19 * | gate_monus | @c semiring.monus(left, right) |
20 * | gate_delta | @c semiring.delta(child) |
21 * | gate_cmp | @c semiring.cmp(left, op, right)|
22 * | gate_semimod| @c semiring.semimod(x, s) |
23 * | gate_agg | @c semiring.agg(op, children) |
24 * | gate_value | @c semiring.value(string) |
25 * | gate_one | @c semiring.one() |
26 * | gate_zero | @c semiring.zero() |
27 * | gate_rv | @c semiring.rv(spec, params) |
28 * | gate_arith | @c semiring.arith(op, children, extra) |
29 * | gate_mixture| @c semiring.mixture(p, x, y) / @c semiring.categorical(…) |
30 * | gate_case | @c semiring.guarded_case(children) |
31 * | gate_observe| @c semiring.observe(child, datum) |
32 * | gate_conditioned | @c semiring.conditioned(children) |
33 *
34 * The last six are measure-carrier gates with no algebraic reading: the
35 * @c Semiring base class refuses them, and only the symbolic @c Formula
36 * pseudo-semiring overrides the hooks (to render them rather than
37 * interpret them).
38 */
39#include "GenericCircuit.h"
40
41extern "C" {
42#include "utils/lsyscache.h"
43#include "miscadmin.h" // check_stack_depth
44}
45
46template<typename S, std::enable_if_t<std::is_base_of_v<semiring::Semiring<typename S::value_type>, S>, int> >
47typename S::value_type GenericCircuit::evaluate(gate_t g, std::unordered_map<gate_t, typename S::value_type> &provenance_mapping, S semiring) const
48{
49 /* Iterative post-order evaluation with @p provenance_mapping doubling as
50 * the memoisation table. Provenance circuits can be as deep as the data
51 * (a recursive fixpoint's times/plus chain, the decomposition-aligned
52 * reachability circuits of path-like graphs), so recursion on wires
53 * would overflow the C stack -- the previous implementation turned that
54 * into a "stack depth limit exceeded" error at a few thousand levels;
55 * the explicit stack removes the ceiling altogether. Every computed
56 * gate is memoised (a gate's semiring value is a pure function of the
57 * gate), so shared sub-DAGs are evaluated once and gate-creating
58 * semirings (BoolExpr, formula) preserve the sharing structurally. */
59 std::vector<gate_t> stack{g};
60
61 while(!stack.empty()) {
62 const gate_t u = stack.back();
63
64 /* The side-band assumption checks run BEFORE the memoisation
65 * lookup: input leaves are preloaded into @p provenance_mapping
66 * from the mapping table, and a fold collapse can redirect a
67 * marked gate onto such a leaf -- the marker must still refuse
68 * incompatible semirings there. */
69
70 /* In-memory Boolean-assumption marker (set by
71 * @c foldBooleanIdentities on gates whose wires were rewritten
72 * under a Boolean-only rule). Mirrors the @c gate_assumed
73 * structural-marker check below but applies to gates that keep
74 * their original type (the rule mutated their wires in place ;
75 * the persistent mmap was not touched). Same compatibility
76 * predicate, same failure mode. */
77 if(isBooleanAssumed(u) && !semiring.compatibleWithBooleanRewrite())
78 throw CircuitException(
79 "The requested semiring does not admit a homomorphism "
80 "from Boolean functions; this gate's wires were rewritten "
81 "under a Boolean-only rule (times-idempotence or "
82 "times-absorbs-plus, applied under the 'boolean' "
83 "provenance class) and the evaluation is unsound under "
84 "this semiring. Re-run under a more general provenance "
85 "class, or pick a Boolean-compatible semiring (boolean, "
86 "boolexpr, formula, ...).");
87
88 /* In-memory absorptive-assumption marker (set by the absorptive
89 * fold rules: plus-idempotence, plus-with-one absorber,
90 * plus-absorbs-times). Sound in every absorptive semiring; a
91 * semiring tolerating the stronger Boolean rewrite tolerates this
92 * weaker, Boolean-function-preserving one as well. */
93 if(isAbsorptiveAssumed(u) && !semiring.absorptive()
94 && !semiring.compatibleWithBooleanRewrite())
95 throw CircuitException(
96 "The requested semiring is not absorptive; this gate's "
97 "wires were rewritten under an absorptive rule "
98 "(plus-idempotence, plus-with-one absorber or "
99 "plus-absorbs-times, applied under the 'absorptive' or "
100 "'boolean' provenance class) and the evaluation is "
101 "unsound under this semiring. Re-run under the "
102 "'semiring' provenance class, or pick an absorptive "
103 "semiring (probability, boolean, nonnegative "
104 "tropical, ...).");
105
106 if(provenance_mapping.find(u) != provenance_mapping.end()) {
107 stack.pop_back();
108 continue;
109 }
110
111 const auto t = getGateType(u);
112
113 /* Leaves. */
114 switch(t) {
115 case gate_one:
116 case gate_update:
117 provenance_mapping.emplace(u, semiring.one());
118 stack.pop_back();
119 continue;
120 case gate_input:
121 case gate_mulinput:
122 // A variable leaf the provenance mapping did not name. By default
123 // it contributes no provenance (the semiring's one); a rendering
124 // semiring overrides unmapped_input to identify it instead.
125 provenance_mapping.emplace(u, semiring.unmapped_input(getUUID(u)));
126 stack.pop_back();
127 continue;
128 case gate_zero:
129 provenance_mapping.emplace(u, semiring.zero());
130 stack.pop_back();
131 continue;
132 case gate_value:
133 provenance_mapping.emplace(u, semiring.value(getExtra(u)));
134 stack.pop_back();
135 continue;
136 case gate_assumed:
137 /* Structural assumption marker: the wrapped sub-circuit was
138 * computed under the assumption named by the gate's label (the
139 * extra string; a gate stored without a label defaults to
140 * 'boolean'). Identity for semirings satisfying
141 * the assumption; fatal for the rest, since otherwise we would
142 * silently return a value the semiring's semantics does not
143 * justify.
144 *
145 * - 'boolean': the sub-circuit only preserves the Boolean
146 * function of the lineage (e.g. the safe-query rewrite
147 * collapses derivation multiplicities into a single witness);
148 * sound for semirings admitting a homomorphism from Boolean
149 * functions.
150 * - 'absorptive': the sub-circuit only represents the
151 * absorptive (Sorp) quotient of the recursive provenance --
152 * either truncated at the absorptive value fixpoint (cyclic
153 * recursion stopped once every minimal,
154 * tuple-repetition-free, derivation is covered) or compiled
155 * by the bounded-treewidth reachability route (whose world
156 * enumeration surfaces exactly the minimal derivation
157 * supports); longer derivations are absorbed in any
158 * absorptive semiring but genuinely missing for the rest
159 * (Deutch, Milo, Roy & Tannen, ICDT 2014). */
160 {
161 const std::string assumption = getExtra(u);
162 if(assumption.empty() || assumption == "boolean") {
163 if(!semiring.compatibleWithBooleanRewrite())
164 throw CircuitException(
165 "The requested semiring does not admit a homomorphism "
166 "from Boolean functions; the wrapped sub-circuit was "
167 "computed under a Boolean-provenance assumption "
168 "(typically by the safe-query rewrite, "
169 "provenance class 'boolean') and the evaluation is "
170 "unsound under this semiring. Re-run the query under "
171 "a more general provenance class, or pick a "
172 "Boolean-compatible semiring (boolean, boolexpr, "
173 "formula, ...).");
174 } else if(assumption == "absorptive") {
175 if(!semiring.absorptive())
176 throw CircuitException(
177 "The requested semiring is not absorptive; the "
178 "wrapped sub-circuit only represents the absorptive "
179 "quotient of a recursive query's provenance "
180 "(fixpoint truncation or compiled reachability "
181 "circuit), so its value is only defined for "
182 "absorptive semirings (probability, boolean, "
183 "formula-with-absorption, nonnegative tropical, "
184 "...). Counting and why-provenance of such a "
185 "recursion are genuinely infinite: a tuple derived "
186 "through itself gains a derivation per round, which "
187 "cyclic data does and so does a null-padded row that "
188 "re-derives itself or a projection onto constants, on "
189 "acyclic data.");
190 /* CAVEAT: absorptive() is a coarser gate than the compiled
191 * reachability route's actual soundness condition. That route
192 * materialises its world enumeration with genuine negation: each
193 * absent edge surfaces as monus(one, edge) (BooleanGate::NOT
194 * lowered to gate_monus; see ReachabilityCompiler.cpp and
195 * CertifiedDDMaterialize.cpp). The absorptive-quotient value
196 * comes out right only because, in every absorptive semiring we
197 * currently ship, (i) monus(one, x) is the times-neutral 'one' on
198 * a present-priced leaf, so the negative literals do not perturb
199 * the path-products, and (ii) any world a negative literal would
200 * kill is dominated by an edge-superset of equal value, hence
201 * absorbed. semiring.absorptive() checks NEITHER property. A
202 * future or user-defined absorptive m-semiring whose monus(one, .)
203 * is not the times-neutral, or whose monus is not
204 * "drop-if-dominated", would pass this gate yet read those
205 * monus(one, edge) gates with a value the path-sum argument does
206 * not justify -- a silently wrong result. If such a semiring is
207 * added, strengthen this guard (e.g. assert monus(one, x) == one
208 * for present-priced leaves, or add a dedicated capability flag)
209 * rather than relying on absorptive() alone. (Truncated cyclic
210 * recursion, the other 'absorptive' producer, ships only minimal
211 * derivations and carries no such negation, so it is unaffected.) */
212 } else
213 throw CircuitException(
214 "Unknown assumption marker '" + assumption + "'");
215 }
216 break;
217 case gate_cmp:
218 {
219 bool ok;
220 cmpOpFromOid(getInfos(u).first, ok);
221 if(!ok)
222 throw CircuitException(
223 "Comparison operator OID " +
224 std::to_string(getInfos(u).first) +
225 " not supported");
226 break;
227 }
228 default:
229 break;
230 }
231
232 /* Internal gate: make sure every child is computed first. */
233 {
234 bool ready = true;
235 for(const auto &c : getWires(u))
236 if(provenance_mapping.find(c) == provenance_mapping.end()) {
237 stack.push_back(c);
238 ready = false;
239 }
240 if(!ready)
241 continue;
242 }
243
244 const auto childValue = [&](int i) -> const typename S::value_type & {
245 return provenance_mapping.at(getWires(u)[i]);
246 };
247
248 switch(t) {
249 case gate_plus:
250 case gate_times:
251 case gate_monus: {
252 std::vector<typename S::value_type> childrenResult;
253 for(const auto &c : getWires(u))
254 childrenResult.push_back(provenance_mapping.at(c));
255 if(t==gate_plus) {
256 childrenResult.erase(std::remove(std::begin(childrenResult), std::end(childrenResult), semiring.zero()),
257 childrenResult.end());
258 provenance_mapping.emplace(u, semiring.plus(childrenResult));
259 } else if(t==gate_times) {
260 bool zero = false;
261 for(const auto &c: childrenResult) {
262 if(c==semiring.zero()) {
263 zero = true;
264 break;
265 }
266 }
267 if(zero)
268 provenance_mapping.emplace(u, semiring.zero());
269 else {
270 childrenResult.erase(std::remove(std::begin(childrenResult), std::end(childrenResult), semiring.one()),
271 childrenResult.end());
272 provenance_mapping.emplace(u, semiring.times(childrenResult));
273 }
274 } else {
275 if(childrenResult[0]==semiring.zero() || childrenResult[0]==childrenResult[1])
276 provenance_mapping.emplace(u, semiring.zero());
277 else
278 provenance_mapping.emplace(u, semiring.monus(childrenResult[0], childrenResult[1]));
279 }
280 break;
281 }
282
283 case gate_delta:
284 provenance_mapping.emplace(u, semiring.delta(childValue(0)));
285 break;
286
287 case gate_project:
288 case gate_eq:
289 case gate_annotation:
290 case gate_assumed:
291 // Where-provenance gates, the transparent annotation wrapper and the
292 // (compatibility-checked above) Boolean-assumption marker: identity
293 // for every admissible semiring. The annotation's extra string is
294 // inert metadata at evaluation time.
295 provenance_mapping.emplace(u, childValue(0));
296 break;
297
298 case gate_cmp:
299 {
300 bool ok;
301 ComparisonOperator op = cmpOpFromOid(getInfos(u).first, ok);
302 provenance_mapping.emplace(u, semiring.cmp(childValue(0), op, childValue(1)));
303 break;
304 }
305
306 case gate_semimod:
307 provenance_mapping.emplace(u, semiring.semimod(childValue(0), childValue(1)));
308 break;
309
310 case gate_agg:
311 {
312 auto infos = getInfos(u);
313
315
316 std::vector<typename S::value_type> vec;
317 for(const auto &c : getWires(u))
318 vec.push_back(provenance_mapping.at(c));
319 provenance_mapping.emplace(u, semiring.agg(op, vec));
320 break;
321 }
322
323 case gate_conditioned: {
324 /* Conditioning marker: P(·|C) requires a normalising division that
325 * no general semiring provides (m-semirings have monus, not a
326 * multiplicative inverse). A conditioned token is evaluable only
327 * in the measure interpretation (probability_evaluate, special-
328 * cased at the root, or the random-variable / agg_token
329 * distribution evaluators); the base-class hook refuses it for
330 * every semiring but the symbolic Formula, which renders the
331 * marker instead of interpreting it. */
332 std::vector<typename S::value_type> vec;
333 for(const auto &c : getWires(u))
334 vec.push_back(provenance_mapping.at(c));
335 provenance_mapping.emplace(u, semiring.conditioned(vec));
336 break;
337 }
338
339 case gate_mobius: {
340 /* The signed Möbius combination is a probability-only shortcut layered
341 * over the normal provenance: the gate carries the literal lineage as a
342 * designated child marked "L:<uuid>" in extra. Every non-probability
343 * evaluator (this semiring path, hence Shapley / Banzhaf / PROV export)
344 * is TRANSPARENT to that lineage, so the token behaves like the ordinary
345 * provenance of the query. A nested gate_mobius (an inner
346 * inclusion-exclusion step) carries no lineage: its value is never used
347 * (the root passes through to the top lineage), but it must not throw, so
348 * it falls back to its first child. */
349 const std::string ex = getExtra(u);
350 gate_t lineage = u; // sentinel: not found
351 const std::string key = "L:";
352 std::size_t p = ex.find(key);
353 if(p != std::string::npos) {
354 std::size_t e = ex.find(' ', p);
355 const std::string luid =
356 ex.substr(p + key.size(),
357 e == std::string::npos ? std::string::npos : e - p - key.size());
358 for(const auto &c : getWires(u))
359 if(getUUID(c) == luid) { lineage = c; break; }
360 }
361 if(lineage != u)
362 provenance_mapping.emplace(u, provenance_mapping.at(lineage));
363 else
364 provenance_mapping.emplace(u, childValue(0));
365 break;
366 }
367
368 case gate_case: {
369 /* Guarded selection over scalar (RV) children: a value chosen by the
370 * first satisfied guard event. This is a measure/RV-carrier operation
371 * (the guards are probabilistic events, the values random variables), not
372 * a semiring one -- evaluable only through the random-variable / measure
373 * evaluators (expected / variance / support / probability / sample),
374 * exactly like gate_rv and gate_arith over RVs. */
375 std::vector<typename S::value_type> vec;
376 for(const auto &c : getWires(u))
377 vec.push_back(provenance_mapping.at(c));
378 provenance_mapping.emplace(u, semiring.guarded_case(vec));
379 break;
380 }
381
382 /* The measure-carrier gates below have no algebraic reading either:
383 * their base-class hooks refuse them for every proper semiring, and
384 * Formula overrides them to render the sub-circuit symbolically. */
385 case gate_rv: {
386 /* A gate_rv is a leaf unless one of its distribution parameters is
387 * wired ("$i" in the extra encoding), which makes it a compound
388 * (latent-variable) leaf over the values of its wires. */
389 std::vector<typename S::value_type> params;
390 for(const auto &c : getWires(u))
391 params.push_back(provenance_mapping.at(c));
392 provenance_mapping.emplace(u, semiring.rv(getExtra(u), params));
393 break;
394 }
395
396 case gate_arith: {
397 bool ok;
398 ArithmeticOperator op = arithOpFromTag(getInfos(u).first, ok);
399 if(!ok)
400 throw CircuitException(
401 "Arithmetic operator tag " +
402 std::to_string(getInfos(u).first) +
403 " not supported");
404 std::vector<typename S::value_type> vec;
405 for(const auto &c : getWires(u))
406 vec.push_back(provenance_mapping.at(c));
407 provenance_mapping.emplace(u, semiring.arith(op, vec, getExtra(u)));
408 break;
409 }
410
411 case gate_mixture: {
412 const auto &w = getWires(u);
413 if(isCategoricalMixture(u)) {
414 /* [key, mul_1, …, mul_n]: each outcome's probability lives in the
415 * mulinput's prob and its value in the mulinput's extra (those
416 * leaves evaluate to one() on their own, so the payload has to be
417 * read off the gate here). */
418 std::vector<double> probs;
419 std::vector<std::string> outcomes;
420 for(std::size_t i = 1; i < w.size(); ++i) {
421 probs.push_back(getProb(w[i]));
422 outcomes.push_back(getExtra(w[i]));
423 }
424 provenance_mapping.emplace(
425 u, semiring.categorical(childValue(0), probs, outcomes));
426 } else {
427 if(w.size() != 3)
428 throw CircuitException(
429 "gate_mixture must have exactly three children "
430 "[p_token, x_token, y_token]");
431 provenance_mapping.emplace(
432 u, semiring.mixture(childValue(0), childValue(1), childValue(2)));
433 }
434 break;
435 }
436
437 case gate_observe: {
438 const auto &w = getWires(u);
439 if(w.size() != 1)
440 throw CircuitException(
441 "gate_observe must have exactly one child (the observed leaf)");
442 provenance_mapping.emplace(u, semiring.observe(childValue(0), getExtra(u)));
443 break;
444 }
445
446 default:
447 throw CircuitException("Invalid gate type for semiring evaluation");
448 }
449
450 stack.pop_back();
451 }
452
453 return provenance_mapping.at(g);
454}
ArithmeticOperator arithOpFromTag(unsigned tag, bool &ok)
Map a gate_arith operator tag to an ArithmeticOperator.
ComparisonOperator cmpOpFromOid(Oid op_oid, bool &ok)
Map a PostgreSQL comparison-operator OID to a ComparisonOperator.
AggregationOperator getAggregationOperator(Oid oid)
Map a PostgreSQL aggregate function OID to an AggregationOperator.
AggregationOperator
SQL aggregation functions tracked by ProvSQL.
Definition Aggregation.h:51
ComparisonOperator
SQL comparison operators used in gate_cmp circuit gates.
Definition Aggregation.h:39
ArithmeticOperator
Arithmetic operations carried by gate_arith circuit gates.
Definition Aggregation.h:74
gate_t
Strongly-typed gate identifier.
Definition Circuit.h:49
Semiring-agnostic in-memory provenance circuit.
Exception type thrown by circuit operations on invalid input.
Definition Circuit.h:206
std::vector< gate_t > & getWires(gate_t g)
Definition Circuit.h:140
gate_type getGateType(gate_t g) const
Definition Circuit.h:130
uuid getUUID(gate_t g) const
Definition Circuit.hpp:46
std::map< gate_t, std::pair< unsigned, unsigned > > infos
Per-gate (info1, info2) annotations.
S::value_type evaluate(gate_t g, std::unordered_map< gate_t, typename S::value_type > &provenance_mapping, S semiring) const
Evaluate the sub-circuit rooted at gate g over semiring semiring.
bool isCategoricalMixture(gate_t g) const
Test whether g is a categorical-form gate_mixture (the explicit provsql.categorical output).
std::string getExtra(gate_t g) const
Return the string extra for gate g.
double getProb(gate_t g) const
Return the probability for gate g.
bool isAbsorptiveAssumed(gate_t g) const
Report whether g carries the absorptive-assumption flag.
bool isBooleanAssumed(gate_t g) const
Report whether g carries the Boolean-assumption flag.
std::pair< unsigned, unsigned > getInfos(gate_t g) const
Return the integer annotation pair for gate g.
@ gate_observe
Latent-variable observation (likelihood-weighting evidence): one wire → an observed bare gate_rv leaf...
@ gate_rv
Continuous random-variable leaf (extra encodes distribution).
@ gate_case
N-ary guarded selection over scalar (RV) children: wires are [guard_1, value_1, .....
@ gate_annotation
Transparent single-child wrapper carrying a query-level annotation in extra (inversion-free certifica...
@ gate_mobius
Signed Möbius combination: a MEASURE-only gate carrying one integer coefficient per child (in extra,...
@ gate_conditioned
Conditioning marker with two children [target, evidence]: measure-only, probability_evaluate returns ...
@ gate_mixture
Probabilistic mixture: three wires [p_token (gate_input Bernoulli), x_token, y_token]; samples x when...
@ gate_arith
n-ary arithmetic gate over scalar-valued children (info1 holds operator tag)
@ gate_assumed
Structural marker over a single child whose sub-circuit was computed under a Boolean-provenance assum...