Skip to content

Commit 1e71be5

Browse files
authored
Add refutation support (#583)
* Add refutation plugin
1 parent 119564b commit 1e71be5

10 files changed

Lines changed: 230 additions & 14 deletions

File tree

src/main/scala/viper/silver/ast/Ast.scala

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -367,40 +367,48 @@ trait Info {
367367

368368
/** A default `Info` that is empty. */
369369
case object NoInfo extends Info {
370-
lazy val comment = Nil
371-
lazy val isCached = false
370+
override val comment = Nil
371+
override val isCached = false
372372
}
373373

374374
/** A simple `Info` that contains a list of comments. */
375375
case class SimpleInfo(comment: Seq[String]) extends Info {
376-
lazy val isCached = false
376+
override val isCached = false
377377
}
378378

379379
/** An `Info` instance for labelling a quantifier as auto-triggered. */
380380
case object AutoTriggered extends Info {
381-
lazy val comment = Nil
382-
lazy val isCached = false
381+
override val comment = Nil
382+
override val isCached = false
383383
}
384384

385385
/** An `Info` instance for labelling a pre-verified AST node (e.g., via caching). */
386386
case object Cached extends Info {
387-
lazy val comment = Nil
388-
lazy val isCached = true
387+
override val comment = Nil
388+
override val isCached = true
389389
}
390390

391391
/** An `Info` instance for labelling a node as synthesized. A synthesized node is one that
392392
* was not present in the original program that was passed to a Viper backend, such as nodes that
393393
* originate from an AST transformation.
394394
*/
395395
case object Synthesized extends Info {
396-
lazy val comment = Nil
397-
lazy val isCached = false
396+
override val comment = Nil
397+
override val isCached = false
398+
}
399+
400+
/** An `Info` instance for labelling an AST node which is expected to fail verification.
401+
* This is used by Silicon to avoid stopping verification.
402+
*/
403+
abstract class FailureExpectedInfo extends Info {
404+
override val comment = Nil
405+
override val isCached = false
398406
}
399407

400408
/** An `Info` instance for composing multiple `Info`s together */
401409
case class ConsInfo(head: Info, tail: Info) extends Info {
402-
lazy val comment = head.comment ++ tail.comment
403-
lazy val isCached = head.isCached || tail.isCached
410+
override val comment = head.comment ++ tail.comment
411+
override val isCached = head.isCached || tail.isCached
404412
}
405413

406414
/** Build a `ConsInfo` instance out of two `Info`s, unless the latter is `NoInfo` (which can be dropped) */

src/main/scala/viper/silver/frontend/SilFrontend.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ trait SilFrontend extends DefaultFrontend {
8383
private val defaultPlugins: Seq[String] = Seq(
8484
"viper.silver.plugin.standard.adt.AdtPlugin",
8585
"viper.silver.plugin.standard.termination.TerminationPlugin",
86+
"viper.silver.plugin.standard.refute.RefutePlugin",
8687
"viper.silver.plugin.standard.predicateinstance.PredicateInstancePlugin"
8788
)
8889
/** For testing of plugin import feature */
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) 2011-2022 ETH Zurich.
6+
7+
package viper.silver.plugin.standard.refute
8+
9+
import viper.silver.ast._
10+
import viper.silver.ast.pretty.FastPrettyPrinter.{ContOps, text, toParenDoc}
11+
import viper.silver.ast.pretty.PrettyPrintPrimitives
12+
13+
/** An `FailureExpectedInfo` info that tells us that this assert is a refute. */
14+
case object RefuteInfo extends FailureExpectedInfo
15+
16+
case class Refute(exp: Exp)(val pos: Position = NoPosition, val info: Info = NoInfo, val errT: ErrorTrafo = NoTrafos) extends ExtensionStmt {
17+
override lazy val prettyPrint: PrettyPrintPrimitives#Cont = text("refute") <+> toParenDoc(exp)
18+
19+
override val extensionSubnodes: Seq[Node] = Seq(exp)
20+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) 2011-2022 ETH Zurich.
6+
7+
package viper.silver.plugin.standard.refute
8+
9+
import viper.silver.verifier._
10+
11+
case class RefuteFailed(override val offendingNode: Refute, override val reason: ErrorReason, override val cached: Boolean = false) extends AbstractVerificationError {
12+
override val id = "refute.failed"
13+
override val text = "Refute holds in all cases or could not be reached (e.g. see Silicon `numberOfErrorsToReport`)."
14+
15+
override def withNode(offendingNode: errors.ErrorNode = this.offendingNode): RefuteFailed =
16+
RefuteFailed(this.offendingNode, this.reason, this.cached)
17+
18+
override def withReason(r: ErrorReason): RefuteFailed = RefuteFailed(offendingNode, r, cached)
19+
}
20+
21+
case class RefutationTrue(offendingNode: reasons.ErrorNode) extends AbstractErrorReason {
22+
override val id: String = "refutation.true"
23+
24+
override val readableMessage: String = s"Assertion $offendingNode definitely holds."
25+
26+
override def withNode(offendingNode: reasons.ErrorNode): ErrorMessage = RefutationTrue(offendingNode)
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) 2011-2022 ETH Zurich.
6+
7+
package viper.silver.plugin.standard.refute
8+
9+
import viper.silver.ast.{Position, Stmt}
10+
import viper.silver.parser.TypeHelper.Bool
11+
import viper.silver.parser._
12+
13+
case class PRefute(e: PExp)(val pos: (Position, Position)) extends PExtender with PStmt {
14+
override val getSubnodes: Seq[PNode] = Seq(e)
15+
16+
override def typecheck(t: TypeChecker, n: NameAnalyser):Option[Seq[String]] = {
17+
t.check(e, Bool)
18+
None
19+
}
20+
21+
override def translateStmt(t: Translator): Stmt = Refute(t.exp(e))(t.liftPos(this))
22+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) 2011-2022 ETH Zurich.
6+
7+
package viper.silver.plugin.standard.refute
8+
9+
import fastparse.P
10+
import viper.silver.ast.utility.ViperStrategy
11+
import viper.silver.ast._
12+
import viper.silver.parser.FastParser.{FP, exp, keyword, whitespace}
13+
import viper.silver.parser.ParserExtension
14+
import viper.silver.plugin.{ParserPluginTemplate, SilverPlugin}
15+
import viper.silver.verifier._
16+
import viper.silver.verifier.errors.AssertFailed
17+
18+
class RefutePlugin extends SilverPlugin with ParserPluginTemplate {
19+
20+
/** Keyword used to define refute statements. */
21+
private val refuteKeyword: String = "refute"
22+
23+
private var refuteAsserts: Map[Position, Refute] = Map()
24+
25+
/** Parser for refute statements. */
26+
def refute[_: P]: P[PRefute] =
27+
FP(keyword(refuteKeyword) ~/ exp).map{ case (pos, e) => PRefute(e)(pos) }
28+
29+
/** Add refute to the parser. */
30+
override def beforeParse(input: String, isImported: Boolean): String = {
31+
// Add new keyword
32+
ParserExtension.addNewKeywords(Set[String](refuteKeyword))
33+
// Add new parser to the precondition
34+
ParserExtension.addNewStmtAtEnd(refute(_))
35+
input
36+
}
37+
38+
/**
39+
* Remove refute statements from the AST and add them as non-det asserts.
40+
* The ⭐ is nice since such a variable name cannot be parsed, but will it cause issues?
41+
*/
42+
override def beforeVerify(input: Program): Program =
43+
ViperStrategy.Slim({
44+
case r@Refute(exp) => {
45+
this.refuteAsserts += (r.pos -> r)
46+
Seqn(Seq(
47+
If(LocalVar(s"__plugin_refute_nondet${this.refuteAsserts.size}", Bool)(r.pos),
48+
Seqn(Seq(
49+
Assert(exp)(r.pos, RefuteInfo),
50+
Inhale(BoolLit(false)(r.pos))(r.pos)
51+
), Seq())(r.pos),
52+
Seqn(Seq(), Seq())(r.pos))(r.pos)
53+
),
54+
Seq(LocalVarDecl(s"__plugin_refute_nondet${this.refuteAsserts.size}", Bool)(r.pos))
55+
)(r.pos)
56+
}
57+
}).recurseFunc({
58+
case Method(_, _, _, _, _, body) => Seq(body)
59+
}).execute(input)
60+
61+
/** Remove refutation related errors and add refuteAsserts that didn't report an error. */
62+
override def mapVerificationResult(input: VerificationResult): VerificationResult = {
63+
val errors: Seq[AbstractError] = (input match {
64+
case Success => Seq()
65+
case Failure(errors) => {
66+
errors.filter {
67+
case AssertFailed(a, _, _) if a.info == RefuteInfo => {
68+
this.refuteAsserts -= a.pos
69+
false
70+
}
71+
case _ => true
72+
}
73+
}
74+
}) ++ this.refuteAsserts.map(r => RefuteFailed(r._2, RefutationTrue(r._2.exp)))
75+
if (errors.length == 0) Success
76+
else Failure(errors)
77+
}
78+
}

src/main/scala/viper/silver/plugin/standard/termination/TerminationPlugin.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class TerminationPlugin(reporter: viper.silver.reporter.Reporter,
2828
/**
2929
* Keyword used to define decreases clauses
3030
*/
31-
private val DecreasesKeyword: String = "decreases"
31+
private val decreasesKeyword: String = "decreases"
3232

3333
/**
3434
* Parser for decreases clauses with following possibilities.
@@ -40,7 +40,7 @@ class TerminationPlugin(reporter: viper.silver.reporter.Reporter,
4040
* decreases *
4141
*/
4242
def decreases[_: P]: P[PDecreasesClause] =
43-
P(keyword(DecreasesKeyword) ~/ (decreasesWildcard | decreasesStar | decreasesTuple) ~ ";".?)
43+
P(keyword(decreasesKeyword) ~/ (decreasesWildcard | decreasesStar | decreasesTuple) ~ ";".?)
4444
def decreasesTuple[_: P]: P[PDecreasesTuple] =
4545
FP(exp.rep(sep = ",") ~/ condition.?).map { case (pos, (a, c)) => PDecreasesTuple(a, c)(pos) }
4646
def decreasesWildcard[_: P]: P[PDecreasesWildcard] = FP("_" ~/ condition.?).map{ case (pos, c) => PDecreasesWildcard(c)(pos) }
@@ -53,7 +53,7 @@ class TerminationPlugin(reporter: viper.silver.reporter.Reporter,
5353
*/
5454
override def beforeParse(input: String, isImported: Boolean): String = {
5555
// Add new keyword
56-
ParserExtension.addNewKeywords(Set[String](DecreasesKeyword))
56+
ParserExtension.addNewKeywords(Set[String](decreasesKeyword))
5757
// Add new parser to the precondition
5858
ParserExtension.addNewPreCondition(decreases(_))
5959
// Add new parser to the postcondition

src/main/scala/viper/silver/verifier/VerificationError.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,12 @@ trait ErrorMessage {
153153

154154
// Should consider refactoring as a transformer, if/once we make the error message structure recursive
155155
def withNode(offendingNode: errors.ErrorNode = this.offendingNode) : ErrorMessage
156+
157+
// Check if the offendingNode contains any `FailureExpectedInfo` info tags
158+
def isExpected: Boolean = offendingNode match {
159+
case i: Infoed => i.info.getUniqueInfo[FailureExpectedInfo].isDefined
160+
case _ => false
161+
}
156162
}
157163

158164
trait VerificationError extends AbstractError with ErrorMessage {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
field f: Int
2+
3+
function foo(x:Ref): Bool
4+
requires acc(x.f) { x.f == x.f }
5+
6+
method bar(y: Int)
7+
requires y > 10
8+
{
9+
var x: Ref
10+
inhale acc(x.f)
11+
label l
12+
while (x.f > y)
13+
invariant acc(x.f) && (old[l](x.f >= y) ==> x.f >= y)
14+
{
15+
refute x.f < y
16+
x.f := x.f - 1
17+
//:: ExpectedOutput(refute.failed:refutation.true)
18+
refute foo(x)
19+
}
20+
refute x.f == y
21+
22+
var z: Int
23+
if (z > 10) {
24+
z := z+1
25+
} else {
26+
z := z-1
27+
}
28+
if (z < -10) {
29+
z := z+1
30+
} else {
31+
z := z-1
32+
}
33+
refute z == 10
34+
//:: ExpectedOutput(refute.failed:refutation.true)
35+
refute z < 9 || z > 10
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
method foo(x: Int) returns (r: Int)
2+
requires x > 10
3+
{
4+
refute !(x > 0)
5+
//:: ExpectedOutput(refute.failed:refutation.true)
6+
refute x > 0
7+
refute false
8+
//:: ExpectedOutput(refute.failed:refutation.true)
9+
refute true
10+
refute false
11+
if (x > 0) {
12+
r := x
13+
} else {
14+
//:: ExpectedOutput(refute.failed:refutation.true)
15+
refute false
16+
r := 0
17+
}
18+
}

0 commit comments

Comments
 (0)