-
Notifications
You must be signed in to change notification settings - Fork 53
Comprehensive backend-independent counterexamples #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
5ee32cf
Add tests for recent Silicon changes
marcoeilers 33bf34f
Merge branch 'master' of https://github.com/viperproject/silver
marcoeilers 2dd7e13
General counterexample definition by Raoul
marcoeilers 1528fbe
General counterexample definition by Raoul
marcoeilers c235125
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers 028ab03
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers 469ec66
Added a convenience method
marcoeilers d15460f
Moving test files from Silicon
marcoeilers f6c7163
Defining common interface for Silicon/Carbon CEs
marcoeilers 062f25c
Moving CE test infrastructure to silver
marcoeilers d7d7e95
Adding quantified field example
marcoeilers 5e1fda9
Predicates in test annotations, fixed an output message
marcoeilers 20e43cf
Renamed CE variations, using AST expressions instead of new CEValue type
marcoeilers 2dbce50
Adding test case
marcoeilers 59fa417
Better model for wand entries
marcoeilers 91b90ac
Renaming
marcoeilers fbd5149
Better test that forces QP information in the SMT model
marcoeilers 6e787ac
Improved tests
marcoeilers 5bb7240
More tests
marcoeilers f2f1796
More tests
marcoeilers d6d3a54
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers fff1b55
Cleanup
marcoeilers dcefc25
Real value parsing
marcoeilers c5a026d
Cleaned up wand entries
marcoeilers cb832f6
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers 91c8585
Changes after code review
marcoeilers 4364429
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers c39e78c
Restoring mapped counterexample test
marcoeilers 6d25d67
Proper tests for wands, fields inside predicates
marcoeilers cfe5000
Clarified documentation to say which CE models are preferred, which a…
marcoeilers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
294 changes: 294 additions & 0 deletions
294
src/main/scala/viper/silver/verifier/Counterexample.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,294 @@ | ||
| // This Source Code Form is subject to the terms of the Mozilla Public | ||
| // License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| // file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| // | ||
| // Copyright (c) 2011-2026 ETH Zurich. | ||
|
|
||
| package viper.silver.verifier | ||
| import viper.silver.ast | ||
| import viper.silver.ast.{AbstractLocalVar, Exp, Type, Resource} | ||
|
|
||
| /** | ||
| * Classes used to build general counterexamples. Two layers are distinguished: | ||
| * | ||
| * - a "raw" counterexample ([[RawCounterexample]]) that collects the information from the backend | ||
| * model in a simple form, with heap resources still identified by the backend-internal | ||
| * (SMT/Boogie) reference and field identifiers, and | ||
| * - a "resolved" counterexample ([[ResolvedCounterexample]]) that makes the raw one | ||
| * human-readable, e.g. by binding heap resources to their AST nodes (fields, predicates and | ||
| * magic wands). | ||
| * | ||
| * Legacy counterexample formats are defined elsewhere: Silicon-specific mapped counterexample inside Silicon, | ||
| * and the old variable-only counterexample is defined along with Viper's format for SMT model entries | ||
| * in viper.silver.verifier.VerificationError. | ||
| * | ||
| * Values are represented as ordinary Viper AST expressions ([[ast.Exp]]). Literals that have no | ||
| * ordinary Viper representation use the dedicated counterexample literals [[ast.RefLit]] (a | ||
| * concrete reference) and [[ast.BackendValueLit]] (an otherwise opaque backend value). | ||
| */ | ||
|
|
||
| trait RawCounterexample { | ||
| val basicVariables: Seq[CEVariable] | ||
| val allSequences: Seq[CECollection] | ||
| val allSets: Seq[CECollection] | ||
| val allMultisets: Seq[CECollection] | ||
| val allMaps: Seq[CECollection] | ||
| lazy val allCollections: Seq[CECollection] = allSequences ++ allSets ++ allMultisets ++ allMaps | ||
| def allRawHeaps: Seq[(String, RawHeap)] | ||
|
|
||
| val domainEntries: Seq[BasicDomainEntry] | ||
| val nonDomainFunctions: Seq[BasicFunctionEntry] | ||
| } | ||
|
|
||
| trait ResolvedCounterexample { | ||
| val rawCE: RawCounterexample | ||
| val ceStore: StoreCounterexample | ||
| val ceHeaps: Seq[(String, HeapCounterexample)] | ||
| lazy val heapMap = ceHeaps.toMap | ||
| val domainEntries: Seq[BasicDomainEntry] | ||
| val functionEntries: Seq[BasicFunctionEntry] | ||
| lazy val domainsAndFunctions = domainEntries ++ functionEntries | ||
| } | ||
|
|
||
| /** | ||
| * Helper for turning a backend value string (plus an optional Viper type) into the AST expression | ||
| * that represents it in a counterexample. | ||
| */ | ||
| object CounterexampleValue { | ||
| /** SMT solvers represent negative integers as an application, e.g. "(- 1)". */ | ||
| private val smtNegative = """^\(\s*-\s*(\d+)\s*\)$""".r | ||
|
|
||
| /** Parses an integer from its backend string representation, handling SMT-style negatives. */ | ||
| def parseInt(value: String): Option[BigInt] = { | ||
| val trimmed = value.trim | ||
| try Some(BigInt(trimmed)) catch { | ||
| case _: NumberFormatException => trimmed match { | ||
| case smtNegative(digits) => Some(-BigInt(digits)) | ||
| case _ => None | ||
| } | ||
| } | ||
| } | ||
|
|
||
| def parseBool(value: String): Option[Boolean] = value.trim.toLowerCase match { | ||
| case "true" => Some(true) | ||
| case "false" => Some(false) | ||
| case _ => None | ||
| } | ||
|
|
||
| /** SMT solvers represent a rational as a division application, e.g. "(/ 1.0 2.0)". */ | ||
| private val smtRealDiv = """^\(\s*/\s+(\S+)\s+(\S+)\s*\)$""".r | ||
|
|
||
| /** Parses a single rational token — an integer "3", a fraction "1/2" or a decimal "0.5"/"1.0" — | ||
| * into a (numerator, denominator) pair. */ | ||
| private def parseRationalToken(value: String): Option[(BigInt, BigInt)] = value.trim.split("/") match { | ||
| case Array(n, d) => for (ni <- parseInt(n); di <- parseInt(d) if di != 0) yield (ni, di) | ||
| case Array(single) if single.contains(".") => | ||
| val dotParts = single.split("\\.", 2) | ||
| val intPart = dotParts(0) | ||
| val fracPart = if (dotParts.length > 1) dotParts(1) else "" | ||
| parseInt(if ((intPart + fracPart).isEmpty) "0" else intPart + fracPart).map(num => (num, BigInt(10).pow(fracPart.length))) | ||
| case Array(single) => parseInt(single).map(i => (i, BigInt(1))) | ||
| case _ => None | ||
| } | ||
|
|
||
| /** Parses a permission amount from its backend representation — a fraction "1/2", an SMT real | ||
| * division "(/ 1.0 2.0)", a decimal or an integer — into a reduced "numerator/denominator" | ||
| * permission literal (rendered like the permission amounts elsewhere in the counterexample, e.g. | ||
| * "1/2", rather than as an AST fraction "1 / 2"). */ | ||
| def parsePerm(value: String): Option[ast.Exp] = { | ||
| val fraction: Option[(BigInt, BigInt)] = value.trim match { | ||
| case smtRealDiv(a, b) => for ((an, ad) <- parseRationalToken(a); (bn, bd) <- parseRationalToken(b) if bn != 0) yield (an * bd, ad * bn) | ||
| case other => parseRationalToken(other) | ||
| } | ||
| fraction.map { case (n, d) => | ||
| val g = n.gcd(d).max(1) | ||
| ast.BackendValueLit(s"${n / g}/${d / g}", ast.Perm)() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Infers a literal from a backend value string when no Viper type is available. The element | ||
| * types of collections are not always known, so this makes value comparison robust: an integer | ||
| * or boolean value becomes the corresponding literal regardless of whether the type was inferred. | ||
| */ | ||
| def inferLiteral(value: String): ast.Exp = | ||
| parseInt(value).map(i => ast.IntLit(i)(): ast.Exp) | ||
| .orElse(parseBool(value).map(b => ast.BoolLit(b)())) | ||
| .getOrElse(ast.BackendValueLit(value, ast.InternalType)()) | ||
|
|
||
| def literal(value: String, typ: Option[ast.Type]): ast.Exp = typ match { | ||
| case Some(ast.Int) => | ||
| parseInt(value).map(i => ast.IntLit(i)()).getOrElse(ast.BackendValueLit(value, ast.Int)()) | ||
| case Some(ast.Bool) => | ||
| parseBool(value).map(b => ast.BoolLit(b)()).getOrElse(ast.BackendValueLit(value, ast.Bool)()) | ||
| case Some(ast.Ref) => ast.RefLit(value)() | ||
| case Some(ast.Perm) => parsePerm(value).getOrElse(ast.BackendValueLit(value, ast.Perm)()) | ||
| case Some(t) => ast.BackendValueLit(value, t)() | ||
| case None => inferLiteral(value) | ||
| } | ||
| } | ||
|
|
||
| case class StoreCounterexample(storeEntries: Seq[StoreEntry]) { | ||
| override lazy val toString = storeEntries.map(x => x.toString).mkString("", "\n", "\n") | ||
| lazy val asMap: Map[String, ast.Exp] = storeEntries.map(se => (se.id.name, se.entry)).toMap | ||
| } | ||
|
|
||
| case class StoreEntry(id: AbstractLocalVar, entry: ast.Exp) { | ||
| override lazy val toString = s"Variable Name: ${id.name}, Value: ${entry.toString}, Type: ${id.typ.toString}" | ||
| } | ||
|
|
||
| case class HeapCounterexample(heapEntries: Seq[(Resource, ResolvedHeapEntry)]) { | ||
| var finalString = "" | ||
| var containsQP = false | ||
| heapEntries.foreach { case (re,he) => if (he.entryType == QPFieldType || he.entryType == QPPredicateType || he.entryType == QPMagicWandType) containsQP = true} | ||
| if (containsQP) | ||
| finalString ++= "The heap contains quantified permissions. Thus, we might own some permissions which are not shown in the counterexample.\n" | ||
| heapEntries.foreach { se => finalString ++= se._2.toString ++ "\n" } | ||
| override lazy val toString = finalString | ||
| } | ||
|
|
||
| sealed trait ResolvedHeapEntry { | ||
| val entryType : HeapEntryType | ||
| } | ||
|
|
||
| case class FieldResolvedEntry(ref: String, field: String, entry: ast.Exp, perm: Option[Rational], typ: Type, het: HeapEntryType) extends ResolvedHeapEntry { | ||
| val entryType = het | ||
| override lazy val toString = s"Field Entry: $ref.$field --> (Value: ${entry.toString}, Type: ${typ}, Perm: ${perm.getOrElse("#undefined").toString})" | ||
| } | ||
|
|
||
| case class PredResolvedEntry(name: String, args: Seq[Exp], perm: Option[Rational], insidePredicate: Option[scala.collection.immutable.Map[Exp, ModelEntry]], het: HeapEntryType) extends ResolvedHeapEntry { | ||
| val entryType = het | ||
| override lazy val toString = s"Predicate Entry: $name(${args.mkString("", ", ", ")")} --> (Perm: ${perm.getOrElse("#undefined").toString}) ${if (insidePredicate.isDefined && !insidePredicate.get.isEmpty) insidePredicate.get.toSeq.map(x => s"${x._1} --> ${x._2}").mkString("{\n ", "\n ", "\n}") else ""}" | ||
| } | ||
|
|
||
| case class WandResolvedEntry(left: Exp, right: Exp, perm: Option[Rational], het: HeapEntryType) extends ResolvedHeapEntry { | ||
| val entryType = het | ||
| override lazy val toString = s"Magic Wand Entry: ${ast.MagicWand(left, right)().toString} (Perm: ${perm.getOrElse("#undefined").toString})" | ||
| } | ||
|
|
||
| object WandResolvedEntry { | ||
| /** | ||
| * Builds a wand entry for the magic-wand structure `mw`, substituting the instance's argument | ||
| * values (`argValues`, given in the order of `subexpressionsToEvaluate`) into the wand's two | ||
| * sides. The substituted values are the same counterexample literals used elsewhere in the model. | ||
| */ | ||
| def fromStructure(mw: ast.MagicWandStructure.MagicWandStructure, argValues: Seq[String], | ||
| perm: Option[Rational], het: HeapEntryType, program: ast.Program): WandResolvedEntry = { | ||
| val structure = mw.structure(program, true) | ||
| val holes = structure.subexpressionsToEvaluate(program) | ||
| val argExps: Seq[ast.Exp] = holes.zip(argValues).map { case (hole, v) => CounterexampleValue.literal(v, Some(hole.typ)) } | ||
| val repl: scala.collection.immutable.Map[ast.Node, ast.Node] = | ||
| scala.collection.immutable.Map.from(holes.zip(argExps): Iterable[(ast.Node, ast.Node)]) | ||
| val transformed = structure.replace(repl) | ||
| WandResolvedEntry(transformed.left, transformed.right, perm, het) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A local (store) variable together with its value. | ||
| * `value` is an AST expression (a literal such as [[ast.IntLit]]/[[ast.RefLit]], or a collection | ||
| * expression such as [[ast.ExplicitSeq]]). | ||
| */ | ||
| case class CEVariable(name: String, value: ast.Exp, typ: Option[Type]) { | ||
| override lazy val toString = s"Variable Name: ${name}, Value: ${value.toString}, Type: ${typ.getOrElse("None").toString}" | ||
| } | ||
|
|
||
| /** | ||
| * A collection value (sequence, set or multiset) reconstructed from the model, together with its | ||
| * backend-internal identifier `id`. `value` is the corresponding AST collection expression | ||
| * (e.g. [[ast.ExplicitSeq]]); `id` is used to link store variables and heap values to the | ||
| * collection they refer to. | ||
| */ | ||
| case class CECollection(id: String, value: ast.Exp) { | ||
| override lazy val toString = s"${id} = ${value.toString}" | ||
| } | ||
|
|
||
| case class RawHeap(rawHeapEntries: Set[RawHeapEntry]) { | ||
| override lazy val toString = rawHeapEntries.map(x => x.toString).mkString("", "\n", "") | ||
| } | ||
|
|
||
| case class RawHeapEntry(reference: Seq[String], field: Seq[String], valueID: String, perm: Option[Rational], het: HeapEntryType, insidePredicate: Option[scala.collection.immutable.Map[Exp, ModelEntry]]) { | ||
| override lazy val toString = { | ||
| het match { | ||
| case PredicateType => | ||
| s"Heap entry: ${reference.mkString("(", ", ", ")")} + ${field.mkString("(", ", ", ")")} --> (Permission: ${perm.getOrElse("None")}) ${if (insidePredicate.isDefined && !insidePredicate.get.isEmpty) insidePredicate.get.toSeq.map(x => s"${x._1} --> ${x._2}").mkString("{\n ", "\n ", "\n}") else ""}" | ||
| case _ => s"Heap entry: ${reference.mkString("(", ", ", ")")} + ${field.mkString("(", ", ", ")")} --> (Value: $valueID, Permission: ${perm.getOrElse("None")})" | ||
| } | ||
| } | ||
| } | ||
|
|
||
| case class BasicDomainEntry(name: String, types: Seq[ast.Type], functions: Seq[BasicFunctionEntry]) { | ||
| override def toString: String = s"domain $valueName{\n ${functions.map(_.toString()).mkString("\n")}\n}" | ||
| val valueName: String = s"$name${printTypes()}" | ||
| private def printTypes(): String = | ||
| if (types.isEmpty) "" | ||
| else types.map(printType).mkString("[", ", ", "]") | ||
| private def printType(t: ast.Type): String = t match { | ||
| case ast.TypeVar(x) => x | ||
| case _ => t.toString() | ||
| } | ||
| } | ||
|
|
||
|
|
||
| case class BasicFunctionEntry(fname: String, argtypes: Seq[ast.Type], returnType: ast.Type, options: Map[Seq[String], String], default: String) { | ||
| override def toString: String = { | ||
| if (options.nonEmpty) | ||
| s"$fname${argtypes.mkString("(", ",", ")")}:${returnType}{\n" + options.map(o => " " + o._1.mkString(" ") + " -> " + o._2).mkString("\n") + "\n else -> " + default + "\n}" | ||
| else | ||
| s"$fname{\n " + default + "\n}" | ||
| } | ||
| } | ||
|
|
||
| sealed trait HeapEntryType | ||
| case object FieldType extends HeapEntryType | ||
| case object PredicateType extends HeapEntryType | ||
| case object QPFieldType extends HeapEntryType | ||
| case object QPPredicateType extends HeapEntryType | ||
| case object MagicWandType extends HeapEntryType | ||
| case object QPMagicWandType extends HeapEntryType | ||
|
|
||
| /* | ||
| Helper class for permissions | ||
| */ | ||
|
|
||
| final class Rational(n: BigInt, d: BigInt) extends Ordered[Rational] { | ||
| require(d != 0, "Denominator of Rational must not be 0.") | ||
|
|
||
| private val g = n.gcd(d) | ||
| val numerator: BigInt = n / g * d.signum | ||
| val denominator: BigInt = d.abs / g | ||
|
|
||
|
marcoeilers marked this conversation as resolved.
|
||
| def +(that: Rational): Rational = { | ||
| val newNum = this.numerator * that.denominator + that.numerator * this.denominator | ||
| val newDen = this.denominator * that.denominator | ||
| Rational(newNum, newDen) | ||
| } | ||
| def -(that: Rational): Rational = this + (-that) | ||
| def unary_- = Rational(-numerator, denominator) | ||
| def abs = Rational(numerator.abs, denominator) | ||
| def signum = Rational(numerator.signum, 1) | ||
|
|
||
| def *(that: Rational): Rational = Rational(this.numerator * that.numerator, this.denominator * that.denominator) | ||
| def /(that: Rational): Rational = this * that.inverse | ||
| def inverse = Rational(denominator, numerator) | ||
|
|
||
| def compare(that: Rational) = (this.numerator * that.denominator - that.numerator * this.denominator).signum | ||
|
|
||
| override def equals(obj: Any) = obj match { | ||
| case that: Rational => this.numerator == that.numerator && this.denominator == that.denominator | ||
| case _ => false | ||
| } | ||
|
|
||
| override def hashCode(): Int = viper.silver.utility.Common.generateHashCode(n, d) | ||
|
|
||
| override lazy val toString = s"$numerator/$denominator" | ||
| } | ||
|
|
||
| object Rational extends ((BigInt, BigInt) => Rational) { | ||
| val zero = Rational(0, 1) | ||
| val one = Rational(1, 1) | ||
|
|
||
| def apply(numer: BigInt, denom: BigInt) = new Rational(numer, denom) | ||
|
|
||
| def unapply(r: Rational) = Some(r.numerator, r.denominator) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.