Skip to content
Merged
Show file tree
Hide file tree
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 Jun 17, 2024
33bf34f
Merge branch 'master' of https://github.com/viperproject/silver
marcoeilers Jul 1, 2024
2dd7e13
General counterexample definition by Raoul
marcoeilers Jul 1, 2024
1528fbe
General counterexample definition by Raoul
marcoeilers Jul 1, 2024
c235125
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers Feb 10, 2025
028ab03
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers Aug 28, 2025
469ec66
Added a convenience method
marcoeilers Aug 29, 2025
d15460f
Moving test files from Silicon
marcoeilers Aug 29, 2025
f6c7163
Defining common interface for Silicon/Carbon CEs
marcoeilers Aug 29, 2025
062f25c
Moving CE test infrastructure to silver
marcoeilers Sep 1, 2025
d7d7e95
Adding quantified field example
marcoeilers Sep 1, 2025
5e1fda9
Predicates in test annotations, fixed an output message
marcoeilers Sep 1, 2025
20e43cf
Renamed CE variations, using AST expressions instead of new CEValue type
marcoeilers Jul 13, 2026
2dbce50
Adding test case
marcoeilers Jul 13, 2026
59fa417
Better model for wand entries
marcoeilers Jul 13, 2026
91b90ac
Renaming
marcoeilers Jul 13, 2026
fbd5149
Better test that forces QP information in the SMT model
marcoeilers Jul 13, 2026
6e787ac
Improved tests
marcoeilers Jul 14, 2026
5bb7240
More tests
marcoeilers Jul 15, 2026
f2f1796
More tests
marcoeilers Jul 15, 2026
d6d3a54
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers Jul 16, 2026
fff1b55
Cleanup
marcoeilers Jul 23, 2026
dcefc25
Real value parsing
marcoeilers Jul 23, 2026
c5a026d
Cleaned up wand entries
marcoeilers Jul 25, 2026
cb832f6
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers Jul 25, 2026
91c8585
Changes after code review
marcoeilers Aug 13, 2026
4364429
Merge branch 'master' into meilers_raoul_counterexamples
marcoeilers Aug 13, 2026
c39e78c
Restoring mapped counterexample test
marcoeilers Aug 13, 2026
6d25d67
Proper tests for wands, fields inside predicates
marcoeilers Aug 14, 2026
cfe5000
Clarified documentation to say which CE models are preferred, which a…
marcoeilers Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/main/scala/viper/silver/ast/Expression.scala
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ case class MagicWand(left: Exp, right: Exp)(val pos: Position = NoPosition, val
collectedExpressions
}

def structure(p: Program): MagicWandStructure = {
def structure(p: Program, uniqueNames: Boolean = false): MagicWandStructure = {
/* High-level idea: take the input wand (`this`) and perform a sequence of
* substitutions that transform the wand into a canonical form suitable for
* checking whether or not a given state provides a particular wand.
Expand Down Expand Up @@ -208,10 +208,12 @@ case class MagicWand(left: Exp, right: Exp)(val pos: Position = NoPosition, val
decl.copy(name(decl.typ, bindings(decl.name)))(decl.pos, decl.info, decl.errT))
}

var uniqueNameIndex = -1
val structure = StrategyBuilder.Context[Node, Bindings](
{
case (exp: Exp, c) if subexpressionsToEvaluate.contains(exp) =>
(LocalVar(exp.typ.toString(),exp.typ)(), c)
val varName = exp.typ.toString() + (if (uniqueNames) s"${uniqueNameIndex += 1; uniqueNameIndex}" else "")
(LocalVar(varName, exp.typ)(), c)

case (quant: QuantifiedExp, context) =>
/* NOTE: This case, i.e. the transformation case, is reached before the
Expand Down Expand Up @@ -271,6 +273,22 @@ case class NullLit()(val pos: Position = NoPosition, val info: Info = NoInfo, va
lazy val typ = Ref
}

/**
* A reference literal denoting a concrete reference value. `name` is the backend-internal
* identifier of the reference (e.g. "$Ref!val!1"). This node exists only for representing
* counterexamples; it is not part of parseable Viper and must not appear in a program AST.
*/
case class RefLit(name: String)(val pos: Position = NoPosition, val info: Info = NoInfo, val errT: ErrorTrafo = NoTrafos) extends Literal {
lazy val typ = Ref
}

/**
* A literal for a value that has no representable Viper literal (e.g. an uninterpreted domain
* element or an otherwise opaque backend value). `value` is the backend-internal string
* representation. Like [[RefLit]], this node exists only for representing counterexamples.
*/
case class BackendValueLit(value: String, typ: Type)(val pos: Position = NoPosition, val info: Info = NoInfo, val errT: ErrorTrafo = NoTrafos) extends Literal

// --- Accessibility predicates

/** A common trait for accessibility predicates. */
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/viper/silver/ast/pretty/PrettyPrinter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,8 @@ object FastPrettyPrinter extends FastPrettyPrinterBase with BracketPrettyPrinter
case IntLit(i) => value(i)
case BoolLit(b) => value(b)
case NullLit() => value(null)
case RefLit(name) => text(name)
case BackendValueLit(v, _) => text(v)
case AbstractLocalVar(n) => n
case FieldAccess(rcv, field) =>
show(rcv) <> "." <> field.name
Expand Down
15 changes: 11 additions & 4 deletions src/main/scala/viper/silver/frontend/SilFrontEndConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,21 @@ abstract class SilFrontendConfig(args: Seq[String], private var projectName: Str
)

val counterexample = opt[CounterexampleModel]("counterexample",
descr="Return counterexample for errors. Pass 'native' for returning the native model from the backend, " +
"'variables' for returning a model of all local Viper variables, or 'mapped' (only available on Silicon) " +
"for returning a model with Ref variables resolved to object-like structures.",
descr="Return counterexample for errors. Pass 'resolved' for the human-readable backend-independent " +
"counterexample (heap resources bound to their AST nodes), or 'raw' for the backend-independent " +
"counterexample with heap resources keyed by backend-internal identifiers. The following are legacy " +
"formats: 'native' for returning the native model from the backend, 'variables' for returning a model " +
"of all local Viper variables, and 'mapped' (only available on Silicon) for returning a model with Ref " +
"variables resolved to object-like structures.",
default = None,
noshort = true,
)(singleArgConverter({
case "native" => NativeModel
case "variables" => VariablesModel
case "mapped" => MappedModel
case i => throw new IllegalArgumentException(s"Unsupported counterexample model provided. Expected 'native', 'variables' or 'mapped' but got $i")
case "resolved" => ResolvedModel
case "raw" => RawModel
case i => throw new IllegalArgumentException(s"Unsupported counterexample model provided. Expected 'resolved', 'raw', 'native', 'variables' or 'mapped' but got $i")
}))

val disableTerminationPlugin = opt[Boolean]("disableTerminationPlugin",
Expand Down Expand Up @@ -211,3 +216,5 @@ trait CounterexampleModel
case object NativeModel extends CounterexampleModel
case object VariablesModel extends CounterexampleModel
case object MappedModel extends CounterexampleModel
case object RawModel extends CounterexampleModel
case object ResolvedModel extends CounterexampleModel
6 changes: 6 additions & 0 deletions src/main/scala/viper/silver/testing/BackendTypeTest.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// 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.testing

import org.scalatest.funsuite.AnyFunSuite
Expand Down
294 changes: 294 additions & 0 deletions src/main/scala/viper/silver/verifier/Counterexample.scala
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
Comment thread
marcoeilers marked this conversation as resolved.
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

Comment thread
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)
}
Loading
Loading