/* * Copyright 2025 Casper Dalgaard Nielsen * Adam Yasser Tallouzi * * Use of this source code is governed by the Apache 2.0 license * that can be found in the LICENSE.md file. */////// This module reconstructs the proof tree given a provenance model./// It does so by re-evaluating the program in a top-down fashion/// to prove the sought after atom. This search is augmented with/// knowledge about which facts are true and the rules used to/// construct them.////// The current implementation assumes that all negative atoms are placed after positive atoms./// More precicely it assumes that all variables in the negative atoms have/// been bound if a rule is evaluated left to right.////// The current implementation assumes that guards satisfy the same property as the negative atoms,/// namely that all their variables are bound when they are encountered when evaluating a rule/// left to right.////// The current implementation does not output the negative atoms. They can be derived from the positive.///pubmod Fixpoint3.ProvenanceReconstruct {use Fixpoint3.Ast.Datalog.{Datalog, Constraint, VarSym, BodyPredicate, HeadPredicate, HeadTerm, BodyTerm, Polarity}use Fixpoint3.Ast.Datalog.Datalog.{Datalog, Model, Join, Provenance}use Fixpoint3.Ast.Ram.{makeFakeRelSym, RelSym}use Fixpoint3.Ast.Shared.{PredSym, Denotation}use Fixpoint3.Boxeduse Fixpoint3.Counteruse Fixpoint3.Util.getOrCrash////// A `ProvIDB` maps from `RelSym` to a map from the facts of the `RelSym`/// to `(depth, ruleUsed)`.///typealiasProvIDB = Map[RelSym, BPlusTree[Vector[Boxed], (Int64, Int32), Static]]////// A `BPLookup` is a over the facts for a specific `RelSym`.////// For a fact set of `(1, 2, 3), (4, 5, 6), (7, 8, 9)`, we might want/// to index based on the second attribute. The `BPLookup` would be/// `{[2] => [1, 3], [5] => [4, 6], [8] => [7, 9]}`.///typealiasBPLookup[r: Region] = BPlusTree[Vector[Boxed], List[(Vector[Boxed], Int64, Int32)], r]////// A mutable map from `PredSym` to a `MutMap` from the wanted lookup-slice to/// a `BPLookup` on said slice.////// Using the previous example with predicate `P` we would have/// `{P => {[1] => [2] => [1, 3], [5] => [4, 6], [8] => [7, 9]}}`///typealiasProvLookupStruct[r: Region] = BPlusTree[PredSym, BPlusTree[Vector[Int32], BPLookup[r], r], r]////// Returns the list of facts for `pred` which satisfies `searchAndVals`.////// If `searchAndVals = [(0, BoxedInt32(2)), (3, BoxedInt32(3))]` this should be/// understood as the first value of the fact of `pred` should be `BoxedInt32(2)`/// and the fourth should be `BoxedInt32(3)`.////// Returns `None` if no values match.////// Returns `Some(v)` where `v` is a list of `(missingFactPart, depth, rule)`. Here/// `missingFactPart` refers to the value-part in the `BPLookup`.///deflookup(rc: Region[r], idb: ProvIDB, searchAndVals: Vector[(Int32, Boxed)], arity: Int32, lookupStruct: ProvLookupStruct[r], pred: PredSym): Option[List[(Vector[Boxed], Int64, Int32)]] \ r + IO =matchMap.get(makeFakeRelSym(pred), idb) {case None => unreachable!() // No facts for `pred`.case Some(fullTree) =>// We are dealing with `Vector[Boxed]` so we cannot use build ordering of `BPlusTree.emptyWithSearch`.// Instead we must manually reorder them to match the `search`.let (search, vals) = Vector.unzip(searchAndVals);letreorderedVals = Vector.init(i -> Vector.get(i, vals), Vector.length(search));letinnerMap = BPlusTree.computeIfAbsent(() -> BPlusTree.empty(rc), pred, lookupStruct);matchBPlusTree.get(search, innerMap) {case Some(tree) => BPlusTree.get(reorderedVals, tree)case None =>// We have never encountered this search before. Initialize the `BPlusTree` for it.// Initializing happens by mapping every fact to the search-part and non-search part// and inserting them as key-value pairs in a `BPLookup`.lettree = BPlusTree.empty(rc);letextractSearchPart = k -> Vector.init(i -> Vector.get(Vector.get(i, search), k), Vector.length(search));letnonSearch = Vector.range(0, arity) |> Vector.filter(v -> notVector.memberOf(v, search));letextractNonSearchPart = k -> Vector.init(i -> Vector.get(Vector.get(i, nonSearch), k), Vector.length(nonSearch));foreach ((k, (depth, rule)) <- fullTree) {letsearchPart = extractSearchPart(k);letnonSearchPart = extractNonSearchPart(k);letnewVal = (nonSearchPart, depth, rule);BPlusTree.putWith(_ -> old -> newVal :: old, searchPart, newVal :: Nil, tree) };BPlusTree.put(search, tree, innerMap);BPlusTree.get(reorderedVals, tree) } }////// A proof tree of an atom.////// `EDB(P, tuple)` signifies that `P(tuple)` is a ground atom, with respect/// to `ProvIDB`.////// `Negative(P, tuple)` signifies that `P(tuple)` is not in the model, with/// respect to `ProvIDB`.////// `IDB(P, tuple, subproofs)` signifies that `P(tuple)` is in the model, with/// respect to `ProvIDB`, and it is not a ground atom. Furthermore `subproofs`/// contains the proofs/facts of the body which (could have) derived `P(tuple)`.///enumProofTree[r: Region] {case EDB(PredSym, Vector[Boxed])case Negative(PredSym, Vector[Boxed])case IDB(PredSym, Vector[Boxed], Array[ProofTree[r], r]) }////// Flattens the proof `p` to a `List`, removing facts that do not satisfy `filterFunc`.////// The tree is traversed in a pre-order fashion meaning that for `IDB` atoms the atom is listed before the children.///deffilterAndFlattenProof(p: ProofTree[r], filter: PredSym -> Bool): Vector[(PredSym, Vector[Boxed])] \ r = regionrc {letres = MutList.empty(rc);defrecurse(workList: List[ProofTree[r]]) = matchworkList {case Nil => ()case ProofTree.EDB(pred, fact) :: tail =>if (filter(pred)) { MutList.push((pred, fact), res) };recurse(tail)case ProofTree.Negative(_, _) :: tail => recurse(tail)case ProofTree.IDB(pred, fact, subTrees) :: tail => {if (filter(pred)) { MutList.push((pred, fact), res) };letfurtherWork = Array.foldRight(cur -> acc -> cur :: acc, tail, subTrees);recurse(furtherWork) } };recurse(p :: Nil);MutList.toVector(res) }////// Returns `None` if `predSym(fact)` is not a part of the provenance model `d`.////// Returns `Some(v)` where `v` is a vector of the facts that can be used to prove `fact`./// In other words they are the result of flattening the proof tree of `predSym(fact)`.////// Crashes if `d` is not a provenance model.///pubdefprovQuery(predSym: PredSym, fact: Vector[Boxed], withh: Vector[PredSym], d: Datalog): Option[Vector[(PredSym, Vector[Boxed])]] =unsafeIO { provQueryInternal(predSym, fact, withh, d) }////// Returns `None` if `predSym(fact)` is not a part of the provenance model `d`.////// Returns `Some(v)` where `v` is a vector of the facts that can be used to prove `fact`./// In other words they are the result of flattening the proof tree of `predSym(fact)`.////// Crashes if `d` is not a provenance model.////// The `IO` effect is from reading from `Static` data structures.///defprovQueryInternal(predSym: PredSym, fact: Vector[Boxed], withh: Vector[PredSym], d: Datalog): Option[Vector[(PredSym, Vector[Boxed])]] \ IO = matchd {case Provenance(rules, provIdb) => regionrc {letannotation = matchMap.get(makeFakeRelSym(predSym), provIdb) {case None => Nonecase Some(v) => matchBPlusTree.get(fact, v) {case None => Nonecase Some(annotations) => Some(annotations) } };matchannotation {case None => Nonecase Some((depth, ruleUsed)) =>letlookupStruct = BPlusTree.empty(rc);lettree = buildProof(predSym, fact, depth, ruleUsed, rules, provIdb, lookupStruct, rc); Some(filterAndFlattenProof(tree, p -> Vector.memberOf(p, withh))) } }case_ => bug!("In Fixpoint.Provenance.provQuery: Can only build a proof from a provenance model") }////// Builds the provenance tree for `topPredSym(topFact)` where `topFact` was derived with/// depth `topDepth` using `rules[topRuleUsed]`.////// `topRuleUsed == -1` means that `topPredSym(topFact)` is an `EDB` atom. `topRuleUsed == -2`/// means that `topPredSym(topFact)` is a negative fact/not a fact in the model.///defbuildProof(topPredSym: PredSym,topFact: Vector[Boxed],topDepth: Int64,topRuleUsed: Int32,rules: Vector[Constraint],provIdb: ProvIDB,lookupStruct: ProvLookupStruct[r],rc: Region[r] ): ProofTree[r] \ r + IO = {letresultArr = Array.empty(rc, 1);////// This is the workhorse for assembling the proof.////// Elements of `workList` consist of `(savePos, saveArr, predSym, (fact, depth, ruleUsed))`./// The meaning is that the proof for `predSym(fact)` with annotations `(depth, ruleUsed)`/// should be stored at `saveArr[savePos]`.////// Proofs of IDB facts are initialized without sub proofs. They contain an empty/// array where sub proofs should be saved. Constructing sub proofs and adding them/// to the array is done by adding a job to `workList`. The job will fill a specific/// position in the array with a sub proof. This sub proof might itself need further/// jobs to finish it and create new jobs.////// The purpose is to allow `buildProofInternal` to be tail-recursive./// Without the `workList` approach deep proofs would lead to a stack overflow.///defbuildProofInternal(workList: List[(Int32, Array[ProofTree[r], r], PredSym, (Vector[Boxed], Int64, Int32))]) = matchworkList {case Nil => Array.get(0, resultArr)case (savePos, saveArr, predSym, (fact, depth, ruleUsed)) :: tail => matchruleUsed {case -1 =>Array.put(ProofTree.EDB(predSym, fact), savePos, saveArr);buildProofInternal(tail)case -2 =>Array.put(ProofTree.Negative(predSym, fact), savePos, saveArr);buildProofInternal(tail)case_ =>letrule = Vector.get(ruleUsed, rules);letbodyAtoms = getMatchingBody(fact, depth, rc, provIdb, lookupStruct, rule);let Constraint.Constraint(_, bodyPreds) = rule;letrelevantPreds = bodyPreds |> Vector.filterMap(bodyAtom -> matchbodyAtom {case BodyPredicate.BodyAtom(pred, _, _, _, _) => Some(pred)case_ => None } );letref = Ref.fresh(rc, tail);letchildProofs = Array.empty(rc, Array.length(bodyAtoms));foreach ((i, atom) <- ForEach.withIndex(relevantPreds)) {letpred = Array.get(i, bodyAtoms);Ref.put((i, childProofs, atom, pred) :: Ref.get(ref), ref) };Array.put(ProofTree.IDB(predSym, fact, childProofs), savePos, saveArr);buildProofInternal(Ref.get(ref)) } };buildProofInternal((0, resultArr, topPredSym, (topFact, topDepth, topRuleUsed)) :: Nil) }////// Returns a vector of `(values, subDepth, subRule)` that satisfy `rule` and would imply `fact`./// In the tuple `subDepth` is the depth at which `values` was derived and `subRule` the rule used/// to derive it.////// In other words return a vector of facts that could have derived `fact`,/// assuming `fact` was derived by `rule`.////// The returned facts will have a depth stricly smaller than `depth`.///defgetMatchingBody(fact: Vector[Boxed],depth: Int64,rc: Region[r],provIdb: ProvIDB,lookupStruct: ProvLookupStruct[r],rule: Constraint ): Array[(Vector[Boxed], Int64, Int32), r] \ r + IO = {let Constraint.Constraint(HeadPredicate.HeadAtom(_, _, headTerms), bodyPreds) = rule;// Array which contains the current assignment of body atoms.// Not all body predicates will be atoms. After termination of `loop` a// `None` at position `i` will be mean that `rule[i]` was not a body atom.letbodyValues: Array[Option[(Vector[Boxed], Int64, Int32)], r] = Array.repeat(rc, Vector.length(bodyPreds), None);letheadApps = Vector.zip(headTerms, fact) |> Vector.filter(match (h, _) -> matchh {case (HeadTerm.App1(_, _)) => truecase (HeadTerm.App2(_, _, _)) => truecase (HeadTerm.App3(_, _, _, _)) => truecase (HeadTerm.App4(_, _, _, _, _)) => truecase (HeadTerm.App5(_, _, _, _, _, _)) => truecase_ => false } );letguards = bodyPreds |> Vector.filter(bodyAtom -> matchbodyAtom {case BodyPredicate.Guard1(_, _) => truecase BodyPredicate.Guard2(_, _, _) => truecase BodyPredicate.Guard3(_, _, _, _) => truecase BodyPredicate.Guard4(_, _, _, _, _) => truecase BodyPredicate.Guard5(_, _, _, _, _, _) => true// Filter out Guard0. They must be true.case_ => false } );////// This is a top down evaluation of `rule`.////// Attempts to satisfy `bodyPreds`, having already found a satisfying assignment/// for `bodyPreds[i]`, where `0 <= i < index`.////// Returns `true` if the current values in `varSymToVal` and `bodyValues` satisfies `rule`/// and `false` otherwise.///defloop(index, varSymToVal) = {if (index==Vector.length(bodyPreds)) {// Base case. We have satisfied all atoms and found a satisfying assignment.areHeadApplicationsSatisfied(headApps, varSymToVal) andareGuardsSatisfied(guards, varSymToVal) } else {matchVector.get(index, bodyPreds) {case BodyPredicate.BodyAtom(pred, _, Polarity.Positive, _, expected) =>// Collect the bound variables in a list so they can be used// to by `lookup`.// `(2, val)` being in the list means that attribute 3 of the tuple// must have value `val`. View this as a `zipWithIndex |> filter`.letfilterAsList = evalGroundTerms(expected, varSymToVal);// List of `VarSym` that become bound when the atom is// satisfied.letfreeVars = getFreeVarSyms(expected, varSymToVal);// Get a list of all values that match the current assignment of body atoms.letmatches = lookup(rc, provIdb, filterAsList, Vector.length(expected), lookupStruct, pred);matchmatches {case None => false// Nothing matches the current assignment. Return false to indicate this.case Some(facts) => {// Go through the found facts and filter out any fact that has a depth higher than// the fact we are trying to prove.// For the kept facts check whether they satisfy the whole rule and if not discard them.facts |> List.exists(match (bodyFact, bodyDepth, bodyRule) -> {if (bodyDepth>=depth) {// The depth of the body fact is higher than the depth of the head.// Reject this to avoid infinite loops.false } else {// This combination could be delayed until we knew we had a satisfying assignment.Array.put(Some((combineToFact(filterAsList, bodyFact), bodyDepth, bodyRule)), index, bodyValues);letnewMap = registerVars(freeVars, bodyFact, varSymToVal);loop(index+1, newMap) } } ) } }case BodyPredicate.BodyAtom(pred, _, Polarity.Negative, _, expected) =>// `not pred(tuple)` is true if `pred(tuple)` is not in the model.lettuple = evalOrCrash(expected, varSymToVal);letisMatch = matchMap.get(makeFakeRelSym(pred), provIdb) {case None => falsecase Some(facts) => BPlusTree.memberOf(tuple, facts) };if (isMatch)falseelse {// No need to save the match. Negative atoms cannot bind variables.Array.put(Some((tuple, -2i64, -2)), index, bodyValues);loop(index+1, varSymToVal) }case BodyPredicate.Functional(outVars, func, inVars) =>letargs = Vector.map(varSym -> getVal(varSym, varSymToVal), inVars);letresult = func(args);Vector.exists(tuple -> {letnewMap = registerVars(outVars, tuple, varSymToVal);loop(index+1, newMap) },result )// We evaluate these latercase BodyPredicate.Guard0(_) =>loop(index+1, varSymToVal)case BodyPredicate.Guard1(_, _) =>loop(index+1, varSymToVal)case BodyPredicate.Guard2(_, _, _) =>loop(index+1, varSymToVal)case BodyPredicate.Guard3(_, _, _, _) =>loop(index+1, varSymToVal)case BodyPredicate.Guard4(_, _, _, _, _) =>loop(index+1, varSymToVal)case BodyPredicate.Guard5(_, _, _, _, _, _) =>loop(index+1, varSymToVal) } } };// A mapping from `VarSym` to its current value.letvarSymToVal: MutMap[VarSym, Boxed, r] = MutMap.empty(rc);// Bind all variables in the head atom to the values of `fact`.foreach ((i, h) <- ForEach.withIndex(headTerms)) {matchh {case HeadTerm.Var(v) => MutMap.put(v, Vector.get(i, fact), varSymToVal)case_ => () } };discardloop(0, MutMap.toMap(varSymToVal));Array.filterMap(rc, identity, bodyValues) }////// Returns the current value of `varName`.///defgetVal(varName: VarSym, varSymToVal: Map[VarSym, Boxed]): Boxed = getOrCrash(Map.get(varName, varSymToVal))////// Evaluates `terms` with respect to `varSymToVal`. Crashes if a free variable is met.////// `BodyTerm.Wild` counts as free variables for this purpose.///defevalOrCrash(terms: Vector[BodyTerm], varSymToVal: Map[VarSym, Boxed]): Vector[Boxed] =terms |> Vector.map(t -> matcht {case BodyTerm.Var(v) => getVal(v, varSymToVal)case BodyTerm.Lit(val) => valcase BodyTerm.Wild => unreachable!() } )////// Evaluates the ground terms in `terms` with respect to `varSymToVal` and return a vector of/// `(index, value)` where `eval(terms[index]) == value`. The list is sorted by the `index`,/// smallest to largest.///defevalGroundTerms(terms: Vector[BodyTerm], varSymToVal: Map[VarSym, Boxed]): Vector[(Int32, Boxed)] =let (filterAsList, _) = ((Nil, 0), terms) ||> Vector.foldLeft(match (acc, i) -> t -> matcht {case BodyTerm.Wild => (acc, i+1)case BodyTerm.Var(v) => matchMap.get(v, varSymToVal) {case None => (acc, i+1)case Some(val) => ((i, val) :: acc, i+1) }case BodyTerm.Lit(val) => ((i, val) :: acc, i+1) } );List.toVector(List.reverse(filterAsList))////// Return a Vector of `VarSym` in `terms` which are currently free, according to `varSymToVal`.////// `BodyTerm.Wild` is considered free and will be returned as `VarSym.VarSym("_")`.///defgetFreeVarSyms(terms: Vector[BodyTerm], varSymToVal: Map[VarSym, Boxed]): Vector[VarSym] =terms |> Vector.filterMap(t -> matcht {case BodyTerm.Var(v) => matchMap.memberOf(v, varSymToVal) {casefalse => Some(v)casetrue => None }// We need to keep the order and positions for `registerVars`.// We do not care about what `_` has been assigned to.case BodyTerm.Wild => Some(VarSym.VarSym("_"))case_ => None } )////// Register the values new values of `varNames` to be `matchedFacts`.////// Concretely, we update the `varSymToVal` to contain `varNames[i] => matchedFacts[i]`.///defregisterVars(varNames: Vector[VarSym],matchedFact: Vector[Boxed],varSymToVal: Map[VarSym, Boxed] ): Map[VarSym, Boxed] = regionrc {letresult = MutMap.empty(rc);Map.forEach((k, v) -> MutMap.put(k, v, result), varSymToVal);foreach ((i, v) <- ForEach.withIndex(varNames)) {MutMap.put(v, Vector.get(i, matchedFact), result) };MutMap.toMap(result) }////// Returns true if all guards are satisfied by the current variable assignment.///defareGuardsSatisfied(guards: Vector[BodyPredicate], varSymToVal: Map[VarSym, Boxed]): Bool = {guards |> Vector.forAll(guard -> matchguard {case BodyPredicate.Guard1(f, v1) => f(getVal(v1, varSymToVal))case BodyPredicate.Guard2(f, v1, v2) => f(getVal(v1, varSymToVal), getVal(v2, varSymToVal))case BodyPredicate.Guard3(f, v1, v2, v3) => f(getVal(v1, varSymToVal), getVal(v2, varSymToVal), getVal(v3, varSymToVal))case BodyPredicate.Guard4(f, v1, v2, v3, v4) => f(getVal(v1, varSymToVal), getVal(v2, varSymToVal), getVal(v3, varSymToVal), getVal(v4, varSymToVal))case BodyPredicate.Guard5(f, v1, v2, v3, v4, v5) => f(getVal(v1, varSymToVal), getVal(v2, varSymToVal), getVal(v3, varSymToVal), getVal(v4, varSymToVal), getVal(v5, varSymToVal))case_ => unreachable!() } ) }////// Returns true if all head applications are satisfied by the current variable assignment.///defareHeadApplicationsSatisfied(headApps: Vector[(HeadTerm, Boxed)], varSymToVal: Map[VarSym, Boxed]): Bool = {headApps |> Vector.forAll(match (app, val) -> matchapp {case (HeadTerm.App1(f, v1)) => val==f(getVal(v1, varSymToVal))case (HeadTerm.App2(f, v1, v2)) => val==f(getVal(v1, varSymToVal), getVal(v2, varSymToVal))case (HeadTerm.App3(f, v1, v2, v3)) => val==f(getVal(v1, varSymToVal), getVal(v2, varSymToVal), getVal(v3, varSymToVal))case (HeadTerm.App4(f, v1, v2, v3, v4)) => val==f(getVal(v1, varSymToVal), getVal(v2, varSymToVal), getVal(v3, varSymToVal), getVal(v4, varSymToVal))case (HeadTerm.App5(f, v1, v2, v3, v4, v5)) => val==f(getVal(v1, varSymToVal), getVal(v2, varSymToVal), getVal(v3, varSymToVal), getVal(v4, varSymToVal), getVal(v5, varSymToVal))case_ => unreachable!() } ) }////// Combines `filter` and `newValues` to a single vector.////// This is used to combine the search/lookup for a fact with the unknown/newly bound values.////// As an for `filter = [(1, Box(99)), (3, Box(42))]` and `newValues = [Box(23), Box(55), Box(77)]`/// Would return `[Box(23), Box(99), Box(55), Box(42), Box(77)]`///defcombineToFact(filter: Vector[(Int32, Boxed)], newValues: Vector[Boxed]): Vector[Boxed] = regionrc {letc1 = Counter.fresh(rc);letc2 = Counter.fresh(rc);letsize = Vector.length(filter) +Vector.length(newValues);Vector.init(i -> {if (Counter.get(c1) <Vector.length(filter) andi==fst(Vector.get(Counter.get(c1), filter))) {snd(Vector.get(Counter.getAndIncrement(c1), filter)) } else {Vector.get(Counter.getAndIncrement(c2), newValues) } },size ) }}