Skip to content
Snippets Groups Projects
  • Etienne Kneuss's avatar
    471e7135
    Introduce RepairHole · 471e7135
    Etienne Kneuss authored
    - evaluate it to simplest value
    - solve it to arbitrary value
    - Use within CEGLESS as bank of exprs
    - Avoid GuidedCloser if non-det expr (contains choose, holes, ..)
    471e7135
    History
    Introduce RepairHole
    Etienne Kneuss authored
    - evaluate it to simplest value
    - solve it to arbitrary value
    - Use within CEGLESS as bank of exprs
    - Avoid GuidedCloser if non-det expr (contains choose, holes, ..)
Cegis.scala 27.51 KiB
/* Copyright 2009-2014 EPFL, Lausanne */

package leon
package synthesis
package rules

import leon.utils.StreamUtils
import solvers._
import solvers.z3._

import purescala.Trees._
import purescala.Common._
import purescala.Definitions._
import purescala.TypeTrees._
import purescala.TreeOps._
import purescala.DefOps._
import purescala.TypeTreeOps._
import purescala.Extractors._
import purescala.ScalaPrinter
import utils.Helpers._

import scala.collection.mutable.{HashMap=>MutableMap, ArrayBuffer}

import evaluators._
import datagen._
import codegen.CodeGenParams

import utils._

case object CEGIS extends CEGISLike("CEGIS") {
  def getGrammar(sctx: SynthesisContext, p: Problem) = {
    ExpressionGrammars.default(sctx, p)
  }
}

case object CEGLESS extends CEGISLike("CEGLESS") {
  override val maxUnfoldings = 3;

  def getGrammar(sctx: SynthesisContext, p: Problem) = {
    import ExpressionGrammars._

    val TopLevelAnds(clauses) = p.pc

    val guide = sctx.program.library.guide.get

    val guides = clauses.collect {
      case FunctionInvocation(TypedFunDef(`guide`, _), Seq(expr)) => expr
    }

    val inputs = p.as.map(_.toVariable)

    val guidedGrammar = guides.map(SimilarTo(_, inputs.toSet)).foldLeft[ExpressionGrammar](Empty)(_ || _)

    guidedGrammar || OneOf(inputs)
  }
}


abstract class CEGISLike(name: String) extends Rule(name) {

  def getGrammar(sctx: SynthesisContext, p: Problem): ExpressionGrammar

  val maxUnfoldings = 3

  def instantiateOn(sctx: SynthesisContext, p: Problem): Traversable[RuleInstantiation] = {

    // CEGIS Flags to actiave or de-activate features
    val useUninterpretedProbe = sctx.options.cegisUseUninterpretedProbe
    val useUnsatCores         = sctx.options.cegisUseUnsatCores
    val useOptTimeout         = sctx.options.cegisUseOptTimeout
    val useVanuatoo           = sctx.options.cegisUseVanuatoo
    val useCETests            = sctx.options.cegisUseCETests
    val useCEPruning          = sctx.options.cegisUseCEPruning

    // Limits the number of programs CEGIS will specifically test for instead of reasonning symbolically
    val testUpTo              = 5
    val useBssFiltering       = sctx.options.cegisUseBssFiltering
    val filterThreshold       = 1.0/2
    val evalParams            = CodeGenParams(maxFunctionInvocations = 2000)
    val evaluator             = new CodeGenEvaluator(sctx.context, sctx.program, evalParams)

    val interruptManager      = sctx.context.interruptManager

    class NonDeterministicProgram(val p: Problem,
                                  val initGuard: Identifier) {

      val grammar = getGrammar(sctx, p)

      // b -> (c, ex) means the clause b => c == ex
      var mappings: Map[Identifier, (Identifier, Expr)] = Map()

      // b -> Set(c1, c2) means c1 and c2 are uninterpreted behind b, requires b to be closed
      private var guardedTerms: Map[Identifier, Set[Identifier]] = Map(initGuard -> p.xs.toSet)

      def isBClosed(b: Identifier) = guardedTerms.contains(b)

      /**
       * Stores which b's guard which c's, which then are represented by which
       * b's:
       *
       * b -> Map(c1 -> Set(b2, b3),
       *          c2 -> Set(b4, b5))
       *
       * means b protects c1 (with sub alternatives b2/b3), and c2 (with sub b4/b5)
       */
      private var bTree = Map[Identifier, Map[Identifier, Set[Identifier]]]( initGuard -> p.xs.map(_ -> Set[Identifier]()).toMap)

      /**
       * Computes dependencies of c's
       *
       * Assuming encoding:
       * b1 => c == F(c2, c3)
       * b2 => c == F(c4, c5)
       *
       * will be represented here as c -> Set(c2, c3, c4, c5)
       */
      private var cChildren: Map[Identifier, Set[Identifier]] = Map().withDefaultValue(Set())

      // Returns all possible assignments to Bs in order to enumerate all possible programs
      def allPrograms(): Traversable[Set[Identifier]] = {
        def allChildPaths(b: Identifier): Stream[Set[Identifier]] = {
          if (isBClosed(b)) {
            Stream.empty
          } else {
            bTree.get(b) match {
              case Some(cToBs) =>
                val streams = cToBs.values.toSeq.map { children =>
                  children.toStream.flatMap(c => allChildPaths(c).map(l => l + b))
                }

                StreamUtils.cartesianProduct(streams).map { ls =>
                  ls.foldLeft(Set[Identifier]())(_ ++ _)
                }
              case None =>
                Stream.cons(Set(b), Stream.empty)
            }
          }
        }

        allChildPaths(initGuard).toSet
      }

      /*
       * Compilation/Execution of programs
       */

      type EvaluationResult = EvaluationResults.Result

      private var triedCompilation = false
      private var progEvaluator: Option[(Seq[Expr], Seq[Expr]) => EvaluationResult] = None

      def canTest() = {
        if (!triedCompilation) {
          progEvaluator = compile()
        }

        progEvaluator.isDefined
      }

      private var bssOrdered: Seq[Identifier] = Seq()

      def testForProgram(bss: Set[Identifier])(ins: Seq[Expr]): Boolean = {
        if (canTest()) {
          val bssValues : Seq[Expr] = bssOrdered.map(i => BooleanLiteral(bss(i)))

          val evalResult = progEvaluator.get.apply(bssValues,  ins)

          evalResult match {
            case EvaluationResults.Successful(res) =>
              res == BooleanLiteral(true)

            case EvaluationResults.RuntimeError(err) =>
              //sctx.reporter.error("Error testing CE: "+err)
              false

            case EvaluationResults.EvaluatorError(err) =>
              sctx.reporter.error("Error testing CE: "+err)
              true
          }
        } else {
          true
        }
      }

      def compile(): Option[(Seq[Expr], Seq[Expr]) => EvaluationResult] = {
        var unreachableCs: Set[Identifier] = guardedTerms.flatMap(_._2).toSet

        val cToExprs = mappings.groupBy(_._2._1).map {
          case (c, maps) =>
            // We only keep cases within the current unfoldings closedBs
            val cases = maps.flatMap{ case (b, (_, ex)) => if (isBClosed(b)) None else Some(b -> ex) }

            // We compute the IF expression corresponding to each c
            val ifExpr = if (cases.isEmpty) {
              // This can happen with ADTs with only cases with arguments
              Error("No valid clause available").setType(c.getType)
            } else {
              cases.tail.foldLeft(cases.head._2) {
                case (elze, (b, thenn)) => IfExpr(Variable(b), thenn, elze)
              }
            }

            c -> ifExpr
        }.toMap

        // Map each x generated by the program to fresh xs passed as argument
        val newXs = p.xs.map(x => x -> FreshIdentifier(x.name, true).setType(x.getType))

        val baseExpr = removeWitnesses(sctx.program)(p.phi)
        bssOrdered = bss.toSeq.sortBy(_.id)

        var res = baseExpr

        def composeWith(c: Identifier) {
          cToExprs.get(c) match {
            case Some(value) =>
              res = Let(c, cToExprs(c), res)
            case None =>
              res = Let(c, Error("No value available").setType(c.getType), res)
          }

          for (dep <- cChildren(c) if !unreachableCs(dep)) {
            composeWith(dep)
          }

        }

        for (c <- p.xs) {
          composeWith(c)
        }

        val simplerRes = simplifyLets(res)

        def compileWithArray(): Option[(Seq[Expr], Seq[Expr]) => EvaluationResult] = {
          val ba = FreshIdentifier("bssArray").setType(ArrayType(BooleanType))
          val bav = Variable(ba)
          val substMap : Map[Expr,Expr] = (bssOrdered.zipWithIndex.map {
            case (b,i) => Variable(b) -> ArraySelect(bav, IntLiteral(i)).setType(BooleanType)
          }).toMap
          val forArray = replace(substMap, simplerRes)

          // We trust arrays to be fast...
          val eval = evaluator.compile(forArray, ba +: p.as)

          eval.map{e => { case (bss, ins) => 
            e(FiniteArray(bss).setType(ArrayType(BooleanType)) +: ins)
          }}
        }

        def compileWithArgs(): Option[(Seq[Expr], Seq[Expr]) => EvaluationResult] = {
          val eval = evaluator.compile(simplerRes, bssOrdered ++ p.as)

          eval.map{e => { case (bss, ins) => 
            e(bss ++ ins)
          }}
        }

        triedCompilation = true

        val localVariables = bss.size + cToExprs.size + p.as.size

        if (localVariables < 128) {
          compileWithArgs().orElse(compileWithArray())
        } else {
          compileWithArray()
        }
      }

      def determinize(bss: Set[Identifier]): Expr = {
        val cClauses = mappings.filterKeys(bss).map(_._2).toMap

        def getCValue(c: Identifier): Expr = {
          val map = for (dep <- cChildren(c) if cClauses contains dep) yield {
            dep -> getCValue(dep)
          }

          substAll(map.toMap, cClauses(c))
        }
        Tuple(p.xs.map(c => getCValue(c))).setType(TupleType(p.xs.map(_.getType)))

      }

      /**
       * Shrinks the non-deterministic program to the provided set of
       * alternatives only
       */
      def filterFor(remainingBss: Set[Identifier]): Seq[Expr] = {
        val filteredBss = remainingBss + initGuard

        // The following code is black-magic, read with caution
        mappings     = mappings.filterKeys(filteredBss)
        guardedTerms = Map()
        bTree        = bTree.filterKeys(filteredBss)
        bTree        = bTree.mapValues(cToBs => cToBs.mapValues(bs => bs & filteredBss))

        val filteredCss  = mappings.map(_._2._1).toSet
        cChildren        = cChildren.filterKeys(filteredCss)
        cChildren        = cChildren.mapValues(css => css & filteredCss)
        for (c <- filteredCss) {
          if (!(cChildren contains c)) {
            cChildren += c -> Set()
          }
        }

        // Finally, we reset the state of the evaluator
        triedCompilation = false
        progEvaluator    = None


        var cGroups = Map[Identifier, (Set[Identifier], Set[Identifier])]()

        for ((parentGuard, cToBs) <- bTree; (c, bss) <- cToBs) {
          val (ps, bs) = cGroups.getOrElse(c, (Set[Identifier](), Set[Identifier]()))

          cGroups += c -> (ps + parentGuard, bs ++ bss)
        }

        // We need to regenerate clauses for each b
        val pathConstraints = for ((_, (parentGuards, bs)) <- cGroups) yield {
          val bvs = bs.toList.map(Variable(_))

          // Represents the case where all parents guards are false, indicating
          // that this C should not be considered at all
          val failedPath = And(parentGuards.toSeq.map(p => Not(p.toVariable)))

          val distinct = bvs.combinations(2).collect {
            case List(a, b) =>
              Or(Not(a) :: Not(b) :: Nil)
          }

          And(Seq(Or(failedPath :: bvs), Implies(failedPath, And(bvs.map(Not(_))))) ++ distinct)
        }

        // Generate all the b => c = ...
        val impliess = mappings.map { case (bid, (recId, ex)) =>
          Implies(Variable(bid), Equals(Variable(recId), ex))
        }

        (pathConstraints ++ impliess).toSeq
      }

      def unfold(finalUnfolding: Boolean): (List[Expr], Set[Identifier]) = {
        var newClauses      = List[Expr]()
        var newGuardedTerms = Map[Identifier, Set[Identifier]]()
        var newMappings     = Map[Identifier, (Identifier, Expr)]()


        var cGroups = Map[Identifier, Set[Identifier]]()

        for ((parentGuard, recIds) <- guardedTerms; recId <- recIds) {
          cGroups += recId -> (cGroups.getOrElse(recId, Set()) + parentGuard)
        }

        for ((recId, parentGuards) <- cGroups) {

          var alts = grammar.getProductions(recId.getType)
          if (finalUnfolding) {
            alts = alts.filter(_.subTrees.isEmpty)
          }

          val altsWithBranches = alts.map(alt => FreshIdentifier("B", true).setType(BooleanType) -> alt)

          val bvs  = altsWithBranches.map(alt => Variable(alt._1))

          // Represents the case where all parents guards are false, indicating
          // that this C should not be considered at all
          val failedPath = And(parentGuards.toSeq.map(p => Not(p.toVariable)))

          val distinct = bvs.combinations(2).collect {
            case List(a, b) =>
              Or(Not(a) :: Not(b) :: Nil)
          }

          val pre = And(Seq(Or(failedPath +: bvs), Implies(failedPath, And(bvs.map(Not(_))))) ++ distinct)

          var cBankCache = Map[TypeTree, Stream[Identifier]]()
          def freshC(t: TypeTree): Stream[Identifier] = Stream.cons(FreshIdentifier("c", true).setType(t), freshC(t))
          def getC(t: TypeTree, index: Int) = cBankCache.getOrElse(t, {
            cBankCache += t -> freshC(t)
            cBankCache(t)
          })(index)

          val cases = for((bid, gen) <- altsWithBranches.toList) yield { // b1 => E(gen1, gen2)     [b1 -> {gen1, gen2}]
            val rec = for ((t, i) <- gen.subTrees.zipWithIndex) yield { getC(t, i) }
            val ex = gen.builder(rec.map(_.toVariable))

            if (!rec.isEmpty) {
              newGuardedTerms += bid -> rec.toSet
              cChildren       += recId -> (cChildren(recId) ++ rec)
            }

            newMappings  += bid -> (recId -> ex)

            Implies(Variable(bid), Equals(Variable(recId), ex))
          }

          val newBIds = altsWithBranches.map(_._1).toSet

          for (parentGuard <- parentGuards) {
            bTree += parentGuard -> (bTree.getOrElse(parentGuard, Map()) + (recId -> newBIds))
          }

          newClauses = newClauses ::: pre :: cases
        }

        sctx.reporter.ifDebug { printer =>
          printer("Grammar so far:");
          grammar.printProductions(printer)
        }

        //program  = And(program :: newClauses)

        mappings = mappings ++ newMappings

        guardedTerms = newGuardedTerms

        // Finally, we reset the state of the evaluator
        triedCompilation = false
        progEvaluator    = None

        (newClauses, newGuardedTerms.keySet)
      }

      def bss = mappings.keySet
      def css : Set[Identifier] = mappings.values.map(_._1).toSet ++ guardedTerms.flatMap(_._2)
    }

    List(new RuleInstantiation(p, this, SolutionBuilder.none, this.name, this.priority) {
      def apply(sctx: SynthesisContext): RuleApplication = {
        var result: Option[RuleApplication]   = None

        var ass = p.as.toSet
        var xss = p.xs.toSet

        val initGuard = FreshIdentifier("START", true).setType(BooleanType)

        val ndProgram = new NonDeterministicProgram(p, initGuard)
        var unfolding = 1
        val maxUnfoldings = CEGISLike.this.maxUnfoldings

        val exSolverTo  = 2000L
        val cexSolverTo = 2000L

        var baseExampleInputs: ArrayBuffer[Seq[Expr]] = new ArrayBuffer[Seq[Expr]]()

        // We populate the list of examples with a predefined one
        sctx.reporter.debug("Acquiring list of examples")

        baseExampleInputs ++= p.getTests(sctx).map(_.ins).toSet

        val safePc = removeWitnesses(sctx.program)(p.pc)

        if (safePc == BooleanLiteral(true)) {
          baseExampleInputs += p.as.map(a => simplestValue(a.getType))
        } else {
          val solver = sctx.newSolver.setTimeout(exSolverTo)

          solver.assertCnstr(safePc)

          try {
            solver.check match {
              case Some(true) =>
                val model = solver.getModel
                baseExampleInputs += p.as.map(a => model.getOrElse(a, simplestValue(a.getType)))

              case Some(false) =>
                sctx.reporter.debug("Path-condition seems UNSAT")
                return RuleFailed()

              case None =>
                sctx.reporter.warning("Solver could not solve path-condition")
                return RuleFailed() // This is not necessary though, but probably wanted
            }
          } finally {
            solver.free()
          }
        }

        sctx.reporter.ifDebug { debug =>
          baseExampleInputs.foreach { in =>
            debug("  - "+in.mkString(", "))
          }
        }

        /**
         * We generate tests for discarding potential programs
         */
        val inputIterator: Iterator[Seq[Expr]] = if (useVanuatoo) {
          new VanuatooDataGen(sctx.context, sctx.program).generateFor(p.as, safePc, 20, 3000)
        } else {
          new NaiveDataGen(sctx.context, sctx.program, evaluator).generateFor(p.as, safePc, 20, 1000)
        }

        val cachedInputIterator = new Iterator[Seq[Expr]] {
          def next() = {
            val i = inputIterator.next()
            baseExampleInputs += i
            i
          }

          def hasNext() = inputIterator.hasNext
        }

        val failedTestsStats = new MutableMap[Seq[Expr], Int]().withDefaultValue(0)

        def hasInputExamples() = baseExampleInputs.size > 0 || cachedInputIterator.hasNext

        var n = 1;
        def allInputExamples() = {
          if (n % 1000 == 0) {
            baseExampleInputs = baseExampleInputs.sortBy(e => -failedTestsStats(e))
          }
          n += 1
          baseExampleInputs.iterator ++ cachedInputIterator
        }

        def checkForPrograms(programs: Set[Set[Identifier]]): RuleApplication = {
          for (prog <- programs) {
            val expr = ndProgram.determinize(prog)
            val res = Equals(Tuple(p.xs.map(Variable(_))), expr)

            val solver3 = sctx.newSolver.setTimeout(cexSolverTo)
            solver3.assertCnstr(And(safePc :: res :: Not(p.phi) :: Nil))

            try {
              solver3.check match {
                case Some(false) =>
                  return RuleClosed(Solution(BooleanLiteral(true), Set(), expr, isTrusted = true))
                case None =>
                  return RuleClosed(Solution(BooleanLiteral(true), Set(), expr, isTrusted = false))
                case Some(true) =>
                  // invalid program, we skip
              }
            } finally {
              solver3.free()
            }
          }

          RuleFailed()
        }

        // Keep track of collected cores to filter programs to test
        var collectedCores = Set[Set[Identifier]]()

        val initExClause = And(safePc :: p.phi :: Variable(initGuard) :: Nil)
        val initCExClause = And(safePc :: Not(p.phi) :: Variable(initGuard) :: Nil)

        // solver1 is used for the initial SAT queries
        var solver1 = sctx.newSolver.setTimeout(exSolverTo)
        solver1.assertCnstr(initExClause)

        // solver2 is used for validating a candidate program, or finding new inputs
        var solver2 = sctx.newSolver.setTimeout(cexSolverTo)
        solver2.assertCnstr(initCExClause)

        var didFilterAlready = false

        val tpe = TupleType(p.xs.map(_.getType))

        try {
          do {
            var skipCESearch = false

            var bssAssumptions = Set[Identifier]()

            if (!didFilterAlready) {
              val (clauses, closedBs) = ndProgram.unfold(unfolding == maxUnfoldings)

              bssAssumptions = closedBs

              sctx.reporter.ifDebug { debug =>
                debug("UNFOLDING: ")
                for (c <- clauses) {
                  debug(" - " + c.asString(sctx.context))
                }
                debug("CLOSED Bs "+closedBs)
              }

              val clause = And(clauses)

              solver1.assertCnstr(clause)
              solver2.assertCnstr(clause)
            }

            // Compute all programs that have not been excluded yet
            var prunedPrograms: Set[Set[Identifier]] = if (useCEPruning) {
                ndProgram.allPrograms.filterNot(p => collectedCores.exists(c => c.subsetOf(p))).toSet
              } else {
                Set()
              }

            val allPrograms = prunedPrograms.size

            sctx.reporter.debug("#Programs: "+prunedPrograms.size)

            var wrongPrograms = Set[Set[Identifier]]();

            // We further filter the set of working programs to remove those that fail on known examples
            if (useCEPruning && hasInputExamples() && ndProgram.canTest()) {

              for (p <- prunedPrograms if !interruptManager.isInterrupted()) {
                var valid = true
                val examples = allInputExamples()
                while(valid && examples.hasNext) {
                  val e = examples.next()
                  if (!ndProgram.testForProgram(p)(e)) {
                    failedTestsStats(e) += 1
                    wrongPrograms += p
                    prunedPrograms -= p
                    valid = false;
                  }
                }

                if (wrongPrograms.size % 1000 == 0) {
                  sctx.reporter.debug("..."+wrongPrograms.size)
                }
              }
            }

            val nPassing = prunedPrograms.size
            sctx.reporter.debug("#Programs passing tests: "+nPassing)

            if (nPassing == 0) {
              skipCESearch = true;
            } else if (nPassing <= testUpTo) {
              // Immediate Test
              checkForPrograms(prunedPrograms) match {
                case rs: RuleClosed if rs.solutions.nonEmpty =>
                  result = Some(rs)
                case _ =>
                  wrongPrograms.foreach { p =>
                    solver1.assertCnstr(Not(And(p.map(Variable(_)).toSeq)))
                  }
              }
            } else if (((nPassing < allPrograms*filterThreshold) || didFilterAlready) && useBssFiltering) {
              // We filter the Bss so that the formula we give to z3 is much smalled
              val bssToKeep = prunedPrograms.foldLeft(Set[Identifier]())(_ ++ _)

              // Cannot unfold normally after having filtered, so we need to
              // repeat the filtering procedure at next unfolding.
              didFilterAlready = true
              
              // Freshening solvers
              solver1.free()
              solver1 = sctx.newSolver.setTimeout(exSolverTo)
              solver1.assertCnstr(initExClause)

              solver2.free()
              solver2 = sctx.newSolver.setTimeout(cexSolverTo)
              solver2.assertCnstr(initCExClause)

              val clauses = ndProgram.filterFor(bssToKeep)
              val clause = And(clauses)

              solver1.assertCnstr(clause)
              solver2.assertCnstr(clause)
            } else {
              wrongPrograms.foreach { p =>
                solver1.assertCnstr(Not(And(p.map(Variable(_)).toSeq)))
              }
            }

            val bss = ndProgram.bss

            while (result.isEmpty && !skipCESearch && !interruptManager.isInterrupted()) {

              solver1.checkAssumptions(bssAssumptions.map(id => Not(Variable(id)))) match {
                case Some(true) =>
                  val satModel = solver1.getModel

                  val bssAssumptions: Set[Expr] = bss.map(b => satModel(b) match {
                    case BooleanLiteral(true)  => Variable(b)
                    case BooleanLiteral(false) => Not(Variable(b))
                  })

                  val validateWithZ3 = if (useCETests && hasInputExamples() && ndProgram.canTest()) {

                    val p = bssAssumptions.collect { case Variable(b) => b }

                    if (allInputExamples().forall(ndProgram.testForProgram(p))) {
                      // All valid inputs also work with this, we need to
                      // make sure by validating this candidate with z3
                      true
                    } else {
                      // One valid input failed with this candidate, we can skip
                      solver1.assertCnstr(Not(And(p.map(Variable(_)).toSeq)))
                      false
                    }
                  } else {
                    // No inputs or capability to test, we need to ask Z3
                    true
                  }

                  if (validateWithZ3) {
                    solver2.checkAssumptions(bssAssumptions) match {
                      case Some(true) =>
                        val invalidModel = solver2.getModel
                        val fixedAss = And(ass.collect {
                          case a if invalidModel contains a => Equals(Variable(a), invalidModel(a))
                        }.toSeq)

                        val newCE = p.as.map(valuateWithModel(invalidModel))

                        baseExampleInputs += newCE

                        // Retest whether the newly found C-E invalidates all programs
                        if (useCEPruning && ndProgram.canTest) {
                          if (prunedPrograms.forall(p => !ndProgram.testForProgram(p)(newCE))) {
                            skipCESearch = true
                          }
                        }

                        val unsatCore = if (useUnsatCores) {
                          solver1.push()
                          solver1.assertCnstr(fixedAss)

                          val core = solver1.checkAssumptions(bssAssumptions) match {
                            case Some(false) =>
                              // Core might be empty if unfolding level is
                              // insufficient, it becomes unsat no matter what
                              // the assumptions are.
                              solver1.getUnsatCore

                            case Some(true) =>
                              // Can't be!
                              bssAssumptions

                            case None =>
                              return RuleFailed()
                          }

                          solver1.pop()

                          collectedCores += core.collect{ case Variable(id) => id }

                          core
                        } else {
                          bssAssumptions
                        }

                        if (unsatCore.isEmpty) {
                          skipCESearch = true
                        } else {
                          solver1.assertCnstr(Not(And(unsatCore.toSeq)))
                        }

                      case Some(false) =>

                        val expr = ndProgram.determinize(satModel.filter(_._2 == BooleanLiteral(true)).keySet)

                        result = Some(RuleClosed(Solution(BooleanLiteral(true), Set(), expr)))

                      case _ =>
                        if (useOptTimeout) {
                          // Interpret timeout in CE search as "the candidate is valid"
                          sctx.reporter.info("CEGIS could not prove the validity of the resulting expression")
                          val expr = ndProgram.determinize(satModel.filter(_._2 == BooleanLiteral(true)).keySet)
                          result = Some(RuleClosed(Solution(BooleanLiteral(true), Set(), expr, isTrusted = false)))
                        } else {
                          return RuleFailed()
                        }
                    }
                  }


                case Some(false) =>
                  if (useUninterpretedProbe) {
                    solver1.check match {
                      case Some(false) =>
                        // Unsat even without blockers (under which fcalls are then uninterpreted)
                        return RuleFailed()

                      case _ =>
                    }
                  }

                  skipCESearch = true

                case _ =>
                  // Last chance, we test first few programs
                  result = Some(checkForPrograms(prunedPrograms.take(testUpTo)))
              }
            }

            unfolding += 1
          } while(unfolding <= maxUnfoldings && result.isEmpty && !interruptManager.isInterrupted())

          result.getOrElse(RuleFailed())

        } catch {
          case e: Throwable =>
            sctx.reporter.warning("CEGIS crashed: "+e.getMessage)
            e.printStackTrace
            RuleFailed()
        } finally {
          solver1.free()
          solver2.free()
        }
      }
    })
  }
}