From bf419fad7f73fc64e5cb078b812c7ef6a3f08f8a Mon Sep 17 00:00:00 2001
From: Sallinen <alex.sallinen@gmail.com>
Date: Wed, 12 Oct 2022 13:34:35 +0200
Subject: [PATCH] Proof transformations (#66)

Proof Transformation: Utility to transform a conditional proof into an unconditional proof of a conditional statement. Move the imports to the left of the conclusion.
---
 .../ProofUnconditionalizer.scala              | 310 ++++++++++++++++++
 .../lisa/utilities/Transformations.scala      | 130 ++++++++
 2 files changed, 440 insertions(+)
 create mode 100644 src/main/scala/lisa/utilities/prooftransform/ProofUnconditionalizer.scala
 create mode 100644 src/test/scala/lisa/utilities/Transformations.scala

diff --git a/src/main/scala/lisa/utilities/prooftransform/ProofUnconditionalizer.scala b/src/main/scala/lisa/utilities/prooftransform/ProofUnconditionalizer.scala
new file mode 100644
index 00000000..fa9dba79
--- /dev/null
+++ b/src/main/scala/lisa/utilities/prooftransform/ProofUnconditionalizer.scala
@@ -0,0 +1,310 @@
+package lisa.utilities.prooftransform
+import lisa.kernel.fol.FOL.*
+import lisa.kernel.proof.SCProof
+import lisa.kernel.proof.SequentCalculus.*
+import lisa.utils.Helpers.*
+import lisa.utils.Printer
+
+/**
+ * A generic transformation on a proof with some useful utilities functions.
+ *
+ * Beware that the parameter should really be the same proof you will mainly work on
+ * @param pr the proof to apply the transformation on
+ */
+abstract class ProofTransformer(pr: SCProof) {
+
+  // PUBLIC FUNCTIONS###################################################################################################################
+  /**
+   * Main transformation function, should be the only function needed to be called by the user
+   *
+   * @return the transformed proof
+   */
+  def transform(): SCProof
+
+  // PRIVATE FUNCTIONS##################################################################################################################
+  /**
+   * Gives for a given step all step that lead to it
+   *
+   * @param step the step for which the predecessors are to be found
+   * @return a set of all steps that lead to the given step
+   */
+  protected def adjacencyMatrix[A](pr: IndexedSeq[A], imports: A => Seq[Int]): Map[Int, Set[Int]] = {
+    val steps = pr
+    val adjMat = scala.collection.mutable.Map[Int, Set[Int]]().withDefault(_ => Set.empty)
+
+    steps.zipWithIndex.foreach((step, i) => {
+      adjMat(i) = Set.empty ++ adjMat(i)
+      imports(step).foreach((prem) => {
+        if (prem > 0)
+          adjMat(prem) = adjMat(prem) + i
+      })
+    })
+    adjMat.toMap
+  }
+
+  /**
+   * Gives a set of step indices that are the maximal tree spanning from the given step index embeded in the given proof
+   *
+   * @param st the root of the tree
+   * @param pr the proof to be used as a graph
+   * @return a set of step indices that are the maximal tree spanning from the given step index embeded in the given proof
+   */
+  protected def findBranch(st: Int, pr: SCProof): Seq[Int] = {
+
+    val adjMat = adjacencyMatrix(pr.steps, (s) => s.premises)
+
+    pr(st).premises.filter(_ > 0) match {
+      case Nil => Seq(st)
+      case pp if adjMat(st).size == 1 => st +: pp.flatMap((prem) => findBranch(prem, pr))
+      case _ => Nil
+    }
+
+  }
+
+  /**
+   * Assigns to each node of a DAG all the imports (as defined in the parameters) attainable from it
+   * Executes in liniear time
+   * @param top_sort a topologically sorted array representing the DAG
+   * @param children returns a list of all nodes linked to a given node
+   * @param imports implementation dependent
+   * @return node -> [imports(v) | exists a path between node and v]
+   */
+  protected def dependency_finder[A](top_sort: IndexedSeq[A], children: A => Seq[A], imports: A => Seq[Int]): Map[A, Set[Int]] = {
+    var cache = scala.collection.mutable.Map[A, Set[Int]]().withDefault(_ => Set())
+    def is_leaf(node: A): Boolean = imports(node).isEmpty | imports(node).forall(_ < 0)
+    val adjMat = adjacencyMatrix(top_sort, imports)
+
+    val rev = top_sort.reverse
+    def inner(v: A): Seq[Int] = v match {
+      case _ if is_leaf(v) => {
+        cache(v) = imports(v).toSet
+        imports(v)
+      }
+      case _ => {
+        val current = imports(v).filter(_ < 0)
+        val res = children(v).flatMap(inner) ++ current
+        cache(v) = res.toSet
+        res
+      }
+    }
+    // Checks for empty proof
+    val roots = adjMat.filter((i, s) => s.isEmpty).keySet.map(top_sort(_))
+    roots.foreach(inner)
+    cache.toMap
+  }
+
+  protected def sequentToFormulaNullable(s: Sequent): Formula = (s.left.toSeq, s.right.toSeq) match {
+
+    case (Nil, _) => ConnectorFormula(Or, s.right.toSeq)
+    case (_, Nil) => ConnectorFormula(And, s.left.toSeq)
+    case (h :: Nil, _) => ConnectorFormula(Implies, List(h, ConnectorFormula(Or, s.right.toSeq)))
+    case (_, h :: Nil) => ConnectorFormula(Implies, List(ConnectorFormula(And, s.left.toSeq), h))
+    case (_, _) => sequentToFormula(s)
+
+  }
+}
+
+/**
+ * A transformation that ensures its result is a proof where no instantiaiton are left, rreplaced by imports and rewrites of their conclusion
+ *
+ * @param pr the proof to be modified
+ */
+case class ProofInstantiationRemover(pr: SCProof) extends ProofTransformer(pr) {
+
+  private val adjMat = adjacencyMatrix(pr.steps, s => s.premises)
+  private val neg_premises = dependency_finder(pr.steps, ps => ps.premises.filter(_ >= 0).map(pr.steps(_)), ps => ps.premises)
+
+  // PUBLIC FUNCTIONS###################################################################################################################
+  /**
+   * Maps a proof to a new proof where Instantiations are replaced by hypothesis
+   *
+   * @return the modified proof
+   */
+  def transform(): SCProof = instsAsImports(pr)
+
+  // PRIVATE FUNCTIONS##################################################################################################################
+  /**
+   * determines if a step is a problematic instantiation that should be transformed into an import
+   *
+   * @param step the step to check for
+   * @return whether the given step is an Instantiation tha thas a link to an import
+   */
+  private def isInst(step: SCProofStep): Boolean = step match {
+    case InstPredSchema(_, t, _) if !neg_premises.getOrElse(step, Seq.empty).isEmpty => true
+    case InstFunSchema(_, t, _) if !neg_premises.getOrElse(step, Seq.empty).isEmpty => true
+    case _ => false
+  }
+
+  /**
+   * Maps Instantiations to Rewrites of imports
+   *
+   * @return a proof where all instantiations are rewrites of imports
+   */
+  private def instsAsImports(pr: SCProof): SCProof = {
+    val imps = pr.steps.zipWithIndex.filter((st, j) => isInst(st)).map(_._2).zipWithIndex
+
+    val mImps = imps.toMap
+    val mSteps = pr.steps.zipWithIndex.map((s, j) =>
+      s match {
+        case _ if isInst(s) => Rewrite(() |- sequentToFormulaNullable(s.bot), -pr.imports.length - mImps(j) - 1)
+        case _ => s
+      }
+    )
+
+    SCProof(mSteps, pr.imports ++ imps.map((j, i) => mSteps(j).bot))
+  }
+
+}
+
+case class ProofUnconditionalizer(prOrig: SCProof) extends ProofTransformer(prOrig) {
+
+  // ATTRIBUTES#########################################################################################################################
+  private val pr = ProofInstantiationRemover(prOrig).transform()
+  private val neg_premises = dependency_finder(pr.steps, ps => ps.premises.filter(_ >= 0).map(pr.steps(_)), ps => ps.premises)
+
+  // We use a var because Subproofs require the results of precedent steps, topological order on proofs ensure we can always do this
+  private var appended_steps = IndexedSeq[SCProofStep]()
+  // PREDICATES#########################################################################################################################
+  /**
+   * Wether a step needs to be more modified than just appending hypothesis on the left of the conclusion
+   *
+   * @param sc the step to analyse
+   * @return whether the step has a negative premise
+   */
+  private def is_problematic(sc: SCProofStep): Boolean = sc.premises.exists(_ < 0)
+
+  // PUBLIC FUNCTIONS###################################################################################################################
+  /**
+   * Maps a proof to a new proof where all imports are made into suppositions on the left of the conclusion
+   *
+   * @return A proof where each steps has the necessary imports added as suppostitions on the left of their bottom
+   */
+  def transform(): SCProof = transformPrivate(IndexedSeq.empty)
+
+  // PRIVATE FUNCTIONS##################################################################################################################
+  /**
+   * Private version used to keep track of imports that should not be removed, useful for SubProofs
+   *
+   * @param keep The indices not to be removed from the transformed proof
+   * @return a proof zhose imports have been removed except for those present in keep
+   */
+  private def transformPrivate(keep: IndexedSeq[Sequent]): SCProof = {
+    pr.steps.zipWithIndex.foreach((pS, i) => {
+      appended_steps = appended_steps.appended(if (is_problematic(pS)) {
+        mapProb(pS)
+      } else {
+        mapStep(pS, b => neg_premises.getOrElse(pS, Set[Int]()).map(i => sequentToFormulaNullable(pr.imports(-i - 1))) ++ b)
+      })
+    })
+    SCProof(appended_steps.toIndexedSeq, keep)
+  }
+
+  /**
+   * Deals with steps that have a negative premise
+   * The strategy is to systematically add an hypothesis to introduce the import, rewrite it
+   * to remove unwanted clutter and apply strictly the same transformation as if there was no negative premise.
+   * To not mess with indices of proof and to keep a clearer proof structure all those steps are wrapped into a SubProof
+   *
+   * @param pS thestep to be modified
+   * @return a subproof whose conclusion is the same step as in the argument but with changed premises and a left side bottom modified as
+   * in the general algorithm
+   */
+  private def mapProb(pS: SCProofStep) = {
+    val premises = pS.premises
+      .filter(_ < 0)
+      .map(i => pr.imports(-i - 1))
+    val hypothesis = premises
+      .map((se) => {
+        val h = Hypothesis((se.left + sequentToFormulaNullable(se)) |- (se.right + sequentToFormulaNullable(se)), sequentToFormulaNullable(se))
+        h
+      })
+    val rewrites = hypothesis.zipWithIndex.map((h, i) => {
+      val r = Rewrite(h.bot.left |- (h.bot.right - sequentToFormulaNullable(premises(i))), i)
+      r
+    })
+    // We must separate the case for subproofs for they need to use the modified version of the precedent
+    val inner = pS match {
+      case SCSubproof(_, _, _) =>
+        SCProof(
+          hypothesis.toIndexedSeq ++ rewrites.toIndexedSeq ++ IndexedSeq(
+            mapStep(
+              pS,
+              b => neg_premises(pS).map(i => sequentToFormulaNullable(pr.imports(-i - 1))) ++ b,
+              s =>
+                s.map(i =>
+                  i match {
+                    case i if i < 0 => -i - 2 + hypothesis.length
+                    case i => -i
+                  }
+                ),
+              rewrites.map(_.bot)
+            )
+          ),
+          pS.premises.filter(_ >= 0).map(appended_steps(_).bot).toIndexedSeq
+        )
+
+      case _ =>
+        SCProof(
+          hypothesis.toIndexedSeq ++ IndexedSeq(
+            mapStep(
+              pS,
+              b => neg_premises(pS).map(i => sequentToFormulaNullable(pr.imports(-i - 1))) ++ b,
+              premise =>
+                premise.zipWithIndex.map((i, j) =>
+                  i match {
+                    case i if i < 0 => j
+                    case i => -j
+                  }
+                )
+            )
+          ),
+          pS.premises.filter(_ >= 0).map(appended_steps(_).bot).toIndexedSeq
+        )
+    }
+    SCSubproof(inner, pS.premises.filter(_ >= 0))
+  }
+
+  /**
+   * This function transform generically a proof step into another one
+   *
+   * @param pS the proofstep to modify
+   * @param f the transformation on left side of bottom
+   * @param fi transformation on premises
+   * @param fsub sequents to keep on subproofs
+   * @return a proofstep where each function is applied to the corresponding
+   */
+  protected def mapStep(pS: SCProofStep, f: Set[Formula] => Set[Formula], fi: Seq[Int] => Seq[Int] = identity, fsub: Seq[Sequent] = Nil): SCProofStep = pS match {
+    case Rewrite(bot, t1) => Rewrite(f(bot.left) |- bot.right, fi(pS.premises).head)
+    case Hypothesis(bot, phi) => Hypothesis(f(bot.left) |- bot.right, phi)
+    case Cut(bot, t1, t2, phi) => Cut(f(bot.left) |- bot.right, fi(pS.premises).head, fi(pS.premises).last, phi)
+    case LeftAnd(bot, t1, phi, psi) => LeftAnd(f(bot.left) |- bot.right, fi(pS.premises).head, phi, psi)
+    case LeftOr(bot, t, disjuncts) => LeftOr(f(bot.left) |- bot.right, fi(t), disjuncts)
+    case LeftImplies(bot, t1, t2, phi, psi) => LeftImplies(f(bot.left) |- bot.right, fi(pS.premises).head, t2, phi, psi)
+    case LeftIff(bot, t1, phi, psi) => LeftIff(f(bot.left) |- bot.right, fi(pS.premises).head, phi, psi)
+    case LeftNot(bot, t1, phi) => LeftNot(f(bot.left) |- bot.right, fi(pS.premises).head, phi)
+    case LeftForall(bot, t1, phi, x, t) => LeftForall(f(bot.left) |- bot.right, fi(pS.premises).head, phi, x, t)
+    case LeftExists(bot, t1, phi, x) => LeftExists(f(bot.left) |- bot.right, fi(pS.premises).head, phi, x)
+    case LeftExistsOne(bot, t1, phi, x) => LeftExistsOne(f(bot.left) |- bot.right, fi(pS.premises).head, phi, x)
+    case RightAnd(bot, t, cunjuncts) => RightAnd(f(bot.left) |- bot.right, fi(t), cunjuncts)
+    case RightOr(bot, t1, phi, psi) => RightOr(f(bot.left) |- bot.right, fi(pS.premises).head, phi, psi)
+    case RightImplies(bot, t1, phi, psi) => RightImplies(f(bot.left) |- bot.right, fi(pS.premises).head, phi, psi)
+    case RightIff(bot, t1, t2, phi, psi) => RightIff(f(bot.left) |- bot.right, fi(pS.premises).head, fi(pS.premises).last, phi, psi)
+    case RightNot(bot, t1, phi) => RightNot(f(bot.left) |- bot.right, fi(pS.premises).head, phi)
+    case RightForall(bot, t1, phi, x) => RightForall(f(bot.left) |- bot.right, fi(pS.premises).head, phi, x)
+    case RightExists(bot, t1, phi, x, t) => RightExists(f(bot.left) |- bot.right, fi(pS.premises).head, phi, x, t)
+    case RightExistsOne(bot, t1, phi, x) => RightExistsOne(f(bot.left) |- bot.right, fi(pS.premises).head, phi, x)
+    case Weakening(bot, t1) => Weakening(f(bot.left) |- bot.right, fi(pS.premises).head)
+    case LeftRefl(bot, t1, fa) => LeftRefl(f(bot.left) |- bot.right, fi(pS.premises).head, fa)
+    case RightRefl(bot, fa) => RightRefl(f(bot.left) |- bot.right, fa)
+    case LeftSubstEq(bot, t1, equals, lambdaPhi) => LeftSubstEq(f(bot.left) |- bot.right, fi(pS.premises).head, equals, lambdaPhi)
+    case RightSubstEq(bot, t1, equals, lambdaPhi) => RightSubstEq(f(bot.left) |- bot.right, fi(pS.premises).head, equals, lambdaPhi)
+    case LeftSubstIff(bot, t1, equals, lambdaPhi) => LeftSubstIff(f(bot.left) |- bot.right, fi(pS.premises).head, equals, lambdaPhi)
+    case RightSubstIff(bot, t1, equals, lambdaPhi) => RightSubstIff(f(bot.left) |- bot.right, fi(pS.premises).head, equals, lambdaPhi)
+    case InstFunSchema(bot, t1, insts) => InstFunSchema(instantiateFunctionSchemaInSequent(f(bot.left) |- bot.right, insts).left |- bot.right, fi(pS.premises).head, insts)
+    case InstPredSchema(bot, t1, insts) => InstPredSchema(instantiatePredicateSchemaInSequent(f(bot.left) |- bot.right, insts).left |- bot.right, fi(pS.premises).head, insts)
+    case SCSubproof(pp, t, b) => {
+      SCSubproof(ProofUnconditionalizer(pp).transformPrivate(fsub.toIndexedSeq ++ t.filter(_ >= 0).map(i => appended_steps(i).bot).toIndexedSeq), fi(t), b)
+    }
+  }
+
+}
diff --git a/src/test/scala/lisa/utilities/Transformations.scala b/src/test/scala/lisa/utilities/Transformations.scala
new file mode 100644
index 00000000..f51cf3aa
--- /dev/null
+++ b/src/test/scala/lisa/utilities/Transformations.scala
@@ -0,0 +1,130 @@
+package lisa.utilities
+import lisa.automation.kernel.Destructors.*
+import lisa.automation.kernel.ProofTactics.*
+import lisa.kernel.fol.*
+import lisa.kernel.proof.SCProof
+import lisa.test.ProofCheckerSuite
+import lisa.utils.Helpers.given_Conversion_VariableLabel_Term
+import lisa.utils.Printer
+
+class Transformations extends ProofCheckerSuite {
+  import lisa.settheory.SetTheoryLibrary.*
+
+  test("Trasnsformation initialises well with empty proof and returns an empty proof") {
+    val nullSCProof = SCProof()
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(nullSCProof)
+    assert(transf.transform() == nullSCProof)
+  }
+
+  /**
+   * Any proof where there are no imports shoud not be modified
+   * Dummy proofs of varying size should be tested
+   */
+  test("A proof with no imports is not modified") {
+    val phi = SchematicPredicateLabel("phi", 0)
+
+    val intro = Hypothesis((phi()) |- (phi()), phi())
+    val outro = Rewrite((phi()) |- (phi()), 0)
+
+    val noImpProof = SCProof(IndexedSeq(intro, outro), IndexedSeq.empty)
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(noImpProof)
+
+    assert((transf.transform() == noImpProof))
+    checkProof(noImpProof)
+  }
+
+  test("A proof with imports is to be modified") {
+    val phi = SchematicPredicateLabel("phi", 0)
+
+    val intro = Rewrite(() |- phi(), -1)
+    val outro = Weakening(intro.bot.right |- intro.bot.right, 0)
+
+    val noImpProof = SCProof(IndexedSeq(intro, outro), IndexedSeq(intro.bot))
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(noImpProof).transform()
+
+    checkProof(transf)
+    assert(transf != noImpProof)
+    assert(isSameSequent(transf.steps.last.bot, (sequentToFormula(intro.bot)) |- intro.bot.right))
+  }
+
+  test("A proof with imports and a step taking multiple premises should be modified accordingly") {
+    val phi = SchematicPredicateLabel("phi", 0)()
+    val psi = SchematicPredicateLabel("psi", 0)()
+
+    val into1 = Rewrite(() |- phi, -2)
+    val into2 = Rewrite(() |- psi, -1)
+    val merge = RightAnd(() |- ConnectorFormula(And, (into1.bot.right ++ into2.bot.right).toSeq), Seq(-2, 0), (into1.bot.right ++ into2.bot.right).toSeq)
+
+    val noImpProof = SCProof(IndexedSeq(into2, merge), IndexedSeq(into2.bot, into1.bot))
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(noImpProof).transform()
+
+    checkProof(transf)
+    assert(transf != noImpProof)
+    assert(isSameSequent(transf.steps.last.bot, (sequentToFormula(into1.bot), sequentToFormula(into2.bot)) |- merge.bot.right))
+  }
+
+  test("A proof with imports and a subproof should be modified accordingly") {
+    val phi = SchematicPredicateLabel("phi", 0)()
+    val intro = Rewrite(() |- phi, -1)
+    val outro = SCSubproof(SCProof(IndexedSeq(Weakening(intro.bot.right |- intro.bot.right, -1)), IndexedSeq(intro.bot)), IndexedSeq(0))
+    val noImpProof = SCProof(IndexedSeq(intro, outro), IndexedSeq(intro.bot))
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(noImpProof).transform()
+
+    checkProof(transf)
+    assert(transf != noImpProof)
+    assert(isSameSequent(transf.steps.last.bot, (outro.bot.left + sequentToFormula(intro.bot)) |- outro.bot.right))
+
+  }
+
+  test("A proof with imports and a complete instantiation should be modified accordingly") {
+    val phi = SchematicPredicateLabel("phi", 0)
+    val psi = SchematicPredicateLabel("psi", 2)
+    val x = VariableLabel("x")
+    val y = VariableLabel("y")
+
+    val intro = Rewrite(() |- phi(), -1)
+    val outro = InstPredSchema(() |- psi(x, y), 0, Map((phi, LambdaTermFormula(Seq(), psi(x, y)))))
+    val noImpProof = SCProof(IndexedSeq(intro, outro), IndexedSeq(intro.bot))
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(noImpProof).transform()
+
+    checkProof(transf)
+    assert(transf != noImpProof)
+    assert(isSameSequent(transf.steps.last.bot, (sequentToFormula(outro.bot)) |- outro.bot.right))
+
+  }
+
+  test("A proof with imports and two partial instantiations should be modified accordingly") {
+    val phi = SchematicPredicateLabel("phi", 0)
+    val psi = SchematicPredicateLabel("psi", 2)
+    val lambda = SchematicPredicateLabel("lambda", 2)
+    val x = VariableLabel("x")
+    val y = VariableLabel("y")
+
+    val intro = Rewrite(() |- phi(), -1)
+    val mid = InstPredSchema(() |- psi(x, y) <=> phi(), 0, Map((phi, LambdaTermFormula(Seq(), psi(x, y) <=> phi()))))
+    val outro = InstPredSchema(() |- psi(x, y) <=> lambda(x, y), 1, Map((phi, LambdaTermFormula(Seq(), lambda(x, y)))))
+    val weak = Weakening((psi(x, y)) |- psi(x, y) <=> lambda(x, y), 2)
+    val noImpProof = SCProof(IndexedSeq(intro, mid, outro, weak), IndexedSeq(intro.bot))
+    val transf = lisa.utilities.prooftransform.ProofUnconditionalizer(noImpProof).transform()
+
+    checkProof(transf)
+    assert(transf != noImpProof)
+    assert(isSameSequent(transf.steps.last.bot, ((weak.bot.left + sequentToFormula(outro.bot))) |- weak.bot.right))
+  }
+
+  test("A proof with instantiation can be transformed into one with a rewrite and an import") {
+    val phi = SchematicPredicateLabel("phi", 0)
+    val psi = SchematicPredicateLabel("psi", 2)
+    val x = VariableLabel("x")
+    val y = VariableLabel("y")
+
+    val intro = Rewrite(() |- phi(), -1)
+    val outro = InstPredSchema(() |- psi(x, y), 0, Map((phi, LambdaTermFormula(Seq(), psi(x, y)))))
+    val noImpProof = SCProof(IndexedSeq(intro, outro), IndexedSeq(intro.bot))
+    val transf = lisa.utilities.prooftransform.ProofInstantiationRemover(noImpProof).transform()
+
+    checkProof(transf)
+    assert(transf != noImpProof)
+    assert(isSameSequent(transf.steps.last.bot, () |- outro.bot.right))
+  }
+}
-- 
GitLab