From 4c7364b8f326399ed7cd3d9fee0943903b42634e Mon Sep 17 00:00:00 2001 From: Nicolas Voirol <voirol.nicolas@gmail.com> Date: Sat, 15 Jun 2013 18:51:45 +0200 Subject: [PATCH] Added automatic testing to termination framework and corrected some small issues in the ChainProcessor system. Some tests ending in .BAK should be runnable but can't for now because of a memory leak somewhere in the framework. --- .../scala/leon/termination/ChainBuilder.scala | 13 ++ .../leon/termination/ChainProcessor.scala | 44 +++--- .../leon/termination/LoopProcessor.scala | 2 +- .../scala/leon/termination/Processor.scala | 14 +- .../leon/termination/RelationProcessor.scala | 2 - .../termination/looping/Looping1.scala.BAK | 33 +++++ .../termination/looping/Looping2.scala.BAK | 32 +++++ .../termination/looping/Looping3.scala.BAK | 32 +++++ .../termination/looping/Numeric1.scala | 11 ++ .../termination/looping/Numeric2.scala | 13 ++ .../looping/Termination_failling1.scala | 22 +++ .../termination/unknown/Numeric3.scala | 8 ++ .../termination/valid/BinaryTreeImp.scala | 101 +++++++++++++ .../termination/valid/BubbleSort.scala | 74 ++++++++++ .../termination/valid/ComplexChains.scala | 26 ++++ .../termination/valid/Fibonacci.scala | 15 ++ .../termination/valid/HardChains.scala.BAK | 37 +++++ .../termination/valid/ListWithSize.scala | 135 ++++++++++++++++++ .../termination/valid/Numeric1.scala | 11 ++ .../termination/valid/QuickSort.scala | 58 ++++++++ .../termination/valid/RedBlackTree.scala | 59 ++++++++ .../termination/valid/SimpInterpret.scala | 67 +++++++++ .../valid/Termination_passing1.scala | 23 +++ .../valid/Termination_passing2.scala | 20 +++ .../termination/TerminationRegression.scala | 109 ++++++++++++++ 25 files changed, 929 insertions(+), 32 deletions(-) create mode 100644 src/test/resources/regression/termination/looping/Looping1.scala.BAK create mode 100644 src/test/resources/regression/termination/looping/Looping2.scala.BAK create mode 100644 src/test/resources/regression/termination/looping/Looping3.scala.BAK create mode 100644 src/test/resources/regression/termination/looping/Numeric1.scala create mode 100644 src/test/resources/regression/termination/looping/Numeric2.scala create mode 100644 src/test/resources/regression/termination/looping/Termination_failling1.scala create mode 100644 src/test/resources/regression/termination/unknown/Numeric3.scala create mode 100644 src/test/resources/regression/termination/valid/BinaryTreeImp.scala create mode 100644 src/test/resources/regression/termination/valid/BubbleSort.scala create mode 100644 src/test/resources/regression/termination/valid/ComplexChains.scala create mode 100644 src/test/resources/regression/termination/valid/Fibonacci.scala create mode 100644 src/test/resources/regression/termination/valid/HardChains.scala.BAK create mode 100644 src/test/resources/regression/termination/valid/ListWithSize.scala create mode 100644 src/test/resources/regression/termination/valid/Numeric1.scala create mode 100644 src/test/resources/regression/termination/valid/QuickSort.scala create mode 100644 src/test/resources/regression/termination/valid/RedBlackTree.scala create mode 100644 src/test/resources/regression/termination/valid/SimpInterpret.scala create mode 100644 src/test/resources/regression/termination/valid/Termination_passing1.scala create mode 100644 src/test/resources/regression/termination/valid/Termination_passing2.scala create mode 100644 src/test/scala/leon/test/termination/TerminationRegression.scala diff --git a/src/main/scala/leon/termination/ChainBuilder.scala b/src/main/scala/leon/termination/ChainBuilder.scala index 065c54db8..741b7c2b6 100644 --- a/src/main/scala/leon/termination/ChainBuilder.scala +++ b/src/main/scala/leon/termination/ChainBuilder.scala @@ -6,7 +6,20 @@ import leon.purescala.Trees._ import leon.purescala.TreeOps._ import leon.purescala.Common._ +object ChainID { + private var counter: Int = 0 + def get: Int = { + counter = counter + 1 + counter + } +} + final case class Chain(chain: List[Relation]) { + val id = ChainID.get + + override def equals(obj: Any): Boolean = obj.isInstanceOf[Chain] && obj.asInstanceOf[Chain].id == id + override def hashCode(): Int = id + def funDef : FunDef = chain.head.funDef def funDefs : Set[FunDef] = chain.map(_.funDef) toSet diff --git a/src/main/scala/leon/termination/ChainProcessor.scala b/src/main/scala/leon/termination/ChainProcessor.scala index e50ab2e2a..06c98ed31 100644 --- a/src/main/scala/leon/termination/ChainProcessor.scala +++ b/src/main/scala/leon/termination/ChainProcessor.scala @@ -16,31 +16,33 @@ class ChainProcessor(checker: TerminationChecker) extends Processor(checker) wit ChainComparator.init def run(problem: Problem): (TraversableOnce[Result], TraversableOnce[Problem]) = { + import scala.collection.mutable.{Map => MutableMap} reporter.info("- Running ChainProcessor") val allChainMap : Map[FunDef, Set[Chain]] = problem.funDefs.map(funDef => funDef -> ChainBuilder.run(funDef)).toMap reporter.info("- Computing all possible Chains") - val possibleChainMap : Map[FunDef, Set[Chain]] = allChainMap.mapValues(chains => chains.filter(chain => isSAT(And(chain.loop())))) + var counter = 0 + val possibleChainMap : Map[FunDef, Set[Chain]] = allChainMap.mapValues(chains => chains.filter(chain => isWeakSAT(And(chain.loop())))) reporter.info("- Collecting re-entrant Chains") - val reentrantChainMap : Map[FunDef, Set[Chain]] = possibleChainMap.mapValues(chains => chains.filter(chain => isSAT(And(chain reentrant chain)))) + val reentrantChainMap : Map[FunDef, Set[Chain]] = possibleChainMap.mapValues(chains => chains.filter(chain => isWeakSAT(And(chain reentrant chain)))) // We build a cross-chain map that determines which chains can reenter into another one after a loop. // Note: We are also checking reentrance SAT here, so again, we negate the formula! reporter.info("- Computing cross-chain map") - val crossChains : Map[Chain, Set[Chain]] = possibleChainMap.map({ case (funDef, chains) => + val crossChains : Map[Chain, Set[Chain]] = possibleChainMap.toSeq.map({ case (funDef, chains) => val reentrant = reentrantChainMap(funDef) - val reentrantPairs = reentrant.map(chain => chain -> Set(chain)) - val crosswise = (chains -- reentrant).map(chain => chain -> { - reentrant.filter(other => isSAT(And(chain reentrant other))) + chains.map(chain => chain -> { + val cross = (reentrant - chain).filter(other => isWeakSAT(And(chain reentrant other))) + val self = if (reentrant(chain)) Set(chain) else Set() + cross ++ self }) - reentrantPairs ++ crosswise }).flatten.toMap - val validChainMap : Map[FunDef, Set[Chain]] = possibleChainMap.map({ case (funDef, chains) => funDef -> chains.filter(crossChains(_).nonEmpty) }) + val validChainMap : Map[FunDef, Set[Chain]] = possibleChainMap.map({ case (funDef, chains) => funDef -> chains.filter(crossChains(_).nonEmpty) }).toMap // We use the cross-chains to build chain clusters. For each cluster, we must prove that the SAME argument // decreases in each of the chains in the cluster! - reporter.info("- Building initial cluster estimation by fix-point iteration") - val generalClusters : Map[FunDef, Set[Set[Chain]]] = { + reporter.info("- Building cluster estimation by fix-point iteration") + val clusters : Map[FunDef, Set[Set[Chain]]] = { def cluster(set: Set[Chain]): Set[Chain] = { set ++ set.map(crossChains(_)).flatten } @@ -50,6 +52,14 @@ class ChainProcessor(checker: TerminationChecker) extends Processor(checker) wit if (a == na) a else fix(f, na) } + def reduceClusters(all: List[Set[Chain]]): List[Set[Chain]] = { + all.map(cluster => cluster.toSeq.sortBy(_.size).foldLeft(Set[Chain]())({ case (acc, chain) => + val chainElements : Set[Relation] = chain.chain.toSet + val seenElements : Set[Relation] = acc.map(_.chain).flatten.toSet + if (chainElements -- seenElements nonEmpty) acc + chain else acc + })).filter(_.nonEmpty) + } + def filterClusters(all: List[Set[Chain]]): List[Set[Chain]] = if (all.isEmpty) Nil else { val newCluster = all.head val rest = all.tail.filter(set => !set.subsetOf(newCluster)) @@ -58,21 +68,14 @@ class ChainProcessor(checker: TerminationChecker) extends Processor(checker) wit def build(chains: Set[Chain]): Set[Set[Chain]] = { val allClusters = chains.map(chain => fix(cluster, Set(chain))) - filterClusters(allClusters.toList.sortBy(- _.size)).toSet + val reducedClusters = reduceClusters(allClusters.toList) + val filteredClusters = filterClusters(reducedClusters.sortBy(- _.size)) + filteredClusters.toSet } validChainMap.map({ case (funDef, chains) => funDef -> build(chains) }) } - reporter.info("- Trimming down to final clusters") - val clusters : Map[FunDef, Set[Set[Chain]]] = generalClusters.map({ case (funDef, clusters) => - funDef -> clusters.map(cluster => cluster.toSeq.sortBy(_.size).foldLeft(Set[Chain]())({ case (acc, chain) => - val chainElements : Set[Relation] = chain.chain.toSet - val seenElements : Set[Relation] = acc.map(_.chain).flatten.toSet - if (chainElements -- seenElements nonEmpty) acc + chain else acc - })).filter(_.nonEmpty) - }) - reporter.info("- Strengthening postconditions") strengthenPostconditions(problem.funDefs) @@ -96,7 +99,6 @@ class ChainProcessor(checker: TerminationChecker) extends Processor(checker) wit (fd, clusters.map(cluster => cluster -> gen(fd, cluster))) }) - initSolvers // add structural size functions to solver formulas.map({ case (fd, clustersWithFormulas) => fd -> clustersWithFormulas.filter({ case (cluster, formula) => !isAlwaysSAT(formula) }).map(_._1) }) diff --git a/src/main/scala/leon/termination/LoopProcessor.scala b/src/main/scala/leon/termination/LoopProcessor.scala index 5f0c6db3c..a45cc3c7d 100644 --- a/src/main/scala/leon/termination/LoopProcessor.scala +++ b/src/main/scala/leon/termination/LoopProcessor.scala @@ -14,7 +14,7 @@ class LoopProcessor(checker: TerminationChecker, k: Int = 10) extends Processor( def run(problem: Problem) = { val allChains : Set[Chain] = problem.funDefs.map(fd => ChainBuilder.run(fd)).flatten // Get reentrant loops (see ChainProcessor for more details) - val chains : Set[Chain] = allChains.filter(chain => isSAT(And(chain reentrant chain))) + val chains : Set[Chain] = allChains.filter(chain => isWeakSAT(And(chain reentrant chain))) val nonTerminating = chains.flatMap({ chain => val freshArgs : Seq[Expr] = chain.funDef.args.map(arg => arg.id.freshen.toVariable) diff --git a/src/main/scala/leon/termination/Processor.scala b/src/main/scala/leon/termination/Processor.scala index 95ec075f5..299d5b338 100644 --- a/src/main/scala/leon/termination/Processor.scala +++ b/src/main/scala/leon/termination/Processor.scala @@ -78,7 +78,7 @@ trait Solvable { self: Processor => def strengthenPostconditions(funDefs: Set[FunDef]) = Solvable.strengthenPostconditions(funDefs)(this) - def initSolvers { + private def initSolvers { val program : Program = self.checker.program val allDefs : Seq[Definition] = program.mainObject.defs ++ StructuralSize.defs val newProgram : Program = program.copy(mainObject = program.mainObject.copy(defs = allDefs)) @@ -92,7 +92,7 @@ trait Solvable { self: Processor => type Solution = (Option[Boolean], Map[Identifier, Expr]) private def solve(problem: Expr): Solution = { - if (solvers == null) initSolvers + initSolvers // drop functions from constraints that might not terminate (and may therefore // make Leon unroll them forever...) val dangerousCallsMap : Map[Expr, Expr] = functionCallsOf(problem).collect({ @@ -123,13 +123,11 @@ trait Solvable { self: Processor => solvers.collectFirst({ case Solved(s, model) => (s, model) }) getOrElse (None, Map()) } - def isSAT(problem: Expr): Boolean = { - solve(problem)._1 getOrElse false - } + def isStrongSAT(problem: Expr): Boolean = solve(problem)._1 getOrElse false - def isAlwaysSAT(problem: Expr): Boolean = { - solve(Not(problem))._1.map(!_) getOrElse false - } + def isWeakSAT(problem: Expr): Boolean = solve(problem)._1 getOrElse true + + def isAlwaysSAT(problem: Expr): Boolean = solve(Not(problem))._1.map(!_) getOrElse false def getModel(problem: Expr): Option[Map[Identifier, Expr]] = { val solution = solve(problem) diff --git a/src/main/scala/leon/termination/RelationProcessor.scala b/src/main/scala/leon/termination/RelationProcessor.scala index 2f094c838..4f2786528 100644 --- a/src/main/scala/leon/termination/RelationProcessor.scala +++ b/src/main/scala/leon/termination/RelationProcessor.scala @@ -35,8 +35,6 @@ class RelationProcessor(checker: TerminationChecker) extends Processor(checker) case class Dep(deps: Set[FunDef]) extends Result case object Failure extends Result - initSolvers - val decreasing = formulas.map({ case (fd, formulas) => val solved = formulas.map({ case (fid, (gt, ge)) => if(isAlwaysSAT(gt)) Success diff --git a/src/test/resources/regression/termination/looping/Looping1.scala.BAK b/src/test/resources/regression/termination/looping/Looping1.scala.BAK new file mode 100644 index 000000000..b21e705bf --- /dev/null +++ b/src/test/resources/regression/termination/looping/Looping1.scala.BAK @@ -0,0 +1,33 @@ +import leon.Utils._ + +object ComplexChains { + + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def looping1(list: List): List = list match { + case Cons(head, tail) if head > 0 => looping2(tail) + case Cons(head, tail) if head < 0 => looping3(tail) + case _ => looping4(Cons(1, list)) + } + + def looping2(list: List): List = list match { + case Cons(head, tail) if head > 0 => looping1(tail) + case _ => looping4(list) + } + + def looping3(list: List): List = list match { + case Cons(head, tail) if head < 0 => looping1(tail) + case _ => looping2(Cons(1, list)) + } + + def looping4(list: List): List = list match { + case Cons(head, tail) if head == 0 => looping4(tail) + case Cons(_, Cons(_, tail)) => looping1(Cons(1, list)) + case Cons(_, tail) => looping3(Nil()) + case Nil() => Nil() + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/looping/Looping2.scala.BAK b/src/test/resources/regression/termination/looping/Looping2.scala.BAK new file mode 100644 index 000000000..fc1da261e --- /dev/null +++ b/src/test/resources/regression/termination/looping/Looping2.scala.BAK @@ -0,0 +1,32 @@ +import leon.Utils._ + +object ComplexChains { + + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def looping1(list: List): List = list match { + case Cons(head, tail) if head > 0 => calling2(list) + case Cons(head, tail) if head < 0 => calling3(list) + case _ => looping4(Cons(1, list)) + } + + def calling2(list: List): List = list match { + case Cons(head, tail) if head > 0 => looping1(tail) + case _ => looping4(list) + } + + def calling3(list: List): List = list match { + case Cons(head, tail) if head < 0 => looping1(tail) + case _ => looping4(list) + } + + def looping4(list: List): List = list match { + case Cons(_, Cons(_, tail)) => looping1(tail) + case Cons(_, tail) => looping1(tail) + case Nil() => calling2(Cons(1, Nil())) + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/looping/Looping3.scala.BAK b/src/test/resources/regression/termination/looping/Looping3.scala.BAK new file mode 100644 index 000000000..c6d81a758 --- /dev/null +++ b/src/test/resources/regression/termination/looping/Looping3.scala.BAK @@ -0,0 +1,32 @@ +import leon.Utils._ + +object ComplexChains { + + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def looping1(list: List): List = list match { + case Cons(head, tail) if head > 0 => looping2(list) + case Cons(head, tail) if head < 0 => looping3(list) + case _ => looping4(Cons(1, list)) + } + + def looping2(list: List): List = list match { + case Cons(head, tail) if head > 0 => looping1(tail) + case _ => looping3(list) + } + + def looping3(list: List): List = list match { + case Cons(head, tail) if head < 0 => looping1(tail) + case _ => looping2(list) + } + + def looping4(list: List): List = list match { + case Cons(_, Cons(_, tail)) => looping1(tail) + case Cons(_, tail) => looping1(tail) + case Nil() => looping2(Cons(1, Nil())) + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/looping/Numeric1.scala b/src/test/resources/regression/termination/looping/Numeric1.scala new file mode 100644 index 000000000..6949040ed --- /dev/null +++ b/src/test/resources/regression/termination/looping/Numeric1.scala @@ -0,0 +1,11 @@ +import leon.Utils._ + +object Numeric { + // division by 0 loops + def looping(x: Int, y: Int): Int = { + if (x < y) 0 + else 1 + looping(x - y, y) + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/looping/Numeric2.scala b/src/test/resources/regression/termination/looping/Numeric2.scala new file mode 100644 index 000000000..0d5c2c04f --- /dev/null +++ b/src/test/resources/regression/termination/looping/Numeric2.scala @@ -0,0 +1,13 @@ +import leon.Utils._ + +object Numeric { + def looping1(x: Int): Int = looping2(x - 1) + + def looping2(x: Int): Int = looping3(x - 1) + + def looping3(x: Int): Int = looping4(x - 1) + + def looping4(x: Int): Int = looping1(x + 3) +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/looping/Termination_failling1.scala b/src/test/resources/regression/termination/looping/Termination_failling1.scala new file mode 100644 index 000000000..4904c9fc5 --- /dev/null +++ b/src/test/resources/regression/termination/looping/Termination_failling1.scala @@ -0,0 +1,22 @@ +import leon.Utils._ + +object Termination { + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def looping1(list: List) : Int = looping2(list) + + def looping2(list: List) : Int = looping1(list) + + def calling1(list: List, b: Boolean) : Int = if(b) calling2(list) else looping1(list) + + def calling2(list: List) : Int = list match { + case Cons(head, tail) => calling1(tail, true) + case Nil() => 0 + } + + def ok(list: List) : Int = 0 +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/unknown/Numeric3.scala b/src/test/resources/regression/termination/unknown/Numeric3.scala new file mode 100644 index 000000000..fd99e3132 --- /dev/null +++ b/src/test/resources/regression/termination/unknown/Numeric3.scala @@ -0,0 +1,8 @@ +import leon.Utils._ + +object Numeric3 { + def unknown(x: Int) : Int = if (x > 0) unknown(x - 1) else unknown(5) +} + + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/valid/BinaryTreeImp.scala b/src/test/resources/regression/termination/valid/BinaryTreeImp.scala new file mode 100644 index 000000000..3005d1991 --- /dev/null +++ b/src/test/resources/regression/termination/valid/BinaryTreeImp.scala @@ -0,0 +1,101 @@ +import scala.collection.immutable.Set +import leon.Annotations._ +import leon.Utils._ + +object BinaryTree { + + sealed abstract class Tree + case class Leaf() extends Tree + case class Node(left: Tree, value: Int, right: Tree) extends Tree + + sealed abstract class OptionInt + case class Some(v : Int) extends OptionInt + case class None() extends OptionInt + + def content(t: Tree) : Set[Int] = t match { + case Leaf() => Set.empty + case Node(l, v, r) => content(l) ++ Set(v) ++ content(r) + } + + def size(t: Tree) : Int = (t match { + case Leaf() => 0 + case Node(l, v, r) => size(l) + 1 + size(r) + }) ensuring(_ >= 0) + + def binaryTreeProp(t: Tree): Boolean = t match { + case Leaf() => true + case Node(left, value, right) => { + val b1 = left match { + case Leaf() => true + case Node(a,b,c) => value >= treeMax(Node(a,b,c)) + } + val b2 = right match { + case Leaf() => true + case Node(a,b,c) => value <= treeMin(Node(a,b,c)) + } + binaryTreeProp(left) && binaryTreeProp(right) && b1 && b2 + } + } + + def treeMin(tree: Node): Int = { + require(binaryTreeProp(tree)) + tree match { + case Node(left, value, _) => left match { + case Leaf() => value + case Node(l, v, r) => treeMin(Node(l, v, r)) + } + } + } ensuring(content(tree).contains(_)) + + //def treeMin(tree: Node): Int = { + // require(binaryTreeProp(tree)) + // val Node(left, value, _) = tree + // var res = value + // var tmpLeft = left + // (while(!tmpLeft.isInstanceOf[Leaf]) { + // val Node(left, value, _) = tmpLeft + // res = value + // tmpLeft = left + // }) invariant(content(tmpLeft).contains(res)) + // res + //} ensuring(content(tree).contains(_)) + + def treeMax(tree: Node): Int = { + require(binaryTreeProp(tree)) + tree match { + case Node(_, v, right) => right match { + case Leaf() => v + case Node(l, v, r) => treeMax(Node(l, v, r)) + } + } + } ensuring(content(tree).contains(_)) + + + def search(t: Tree, x: Int): Boolean = { + require(binaryTreeProp(t)) + t match { + case Leaf() => false + case Node(left, value, right) => + if(value == x) true + else if(value < x) search(right, x) + else search(left, x) + } + } ensuring(res => res == content(t).contains(x)) + + def searchImp(t: Tree, x: Int): Boolean = { + require(binaryTreeProp(t)) + var res = false + var t2: Tree = t + (while(!t2.isInstanceOf[Leaf]) { + val Node(left, value, right) = t2 + if(value == x) + res = true + else if(value < x) + t2 = right + else + t2 = left + }) + res + } ensuring(res => res == content(t).contains(x)) + +} diff --git a/src/test/resources/regression/termination/valid/BubbleSort.scala b/src/test/resources/regression/termination/valid/BubbleSort.scala new file mode 100644 index 000000000..3551ebf16 --- /dev/null +++ b/src/test/resources/regression/termination/valid/BubbleSort.scala @@ -0,0 +1,74 @@ +import leon.Utils._ + +/* The calculus of Computation textbook */ + +object BubbleSort { + + def sort(a: Array[Int]): Array[Int] = ({ + require(a.length >= 1) + var i = a.length - 1 + var j = 0 + val sa = a.clone + (while(i > 0) { + j = 0 + (while(j < i) { + if(sa(j) > sa(j+1)) { + val tmp = sa(j) + sa(j) = sa(j+1) + sa(j+1) = tmp + } + j = j + 1 + }) invariant( + j >= 0 && + j <= i && + i < sa.length && + sa.length >= 0 && + partitioned(sa, 0, i, i+1, sa.length-1) && + sorted(sa, i, sa.length-1) && + partitioned(sa, 0, j-1, j, j) + ) + i = i - 1 + }) invariant( + i >= 0 && + i < sa.length && + sa.length >= 0 && + partitioned(sa, 0, i, i+1, sa.length-1) && + sorted(sa, i, sa.length-1) + ) + sa + }) ensuring(res => sorted(res, 0, a.length-1)) + + def sorted(a: Array[Int], l: Int, u: Int): Boolean = { + require(a.length >= 0 && l >= 0 && u < a.length && l <= u) + var k = l + var isSorted = true + (while(k < u) { + if(a(k) > a(k+1)) + isSorted = false + k = k + 1 + }) invariant(k <= u && k >= l) + isSorted + } + + def partitioned(a: Array[Int], l1: Int, u1: Int, l2: Int, u2: Int): Boolean = { + require(a.length >= 0 && l1 >= 0 && u1 < l2 && u2 < a.length) + if(l2 > u2 || l1 > u1) + true + else { + var i = l1 + var j = l2 + var isPartitionned = true + (while(i <= u1) { + j = l2 + (while(j <= u2) { + if(a(i) > a(j)) + isPartitionned = false + j = j + 1 + }) invariant(j >= l2 && i <= u1) + i = i + 1 + }) invariant(i >= l1) + isPartitionned + } + } + +} diff --git a/src/test/resources/regression/termination/valid/ComplexChains.scala b/src/test/resources/regression/termination/valid/ComplexChains.scala new file mode 100644 index 000000000..568e09bb1 --- /dev/null +++ b/src/test/resources/regression/termination/valid/ComplexChains.scala @@ -0,0 +1,26 @@ +import leon.Utils._ + +object ComplexChains { + + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def f1(list: List): List = list match { + case Cons(head, tail) if head > 0 => f2(Cons(1, list)) + case Cons(head, tail) if head < 0 => f3(Cons(-1, list)) + case Cons(head, tail) => f1(tail) + case Nil() => Nil() + } + + def f2(list: List): List = f3(Cons(0, list)) + + def f3(list: List): List = f1(list match { + case Cons(head, Cons(head2, Cons(head3, tail))) => tail + case Cons(head, Cons(head2, tail)) => tail + case Cons(head, tail) => tail + case Nil() => Nil() + }) +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/valid/Fibonacci.scala b/src/test/resources/regression/termination/valid/Fibonacci.scala new file mode 100644 index 000000000..f2140e54d --- /dev/null +++ b/src/test/resources/regression/termination/valid/Fibonacci.scala @@ -0,0 +1,15 @@ +object Fibonacci { + def fib(x: Int) : Int = { + require(x >= 0) + if(x < 2) { + x + } else { + fib(x - 1) + fib(x - 2) + } + } + + // requires that fib is universally quantified to work... + def check() : Boolean = { + fib(5) == 5 + } ensuring(_ == true) +} diff --git a/src/test/resources/regression/termination/valid/HardChains.scala.BAK b/src/test/resources/regression/termination/valid/HardChains.scala.BAK new file mode 100644 index 000000000..4ffe63f60 --- /dev/null +++ b/src/test/resources/regression/termination/valid/HardChains.scala.BAK @@ -0,0 +1,37 @@ +import leon.Utils._ + +object HardChains { + + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def f1(list: List): List = list match { + case Cons(head, tail) if head > 0 => f2(list) + case Cons(head, tail) if head < 0 => f3(list) + case Nil() => Nil() + case _ => f4(list) + } + + def f2(list: List): List = list match { + case Cons(head, tail) if head > 0 => f4(Cons(0, list)) + case _ => f1(list) + } + + def f3(list: List): List = list match { + case Cons(head, tail) if head < 0 => f4(Cons(0, list)) + case Cons(head, tail) if head == 0 => f2(tail) + case _ => f4(list) + } + + def f4(list: List): List = list match { + case Cons(head, tail) if head == 0 => f3(tail) + case Cons(head, Cons(head2, Cons(head3, tail))) => tail + case Cons(head, Cons(head2, tail)) => tail + case Cons(head, tail) if head > 0 => f4(Cons(0, tail)) + case Cons(head, tail) => f4(tail) + case _ => f1(list) + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/valid/ListWithSize.scala b/src/test/resources/regression/termination/valid/ListWithSize.scala new file mode 100644 index 000000000..868cb4556 --- /dev/null +++ b/src/test/resources/regression/termination/valid/ListWithSize.scala @@ -0,0 +1,135 @@ +import scala.collection.immutable.Set +import leon.Annotations._ +import leon.Utils._ + +object ListWithSize { + sealed abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil() extends List + + sealed abstract class IntPairList + case class IPCons(head: IntPair, tail: IntPairList) extends IntPairList + case class IPNil() extends IntPairList + + sealed abstract class IntPair + case class IP(fst: Int, snd: Int) extends IntPair + + // proved with unrolling=0 + def size(l: List) : Int = (l match { + case Nil() => 0 + case Cons(_, t) => 1 + size(t) + }) ensuring(res => res >= 0) + + def iplSize(l: IntPairList) : Int = (l match { + case IPNil() => 0 + case IPCons(_, xs) => 1 + iplSize(xs) + }) ensuring(_ >= 0) + + def zip(l1: List, l2: List) : IntPairList = { + // try to comment this and see how pattern-matching becomes + // non-exhaustive and post-condition fails + require(size(l1) == size(l2)) + + l1 match { + case Nil() => IPNil() + case Cons(x, xs) => l2 match { + case Cons(y, ys) => IPCons(IP(x, y), zip(xs, ys)) + } + } + } ensuring(iplSize(_) == size(l1)) + + def sizeTailRec(l: List) : Int = sizeTailRecAcc(l, 0) + def sizeTailRecAcc(l: List, acc: Int) : Int = { + require(acc >= 0) + l match { + case Nil() => acc + case Cons(_, xs) => sizeTailRecAcc(xs, acc+1) + } + } ensuring(res => res == size(l) + acc) + + def sizesAreEquiv(l: List) : Boolean = { + size(l) == sizeTailRec(l) + } holds + + def content(l: List) : Set[Int] = l match { + case Nil() => Set.empty[Int] + case Cons(x, xs) => Set(x) ++ content(xs) + } + + def sizeAndContent(l: List) : Boolean = { + size(l) == 0 || content(l) != Set.empty[Int] + } holds + + def drunk(l : List) : List = (l match { + case Nil() => Nil() + case Cons(x,l1) => Cons(x,Cons(x,drunk(l1))) + }) ensuring (size(_) == 2 * size(l)) + + // proved with unrolling=1 + def funnyCons(x: Int, l: List) : List = (l match { + case Nil() => Cons(x, Nil()) + case c @ Cons(_,_) => Cons(x, c) + }) ensuring(size(_) > 0) + + // proved with unrolling=2 + def reverse(l: List) : List = reverse0(l, Nil()) ensuring(content(_) == content(l)) + def reverse0(l1: List, l2: List) : List = (l1 match { + case Nil() => l2 + case Cons(x, xs) => reverse0(xs, Cons(x, l2)) + }) ensuring(content(_) == content(l1) ++ content(l2)) + + def append(l1 : List, l2 : List) : List = (l1 match { + case Nil() => l2 + case Cons(x,xs) => Cons(x, append(xs, l2)) + }) ensuring(content(_) == content(l1) ++ content(l2)) + + @induct + def nilAppend(l : List) : Boolean = (append(l, Nil()) == l) holds + + // unclear if we needed this--it was meant to force folding + //def appendFold(x : Int, xs : List, ys : List) : Boolean = { + // true + //} ensuring (res => res && Cons(x,append(xs, ys)) == append(Cons(x,xs), ys)) + + @induct + def appendAssoc(xs : List, ys : List, zs : List) : Boolean = + (append(append(xs, ys), zs) == append(xs, append(ys, zs))) holds + + def revAuxBroken(l1 : List, e : Int, l2 : List) : Boolean = { + (append(reverse(l1), Cons(e,l2)) == reverse0(l1, l2)) + } // holds + + @induct + def reverse0exposed(l1 : List, l2 : List) : Boolean = { + (reverse0(l1, l2) == append(reverse(l1), l2)) + } // holds + + @induct + def sizeAppend(l1 : List, l2 : List) : Boolean = + (size(append(l1, l2)) == size(l1) + size(l2)) holds + + // proved with unrolling=4 + @induct + def concat(l1: List, l2: List) : List = + concat0(l1, l2, Nil()) ensuring(content(_) == content(l1) ++ content(l2)) + + @induct + def concat0(l1: List, l2: List, l3: List) : List = (l1 match { + case Nil() => l2 match { + case Nil() => reverse(l3) + case Cons(y, ys) => { + concat0(Nil(), ys, Cons(y, l3)) + } + } + case Cons(x, xs) => concat0(xs, l2, Cons(x, l3)) + }) ensuring(content(_) == content(l1) ++ content(l2) ++ content(l3)) + + def reverseConcat0(l1: List, l2: List) : Boolean = { + reverse(concat(l1, l2)) == concat(reverse(l2), reverse(l1)) + } // holds + + // proved with unrolling=2 ??? + def reverseConcat(l1: List, l2: List) : Boolean = { + reverse(concat(l1, l2)) == concat(reverse(l2), reverse(l1)) + } // holds +} diff --git a/src/test/resources/regression/termination/valid/Numeric1.scala b/src/test/resources/regression/termination/valid/Numeric1.scala new file mode 100644 index 000000000..2c4f65137 --- /dev/null +++ b/src/test/resources/regression/termination/valid/Numeric1.scala @@ -0,0 +1,11 @@ +import leon.Utils._ + +object Numeric { + + def f1(x: Int): Int = f2(x - 2) + + def f2(x: Int): Int = if (x < 0) 0 else f1(x + 1) +} + + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/valid/QuickSort.scala b/src/test/resources/regression/termination/valid/QuickSort.scala new file mode 100644 index 000000000..83dd2c364 --- /dev/null +++ b/src/test/resources/regression/termination/valid/QuickSort.scala @@ -0,0 +1,58 @@ +import scala.collection.immutable.Set + +object QuickSort { + sealed abstract class List + case class Cons(head:Int,tail:List) extends List + case class Nil() extends List + + def contents(l: List): Set[Int] = l match { + case Nil() => Set.empty + case Cons(x,xs) => contents(xs) ++ Set(x) + } + + def is_sorted(l: List): Boolean = l match { + case Nil() => true + case Cons(x,Nil()) => true + case Cons(x,Cons(y,xs)) => x<=y && is_sorted(Cons(y,xs)) + } + + def rev_append(aList:List,bList:List): List = aList match { + case Nil() => bList + case Cons(x,xs) => rev_append(xs,Cons(x,bList)) + } + + def reverse(list:List): List = rev_append(list,Nil()) + + def append(aList:List,bList:List): List = aList match { + case Nil() => bList + case _ => rev_append(reverse(aList),bList) + } + + def greater(n:Int,list:List) : List = list match { + case Nil() => Nil() + case Cons(x,xs) => if (n < x) Cons(x,greater(n,xs)) else greater(n,xs) + } + + def smaller(n:Int,list:List) : List = list match { + case Nil() => Nil() + case Cons(x,xs) => if (n>x) Cons(x,smaller(n,xs)) else smaller(n,xs) + } + + def equals(n:Int,list:List) : List = list match { + case Nil() => Nil() + case Cons(x,xs) => if (n==x) Cons(x,equals(n,xs)) else equals(n,xs) + } + + def quickSort(list:List): List = (list match { + case Nil() => Nil() + case Cons(x,Nil()) => list + case Cons(x,xs) => append(append(quickSort(smaller(x,xs)),Cons(x,equals(x,xs))),quickSort(greater(x,xs))) + }) ensuring(res => contents(res) == contents(list)) // && is_sorted(res)) + + def main(args: Array[String]): Unit = { + val ls: List = Cons(5, Cons(2, Cons(4, Cons(5, Cons(1, Cons(8,Nil())))))) + + println(ls) + println(quickSort(ls)) + } +} diff --git a/src/test/resources/regression/termination/valid/RedBlackTree.scala b/src/test/resources/regression/termination/valid/RedBlackTree.scala new file mode 100644 index 000000000..0952a11fe --- /dev/null +++ b/src/test/resources/regression/termination/valid/RedBlackTree.scala @@ -0,0 +1,59 @@ +import scala.collection.immutable.Set +//import scala.collection.immutable.Multiset + +object RedBlackTree { + sealed abstract class Color + case class Red() extends Color + case class Black() extends Color + + sealed abstract class Tree + case class Empty() extends Tree + case class Node(color: Color, left: Tree, value: Int, right: Tree) extends Tree + + def content(t : Tree) : Set[Int] = t match { + case Empty() => Set.empty + case Node(_, l, v, r) => content(l) ++ Set(v) ++ content(r) + } + + def size(t : Tree) : Int = t match { + case Empty() => 0 + case Node(_, l, v, r) => size(l) + 1 + size(r) + } + + def ins(x : Int, t: Tree): Tree = (t match { + case Empty() => Node(Red(),Empty(),x,Empty()) + case Node(c,a,y,b) => + if (x < y) balance(c, ins(x, a), y, b) + else if (x == y) Node(c,a,y,b) + else balance(c,a,y,ins(x, b)) + }) ensuring (res => ( + (content(res) == content(t) ++ Set(x)) + && (size(t) <= size(res) && size(res) < size(t) + 2) + )) + + def add(x: Int, t: Tree): Tree = { + makeBlack(ins(x, t)) + } ensuring (content(_) == content(t) ++ Set(x)) + + def balance(c: Color, a: Tree, x: Int, b: Tree): Tree = (Node(c,a,x,b) match { + case Node(Black(),Node(Red(),Node(Red(),a,xV,b),yV,c),zV,d) => + Node(Red(),Node(Black(),a,xV,b),yV,Node(Black(),c,zV,d)) + case Node(Black(),Node(Red(),a,xV,Node(Red(),b,yV,c)),zV,d) => + Node(Red(),Node(Black(),a,xV,b),yV,Node(Black(),c,zV,d)) + case Node(Black(),a,xV,Node(Red(),Node(Red(),b,yV,c),zV,d)) => + Node(Red(),Node(Black(),a,xV,b),yV,Node(Black(),c,zV,d)) + case Node(Black(),a,xV,Node(Red(),b,yV,Node(Red(),c,zV,d))) => + Node(Red(),Node(Black(),a,xV,b),yV,Node(Black(),c,zV,d)) + case Node(c,a,xV,b) => Node(c,a,xV,b) + }) ensuring (res => content(res) == content(Node(c,a,x,b))) + + def makeBlack(n: Tree): Tree = n match { + case Node(Red(),l,v,r) => Node(Black(),l,v,r) + case _ => n + } + + def flip(t : Tree) : Tree = t match { + case Empty() => Empty() + case Node(color,l,e,r) => Node(color,flip(r),e,flip(l)) + } +} diff --git a/src/test/resources/regression/termination/valid/SimpInterpret.scala b/src/test/resources/regression/termination/valid/SimpInterpret.scala new file mode 100644 index 000000000..bcb7bafc8 --- /dev/null +++ b/src/test/resources/regression/termination/valid/SimpInterpret.scala @@ -0,0 +1,67 @@ +//import scala.collection.immutable.Set +//import leon.Annotations._ +import leon.Utils._ + +object Interpret { + abstract class BoolTree + case class Eq(t1 : IntTree, t2 : IntTree) extends BoolTree + case class And(t1 : BoolTree, t2 : BoolTree) extends BoolTree + case class Not(t : BoolTree) extends BoolTree + + abstract class IntTree + case class Const(c:Int) extends IntTree + case class Var() extends IntTree + case class Plus(t1 : IntTree, t2 : IntTree) extends IntTree + case class Minus(t1 : IntTree, t2 : IntTree) extends IntTree + case class Less(t1 : IntTree, t2 : IntTree) extends BoolTree + case class If(cond : BoolTree, t : IntTree, e : IntTree) extends IntTree + + def repOk(t : IntTree) : Boolean = { + true + } + + def beval(t:BoolTree, x0 : Int) : Boolean = { + t match { + case Less(t1, t2) => ieval(t1,x0) < ieval(t2,x0) + case Eq(t1, t2) => ieval(t1,x0) == ieval(t2,x0) + case And(t1, t2) => beval(t1,x0) && beval(t2,x0) + case Not(t1) => !beval(t1,x0) + } + } + + def ieval(t:IntTree, x0 : Int) : Int = { + t match { + case Const(c) => c + case Var() => x0 + case Plus(t1,t2) => ieval(t1,x0) + ieval(t2,x0) + case Minus(t1, t2) => ieval(t1,x0) - ieval(t2,x0) + case If(c,t1,t2) => if (beval(c,x0)) ieval(t1,x0) else ieval(t2,x0) + } + } + def computesPositive(t : IntTree) : Boolean = { + ieval(t,0) >= 0 && + ieval(t,1) >= 0 && + ieval(t,-1) >= 0 && + ieval(t,-2) >= 0 && + ieval(t,2) >= 0 + } + def identityForPositive(t : IntTree) : Boolean = { + ieval(t, 5) == 5 && + ieval(t, 33) == 33 && + ieval(t, 0) == 0 && + ieval(t, -1) == 1 && + ieval(t, -2) == 2 + } + + def treeBad(t : IntTree) : Boolean = { + !(repOk(t) && computesPositive(t) && identityForPositive(t)) + } holds + + def thereIsGoodTree() : Boolean = { + !treeBad(If(Less(Const(0),Var()), Var(), Minus(Const(0),Var()))) + } holds + + def main(args : Array[String]) { + thereIsGoodTree() + } +} diff --git a/src/test/resources/regression/termination/valid/Termination_passing1.scala b/src/test/resources/regression/termination/valid/Termination_passing1.scala new file mode 100644 index 000000000..fffb8bca9 --- /dev/null +++ b/src/test/resources/regression/termination/valid/Termination_passing1.scala @@ -0,0 +1,23 @@ +import leon.Utils._ + +object Termination { + abstract class List + case class Cons(head: Int, tail: List) extends List + case class Nil extends List + + def f1(list: List) : Int = f2(list) + + def f2(list: List) : Int = list match { + case Cons(head, tail) => f1(tail) + case Nil() => 0 + } + + def f3(list: List, b: Boolean) : Int = if(b) f4(list) else f1(list) + + def f4(list: List) : Int = list match { + case Cons(head, tail) => f3(tail, true) + case Nil() => 0 + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/resources/regression/termination/valid/Termination_passing2.scala b/src/test/resources/regression/termination/valid/Termination_passing2.scala new file mode 100644 index 000000000..8fbbcd83e --- /dev/null +++ b/src/test/resources/regression/termination/valid/Termination_passing2.scala @@ -0,0 +1,20 @@ +import leon.Utils._ + +object Termination { + def f1(x: Int, b: Boolean) : Int = { + if (b) f2(x) + else f3(x) + } + + def f2(x: Int) : Int = { + if (x < 0) 0 + else f1(x - 1, true) + } + + def f3(x: Int) : Int = { + if (x > 0) 0 + else f1(x + 1, false) + } +} + +// vim: set ts=4 sw=4 et: diff --git a/src/test/scala/leon/test/termination/TerminationRegression.scala b/src/test/scala/leon/test/termination/TerminationRegression.scala new file mode 100644 index 000000000..0ccc6c864 --- /dev/null +++ b/src/test/scala/leon/test/termination/TerminationRegression.scala @@ -0,0 +1,109 @@ +/* Copyright 2009-2013 EPFL, Lausanne */ + +package leon +package test +package termination + +import leon.termination._ + +import org.scalatest.FunSuite + +import java.io.File + +import TestUtils._ + +class TerminationRegression extends FunSuite { + private var counter : Int = 0 + private def nextInt() : Int = { + counter += 1 + counter + } + private case class Output(report : TerminationReport, reporter : Reporter) + + private def mkPipeline : Pipeline[List[String],TerminationReport] = + leon.plugin.ExtractionPhase andThen leon.SubtypingPhase andThen leon.termination.TerminationPhase + + private def mkTest(file : File, leonOptions: Seq[LeonOption], forError: Boolean)(block: Output=>Unit) = { + val fullName = file.getPath() + val start = fullName.indexOf("regression") + + val displayName = if(start != -1) { + fullName.substring(start, fullName.length) + } else { + fullName + } + + test("%3d: %s %s".format(nextInt(), displayName, leonOptions.mkString(" "))) { + assert(file.exists && file.isFile && file.canRead, + "Benchmark %s is not a readable file".format(displayName)) + + val ctx = LeonContext( + settings = Settings( + synthesis = false, + xlang = false, + verify = false, + termination = true + ), + options = leonOptions, + files = List(file), + reporter = new SilentReporter + ) + + val pipeline = mkPipeline + + if(forError) { + intercept[LeonFatalError]{ + pipeline.run(ctx)(file.getPath :: Nil) + } + } else { + + val report = pipeline.run(ctx)(file.getPath :: Nil) + + block(Output(report, ctx.reporter)) + } + } + } + + private def forEachFileIn(cat : String, forError: Boolean = false)(block : Output=>Unit) { + val fs = filesInResourceDir( + "regression/termination/" + cat, + _.endsWith(".scala")) + + for(f <- fs) { + mkTest(f, Seq(), forError)(block) + } + } + + forEachFileIn("valid") { output => + val Output(report, reporter) = output + val failures = report.results.collect { case (fd, guarantee) if !guarantee.isGuaranteed => fd } + assert(failures.isEmpty, "Functions " + failures.map(_.id) + " should terminate") + // can't say anything about error counts because of postcondition strengthening that might fail (normal behavior) + // assert(reporter.errorCount === 0) + assert(reporter.warningCount === 0) + } + + forEachFileIn("looping") { output => + val Output(report, reporter) = output + val looping = report.results.filter { case (fd, guarantee) => fd.id.name.startsWith("looping") } + assert(looping.forall(_._2.isInstanceOf[LoopsGivenInputs]), "Functions " + looping.filter(!_._2.isInstanceOf[LoopsGivenInputs]).map(_._1.id) + " should loop") + val calling = report.results.filter { case (fd, guarantee) => fd.id.name.startsWith("calling") } + assert(calling.forall(_._2.isInstanceOf[CallsNonTerminating]), "Functions " + calling.filter(!_._2.isInstanceOf[CallsNonTerminating]).map(_._1.id) + " should call non-terminating") + val ok = report.results.filter { case (fd, guarantee) => fd.id.name.startsWith("ok") } + assert(ok.forall(_._2.isGuaranteed), "Functions " + ok.filter(!_._2.isGuaranteed).map(_._1.id) + " should terminate") +// assert(reporter.errorCount >= looping.size + calling.size) + assert(reporter.warningCount === 0) + } + + forEachFileIn("unknown") { output => + val Output(report, reporter) = output + val unknown = report.results.filter { case (fd, guarantee) => fd.id.name.startsWith("unknown") } + assert(unknown.forall(_._2 == NoGuarantee), "Functions " + unknown.filter(_._2 != NoGuarantee).map(_._1.id) + " should be unknown") + // can't say anything about error counts because of postcondition strengthening that might fail (normal behavior) + // assert(reporter.errorCount === 0) + assert(reporter.warningCount === 0) + } + + forEachFileIn("error", true) { output => () } + +} -- GitLab