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 cyclic "
185 "recursion are genuinely infinite; on acyclic "
186 "data, re-run under the 'semiring' provenance "
187 "class.");
188 /* CAVEAT: absorptive() is a coarser gate than the compiled
189 * reachability route's actual soundness condition. That route
190 * materialises its world enumeration with genuine negation: each
191 * absent edge surfaces as monus(one, edge) (BooleanGate::NOT
192 * lowered to gate_monus; see ReachabilityCompiler.cpp and
193 * CertifiedDDMaterialize.cpp). The absorptive-quotient value
194 * comes out right only because, in every absorptive semiring we
195 * currently ship, (i) monus(one, x) is the times-neutral 'one' on
196 * a present-priced leaf, so the negative literals do not perturb
197 * the path-products, and (ii) any world a negative literal would
198 * kill is dominated by an edge-superset of equal value, hence
199 * absorbed. semiring.absorptive() checks NEITHER property. A
200 * future or user-defined absorptive m-semiring whose monus(one, .)
201 * is not the times-neutral, or whose monus is not
202 * "drop-if-dominated", would pass this gate yet read those
203 * monus(one, edge) gates with a value the path-sum argument does
204 * not justify -- a silently wrong result. If such a semiring is
205 * added, strengthen this guard (e.g. assert monus(one, x) == one
206 * for present-priced leaves, or add a dedicated capability flag)
207 * rather than relying on absorptive() alone. (Truncated cyclic
208 * recursion, the other 'absorptive' producer, ships only minimal
209 * derivations and carries no such negation, so it is unaffected.) */
210 } else
211 throw CircuitException(
212 "Unknown assumption marker '" + assumption + "'");
213 }
214 break;
215 case gate_cmp:
216 {
217 bool ok;
218 cmpOpFromOid(getInfos(u).first, ok);
219 if(!ok)
220 throw CircuitException(
221 "Comparison operator OID " +
222 std::to_string(getInfos(u).first) +
223 " not supported");
224 break;
225 }
226 default:
227 break;
228 }
229
230 /* Internal gate: make sure every child is computed first. */
231 {
232 bool ready = true;
233 for(const auto &c : getWires(u))
234 if(provenance_mapping.find(c) == provenance_mapping.end()) {
235 stack.push_back(c);
236 ready = false;
237 }
238 if(!ready)
239 continue;
240 }
241
242 const auto childValue = [&](int i) -> const typename S::value_type & {
243 return provenance_mapping.at(getWires(u)[i]);
244 };
245
246 switch(t) {
247 case gate_plus:
248 case gate_times:
249 case gate_monus: {
250 std::vector<typename S::value_type> childrenResult;
251 for(const auto &c : getWires(u))
252 childrenResult.push_back(provenance_mapping.at(c));
253 if(t==gate_plus) {
254 childrenResult.erase(std::remove(std::begin(childrenResult), std::end(childrenResult), semiring.zero()),
255 childrenResult.end());
256 provenance_mapping.emplace(u, semiring.plus(childrenResult));
257 } else if(t==gate_times) {
258 bool zero = false;
259 for(const auto &c: childrenResult) {
260 if(c==semiring.zero()) {
261 zero = true;
262 break;
263 }
264 }
265 if(zero)
266 provenance_mapping.emplace(u, semiring.zero());
267 else {
268 childrenResult.erase(std::remove(std::begin(childrenResult), std::end(childrenResult), semiring.one()),
269 childrenResult.end());
270 provenance_mapping.emplace(u, semiring.times(childrenResult));
271 }
272 } else {
273 if(childrenResult[0]==semiring.zero() || childrenResult[0]==childrenResult[1])
274 provenance_mapping.emplace(u, semiring.zero());
275 else
276 provenance_mapping.emplace(u, semiring.monus(childrenResult[0], childrenResult[1]));
277 }
278 break;
279 }
280
281 case gate_delta:
282 provenance_mapping.emplace(u, semiring.delta(childValue(0)));
283 break;
284
285 case gate_project:
286 case gate_eq:
287 case gate_annotation:
288 case gate_assumed:
289 // Where-provenance gates, the transparent annotation wrapper and the
290 // (compatibility-checked above) Boolean-assumption marker: identity
291 // for every admissible semiring. The annotation's extra string is
292 // inert metadata at evaluation time.
293 provenance_mapping.emplace(u, childValue(0));
294 break;
295
296 case gate_cmp:
297 {
298 bool ok;
299 ComparisonOperator op = cmpOpFromOid(getInfos(u).first, ok);
300 provenance_mapping.emplace(u, semiring.cmp(childValue(0), op, childValue(1)));
301 break;
302 }
303
304 case gate_semimod:
305 provenance_mapping.emplace(u, semiring.semimod(childValue(0), childValue(1)));
306 break;
307
308 case gate_agg:
309 {
310 auto infos = getInfos(u);
311
313
314 std::vector<typename S::value_type> vec;
315 for(const auto &c : getWires(u))
316 vec.push_back(provenance_mapping.at(c));
317 provenance_mapping.emplace(u, semiring.agg(op, vec));
318 break;
319 }
320
321 case gate_conditioned: {
322 /* Conditioning marker: P(·|C) requires a normalising division that
323 * no general semiring provides (m-semirings have monus, not a
324 * multiplicative inverse). A conditioned token is evaluable only
325 * in the measure interpretation (probability_evaluate, special-
326 * cased at the root, or the random-variable / agg_token
327 * distribution evaluators); the base-class hook refuses it for
328 * every semiring but the symbolic Formula, which renders the
329 * marker instead of interpreting it. */
330 std::vector<typename S::value_type> vec;
331 for(const auto &c : getWires(u))
332 vec.push_back(provenance_mapping.at(c));
333 provenance_mapping.emplace(u, semiring.conditioned(vec));
334 break;
335 }
336
337 case gate_mobius: {
338 /* The signed Möbius combination is a probability-only shortcut layered
339 * over the normal provenance: the gate carries the literal lineage as a
340 * designated child marked "L:<uuid>" in extra. Every non-probability
341 * evaluator (this semiring path, hence Shapley / Banzhaf / PROV export)
342 * is TRANSPARENT to that lineage, so the token behaves like the ordinary
343 * provenance of the query. A nested gate_mobius (an inner
344 * inclusion-exclusion step) carries no lineage: its value is never used
345 * (the root passes through to the top lineage), but it must not throw, so
346 * it falls back to its first child. */
347 const std::string ex = getExtra(u);
348 gate_t lineage = u; // sentinel: not found
349 const std::string key = "L:";
350 std::size_t p = ex.find(key);
351 if(p != std::string::npos) {
352 std::size_t e = ex.find(' ', p);
353 const std::string luid =
354 ex.substr(p + key.size(),
355 e == std::string::npos ? std::string::npos : e - p - key.size());
356 for(const auto &c : getWires(u))
357 if(getUUID(c) == luid) { lineage = c; break; }
358 }
359 if(lineage != u)
360 provenance_mapping.emplace(u, provenance_mapping.at(lineage));
361 else
362 provenance_mapping.emplace(u, childValue(0));
363 break;
364 }
365
366 case gate_case: {
367 /* Guarded selection over scalar (RV) children: a value chosen by the
368 * first satisfied guard event. This is a measure/RV-carrier operation
369 * (the guards are probabilistic events, the values random variables), not
370 * a semiring one -- evaluable only through the random-variable / measure
371 * evaluators (expected / variance / support / probability / sample),
372 * exactly like gate_rv and gate_arith over RVs. */
373 std::vector<typename S::value_type> vec;
374 for(const auto &c : getWires(u))
375 vec.push_back(provenance_mapping.at(c));
376 provenance_mapping.emplace(u, semiring.guarded_case(vec));
377 break;
378 }
379
380 /* The measure-carrier gates below have no algebraic reading either:
381 * their base-class hooks refuse them for every proper semiring, and
382 * Formula overrides them to render the sub-circuit symbolically. */
383 case gate_rv: {
384 /* A gate_rv is a leaf unless one of its distribution parameters is
385 * wired ("$i" in the extra encoding), which makes it a compound
386 * (latent-variable) leaf over the values of its wires. */
387 std::vector<typename S::value_type> params;
388 for(const auto &c : getWires(u))
389 params.push_back(provenance_mapping.at(c));
390 provenance_mapping.emplace(u, semiring.rv(getExtra(u), params));
391 break;
392 }
393
394 case gate_arith: {
395 bool ok;
396 ArithmeticOperator op = arithOpFromTag(getInfos(u).first, ok);
397 if(!ok)
398 throw CircuitException(
399 "Arithmetic operator tag " +
400 std::to_string(getInfos(u).first) +
401 " not supported");
402 std::vector<typename S::value_type> vec;
403 for(const auto &c : getWires(u))
404 vec.push_back(provenance_mapping.at(c));
405 provenance_mapping.emplace(u, semiring.arith(op, vec, getExtra(u)));
406 break;
407 }
408
409 case gate_mixture: {
410 const auto &w = getWires(u);
411 if(isCategoricalMixture(u)) {
412 /* [key, mul_1, …, mul_n]: each outcome's probability lives in the
413 * mulinput's prob and its value in the mulinput's extra (those
414 * leaves evaluate to one() on their own, so the payload has to be
415 * read off the gate here). */
416 std::vector<double> probs;
417 std::vector<std::string> outcomes;
418 for(std::size_t i = 1; i < w.size(); ++i) {
419 probs.push_back(getProb(w[i]));
420 outcomes.push_back(getExtra(w[i]));
421 }
422 provenance_mapping.emplace(
423 u, semiring.categorical(childValue(0), probs, outcomes));
424 } else {
425 if(w.size() != 3)
426 throw CircuitException(
427 "gate_mixture must have exactly three children "
428 "[p_token, x_token, y_token]");
429 provenance_mapping.emplace(
430 u, semiring.mixture(childValue(0), childValue(1), childValue(2)));
431 }
432 break;
433 }
434
435 case gate_observe: {
436 const auto &w = getWires(u);
437 if(w.size() != 1)
438 throw CircuitException(
439 "gate_observe must have exactly one child (the observed leaf)");
440 provenance_mapping.emplace(u, semiring.observe(childValue(0), getExtra(u)));
441 break;
442 }
443
444 default:
445 throw CircuitException("Invalid gate type for semiring evaluation");
446 }
447
448 stack.pop_back();
449 }
450
451 return provenance_mapping.at(g);
452}
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...