diff --git a/silver b/silver index 42fa22f0c..896b6d465 160000 --- a/silver +++ b/silver @@ -1 +1 @@ -Subproject commit 42fa22f0c31923b0f304039805328288c45979d3 +Subproject commit 896b6d46554dd87e6d4f59a455f40275efebe250 diff --git a/src/main/scala/Config.scala b/src/main/scala/Config.scala index 33250b52d..4983621df 100644 --- a/src/main/scala/Config.scala +++ b/src/main/scala/Config.scala @@ -6,16 +6,17 @@ package viper.silicon -import java.nio.file.{Files, Path, Paths} -import scala.collection.immutable.ArraySeq -import scala.util.matching.Regex -import scala.util.Properties._ import org.rogach.scallop._ import viper.silicon.Config.JoinMode.JoinMode import viper.silicon.Config.StateConsolidationMode.StateConsolidationMode import viper.silicon.decider.{Cvc5ProverStdIO, Z3ProverAPI, Z3ProverStdIO} import viper.silver.frontend.SilFrontendConfig +import java.nio.file.{Files, Path, Paths} +import scala.collection.immutable.ArraySeq +import scala.util.Properties._ +import scala.util.matching.Regex + class Config(args: Seq[String]) extends SilFrontendConfig(args, "Silicon") { import Config._ @@ -691,6 +692,48 @@ class Config(args: Seq[String]) extends SilFrontendConfig(args, "Silicon") { noshort = true ) + val enableUnsatCores: ScallopOption[Boolean] = opt[Boolean]("enableUnsatCores", + descr = "Compute UNSAT cores. Useful for evaluating the overhead of this feature in an isolated manner.", + default = Some(false), + noshort = true + ) + + /* + * For benchmarking and testing purposes one can also use the command `computeGraphOnly` that enables dep analysis + * and computes the final dependency graph but does not run any graph queries. + */ + val dependencyAnalysis: ScallopOption[String] = opt[String]("dependencyAnalysis", + descr = "Enable dependency analysis and set the commands (separated by ;) to be executed after verification. The available commands are `interactive` " + + "and all commands supported by the interactive CLI tool", + default = None, + noshort = true + ) + + val disableDependencyAnalysisJoinPrecisionOpt: ScallopOption[Boolean] = opt[Boolean]("disableDependencyAnalysisJoinPrecisionOpt", + descr = "Use a less precise but faster algorithm for computing dependencies.", + default = Some(false), + noshort = true + ) + + val enableDependencyAnalysisDebugging: ScallopOption[Boolean] = opt[Boolean]("enableDependencyAnalysisDebugging", + descr = "Enable debugging for the verification dependency analysis", + default = Some(false), + noshort = true + ) + + private val disableInfeasiblePaths: ScallopOption[Boolean] = opt[Boolean]("disableInfeasiblePaths", + descr = "This flag makes the dependency analysis faster by skipping the analysis of dead paths, but it may produce unsound results " + + "where the dependencies of a proof obligation in an infeasible branch are not completely listed", + default = Some(false), + noshort = true + ) + + /** + * If true, verification always steps through all paths; even if provably infeasible. Might have a huge performance penalty! + * Note: analyzeInfeasiblePaths might be used independent of the dependency analysis but this has not been tested yet. + */ + lazy val analyzeInfeasiblePaths: Boolean = dependencyAnalysis.isDefined && !disableInfeasiblePaths() + /* Option validation (trailing file argument is validated by parent class) */ validateOpt(prover) { @@ -738,6 +781,31 @@ class Config(args: Seq[String]) extends SilFrontendConfig(args, "Silicon") { validateFileOpt(multisetAxiomatizationFile) validateFileOpt(sequenceAxiomatizationFile) + validateOpt(dependencyAnalysis, parallelizeBranches) { + case (dam, Some(true)) if dam.isDefined => + Left(s"Option ${dependencyAnalysis.name} is not supported in combination with ${parallelizeBranches.name}") + case (_, Some(false)) => Right(()) + } + + validateOpt(rawProverArgs, dependencyAnalysis) { + case (_, dam) if dam.isEmpty => Right(()) + case (Some(args), dam) if dam.isDefined && args.toLowerCase.contains("proof=true") && args.toLowerCase.contains("unsat-core=true") => Right(()) + case (_, _) => + Left(s"Option ${dependencyAnalysis.name} requires ${rawProverArgs.name} with \"proof=true unsat-core=true\"") + } + + validateOpt(prover, dependencyAnalysis) { + case (p, dam) if !p.contains(Z3ProverStdIO.name) && dam.isDefined => + Left(s"Dependency analysis is only supported with ${Z3ProverStdIO.name}") + case _ => Right(()) + } + + validateOpt(disableInfeasiblePaths, dependencyAnalysis) { + case (Some(true), dam) if dam.isEmpty => + Left(s"The ${disableInfeasiblePaths.name} is currently only supported in combination with ${dependencyAnalysis.name}") + case _ => Right(()) + } + /* Finalise configuration */ verify() diff --git a/src/main/scala/Silicon.scala b/src/main/scala/Silicon.scala index b9285840a..122f3602c 100644 --- a/src/main/scala/Silicon.scala +++ b/src/main/scala/Silicon.scala @@ -6,27 +6,26 @@ package viper.silicon -import java.text.SimpleDateFormat -import java.util.concurrent.{Callable, Executors, TimeUnit, TimeoutException} -import scala.collection.immutable.ArraySeq -import scala.util.{Left, Right} import ch.qos.logback.classic.{Level, Logger} import com.typesafe.scalalogging.LazyLogging import org.slf4j.LoggerFactory -import viper.silver.ast -import viper.silver.frontend.{DefaultStates, MinimalViperFrontendAPI, SilFrontend, ViperFrontendAPI} -import viper.silver.reporter._ -import viper.silver.verifier.{AbstractVerificationError => SilAbstractVerificationError, Failure => SilFailure, Success => SilSuccess, TimeoutOccurred => SilTimeoutOccurred, VerificationResult => SilVerificationResult, Verifier => SilVerifier} +import viper.silicon.decider.{Cvc5ProverStdIO, Z3ProverStdIO} import viper.silicon.interfaces.Failure import viper.silicon.logger.{MemberSymbExLogger, SymbExLogger} import viper.silicon.reporting.{MultiRunRecorders, condenseToViperResult} -import viper.silicon.verifier.DefaultMainVerifier -import viper.silicon.decider.{Cvc5ProverStdIO, Z3ProverStdIO} +import viper.silicon.verifier.{DefaultMainVerifier, DependencyAnalysisAwareMainVerifier} +import viper.silver.ast import viper.silver.cfg.silver.SilverCfg +import viper.silver.frontend.{DefaultStates, MinimalViperFrontendAPI, SilFrontend, ViperFrontendAPI} import viper.silver.logger.ViperStdOutLogger -import viper.silver.utility.{FileProgramSubmitter} +import viper.silver.reporter._ +import viper.silver.utility.FileProgramSubmitter +import viper.silver.verifier.{AbstractVerificationError => SilAbstractVerificationError, Failure => SilFailure, Success => SilSuccess, TimeoutOccurred => SilTimeoutOccurred, VerificationResult => SilVerificationResult, Verifier => SilVerifier} -import scala.util.chaining._ +import java.text.SimpleDateFormat +import java.util.concurrent.{Callable, Executors, TimeUnit, TimeoutException} +import scala.collection.immutable.ArraySeq +import scala.util.{Left, Right} object Silicon { val name = BuildInfo.projectName @@ -136,7 +135,7 @@ class Silicon(val reporter: Reporter, private var debugInfo: Seq[(String, Any)] setLogLevelsFromConfig() - verifier = new DefaultMainVerifier(config, reporter, symbExLog) + verifier = if (config.dependencyAnalysis.isDefined) new DependencyAnalysisAwareMainVerifier(config, reporter, symbExLog) else new DefaultMainVerifier(config, reporter, symbExLog) verifier.start() } diff --git a/src/main/scala/debugger/DebugParser.scala b/src/main/scala/debugger/DebugParser.scala index 3d2a792e2..9db8a9c7f 100644 --- a/src/main/scala/debugger/DebugParser.scala +++ b/src/main/scala/debugger/DebugParser.scala @@ -2,6 +2,7 @@ package viper.silicon.debugger import fastparse._ import viper.silver.ast._ +import viper.silver.dependencyAnalysis.DependencyType import viper.silver.parser._ import scala.collection.mutable @@ -36,7 +37,7 @@ class DebugParser extends FastParser { class DebugTranslator(p: PProgram, override val members: mutable.Map[String, Node]) extends Translator(p) { - override protected def expInternal(pexp: PExp, pos: PExp, info: Info): Exp = { + override protected def expInternal(pexp: PExp, pos: PExp, info: Info, dependencyType: Option[DependencyType]): Exp = { pexp match { case pviu@PVersionedIdnUseExp(_, _, _) => pexp.typ match { @@ -45,7 +46,7 @@ class DebugTranslator(p: PProgram, override val members: mutable.Map[String, Nod } case PDebugLabelledOldExp(_, lbl, e) => DebugLabelledOld(exp(e), lbl.versionedName)(pos, info) - case _ => super.expInternal(pexp, pos, info) + case _ => super.expInternal(pexp, pos, info, dependencyType) } } diff --git a/src/main/scala/debugger/SiliconDebugger.scala b/src/main/scala/debugger/SiliconDebugger.scala index 1efe421b2..64f6593d2 100644 --- a/src/main/scala/debugger/SiliconDebugger.scala +++ b/src/main/scala/debugger/SiliconDebugger.scala @@ -2,13 +2,13 @@ package viper.silicon.debugger import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.decider.{Cvc5ProverStdIO, RecordedPathConditions, Z3ProverStdIO} -import viper.silicon.interfaces.state.Chunk +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.{Failure, SiliconDebuggingFailureContext, Success, VerificationResult} import viper.silicon.resources.{FieldID, PredicateID} import viper.silicon.rules.evaluator -import viper.silicon.state.Heap +import viper.silicon.state.chunks._ import viper.silicon.state.terms.{Term, True} -import viper.silicon.state.{BasicChunk, IdentifierFactory, MagicWandChunk, QuantifiedFieldChunk, QuantifiedMagicWandChunk, QuantifiedPredicateChunk, State} +import viper.silicon.state.{Heap, IdentifierFactory, State} import viper.silicon.utils.ast.simplifyVariableName import viper.silicon.verifier.{MainVerifier, Verifier, WorkerVerifier} import viper.silver.ast @@ -430,7 +430,7 @@ class SiliconDebugger(verificationResults: List[VerificationResult], var resE: ast.Exp = null var resV: Verifier = null val pve: PartialVerificationError = PartialVerificationError(r => ContractNotWellformed(assertionE, r)) - val verificationResult = evaluator.eval3(obl.s, assertionE, pve, obl.v)((_, t, newE, newV) => { + val verificationResult = evaluator.eval3(obl.s, assertionE, pve, obl.v, DependencyAnalysisInfos.DefaultInfos.withEnabled(false))((_, t, newE, newV) => { resT = t resE = newE.get resV = newV @@ -497,7 +497,7 @@ class SiliconDebugger(verificationResults: List[VerificationResult], var evalPcs: RecordedPathConditions = null val pve: PartialVerificationError = PartialVerificationError(r => ContractNotWellformed(e, r)) val beforeEval = v.decider.setPathConditionMark() - val verificationResult = evaluator.eval3(obl.s, e, pve, v)((newS, t, newE, newV) => { + val verificationResult = evaluator.eval3(obl.s, e, pve, v, v.decider.defaultAnalysisInfos)((newS, t, newE, newV) => { resS = newS resT = t resE = newE.get diff --git a/src/main/scala/decider/CVC5ProverStdIO.scala b/src/main/scala/decider/CVC5ProverStdIO.scala index f89478c9e..740e899f2 100644 --- a/src/main/scala/decider/CVC5ProverStdIO.scala +++ b/src/main/scala/decider/CVC5ProverStdIO.scala @@ -6,12 +6,13 @@ package viper.silicon.decider -import java.nio.file.{Path, Paths} +import viper.silicon.common.config.Version import viper.silicon.state.IdentifierFactory import viper.silicon.verifier.Verifier -import viper.silver.verifier.{DefaultDependency => SilDefaultDependency} import viper.silver.reporter.Reporter -import viper.silicon.common.config.Version +import viper.silver.verifier.{DefaultDependency => SilDefaultDependency} + +import java.nio.file.{Path, Paths} object Cvc5ProverStdIO { val name = "cvc5" diff --git a/src/main/scala/decider/Decider.scala b/src/main/scala/decider/Decider.scala index fbf93b066..56ccc72d3 100644 --- a/src/main/scala/decider/Decider.scala +++ b/src/main/scala/decider/Decider.scala @@ -7,9 +7,10 @@ package viper.silicon.decider import com.typesafe.scalalogging.Logger -import viper.silicon.debugger.DebugExp import viper.silicon._ import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis._ import viper.silicon.interfaces._ import viper.silicon.interfaces.decider._ import viper.silicon.logger.records.data.{DeciderAssertRecord, DeciderAssumeRecord, ProverAssertRecord} @@ -20,13 +21,14 @@ import viper.silicon.verifier.{Verifier, VerifierComponent} import viper.silver.ast import viper.silver.ast.{LocalVarWithVersion, NoPosition} import viper.silver.components.StatefulComponent +import viper.silver.dependencyAnalysis._ import viper.silver.parser.{PKw, PPrimitiv, PReserved, PType} import viper.silver.reporter.{ConfigurationConfirmation, InternalWarningMessage} import viper.silver.verifier.{DependencyNotFoundError, Model} import scala.collection.immutable.HashSet -import scala.reflect.{ClassTag, classTag} import scala.collection.mutable +import scala.reflect.{ClassTag, classTag} /* * Interfaces @@ -49,31 +51,35 @@ trait Decider { def pushScope(): Unit def popScope(): Unit - def checkSmoke(isAssert: Boolean = false): Boolean + def checkSmoke(analysisInfos: DependencyAnalysisInfos, isAssert: Boolean = false): Boolean - def setCurrentBranchCondition(t: Term, te: (ast.Exp, Option[ast.Exp])): Unit + def setCurrentBranchCondition(t: Term, te: (ast.Exp, Option[ast.Exp]), analysisInfos: DependencyAnalysisInfos): Unit def setPathConditionMark(): Mark def finishDebugSubExp(description : String): Unit def startDebugSubExp(): Unit - def assume(t: Term, e: ast.Exp, finalExp: ast.Exp): Unit - def assume(t: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp]): Unit - def assume(t: Term, debugExp: Option[DebugExp]): Unit - def assume(terms: Seq[Term], debugExps: Option[Seq[DebugExp]]): Unit - def assumeDefinition(t: Term, debugExp: Option[DebugExp]): Unit - def assume(assumptions: Iterable[(Term, Option[DebugExp])]): Unit - def assume(assumptions: InsertionOrderedSet[(Term, Option[DebugExp])], enforceAssumption: Boolean = false, isDefinition: Boolean = false): Unit - def assume(terms: Iterable[Term], debugExp: Option[DebugExp], enforceAssumption: Boolean): Unit + def debuggerAssume(terms: Iterable[Term], de: DebugExp) - def check(t: Term, timeout: Int): Boolean + def isPathMarkedInfeasible: Boolean + def handleInfeasiblePath(hasAssertions: Boolean, hasAssumptions: Boolean, analysisInfos: DependencyAnalysisInfos): Unit + + def assume(t: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): Unit + def assume(t: Term, debugExp: Option[DebugExp], analysisInfos: DependencyAnalysisInfos): Unit + def assume(assumptions: Iterable[Term], debugExps: Option[Iterable[DebugExp]], description: String, enforceAssumption: Boolean, analysisInfos: DependencyAnalysisInfos): Unit + def assume(terms: Seq[Term], debugExps: Option[Seq[DebugExp]], analysisInfos: DependencyAnalysisInfos): Unit + def assumeDefinition(t: Term, debugExp: Option[DebugExp], analysisInfos: DependencyAnalysisInfos): Unit + def assume(terms: Iterable[Term], debugExp: Option[DebugExp], enforceAssumption: Boolean, analysisInfos: DependencyAnalysisInfos): Unit + + def check(t: Term, timeout: Int, analysisInfos: DependencyAnalysisInfos): Boolean /* TODO: Consider changing assert such that * 1. It passes State and Operations to the continuation * 2. The implementation reacts to a failing assertion by e.g. a state consolidation */ - def assert(t: Term, timeout: Option[Int] = None)(Q: Boolean => VerificationResult): VerificationResult + def assert(t: Term, analysisInfos: DependencyAnalysisInfos)(Q: Boolean => VerificationResult): VerificationResult + def assert(t: Term, analysisInfos: DependencyAnalysisInfos, timeout: Option[Int])(Q: Boolean => VerificationResult): VerificationResult def fresh(id: String, sort: Sort, ptype: Option[PType]): Var def fresh(id: String, argSorts: Seq[Sort], resultSort: Sort): Function @@ -101,6 +107,10 @@ trait Decider { def setPcs(other: PathConditionStack): Unit def statistics(): Map[String, String] + + def handleFailedAssertion(failedAssertion: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, assumeFailedAssertion: Boolean): Unit + + def defaultAnalysisInfos: DependencyAnalysisInfos } /* @@ -118,9 +128,13 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => */ def identifierFactory: IdentifierFactory - object decider extends Decider with StatefulComponent { - private var _prover: Prover = _ - private var pathConditions: PathConditionStack = _ + def decider: AbstractDecider = DefaultDecider + + object DefaultDecider extends AbstractDecider + + trait AbstractDecider extends Decider with StatefulComponent { + protected var _prover: Prover = _ + protected var pathConditions: PathConditionStack = _ private var _declaredFreshFunctions: Set[FunctionDecl] = _ /* [BRANCH-PARALLELISATION] */ private var _declaredFreshMacros: Vector[MacroDecl] = _ @@ -129,7 +143,7 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => private var _proverOptions: Map[String, String] = Map.empty private var _proverResetOptions: Map[String, String] = Map.empty private val _debuggerAssumedTerms: mutable.Set[Term] = mutable.Set.empty - + def functionDecls: Set[FunctionDecl] = _declaredFreshFunctions def macroDecls: Vector[MacroDecl] = _declaredFreshMacros @@ -139,6 +153,8 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => var debugVariableTypes : Map[String, PType] = Map.empty + override def defaultAnalysisInfos: DependencyAnalysisInfos = DependencyAnalysisInfos.DefaultInfos.withEnabled(false) + def setPcs(other: PathConditionStack) = { /* [BRANCH-PARALLELISATION] */ pathConditions = other @@ -153,7 +169,7 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => }) } - private def getProver(prover: String): Prover = prover match { + protected def getProver(prover: String): Prover = prover match { case Z3ProverStdIO.name => new Z3ProverStdIO(uniqueId, termConverter, identifierFactory, reporter) case Cvc5ProverStdIO.name => new Cvc5ProverStdIO(uniqueId, termConverter, identifierFactory, reporter) case Z3ProverAPI.name => new Z3ProverAPI(uniqueId, new TermToZ3APIConverter(), identifierFactory, reporter, triggerGenerator) @@ -163,8 +179,12 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => getProver(Z3ProverStdIO.name) } - def createProver(proverName: String, userArgsString: Option[String]): Option[DependencyNotFoundError] = { + protected def initProver(proverName: String): Unit = { _prover = getProver(proverName) + } + + def createProver(proverName: String, userArgsString: Option[String]): Option[DependencyNotFoundError] = { + initProver(proverName) _prover.start(userArgsString) /* Cannot query prover version otherwise */ @@ -249,9 +269,10 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => //symbExLog.closeScope(sepIdentifier) } - def setCurrentBranchCondition(t: Term, te: (ast.Exp, Option[ast.Exp])): Unit = { + def setCurrentBranchCondition(t: Term, te: (ast.Exp, Option[ast.Exp]), analysisInfos: DependencyAnalysisInfos): Unit = + if (!isPathMarkedInfeasible) { pathConditions.setCurrentBranchCondition(t, te) - assume(t, Option.when(te._2.isDefined)(te._1), te._2) + assume(t, Option.when(te._2.isDefined)(te._1), te._2, analysisInfos) } def setPathConditionMark(): Mark = pathConditions.mark() @@ -270,36 +291,33 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => } } + def isPathMarkedInfeasible: Boolean = Verifier.config.analyzeInfeasiblePaths && pcs.isPathMarkedInfeasible + + def handleInfeasiblePath(hasAssertions: Boolean, hasAssumptions: Boolean, analysisInfos: DependencyAnalysisInfos): Unit = {} + def addDebugExp(e: DebugExp): Unit = { if (debugMode) { pathConditions.addDebugExp(e) } } - def assume(t: Term, e : ast.Exp, finalExp : ast.Exp): Unit = { - assume(assumptions=InsertionOrderedSet((t, Some(DebugExp.createInstance(e, finalExp)))), false, false) - } - - def assume(t: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp]): Unit = { + def assume(t: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): Unit = { if (finalExp.isDefined) { - assume(assumptions=InsertionOrderedSet((t, Some(DebugExp.createInstance(e.get, finalExp.get)))), false, false) + assume(assumptions=InsertionOrderedSet((t, Some(DebugExp.createInstance(e.get, finalExp.get)))), enforceAssumption = false, isDefinition = false, analysisInfos) } else { - assume(assumptions=InsertionOrderedSet((t, None)), false, false) + assume(assumptions=InsertionOrderedSet((t, None)), enforceAssumption = false, isDefinition = false, analysisInfos) } } - def assume(t: Term, debugExp: Option[DebugExp]): Unit = { - assume(InsertionOrderedSet(Seq((t, debugExp))), false) + def assume(t: Term, debugExp: Option[DebugExp], analysisInfos: DependencyAnalysisInfos): Unit = { + assume(InsertionOrderedSet(Seq((t, debugExp))), enforceAssumption = false, isDefinition = false, analysisInfos) } - def assumeDefinition(t: Term, debugExp: Option[DebugExp]): Unit = { - assume(InsertionOrderedSet(Seq((t, debugExp))), enforceAssumption=false, isDefinition=true) + def assumeDefinition(t: Term, debugExp: Option[DebugExp], analysisInfos: DependencyAnalysisInfos): Unit = { + assume(InsertionOrderedSet(Seq((t, debugExp))), enforceAssumption=false, isDefinition=true, analysisInfos) } - def assume(assumptions: Iterable[(Term, Option[DebugExp])]): Unit = - assume(InsertionOrderedSet(assumptions), false) - - def assume(assumptions: InsertionOrderedSet[(Term, Option[DebugExp])], enforceAssumption: Boolean = false, isDefinition: Boolean = false): Unit = { + def assume(assumptions: InsertionOrderedSet[(Term, Option[DebugExp])], enforceAssumption: Boolean, isDefinition: Boolean, analysisInfos: DependencyAnalysisInfos): Unit = { val filteredAssumptions = if (enforceAssumption) assumptions else assumptions filterNot (a => isKnownToBeTrue(a._1)) @@ -309,26 +327,45 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => filteredAssumptions foreach (a => addDebugExp(a._2.get.withTerm(a._1))) } - if (filteredAssumptions.nonEmpty) assumeWithoutSmokeChecks(filteredAssumptions map (_._1), isDefinition=isDefinition) + if (filteredAssumptions.nonEmpty) assumeWithoutSmokeChecks(InsertionOrderedSet(filteredAssumptions.map(t => (t._1, ""))), analysisInfos, isDefinition=isDefinition) } - def assume(assumptions: Seq[Term], debugExps: Option[Seq[DebugExp]]): Unit = { - assumeWithoutSmokeChecks(InsertionOrderedSet(assumptions)) + def assume(assumptions: Seq[Term], debugExps: Option[Seq[DebugExp]], analysisInfos: DependencyAnalysisInfos): Unit = { + assumeWithoutSmokeChecks(InsertionOrderedSet(assumptions.map(t => (t, ""))), analysisInfos) if (debugMode) { debugExps.get foreach (e => addDebugExp(e)) } } - def assume(terms: Iterable[Term], debugExp: Option[DebugExp], enforceAssumption: Boolean): Unit = { + def assume(assumptions: Iterable[Term], debugExps: Option[Iterable[DebugExp]], description: String, enforceAssumption: Boolean, analysisInfos: DependencyAnalysisInfos): Unit = { + val debugExp = Option.when(debugExps.isDefined)(DebugExp.createInstance(description, InsertionOrderedSet(debugExps.get))) + + val filteredTerms = + if (enforceAssumption) assumptions + else assumptions filterNot isKnownToBeTrue + + if (filteredTerms.nonEmpty) { + assumeWithoutSmokeChecks(InsertionOrderedSet(filteredTerms.map(t => (t, ""))), analysisInfos) + + if (debugMode && debugExp.isDefined) { + addDebugExp(debugExp.get) + } + } + } + + + def assume(terms: Iterable[Term], debugExp: Option[DebugExp], enforceAssumption: Boolean, analysisInfos: DependencyAnalysisInfos): Unit = { val filteredTerms = if (enforceAssumption) terms else terms filterNot isKnownToBeTrue - if (debugMode && filteredTerms.nonEmpty) { - addDebugExp(debugExp.get.withTerm(And(filteredTerms))) - } + if (filteredTerms.nonEmpty) { + if (debugMode) { + addDebugExp(debugExp.get.withTerm(And(filteredTerms))) + } - if (filteredTerms.nonEmpty) assumeWithoutSmokeChecks(InsertionOrderedSet(filteredTerms)) + assumeWithoutSmokeChecks(InsertionOrderedSet(filteredTerms.map(t => (t, ""))), analysisInfos) + } } def debuggerAssume(terms: Iterable[Term], de: DebugExp) = { @@ -341,38 +378,56 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => }) } - private def assumeWithoutSmokeChecks(terms: InsertionOrderedSet[Term], isDefinition: Boolean = false) = { - val assumeRecord = new DeciderAssumeRecord(terms) - val sepIdentifier = symbExLog.openScope(assumeRecord) + protected def assumeWithoutSmokeChecks(termsWithLabel: InsertionOrderedSet[(Term, String)], analysisInfos: DependencyAnalysisInfos, isDefinition: Boolean = false): Unit = + if (!isPathMarkedInfeasible) { + val terms = termsWithLabel map (_._1) + val assumeRecord = new DeciderAssumeRecord(terms) + val sepIdentifier = symbExLog.openScope(assumeRecord) - /* Add terms to Silicon-managed path conditions */ - if (isDefinition) { - terms foreach pathConditions.addDefinition - } else { - terms foreach pathConditions.add - } + /* Add terms to Silicon-managed path conditions */ + if (isDefinition) { + terms foreach pathConditions.addDefinition + } else { + terms foreach pathConditions.add + } - /* Add terms to the prover's assumptions */ - terms foreach prover.assume + /* Add terms to the prover's assumptions */ + termsWithLabel foreach { case (t, label) => prover.assume(t, label) } - symbExLog.closeScope(sepIdentifier) - None - } + symbExLog.closeScope(sepIdentifier) + } /* Asserting facts */ - def checkSmoke(isAssert: Boolean = false): Boolean = { + def checkSmoke(analysisInfos: DependencyAnalysisInfos, isAssert: Boolean=false): Boolean = { + checkSmokeInternal(isAssert) + } + + protected def checkSmokeInternal(isAssert: Boolean=false, label: String=""): Boolean = { val timeout = if (isAssert) Verifier.config.assertTimeout.toOption else Verifier.config.checkTimeout.toOption - prover.check(timeout) == Unsat + val result = prover.check(timeout, label) == Unsat + if(result) pcs.markPathInfeasible(true) + result } - def check(t: Term, timeout: Int): Boolean = deciderAssert(t, Some(timeout)) + override def handleFailedAssertion(failedAssertion: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, assumeFailedAssertion: Boolean): Unit = { + if (assumeFailedAssertion) { + assume(failedAssertion, e, finalExp, analysisInfos.withDependencyType(AssumptionType.Explicit)) + failedAssertion match { + case False => checkSmoke(analysisInfos) + case _ => + } + } + } + + def check(t: Term, timeout: Int, analysisInfos: DependencyAnalysisInfos): Boolean = { + deciderAssert(t, analysisInfos, Some(timeout), isCheck=true) + } - def assert(t: Term, timeout: Option[Int] = Verifier.config.assertTimeout.toOption) - (Q: Boolean => VerificationResult) - : VerificationResult = { + def assert(t: Term, analysisInfos: DependencyAnalysisInfos)(Q: Boolean => VerificationResult): VerificationResult = assert(t, analysisInfos, timeout=Verifier.config.assertTimeout.toOption)(Q) - val success = deciderAssert(t, timeout) + def assert(t: Term, analysisInfos: DependencyAnalysisInfos, timeout: Option[Int])(Q: Boolean => VerificationResult): VerificationResult = { + val success = deciderAssert(t, analysisInfos, timeout) // If the SMT query was not successful, store it (possibly "overwriting" // any previously saved query), otherwise discard any query we had saved @@ -386,18 +441,23 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => Q(success) } - private def deciderAssert(t: Term, timeout: Option[Int]) = { + private def deciderAssert(t: Term, analysisInfos: DependencyAnalysisInfos, timeout: Option[Int], isCheck: Boolean=false) = { val assertRecord = new DeciderAssertRecord(t, timeout) val sepIdentifier = symbExLog.openScope(assertRecord) val asserted = isKnownToBeTrue(t) - val result = asserted || proverAssert(t, timeout) + + val result = deciderAssertInternal(asserted, t, timeout, analysisInfos, isCheck) symbExLog.closeScope(sepIdentifier) result } - private def isKnownToBeTrue(t: Term) = t match { + protected def deciderAssertInternal(asserted: Boolean, t: Term, timeout: Option[Int], analysisInfos: DependencyAnalysisInfos, isCheck: Boolean, label: String="") = { + asserted || proverAssert(t, timeout, label) + } + + protected def isKnownToBeTrue(t: Term) = t match { case True => true // case eq: BuiltinEquals => eq.p0 == eq.p1 /* WARNING: Blocking trivial equalities might hinder axiom triggering. */ case _ if pcs.assumptions contains t => true @@ -405,11 +465,11 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => case _ => false } - private def proverAssert(t: Term, timeout: Option[Int]) = { + protected def proverAssert(t: Term, timeout: Option[Int], label: String) = { val assertRecord = new ProverAssertRecord(t, timeout) val sepIdentifier = symbExLog.openScope(assertRecord) - val result = prover.assert(t, timeout) + val result = isPathMarkedInfeasible || prover.assert(t, timeout, label) symbExLog.whenEnabled { assertRecord.statistics = Some(symbExLog.deltaStatistics(prover.statistics())) @@ -527,7 +587,7 @@ trait DefaultDeciderProvider extends VerifierComponent { this: Verifier => def statistics(): Map[String, String] = prover.statistics() - override def generateModel(): Unit = proverAssert(False, Verifier.config.assertTimeout.toOption) + override def generateModel(): Unit = proverAssert(False, Verifier.config.assertTimeout.toOption, "") override def getModel(): Model = prover.getModel() diff --git a/src/main/scala/decider/DependencyAnalysisAwareDeciderProvider.scala b/src/main/scala/decider/DependencyAnalysisAwareDeciderProvider.scala new file mode 100644 index 000000000..15bf8f284 --- /dev/null +++ b/src/main/scala/decider/DependencyAnalysisAwareDeciderProvider.scala @@ -0,0 +1,186 @@ +// 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.silicon.decider + +import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.state.chunks.{Chunk, GeneralChunk} +import viper.silicon.state.terms.{False, Term, True} +import viper.silicon.verifier.{DependencyAnalysisAwareVerifier, Verifier} +import viper.silver.ast +import viper.silver.ast.Member + +trait DependencyAnalysisHandler { + + def isDependencyAnalysisEnabled: Boolean + + def initDependencyAnalyzer(member: Member, preambleNodes: Iterable[DependencyAnalysisNode]): Unit + def getDependencyAnalyzer: DependencyAnalyzer + def resetDependencyAnalyzer(): Unit + + def registerChunk[CH <: GeneralChunk](buildChunk: Term => CH, perm: Term, analysisInfos: DependencyAnalysisInfos, isExhale: Boolean): CH + def registerDerivedChunk[CH <: GeneralChunk](sourceChunks: Set[Chunk], buildChunk: Term => CH, perm: Term, analysisInfos: DependencyAnalysisInfos, isExhale: Boolean, createLabel: Boolean = true): CH + + def getOrCreateAnalysisLabelNode(sourceChunks: Iterable[Chunk] = Set.empty, sourceTerms: Iterable[Term] = Set.empty): Option[LabelNode] +} + +trait DependencyAnalysisAwareDeciderProvider extends DefaultDeciderProvider { v: DependencyAnalysisAwareVerifier => + override def decider: DependencyAnalysisAwareDecider = DADecider + + protected object DADecider extends DependencyAnalysisAwareDecider + + trait DependencyAnalysisAwareDecider extends AbstractDecider with DependencyAnalysisHandler { + + override def isDependencyAnalysisEnabled: Boolean = Verifier.config.dependencyAnalysis.isDefined && !dependencyAnalyzer.isInstanceOf[NoDependencyAnalyzer] + + override def defaultAnalysisInfos: DependencyAnalysisInfos = DependencyAnalysisInfos.DefaultInfos.withEnabled(isDependencyAnalysisEnabled) + + protected var _daProver: DependencyAnalysisAwareZ3ProverStdIO = _ + override def prover: DependencyAnalysisAwareZ3ProverStdIO = _daProver + + override protected def initProver(proverName: String): Unit = { + _daProver = getProver(proverName) + _prover = _daProver + } + + override protected def getProver(prover: String): DependencyAnalysisAwareZ3ProverStdIO = prover match { + case Z3ProverStdIO.name => new DependencyAnalysisAwareZ3ProverStdIO(uniqueId, termConverter, identifierFactory, reporter) + case prover => + val msg1 = s"Prover '$prover' not supported in combination with the dependency analysis. Defaulting to ${Z3ProverStdIO.name}." + logger warn msg1 + getProver(Z3ProverStdIO.name) + } + + private var dependencyAnalyzer: DependencyAnalyzer = new NoDependencyAnalyzer() + override def getDependencyAnalyzer: DependencyAnalyzer = dependencyAnalyzer + + override def initDependencyAnalyzer(member: Member, preambleNodes: Iterable[DependencyAnalysisNode]): Unit = { + val isAnalysisEnabled = DependencyAnalyzer.extractEnableAnalysisFromInfo(member.info).getOrElse(Verifier.config.dependencyAnalysis.isDefined) + if (isAnalysisEnabled) { + dependencyAnalyzer = new DefaultDependencyAnalyzer(Some(member)) + dependencyAnalyzer.addNodes(preambleNodes) + } else { + dependencyAnalyzer = new NoDependencyAnalyzer + } + } + + override def resetDependencyAnalyzer(): Unit = { + dependencyAnalyzer = new NoDependencyAnalyzer + } + + override protected def assumeWithoutSmokeChecks(termsWithLabel: InsertionOrderedSet[(Term, String)], analysisInfos: DependencyAnalysisInfos, isDefinition: Boolean = false): Unit = { + super.assumeWithoutSmokeChecks(addAssumptionLabels(termsWithLabel.map(_._1), analysisInfos), analysisInfos, isDefinition) + } + + def assumeLabel(term: Term, assumptionLabel: String): Unit = { + pathConditions.addAnalysisLabel(term) + prover.assume(term, assumptionLabel) + } + + private def addAssumptionLabels(filteredTerms: Iterable[Term], analysisInfos: DependencyAnalysisInfos) = { + InsertionOrderedSet(filteredTerms map (t => { + val assumptionIds = dependencyAnalyzer.addAssumption(t, analysisInfos) + (t, DependencyAnalyzer.createAssumptionLabel(assumptionIds)) + })) + } + + override protected def isKnownToBeTrue(t: Term): Boolean = t.equals(True) + + override protected def deciderAssertInternal(asserted: Boolean, t: Term, timeout: Option[Int], analysisInfos: DependencyAnalysisInfos, isCheck: Boolean, label: String = ""): Boolean = { + + val assertNode = if (!asserted) dependencyAnalyzer.createAssertOrCheckNode(t, analysisInfos, isCheck) else None + + val label = DependencyAnalyzer.createAssertionLabel(assertNode map (_.id)) + + val result: Boolean = super.deciderAssertInternal(asserted, t, timeout, analysisInfos, isCheck, label) + + if (result) { + assertNode foreach dependencyAnalyzer.addAssertionNode + } + + result + } + + override protected def proverAssert(t: Term, timeout: Option[Mark], label: String): Boolean = { + val result = super.proverAssert(t, timeout, label) + if (isPathMarkedInfeasible) + dependencyAnalyzer.addDependency(pcs.getCurrentInfeasibilityNode, DependencyAnalyzer.getIdFromLabel(label)) + else if (result) + dependencyAnalyzer.processUnsatCoreAndAddDependencies(prover.getLastUnsatCore, label) + result + } + + override def checkSmoke(analysisInfos: DependencyAnalysisInfos, isAssert: Boolean = false): Boolean = { + val checkNode = dependencyAnalyzer.createAssertOrCheckNode(False, analysisInfos, !isAssert) + val label = DependencyAnalyzer.createAssertionLabel(checkNode.map(_.id)) + + if (isPathMarkedInfeasible) { + checkNode foreach dependencyAnalyzer.addAssertionNode + dependencyAnalyzer.addDependency(pcs.getCurrentInfeasibilityNode, checkNode.map(_.id)) + true + } else { + val result = super.checkSmokeInternal(isAssert, label) + + if (result) { + checkNode foreach dependencyAnalyzer.addAssertionNode + dependencyAnalyzer.processUnsatCoreAndAddDependencies(prover.getLastUnsatCore, label) + val infeasibleNodeId = dependencyAnalyzer.addInfeasibilityNode(!isAssert, analysisInfos) + dependencyAnalyzer.addDependency(checkNode.map(_.id), infeasibleNodeId) + pcs.setCurrentInfeasibilityNode(infeasibleNodeId) + } else if (isAssert) { + checkNode foreach (node => dependencyAnalyzer.addAssertionNode(node.getAssertFailedNode)) + } + result + } + } + + override def handleFailedAssertion(failedAssertion: Term, e: Option[ast.Exp], finalExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, assumeFailedAssertion: Boolean): Unit = { + dependencyAnalyzer.addAssertionFailedNode(failedAssertion, analysisInfos) + super.handleFailedAssertion(failedAssertion, e, finalExp, analysisInfos, assumeFailedAssertion) + } + + override def handleInfeasiblePath(hasAssertions: Boolean, hasAssumptions: Boolean, analysisInfos: DependencyAnalysisInfos): Unit = { + if (!isPathMarkedInfeasible) return + super.handleInfeasiblePath(hasAssertions, hasAssumptions, analysisInfos) + if (hasAssertions) { + dependencyAnalyzer.addAssertionWithDepToInfeasNode(pcs.getCurrentInfeasibilityNode, analysisInfos) + } + if (hasAssumptions) { + dependencyAnalyzer.addAssumption(True, analysisInfos) + } + } + + override def registerChunk[CH <: GeneralChunk](buildChunk: Term => CH, perm: Term, analysisInfos: DependencyAnalysisInfos, isExhale: Boolean): CH = { + registerDerivedChunk(Set.empty, buildChunk, perm, analysisInfos, isExhale) + } + + override def registerDerivedChunk[CH <: GeneralChunk](sourceChunks: Set[Chunk], buildChunk: Term => CH, perm: Term, analysisInfos: DependencyAnalysisInfos, isExhale: Boolean, createLabel: Boolean = true): CH = { + if (!isDependencyAnalysisEnabled) + return buildChunk(perm) + + val labelNodeOpt = getOrCreateAnalysisLabelNode() + + if (isExhale) + dependencyAnalyzer.registerExhaleChunk(sourceChunks, buildChunk, perm, labelNodeOpt, analysisInfos) + else { + dependencyAnalyzer.registerInhaleChunk(sourceChunks, buildChunk, perm, labelNodeOpt, analysisInfos) + } + } + + override def getOrCreateAnalysisLabelNode(sourceChunks: Iterable[Chunk] = Set.empty, sourceTerms: Iterable[Term] = Set.empty): Option[LabelNode] = { + if (!isDependencyAnalysisEnabled) + return None + + val (label, _) = fresh(ast.LocalVar(DependencyAnalyzer.analysisLabelName, ast.Bool)()) + val labelNode = dependencyAnalyzer.createLabelNode(label, sourceChunks, sourceTerms) + val smtLabel = DependencyAnalyzer.createAssumptionLabel(labelNode.map(_.id)) + assumeLabel(label, smtLabel) + labelNode + } + } +} diff --git a/src/main/scala/decider/DependencyAnalysisAwareZ3ProverStdIO.scala b/src/main/scala/decider/DependencyAnalysisAwareZ3ProverStdIO.scala new file mode 100644 index 000000000..ff49feecf --- /dev/null +++ b/src/main/scala/decider/DependencyAnalysisAwareZ3ProverStdIO.scala @@ -0,0 +1,41 @@ +// 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.silicon.decider + +import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugAxiom +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.{DefaultDependencyAnalyzer, DependencyAnalysisAxiomInfo, DependencyAnalyzer} +import viper.silicon.interfaces.decider.ProverLike +import viper.silicon.state.IdentifierFactory +import viper.silicon.state.terms.Term +import viper.silver.reporter.Reporter + +class DependencyAnalysisAwareZ3ProverStdIO(uniqueId: String, termConverter: TermToSMTLib2Converter, identifierFactory: IdentifierFactory, reporter: Reporter) + extends Z3ProverStdIO(uniqueId, termConverter, identifierFactory, reporter) with DependencyAnalysisProverHandler + +trait DependencyAnalysisProverHandler extends ProverLike { + protected val preambleDependencyAnalyzer: DependencyAnalyzer = new DefaultDependencyAnalyzer(None) + + def getPreambleAnalysisNodes: Iterable[DependencyAnalysisNode] = preambleDependencyAnalyzer.getNodes + + def assumeAxiomsWithAnalysisInfo(axioms: InsertionOrderedSet[(Term, DependencyAnalysisAxiomInfo)], description: String): Unit = { + if (debugMode) + preambleAssumptions :+= new DebugAxiom(description, axioms.map(_._1)) + + axioms.foreach(axiom => { + val analysisAxiomInfo = axiom._2 + if (analysisAxiomInfo.analysisInfos.analysisEnabled) { + val id = preambleDependencyAnalyzer.addAxiom(axiom._1, analysisAxiomInfo) + assume(axiom._1, DependencyAnalyzer.createAxiomLabel(id)) + } else { + assume(axiom._1, "") + } + }) + } +} + diff --git a/src/main/scala/decider/PathConditions.scala b/src/main/scala/decider/PathConditions.scala index 31f3f7ea4..cb32bd537 100644 --- a/src/main/scala/decider/PathConditions.scala +++ b/src/main/scala/decider/PathConditions.scala @@ -6,9 +6,9 @@ package viper.silicon.decider -import viper.silicon.debugger.DebugExp import viper.silicon.Stack import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp import viper.silicon.state.terms._ import viper.silicon.utils.Counter import viper.silicon.verifier.Verifier @@ -31,6 +31,9 @@ trait RecordedPathConditions { def definingAssumptions: InsertionOrderedSet[Term] def definingAssumptionExps: InsertionOrderedSet[DebugExp] def declarations: InsertionOrderedSet[Decl] + def analysisLabels: InsertionOrderedSet[Term] + def infeasibilityNodeId: Option[Int] + def isPathMarkedInfeasible: Boolean def definitionsOnly: RecordedPathConditions @@ -68,6 +71,11 @@ trait PathConditionStack extends RecordedPathConditions { def popScope(): Unit def mark(): Mark def popUntilMark(mark: Mark): Unit + def isPathMarkedInfeasible: Boolean + def markPathInfeasible(infeasible: Boolean): Unit + def setCurrentInfeasibilityNode(node: Option[Int]): Unit + def getCurrentInfeasibilityNode: Option[Int] + def addAnalysisLabel(assumption: Term): Unit def startDebugSubExp(): Unit def finishDebugSubExp(description : String): Unit @@ -91,6 +99,8 @@ private class PathConditionStackLayer private var _branchCondition: Option[Term] = None private var _branchConditionExp: Option[(ast.Exp, Option[ast.Exp])] = None + private var _isPathMarkedInfeasible: Boolean = false + private var _infeasibilityNodeId: Option[Int] = None private var _globalAssumptions: InsertionOrderedSet[Term] = InsertionOrderedSet.empty private var _nonGlobalAssumptions: InsertionOrderedSet[Term] = InsertionOrderedSet.empty private var _globalAssumptionDebugExps: InsertionOrderedSet[DebugExp] = InsertionOrderedSet.empty @@ -100,6 +110,7 @@ private class PathConditionStackLayer private var _globalDefiningAssumptionDebugExps: InsertionOrderedSet[DebugExp] = InsertionOrderedSet.empty private var _nonGlobalDefiningAssumptionDebugExps: InsertionOrderedSet[DebugExp] = InsertionOrderedSet.empty private var _declarations: InsertionOrderedSet[Decl] = InsertionOrderedSet.empty + private var _analysisLabels: InsertionOrderedSet[Term] = InsertionOrderedSet.empty def branchCondition: Option[Term] = _branchCondition def branchConditionExp: Option[(ast.Exp, Option[ast.Exp])] = _branchConditionExp @@ -112,6 +123,17 @@ private class PathConditionStackLayer def nonGlobalDefiningAssumptionDebugExps: InsertionOrderedSet[DebugExp] = _nonGlobalDefiningAssumptionDebugExps def nonGlobalAssumptionDebugExps: InsertionOrderedSet[DebugExp] = _nonGlobalAssumptionDebugExps ++ debugExpStack.flatten def declarations: InsertionOrderedSet[Decl] = _declarations + def analysisLabels: InsertionOrderedSet[Term] = _analysisLabels + + def isPathMarkedInfeasible: Boolean = _isPathMarkedInfeasible + def markPathInfeasible(infeasible: Boolean): Unit = { + _isPathMarkedInfeasible = infeasible + } + + def infeasibilityNodeId: Option[Int] = _infeasibilityNodeId + def setInfeasibilityNodeId(id: Option[Int]): Unit = { + _infeasibilityNodeId = id + } def assumptions: InsertionOrderedSet[Term] = globalAssumptions ++ nonGlobalAssumptions def assumptionDebugExps: InsertionOrderedSet[DebugExp] = globalAssumptionDebugExps ++ nonGlobalAssumptionDebugExps @@ -130,6 +152,7 @@ private class PathConditionStackLayer result._globalDefiningAssumptionDebugExps = _globalDefiningAssumptionDebugExps result._nonGlobalAssumptionDebugExps = _nonGlobalDefiningAssumptionDebugExps result._nonGlobalDefiningAssumptionDebugExps = _nonGlobalDefiningAssumptionDebugExps + result._analysisLabels = _analysisLabels result } @@ -162,6 +185,15 @@ private class PathConditionStackLayer _nonGlobalAssumptions += assumption } + def addAnalysisLabel(assumption: Term): Unit = { + assert( + !assumption.isInstanceOf[And], + s"Unexpectedly found a conjunction (should have been split): $assumption") + _globalAssumptions += assumption // labels are always global + _globalDefiningAssumptions += assumption + _analysisLabels += assumption + } + def addNonGlobalDebugExp(debugExp : DebugExp): Unit = { _nonGlobalAssumptionDebugExps += debugExp } @@ -257,9 +289,18 @@ private trait LayeredPathConditionStackLike { protected def declarations(layers: Stack[PathConditionStackLayer]): InsertionOrderedSet[Decl] = InsertionOrderedSet(layers.flatMap(_.declarations)) // Note: Performance? + protected def analysisLabels(layers: Stack[PathConditionStackLayer]): InsertionOrderedSet[Term] = + InsertionOrderedSet(layers.flatMap(_.analysisLabels)) + + protected def isPathMarkedInfeasible(layers: Stack[PathConditionStackLayer]): Boolean = layers.exists(_.isPathMarkedInfeasible) + + protected def infeasibilityNodeId(layers: Stack[PathConditionStackLayer]): Option[Int] = + layers.flatMap(_.infeasibilityNodeId).headOption + protected def contains(layers: Stack[PathConditionStackLayer], assumption: Term): Boolean = layers exists (_.contains(assumption)) + protected def conditionalized(layers: Stack[PathConditionStackLayer]): Seq[Term] = { var unconditionalTerms = Vector.empty[Term] var conditionalTerms = Vector.empty[Term] @@ -274,8 +315,7 @@ private trait LayeredPathConditionStackLike { case None => } - conditionalTerms :+= - Implies(implicationLHS, And(layer.nonGlobalAssumptions)) + conditionalTerms :+= Implies(implicationLHS, And(layer.nonGlobalAssumptions)) } unconditionalTerms ++ conditionalTerms @@ -402,6 +442,9 @@ private class DefaultRecordedPathConditions(from: Stack[PathConditionStackLayer] val definingAssumptions: InsertionOrderedSet[Term] = definingAssumptions(from) val definingAssumptionExps: InsertionOrderedSet[DebugExp] = definingAssumptionExps(from) val declarations: InsertionOrderedSet[Decl] = declarations(from) + val analysisLabels: InsertionOrderedSet[Term] = analysisLabels(from) + val infeasibilityNodeId: Option[Int] = infeasibilityNodeId(from) + val isPathMarkedInfeasible: Boolean = isPathMarkedInfeasible(from) def contains(assumption: Term): Boolean = contains(from, assumption) @@ -459,6 +502,19 @@ private[decider] class LayeredPathConditionStack layers.head.branchConditionExp = conditionExp } + def markPathInfeasible(isInfeasible: Boolean): Unit = { + layers.head.markPathInfeasible(isInfeasible) + } + + def isPathMarkedInfeasible: Boolean = { + layers.exists(_.isPathMarkedInfeasible) + } + + def setCurrentInfeasibilityNode(node: Option[Int]): Unit = { + layers.head.setInfeasibilityNodeId(node) + } + def getCurrentInfeasibilityNode: Option[Int] = layers.map(_.infeasibilityNodeId).find(_.isDefined).flatten + def startDebugSubExp(): Unit = { layers.head.startDebugSubExp() } @@ -501,6 +557,10 @@ private[decider] class LayeredPathConditionStack layers.head.add(declaration) } + def addAnalysisLabel(assumption: Term): Unit = { + layers.head.addAnalysisLabel(assumption) + } + def pushScope(): Unit = { val scopeMark = pushLayer() scopeMarks = scopeMark :: scopeMarks @@ -568,6 +628,10 @@ private[decider] class LayeredPathConditionStack def declarations: InsertionOrderedSet[Decl] = InsertionOrderedSet(layers.flatMap(_.declarations)) // Note: Performance? + def analysisLabels: InsertionOrderedSet[Term] = InsertionOrderedSet(layers.flatMap(_.analysisLabels)) + + override def infeasibilityNodeId: Option[Mark] = layers.flatMap(_.infeasibilityNodeId).headOption + def definingAssumptions: InsertionOrderedSet[Term] = InsertionOrderedSet(layers.flatMap(_.globalDefiningAssumptions) ++ layers.flatMap(_.nonGlobalDefiningAssumptions)) // Note: Performance? diff --git a/src/main/scala/decider/ProverStdIO.scala b/src/main/scala/decider/ProverStdIO.scala index d64fbddd7..ad1afd7f0 100644 --- a/src/main/scala/decider/ProverStdIO.scala +++ b/src/main/scala/decider/ProverStdIO.scala @@ -6,21 +6,20 @@ package viper.silicon.decider -import java.io._ -import java.nio.file.Path -import java.util.concurrent.TimeUnit import com.typesafe.scalalogging.LazyLogging import viper.silicon.common.config.Version -import viper.silicon.interfaces.decider.{Prover, Result, Sat, Unknown, Unsat} +import viper.silicon.interfaces.decider._ import viper.silicon.reporting.{ExternalToolError, ProverInteractionFailed} import viper.silicon.state.IdentifierFactory import viper.silicon.state.terms._ import viper.silicon.verifier.Verifier -import viper.silver.verifier.{DefaultDependency => SilDefaultDependency} import viper.silicon.{Config, Map, toMap} import viper.silver.reporter.{ConfigurationConfirmation, InternalWarningMessage, QuantifierInstantiationsMessage, Reporter} -import viper.silver.verifier.Model +import viper.silver.verifier.{Model, DefaultDependency => SilDefaultDependency} +import java.io._ +import java.nio.file.Path +import java.util.concurrent.TimeUnit import scala.collection.mutable abstract class ProverStdIO(uniqueId: String, @@ -39,10 +38,12 @@ abstract class ProverStdIO(uniqueId: String, protected var output: PrintWriter = _ protected var allDecls: Seq[Decl] = Seq() protected var allEmits: Seq[String] = Seq() + protected var proverLabelId: Int = 0 var proverPath: Path = _ var lastReasonUnknown : String = _ var lastModel : String = _ + protected var lastUnsatCore_ : String = _ def exeEnvironmentalVariable: String def dependencies: Seq[SilDefaultDependency] @@ -220,6 +221,10 @@ abstract class ProverStdIO(uniqueId: String, // private val quantificationLogger = bookkeeper.logfiles("quantification-problems") def assume(term: Term): Unit = { + assume(term, nextProverLabel()) + } + + def assume(term: Term, label: String): Unit = { // /* Detect certain simple problems with quantifiers. // * Note that the current checks don't take in account whether or not a // * quantification occurs in positive or negative position. @@ -233,26 +238,37 @@ abstract class ProverStdIO(uniqueId: String, // problems.foreach(p => quantificationLogger.println(s" $p")) // } // }) + val finalLabel = if (label.isEmpty) nextProverLabel() else label + assume(termConverter.convert(term), finalLabel) + } - assume(termConverter.convert(term)) + def nextProverLabel(): String = { + val label = "prover_" + proverLabelId + proverLabelId += 1 + label } - def assume(term: String): Unit = { + def assume(term: String, label: String): Unit = { // bookkeeper.assumptionCounter += 1 - writeLine("(assert " + term + ")") + if ((Verifier.config.dependencyAnalysis.isDefined && label.nonEmpty) || Verifier.config.enableUnsatCores()) { + writeLine("(assert (! " + term + " :named " + (if (label.nonEmpty) label else nextProverLabel()) + "))") + } else { + writeLine("(assert " + term + ")") + } + readSuccess() } - def assert(goal: Term, timeout: Option[Int] = None): Boolean = - assert(termConverter.convert(goal), timeout) + def assert(goal: Term, timeout: Option[Int] = None, label: String = ""): Boolean = + assert(termConverter.convert(goal), timeout, label) - def assert(goal: String, timeout: Option[Int]): Boolean = { + def assert(goal: String, timeout: Option[Int], label: String): Boolean = { // bookkeeper.assertionCounter += 1 val (result, duration) = Verifier.config.assertionMode() match { case Config.AssertionMode.SoftConstraints => assertUsingSoftConstraints(goal, timeout) - case Config.AssertionMode.PushPop => assertUsingPushPop(goal, timeout) + case Config.AssertionMode.PushPop => assertUsingPushPop(goal, timeout, label) } comment(s"${viper.silver.reporter.format.formatMillisReadably(duration)}") @@ -261,11 +277,16 @@ abstract class ProverStdIO(uniqueId: String, result } - protected def assertUsingPushPop(goal: String, timeout: Option[Int]): (Boolean, Long) = { + protected def assertUsingPushPop(goal: String, timeout: Option[Int], label: String): (Boolean, Long) = { push() setTimeout(timeout) - writeLine("(assert (not " + goal + "))") + if ((Verifier.config.dependencyAnalysis.isDefined && label.nonEmpty) || Verifier.config.enableUnsatCores()) { + writeLine("(assert (! (not " + goal + ") :named " + (if (label.nonEmpty) label else nextProverLabel()) + "))") + } else { + writeLine("(assert (not " + goal + "))") + } + readSuccess() val startTime = System.currentTimeMillis() @@ -276,6 +297,8 @@ abstract class ProverStdIO(uniqueId: String, if (!result) { retrieveAndSaveModel() retrieveReasonUnknown() + } else if (Verifier.config.dependencyAnalysis.isDefined) { + lastUnsatCore_ = extractUnsatCore() } pop() @@ -283,6 +306,15 @@ abstract class ProverStdIO(uniqueId: String, (result, endTime - startTime) } + def extractUnsatCore(): String = { + writeLine("(get-unsat-core)") + val unsatCore = input.readLine() + comment("unsat core: " + unsatCore) + unsatCore + } + + def getLastUnsatCore: String = lastUnsatCore_ + def saturate(data: Option[Config.ProverStateSaturationTimeout]): Unit = { data match { case Some(Config.ProverStateSaturationTimeout(timeout, comment)) => saturate(timeout, comment) @@ -348,16 +380,21 @@ abstract class ProverStdIO(uniqueId: String, (result, endTime - startTime) } - def check(timeout: Option[Int] = None): Result = { + def check(timeout: Option[Int] = None, label: String = ""): Result = { setTimeout(timeout) writeLine("(check-sat)") - readLine() match { + val result = readLine() match { case "sat" => Sat case "unsat" => Unsat case "unknown" => Unknown } + + if (result == Unsat && Verifier.config.dependencyAnalysis.isDefined) + lastUnsatCore_ = extractUnsatCore() + + result } def statistics(): Map[String, String] = { diff --git a/src/main/scala/decider/Z3ProverAPI.scala b/src/main/scala/decider/Z3ProverAPI.scala index a9effd0c7..cbc395fb6 100644 --- a/src/main/scala/decider/Z3ProverAPI.scala +++ b/src/main/scala/decider/Z3ProverAPI.scala @@ -6,23 +6,21 @@ package viper.silicon.decider +import com.microsoft.z3._ +import com.microsoft.z3.enumerations.Z3_param_kind import com.typesafe.scalalogging.LazyLogging import viper.silicon.common.config.Version -import viper.silicon.interfaces.decider.{Prover, Result, Sat, Unknown, Unsat} +import viper.silicon.interfaces.decider._ +import viper.silicon.reporting.{ExternalToolError, ProverInteractionFailed} import viper.silicon.state.IdentifierFactory import viper.silicon.state.terms.{App, Decl, Fun, FunctionDecl, Implies, MacroDecl, Not, Quantification, Sort, SortDecl, SortWrapperDecl, Term, TriggerGenerator, Var, sorts} -import viper.silicon.{Config, Map} import viper.silicon.verifier.Verifier +import viper.silicon.{Config, Map} import viper.silver.reporter.{InternalWarningMessage, Reporter} import viper.silver.verifier.{MapEntry, ModelEntry, ModelParser, ValueEntry, DefaultDependency => SilDefaultDependency, Model => ViperModel} import java.nio.file.Path import scala.collection.mutable -import com.microsoft.z3._ -import com.microsoft.z3.enumerations.Z3_param_kind -import viper.silicon.reporting.ExternalToolError -import viper.silicon.reporting.ProverInteractionFailed - import scala.jdk.CollectionConverters.MapHasAsJava import scala.util.Random @@ -263,6 +261,10 @@ class Z3ProverAPI(uniqueId: String, throw new ProverInteractionFailed(uniqueId, "Dynamically setting prover options via Z3 API is currently not supported.") } + def assume(term: Term, label: String): Unit = { + assume(term) + } + def assume(term: Term): Unit = { try { if (preamblePhaseOver) @@ -294,7 +296,7 @@ class Z3ProverAPI(uniqueId: String, cleanTerm } - def assert(goal: Term, timeout: Option[Int]): Boolean = { + def assert(goal: Term, timeout: Option[Int], label: String = ""): Boolean = { endPreamblePhase() try { @@ -389,7 +391,7 @@ class Z3ProverAPI(uniqueId: String, (result, endTime - startTime) } - def check(timeout: Option[Int] = None): Result = { + def check(timeout: Option[Int] = None, label: String = ""): Result = { endPreamblePhase() setTimeout(timeout) @@ -402,6 +404,8 @@ class Z3ProverAPI(uniqueId: String, } } + def getLastUnsatCore: String = "" + def endPreamblePhase(): Unit = { if (!preamblePhaseOver) { preamblePhaseOver = true diff --git a/src/main/scala/dependencyAnalysis/DependencyAnalysisInfos.scala b/src/main/scala/dependencyAnalysis/DependencyAnalysisInfos.scala new file mode 100644 index 000000000..91bad2819 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/DependencyAnalysisInfos.scala @@ -0,0 +1,145 @@ +// 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.silicon.dependencyAnalysis + +import viper.silicon.SiliconRunner +import viper.silicon.verifier.Verifier +import viper.silver.ast +import viper.silver.ast._ +import viper.silver.dependencyAnalysis.AssumptionType.AssumptionType +import viper.silver.dependencyAnalysis._ + +/** + * Stores all information about the currently evaluated statement/expression such that the dependency analysis can + * correctly add nodes and edges to the graph. + */ +case class DependencyAnalysisInfos(sourceInfos: List[DependencyAnalysisSourceInfo], dependencyTypes: List[DependencyTypeInfo], mergeInfos: List[DependencyAnalysisMergeInfo], joinInfos: List[DependencyAnalysisJoinInfo], nodes: List[ast.Node], analysisEnabled: Boolean = true) { + + private def isAnalysisEnabled = Verifier.config.dependencyAnalysis.isDefined && analysisEnabled + + def addInfo(info: ast.Info, node: ast.Node): DependencyAnalysisInfos = { + if (isAnalysisEnabled) { + val newSourceInfos = sourceInfos ++ info.getUniqueInfo[DependencyAnalysisSourceInfo].toList + val newDependencyInfos = dependencyTypes ++ info.getUniqueInfo[DependencyTypeInfo].toList + val newMergeInfos = mergeInfos ++ info.getUniqueInfo[DependencyAnalysisMergeInfo].toList + val newJoinInfos = joinInfos ++ info.getUniqueInfo[DependencyAnalysisJoinInfo].toList + this.copy(sourceInfos=newSourceInfos, dependencyTypes=newDependencyInfos, mergeInfos=newMergeInfos, joinInfos=newJoinInfos, nodes=nodes ++ List(node)) + } else { + this + } + } + + def addInfo(info: ast.Info): DependencyAnalysisInfos = { + if (isAnalysisEnabled) { + val newSourceInfos = sourceInfos ++ info.getUniqueInfo[DependencyAnalysisSourceInfo].toList + val newDependencyInfos = dependencyTypes ++ info.getUniqueInfo[DependencyTypeInfo].toList + val newMergeInfos = mergeInfos ++ info.getUniqueInfo[DependencyAnalysisMergeInfo].toList + val newJoinInfos = joinInfos ++ info.getUniqueInfo[DependencyAnalysisJoinInfo].toList + this.copy(sourceInfos=newSourceInfos, dependencyTypes=newDependencyInfos, mergeInfos=newMergeInfos, joinInfos=newJoinInfos) + } else { + this + } + } + + def addInfo(infoString: String, pos: ast.Position, dependencyType: DependencyType): DependencyAnalysisInfos = if (isAnalysisEnabled) { + this.copy(sourceInfos = sourceInfos ++ List(StringDependencyAnalysisSourceInfo(infoString, pos)), dependencyTypes = dependencyTypes ++ List(DependencyTypeInfo(dependencyType))) + } else { + this + } + + def withDependencyType(dependencyType: DependencyType): DependencyAnalysisInfos = { + if (isAnalysisEnabled) this.copy(dependencyTypes = DependencyTypeInfo(dependencyType) +: dependencyTypes) + else this + } + + def withDependencyType(assumptionType: AssumptionType): DependencyAnalysisInfos = { + if (isAnalysisEnabled) this.copy(dependencyTypes = DependencyTypeInfo(DependencyType(assumptionType)) +: dependencyTypes) + else this + } + + def withSource(source: DependencyAnalysisSourceInfo): DependencyAnalysisInfos = + if (isAnalysisEnabled) this.copy(sourceInfos = source +: sourceInfos) else this + + private def getNodeInfo(n: ast.Node): String = { + n match { + case np: Positioned => + s"${n.toString()} (${np.pos})" + case _ => + s"${n.toString()} (???)" + } + } + + private def getDebugInfo: String = { + val sourceInfo = sourceInfos.headOption.map("source info: " + _.toString + " ").getOrElse("") + val nodeInfo = if (nodes.nonEmpty) "nodes: " + nodes.map(getNodeInfo).mkString(", ") else "" + s"$sourceInfo$nodeInfo" + } + + def getSourceInfo: DependencyAnalysisSourceInfo = { + if (isAnalysisEnabled) { + val sourceInfoOpt = sourceInfos.headOption + if (sourceInfoOpt.isDefined) { + sourceInfoOpt.get + } else { + SiliconRunner.logger.warn(s"WARN: Missing source info for $getDebugInfo") + nodes.headOption.map(DependencyAnalysisSourceInfo.createAnalysisSourceInfo).getOrElse(StringDependencyAnalysisSourceInfo("Unknown", NoPosition)) + } + } else { + StringDependencyAnalysisSourceInfo("Unknown", NoPosition) + } + } + + def getDependencyType: DependencyType = { + if (isAnalysisEnabled) { + val dependencyTypeOpt = dependencyTypes.headOption.map(_.dependencyType) + if (dependencyTypeOpt.isDefined) { + dependencyTypeOpt.get + } else { + SiliconRunner.logger.warn(s"WARN: Missing dependency type for $getDebugInfo") + AssumptionType.Unknown.asDepType() + } + } else { + AssumptionType.Unknown.asDepType() + } + } + + def getMergeInfo: DependencyAnalysisMergeInfo = { + if (isAnalysisEnabled) mergeInfos.headOption.getOrElse(SimpleDependencyAnalysisMerge(getSourceInfo)) + else NoDependencyAnalysisMerge() + } + + def getJoinInfo: List[SimpleDependencyAnalysisJoin] = { + if (isAnalysisEnabled) { + joinInfos.map { + case EvalStackDependencyAnalysisJoin(joinType, edgeType) => + if (sourceInfos.lastOption.isEmpty) SiliconRunner.logger.warn(s"WARN: Missing source info for $getDebugInfo") + SimpleDependencyAnalysisJoin(sourceInfos.lastOption.orElse(nodes.lastOption.map(DependencyAnalysisSourceInfo.createAnalysisSourceInfo)).getOrElse(StringDependencyAnalysisSourceInfo("Unknown", NoPosition)), joinType, edgeType) + case a: SimpleDependencyAnalysisJoin => a + } + } else List.empty + } + + def withMergeInfo(mergeInfo: DependencyAnalysisMergeInfo): DependencyAnalysisInfos = + if (isAnalysisEnabled) this.copy(mergeInfos = mergeInfo +: mergeInfos) else this + + def withJoinInfo(joinInfo: DependencyAnalysisJoinInfo): DependencyAnalysisInfos = + if (isAnalysisEnabled) this.copy(joinInfos = joinInfo +: joinInfos) else this + + def withEnabled(analysisEnabled: Boolean): DependencyAnalysisInfos = this.copy(analysisEnabled=analysisEnabled) + + def withInfo(sourceInfo: DependencyAnalysisSourceInfo, dependencyType: DependencyType): DependencyAnalysisInfos = + this.withSource(sourceInfo).withDependencyType(dependencyType) + + def withInfo(sourceInfo: DependencyAnalysisSourceInfo, assumptionType: AssumptionType): DependencyAnalysisInfos = + this.withSource(sourceInfo).withDependencyType(assumptionType) +} + +object DependencyAnalysisInfos { + val DefaultInfos = DependencyAnalysisInfos(List.empty, List.empty, List.empty, List.empty, List.empty) +} + +case class DependencyAnalysisAxiomInfo(analysisInfos: DependencyAnalysisInfos, memberStr: String) diff --git a/src/main/scala/dependencyAnalysis/DependencyAnalysisReporter.scala b/src/main/scala/dependencyAnalysis/DependencyAnalysisReporter.scala new file mode 100644 index 000000000..290028f18 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/DependencyAnalysisReporter.scala @@ -0,0 +1,16 @@ +// 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.silicon.dependencyAnalysis + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silver.reporter.{Message, Reporter} + +case class DependencyAnalysisReporter(name: String = "dependencyAnalysis_reporter", path: String = "report.csv") extends Reporter { + var joinedDependencyGraphInterpreter: Option[DependencyGraphInterpreter[Final]] = None + override def report(msg: Message): Unit = {} +} diff --git a/src/main/scala/dependencyAnalysis/DependencyAnalysisResult.scala b/src/main/scala/dependencyAnalysis/DependencyAnalysisResult.scala new file mode 100644 index 000000000..0191b3b20 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/DependencyAnalysisResult.scala @@ -0,0 +1,13 @@ +// 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.silicon.dependencyAnalysis + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silver.ast.Program + +case class DependencyAnalysisResult(program: Program, dependencyGraphInterpreter: DependencyGraphInterpreter[Final]) diff --git a/src/main/scala/dependencyAnalysis/DependencyAnalyzer.scala b/src/main/scala/dependencyAnalysis/DependencyAnalyzer.scala new file mode 100644 index 000000000..6c940039d --- /dev/null +++ b/src/main/scala/dependencyAnalysis/DependencyAnalyzer.scala @@ -0,0 +1,421 @@ +// 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.silicon.dependencyAnalysis + +import viper.silicon.decider.{Decider, DependencyAnalysisHandler} +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.state.chunks.{Chunk, GeneralChunk} +import viper.silicon.state.terms.{Implies, NoPerm, _} +import viper.silicon.verifier.Verifier +import viper.silver.ast +import viper.silver.ast._ +import viper.silver.dependencyAnalysis._ + +import scala.collection.mutable + + +trait DependencyAnalyzer { + protected val dependencyGraph: DependencyGraph[Init] = new DependencyGraph() + + def getMember: ast.Member + + def getNodes: Iterable[DependencyAnalysisNode] + + def addNodes(nodes: Iterable[DependencyAnalysisNode]): Unit + def addAssertionNode(node: GeneralAssertionNode): Unit + def addAssumptionNode(node: GeneralAssumptionNode): Unit + def addAssumption(assumption: Term, analysisInfos: DependencyAnalysisInfos, description: Option[String] = None): Option[Int] + def addAxiom(assumption: Term, analysisAxiomInfo: DependencyAnalysisAxiomInfo, description: Option[String] = None): Option[Int] + def registerInhaleChunk[CH <: GeneralChunk](sourceChunks: Set[Chunk], buildChunk: Term => CH, perm: Term, labelNode: Option[LabelNode], analysisInfos: DependencyAnalysisInfos): CH = buildChunk(perm) + def registerExhaleChunk[CH <: GeneralChunk](sourceChunks: Set[Chunk], buildChunk: Term => CH, perm: Term, labelNodeOpt: Option[LabelNode], analysisInfos: DependencyAnalysisInfos): CH = buildChunk(perm) + def createLabelNode(label: Var, sourceChunks: Iterable[Chunk], sourceTerms: Iterable[Term]): Option[LabelNode] + + def createAssertOrCheckNode(term: Term, analysisInfos: DependencyAnalysisInfos, isCheck: Boolean): Option[GeneralAssertionNode] + def addAssertFalseNode(isCheck: Boolean, analysisInfos: DependencyAnalysisInfos): Option[Int] + def addInfeasibilityNode(isCheck: Boolean, analysisInfos: DependencyAnalysisInfos): Option[Int] + def addAssertionFailedNode(failedAssertion: Term, analysisInfos: DependencyAnalysisInfos): Option[Int] + + /** + * Adds a dependency between all pairs of source and dest node ids. + */ + def addDependency(source: Option[Int], dest: Option[Int]): Unit + + /** + * @param dep The UNSAT core as reported by Z3. + * @param assertionLabel the label of the assertion that was proven using the UNSAT core + * + * Parses the UNSAT core and adds all its components as dependencies of the provided assertion. + */ + def processUnsatCoreAndAddDependencies(dep: String, assertionLabel: String): Unit + + /** + * Adds dependencies between all pairs of sourceExps and targetExps, where sourceExps should be preconditions and + * targetExps should be postconditions of an abstract function or method. + */ + def addDependenciesForAbstractMembers(sourceExps: Seq[ast.Exp], targetExps: Seq[ast.Exp], analysisInfos: DependencyAnalysisInfos): Unit + + /** + * Adds an assertion and assumption node with the given analysis source info including dependencies to the current infeasibility node. + */ + def addAssertionWithDepToInfeasNode(infeasNodeId: Option[Int], analysisInfos: DependencyAnalysisInfos): Unit = {} + + /** + * @return the final dependency graph representing all (direct and transitive) intraprocedural dependencies + */ + def buildFinalGraph(): Option[DependencyGraph[IntraProcedural]] + + /** + * Stores the information that all nodes having a merge info in sourceExps should be connected to all nodes having a + * merge info in targetExps. + * These edges are eventually added when building the final graph ([[viper.silicon.dependencyAnalysis.DependencyAnalyzer#buildFinalGraph]]). + */ + def addCustomDependenciesBetweenMergeInfos(sourceExps: Seq[Exp], targetExps: Seq[Exp]): Unit +} + +object DependencyAnalyzer { + val analysisLabelName: String = "$$analysisLabel$$" + private val assumptionLabelPrefix = "assumption" + private val assertionLabelPrefix = "assertion" + private val axiomLabelPrefix = "axiom" + private val enableDependencyAnalysisAnnotationKey = "enableDependencyAnalysis" + + private def extractAnnotationFromInfo(info: ast.Info, annotationKey: String): Option[Seq[String]] = { + info.getAllInfos[ast.AnnotationInfo] + .filter(_.values.contains(annotationKey)) + .map(_.values(annotationKey)).headOption + } + + def extractEnableAnalysisFromInfo(info: ast.Info): Option[Boolean] = { + val annotation = extractAnnotationFromInfo(info, enableDependencyAnalysisAnnotationKey) + if (annotation.isDefined && annotation.get.nonEmpty) annotation.get.head.toBooleanOption else None + } + + def createAssumptionLabel(id: Option[Int]): String = { + createLabel(assumptionLabelPrefix, id) + } + + def createAssertionLabel(id: Option[Int]): String = { + createLabel(assertionLabelPrefix, id) + } + + def createAxiomLabel(id: Option[Int]): String = { + createLabel(axiomLabelPrefix, id) + } + + private def createLabel(description: String, id: Option[Int]): String = { + if (id.isDefined) description + "_" + id.get + else "" + } + + private lazy val labelPrefixes = Seq(assumptionLabelPrefix, assertionLabelPrefix, axiomLabelPrefix) + def getIdFromLabel(label: String): Option[Int] = { + val parts = label.split("_") + if (labelPrefixes.exists(parts(0).startsWith)) + parts(1).toIntOption + else + None + } + + def addAssumption(decider: Decider, assumption: Term, analysisInfos: DependencyAnalysisInfos, description: Option[String] = None): Option[Int] = decider match { + case daDecider: DependencyAnalysisHandler => daDecider.getDependencyAnalyzer.addAssumption(assumption, analysisInfos, description) + case _ => None + } + + def addCustomDependenciesBetweenMergeInfos(decider: Decider, sourceExps: Seq[Exp], targetExps: Seq[Exp]): Unit = decider match { + case daDecider: DependencyAnalysisHandler => daDecider.getDependencyAnalyzer.addCustomDependenciesBetweenMergeInfos(sourceExps, targetExps) + case _ => + } + + + def wrapWithDependencyAnalysisLabel(decider: Decider, term: Term, sourceChunks: Iterable[Chunk] = Set.empty, sourceTerms: Iterable[Term] = Set.empty): Term = decider match { + case daDecider: DependencyAnalysisHandler => + if (!daDecider.isDependencyAnalysisEnabled || term.equals(True) || sourceChunks.size + sourceTerms.size == 0) + term + else { + val labelNode = daDecider.getOrCreateAnalysisLabelNode(sourceChunks, sourceTerms) + labelNode.map(n => Implies(n.term, term)).getOrElse(term) + } + case _ => term + } + + def handleAndGetUpdatedAnalysisInfos(decider: Decider, analysisInfos: DependencyAnalysisInfos, info: Info, node: ast.Node): DependencyAnalysisInfos = decider match { + case daDecider: DependencyAnalysisHandler => + val newAnalysisInfos = analysisInfos.addInfo(info, node) + info.getAllInfos[AdditionalDependencyNodeInfo].foreach { + case AdditionalAssertionNode() => daDecider.getDependencyAnalyzer.createAssertOrCheckNode(True, newAnalysisInfos, isCheck = false).foreach(n => { + daDecider.getDependencyAnalyzer.addAssertionNode(n) + if (daDecider.isPathMarkedInfeasible) daDecider.getDependencyAnalyzer.addDependency(daDecider.pcs.getCurrentInfeasibilityNode, Some(n.id)) + }) + case AdditionalAssumptionNode() => daDecider.getDependencyAnalyzer.addAssumption(True, newAnalysisInfos) + } + newAnalysisInfos + case _ => analysisInfos + } + +} + +class DefaultDependencyAnalyzer(member: Option[ast.Member]) extends DependencyAnalyzer { + protected var customMergeDependencies: Set[(Set[DependencyAnalysisMergeInfo], Set[DependencyAnalysisMergeInfo])] = Set.empty + + override def getMember: ast.Member = member.get + + override def getNodes: Iterable[DependencyAnalysisNode] = dependencyGraph.getNodes + + private def getNodeIdsByTerm(terms: Set[Term]): Set[Int] = { + dependencyGraph.getNodes + .filter(t => terms.contains(t.term)) + .map(_.id) + } + + + override def addNodes(nodes: Iterable[DependencyAnalysisNode]): Unit = { + nodes foreach dependencyGraph.addNode + } + + override def addAssumptionNode(node: GeneralAssumptionNode): Unit = dependencyGraph.addAssumptionNode(node) + + override def addAssertionNode(node: GeneralAssertionNode): Unit = dependencyGraph.addAssertionNode(node) + + override def addAssumption(assumption: Term, analysisInfos: DependencyAnalysisInfos, description: Option[String]): Option[Int] = { + val node = DependencyNodeFactory.createSimpleAssumptionNode(assumption, description, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assumptionType, analysisInfos.getMergeInfo, analysisInfos.getJoinInfo, getMember.name) + addAssumptionNode(node) + Some(node.id) + } + + override def addAxiom(assumption: Term, analysisAxiomInfo: DependencyAnalysisAxiomInfo, description: Option[String]): Option[Int] = { + val analysisInfos = analysisAxiomInfo.analysisInfos + val node = DependencyNodeFactory.createAxiomAssumptionNode(assumption, description, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assumptionType, analysisInfos.getMergeInfo, analysisInfos.getJoinInfo, analysisAxiomInfo.memberStr) + addAssumptionNode(node) + Some(node.id) + } + + override def registerExhaleChunk[CH <: GeneralChunk](sourceChunks: Set[Chunk], buildChunk: Term => CH, perm: Term, labelNodeOpt: Option[LabelNode], analysisInfos: DependencyAnalysisInfos): CH = { + val labelNode = labelNodeOpt.get + val chunk = buildChunk(Ite(labelNode.term, perm, NoPerm)) + val chunkNode = addPermissionExhaleNode(chunk, chunk.perm, analysisInfos, labelNode) + if (chunkNode.isDefined) addDependency(chunkNode, Some(labelNode.id)) + chunk + } + + override def registerInhaleChunk[CH <: GeneralChunk](sourceChunks: Set[Chunk], buildChunk: Term => CH, perm: Term, labelNodeOpt: Option[LabelNode], analysisInfos: DependencyAnalysisInfos): CH = { + val labelNode = labelNodeOpt.get + val chunk = buildChunk(Ite((labelNode.term, perm, NoPerm))) + val chunkNode = addPermissionInhaleNode(chunk, chunk.perm, analysisInfos, labelNode) + if (chunkNode.isDefined) addDependency(chunkNode, Some(labelNode.id)) + chunk + } + + private def addPermissionInhaleNode(chunk: Chunk, permAmount: Term, analysisInfos: DependencyAnalysisInfos, labelNode: LabelNode): Option[Int] = { + val node = DependencyNodeFactory.createPermissionInhaleNode(chunk, permAmount, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assumptionType, analysisInfos.getMergeInfo, labelNode, analysisInfos.getJoinInfo, getMember.name) + addAssumptionNode(node) + Some(node.id) + } + + private def addPermissionExhaleNode(chunk: Chunk, permAmount: Term, analysisInfos: DependencyAnalysisInfos, labelNode: LabelNode): Option[Int] = { + val node = DependencyNodeFactory.createPermissionExhaleNode(chunk, permAmount, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assertionType, analysisInfos.getMergeInfo, labelNode, analysisInfos.getJoinInfo, getMember.name) + addAssertionNode(node) + Some(node.id) + } + + override def createLabelNode(label: Var, sourceChunks: Iterable[Chunk], sourceTerms: Iterable[Term]): Option[LabelNode] = { + val labelNode = DependencyNodeFactory.createLabelNode(label, getMember.name) + addAssumptionNode(labelNode) + dependencyGraph.addEdges(getNodeIdsByTerm(sourceTerms.toSet), labelNode.id) + Some(labelNode) + } + + override def createAssertOrCheckNode(term: Term, analysisInfos: DependencyAnalysisInfos, isCheck: Boolean): Option[GeneralAssertionNode] = { + if (isCheck) + Some(DependencyNodeFactory.createSimpleCheckNode(term, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assertionType, analysisInfos.getMergeInfo, analysisInfos.getJoinInfo, getMember.name)) + else + Some(DependencyNodeFactory.createSimpleAssertionNode(term, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assertionType, analysisInfos.getMergeInfo, analysisInfos.getJoinInfo, getMember.name)) + } + + private def addAssertNode(term: Term, analysisInfos: DependencyAnalysisInfos): Option[Int] = { + val node = createAssertOrCheckNode(term, analysisInfos, isCheck=false) + node foreach addAssertionNode + node map (_.id) + } + + override def addAssertFalseNode(isCheck: Boolean, analysisInfos: DependencyAnalysisInfos): Option[Int] = { + val node = createAssertOrCheckNode(False, analysisInfos, isCheck) + addAssertionNode(node.get) + node.map(_.id) + } + + override def addInfeasibilityNode(isCheck: Boolean, analysisInfos: DependencyAnalysisInfos): Option[Int] = { + val node = DependencyNodeFactory.createInfeasibilityNode(analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assumptionType, getMember.name) + addAssumptionNode(node) + Some(node.id) + } + + override def addAssertionFailedNode(failedAssertion: Term, analysisInfos: DependencyAnalysisInfos): Option[Int] = { + val assumeNode = DependencyNodeFactory.createSimpleAssumptionNode(failedAssertion, None, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assertionType, analysisInfos.getMergeInfo, analysisInfos.getJoinInfo, getMember.name) + val assertFailedNode = DependencyNodeFactory.createSimpleAssertionNode(failedAssertion, analysisInfos.getSourceInfo, analysisInfos.getDependencyType.assertionType, analysisInfos.getMergeInfo, analysisInfos.getJoinInfo, getMember.name, hasFailed=true) + dependencyGraph.addNode(assumeNode) + dependencyGraph.addNode(assertFailedNode) + dependencyGraph.addEdges(Set(assumeNode.id), assertFailedNode.id) + Some(assertFailedNode.id) + } + + + override def addDependency(source: Option[Int], dest: Option[Int]): Unit = { + if (source.isDefined && dest.isDefined) + dependencyGraph.addEdges(source.get, Set(dest.get)) + } + + override def processUnsatCoreAndAddDependencies(dep: String, assertionLabel: String): Unit = { + val assumptionLabels = dep.replace("(", "").replace(")", "").split(" ") + val assertionId = DependencyAnalyzer.getIdFromLabel(assertionLabel).get + val assumptionIds = assumptionLabels.flatMap(DependencyAnalyzer.getIdFromLabel).toSet + if (!assumptionIds.contains(assertionId)) + dependencyGraph.addVacuousProof(assertionId) + dependencyGraph.addEdges(assumptionIds.diff(Set(assertionId)), assertionId) + } + + override def addDependenciesForAbstractMembers(sourceExps: Seq[ast.Exp], targetExps: Seq[ast.Exp], analysisInfos: DependencyAnalysisInfos): Unit = { + val sourceNodeIds = sourceExps.flatMap(e => addAssumption(True, analysisInfos.addInfo(e.info, e).withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Sink, EdgeType.Up)))) + val targetNodes = targetExps.flatMap(e => addAssertNode(True, analysisInfos.addInfo(e.info, e).withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Source, EdgeType.Down)))) + dependencyGraph.addEdges(sourceNodeIds.toSet, targetNodes.toSet) + } + + override def addCustomDependenciesBetweenMergeInfos(sourceExps: Seq[Exp], targetExps: Seq[Exp]): Unit = { + val sourceMergeInfos = sourceExps.flatMap(_.info.getUniqueInfo[DependencyAnalysisMergeInfo]).filter(_.isMerge).toSet + val targetMergeInfos = targetExps.flatMap(_.info.getUniqueInfo[DependencyAnalysisMergeInfo]).filter(_.isMerge).toSet + + customMergeDependencies = Set((sourceMergeInfos, targetMergeInfos)) ++ customMergeDependencies + } + + protected def addCustomMergeDependencies(mergedGraph: DependencyGraph[IntraProcedural]): Unit = { + customMergeDependencies.foreach{ case (sourceMergeInfos, targetMergeInfos) => + val sourceNodes = mergedGraph.getNodes.filter(node => sourceMergeInfos.contains(node.mergeInfo)).map(_.id) + val targetNodes = mergedGraph.getNodes.filter(node => targetMergeInfos.contains(node.mergeInfo)).map(_.id) + mergedGraph.addEdges(sourceNodes, targetNodes) + } + } + + /** + * + * @return the final dependency graph + * This operation ensures sound computation of transitive dependencies by adding edges between nodes originating from the same + * source code statement. + * Further, this operation removes unnecessary details from the graph by, for example, removing label nodes and merging identical nodes. + */ + override def buildFinalGraph(): Option[DependencyGraph[IntraProcedural]] = { + dependencyGraph.removeLabelNodes() + val mergedGraph = if (Verifier.config.enableDependencyAnalysisDebugging()) dependencyGraph.asInstanceOf[DependencyGraph[IntraProcedural]] else buildAndGetMergedGraph() + addTransitiveEdges(mergedGraph) + if (!Verifier.config.enableDependencyAnalysisDebugging()) mergedGraph.removeInternalNodes() + Some(mergedGraph) + } + + /** + * Adds edges between the nodes with identical merge info and the ones to be connected as given by the custom merge dependencies + * to the merged graph. This step is necessary to ensure that the low-level graph is connected and encodes all direct + * and indirect dependencies. + */ + private def addTransitiveEdges(mergedGraph: DependencyGraph[IntraProcedural]): Unit = { + val nodesPerSourceInfo = mergedGraph.getNodes.filter(_.mergeInfo.isMerge).groupBy(_.mergeInfo) + nodesPerSourceInfo foreach {case (_, nodes) => + val asserts = nodes.filter(_.isInstanceOf[GeneralAssertionNode]) + val assumes = nodes.filter(n => n.isInstanceOf[GeneralAssumptionNode] && !n.isInstanceOf[LabelNode]) + mergedGraph.addEdges(asserts.map(_.id), assumes.map(_.id)) + val checks = asserts.filter(_.isInstanceOf[SimpleCheckNode]) + val notChecks = nodes.filter(n => !n.isInstanceOf[SimpleCheckNode]) + mergedGraph.addEdges(checks.map(_.id), notChecks.map(_.id)) // TODO ake: why do we need this? + } + + addCustomMergeDependencies(mergedGraph) + } + + /** + * Creates a new graph where nodes that only differ in irrelevant information are merged into one node. + * As a result, this operation removes some lower-level details from the graph. + * This step can be skipped for debugging purposes by setting the enableDependencyAnalysisDebugging flag. Doing so + * has no effect on the dependency results but allows to inspect low-level details while debugging and exporting + * the low-level graph containing all details. + */ + private def buildAndGetMergedGraph(): DependencyGraph[IntraProcedural] = { + def keepNode(n: DependencyAnalysisNode): Boolean = !n.mergeInfo.isMerge || n.joinInfos.nonEmpty || n.isInstanceOf[InfeasibilityNode] || n.isInstanceOf[AxiomAssumptionNode] + + val mergedGraph = new DependencyGraph[IntraProcedural] + val nodeMap = mutable.HashMap[Int, Int]() + + dependencyGraph.getAssumptionNodes.filter(keepNode).foreach { n => + nodeMap.put(n.id, n.id) + mergedGraph.addAssumptionNode(n) + } + val assumptionNodesBySource = dependencyGraph.getAssumptionNodes.filter(!keepNode(_)).groupBy(n => (n.sourceInfo, n.assumptionType, n.mergeInfo, n.joinInfos, n.memberStr)) + assumptionNodesBySource foreach { case ((sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr), assumptionNodes) => + if (assumptionNodes.nonEmpty) { + val newNode = DependencyNodeFactory.createSimpleAssumptionNode(True, None, sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr) + assumptionNodes foreach (n => nodeMap.put(n.id, newNode.id)) + mergedGraph.addAssumptionNode(newNode) + } + } + + dependencyGraph.getAssertionNodes.filter(keepNode).foreach { n => + nodeMap.put(n.id, n.id) + mergedGraph.addAssertionNode(n) + } + val assertionNodesBySource = dependencyGraph.getAssertionNodes.filter(!keepNode(_)).groupBy(n => (n.sourceInfo, n.assumptionType, n.mergeInfo, n.joinInfos, n.memberStr)) + assertionNodesBySource foreach { case ((sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr), assertionNodes) => + if (assertionNodes.nonEmpty) { + val newNode = DependencyNodeFactory.createSimpleAssertionNode(True, sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr, hasFailed=assertionNodes.exists(_.hasFailed)) + assertionNodes foreach (n => nodeMap.put(n.id, newNode.id)) + mergedGraph.addAssertionNode(newNode) + } + } + + dependencyGraph.getAllEdges foreach { case (target, deps) => + val newTarget = nodeMap.getOrElse(target, target) + mergedGraph.addEdges(deps.map(d => nodeMap.getOrElse(d, d)), newTarget) + } + + mergedGraph + } + + /** + * Adds an assertion node with the given analysis source info and dependencies to the current infeasibility node. + * The resulting assertion node is required to detect dependencies of the source statement/expression on infeasible paths. + */ + override def addAssertionWithDepToInfeasNode(infeasNodeId: Option[Int], analysisInfos: DependencyAnalysisInfos): Unit = { + val newAssertionNodeId = addAssertNode(False, analysisInfos) + addDependency(infeasNodeId, newAssertionNodeId) + } +} + +/** + * This DependencyAnalyzer implementation is used by default and does nothing. + */ +class NoDependencyAnalyzer extends DependencyAnalyzer { + + override def getMember: ast.Member = ast.Method("none", Seq.empty, Seq.empty, Seq.empty, Seq.empty, None)() + + override def getNodes: Iterable[DependencyAnalysisNode] = Set.empty + + override def addNodes(nodes: Iterable[DependencyAnalysisNode]): Unit = {} + override def addAssertionNode(node: GeneralAssertionNode): Unit = {} + override def addAssumptionNode(node: GeneralAssumptionNode): Unit = {} + override def addAssumption(assumption: Term, analysisInfos: DependencyAnalysisInfos, description: Option[String] = None): Option[Int] = None + override def addAxiom(assumption: Term, analysisAxiomInfo: DependencyAnalysisAxiomInfo, description: Option[String]): Option[Int] = None + override def createLabelNode(labelTerm: Var, sourceChunks: Iterable[Chunk], sourceTerms: Iterable[Term]): Option[LabelNode] = None + + override def createAssertOrCheckNode(term: Term, analysisInfos: DependencyAnalysisInfos, isCheck: Boolean): Option[GeneralAssertionNode] = None + override def addAssertFalseNode(isCheck: Boolean, analysisInfos: DependencyAnalysisInfos): Option[Int] = None + override def addInfeasibilityNode(isCheck: Boolean, analysisInfos: DependencyAnalysisInfos): Option[Int] = None + override def addAssertionFailedNode(failedAssertion: Term, analysisInfos: DependencyAnalysisInfos): Option[Int] = None + + override def addDependency(source: Option[Int], dest: Option[Int]): Unit = {} + override def processUnsatCoreAndAddDependencies(dep: String, assertionLabel: String): Unit = {} + override def addDependenciesForAbstractMembers(sourceExps: Seq[ast.Exp], targetExps: Seq[ast.Exp], analysisInfos: DependencyAnalysisInfos): Unit = {} + + override def buildFinalGraph(): Option[DependencyGraph[IntraProcedural]] = None + + override def addCustomDependenciesBetweenMergeInfos(sourceExps: Seq[Exp], targetExps: Seq[Exp]): Unit = {} + +} diff --git a/src/main/scala/dependencyAnalysis/DependencyGraphJoiner.scala b/src/main/scala/dependencyAnalysis/DependencyGraphJoiner.scala new file mode 100644 index 000000000..6980c2973 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/DependencyGraphJoiner.scala @@ -0,0 +1,101 @@ +// 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.silicon.dependencyAnalysis + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silicon.verifier.Verifier +import viper.silver.dependencyAnalysis.EdgeType.EdgeType +import viper.silver.dependencyAnalysis.JoinType.JoinType +import viper.silver.dependencyAnalysis.{DependencyAnalysisSourceInfo, EdgeType, JoinType} + +import scala.collection.mutable + +/** + * * @param name Optional name for the joined graph. + * * @param dependencyGraphInterpreters The graphs to be joined. + */ +class DependencyGraphJoiner(name: String, dependencyGraphInterpreters: Set[DependencyGraphInterpreter[IntraProcedural]]) { + + /** + * @return A dependency graph interpreter operating on a new dependency graph that represents all input graphs and + * all dependencies between them. + * The new graph is built by adding all existing nodes and edges of all input graphs and joining them + * via the join information stored in each node. + */ + def joinGraphsAndGetInterpreter(): DependencyGraphInterpreter[Final] = { + val newGraph = new DependencyGraph[Final] + + newGraph.addAssumptionNodes(dependencyGraphInterpreters.flatMap (_.getAssumptionNodes)) + newGraph.addAssertionNodes(dependencyGraphInterpreters.flatMap (_.getAssertionNodes)) + dependencyGraphInterpreters foreach (interpreter => interpreter.getGraph.getAllEdges foreach {case (t, deps) => newGraph.addEdges(deps, t)}) + + val joinSourceNodes = dependencyGraphInterpreters flatMap(i => i.joinSourceNodes) + val joinSinkNodes = dependencyGraphInterpreters flatMap(i => i.joinSinkNodes) + + def getJoinNodesByJoinInfo(candidateNodes: Set[DependencyAnalysisNode], joinType: JoinType): Map[(DependencyAnalysisSourceInfo, EdgeType), Set[DependencyAnalysisNode]] = { + val acc: mutable.Map[(DependencyAnalysisSourceInfo, EdgeType), Set[DependencyAnalysisNode]] = mutable.Map.empty + candidateNodes.foreach { + node => + node.joinInfos.foreach { joinInfo => + if (joinInfo.joinType.equals(joinType)) { + val key = (joinInfo.sourceInfo, joinInfo.edgeType) + acc.update(key, acc.getOrElse(key, Set.empty) + node) + } + } + } + acc.toMap + } + + val sourceNodesByJoinInfo = getJoinNodesByJoinInfo(joinSourceNodes, JoinType.Source) + val sinkNodesByJoinInfo = getJoinNodesByJoinInfo(joinSinkNodes, JoinType.Sink) + + val sourceInfoToNodeIds = newGraph.getNodes.groupBy(_.sourceInfo).view.mapValues(_.map(_.id)).toMap + + sinkNodesByJoinInfo.foreach { case (joinKey, sinkNodes) => + val matchingSourceNodes = sourceNodesByJoinInfo.getOrElse(joinKey, Set.empty) + addEdgesConnectingMethods(newGraph, joinKey._2, matchingSourceNodes, sinkNodes, sourceInfoToNodeIds) + } + + val newInterpreter = new DependencyGraphInterpreter[Final](name, newGraph, dependencyGraphInterpreters.toList.flatMap(_.getErrors)) + newInterpreter + } + + private def addEdgesConnectingMethods(newGraph: DependencyGraph[Final], edgeType: EdgeType, sourceNodes: Set[DependencyAnalysisNode], sinkNodes: Set[DependencyAnalysisNode], sourceInfoToNodeIds: Map[DependencyAnalysisSourceInfo, Set[Int]]): Unit = { + if (edgeType.equals(EdgeType.Up)) { + val directDepsOfSources = if(!Verifier.config.disableDependencyAnalysisJoinPrecisionOpt()) { + // Preconditions are connected to the dependencies required to prove them at all call sites. However, they do not depend on the calls themselves. + sourceNodes.groupBy(_.sourceInfo).flatMap(t => getDirectIntraMethodDependencies(t._2.map(_.id), sourceInfoToNodeIds(t._1), newGraph)) + } else { + // Connect preconditions directly to call and therefore, indirectly to all its dependencies -> imprecise but might be faster and + // more user-friendly since it becomes apparent which call introduced these indirect dependencies. + sourceNodes.map(_.id) + } + newGraph.addEdgesConnectingMethodsUpwards(directDepsOfSources.toSet, sinkNodes.map(_.id)) + } else { + newGraph.addEdgesConnectingMethodsDownwards(sourceNodes.map(_.id), sinkNodes.map(_.id)) + } + } + + private def getDirectIntraMethodDependencies(initQueue: Set[Int], allNodesWithSameSource: Set[Int], graph: DependencyGraph[Final]): Set[Int] = { + assert(initQueue.subsetOf(allNodesWithSameSource), s"Target ids do not all belong to sourceInfo $allNodesWithSameSource") + val visited: mutable.Set[Int] = mutable.Set.empty + val result: mutable.Set[Int] = mutable.Set.empty + val queue: mutable.Queue[Int] = mutable.Queue(initQueue.toSeq: _*) + val relevantEdges = graph. + getIntraMethodEdges + while (queue.nonEmpty) { + val curr = queue.dequeue() + val newVisits = relevantEdges.getOrElse(curr, Set()).diff(visited) + val newQueues = newVisits.intersect(allNodesWithSameSource) + visited.addAll(newVisits) + result.addAll(newVisits.diff(newQueues)) + queue.enqueueAll(newQueues.diff(queue.toSet)) + } + result.toSet + } +} diff --git a/src/main/scala/dependencyAnalysis/README.md b/src/main/scala/dependencyAnalysis/README.md new file mode 100644 index 000000000..64a75c1e0 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/README.md @@ -0,0 +1,84 @@ +# Dependency Analysis + +# Running Silicon with Dependency Analysis + +Dependency Analysis is enabled thorough the following configuration options: +`--dependencyAnalysis [CMDS] --proverArgs "proof=true unsat-core=true"` +The available commands are `interactive` and any command supported by the CLI tool. For example, +- `--dependencyAnalysis=interactive` + - Automatically starts the command-line tool once verification terminates. +- `--dependencyAnalysis=export>[folder]` + - e.g., `--dependencyAnalysis=export>graphExports` + - Exports the graph to a folder named after the verified program under the given path (e.g. `graphExports/src_test_resources_andrea_quickTest` for input program `src/test/resources/andrea/quickTest.vpr`) +- Several modes can be combined by `;` + - e.g. `--dependencyAnalysis=export>graphExports/viperTest;progress;interactive` + +For debugging dependency analysis results, the option `--enableDependencyAnalysisDebugging` can be used which disables the merging of nodes. +As a result, the graph used for query computation and the exported graph contain all low-level details. + +The dependency analysis steps through all paths (including provably infeasible paths). +This behavior can be disabled by setting the flat `--disableInfeasiblePaths`, which might be faster but results in unsound dependency results. + + +# Command-Line Tool + +Requires `--dependencyAnalysis=interactive`. + +Example queries for program `src/test/resources/dependencyAnalysisTests/unitTests/B-permissions.vpr`: +- `dep 99` + - Returns all dependencies of assertions on line 99. +- `dep B-permissions@19` + - Uniquely identifies the given line when there are many source files. + - This notation can be used in every command. +- `dep 66 71` + - Query multiple lines. +- `downDep 14` + - Returns all dependents of assumptions on line 14. +- `hasDep 64 66 71` + - Returns true iff there is any dependency between any two queried lines and false otherwise. +- `prune 66 71` + - Exports the program pruned with respect to lines 66 and 71. + - exportFileName: path and file name for the pruned program (e.g. `prunedPrograms/test.vpr` +- `progress` + - Prints the verification progress of the program +- `guide` + - Prints verification guidance + + +# Neo4j Scripts and Usage + +Graphs exported when using `--dependencyAnalysis=export>[PATH TO EXPORT FOLDER]` can be imported to a [Neo4j database]({https://neo4j.com/) using the `neo4j_importer.py` script. + +Importing dependency graphs to Neo4j: + +1. [Create your own Neo4j instance](https://neo4j.com/docs/aura/getting-started/create-account/). + +1. Replace `[NEO4J_URI]` with the URI to your instance (e.g., `neo4j+ssc://df123ab4.databases.neo4j.io`) + +1. Set your environment variable `NEO4J-PW` to the password of your instance. + +1. Make sure the instance is up and running. + +1. Execute `python src/main/scala/dependencyAnalysis/neo4j_importer.py` and when queried provide the following inputs: + 1. foldername: relative path to the export folder of the dependency graph (e.g. `graphExports/src_test_resources_andrea_quickTest`) + 1. node_label: label to be given to the nodes created in Neo4j + 1. Note that existing nodes with the same label are deleted! + +1. Login to Neo4j are explore the graphs. The script creates three graphs, each using a different label for its nodes: + 1. \[label\]: Graph as defined in the export files. + 1. \[label\]_NonInternal: Graph that does not contain any internal nodes and low-level nodes with identical source are already merged into one node. + 1. \[label\]_Explicit: Graph that only contains explicit assumption nodes (and all assertions) and low-level nodes with identical source are already merged into one node. + 1. For a quick start, open the Explore tool and search for `label_NonInternal-(any)-label_NonInternal`. The tool presents the graph described in (ii). + 1. Some query templates, which can be imported to Neo4j, can be found in `neo4j_query_saved_cypher_2025-9-17.csv`. + +# Graph Importer and Stand-Alone Interpreter + +Once a graph has been exported, it can also be loaded and analyzed using a stand-along importer and interpreter, namely `viper.silicon.dependencyAnalysis.DependencyGraphImporter`. + +Example arguments: +- `--graphFolder "C:\Users\andre\dev\viper\gobra\viperserver\silicon\graphExports\src_test_resources_dependencyAnalysisTests_viperTest" --cmds "dep 16;downDep 14;progress"` + - executes each of the semicolon-separated commands. +- `--graphFolder "C:\Users\andre\dev\viper\gobra\viperserver\silicon\graphExports\src_test_resources_dependencyAnalysisTests_viperTest"` + - starts the interactive command line tool. + +Note that currently the pruning is not supported. diff --git a/src/main/scala/dependencyAnalysis/UserLevelDependencyAnalysisNode.scala b/src/main/scala/dependencyAnalysis/UserLevelDependencyAnalysisNode.scala new file mode 100644 index 000000000..bc23d2d3f --- /dev/null +++ b/src/main/scala/dependencyAnalysis/UserLevelDependencyAnalysisNode.scala @@ -0,0 +1,69 @@ +// 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.silicon.dependencyAnalysis + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silver.ast.Position +import viper.silver.dependencyAnalysis.AssumptionType.AssumptionType +import viper.silver.dependencyAnalysis.{DependencyAnalysisSourceInfo, StringDependencyAnalysisSourceInfo} + +object UserLevelDependencyAnalysisNode { + + def from(dependencyNodes: Iterable[DependencyAnalysisNode]): Set[UserLevelDependencyAnalysisNode] = { + val res = dependencyNodes + .map(n => ((StringDependencyAnalysisSourceInfo(n.sourceInfo.getDescription, n.sourceInfo.getPosition), n.memberStr), n)) + .groupBy(_._1).map { case (key, nodes) => + UserLevelDependencyAnalysisNode(key._1, key._2, nodes.map(_._2).toSet) + }.toSet + res + } + + def extractByAssumptionType(nodes: Set[UserLevelDependencyAnalysisNode], filterCriteria: AssumptionType => Boolean): Set[UserLevelDependencyAnalysisNode] = { + nodes.filter(node => node.assumptionTypes exists filterCriteria) + } + + def mkUserLevelString(nodes: Set[DependencyAnalysisNode], sep: String = "\n"): String = { + from(nodes).toList.sortBy(n => (n.source.getLineNumber, n.source.toString)).mkString(sep) + } + + implicit class SetNodeOps(private val left: Set[UserLevelDependencyAnalysisNode]) extends AnyVal { + def diffBySource(right: Set[UserLevelDependencyAnalysisNode]): Set[UserLevelDependencyAnalysisNode] = { + val sources = right.map(_.groupingCondition) + left.filterNot(n => sources.contains(n.groupingCondition)) + } + + def toSourceSet(): Set[DependencyAnalysisSourceInfo] = { + left.map(_.source) + } + + def toSourceMemberSet(): Set[(DependencyAnalysisSourceInfo, String)] = { + left.map(n => (n.source, n.member)) + } + } +} + +case class UserLevelDependencyAnalysisNode(source: DependencyAnalysisSourceInfo, member: String, lowerLevelNodes: Set[DependencyAnalysisNode]) { + + def position: Position = source.getPosition + + def assumptionTypes: Set[AssumptionType] = lowLevelAssumptionNodes.map(_.assumptionType) + def assertionTypes: Set[AssumptionType] = lowLevelAssertionNodes.map(_.assumptionType) + + lazy val lowLevelAssumptionNodes: Set[GeneralAssumptionNode] = lowerLevelNodes.collect { case node: GeneralAssumptionNode => node } + lazy val lowLevelAssertionNodes: Set[GeneralAssertionNode] = lowerLevelNodes.collect { case node: GeneralAssertionNode => node } + + lazy val hasFailures: Boolean = lowLevelAssertionNodes.exists(_.hasFailed) + + override def toString: String = source.toString + + def groupingCondition: (String, Position) = (source.toString, position) + +} + +case class CompactUserLevelDependencyAnalysisNode(source: DependencyAnalysisSourceInfo, assumptionTypes: Set[AssumptionType], assertionTypes: Set[AssumptionType], hasFailures: Boolean) { + def position: Position = source.getPosition +} diff --git a/src/main/scala/dependencyAnalysis/cliTool/AbstractDependencyAnalysisCliTool.scala b/src/main/scala/dependencyAnalysis/cliTool/AbstractDependencyAnalysisCliTool.scala new file mode 100644 index 000000000..d667f4bf4 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/AbstractDependencyAnalysisCliTool.scala @@ -0,0 +1,64 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon.dependencyAnalysis.UserLevelDependencyAnalysisNode +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter + +trait AbstractDependencyAnalysisCliTool { + val interpreter: DependencyGraphInterpreter[Final] + + protected def getSourceInfoString(nodes: Set[DependencyAnalysisNode]): String = { + UserLevelDependencyAnalysisNode.mkUserLevelString(nodes, "\n\t") + } + + protected def getQueriedNodesFromInput(inputs: Set[String]): Set[DependencyAnalysisNode] = { + inputs flatMap (input => { + val parts = input.split("@") + if (parts.size == 2) + parts(1).toIntOption.map(interpreter.getNodesByPosition(parts(0), _)).getOrElse(Set.empty) + else if (parts.size == 1) { + parts(0).toIntOption map interpreter.getNodesByLine getOrElse Set.empty + } else { + Set.empty + } + }) + } + + protected def measureTime[T](function: => T): (T, Double) = { + val startAnalysis = System.nanoTime() + val res = function + val endAnalysis = System.nanoTime() + val durationMs = (endAnalysis - startAnalysis) / 1e6 + (res, durationMs) + } +} + + +trait DependencyAnalysisCliToolExtension extends AbstractDependencyAnalysisCliTool { + val name: String + val commands: List[DependencyAnalysisCliCommand] + + def getInfoString(separator: String): String = s"$name$separator\t${commands.map(_.description).mkString(s"$separator\t")}" + + def visit(inputs: Seq[String]): Boolean = commands map (_.visit(inputs)) exists identity +} + +trait DependencyAnalysisCliCommand { + val cmdName: String + val cmd: Seq[String] => Unit + val description: String + + def accept(inputs: Seq[String]): Boolean = inputs.nonEmpty && inputs.head.equalsIgnoreCase(cmdName) + + def visit(inputs: Seq[String]): Boolean = { + val accepted = accept(inputs) + if (accepted) cmd(inputs.tail) + accepted + } +} diff --git a/src/main/scala/dependencyAnalysis/cliTool/BenchmarkDependencyAnalysisCliExtension.scala b/src/main/scala/dependencyAnalysis/cliTool/BenchmarkDependencyAnalysisCliExtension.scala new file mode 100644 index 000000000..e99f2cf68 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/BenchmarkDependencyAnalysisCliExtension.scala @@ -0,0 +1,248 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon.dependencyAnalysis.UserLevelDependencyAnalysisNode +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silver.ast +import viper.silver.ast.{AnnotationInfo, AnonymousDomainAxiom, Assume, Goto, If, Inhale, Label, LocalVarDeclStmt, MakeInfoPair, NamedDomainAxiom, Package, Seqn, While} + +import java.io.{BufferedWriter, FileWriter, PrintWriter} +import java.nio.file.{Path, Paths} +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import scala.io.Source +import scala.io.StdIn.readLine +import scala.util.matching.Regex + +class BenchmarkDependencyAnalysisCliExtension(override val interpreter: DependencyGraphInterpreter[Final], program: ast.Program) extends DependencyAnalysisCliToolExtension { + + override val name: String = "Benchmark Features" + override val commands: List[DependencyAnalysisCliCommand] = + List( + new PerformanceBenchmarkCommand, + new GraphSizeCommand, + new AnnotateProgramCommand, + new PrecisionEvaluationCommand + ) + + private class PerformanceBenchmarkCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "benchmark" + override val cmd: Seq[String] => Unit = _ => handleBenchmarkQuery() + override val description: String = s"'$cmdName' to run the performance benchmark" + + private def handleBenchmarkQuery(): Unit = { + val N = 12 + var check = true + println("Result file name: ") + val exportFileName = readLine() + val writer = new PrintWriter(exportFileName) + writer.println("queried line,#lowLevelDeps,#deps,runtimes [ms]") + + while(check){ + println("enter line number(s) for query or 'q' to quit") + val userInput = readLine() + if (userInput.equalsIgnoreCase("q")) { + println("Quit.") + check = false + } else { + val inputs = userInput.split(" ").toSet + + val queriedNodes = getQueriedNodesFromInput(inputs) + var allTimes = Seq.empty[Double] + var numDeps = 0 + var numLowLevelDeps = 0 + + for (_ <- 0 to N) { + val (allDependencies, time) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeNonInternalDependencies(queriedNodes)) + allTimes = allTimes :+ time + numLowLevelDeps = allDependencies.size + numDeps = UserLevelDependencyAnalysisNode.from(allDependencies).size + } + + writer.println(s"$userInput,$numLowLevelDeps,$numDeps,${allTimes.mkString(",")}") + println(s"Avg: ${allTimes.sum/allTimes.size}") + } + } + + writer.close() + } + } + + private class PrecisionEvaluationCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "precisionEval" + override val cmd: Seq[String] => Unit = inputs => handlePrecisionEval(inputs.head) + override val description: String = s"'$cmdName [folder]' to run precision evaluation with respect to the ground truth and call graphs specified in the provided folder" + + override def accept(inputs: Seq[String]): Boolean = super.accept(inputs) && inputs.size >= 2 + + + private def handlePrecisionEval(pathToTestFolder: String): Unit = { + val labelPattern: Regex = """@label\(\s*("?)([^")\s]+)\1\s*\)""".r + val header = "Assertion Label,Sound?,#True Dependencies,#Reported Dependencies,#False-Positives,Call Graph Size,Runtime,Noise" + + def readFile(path: String): Map[String, Set[String]] = { + val src = Source.fromFile(path) + try { + src.getLines() + .filter(_.trim.nonEmpty) // skip empty lines + .map { line => + val Array(left, right) = line.split("=", 2) // split into key and rest + val key = left.trim + val values = right.split(",").map(_.trim).toSet + key -> values + } + .toMap + } finally { + src.close() + } + } + + def addOutput(bw: BufferedWriter, output: String): Unit = { + bw.write(output) + bw.newLine() + println(output) + } + + def evalSingleAssertion(assertionLabel: String, groundTruthLabels: Set[String], callGraphLabels: Set[String], bw: BufferedWriter): Unit = { + val startAnalysis = System.nanoTime() + val queriedAssertions = interpreter.getNodesByLabel(assertionLabel) + val allDependencies = interpreter.computeNonInternalDependencies(queriedAssertions) + val sourceDependencies = UserLevelDependencyAnalysisNode.from(allDependencies).toSourceSet().diff(UserLevelDependencyAnalysisNode.from(queriedAssertions).toSourceSet()) + + val endAnalysis = System.nanoTime() + val durationMs = (endAnalysis - startAnalysis) / 1e6 + + val labelsInReportedDeps: Set[Set[String]] = sourceDependencies.map(node => labelPattern.findAllMatchIn(node.toString).map(_.group(2)).toSet) + + val actualLabelInReportedDeps = labelsInReportedDeps.filter(_.size == 1).flatten + val noise = labelsInReportedDeps.filterNot(_.size == 1) + + val isSound = groundTruthLabels.diff(actualLabelInReportedDeps).isEmpty + val imprecise = actualLabelInReportedDeps.diff(groundTruthLabels) + + assert(!isSound || groundTruthLabels.size + imprecise.size == actualLabelInReportedDeps.size, s"Imprecision calculation is wrong.") + assert(actualLabelInReportedDeps.size <= callGraphLabels.size, "Call graph size is smaller than reported dependencies.") + + addOutput(bw, s"$assertionLabel,${if (isSound) "YES" else "NO"},${groundTruthLabels.size},${actualLabelInReportedDeps.size},${imprecise.size},${callGraphLabels.size},${durationMs}ms,${noise.size}") + } + + val dir: Path = Paths.get(pathToTestFolder) + + val pathToGroundTruth = dir.resolve("ground-truth.txt") + val pathToCallGraphs = dir.resolve("call-graphs.txt") + + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HHmmss") + val timestamp = LocalDateTime.now().format(formatter) + + val output: Path = dir.resolve(s"result_$timestamp.csv") + val bw = new BufferedWriter(new FileWriter(output.toUri.getPath)) + + try { + val groundTruths = readFile(pathToGroundTruth.toUri.getPath) + val callGraphs = readFile(pathToCallGraphs.toUri.getPath) + addOutput(bw, header) + callGraphs.foreach { case (assertionLabel, callGraphLabels) => evalSingleAssertion(assertionLabel, groundTruths(assertionLabel), callGraphLabels, bw) } + + bw.close() + + } catch { + case e: Throwable => println(s"Failed. ${e.getMessage}") + } finally { + bw.close() + } + } + } + + private class AnnotateProgramCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "annotate" + override val cmd: Seq[String] => Unit = inputs => handleAnnotateQuery(inputs.head) + override val description: String = s"'$cmdName [file name] to annotate each statement with a label and write the resulting program to the provided file" + + override def accept(inputs: Seq[String]): Boolean = super.accept(inputs) && inputs.size >= 2 + + + private def handleAnnotateQuery(resultFileName: String): Unit = { + var n = 0 + def nextN: Int = { + n = n + 1 + n + } + + def newInfo(info: ast.Info): ast.Info = MakeInfoPair(AnnotationInfo(Map(("label", Seq(s"L$nextN")))), info) + + + def annotateConjungts(exp: ast.Exp): ast.Exp = { + exp match { + case ast.And(l, r) => ast.And(annotateConjungts(l), annotateConjungts(r))(exp.pos, exp.info, exp.errT) + case _ => annotateExp(exp) + } + } + + def annotateExp(exp: ast.Exp): ast.Exp = exp.withMeta((exp.pos, newInfo(exp.info), exp.errT)) + + def annotateSeqn(seqn: ast.Seqn):ast.Seqn = Seqn(seqn.ss.map(annotateStmt), seqn.scopedSeqnDeclarations)(seqn.pos, seqn.info, seqn.errT) + + def annotateStmt(stmt: ast.Stmt): ast.Stmt = { + stmt match { + case Inhale(exp) => Inhale(annotateConjungts(exp))(exp.pos, exp.info, exp.errT) + case Assume(exp) => Assume(annotateConjungts(exp))(exp.pos, exp.info, exp.errT) + case seqn: Seqn => annotateSeqn(seqn) + case If(cond, thn, els) => If(annotateExp(cond), annotateSeqn(thn), annotateSeqn(els))(stmt.pos, stmt.info, stmt.errT) + case While(cond, invs, body) => While(annotateExp(cond), invs.map(annotateConjungts), annotateSeqn(body))(stmt.pos, stmt.info, stmt.errT) + case Label(name, invs) => Label(name, invs.map(annotateConjungts))(stmt.pos, stmt.info, stmt.errT) + case _: Goto | _: LocalVarDeclStmt => stmt + case Package(wand, proofScript) => Package(wand, annotateSeqn(proofScript))(stmt.pos, newInfo(stmt.info), stmt.errT) + case _ => stmt.withMeta((stmt.pos, newInfo(stmt.info), stmt.errT)) + } + } + + def annotateDomain(domain: ast.Domain): ast.Domain = { + def annotateAxiom(axiom: ast.DomainAxiom): ast.DomainAxiom = axiom match { + case NamedDomainAxiom(name, exp) => NamedDomainAxiom(name, annotateExp(exp))(axiom.pos, axiom.info, axiom.domainName, axiom.errT) + case AnonymousDomainAxiom(exp) => AnonymousDomainAxiom(annotateExp(exp))(axiom.pos, axiom.info, axiom.domainName, axiom.errT) + } + domain.copy(axioms = domain.axioms.map(annotateAxiom))(domain.pos, domain.info, domain.errT) + } + + def annotateFunction(function: ast.Function): ast.Function = + function.copy(pres=function.pres.map(annotateConjungts), posts=function.posts.map(annotateConjungts), body=function.body.map(annotateExp))(function.pos, function.info, function.errT) + + def annotateMethod(method: ast.Method): ast.Method = + method.copy(pres=method.pres.map(annotateConjungts), posts=method.posts.map(annotateConjungts), body=method.body.map(annotateSeqn))(method.pos, method.info, method.errT) + + val newProgram: ast.Program = program.copy(domains=program.domains.map(annotateDomain), functions=program.functions.map(annotateFunction), + methods=program.methods.map(annotateMethod))(program.pos, program.info, program.errT) + + val writer = new PrintWriter(resultFileName) + writer.println(newProgram.toString()) + writer.close() + } + } + + private class GraphSizeCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "graphSize" + override val cmd: Seq[String] => Unit = _ => handleGraphSizeQuery() + override val description: String = s"'$cmdName' to print the size of the graph" + + private def handleGraphSizeQuery(): Unit = { + val allAssumptions = interpreter.getNonInternalAssumptionNodes + val assumptions = UserLevelDependencyAnalysisNode.from(allAssumptions) + val allAssertions = interpreter.getNonInternalAssertionNodes + val assertions = UserLevelDependencyAnalysisNode.from(allAssertions) + val nodes = UserLevelDependencyAnalysisNode.from(allAssertions.toSet[DependencyAnalysisNode].union(allAssumptions)) + println(s"#Assumptions = ${assumptions.size}") + println(s"#Assertions = ${assertions.size}") + println(s"#Nodes = ${nodes.size}") + println(s"#low-level Assumptions (non-internal) = ${allAssumptions.size}") + println(s"#low-level Assumptions (all) = ${interpreter.getAssumptionNodes.size}") + println(s"#low-level Assertions (non-internal) = ${allAssertions.size}") + println(s"#low-level Assertions (all) = ${interpreter.getAssertionNodes.size}") + } + } +} diff --git a/src/main/scala/dependencyAnalysis/cliTool/DebugDependencyAnalysisCliExtension.scala b/src/main/scala/dependencyAnalysis/cliTool/DebugDependencyAnalysisCliExtension.scala new file mode 100644 index 000000000..f3b95d215 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/DebugDependencyAnalysisCliExtension.scala @@ -0,0 +1,99 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silicon.verifier.Verifier +import viper.silver.dependencyAnalysis.AssumptionType.AssumptionType +import viper.silver.dependencyAnalysis.{AssumptionType, DependencyAnalysisSourceInfo} + +class DebugDependencyAnalysisCliExtension(override val interpreter: DependencyGraphInterpreter[Final]) extends DependencyAnalysisCliToolExtension{ + override val name: String = "Debug Features" + override val commands: List[DependencyAnalysisCliCommand] = List( + new AssumptionTypesCommand, + new AssertionTypesCommand, + new LowLevelNodesCommand, + new WeirdNodesCommand + ) + + private class AssumptionTypesCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "assumptionTypes" + override val cmd: Seq[String] => Unit = { inputs => + if (inputs.isEmpty) + println(getAssumptionTypesPerNode.mkString("\n")) + else + inputs.flatMap(_.toIntOption).foreach(i => println(s"$i: ${getAssumptionTypesByLine(i)}")) + } + override val description: String = s"'$cmdName [line numbers]' to print the assumption types of all nodes or just the provided lines" + + private def getAssumptionTypesByLine(line: Int): Set[AssumptionType] = { + interpreter.getNodesByLine(line).filter(_.isInstanceOf[GeneralAssumptionNode]).map(_.assumptionType) + } + + private def getAssumptionTypesPerNode: Map[DependencyAnalysisSourceInfo, Set[AssumptionType]] = + getAssumptionTypesPerNode(interpreter.getAssumptionNodes) + + private def getAssumptionTypesPerNode(nodes: Set[GeneralAssumptionNode]): Map[DependencyAnalysisSourceInfo, Set[AssumptionType]] = + nodes.groupBy(_.sourceInfo).view.mapValues(_.map(_.assumptionType)).toMap + } + + private class AssertionTypesCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "assertionTypes" + override val cmd: Seq[String] => Unit = { inputs => + if (inputs.isEmpty) + println(getAssertionTypesPerNode.mkString("\n")) + else + inputs.flatMap(_.toIntOption).foreach(i => println(s"$i: ${getAssertionTypesByLine(i)}")) + } + override val description: String = s"'$cmdName [line numbers]' to print the assertion types of all nodes or just the provided lines" + + private def getAssertionTypesByLine(line: Int): Set[AssumptionType] = { + interpreter.getNodesByLine(line).filter(_.isInstanceOf[GeneralAssertionNode]).map(_.assumptionType) + } + + private def getAssertionTypesPerNode: Map[DependencyAnalysisSourceInfo, Set[AssumptionType]] = + getAssertionTypesPerNode(interpreter.getAssertionNodes) + + private def getAssertionTypesPerNode(nodes: Set[GeneralAssertionNode]): Map[DependencyAnalysisSourceInfo, Set[AssumptionType]] = + nodes.groupBy(_.sourceInfo).view.mapValues(_.map(_.assumptionType)).toMap + } + + private class LowLevelNodesCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "lowLevelNodes" + override val cmd: Seq[String] => Unit = inputs => + inputs.flatMap(_.toIntOption).foreach(i => println(s"$i:\n\t${getLowLevelNodesByLine(i).mkString("\n\t")}")) + override val description: String = s"'$cmdName [line numbers]' to print all low-level nodes of the provided lines" + + override def accept(inputs: Seq[String]): Boolean = super.accept(inputs) && inputs.tail.nonEmpty + + private def getLowLevelNodesByLine(line: Int): List[DependencyAnalysisNode] = { + interpreter.getNodesByLine(line).toList.sortBy(_.id) + } + } + + private class WeirdNodesCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "weirdNodes" + override val cmd: Seq[String] => Unit = _ => printWeirdNodes() + override val description: String = s"'$cmdName' to print weird nodes" + private val weirdNodePattern = """\b(function|func|method|axiom|if|else|while|for|interface|struct|package|import|type)\b""".r + + private def printWeirdNodes(): Unit = { + interpreter.getNodes.filter(n => !n.assumptionType.isInstanceOf[AssumptionType.InternalType]).groupBy(_.sourceInfo) + .filter{case (sourceInfo, _) => weirdNodePattern.findFirstIn(sourceInfo.toString).isDefined} + .foreach (printSingleNode) + } + + private def printSingleNode(infoAndNodes: (DependencyAnalysisSourceInfo, Set[DependencyAnalysisNode])): Unit = { + val (sourceInfo, nodes) = infoAndNodes + val lowLevelNodesStr = if(Verifier.config.enableDependencyAnalysisDebugging()) + s"\n\t${nodes.map(n => s"${n.getNodeString} | ${n.assumptionType}").mkString("\n\t")}" + else "" + println(s"\n--\n${sourceInfo.toString}$lowLevelNodesStr\n--") + } + } +} diff --git a/src/main/scala/dependencyAnalysis/cliTool/DependencyAnalysisCliTool.scala b/src/main/scala/dependencyAnalysis/cliTool/DependencyAnalysisCliTool.scala new file mode 100644 index 000000000..cea54fbb9 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/DependencyAnalysisCliTool.scala @@ -0,0 +1,193 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.{DependencyAnalysisProgressSupporter, DependencyGraphInterpreter} +import viper.silicon.interfaces.Failure +import viper.silver.ast + +import java.io.PrintWriter +import scala.annotation.tailrec +import scala.io.StdIn.readLine + +class DependencyAnalysisCliTool(override val interpreter: DependencyGraphInterpreter[Final], + program: ast.Program, verificationErrors: List[Failure]) extends AbstractDependencyAnalysisCliTool { + + val extensions: List[DependencyAnalysisCliToolExtension] = List( + new DebugDependencyAnalysisCliExtension(interpreter), + new TestDependencyAnalysisCliExtension(interpreter), + new BenchmarkDependencyAnalysisCliExtension(interpreter, program) + ) + + def run(commandStr: String): Unit = { + commandStr.toLowerCase() match { + case "interactive" => handleInteractiveMode() + case _ => handleCommand(commandStr) + } + } + + private val infoString = "Enter " + + "\n\t`help` to print this command overview" + + "\n\t'dep [line numbers]' to print the direct, explicit, and all dependencies of the given line numbers" + + "\n\t'allDeps [line numbers]' (short: 'ad') to print all dependencies of the given line numbers" + + "\n\t'downDep [line numbers]' to print the dependents of the given line numbers" + + "\n\t'progress' to compute the verification progress of the program" + + "\n\t'guide' to compute verification guidance" + + "\n\t'prune [line numbers] > [file]' to prune the program with respect to the given line numbers and export the new program to file" + + "\n\t'export > [folder]' to export the dependency graph to the given folder" + + "\n\t'failures' to print the verification failures" + + (if (extensions.nonEmpty) "\n\t" else "") + + extensions.map(_.getInfoString("\n\t")).mkString("\n\t") + + "\n\t'q' to quit" + + private def handleInteractiveMode(): Unit = { + println("Dependency Analysis Tool started.") + println(infoString) + if(verificationErrors.nonEmpty || interpreter.getAssertionNodesWithFailures.nonEmpty) + println("Program did not verify!") + + runInteractiveMode() + } + + @tailrec + private def runInteractiveMode(): Unit = { + try { + val userInput = readLine() + if (userInput.equalsIgnoreCase("q") || userInput.equalsIgnoreCase("quit")) { + return + } else if (userInput.nonEmpty) { + handleCommand(userInput, isInteractive = true) + } else { + println(infoString) + } + } catch { + case e: Exception => println("ERROR:\n\t" + e.getMessage) + case e: AssertionError => println("ERROR:\n\t" + e.getMessage) + } + runInteractiveMode() + } + + private def handleCommand(cmd: String, isInteractive: Boolean = false): Unit = { + val exportFileName = cmd.split(">").tail.headOption.map(_.trim) + val cmdParts = cmd.takeWhile(_ != '>').split(" ").toSeq + if (cmdParts.nonEmpty) { + cmdParts.head.toLowerCase match { + case "help" | "h" => println(infoString) + case "dep" => handleDependencyQuery(cmdParts.tail.toSet) + case "ad" | "alldeps" => handleAllDependenciesQuery(cmdParts.tail.toSet) + case "downdep" => handleDependentsQuery(cmdParts.tail.toSet) + case "export" => interpreter.exportGraph(program, exportFileName.get) + case "progress" | "prog" => handleVerificationProgressQuery(cmdParts.tail, exportFileName) + case "guidance" | "guide" => handleVerificationGuidanceQuery(cmdParts.tail) + case "prune" => handlePruningRequest(cmdParts.tail, exportFileName.get) + case "failures" => handleFailuresRequest() + case "computegraphonly" => println("Command computeGraphOnly executed.") + case _ => + val visitedExtensions = extensions.map(_.visit(cmdParts)) + if (!visitedExtensions.exists(identity)) println(s"Unknown command $cmd.\n$infoString") + } + println("Done.") + } + } + + private def handleFailuresRequest(): Unit = { + println("Reported verification failures:") + println(s"\t${verificationErrors.mkString("\n\t")}") + println(s"Dependency nodes of failures:") + println(s"\t${interpreter.getAssertionNodesWithFailures.map(_.sourceInfo).mkString("\n\t")}") + } + + private def handleVerificationProgressQuery(inputs: Seq[String], exportFileNameOpt: Option[String] = None): Unit = { + val enableDebugging = inputs.nonEmpty && inputs.head.equals("debug") + + val ((optProgressPeter, optProgressLea), optTime) = measureTime(interpreter.progressSupporter.computeVerificationProgress(enableDebugging)) + + println(s"Peter: ${optProgressPeter.progress}; Lea: ${optProgressLea.progress}\nFinished in ${optTime}ms") + + if (exportFileNameOpt.isDefined) { + val writer = new PrintWriter(exportFileNameOpt.get) + writer.println("Spec Quality, Proof Quality (Peter), Progress (Peter), Proof Quality (Lea), Progress (Lea), Runtime [ms]") + writer.println(s"${optProgressLea.specQuality},${optProgressPeter.proofQuality},${optProgressPeter.progress},${optProgressLea.proofQuality},${optProgressLea.progress},$optTime") + writer.close() + } + } + + private def handleDependencyQuery(inputs: Set[String]): Unit = { + val queriedNodes = getQueriedNodesFromInput(inputs) + val queriedAssertions = queriedNodes.filter(node => node.isInstanceOf[GeneralAssertionNode]) + + val (directDependencies, timeDirect) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeDirectDependencies(queriedAssertions)) + val (allDependencies, timeAll) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeNonInternalDependencies(queriedAssertions)) + val (allDependenciesWithoutInfeasibility, timeWithoutInfeasibility) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeNonInternalDependencies(queriedAssertions, includeInfeasibilityNodes=false)) + val (explicitDependencies, timeExplicit) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeExplicitDependencies(queriedAssertions)) + + println(s"Queried:\n\t${getSourceInfoString(queriedNodes)}") + + println(s"\nDirect Dependencies (${timeDirect}ms):\n\t${getSourceInfoString(directDependencies.diff(queriedNodes))}") + println(s"\nAll Dependencies (${timeAll}ms):\n\t${getSourceInfoString(allDependencies.diff(queriedNodes))}") + println(s"\nDependencies without infeasibility (${timeWithoutInfeasibility}ms):\n\t${getSourceInfoString(allDependenciesWithoutInfeasibility.diff(queriedNodes))}") + println(s"\nExplicit Dependencies (${timeExplicit}ms):\n\t${getSourceInfoString(explicitDependencies.diff(queriedNodes))}") + + if (queriedAssertions.exists(_.asInstanceOf[GeneralAssertionNode].hasFailed)) println("\nQueried assertions (partially) FAILED!\n") + + } + + private def handleAllDependenciesQuery(inputs: Set[String]): Unit = { + val queriedNodes = getQueriedNodesFromInput(inputs) + val queriedAssertions = queriedNodes.filter(node => node.isInstanceOf[GeneralAssertionNode]) + + val (allDependencies, timeAll) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeNonInternalDependencies(queriedAssertions)) + + println(s"Queried:\n\t${getSourceInfoString(queriedNodes)}") + + println(s"\nAll Dependencies (${timeAll}ms):\n\t${getSourceInfoString(allDependencies.diff(queriedNodes))}") + + if (queriedAssertions.exists(_.asInstanceOf[GeneralAssertionNode].hasFailed)) println("\nQueried assertions (partially) FAILED!\n") + } + + private def handleDependentsQuery(inputs: Set[String]): Unit = { + + val queriedNodes = getQueriedNodesFromInput(inputs).filter(interpreter.isNonInternalAssumptionNode) + + val (directDependents, timeDirect) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeDirectDependents(queriedNodes)) + val (allDependents, timeAll) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeNonInternalDependents(queriedNodes)) + val (dependentsWithoutInfeasibility, timeWithoutInfeasibility) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeNonInternalDependents(queriedNodes, includeInfeasibilityNodes=false)) + val (explicitDependents, timeExplicit) = measureTime[Set[DependencyAnalysisNode]](interpreter.computeExplicitDependents(queriedNodes)) + + println(s"Queried:\n\t${getSourceInfoString(queriedNodes)}") + + println(s"\nDirect Dependents (${timeDirect}ms):\n\t${getSourceInfoString(directDependents)}") + println(s"\nAll Dependents (${timeAll}ms):\n\t${getSourceInfoString(allDependents)}") + println(s"\nDependents without infeasibility (${timeWithoutInfeasibility}ms):\n\t${getSourceInfoString(dependentsWithoutInfeasibility)}") + println(s"\nExplicit Dependents (${timeExplicit}ms):\n\t${getSourceInfoString(explicitDependents)}") + + } + + private def handlePruningRequest(inputs: Seq[String], exportFileName: String): Unit = { + val queriedNodes = getQueriedNodesFromInput(inputs.toSet) + interpreter.pruningSupporter.pruneProgramAndExport(queriedNodes, program, exportFileName) + } + + private def handleVerificationGuidanceQuery(inputs: Seq[String]): Unit = { + val enableDebugging = inputs.nonEmpty && inputs.head.equals("debug") + + val assumptionRanking = interpreter.progressSupporter.computeAssumptionRanking().filter(_._2 > 0.0) + println(s"Assumptions/unverified assertions and the number of dependents:\n\t${assumptionRanking.mkString("\n\t")}\n") + + println("Uncovered source code per method: ") + val uncoveredStatements = new DependencyAnalysisProgressSupporter(interpreter).computeUncoveredStatementsPerMember() + + val memberCoverageRanking = uncoveredStatements.view.mapValues(_.size).toList.filter(_._2 > 0).sortBy(_._2).reverse + println(s"\nMethods and the number of uncovered statements:\n\t${memberCoverageRanking.mkString("\n\t")}\n") + + if(enableDebugging) + println(s"\nUncovered statements by member:\n\t${uncoveredStatements.view.mapValues(v => (v, v.size)).toList.filter(_._2._2 > 0).sortBy(_._2._2).reverse}") + + } +} + diff --git a/src/main/scala/dependencyAnalysis/cliTool/DependencyAnalysisTool.scala b/src/main/scala/dependencyAnalysis/cliTool/DependencyAnalysisTool.scala new file mode 100644 index 000000000..64e004580 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/DependencyAnalysisTool.scala @@ -0,0 +1,83 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon.dependencyAnalysis.cliTool.DependencyGraphImporter.{importGraphFromCsv, importProgram} +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silicon.dependencyAnalysis.{DependencyAnalysisResult, DependencyGraphJoiner} +import viper.silicon.interfaces.{Failure, VerificationResult} +import viper.silicon.verifier.Verifier +import viper.silver.ast + +object DependencyAnalysisTool { + + /** + * This method processes command line arguments to import a dependency graph and execute queries on it. + * + * Expected command line arguments: + * - `--graphFolder "[PATH_TO_GRAPH]"`: (Required) Specifies the path to the folder containing the dependency graph export files. + * - `--cmds "[SEMICOLON_SEPARATED_LIST_OF_QUERIES]"`: (Optional) Specifies a series of commands separated by semicolons. + * The supported commands correspond to the ones of the DependencyAnalysisUserTool. + * If this argument is not provided, the interactive mode of the DependencyAnalysisUserTool will start instead. + * + * @throws IllegalArgumentException if the `--graphFolder` argument is not provided. + */ + + def main(args: Array[String]): Unit = { + val graphFolder = extractGraphFolderFromArgs(args) + val graph = importGraphFromCsv(graphFolder) + // TODO ake: doesn't fully work yet, because the exported program has a different line numbering than the program used for the analysis + val program = importProgram(graphFolder) + + val interpreter = new DependencyGraphInterpreter[Final]("test", graph, List.empty, None) + val userTool = new DependencyAnalysisCliTool(interpreter, program, List.empty) + + val cmdsIndex = args.indexOf("--cmds") + val cmds = if (0 <= cmdsIndex && cmdsIndex < args.length - 1) args(cmdsIndex + 1) else "" + + runUserTool(cmds, userTool) + } + + private def extractGraphFolderFromArgs(args: Array[String]): String = { + val idx = args.indexOf("--graphFolder") + if (0 <= idx && idx < args.length - 1) + args(idx + 1) + else + throw new IllegalArgumentException("Error: --graphFolder argument is required but not found.") + } + + def runDependencyAnalysisWorkflow(verificationResults: List[VerificationResult], program: ast.Program, inputFile: Option[String]): Option[DependencyAnalysisResult] = { + Verifier.config.dependencyAnalysis.toOption match { + case None => None + case Some(cmds) => + val dependencyGraphInterpreters = verificationResults.filter(_.dependencyGraphInterpreter.isDefined).map(_.dependencyGraphInterpreter.get) + val verificationErrors: List[Failure] = (verificationResults filter (_.isInstanceOf[Failure])) map (_.asInstanceOf[Failure]) + // TODO ake: make sure we can access the name of frontend programs (instead of naming it "joined") + val programName = inputFile.map(_.replaceAll("\\\\", "_").replaceAll("/", "_").replaceAll(".vpr", "")).getOrElse("joined") + + val fullGraphInterpreter = new DependencyGraphJoiner(programName, dependencyGraphInterpreters.toSet).joinGraphsAndGetInterpreter() + + + val userTool = new DependencyAnalysisCliTool(fullGraphInterpreter, program, verificationErrors) + runUserTool(cmds, userTool) + + val result = DependencyAnalysisResult(program, fullGraphInterpreter) + Some(result) + } + } + + private def runUserTool(cmdStr: String, userTool: DependencyAnalysisCliTool): Unit = + if (cmdStr.nonEmpty) { + val cmds = cmdStr.split(";").map(_.trim) + + cmds foreach {c => + println(s"\n--------\nProcessing command \"$c\"...") + userTool.run(c) + } + } +} diff --git a/src/main/scala/dependencyAnalysis/cliTool/DependencyGraphImporter.scala b/src/main/scala/dependencyAnalysis/cliTool/DependencyGraphImporter.scala new file mode 100644 index 000000000..dbd848f27 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/DependencyGraphImporter.scala @@ -0,0 +1,142 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon +import viper.silicon.SiliconFrontend +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.state.SimpleIdentifier +import viper.silicon.state.chunks.Chunk +import viper.silicon.state.terms.sorts.Bool +import viper.silicon.state.terms.{NoPerm, Term, True, Var} +import viper.silver.ast +import viper.silver.ast._ +import viper.silver.dependencyAnalysis.{AssumptionType, SimpleDependencyAnalysisJoin, SimpleDependencyAnalysisMerge, StringDependencyAnalysisSourceInfo} +import viper.silver.frontend.SilFrontend + +import java.nio.file.Paths +import scala.io.Source + +object DependencyGraphImporter { + lazy val dummyVar: Var = Var.actualCreate((SimpleIdentifier("a"), Bool, false)) + lazy val frontend: SiliconFrontend = createFrontend(Seq.empty) + + def importGraphFromCsv(csvFilePath: String): ReadOnlyDependencyGraph[Final] = { + val graph = new DependencyGraph[Final]() + + val path = Paths.get(csvFilePath) + createNodesFromCsv(graph, path.toString) + createEdgesFromCsv(graph, path.toString) + graph + } + + def importProgram(userInput: String): Program = { + loadProgram(userInput +"\\", frontend) + } + + private def createNodesFromCsv(graph: DependencyGraph[Final], csvFilePath: String): Unit = { + + val bufferedSource = Source.fromFile(csvFilePath + "/nodes.csv") + for (line <- bufferedSource.getLines().filter(_.nonEmpty).drop(1)) { + val fields = line.split("#").map(_.trim) + val nodeIdStr = fields(0) + val nodeType = fields(1) + val assumptionType = AssumptionType.fromString(fields(2)).get + val position = parsePositionString(fields(5)) + val sourceInfo = StringDependencyAnalysisSourceInfo(fields(7), position) + val memberStr: String = fields(8) + val hasFailed: Boolean = fields(9).toBoolean + + // The following node properties are only relevant for graph construction, thus we can use dummy values while querying the graph. + val term: Term = True + val chunk: Chunk = DummyChunk() + val description: Option[String] = None + val mergeInfo: SimpleDependencyAnalysisMerge = SimpleDependencyAnalysisMerge(sourceInfo) + val labelNode: LabelNode = LabelNode(-1, dummyVar, memberStr) + val joinNodeInfos: List[SimpleDependencyAnalysisJoin] = List.empty + + val nodeId = nodeIdStr.toInt + // Create node based on type + val node = nodeType match { + case "Assumption" => SimpleAssumptionNode(nodeId, term, description, sourceInfo, assumptionType, mergeInfo, joinNodeInfos, memberStr) + case "Axiom" => AxiomAssumptionNode(nodeId, term, description, sourceInfo, assumptionType, mergeInfo, joinNodeInfos, memberStr) + case "Assertion" => SimpleAssertionNode(nodeId, term, sourceInfo, assumptionType, mergeInfo, joinNodeInfos, memberStr, hasFailed) + case "Check" => SimpleCheckNode(nodeId, term, sourceInfo, assumptionType, mergeInfo, joinNodeInfos, memberStr, hasFailed) + case "Inhale" => PermissionInhaleNode(nodeId, chunk, term, sourceInfo, assumptionType, mergeInfo, labelNode, joinNodeInfos, memberStr) + case "Exhale" => PermissionExhaleNode(nodeId, chunk, term, sourceInfo, assumptionType, mergeInfo, labelNode, joinNodeInfos, memberStr, hasFailed) + case "Label" => LabelNode(nodeId, dummyVar, memberStr) + case "Infeasible" => InfeasibilityNode(nodeId, sourceInfo, assumptionType, memberStr) + case _ => throw new IllegalArgumentException(s"Unknown node type: $nodeType") + } + + graph.addNode(node) + } + bufferedSource.close() + } + + private def createEdgesFromCsv(graph: DependencyGraph[Final], csvFilePath: String): Unit = { + + val bufferedSource = Source.fromFile(csvFilePath + "/edges.csv") + for (line <- bufferedSource.getLines().filter(_.nonEmpty).drop(1)) { + val Array(sourceId, targetId, tag) = line.split(",").map(_.trim) + + tag match { + case "direct" => graph.addEdges(Set(sourceId.toInt), targetId.toInt) + case "interprocedural downward" => graph.addEdgesConnectingMethodsDownwards(Set(sourceId.toInt), targetId.toInt) + case "interprocedural upward" => graph.addEdgesConnectingMethodsUpwards(Set(sourceId.toInt), targetId.toInt) + case _ => throw new IllegalArgumentException(s"Unknown tag: $tag") + } + + } + bufferedSource.close() + } + + private def createFrontend(commandLineArgs: Seq[String]): SiliconFrontend = { + val reporter = DependencyAnalysisReporter() + val fe = new SiliconFrontend(reporter) + val backend = fe.createVerifier("") + backend.parseCommandLine(commandLineArgs ++ List("--ignoreFile", "dummy.sil")) + fe.init(backend) + fe.setVerifier(backend) + backend.start() + fe + } + + private def loadProgram(filePrefix: String, frontend: SilFrontend): Program = { + val testFile = Paths.get(filePrefix + "program.vpr") + + frontend.reset(testFile) + frontend.runTo(frontend.Translation) + + frontend.translationResult + } + + private def parsePositionString(positionString: String): Position = positionString match { + case "???" => NoPosition + case str if str.startsWith("label ") => + val identifier = str.stripPrefix("label ") + VirtualPosition(identifier) + case str if str.contains(" @ line ") => + val parts = str.split(" @ line ") + val fileName = parts(0) + val line = parts(1).toInt + FilePosition(Paths.get(fileName), line, 0) + case str if str.startsWith("line ") => + val line = str.stripPrefix("line ").toInt + LineColumnPosition(line, 0) + case _ => + throw new IllegalArgumentException(s"Cannot parse position from string: $positionString") + } +} + +private case class DummyChunk() extends Chunk { + val perm: Term = NoPerm + val permExp: Option[ast.Exp] = None + + override def substitute(terms: silicon.Map[Term, Term]): Chunk = this +} diff --git a/src/main/scala/dependencyAnalysis/cliTool/TestDependencyAnalysisCliExtension.scala b/src/main/scala/dependencyAnalysis/cliTool/TestDependencyAnalysisCliExtension.scala new file mode 100644 index 000000000..79cae93d6 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/cliTool/TestDependencyAnalysisCliExtension.scala @@ -0,0 +1,61 @@ +// 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.silicon.dependencyAnalysis.cliTool + +import viper.silicon.dependencyAnalysis.UserLevelDependencyAnalysisNode +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.{DependencyGraphInterpreter, DependencyGraphTestSupporter} + +class TestDependencyAnalysisCliExtension(override val interpreter: DependencyGraphInterpreter[Final]) extends DependencyAnalysisCliToolExtension{ + override val name: String = "Test Features" + override val commands: List[DependencyAnalysisCliCommand] = List( + new TestAllCommand, + new NodeTypeTestCommand, + new DependenciesTestCommand, + ) + + private class TestAllCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "test" + override val description: String = s"""'$cmdName' to test all node types and dependencies with respect to the @dependencyInfo(...) annotations""" + override val cmd: Seq[String] => Unit = { _ => + val testSupporter = new DependencyGraphTestSupporter(interpreter) + if (interpreter.getAssertionNodesWithFailures.nonEmpty) println(s"Program contains verification failures at \n\t${interpreter.getAssertionNodesWithFailures.mkString("\n\t")}") + testSupporter.testDependencies() + testSupporter.testNodeTypes() + } + } + + private class NodeTypeTestCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "testNodeTypes" + override val description: String = s"""'$cmdName [line numbers]' to test the node type with respect to the @dependencyInfo(...) annotations""" + override val cmd: Seq[String] => Unit = { inputs => + val testSupporter = new DependencyGraphTestSupporter(interpreter) + if (inputs.isEmpty) + testSupporter.testNodeTypes() + else + inputs.flatMap(_.toIntOption).foreach(line => testSupporter.testNodeTypes(interpreter.getNodesByLine(line))) + } + } + + private class DependenciesTestCommand extends DependencyAnalysisCliCommand { + override val cmdName: String = "testDependencies" + override val description: String = s"""'$cmdName [line numbers]' to test the node type with respect to the @dependencyInfo(...) annotations""" + override val cmd: Seq[String] => Unit = { inputs => + val testSupporter = new DependencyGraphTestSupporter(interpreter) + if (inputs.isEmpty) + testSupporter.testDependencies() + else + inputs.flatMap(_.toIntOption).foreach(line => { + val testResult = UserLevelDependencyAnalysisNode.from(interpreter.getNodesByLine(line)) map testSupporter.testDependencies + val resultStr = if (testResult.forall(_.isEmpty)) "Skipped." + else if (testResult.forall(test => test.isEmpty || test.get)) "Passed." + else "Failed." + println(s"Line $line: $resultStr") + }) + } + } +} diff --git a/src/main/scala/dependencyAnalysis/graph/DependencyAnalysisNode.scala b/src/main/scala/dependencyAnalysis/graph/DependencyAnalysisNode.scala new file mode 100644 index 000000000..882d97f26 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/graph/DependencyAnalysisNode.scala @@ -0,0 +1,230 @@ +// 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.silicon.dependencyAnalysis.graph + +import viper.silicon.state.chunks.Chunk +import viper.silicon.state.terms.{False, Term, Var} +import viper.silver.dependencyAnalysis.AssumptionType.AssumptionType +import viper.silver.dependencyAnalysis._ + + +trait DependencyAnalysisNode { + + /** + * The unique node id, which is also given to the SMT solver such that unsat cores can be mapped back to dependency nodes. + */ + val id: Int + + + /** + * Stores information about which source code statement / expression created this node. + * This information is crucial to lift lower-level graphs to higher-level graphs and to present user-readable + * dependency results. + */ + val sourceInfo: DependencyAnalysisSourceInfo + + /** + * The assumption / assertion type of the node which is heavily used in dependency graph queries to filter nodes, + * for example, to only present explicit assumptions. + */ + val assumptionType: AssumptionType + + /** + * The merge info determines which nodes should be "merged" when lifting the graph to the user level. In reality, + * the nodes are connected by edges instead and are only partially merged. + */ + val mergeInfo: DependencyAnalysisMergeInfo + + /** + * The join infos specify how the node should be joined with nodes of other verification component's graphs. + */ + val joinInfos: List[SimpleDependencyAnalysisJoin] + + /** + * The program member (method, function) that the node belongs to + */ + val memberStr: String + + /** + * The assumes or asserted Silicon term. Currently, only used for debugging purposes. + */ + val term: Term + + def getUserLevelRepresentation: String = sourceInfo.toString + + /** + Some string representations, mainly used for debugging purposes. + The strings represented to users are obtained via sourceInfo.toString and do not contain any low-level information + about the node (such as the id or term). + */ + override def toString: String = id.toString + " | " + getNodeString + " | " + sourceInfo.toString + def getNodeString: String + def getNodeType: String +} + +trait GeneralAssumptionNode extends DependencyAnalysisNode { + override def getNodeType: String = "Assumption" + +} + +trait GeneralAssertionNode extends DependencyAnalysisNode { + override def getNodeType: String = "Assertion" + + val hasFailed: Boolean + + /** + * @return a copy of the current node (including the id!) but with hasFailed set to true. + */ + def getAssertFailedNode: GeneralAssertionNode +} + +// this is not strictly needed anymore but storing the chunk and label node is useful for debugging purposes +trait ChunkAnalysisInfo { + val chunk: Chunk + val labelNode: LabelNode +} + +case class SimpleAssumptionNode private[graph] (override val id: Int, override val term: Term, + description: Option[String], override val sourceInfo: DependencyAnalysisSourceInfo, + override val assumptionType: AssumptionType, override val mergeInfo: DependencyAnalysisMergeInfo, + override val joinInfos: List[SimpleDependencyAnalysisJoin], override val memberStr: String + ) extends GeneralAssumptionNode { + override def getNodeString: String = "assume " + term.toString + description.map(" (" + _ + ")").getOrElse("") +} + +case class AxiomAssumptionNode private[graph] (override val id: Int, override val term: Term, + description: Option[String], override val sourceInfo: DependencyAnalysisSourceInfo, + override val assumptionType: AssumptionType, override val mergeInfo: DependencyAnalysisMergeInfo, + override val joinInfos: List[SimpleDependencyAnalysisJoin], override val memberStr: String + ) extends GeneralAssumptionNode { + override def getNodeString: String = "assume axiom " + term.toString + description.map(" (" + _ + ")").getOrElse("") + override def getNodeType: String = "Axiom" +} + +case class SimpleAssertionNode private[graph] (override val id: Int, override val term: Term, + override val sourceInfo: DependencyAnalysisSourceInfo, override val assumptionType: AssumptionType, + override val mergeInfo: DependencyAnalysisMergeInfo, override val joinInfos: List[SimpleDependencyAnalysisJoin], + override val memberStr: String, override val hasFailed: Boolean = false + ) extends GeneralAssertionNode { + override def getNodeString: String = "assert " + term.toString + + override def getAssertFailedNode: GeneralAssertionNode = + SimpleAssertionNode(id, term, sourceInfo, assumptionType, mergeInfo, + hasFailed = true, joinInfos = joinInfos, memberStr = memberStr) +} + +case class SimpleCheckNode private[graph] (override val id: Int, override val term: Term, + override val sourceInfo: DependencyAnalysisSourceInfo, override val assumptionType: AssumptionType, + override val mergeInfo: DependencyAnalysisMergeInfo, override val joinInfos: List[SimpleDependencyAnalysisJoin], + override val memberStr: String, override val hasFailed: Boolean = false + ) extends GeneralAssertionNode { + override def getNodeString: String = "check " + term + override def getNodeType: String = "Check" + + override def getAssertFailedNode: GeneralAssertionNode = + SimpleCheckNode(id, term, sourceInfo, assumptionType, mergeInfo, joinInfos, + memberStr = memberStr, hasFailed = true) +} + +case class PermissionInhaleNode private[graph] (override val id: Int, override val chunk: Chunk, + override val term: Term, override val sourceInfo: DependencyAnalysisSourceInfo, + override val assumptionType: AssumptionType, override val mergeInfo: DependencyAnalysisMergeInfo, + override val labelNode: LabelNode, override val joinInfos: List[SimpleDependencyAnalysisJoin], + override val memberStr: String + ) extends GeneralAssumptionNode with ChunkAnalysisInfo { + override def getNodeString: String = "inhale " + chunk.toString + override def getNodeType: String = "Inhale" +} + +case class PermissionExhaleNode private[graph] (override val id: Int, override val chunk: Chunk, + override val term: Term, override val sourceInfo: DependencyAnalysisSourceInfo, + override val assumptionType: AssumptionType, override val mergeInfo: DependencyAnalysisMergeInfo, + override val labelNode: LabelNode, override val joinInfos: List[SimpleDependencyAnalysisJoin], + override val memberStr: String, override val hasFailed: Boolean = false + ) extends GeneralAssertionNode with ChunkAnalysisInfo { + override def getNodeType: String = "Exhale" + override def getNodeString: String = "exhale " + chunk.toString + + override def getAssertFailedNode: GeneralAssertionNode = + PermissionExhaleNode(id, chunk, term, sourceInfo, assumptionType, + mergeInfo, labelNode, joinInfos, memberStr, hasFailed = true) +} + +/** + * Label nodes are nodes used internally, mostly used to improve precision of the dependency analysis. + * By default, they get removed from the final graph. When debugging the DA is enabled, they are kept but still marked internal. + */ +case class LabelNode private[graph] (override val id: Int, override val term: Var, override val memberStr: String) extends GeneralAssumptionNode { + override val sourceInfo: DependencyAnalysisSourceInfo = NoDependencyAnalysisSourceInfo() + override val assumptionType: AssumptionType = AssumptionType.Internal + override val mergeInfo: DependencyAnalysisMergeInfo = NoDependencyAnalysisMerge() + val description: String = term.toString + override val joinInfos: List[SimpleDependencyAnalysisJoin] = List.empty + override def getNodeType: String = "Label" + override def getNodeString: String = "assume " + description +} + +/** + * Represents the fact that a branch has been found to be infeasible allowing us to distinguish between dependencies + * coming from the fact that the assertion is not reachable on a given path and dependencies used to prove the + * assertion on feasible paths. + * + * Infeasibility nodes should always depend on the proof of false. + * All subsequent assertions on the infeasible path should depend on the infeasibility node. + */ +case class InfeasibilityNode private[graph] (override val id: Int, override val sourceInfo: DependencyAnalysisSourceInfo, + override val assumptionType: AssumptionType, + override val memberStr: String) extends GeneralAssumptionNode { + override val term: Term = False + override val mergeInfo: DependencyAnalysisMergeInfo = NoDependencyAnalysisMerge() + val description: String = "False" + override val joinInfos: List[SimpleDependencyAnalysisJoin] = List.empty + + override def getNodeType: String = "Infeasible" + override def getNodeString: String = "infeasible" +} + +object DependencyNodeFactory { + + def createSimpleAssumptionNode(term: Term, description: Option[String], sourceInfo: DependencyAnalysisSourceInfo, + assumptionType: AssumptionType, mergeInfo: DependencyAnalysisMergeInfo, + joinInfos: List[SimpleDependencyAnalysisJoin], memberStr: String): SimpleAssumptionNode = + SimpleAssumptionNode(DependencyGraphHelper.nextId(), term, description, sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr) + + def createAxiomAssumptionNode(term: Term, description: Option[String], sourceInfo: DependencyAnalysisSourceInfo, + assumptionType: AssumptionType, mergeInfo: DependencyAnalysisMergeInfo, + joinInfos: List[SimpleDependencyAnalysisJoin], memberStr: String): AxiomAssumptionNode = + AxiomAssumptionNode(DependencyGraphHelper.nextId(), term, description, sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr) + + def createSimpleAssertionNode(term: Term, sourceInfo: DependencyAnalysisSourceInfo, assumptionType: AssumptionType, + mergeInfo: DependencyAnalysisMergeInfo, joinInfos: List[SimpleDependencyAnalysisJoin], + memberStr: String, hasFailed: Boolean = false): SimpleAssertionNode = + SimpleAssertionNode(DependencyGraphHelper.nextId(), term, sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr, hasFailed) + + def createSimpleCheckNode(term: Term, sourceInfo: DependencyAnalysisSourceInfo, assumptionType: AssumptionType, + mergeInfo: DependencyAnalysisMergeInfo, joinInfos: List[SimpleDependencyAnalysisJoin], + memberStr: String, hasFailed: Boolean = false): SimpleCheckNode = + SimpleCheckNode(DependencyGraphHelper.nextId(), term, sourceInfo, assumptionType, mergeInfo, joinInfos, memberStr, hasFailed) + + def createPermissionInhaleNode(chunk: Chunk, term: Term, sourceInfo: DependencyAnalysisSourceInfo, + assumptionType: AssumptionType, mergeInfo: DependencyAnalysisMergeInfo, + labelNode: LabelNode, joinInfos: List[SimpleDependencyAnalysisJoin], + memberStr: String): PermissionInhaleNode = + PermissionInhaleNode(DependencyGraphHelper.nextId(), chunk, term, sourceInfo, assumptionType, mergeInfo, labelNode, joinInfos, memberStr) + + def createPermissionExhaleNode(chunk: Chunk, term: Term, sourceInfo: DependencyAnalysisSourceInfo, + assumptionType: AssumptionType, mergeInfo: DependencyAnalysisMergeInfo, + labelNode: LabelNode, joinInfos: List[SimpleDependencyAnalysisJoin], + memberStr: String, hasFailed: Boolean = false): PermissionExhaleNode = + PermissionExhaleNode(DependencyGraphHelper.nextId(), chunk, term, sourceInfo, assumptionType, mergeInfo, labelNode, joinInfos, memberStr, hasFailed) + + def createLabelNode(term: Var, memberStr: String): LabelNode = + LabelNode(DependencyGraphHelper.nextId(), term, memberStr) + + def createInfeasibilityNode(sourceInfo: DependencyAnalysisSourceInfo, assumptionType: AssumptionType, memberStr: String): InfeasibilityNode = + InfeasibilityNode(DependencyGraphHelper.nextId(), sourceInfo, assumptionType, memberStr) +} diff --git a/src/main/scala/dependencyAnalysis/graph/DependencyGraph.scala b/src/main/scala/dependencyAnalysis/graph/DependencyGraph.scala new file mode 100644 index 000000000..80a60e59a --- /dev/null +++ b/src/main/scala/dependencyAnalysis/graph/DependencyGraph.scala @@ -0,0 +1,409 @@ +// 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.silicon.dependencyAnalysis.graph + +import viper.silver.dependencyAnalysis.{AssumptionType, DependencyAnalysisSourceInfo} + +import java.io.PrintWriter +import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicInteger +import scala.annotation.unused +import scala.collection.mutable + + +object DependencyGraphHelper { + private val idCounter: AtomicInteger = new AtomicInteger(0) + + /** + * Helper function used to ensure uniqueness of all dependency node ids. + */ + def nextId(): Int = { + idCounter.getAndIncrement() + } +} + +sealed trait DependencyGraphState +class Init extends DependencyGraphState +class IntraProcedural extends DependencyGraphState +class Final extends DependencyGraphState + +trait ReadOnlyDependencyGraph[T <: DependencyGraphState] { + def getNodes: Set[DependencyAnalysisNode] + def getAssumptionNodes: Set[GeneralAssumptionNode] + def getAssertionNodes: Set[GeneralAssertionNode] + + def getNodeById(id: Int): Option[DependencyAnalysisNode] + def getNodesByIds(targets: Set[Int]): Set[DependencyAnalysisNode] + + /** + * @return a map from node to the set of direct dependencies in the intraprocedural graph + */ + def getIntraMethodEdges: Map[Int, Set[Int]] + + /** + * @return all interprocedural downward edges in the graph as a map from node to all its direct downward dependencies. + * A downward edge connects a node representing the proof of a property to a node representing the assumption of + * said property in another verification component. + * For example, a downward edge may connect a postcondition with a corresponding method call. + */ + def getEdgesConnectingMethodsDownwards: Map[Int, Set[Int]] + + /** + * @return all interprocedural upwards edges in the graph as a map from node to all its direct upwards dependencies. + * An upwards edge connects a node justifying an assumption (by proving it) to a node representing the specification + * element depending on it in another verification component. + * For example, an upwards edge may connect a method call with a corresponding precondition. + */ + def getEdgesConnectingMethodsUpwards: Map[Int, Set[Int]] // e.g. edges connecting PREconditions with method/function calls + + /** + * @return a map from node to the set of direct dependencies, where all types of edges are included + */ + def getAllEdges: Map[Int, Set[Int]] + + /** + * @param includeUpwardEdges: if set to true, interprocedural upward edges are included in the result + * @param includeDownwardEdges: if set to true, interprocedural downward edges are included in the result + * @return a map from node to the set of direct dependencies + */ + def getAllEdges(includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Map[Int, Set[Int]] + + /** + * @param nodesToAnalyze the set of dependency nodes for which all dependencies should be computed + * @param includeInfeasibilityNodes if set to true, dependencies found via infeasibility nodes are included in the result + * @param includeUpwardEdges if set to true, interprocedural upward edges are taken into account + * @param includeDownwardEdges if set to true, interprocedural downward edges are taken into account + * @return the set of dependencies of the provided sources + */ + def computeDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] + + /** + * @param nodesToAnalyze the set of dependency nodes for which all dependents should be computed + * @param includeInfeasibilityNodes if set to true, dependents found via infeasibility nodes are included in the result + * @param includeUpwardEdges if set to true, interprocedural upward edges are taken into account + * @param includeDownwardEdges if set to true, interprocedural downward edges are taken into account + * @return the set of dependents of the provided sources + */ + def computeDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] + + /** + * @param nodesToAnalyze the set of dependency nodes for which the direct dependencies should be computed + * @param includeInfeasibilityNodes if set to true, dependencies found via infeasibility nodes are included in the result + * @param includeUpwardEdges if set to true, interprocedural upward edges are taken into account + * @param includeDownwardEdges if set to true, interprocedural downward edges are taken into account + * @return the set of direct dependencies of the provided sources + */ + def computeDirectDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] + + /** + * @param nodesToAnalyze the set of dependency nodes for which the direct dependents should be computed + * @param includeInfeasibilityNodes if set to true, dependents found via infeasibility nodes are included in the result + * @param includeUpwardEdges if set to true, interprocedural upward edges are taken into account + * @param includeDownwardEdges if set to true, interprocedural downward edges are taken into account + * @return the set of direct dependents of the provided targets + */ + def computeDirectDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] + + /** + * Exports the graph to the folder 'dirName'. + */ + def exportGraph(dirName: String): Unit +} + +class DependencyGraph[T <: DependencyGraphState] extends ReadOnlyDependencyGraph[T] { + private val intraMethodEdges: mutable.Map[Int, Set[Int]] = mutable.Map.empty + private val edgesConnectingMethodsDownwards: mutable.Map[Int, Set[Int]] = mutable.Map.empty // e.g. edges connecting POSTcondition with method/function calls + private val edgesConnectingMethodsUpwards: mutable.Map[Int, Set[Int]] = mutable.Map.empty // e.g. edges connecting PREconditions with method/function calls + private val allEdges: mutable.Map[Int, Set[Int]] = mutable.Map.empty // should contain exactly all edges from intraMethodEdges, edgesConnectingMethodsDownwards, edgesConnectingMethodsUpwards + private val vacuousProofs: mutable.Set[Int] = mutable.Set() + + private val assumptionNodes: mutable.Map[Int, GeneralAssumptionNode] = mutable.HashMap.empty + private val assertionNodes: mutable.Map[Int, GeneralAssertionNode] = mutable.HashMap.empty + + def getNodes: Set[DependencyAnalysisNode] = getAssumptionNodes ++ getAssertionNodes + def getAssumptionNodes: Set[GeneralAssumptionNode] = assumptionNodes.values.toSet + def getAssertionNodes: Set[GeneralAssertionNode] = assertionNodes.values.toSet + + def getIntraMethodEdges: Map[Int, Set[Int]] = intraMethodEdges.toMap + def getEdgesConnectingMethodsDownwards: Map[Int, Set[Int]] = edgesConnectingMethodsDownwards.toMap + def getEdgesConnectingMethodsUpwards: Map[Int, Set[Int]] = edgesConnectingMethodsUpwards.toMap + + def getNodeById(id: Int): Option[DependencyAnalysisNode] = assumptionNodes.get(id).orElse(assertionNodes.get(id)) + + def getNodesByIds(targets: Set[Int]): Set[DependencyAnalysisNode] = { + targets flatMap getNodeById + } + + def getAllEdges: Map[Int, Set[Int]] = { + allEdges.toMap + } + + def getAllEdges(includeDownwardEdges: Boolean, includeUpwardEdges: Boolean): Map[Int, Set[Int]] = { + if(includeUpwardEdges && includeDownwardEdges) getAllEdges // avoids expensive computation + else { + val intraMethodEdges = getIntraMethodEdges + val upwardEdges: mutable.Map[Int, Set[Int]] = if (includeUpwardEdges) edgesConnectingMethodsUpwards else mutable.Map.empty + val downwardEdges: mutable.Map[Int, Set[Int]] = if (includeDownwardEdges) edgesConnectingMethodsDownwards else mutable.Map.empty + val keys = intraMethodEdges.keySet ++ downwardEdges.keySet ++ upwardEdges.keySet + val allEdges = mutable.Map[Int, Set[Int]]() + keys foreach { key => + allEdges.update(key, intraMethodEdges.getOrElse(key, Set()) ++ downwardEdges.getOrElse(key, Set()) ++ upwardEdges.getOrElse(key, Set())) + } + allEdges.toMap + } + } + + @unused + def getVacuousProofs: Set[Int] = vacuousProofs.toSet // TODO ake: what to do with these? + + def addAssumptionNode(node: GeneralAssumptionNode): Unit = { + assumptionNodes.update(node.id, node) + } + + def addAssumptionNodes(newNodes: Iterable[GeneralAssumptionNode]): Unit = { + newNodes foreach addAssumptionNode + } + + def addNode(node: DependencyAnalysisNode): Unit = { + node match { + case n: GeneralAssertionNode => addAssertionNode(n) + case n: GeneralAssumptionNode => addAssumptionNode(n) + } + } + + def addAssertionNode(node: GeneralAssertionNode): Unit = { + assertionNodes.update(node.id, node) + } + + def addAssertionNodes(newNodes: Iterable[GeneralAssertionNode]): Unit = { + newNodes foreach addAssertionNode + } + + def addEdges(source: Int, targets: Set[Int]): Unit = { + addEdges(Set(source), targets) + } + + def addEdges(sources: Set[Int], target: Int): Unit = { + addToAllEdges(sources, target) + val oldSources = intraMethodEdges.getOrElse(target, Set.empty) + val newSources = sources.filter(_ != target) + if (newSources.nonEmpty) + intraMethodEdges.update(target, oldSources ++ newSources) + } + + private def addToAllEdges(sources: Set[Int], target: Int): Unit = { + val oldSources = allEdges.getOrElse(target, Set.empty) + val newSources = sources.filter(_ != target) + if (newSources.nonEmpty) + allEdges.update(target, oldSources ++ newSources) + } + + def addEdges(sources: Set[Int], targets: Set[Int]): Unit = { + targets foreach (addEdges(sources, _)) + } + + def addEdgesConnectingMethodsDownwards(sources: Set[Int], target: Int): Unit = { + addToAllEdges(sources, target) + val oldSources = edgesConnectingMethodsDownwards.getOrElse(target, Set.empty) + val newSources = sources.filter(_ != target) + if (newSources.nonEmpty) + edgesConnectingMethodsDownwards.update(target, oldSources ++ newSources) + } + + def addEdgesConnectingMethodsDownwards(sources: Set[Int], targets: Set[Int]): Unit = { + targets foreach (addEdgesConnectingMethodsDownwards(sources, _)) + } + + def addEdgesConnectingMethodsDownwards(source: Int, targets: Set[Int]): Unit = { + addEdgesConnectingMethodsDownwards(Set(source), targets) + } + + def addEdgesConnectingMethodsUpwards(sources: Set[Int], target: Int): Unit = { + addToAllEdges(sources, target) + val oldSources = edgesConnectingMethodsUpwards.getOrElse(target, Set.empty) + val newSources = sources.filter(_ != target) + if (newSources.nonEmpty) + edgesConnectingMethodsUpwards.update(target, oldSources ++ newSources) + } + + def addEdgesConnectingMethodsUpwards(sources: Set[Int], targets: Set[Int]): Unit = { + targets foreach (addEdgesConnectingMethodsUpwards(sources, _)) + } + + def addEdgesConnectingMethodsUpwards(source: Int, targets: Set[Int]): Unit = { + addEdgesConnectingMethodsUpwards(Set(source), targets) + } + + + def addVacuousProof(assertionId: Int): Unit = { + vacuousProofs.add(assertionId) + } + + def computeDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] = { + val infeasibilityNodeIds: Set[Int] = if (includeInfeasibilityNodes) Set.empty else getAssumptionNodes filter (_.isInstanceOf[InfeasibilityNode]) map (_.id) + val visited: mutable.Set[Int] = mutable.Set.empty + val queue: mutable.Queue[Int] = mutable.Queue(nodesToAnalyze.map(_.id).toSeq: _*) + val allEdges = getAllEdges(includeDownwardEdges, includeUpwardEdges) + while(queue.nonEmpty) { + val curr = queue.dequeue() + val newVisits = allEdges.getOrElse(curr, Set()).diff(infeasibilityNodeIds) + visited.add(curr) + queue.enqueueAll(newVisits.diff(visited).diff(queue.toSet)) + } + (visited flatMap getNodeById).toSet + } + + def computeDirectDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] = { + val depIds = nodesToAnalyze.groupBy(_.sourceInfo).flatMap { case (sourceInfo, nodes) => + getDirectDependenciesInternal(nodes.map(_.id).toList, sourceInfo, includeInfeasibilityNodes, includeUpwardEdges, includeDownwardEdges) + } + (depIds flatMap getNodeById).toSet + } + + private def getDirectDependenciesInternal(initQueue: List[Int], targetSourceInfo: DependencyAnalysisSourceInfo, includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[Int] = { + val infeasibilityNodeIds: Set[Int] = if (includeInfeasibilityNodes) Set.empty else getAssumptionNodes.filter(_.isInstanceOf[InfeasibilityNode]).map(_.id) + val targetIds: Set[Int] = initQueue.toSet + val sourceInfoNodeIds: Set[Int] = getNodes.filter(_.sourceInfo == targetSourceInfo).map(_.id) + assert(targetIds.subsetOf(sourceInfoNodeIds), s"Target ids do not all belong to sourceInfo $targetSourceInfo") + val visited: mutable.Set[Int] = mutable.Set.empty + val result: mutable.Set[Int] = mutable.Set.empty + var queue: List[Int] = initQueue + val allEdges = getAllEdges(includeDownwardEdges, includeUpwardEdges) + while (queue.nonEmpty) { + val curr = queue.head + val newVisits = allEdges.getOrElse(curr, Set()).diff(infeasibilityNodeIds).diff(visited) + val newQueues = newVisits.intersect(sourceInfoNodeIds) + visited.addAll(newVisits) + result.addAll(newVisits.diff(newQueues)) + queue = queue.tail ++ newQueues.diff(queue.toSet) + } + result.toSet + } + + def computeDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] = { + val infeasibilityNodeIds: Set[Int] = if (includeInfeasibilityNodes) Set.empty else getAssumptionNodes filter (_.isInstanceOf[InfeasibilityNode]) map (_.id) + val visited: mutable.Set[Int] = mutable.Set.empty + var queue: Set[Int] = nodesToAnalyze.map(_.id) + val allEdges = getAllEdges(includeDownwardEdges, includeUpwardEdges) + while(queue.nonEmpty) { + val newVisits = allEdges.filter{ case (t, s) => s.intersect(queue).nonEmpty && !infeasibilityNodeIds.contains(t) }.keys.toSet + visited.addAll(queue) + queue = newVisits.diff(visited) + } + (visited flatMap getNodeById).toSet + } + + def computeDirectDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean, includeUpwardEdges: Boolean, includeDownwardEdges: Boolean): Set[DependencyAnalysisNode] = { + val infeasibilityNodeIds: Set[Int] = if (includeInfeasibilityNodes) Set.empty else getAssumptionNodes filter (_.isInstanceOf[InfeasibilityNode]) map (_.id) + val visited: mutable.Set[Int] = mutable.Set.empty + val result: mutable.Set[Int] = mutable.Set.empty + var queue: Set[Int] = nodesToAnalyze.map(_.id) + val sourceSourceInfos = nodesToAnalyze.map(_.sourceInfo) + val allEdges = getAllEdges(includeDownwardEdges, includeUpwardEdges) + while(queue.nonEmpty) { + visited.addAll(queue) + val newVisits = allEdges.filter{case (t, s) => s.intersect(queue).nonEmpty && !infeasibilityNodeIds.contains(t)}.keys.toSet.diff(visited) + val newQueues = getNodesByIds(newVisits).filter(n => sourceSourceInfos.contains(n.sourceInfo)).map(_.id) + result.addAll(newVisits.diff(newQueues)) + queue = newQueues.diff(visited) + } + (result flatMap getNodeById).toSet + } + + /** + * Removes the provided nodes while perceiving the transitive closure by adding edges between the predecessors and successors. + */ + private def removeAllEdgesForNode(node: DependencyAnalysisNode): Unit = { + val id = node.id + + def removeEdgesFromMap(edges: mutable.Map[Int, Set[Int]]): Unit = { + val predecessors = (edges filter { case (_, t) => t.contains(id) }).keys + val successors = edges.getOrElse(id, Set.empty) + edges.remove(id) + predecessors foreach (pid => edges.update(pid, edges.getOrElse(pid, Set.empty).filter(_ != id) ++ successors)) + } + + removeEdgesFromMap(intraMethodEdges) + removeEdgesFromMap(edgesConnectingMethodsDownwards) + removeEdgesFromMap(edgesConnectingMethodsUpwards) + removeEdgesFromMap(allEdges) + } + + + /** + * Removes all label nodes while perceiving the transitive closure by adding edges between the predecessors and successors. + */ + def removeLabelNodes(): Unit = { + def filterCriteria(n: DependencyAnalysisNode) = n.isInstanceOf[LabelNode] + + val nodesToRemove = getAssumptionNodes filter filterCriteria + nodesToRemove foreach removeAllEdgesForNode + removeNodes(nodesToRemove.map(_.id)) + } + + /** + * Removes internal nodes while perceiving the transitive closure by adding edges between the predecessors and successors. + */ + def removeInternalNodes(): Unit = { + def filterCriteria(n: DependencyAnalysisNode) = { + n.assumptionType.isInstanceOf[AssumptionType.InternalType] && + !n.isInstanceOf[InfeasibilityNode] && // infeasibility nodes are required for some of the graph queries + n.joinInfos.isEmpty // join nodes must not be removed because we need them for joining the graphs later on + } + + val nodesToRemove = getNodes filter filterCriteria + nodesToRemove foreach removeAllEdgesForNode + removeNodes(nodesToRemove.map(_.id)) + } + + private def removeNodes(nodeIds: Iterable[Int]) = { + nodeIds map assumptionNodes.remove + nodeIds map assertionNodes.remove + } + + def exportGraph(dirName: String): Unit = { + val directory = Paths.get(dirName).toFile + directory.mkdirs() + exportNodes(dirName + "/nodes.csv") + exportEdges(dirName + "/edges.csv") + } + + private def exportEdges(fileName: String): Unit = { + val builder = new StringBuilder() + getIntraMethodEdges foreach (e => e._2 foreach (s => builder.append(s.toString + "," + e._1.toString + ",direct" + "\n"))) + getEdgesConnectingMethodsDownwards foreach (e => e._2 foreach (s => builder.append(s.toString + "," + e._1.toString + ",interprocedural downward" + "\n"))) + getEdgesConnectingMethodsUpwards foreach (e => e._2 foreach (s => builder.append(s.toString + "," + e._1.toString + ",interprocedural upward" + "\n"))) + + val writer = new PrintWriter(fileName) + writer.println("source,target,label") + writer.println(builder.toString()) + writer.close() + } + + private def exportNodes(fileName: String): Unit = { + val sep = "#" + def getNodeExportString(node: DependencyAnalysisNode): String = { + val hasFailed = node match { + case node: GeneralAssertionNode => node.hasFailed + case _ => false + } + val parts = mutable.Seq(node.id.toString, node.getNodeType, node.toString, node.getNodeString, node.sourceInfo.toString, + node.sourceInfo.getPositionString, node.mergeInfo.toString, node.sourceInfo.getDescription, node.memberStr, hasFailed.toString) + parts.map(_.replace("#", "@")).mkString(sep) + } + val headerParts = mutable.Seq("id", "node type", "assumption type", "node info", "source info", "position", "merge info", "description", "member name", "failed?") + val builder = new StringBuilder() + getNodes foreach (n => builder.append(getNodeExportString(n).replace("\n", " ") + "\n")) + + val writer = new PrintWriter(fileName) + writer.println(headerParts.mkString(sep)) + writer.println(builder.result()) + writer.close() + } +} + + diff --git a/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyAnalysisProgressSupporter.scala b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyAnalysisProgressSupporter.scala new file mode 100644 index 000000000..233700b00 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyAnalysisProgressSupporter.scala @@ -0,0 +1,256 @@ +// 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.silicon.dependencyAnalysis.graphInterpretation + +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DATraversalMode.DATraversalMode +import viper.silver.dependencyAnalysis +import viper.silver.dependencyAnalysis.AssumptionType.PreconditionType +import viper.silver.dependencyAnalysis.{AssumptionType, DependencyAnalysisSourceInfo} + +import scala.collection.mutable + +class DependencyAnalysisProgressSupporter[T <: DependencyGraphState](interpreter: DependencyGraphInterpreter[T]) { + + private val dependencyGraph = interpreter.getGraph + private lazy val sourceToAssertionNodesMap: Map[DependencyAnalysisSourceInfo, Set[GeneralAssertionNode]] = interpreter.getNonInternalAssertionNodes.groupBy(_.sourceInfo) + + def computeVerificationProgress(enableDebugging: Boolean = false): (VerificationProgress, VerificationProgress) = { + computeVerificationProgressOptimized(enableDebugging) + } + + /** + * Computes all dependencies of a given dependency node. Intermediate results are cached at procedure-boundaries. + * That is, the dependencies of each pre- and postcondition are cached and can thus be reused for subsequent computations. + * We do not cache intraprocedural dependencies to allow for precise computation of dependencies using the low-level graph. + */ + val deps: DAMemo[(DependencyAnalysisSourceInfo, DATraversalMode), Set[CompactUserLevelDependencyAnalysisNode]] = DAMemo { case (assertionNode, mode) => + // TODO ake: maybe this should be moved to the DependencyGraphInterpreter such that other queries can be optimized as well. + def computeDependencies(currentNode: DependencyAnalysisSourceInfo, visited: Set[(DependencyAnalysisSourceInfo, DATraversalMode)], traversalMode: DATraversalMode): Set[CompactUserLevelDependencyAnalysisNode] = { + if (visited.contains((currentNode, traversalMode))) { + Set.empty // break cycles to avoid infinite loops + } else if (deps.contains(currentNode, traversalMode)) { + deps((currentNode, traversalMode)) + } else { + val updatedVisited = visited ++ Set((currentNode, traversalMode)) + val allNonInternalAssertions = sourceToAssertionNodesMap.getOrElse(currentNode, Set.empty) + + // compute intraprocedural dependencies without caching any intermediate results + val intraMethodDependencies = dependencyGraph.computeDependencies(allNonInternalAssertions.toSet, includeInfeasibilityNodes = true, includeUpwardEdges = false, includeDownwardEdges = false) + val nonInternalIntraMethodDependencies = intraMethodDependencies.filter(interpreter.isNonInternalAssumptionNode) + + // recursively compute all interprocedural dependencies and cache results at procedure-boundaries + val relevantInterProceduralEdges = traversalMode match { + case DATraversalMode.Upwards => dependencyGraph.getEdgesConnectingMethodsUpwards + case DATraversalMode.Downwards => dependencyGraph.getEdgesConnectingMethodsDownwards + } + val interProceduralNodeIds = intraMethodDependencies.flatMap(n => relevantInterProceduralEdges.getOrElse(n.id, Set.empty)) + val interProceduralNodes = interProceduralNodeIds.flatMap(interpreter.getGraph.getNodeById) + val interProceduralDependencies = interProceduralNodes.map(_.sourceInfo).flatMap(node => computeDependencies(node, updatedVisited, traversalMode)) + + // put together all identified dependencies and cache the result + val result = reduceCompactUserLevelNodes(toCompactUserLevelNodes(nonInternalIntraMethodDependencies ++ interProceduralNodes) ++ interProceduralDependencies).filterNot(_.source.equals(currentNode)) + deps.put((currentNode, traversalMode), result) + result + } + } + + computeDependencies(assertionNode, Set.empty, mode) + } + + // merges results of several computations by merging low-level nodes belonging to the same source + private def reduceCompactUserLevelNodes(inputNodes: Iterable[CompactUserLevelDependencyAnalysisNode]): Set[CompactUserLevelDependencyAnalysisNode] = { + inputNodes.foldLeft(Map.empty[DependencyAnalysisSourceInfo, CompactUserLevelDependencyAnalysisNode]) { case (resultMap, node) => + val updatedNode = resultMap.get(node.source) match { + case Some(existing) => + CompactUserLevelDependencyAnalysisNode( + source = node.source, + assumptionTypes = existing.assumptionTypes ++ node.assumptionTypes, + assertionTypes = existing.assertionTypes ++ node.assertionTypes, + hasFailures = existing.hasFailures || node.hasFailures + ) + case None => node + } + resultMap.updated(node.source, updatedNode) + }.values.toSet + } + + private def toCompactUserLevelNodes(lowLevelNodes: Set[DependencyAnalysisNode]): Set[CompactUserLevelDependencyAnalysisNode] = { + lowLevelNodes.groupBy(_.sourceInfo).map { case (source, nodes) => + val assertionNodes = nodes.collect { case node: GeneralAssertionNode => node } + CompactUserLevelDependencyAnalysisNode(source, + nodes.collect { case node: GeneralAssumptionNode => node.assumptionType }, + assertionNodes.map(_.assumptionType), + assertionNodes.exists(_.hasFailed) + )}.toSet + } + + /** + * Computes the assertion quality given an assertion node and all of its dependencies. + * It returns None if the assertion trivially verifies (it has no dependencies). Otherwise, it returns the assertion quality. + * If the assertion encountered a failure, i.e. it could not be proven to hold, then the assertion quality is 0.0. + * Otherwise, the assertion quality is defined as the fraction of non-assumption dependencies over all dependencies. + */ + private def computeAssertionQuality(allDependencies: Set[CompactUserLevelDependencyAnalysisNode], assertion: DependencyAnalysisSourceInfo): Option[Double] = { + val assertionNodes = sourceToAssertionNodesMap.getOrElse(assertion, Set.empty).collect { case node: GeneralAssertionNode => node } + val failedAssertionNodes = assertionNodes.filter(node => node.hasFailed || node.assumptionType.equals(AssumptionType.ExplicitPostcondition)) + // assertions with failures have quality of 0.0 + if (failedAssertionNodes.nonEmpty) + Some(0.0) + else if (allDependencies.isEmpty) + None // we filter out trivial assertions, e.g. assertions that do not have any dependencies + else { + val explicitDeps = allDependencies.filter(_.assumptionTypes.exists(_.isInstanceOf[dependencyAnalysis.AssumptionType.ExplicitAssumptionType])).map(_.source) + val numDepsTotal = allDependencies.size + Some((numDepsTotal - explicitDeps.size).toDouble / numDepsTotal.toDouble) + } + } + + private def getAssertionsRelevantForProgress: Map[DependencyAnalysisSourceInfo, Set[GeneralAssertionNode]] = { + sourceToAssertionNodesMap.filter { + case (_, assertionNodes) => !assertionNodes.exists(node => + node.assumptionType.isInstanceOf[AssumptionType.ImportedType] || node.assumptionType.isInstanceOf[PreconditionType]) + } + } + + /** + * @return the verification progress of the entire program. + * Verification progress is defined as the product of specification quality and proof quality. + * Specification quality is the fraction of source code statements that is a dependency of any proof obligation. + * Proof quality is defined as the average assertion quality over all proof obligations. + * Assertion quality of an assertion a is the fraction of non-assumption dependencies over all dependencies of the assertion a. + */ + def computeVerificationProgressOptimized(enableDebugOutput: Boolean = false): (VerificationProgress, VerificationProgress) = { + + // compute all dependencies of each proof obligation + val allAssertions = getAssertionsRelevantForProgress.keySet.toList + val allDependenciesPerAssertionNode = allAssertions map (ass => ({ + val ups = deps((ass, DATraversalMode.Upwards)) + val downs = deps((ass, DATraversalMode.Downwards)) + reduceCompactUserLevelNodes(ups ++ downs) + }, ass)) + + val specQuality = computeSpecQuality(allDependenciesPerAssertionNode.flatMap(_._1).toSet, enableDebugOutput) + + val assertionQualities = allDependenciesPerAssertionNode map (ass => (computeAssertionQuality(ass._1, ass._2), ass._2)) collect { case (Some(q), s) => (q, s) } + val numNonTrivialAssertions = assertionQualities.size + + // compute Peter's proof quality + val fullyVerifiedAssertions = assertionQualities.filter(_._1 == 1.0) + val proofQualityPeter = if (numNonTrivialAssertions > 0) fullyVerifiedAssertions.size.toDouble / numNonTrivialAssertions.toDouble else 1.0 + + // compute Lea's proof quality + val assertionQualitiesSum = assertionQualities.map(_._1).sum + val proofQualityLea = if (numNonTrivialAssertions > 0) assertionQualitiesSum / numNonTrivialAssertions.toDouble else 1.0 + + if (enableDebugOutput) + println( + s"fullyVerifiedAssertions:\n\t${fullyVerifiedAssertions.sortBy(n => (n._2.getLineNumber, n._2.toString)).mkString("\n\t")}\n" + + s"assertionQualitiesSum:\n\t${assertionQualities.sortBy(n => (n._2.getLineNumber, n._2.toString)).mkString("\n\t")}" + ) + + println( + s"specQuality = $specQuality\n" + + s"proof quality (Peter): ${fullyVerifiedAssertions.size} / $numNonTrivialAssertions = $proofQualityPeter\n" + + s"proof quality (Lea): $assertionQualitiesSum / $numNonTrivialAssertions = $proofQualityLea\n" + ) + + val progressPeter = VerificationProgress(specQuality, proofQualityPeter) + val progressLea = VerificationProgress(specQuality, proofQualityLea) + (progressPeter, progressLea) + } + + + /** + * Computes the specification quality given the covered nodes. Specification quality is defined as the fraction of + * all (user-level) nodes in the graph that are covered. + */ + private def computeSpecQuality(coveredNodes: Set[CompactUserLevelDependencyAnalysisNode], enableDebugOutput: Boolean = false): Double = { + + val explicitAssertions = toCompactUserLevelNodes(interpreter.getExplicitAssertionNodes.toSet) + val allSourceCodeNodes = toCompactUserLevelNodes(interpreter.getNonInternalAssumptionNodes).filter(n => n.assumptionTypes.exists(_.isInstanceOf[AssumptionType.SourceCodeType])).map(_.source).diff(explicitAssertions.map(_.source)) + + if (allSourceCodeNodes.isEmpty) + 1.0 + else { + val coveredSourceCodeNodes = coveredNodes.map(_.source).intersect(allSourceCodeNodes) + if (enableDebugOutput) + println( + s"Covered Source Code:\n\t${coveredSourceCodeNodes.toList.sortBy(n => (n.getLineNumber, n.toString())).mkString("\n\t")}\n" + + s"Uncovered Source Code:\n\t${allSourceCodeNodes.diff(coveredSourceCodeNodes).toList.sortBy(n => (n.getLineNumber, n.toString())).mkString("\n\t")}" + ) + + println(s"Spec Quality = ${coveredSourceCodeNodes.size} / ${allSourceCodeNodes.size}") + coveredSourceCodeNodes.size.toDouble / allSourceCodeNodes.size.toDouble + } + } + + + /** + * @return a list of assumption nodes ordered by their impact on proof quality. + */ + def computeAssumptionRanking(): List[(String, Double)] = { + val allAssertions = interpreter.toUserLevelNodes(getAssertionsRelevantForProgress.values.flatten) + + val relevantDependenciesPerAssertion = allAssertions + .map(ass => (ass, interpreter.toUserLevelNodes(interpreter.computeNonInternalDependencies(ass.lowerLevelNodes)).diffBySource(Set(ass)))).toMap + .filter { case (assertion, assumptions) => assumptions.nonEmpty || assertion.hasFailures || assertion.assertionTypes.contains(AssumptionType.ExplicitPostcondition) } + val numAssertions = relevantDependenciesPerAssertion.size.toDouble + + val assumptionImpacts = relevantDependenciesPerAssertion.toList.flatMap { case (_, assumptions) => + val explicitDeps = UserLevelDependencyAnalysisNode.extractByAssumptionType(assumptions, _.isInstanceOf[AssumptionType.ExplicitAssumptionType]) + explicitDeps.map(node => (node.source, 1.0 / assumptions.size / numAssertions)).toList + } + + val unverifiedAssertionImpacts = getAssertionsWithZeroQuality.map(assertion => (assertion, 1.0 / numAssertions)).toList + + val totalImpacts1 = (assumptionImpacts ++ unverifiedAssertionImpacts).groupBy(_._1) + val totalImpacts = totalImpacts1.map { case (assumption, impacts) => (assumption.toString, impacts.map(_._2).sum) }.toList + + totalImpacts.sortBy(_._2).reverse + } + + private def getAssertionsWithZeroQuality: Set[DependencyAnalysisSourceInfo] = { + val allAssertions = interpreter.toUserLevelNodes(interpreter.getNonInternalAssertionNodes) + allAssertions.filter(assertion => assertion.hasFailures || assertion.assertionTypes.contains(AssumptionType.ExplicitPostcondition)).toSourceSet() + } + + /** + * Returns all uncovered source code statements. + */ + def computeUncoveredStatementsPerMember(): Map[String, List[DependencyAnalysisSourceInfo]] = { + val allAssertions = interpreter.toUserLevelNodes(getAssertionsRelevantForProgress.values.flatten) + val allDependencies = allAssertions.flatMap(ass => interpreter.toUserLevelNodes(interpreter.computeNonInternalDependencies(ass.lowerLevelNodes)).diffBySource(Set(ass))).toSourceMemberSet() + + val explicitAssertions = interpreter.toUserLevelNodes(interpreter.getExplicitAssertionNodes) + val allNodes = interpreter.toUserLevelNodes(interpreter.getNonInternalAssumptionNodes) + val allSourceCodeStmts = UserLevelDependencyAnalysisNode.extractByAssumptionType(allNodes, _.isInstanceOf[AssumptionType.SourceCodeType]).toSourceMemberSet().diff(explicitAssertions.toSourceMemberSet()) + + val uncoveredSourceCodeStmts = allSourceCodeStmts.diff(allDependencies) + uncoveredSourceCodeStmts.groupBy(_._2).map { case (member, sources) => + (member, sources.map(_._1).toList.sortBy(_.getLineNumber)) + } + } +} + +case class DAMemo[A,B](f: A => B) extends (A => B) { + private val cache = mutable.Map.empty[A, B] + def apply(x: A): B = cache getOrElseUpdate (x, f(x)) + + def put(a: A, b: B): Option[B] = { + cache.put(a, b) + } + + def contains(a: A): Boolean = { + cache.contains(a) + } +} + +case class VerificationProgress(specQuality: Double, proofQuality: Double) { + lazy val progress: Double = specQuality * proofQuality +} diff --git a/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyAnalysisPruningSupporter.scala b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyAnalysisPruningSupporter.scala new file mode 100644 index 000000000..72b47c990 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyAnalysisPruningSupporter.scala @@ -0,0 +1,128 @@ +// 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.silicon.dependencyAnalysis.graphInterpretation + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silver.ast +import viper.silver.ast.utility.ViperStrategy +import viper.silver.ast.utility.rewriter.Traverse +import viper.silver.ast.{If, Stmt} +import viper.silver.dependencyAnalysis.DependencyAnalysisSourceInfo + +import java.io.PrintWriter + +class DependencyAnalysisPruningSupporter[T <: DependencyGraphState](interpreter: DependencyGraphInterpreter[T]) { + + /** + * Prunes the program. That is, all statements and expressions that are not a crucial node are removed from the program. + */ + def getPrunedProgram(crucialNodes: Set[DependencyAnalysisNode], program: ast.Program): (ast.Program, Double) = { + + def isCrucialExp(exp: ast.Exp, crucialNodePositionStrings: Set[String]): Boolean = { + crucialNodePositionStrings.contains(DependencyAnalysisSourceInfo.extractPositionString(exp.pos)) // TODO ake: currently we compare only lines not columns! + } + + def isCrucialStmt(stmt: ast.Stmt, crucialNodePositionStrings: Set[String]): Boolean = { + crucialNodePositionStrings.contains(DependencyAnalysisSourceInfo.extractPositionString(stmt.pos)) + } + + val crucialNodeSourceInfos = crucialNodes.map(_.sourceInfo) + val crucialNodePositionStrings = crucialNodeSourceInfos.map(_.getPositionString) + var total = 0 + var removed = 0 + var nonDetermBoolCount = 0 + + def getNextNonDetermBool: String = { + nonDetermBoolCount += 1 + s"nonDetermBool_$nonDetermBoolCount" + } + + val newProgram: ast.Program = ViperStrategy.Slim({ + case s@(_: ast.Seqn | _: ast.Goto) => s + case domain@ast.Domain(name, functions, axioms, typVars, interpretations) => + val newAxioms = axioms.filter(a => + crucialNodePositionStrings.contains(DependencyAnalysisSourceInfo.extractPositionString(a.exp.pos)) || + crucialNodePositionStrings.contains(DependencyAnalysisSourceInfo.extractPositionString(a.pos))) + ast.Domain(name, functions, newAxioms, typVars, interpretations)(domain.pos, domain.info, domain.errT) + case function@ast.Function(name, formalArgs, typ, pres, posts, body) => + val newPres = pres.filter(isCrucialExp(_, crucialNodePositionStrings)) + val newPosts = posts.filter(isCrucialExp(_, crucialNodePositionStrings)) + val newBody = body.filter(isCrucialExp(_, crucialNodePositionStrings)) + ast.Function(name, formalArgs, typ, newPres, newPosts, newBody)(function.pos, function.info, function.errT) + case meth@ast.Method(name, inVars, outVars, pres, posts, body) => + val newPres = pres.filter(isCrucialExp(_, crucialNodePositionStrings)) + val newPosts = posts.filter(isCrucialExp(_, crucialNodePositionStrings)) + total += pres.size + posts.size + removed += (pres.size - newPres.size) + (posts.size - newPosts.size) + ast.Method(name, inVars, outVars, newPres, newPosts, body)(meth.pos, meth.info, meth.errT) + case ifStmt@ast.If(cond, thenBody, elseBody) if !isCrucialExp(cond, crucialNodePositionStrings) => + total += 1 + removed += 1 + val nonDetermBool = getNextNonDetermBool + ast.Seqn(Seq( + ast.LocalVarDeclStmt(ast.LocalVarDecl(nonDetermBool, ast.Bool)())(), + ast.If(ast.LocalVar(nonDetermBool, ast.Bool)(cond.pos, cond.info, cond.errT), thenBody, elseBody)(ifStmt.pos, ifStmt.info, ifStmt.errT)) + , Seq())(ifStmt.pos, ifStmt.info, ifStmt.errT) + case ifStmt: If => + total += 1 + ifStmt + case whileStmt@ast.While(cond, invs, body) if !isCrucialExp(cond, crucialNodePositionStrings) => + val newInvs = invs.filter(isCrucialExp(_, crucialNodePositionStrings)) + total += 1 + invs.size + removed += 1 + (invs.size - newInvs.size) + val nonDetermBool = getNextNonDetermBool + ast.Seqn(Seq( + ast.LocalVarDeclStmt(ast.LocalVarDecl(nonDetermBool, ast.Bool)())(), + ast.While(ast.LocalVar(nonDetermBool, ast.Bool)(cond.pos, cond.info, cond.errT), newInvs, body)(whileStmt.pos, whileStmt.info, whileStmt.errT)) + , Seq())(whileStmt.pos, whileStmt.info, whileStmt.errT) + case whileStmt@ast.While(cond, invs, body) => + val newInvs = invs.filter(isCrucialExp(_, crucialNodePositionStrings)) + total += 1 + invs.size + removed += (invs.size - newInvs.size) + ast.While(cond, newInvs, body)(whileStmt.pos, whileStmt.info, whileStmt.errT) + case label@ast.Label(name, invs) => + val newInvs = invs.filter(isCrucialExp(_, crucialNodePositionStrings)) + total += 1 + invs.size + removed += (invs.size - newInvs.size) + ast.Label(name, newInvs)(label.pos, label.info, label.errT) + case s: ast.Package if !isCrucialStmt(s, crucialNodePositionStrings) => + total += 1 + removed += 1 + ast.Inhale(ast.TrueLit()(s.pos, s.info, s.errT))(s.pos, s.info, s.errT) + case s: Stmt if !isCrucialStmt(s, crucialNodePositionStrings) => + total += 1 + removed += 1 + ast.Inhale(ast.TrueLit()(s.pos, s.info, s.errT))(s.pos, s.info, s.errT) + case s: Stmt => + total += 1 + s + }, Traverse.BottomUp).execute(program) + (newProgram, removed.toDouble / total.toDouble) + } + + private def getCrucialNodes(queriedNodes: Set[DependencyAnalysisNode]): Set[DependencyAnalysisNode] = { + val dependencies = interpreter.computeNonInternalDependencies(queriedNodes) + queriedNodes ++ dependencies + } + + /** + * Prunes the given program with respect to the queries nodes. That is, all statements and expressions that are neither + * one of the queried nodes nor a dependencies of a queried node are removed from the program. The program is exported + * to the given export file. + */ + def pruneProgramAndExport(queriedNodes: Set[DependencyAnalysisNode], program: ast.Program, exportFileName: String): Unit = { + val writer = new PrintWriter(exportFileName) + + val crucialNodes = getCrucialNodes(queriedNodes) + println(s"Found ${crucialNodes.size} crucial nodes. Pruning...") + + val (newProgram, pruningFactor) = getPrunedProgram(crucialNodes, program) + writer.println("// pruning factor: " + pruningFactor) + writer.println(newProgram.toString()) + writer.close() + } +} diff --git a/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyGraphInterpreter.scala b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyGraphInterpreter.scala new file mode 100644 index 000000000..f82527fed --- /dev/null +++ b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyGraphInterpreter.scala @@ -0,0 +1,151 @@ +// 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.silicon.dependencyAnalysis.graphInterpretation + +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.interfaces.Failure +import viper.silver.ast +import viper.silver.ast.Program +import viper.silver.dependencyAnalysis.{AssumptionType, JoinType} + +import java.io.PrintWriter +import java.nio.file.Paths + +object DATraversalMode extends Enumeration { + type DATraversalMode = Value + val Upwards, Downwards = Value +} + +class DependencyGraphInterpreter[T <: DependencyGraphState](name: String, dependencyGraph: ReadOnlyDependencyGraph[T], errors: List[Failure], member: Option[ast.Member]=None) { + val pruningSupporter: DependencyAnalysisPruningSupporter[T] = new DependencyAnalysisPruningSupporter[T](this) + val progressSupporter: DependencyAnalysisProgressSupporter[T] = new DependencyAnalysisProgressSupporter[T](this) + + + def getGraph: ReadOnlyDependencyGraph[T] = dependencyGraph + + def getName: String = name + + def getMember: Option[ast.Member] = member + + def getNodes: Set[DependencyAnalysisNode] = dependencyGraph.getNodes + + def getAssumptionNodes: Set[GeneralAssumptionNode] = dependencyGraph.getAssumptionNodes + + def getAssertionNodes: Set[GeneralAssertionNode] = dependencyGraph.getAssertionNodes + + def getErrors: List[Failure] = errors + + // TODO ake: join nodes are not needed for the final graph. Maybe we can outsource this to a dedicated intraprocedural graph interpreter class. + val joinSinkNodes: Set[DependencyAnalysisNode] = getJoinCandidateNodes(getNodes).filter(_.joinInfos.exists(_.joinType.equals(JoinType.Sink))) + val joinSourceNodes: Set[DependencyAnalysisNode] = getJoinCandidateNodes(getNodes).filter(_.joinInfos.exists(_.joinType.equals(JoinType.Source))) + + private def getJoinCandidateNodes(nodes: Set[DependencyAnalysisNode]): Set[DependencyAnalysisNode] = nodes.filter(node => node.joinInfos.nonEmpty) + + def toUserLevelNodes(nodes: Iterable[DependencyAnalysisNode]): Set[UserLevelDependencyAnalysisNode] = UserLevelDependencyAnalysisNode.from(nodes) + + def getNodesByLine(line: Int): Set[DependencyAnalysisNode] = + getNodes.filter(isVisibleNode).filter(node => node.sourceInfo.getLineNumber.contains(line)) + + def getNodesByPosition(file: String, line: Int): Set[DependencyAnalysisNode] = + getNodes.filter(isVisibleNode).filter(node => node.sourceInfo.getLineNumber.contains(line) && node.sourceInfo.getPositionString.startsWith(file + ".")) + + + def getNodesByLabel(label: String): Set[DependencyAnalysisNode] = { + val fullAnnotation = ("""@label\(\s*"?""" + java.util.regex.Pattern.quote(label) + """"?\s*\)""").r + getNodes.filter(node => fullAnnotation.findFirstIn(node.toString).isDefined) + } + + def computeDirectDependencies(nodesToAnalyze: Set[DependencyAnalysisNode]): Set[DependencyAnalysisNode] = { + val result = dependencyGraph.computeDirectDependencies(nodesToAnalyze, includeInfeasibilityNodes = true, includeUpwardEdges = true, includeDownwardEdges = true) + result filter isNonInternalAssumptionNode + } + + private def computeDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean = true): Set[DependencyAnalysisNode] = { + val allDependenciesUpwards = dependencyGraph.computeDependencies(nodesToAnalyze, includeInfeasibilityNodes, includeUpwardEdges = true, includeDownwardEdges = false) + val allDependenciesDownwards = dependencyGraph.computeDependencies(nodesToAnalyze ++ allDependenciesUpwards, includeInfeasibilityNodes, includeUpwardEdges = false, includeDownwardEdges = true) + allDependenciesUpwards ++ allDependenciesDownwards + } + + def computeNonInternalDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean = true): Set[DependencyAnalysisNode] = { + computeDependencies(nodesToAnalyze, includeInfeasibilityNodes) filter isNonInternalAssumptionNode + } + + def computeExplicitDependencies(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean = true): Set[DependencyAnalysisNode] = { + val allDeps = computeDependencies(nodesToAnalyze, includeInfeasibilityNodes) + allDeps filter isExplicitAssumptionNode + } + + def computeDirectDependents(nodesToAnalyze: Set[DependencyAnalysisNode]): Set[DependencyAnalysisNode] = { + val result = dependencyGraph.computeDirectDependents(nodesToAnalyze, includeInfeasibilityNodes = true, includeUpwardEdges = true, includeDownwardEdges = true) + result filter isNonInternalAssertionNode + } + + private def computeDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean = true): Set[DependencyAnalysisNode] = { + val allDependentsDownwards = dependencyGraph.computeDependents(nodesToAnalyze, includeInfeasibilityNodes, includeUpwardEdges = false, includeDownwardEdges = true) + val allDependentsUpwards = dependencyGraph.computeDependents(nodesToAnalyze ++ allDependentsDownwards, includeInfeasibilityNodes, includeUpwardEdges = true, includeDownwardEdges = false) + allDependentsUpwards ++ allDependentsDownwards + } + + def computeNonInternalDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean = true): Set[DependencyAnalysisNode] = { + val allDeps = computeDependents(nodesToAnalyze, includeInfeasibilityNodes) + allDeps filter isNonInternalAssertionNode + } + + def computeExplicitDependents(nodesToAnalyze: Set[DependencyAnalysisNode], includeInfeasibilityNodes: Boolean = true): Set[DependencyAnalysisNode] = { + val allDeps = computeDependents(nodesToAnalyze, includeInfeasibilityNodes) + allDeps filter isExplicitAssertionNode + } + + private def isVisibleNode(node: DependencyAnalysisNode): Boolean = + !node.assumptionType.isInstanceOf[AssumptionType.InternalType] + + def getNonInternalAssumptionNodes: Set[DependencyAnalysisNode] = getNodes filter isNonInternalAssumptionNode + + def isNonInternalAssumptionNode(node: DependencyAnalysisNode): Boolean = { + node match { + case _: GeneralAssumptionNode if isVisibleNode(node) => true + case _ => false + } + } + + private def isExplicitAssumptionNode(node: DependencyAnalysisNode): Boolean = node match { + case node: GeneralAssumptionNode => node.assumptionType.isInstanceOf[AssumptionType.ExplicitAssumptionType] + case _ => false + } + + def getNonInternalAssertionNodes: Set[GeneralAssertionNode] = + getAssertionNodes filter isNonInternalAssertionNode + + private def isNonInternalAssertionNode(node: DependencyAnalysisNode): Boolean = node match { + case node: GeneralAssertionNode if isVisibleNode(node) => true + case _ => false + } + + def getExplicitAssertionNodes: Set[GeneralAssertionNode] = + getAssertionNodes filter isExplicitAssertionNode + + private def isExplicitAssertionNode(node: DependencyAnalysisNode): Boolean = node.assumptionType.isInstanceOf[AssumptionType.ExplicitAssertionType] + + def getAssertionNodesWithFailures: Set[GeneralAssertionNode] = + getNonInternalAssertionNodes filter (_.hasFailed) + + def exportGraph(program: ast.Program, exportPath: String): Unit = + if (exportPath.nonEmpty) { + val directory = Paths.get(exportPath).toFile + directory.mkdirs() + dependencyGraph.exportGraph(exportPath) + exportProgram(program, exportPath) + } + + private def exportProgram(program: Program, path: String): Unit = { + // TODO ake: we should copy the original source file in order to keep the line numbering! + val writer = new PrintWriter(path + "/program.vpr") + writer.println(program.toString()) + writer.close() + } +} diff --git a/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyGraphTestSupporter.scala b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyGraphTestSupporter.scala new file mode 100644 index 000000000..d71682f49 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/graphInterpretation/DependencyGraphTestSupporter.scala @@ -0,0 +1,95 @@ +// 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.silicon.dependencyAnalysis.graphInterpretation + +import viper.silicon.dependencyAnalysis.UserLevelDependencyAnalysisNode +import viper.silicon.dependencyAnalysis.graph._ +import viper.silver.dependencyAnalysis.AssumptionType + +class DependencyGraphTestSupporter(interpreter: DependencyGraphInterpreter[Final]) { + + private val assumptionTypeRegex = """assumptionType:([^\s,)\"]+)""".r + private val assertionTypeRegex = """assertionType:([^\s,)\"]+)""".r + private val nodeLabelRegex = """label:([^\s,)\"]+)""".r + private val expectedDependenciesRegex = """expectedDependencies:\[([^\]]+)\]""".r + private val dependencyInfoRegex = """@dependencyInfo\(([^()]*)\)""".r + + def testNodeTypes(): Unit = { + testNodeTypes(interpreter.getNonInternalAssertionNodes ++ interpreter.getNonInternalAssumptionNodes) + } + + def testNodeTypes(nodes: Set[DependencyAnalysisNode]): Unit = { + val userLevelNodes = UserLevelDependencyAnalysisNode.from(nodes) + val tests = userLevelNodes.toList map testUserLevelNode + val numExecutedTests = tests.count(_.isDefined) + val numPassedTests = tests.count(_.getOrElse(false)) + println(s"Node type tests: Passed $numPassedTests/$numExecutedTests tests.") + assert(numPassedTests == numExecutedTests, s"Node type test failed. Only $numPassedTests/$numExecutedTests tests passed.") + } + + private def testUserLevelNode(ulNode: UserLevelDependencyAnalysisNode): Option[Boolean] = { + val dependencyInfoOpt = dependencyInfoRegex.findFirstMatchIn(ulNode.source.toString).map(_.group(1)) + dependencyInfoOpt match { + case None => None + case Some(dependencyInfo) => + var isTested = false + + val expectedAssumptionTypeOpt: Option[String] = assumptionTypeRegex.findFirstMatchIn(dependencyInfo).map(_.group(1)) + val isAssumptionTypeCorrect = expectedAssumptionTypeOpt match { + case Some(expectedTypeStr) => + val expectedType = AssumptionType.fromString(expectedTypeStr).get + isTested = true + ulNode.assumptionTypes.filterNot(_.isInstanceOf[AssumptionType.InternalType]).equals(Set(expectedType)) + case None => true + } + + val expectedAssertionTypeOpt: Option[String] = assertionTypeRegex.findFirstMatchIn(dependencyInfo).map(_.group(1)) + val isAssertionTypeCorrect = expectedAssertionTypeOpt match { + case Some(expectedTypeStr) => + val expectedType = AssumptionType.fromString(expectedTypeStr).get + isTested = true + ulNode.assertionTypes.filterNot(_.isInstanceOf[AssumptionType.InternalType]).equals(Set(expectedType)) + case None => true + } + + printIfFalse(isAssumptionTypeCorrect, s"Wrong assumption type for node ${ulNode.source.toString} having assumption types ${ulNode.assumptionTypes}.") + printIfFalse(isAssertionTypeCorrect, s"Wrong assertion type for node ${ulNode.source.toString} having assertion types ${ulNode.assertionTypes}.") + Option.when(isTested)(isAssumptionTypeCorrect && isAssertionTypeCorrect) + } + } + + private def printIfFalse(test: Boolean, message: String): Unit = + if (!test) + println(message) + + def testDependencies(): Unit = { + val testResults = UserLevelDependencyAnalysisNode.from(interpreter.getNonInternalAssertionNodes).toList map testDependencies + val numExecutedTests = testResults.count(_.isDefined) + val numPassedTests = testResults.count(_.getOrElse(false)) + println(s"Dependency tests: Passed $numPassedTests/$numExecutedTests tests.") + assert(numPassedTests == numExecutedTests, s"Dependency test failed. Only $numPassedTests/$numExecutedTests tests passed.") + } + + def testDependencies(assertionNode: UserLevelDependencyAnalysisNode): Option[Boolean] = { + val expectedLabelsOpt = expectedDependenciesRegex.findFirstMatchIn(assertionNode.source.toString).map(_.group(1).split(",").map(_.trim).toSet) + expectedLabelsOpt match { + case None => None + case Some(expectedLabels) => + val queriedAssertions = assertionNode.lowLevelAssertionNodes + val allDependencies = interpreter.computeNonInternalDependencies(queriedAssertions.toSet) + val sourceDependencies = UserLevelDependencyAnalysisNode.from(allDependencies).toSourceSet().diff(UserLevelDependencyAnalysisNode.from(queriedAssertions).toSourceSet()) + + val labelsInReportedDeps: Set[Set[String]] = sourceDependencies.map(node => nodeLabelRegex.findAllMatchIn(node.toString).map(_.group(1)).toSet) + val actualLabelInReportedDeps = labelsInReportedDeps.filter(_.size == 1).flatten + + val labelDiff = expectedLabels.diff(actualLabelInReportedDeps) + val isSound = labelDiff.isEmpty + printIfFalse(isSound, s"Missing dependencies (${labelDiff.mkString(", ")}) for ${assertionNode.source.toString}. Reported dependencies: ${actualLabelInReportedDeps.mkString(", ")}") + Some(isSound) + } + } +} diff --git a/src/main/scala/dependencyAnalysis/neo4j_importer.py b/src/main/scala/dependencyAnalysis/neo4j_importer.py new file mode 100644 index 000000000..fdb8adb54 --- /dev/null +++ b/src/main/scala/dependencyAnalysis/neo4j_importer.py @@ -0,0 +1,206 @@ + +import os +import pandas as pd +from neo4j import GraphDatabase +from neo4j import Transaction + +URI = "neo4j+ssc://27de0942.databases.neo4j.io" +AUTH = ("27de0942", os.environ['NEO4J-PW']) + + +ASSUMPTION_NODE_TYPES = """["Inhale", "Assumption", "Infeasible", "Label"]""" +ASSERTION_NODE_TYPES = """["Exhale", "Assertion", "Check"]""" +POSTCONDITION_TYPES = """["ExplicitPostcondition", "ImplicitPostcondition"]""" + +driver = GraphDatabase.driver(URI, auth=AUTH) +session = driver.session(database="27de0942") +driver.verify_connectivity() + +def create_id_uniquness_constraint(tx: Transaction, node_label: str): + tx.run(f"""CREATE CONSTRAINT `id_{node_label}_uniq` IF NOT EXISTS + FOR (n: {node_label}) + REQUIRE (n.`id`) IS UNIQUE;""") + tx.run(f"""CREATE INDEX {node_label}_pos_index IF NOT EXISTS + FOR (n:{node_label}) ON (n.position)""") + tx.run(f"""CREATE INDEX {node_label}_assumption_type_index IF NOT EXISTS + FOR (n:{node_label}) ON (n.`assumption type`)""") + tx.run(f"""CREATE INDEX {node_label}_node_type_index IF NOT EXISTS + FOR (n:{node_label}) ON (n.`node type`)""") + +def create_indices(tx: Transaction, label: str, node_label_postfix: str): + tx.run(f"""CREATE INDEX {label}{node_label_postfix}_pos_index IF NOT EXISTS + FOR (n:{label}{node_label_postfix}) ON (n.position)""") + + +def load_nodes(tx: Transaction, file_path: str, node_label: str): + with open(file_path, "r") as f: + nodes_raw = pd.read_csv(f, sep="#") + nodes = [] + for _, e in nodes_raw.iterrows(): + nodes.append(dict(e)) + tx.run(f""" + UNWIND $nodes AS row + WITH row + WHERE NOT toInteger(row.`id`) IS NULL + CALL {{ + WITH row + MERGE (n: {node_label} {{ `id`: toInteger(row.`id`) }}) + SET n.`id` = toInteger(row.`id`) + SET n.`node type` = row.`node type` + SET n.`assumption type` = row.`assumption type` + SET n.`node info` = row.`node info` + SET n.`source info` = row.`source info` + SET n.`position` = row.`position` + SET n.`merge info` = row.`merge info` + }}; + """ + , nodes=nodes + ) + +def load_edges(tx: Transaction, file_path: str, node_label: str): + with open(file_path, "r") as f: + edges_raw = pd.read_csv(f, sep=",") + edges = [] + for _, e in edges_raw.iterrows(): + edges.append(dict(e)) + # print(edges) + tx.run(f""" + UNWIND $edges AS row + WITH row + CALL {{ + WITH row + MATCH (source: {node_label} {{ `id`: toInteger(row.`source`) }}) + MATCH (target: {node_label} {{ `id`: toInteger(row.`target`) }}) + MERGE (source)-[r: `flows_into`]->(target) + SET r.`type` = row.`label` + }}; + """ + , edges=edges + ) + +def import_graph2(tx: Transaction, foldername: str, node_label: str, is_overwrite=True): + if is_overwrite: + delete_nodes_detach(tx, node_label, "") + load_nodes(tx, foldername + "/nodes.csv", node_label) + load_edges(tx, foldername + "/edges.csv", node_label) + +def import_low_level_graph(foldername: str, node_label: str): + session.execute_write(create_id_uniquness_constraint, node_label) # transaction! + session.execute_write(import_graph2, foldername, node_label) # transaction! + +def delete_nodes_detach(tx, label, node_label_postfix): + print(f"delete nodes {label}{node_label_postfix}") + tx.run(f"MATCH (n:{label}{node_label_postfix}) DETACH DELETE n;") + +def create_nodes(tx, label, node_label_postfix, assumption_node_selection): + print(f"create nodes {label}{node_label_postfix}") + tx.run(f""" + MATCH (a:{label}) + WHERE {assumption_node_selection.replace("$ID", "a")} + MERGE (aNew :{label}{node_label_postfix} {{`source info`: a.`source info`, position: a.position}}) + RETURN aNew; + """) # OR a.`node type` IN {ASSERTION_NODE_TYPES} + +def create_infeasibility_nodes(tx, label, node_label_postfix): + print(f"create infeasibility nodes") + tx.run(f""" + MATCH (c:{label})-[r:flows_into]->(a:{label}), + (c1:{label}{node_label_postfix}) + WHERE a.`node type` = "Infeasible" + AND c1.`source info` = c.`source info` AND c1.position = c.position + MERGE (c1)-[:flows_into]->(aNew :{label}{node_label_postfix} {{`source info`: a.`source info`, position: a.position, `node type`: a.`node type`}}) + RETURN aNew; + """) + tx.run(f""" + MATCH (c:{label})-[r1:flows_into]->(b:{label})-[r2:flows_into]->(a:{label}), (a1:{label}{node_label_postfix}), (c1:{label}{node_label_postfix}) + WHERE a.`node type` = "Infeasible" + AND c1.`source info` = c.`source info` AND c1.position = c.position + AND a1.`source info` = a.`source info` AND a1.position = a.position AND a1.`node type` = a.`node type` + MERGE (c1)-[:flows_into]->(a1) + RETURN a1; + """) + tx.run(f""" + MATCH (a:{label})-[r1:flows_into]->(c:{label}), (a1:{label}{node_label_postfix}), (c1:{label}{node_label_postfix}) + WHERE a.`node type` = "Infeasible" + AND c1.`source info` = c.`source info` AND c1.position = c.position + AND a1.`source info` = a.`source info` AND a1.position = a.position AND a1.`node type` = a.`node type` + MERGE (a1)-[:flows_into]->(c1) + RETURN a1; + """) + +def create_direct_edges(tx, label, node_label_postfix, assumption_node_selection): + print(f"add direct edges") + tx.run(f""" + MATCH (a:{label})-[r2:flows_into]->(c:{label}), + (a1:{label}{node_label_postfix}), (c1:{label}{node_label_postfix}) + WHERE {assumption_node_selection.replace("$ID", "a")} AND a1.`source info` = a.`source info` AND a1.position = a.position + AND a.`node type` IN {ASSUMPTION_NODE_TYPES} + AND c1.`source info` = c.`source info` AND c1.position = c.position + AND c.`node type` IN {ASSERTION_NODE_TYPES} + AND a1 <> c1 + MERGE (a1)-[n:flows_into]->(c1) + RETURN a1; + """) + tx.run(f""" + MATCH (a:{label})-[r2:flows_into]->(c:{label}), + (a1:{label}{node_label_postfix}), (c1:{label}{node_label_postfix}) + WHERE {assumption_node_selection.replace("$ID", "a")} AND a1.`source info` = a.`source info` AND a1.position = a.position + AND a.`node type` IN {ASSERTION_NODE_TYPES} AND a.`assumption type` IN {POSTCONDITION_TYPES} + AND c1.`source info` = c.`source info` AND c1.position = c.position + AND c.`node type` IN {ASSUMPTION_NODE_TYPES} + AND a1 <> c1 + MERGE (a1)-[n:flows_into]->(c1) + RETURN a1; + """) + +def create_indirect_edges(tx, label, node_label_postfix, assumption_node_selection): + print(f"add indirect edges") + tx.run(f""" + MATCH (a:{label})-[r:flows_into]->(b:{label}) + ((b1:{label})-[:flows_into]->(b2:{label})){{0, 5}} + (b3:{label})-[r2:flows_into]->(c:{label}), + (a1:{label}{node_label_postfix}), (c1:{label}{node_label_postfix}) + WHERE all(x in (b + b1 + b2) where NOT (x.`node type` IN {ASSUMPTION_NODE_TYPES} AND {assumption_node_selection.replace("$ID", "x")})) + AND all(x in (b + b1 + b2) where (NOT x.`node type` = "Infeasible")) + AND {assumption_node_selection.replace("$ID", "a")} AND a.`node type` IN {ASSUMPTION_NODE_TYPES} + AND c.`node type` IN {ASSERTION_NODE_TYPES} + AND a1.`source info` = a.`source info` AND a1.position = a.position + AND c1.`source info` = c.`source info` AND c1.position = c.position + AND a1 <> c1 + MERGE (a1)-[n:flows_into]->(c1) + RETURN a1; + """) + +def import_graph_view(label, is_overwrite, node_label_postfix, assumption_node_selection): + if is_overwrite: + session.execute_write(delete_nodes_detach, label, node_label_postfix) + session.execute_write(create_indices, label, node_label_postfix) + session.execute_write(create_nodes, label, node_label_postfix, assumption_node_selection) + session.execute_write(create_infeasibility_nodes, label, node_label_postfix) + session.execute_write(create_direct_edges, label, node_label_postfix, assumption_node_selection) + session.execute_write(create_indirect_edges, label, node_label_postfix, assumption_node_selection) + +def import_graph_without_internal_nodes(label, is_overwrite=True): + node_label_postfix = "_NonInternal" + internal_assumption_types = """["Internal", "Trigger"]""" + assumption_node_selection = f"NOT $ID.`assumption type` IN {internal_assumption_types}" + import_graph_view(label, is_overwrite, node_label_postfix, assumption_node_selection) + print("non internal view done") + +def import_graph_with_explicit_nodes_only(label, is_overwrite=True): + node_label_postfix = "_Explicit" + explicit_assumption_types = """["Explicit", "ExplicitPostcondition"]""" + assumption_node_selection = f"$ID.`assumption type` IN {explicit_assumption_types}" + import_graph_view(label, is_overwrite, node_label_postfix, assumption_node_selection) + print("explicit view done") + +foldername = input("foldername: ") +node_label = input("node label: ") +import_low_level_graph(foldername, node_label) +print("Low-level graph imported successfully") +import_graph_with_explicit_nodes_only(node_label) +print("Explicit-only Viper graph imported successfully") +import_graph_without_internal_nodes(node_label) +print("Viper graph imported successfully") +session.close() +driver.close() diff --git a/src/main/scala/interfaces/Verification.scala b/src/main/scala/interfaces/Verification.scala index 80aada5ea..4804f3e40 100644 --- a/src/main/scala/interfaces/Verification.scala +++ b/src/main/scala/interfaces/Verification.scala @@ -6,11 +6,13 @@ package viper.silicon.interfaces -import viper.silicon.debugger.{DebugAxiom, DebugExp} import viper.silicon.common.collections.immutable.InsertionOrderedSet -import viper.silicon.interfaces.state.Chunk +import viper.silicon.debugger.{DebugAxiom, DebugExp} +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter import viper.silicon.reporting._ -import viper.silicon.state.terms.{BooleanLiteral, FunctionDecl, IntLiteral, MacroDecl, Term, Var} +import viper.silicon.state.chunks.Chunk +import viper.silicon.state.terms._ import viper.silicon.state.{State, Store} import viper.silicon.verifier.Verifier import viper.silver.ast @@ -30,6 +32,7 @@ sealed abstract class VerificationResult { var previous: Vector[VerificationResult] = Vector() //Sets had problems with equality val continueVerification: Boolean = true var isReported: Boolean = false + var dependencyGraphInterpreter: Option[DependencyGraphInterpreter[IntraProcedural]] = None def isFatal: Boolean def &&(other: => VerificationResult): VerificationResult diff --git a/src/main/scala/interfaces/decider/Prover.scala b/src/main/scala/interfaces/decider/Prover.scala index 8c881b1e2..f5550d1bb 100644 --- a/src/main/scala/interfaces/decider/Prover.scala +++ b/src/main/scala/interfaces/decider/Prover.scala @@ -6,13 +6,13 @@ package viper.silicon.interfaces.decider -import viper.silicon.debugger.DebugAxiom import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.common.config.Version -import viper.silver.components.StatefulComponent -import viper.silicon.{Config, Map} +import viper.silicon.debugger.DebugAxiom import viper.silicon.state.terms._ import viper.silicon.verifier.Verifier +import viper.silicon.{Config, Map} +import viper.silver.components.StatefulComponent import viper.silver.verifier.Model sealed abstract class Result @@ -32,8 +32,10 @@ trait ProverLike { preambleAssumptions :+= new DebugAxiom(description, terms) terms foreach assume } + def setOption(name: String, value: String): String def assume(term: Term): Unit + def assume(term: Term, label: String): Unit def declare(decl: Decl): Unit def comment(content: String): Unit def saturate(timeout: Int, comment: String): Unit @@ -42,8 +44,9 @@ trait ProverLike { trait Prover extends ProverLike with StatefulComponent { def start(userArgsString: Option[String]): Unit - def assert(goal: Term, timeout: Option[Int] = None): Boolean - def check(timeout: Option[Int] = None): Result + def assert(goal: Term, timeout: Option[Int] = None, label: String = ""): Boolean + def check(timeout: Option[Int] = None, label: String = ""): Result + def getLastUnsatCore: String def fresh(id: String, argSorts: Seq[Sort], resultSort: Sort): Function def statistics(): Map[String, String] def hasModel(): Boolean diff --git a/src/main/scala/interfaces/state/Chunks.scala b/src/main/scala/interfaces/state/Chunks.scala deleted file mode 100644 index 9eb103064..000000000 --- a/src/main/scala/interfaces/state/Chunks.scala +++ /dev/null @@ -1,53 +0,0 @@ -// 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-2019 ETH Zurich. - -package viper.silicon.interfaces.state - -import viper.silicon -import viper.silicon.resources.ResourceID -import viper.silicon.state.terms.{Term, Var} -import viper.silver.ast - -trait Chunk { - def substitute(terms: silicon.Map[Term, Term]): Chunk -} -trait ChunkIdentifer - -trait GeneralChunk extends Chunk { - val resourceID: ResourceID - val id: ChunkIdentifer - val perm: Term - def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): GeneralChunk - def permMinus(perm: Term, permExp: Option[ast.Exp]): GeneralChunk - def permPlus(perm: Term, permExp: Option[ast.Exp]): GeneralChunk - - def permScale(perm: Term, permExp: Option[ast.Exp]): GeneralChunk - - val permExp: Option[ast.Exp] -} - -trait NonQuantifiedChunk extends GeneralChunk { - val args: Seq[Term] - val argsExp: Option[Seq[ast.Exp]] - val snap: Term - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): NonQuantifiedChunk - override def permMinus(perm: Term, permExp: Option[ast.Exp]): NonQuantifiedChunk - override def permPlus(perm: Term, permExp: Option[ast.Exp]): NonQuantifiedChunk - def withPerm(perm: Term, permExp: Option[ast.Exp]): NonQuantifiedChunk - def withSnap(snap: Term, snapExp: Option[ast.Exp]): NonQuantifiedChunk -} - -trait QuantifiedChunk extends GeneralChunk { - val quantifiedVars: Seq[Var] - val quantifiedVarExps: Option[Seq[ast.LocalVarDecl]] - - def snapshotMap: Term - def valueAt(arguments: Seq[Term]): Term - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): QuantifiedChunk - override def permMinus(perm: Term, permExp: Option[ast.Exp]): QuantifiedChunk - override def permPlus(perm: Term, permExp: Option[ast.Exp]): QuantifiedChunk - def withSnapshotMap(snap: Term): QuantifiedChunk -} \ No newline at end of file diff --git a/src/main/scala/logger/records/data/SingleMergeRecord.scala b/src/main/scala/logger/records/data/SingleMergeRecord.scala index aa8467dc4..9cfc89267 100644 --- a/src/main/scala/logger/records/data/SingleMergeRecord.scala +++ b/src/main/scala/logger/records/data/SingleMergeRecord.scala @@ -6,9 +6,9 @@ package viper.silicon.logger.records.data +import viper.silicon.state.chunks.{Chunk, NonQuantifiedChunk} import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.decider.PathConditionStack -import viper.silicon.interfaces.state.{Chunk, NonQuantifiedChunk} import viper.silicon.state._ import viper.silicon.state.terms.Term import viper.silver.ast diff --git a/src/main/scala/logger/writer/SymbExLogReportWriter.scala b/src/main/scala/logger/writer/SymbExLogReportWriter.scala index 3e00e4bd4..bc8f6acdb 100644 --- a/src/main/scala/logger/writer/SymbExLogReportWriter.scala +++ b/src/main/scala/logger/writer/SymbExLogReportWriter.scala @@ -7,9 +7,9 @@ package viper.silicon.logger.writer import spray.json.{JsArray, JsBoolean, JsNull, JsNumber, JsObject, JsString, JsTrue, JsValue} +import viper.silicon.state.chunks.Chunk import viper.silicon.Map import viper.silicon.common.collections.immutable.InsertionOrderedSet -import viper.silicon.interfaces.state.Chunk import viper.silicon.logger.records.scoping.{CloseScopeRecord, OpenScopeRecord} import viper.silicon.logger.records.structural.{BranchInfo, BranchingRecord, JoiningRecord} import viper.silicon.logger.records.{RecordData, SymbolicRecord} @@ -17,6 +17,7 @@ import viper.silicon.logger.{LogConfig, MemberSymbExLog} import viper.silicon.resources.{FieldID, PredicateID} import viper.silicon.rules.InverseFunctions import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms.Term import viper.silver.ast.AbstractLocalVar diff --git a/src/main/scala/reporting/Converter.scala b/src/main/scala/reporting/Converter.scala index ed437f989..75d78f32c 100644 --- a/src/main/scala/reporting/Converter.scala +++ b/src/main/scala/reporting/Converter.scala @@ -6,22 +6,21 @@ package viper.silicon.reporting +import viper.silicon.state.chunks.Chunk import viper.silicon - -import scala.util.{Success, Try} -import viper.silver.verifier.{ApplicationEntry, ConstantEntry, MapEntry, Model, ModelEntry, ValueEntry} -import viper.silver.ast import viper.silicon.Map -import viper.silicon.interfaces.state.Chunk -import viper.silicon.resources.{FieldID, PredicateID} -import viper.silicon.state.{BasicChunk, DefaultSymbolConverter, State, Store, SymbolConverter} -import viper.silicon.{state => st} -import viper.silicon.state.terms._ import viper.silicon.decider.TermToSMTLib2Converter import viper.silicon.interfaces.decider.TermConverter -import viper.silicon.state.terms.sorts.UserSort -import viper.silicon.state.terms.sorts.Snap +import viper.silicon.resources.{FieldID, PredicateID} +import viper.silicon.state.chunks._ +import viper.silicon.state.terms._ +import viper.silicon.state.terms.sorts.{Snap, UserSort} +import viper.silicon.state.{DefaultSymbolConverter, State, Store, SymbolConverter} +import viper.silver.ast import viper.silver.utility.Common.Rational +import viper.silver.verifier._ + +import scala.util.{Success, Try} case class ExtractedModel(entries: Map[String, ExtractedModelEntry]) { override lazy val toString: String = @@ -353,7 +352,7 @@ object Converter { entries = entries :+ entry case c: BasicChunk => entries = entries :+ UnresolvedHeapEntry(c, "Magic Wands not supported") - case c: st.QuantifiedFieldChunk => + case c: QuantifiedFieldChunk => val entry = c.snapshotMap val fvf = evaluateTerm(entry, model) val fieldname = c.id.name @@ -369,7 +368,7 @@ object Converter { } catch { case _: Throwable => // continue } - case _: st.QuantifiedPredicateChunk => // it seems that sometimes QPs do occur but not deterministically... :) + case _: QuantifiedPredicateChunk => // it seems that sometimes QPs do occur but not deterministically... :) case c => entries = entries :+ UnresolvedHeapEntry(c, "Non-basic chunks not supported") @@ -761,8 +760,8 @@ case class DomainValueEntry(domain: String, id: String) extends ExtractedModelEn * result when using original as input for the function */ case class ExtendedDomainValueEntry(original: DomainValueEntry, info: Map[ExtractedFunction, ExtractedModelEntry]) extends ExtractedModelEntry { - def asValueEntry: ValueEntry = original.asValueEntry - override def toString: String = + def asValueEntry: ValueEntry = original.asValueEntry + override def toString: String = original.toString ++" where " ++ info.map(infoEntryToString).mkString("{\n\t",";\n\t","\n\t}") private def infoEntryToString(entry: (ExtractedFunction, ExtractedModelEntry)): String = diff --git a/src/main/scala/resources/NonQuantifiedPropertyInterpreter.scala b/src/main/scala/resources/NonQuantifiedPropertyInterpreter.scala index 37621ea33..cf70fb3cb 100644 --- a/src/main/scala/resources/NonQuantifiedPropertyInterpreter.scala +++ b/src/main/scala/resources/NonQuantifiedPropertyInterpreter.scala @@ -7,12 +7,14 @@ package viper.silicon.resources import viper.silicon.Map -import viper.silicon.interfaces.state._ +import viper.silicon.dependencyAnalysis.{DependencyAnalysisInfos, DependencyAnalyzer} +import viper.silicon.state.chunks._ +import viper.silicon.state.terms import viper.silicon.state.terms.Term -import viper.silicon.state.{QuantifiedBasicChunk, terms} import viper.silicon.utils.ast.{BigAnd, replaceVarsInExp} import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.dependencyAnalysis.{AssumptionType, StringDependencyAnalysisSourceInfo} class NonQuantifiedPropertyInterpreter(heap: Iterable[Chunk], verifier: Verifier) extends PropertyInterpreter { @@ -120,8 +122,9 @@ class NonQuantifiedPropertyInterpreter(heap: Iterable[Chunk], verifier: Verifier otherwise: PropertyExpression[K], info: Info): (Term, Option[ast.Exp]) = { val conditionTerm = buildPathCondition(condition, info)._1 - if (verifier.decider.check(conditionTerm, Verifier.config.checkTimeout())) { - buildPathCondition(thenDo, info) + if (verifier.decider.check(conditionTerm, Verifier.config.checkTimeout(), DependencyAnalysisInfos.DefaultInfos.withInfo(StringDependencyAnalysisSourceInfo(s"property interpreter: ${conditionTerm.toString}"), AssumptionType.Internal.asDepType()))) { + val (t, e) = buildPathCondition(thenDo, info) + (DependencyAnalyzer.wrapWithDependencyAnalysisLabel(verifier.decider, t, Set.empty, Set(conditionTerm)), e) // TODO ake: causes imprecision! } else { buildPathCondition(otherwise, info) } @@ -147,15 +150,19 @@ class NonQuantifiedPropertyInterpreter(heap: Iterable[Chunk], verifier: Verifier body: PropertyExpression[kinds.Boolean], info: Info) : (Term, Option[ast.Exp]) = { - val builder: GeneralChunk => (Term, Option[ast.Exp]) = chunkVariables match { - case c +: Seq() => chunk => buildPathCondition(body, info.addMapping(c, chunk)) - case c +: tail => chunk => buildForEach(chunks, tail, body, info.addMapping(c, chunk)) + val builder: GeneralChunk => (Term, Option[ast.Exp]) = {chunk => + val res = chunkVariables match { + case c +: Seq() => buildPathCondition(body, info.addMapping(c, chunk)) + case c +: tail => buildForEach(chunks, tail, body, info.addMapping(c, chunk)) + } + (DependencyAnalyzer.wrapWithDependencyAnalysisLabel(verifier.decider, res._1, Set(chunk)), res._2) } val conds = chunks.flatMap { chunk => // check that only distinct tuples are handled // TODO: Is it possible to get this behavior without having to check every tuple? if (!info.pm.values.exists(chunk eq _)) { - Some(builder(chunk)) + val res = builder(chunk) + Some((DependencyAnalyzer.wrapWithDependencyAnalysisLabel(verifier.decider, res._1, Set(chunk)), res._2)) } else { None } diff --git a/src/main/scala/resources/QuantifiedPropertyInterpreter.scala b/src/main/scala/resources/QuantifiedPropertyInterpreter.scala index 09510ad7a..b0950bfa1 100644 --- a/src/main/scala/resources/QuantifiedPropertyInterpreter.scala +++ b/src/main/scala/resources/QuantifiedPropertyInterpreter.scala @@ -6,7 +6,7 @@ package viper.silicon.resources -import viper.silicon.interfaces.state.QuantifiedChunk +import viper.silicon.state.chunks.QuantifiedChunk import viper.silicon.state.terms import viper.silicon.state.terms.{Term, Trigger, Var} import viper.silicon.utils.ast.replaceVarsInExp diff --git a/src/main/scala/rules/Brancher.scala b/src/main/scala/rules/Brancher.scala index 3c1cf3240..a3a122113 100644 --- a/src/main/scala/rules/Brancher.scala +++ b/src/main/scala/rules/Brancher.scala @@ -6,18 +6,20 @@ package viper.silicon.rules -import java.util.concurrent._ import viper.silicon.common.concurrency._ import viper.silicon.decider.PathConditionStack +import viper.silicon.dependencyAnalysis.{DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces.{Unreachable, VerificationResult} import viper.silicon.reporting.condenseToViperResult import viper.silicon.state.State import viper.silicon.state.terms.{FunctionDecl, MacroDecl, Not, Term} import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.dependencyAnalysis.{AssumptionType, NoDependencyAnalysisMerge} import viper.silver.reporter.BranchFailureMessage import viper.silver.verifier.Failure +import java.util.concurrent._ import scala.collection.immutable.HashSet trait BranchingRules extends SymbolicExecutionRules { @@ -25,6 +27,7 @@ trait BranchingRules extends SymbolicExecutionRules { condition: Term, conditionExp: (ast.Exp, Option[ast.Exp]), v: Verifier, + analysisInfos: DependencyAnalysisInfos, fromShortCircuitingAnd: Boolean = false) (fTrue: (State, Verifier) => VerificationResult, fFalse: (State, Verifier) => VerificationResult) @@ -36,11 +39,18 @@ object brancher extends BranchingRules { condition: Term, conditionExp: (ast.Exp, Option[ast.Exp]), v: Verifier, + analysisInfos: DependencyAnalysisInfos, fromShortCircuitingAnd: Boolean = false) (fThen: (State, Verifier) => VerificationResult, fElse: (State, Verifier) => VerificationResult) : VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + val analysisInfos1 = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, analysisInfos, conditionExp._1.info, conditionExp._1) + v.decider.handleInfeasiblePath(false, true, analysisInfos1) + return fThen(s, v).combine(fElse(s, v)) + } + val negatedCondition = Not(condition) val negatedConditionExp = ast.Not(conditionExp._1)(pos = conditionExp._1.pos, info = conditionExp._1.info, ast.NoTrafos) val negatedConditionExpNew = conditionExp._2.map(ce => ast.Not(ce)(pos = ce.pos, info = ce.info, ast.NoTrafos)) @@ -56,16 +66,18 @@ object brancher extends BranchingRules { && s.quantifiedVariables.map(_._1).exists(condition.freeVariables.contains)) ) + val analysisInfos1 = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, analysisInfos, conditionExp._1.info, conditionExp._1) + /* True if the then-branch is to be explored */ val executeThenBranch = ( skipPathFeasibilityCheck - || !v.decider.check(negatedCondition, Verifier.config.checkTimeout())) + || !v.decider.check(negatedCondition, Verifier.config.checkTimeout(), analysisInfos1.withDependencyType(AssumptionType.Internal).withMergeInfo(NoDependencyAnalysisMerge()))) /* False if the then-branch is to be explored */ val executeElseBranch = ( !executeThenBranch /* Assumes that ast least one branch is feasible */ || skipPathFeasibilityCheck - || !v.decider.check(condition, Verifier.config.checkTimeout())) + || !v.decider.check(condition, Verifier.config.checkTimeout(), analysisInfos1.withDependencyType(AssumptionType.Internal).withMergeInfo(NoDependencyAnalysisMerge()))) val parallelizeElseBranch = s.parallelizeBranches && executeThenBranch && executeElseBranch @@ -97,7 +109,7 @@ object brancher extends BranchingRules { var noOfErrors = 0 val elseBranchVerificationTask: Verifier => VerificationResult = - if (executeElseBranch) { + if (executeElseBranch || Verifier.config.analyzeInfeasiblePaths) { /* [BRANCH-PARALLELISATION] */ /* Compute the following sets * 1. only if the else-branch needs to be explored @@ -143,7 +155,8 @@ object brancher extends BranchingRules { executionFlowController.locally(s, v0)((s1, v1) => { v1.decider.prover.comment(s"[else-branch: $cnt | $negatedCondition]") - v1.decider.setCurrentBranchCondition(negatedCondition, (negatedConditionExp, negatedConditionExpNew)) + v1.decider.setCurrentBranchCondition(negatedCondition, (negatedConditionExp, negatedConditionExpNew), analysisInfos1) + if (Verifier.config.dependencyAnalysis.isDefined && !executeElseBranch) v.decider.checkSmoke(analysisInfos1.withDependencyType(AssumptionType.Internal).withMergeInfo(NoDependencyAnalysisMerge())) var functionsOfElseBranchdDeciderBefore: Set[FunctionDecl] = null var nMacrosOfElseBranchDeciderBefore: Int = 0 @@ -173,7 +186,7 @@ object brancher extends BranchingRules { } val elseBranchFuture: Future[Seq[VerificationResult]] = - if (executeElseBranch) { + if (executeElseBranch || Verifier.config.analyzeInfeasiblePaths) { if (parallelizeElseBranch) { /* [BRANCH-PARALLELISATION] */ v.verificationPoolManager.queueVerificationTask(v0 => { @@ -189,11 +202,12 @@ object brancher extends BranchingRules { } val res = { - val thenRes = if (executeThenBranch) { + val thenRes = if (executeThenBranch || Verifier.config.analyzeInfeasiblePaths) { v.symbExLog.markReachable(uidBranchPoint) executionFlowController.locally(s, v)((s1, v1) => { v1.decider.prover.comment(s"[then-branch: $cnt | $condition]") - v1.decider.setCurrentBranchCondition(condition, conditionExp) + v1.decider.setCurrentBranchCondition(condition, conditionExp, analysisInfos1) + if (Verifier.config.dependencyAnalysis.isDefined && !executeThenBranch) v.decider.checkSmoke(analysisInfos1.withDependencyType(AssumptionType.Internal).withMergeInfo(NoDependencyAnalysisMerge())) fThen(v1.stateConsolidator(s1).consolidateOptionally(s1, v1), v1) }) diff --git a/src/main/scala/rules/ChunkSupporter.scala b/src/main/scala/rules/ChunkSupporter.scala index 6988b2d87..d9bca1295 100644 --- a/src/main/scala/rules/ChunkSupporter.scala +++ b/src/main/scala/rules/ChunkSupporter.scala @@ -7,15 +7,18 @@ package viper.silicon.rules import viper.silicon.debugger.DebugExp -import viper.silicon.interfaces.state._ +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.{Success, VerificationResult} import viper.silicon.resources.{NonQuantifiedPropertyInterpreter, Resources} import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.state.terms.perms.IsPositive import viper.silicon.utils.ast.buildMinExp import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.ast.FalseLit +import viper.silver.dependencyAnalysis.{AssumptionType, StringDependencyAnalysisSourceInfo} import viper.silver.parser.PUnknown import viper.silver.verifier.VerificationError @@ -32,7 +35,8 @@ trait ChunkSupportRules extends SymbolicExecutionRules { returnSnap: Boolean, ve: VerificationError, v: Verifier, - description: String) + description: String, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult @@ -46,7 +50,8 @@ trait ChunkSupportRules extends SymbolicExecutionRules { args: Seq[Term], argsExp: Option[Seq[ast.Exp]], ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Term, Verifier) => VerificationResult) : VerificationResult @@ -54,7 +59,8 @@ trait ChunkSupportRules extends SymbolicExecutionRules { (chunks: Iterable[Chunk], id: ChunkIdentifer, args: Iterable[Term], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : Option[CH] def findChunksWithID[CH <: NonQuantifiedChunk: ClassTag] @@ -75,11 +81,16 @@ object chunkSupporter extends ChunkSupportRules { returnSnap: Boolean, ve: VerificationError, v: Verifier, - description: String) + description: String, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + return Q(s, h, Option.when(returnSnap)(Unit), v) + } - consume2(s, h, resource, args, argsExp, perms, permsExp, returnSnap, ve, v)((s2, h2, optSnap, v2) => + consume2(s, h, resource, args, argsExp, perms, permsExp, returnSnap, ve, v, analysisInfos)((s2, h2, optSnap, v2) => optSnap match { case Some(snap) => Q(s2, h2, Some(snap.convert(sorts.Snap)), v2) @@ -107,14 +118,15 @@ object chunkSupporter extends ChunkSupportRules { permsExp: Option[ast.Exp], returnSnap: Boolean, ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { val id = ChunkIdentifier(resource, s.program) if (s.exhaleExt) { val failure = createFailure(ve, v, s, "chunk consume in package") - magicWandSupporter.transfer(s, perms, permsExp, failure, Seq(), v)(consumeGreedy(_, _, id, args, _, _, _))((s1, optCh, v1) => + magicWandSupporter.transfer(s, perms, permsExp, failure, Seq(), v, analysisInfos)(consumeGreedy(_, _, id, args, _, _, _, analysisInfos))((s1, optCh, v1) => if (returnSnap){ Q(s1, h, optCh.flatMap(ch => Some(ch.snap)), v1) } else { @@ -123,15 +135,15 @@ object chunkSupporter extends ChunkSupportRules { } else { executionFlowController.tryOrFail2[Heap, Option[Term]](s.copy(h = h), v)((s1, v1, QS) => if (s1.moreCompleteExhale) { - moreCompleteExhaleSupporter.consumeComplete(s1, s1.h, resource, args, argsExp, perms, permsExp, returnSnap, ve, v1)((s2, h2, snap2, v2) => { + moreCompleteExhaleSupporter.consumeComplete(s1, s1.h, resource, args, argsExp, perms, permsExp, returnSnap, ve, v1, analysisInfos)((s2, h2, snap2, v2) => { QS(s2.copy(h = s.h), h2, snap2, v2) }) } else { - consumeGreedy(s1, s1.h, id, args, perms, permsExp, v1) match { + consumeGreedy(s1, s1.h, id, args, perms, permsExp, v1, analysisInfos) match { case (Complete(), s2, h2, optCh2) => val snap = optCh2 match { case Some(ch) if returnSnap => - if (v1.decider.check(IsPositive(perms), Verifier.config.checkTimeout())) { + if (v1.decider.check(IsPositive(perms), Verifier.config.checkTimeout(), analysisInfos)) { Some(ch.snap) } else { Some(Ite(IsPositive(perms), ch.snap.convert(sorts.Snap), Unit)) @@ -139,10 +151,23 @@ object chunkSupporter extends ChunkSupportRules { case _ => None } QS(s2.copy(h = s.h), h2, snap, v1) - case _ if v1.decider.checkSmoke(true) => - Success() // TODO: Mark branch as dead? + case (_, s2, h2, _) if v1.decider.checkSmoke(analysisInfos, isAssert = true) => + if (Verifier.config.analyzeInfeasiblePaths) + QS(s2.copy(h = s.h), h2, None, v1) + else + Success() // TODO: Mark branch as dead? case _ => - createFailure(ve, v1, s1, "consuming chunk", true) + val failure = createFailure(ve, v1, s1, "consuming chunk", true) + if (s1.retryLevel == 0) { + val falseExp = Option.when(withExp)(FalseLit()()) + v1.decider.handleFailedAssertion(False, falseExp, falseExp, analysisInfos, v1.reportFurtherErrors()) + } + + if (s1.retryLevel == 0 && v1.reportFurtherErrors() && Verifier.config.analyzeInfeasiblePaths) { + failure combine QS(s1.copy(h = s.h), s1.h, None, v1) + } else { + failure + } } } )(Q) @@ -155,7 +180,8 @@ object chunkSupporter extends ChunkSupportRules { args: Seq[Term], perms: Term, permsExp: Option[ast.Exp], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : (ConsumptionResult, State, Heap, Option[NonQuantifiedChunk]) = { val consumeExact = terms.utils.consumeExactRead(perms, s.constrainableARPs) @@ -164,13 +190,13 @@ object chunkSupporter extends ChunkSupportRules { val interpreter = new NonQuantifiedPropertyInterpreter(heap.values, v) val resource = Resources.resourceDescriptions(chunk.resourceID) val pathCond = interpreter.buildPathConditionsForChunk(chunk, resource.instanceProperties(s.mayAssumeUpperBounds)) - pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)))) + pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)), analysisInfos)) } - findChunk[NonQuantifiedChunk](h.values, id, args, v) match { + findChunk[NonQuantifiedChunk](h.values, id, args, v, analysisInfos) match { case Some(ch) => if (s.assertReadAccessOnly) { - if (v.decider.check(Implies(IsPositive(perms), IsPositive(ch.perm)), Verifier.config.assertTimeout.getOrElse(0))) { + if (v.decider.check(Implies(IsPositive(perms), IsPositive(ch.perm)), Verifier.config.assertTimeout.getOrElse(0), analysisInfos)) { (Complete(), s, h, Some(ch)) } else { (Incomplete(perms, permsExp), s, h, None) @@ -179,22 +205,22 @@ object chunkSupporter extends ChunkSupportRules { val toTake = PermMin(ch.perm, perms) val toTakeExp = permsExp.map(pe => buildMinExp(Seq(ch.permExp.get, pe), ast.Perm)) val newPermExp = permsExp.map(pe => ast.PermSub(ch.permExp.get, toTakeExp.get)(pe.pos, pe.info, pe.errT)) - val newChunk = ch.withPerm(PermMinus(ch.perm, toTake), newPermExp) - val takenChunk = Some(ch.withPerm(toTake, toTakeExp)) + val newChunk = v.chunkFactory.withPermNonQuantifiedChunk(ch, PermMinus(ch.perm, toTake), newPermExp, analysisInfos) + val takenChunk = Some(v.chunkFactory.withPermNonQuantifiedChunk(ch, toTake, toTakeExp, analysisInfos, isExhale=true)) var newHeap = h - ch - if (!v.decider.check(newChunk.perm === NoPerm, Verifier.config.checkTimeout())) { + if (!v.decider.check(newChunk.perm === NoPerm, Verifier.config.checkTimeout(), analysisInfos.withDependencyType(AssumptionType.Internal))) { newHeap = newHeap + newChunk assumeProperties(newChunk, newHeap) } val remainingExp = permsExp.map(pe => ast.PermSub(pe, toTakeExp.get)(pe.pos, pe.info, pe.errT)) - (ConsumptionResult(PermMinus(perms, toTake), remainingExp, Seq(), v, 0), s, newHeap, takenChunk) + (ConsumptionResult(PermMinus(perms, toTake), remainingExp, Seq(), v, 0, analysisInfos), s, newHeap, takenChunk) } else { - if (v.decider.check(ch.perm !== NoPerm, Verifier.config.checkTimeout())) { + if (v.decider.check(ch.perm !== NoPerm, Verifier.config.checkTimeout(), analysisInfos)) { val constraintExp = permsExp.map(pe => ast.PermLtCmp(pe, ch.permExp.get)(pe.pos, pe.info, pe.errT)) - v.decider.assume(PermLess(perms, ch.perm), Option.when(withExp)(DebugExp.createInstance(constraintExp, constraintExp))) + v.decider.assume(PermLess(perms, ch.perm), Option.when(withExp)(DebugExp.createInstance(constraintExp, constraintExp)), analysisInfos) val newPermExp = permsExp.map(pe => ast.PermSub(ch.permExp.get, pe)(pe.pos, pe.info, pe.errT)) - val newChunk = ch.withPerm(PermMinus(ch.perm, perms), newPermExp) - val takenChunk = ch.withPerm(perms, permsExp) + val newChunk = v.chunkFactory.withPermNonQuantifiedChunk(ch, PermMinus(ch.perm, perms), newPermExp, analysisInfos) + val takenChunk = v.chunkFactory.withPermNonQuantifiedChunk(ch, perms, permsExp, analysisInfos, isExhale=true) val newHeap = h - ch + newChunk assumeProperties(newChunk, newHeap) (Complete(), s, newHeap, Some(takenChunk)) @@ -203,7 +229,7 @@ object chunkSupporter extends ChunkSupportRules { } } case None => - if (consumeExact && s.retrying && v.decider.check(perms === NoPerm, Verifier.config.checkTimeout())) { + if (consumeExact && s.retrying && v.decider.check(perms === NoPerm, Verifier.config.checkTimeout(), analysisInfos)) { (Complete(), s, h, None) } else { (Incomplete(perms, permsExp), s, h, None) @@ -214,10 +240,10 @@ object chunkSupporter extends ChunkSupportRules { def produce(s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier) (Q: (State, Heap, Verifier) => VerificationResult) : VerificationResult = { - + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo("produce", ast.NoPosition), AssumptionType.Internal) // Try to merge the chunk into the heap by finding an alias. // In any case, property assumptions are added after the merge step. - val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, h, ch, v) + val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, h, ch, v, analysisInfos) Q(s.copy(functionRecorder = fr1), h1, v) } @@ -227,15 +253,20 @@ object chunkSupporter extends ChunkSupportRules { args: Seq[Term], argsExp: Option[Seq[ast.Exp]], ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Term, Verifier) => VerificationResult) : VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + return Q(s, h, Unit, v) + } executionFlowController.tryOrFail2[Heap, Term](s.copy(h = h), v)((s1, v1, QS) => { val lookupFunction = if (s1.moreCompleteExhale) moreCompleteExhaleSupporter.lookupComplete _ else lookupGreedy _ - lookupFunction(s1, s1.h, resource, args, argsExp, ve, v1)((s2, tSnap, v2) => + lookupFunction(s1, s1.h, resource, args, argsExp, ve, v1, analysisInfos)((s2, tSnap, v2) => QS(s2.copy(h = s.h), s2.h, tSnap, v2)) })(Q) } @@ -246,24 +277,35 @@ object chunkSupporter extends ChunkSupportRules { args: Seq[Term], argsExp: Option[Seq[ast.Exp]], ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult) : VerificationResult = { val id = ChunkIdentifier(resource, s.program) - val findRes = findChunk[NonQuantifiedChunk](h.values, id, args, v) + val findRes = findChunk[NonQuantifiedChunk](h.values, id, args, v, analysisInfos) findRes match { - case Some(ch) if v.decider.check(IsPositive(ch.perm), Verifier.config.assertTimeout.getOrElse(0)) => + case Some(ch) if v.decider.check(IsPositive(ch.perm), Verifier.config.assertTimeout.getOrElse(0), analysisInfos) => Q(s, ch.snap, v) - case _ if v.decider.checkSmoke(true) => - if (s.isInPackage) { + case _ if v.decider.checkSmoke(analysisInfos, isAssert = true) => + if (s.isInPackage || Verifier.config.analyzeInfeasiblePaths) { val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) Q(s, snap, v) } else { Success() // TODO: Mark branch as dead? } case _ => - createFailure(ve, v, s, "looking up chunk", true) + val failure = createFailure(ve, v, s, "looking up chunk", true) + if (s.retryLevel == 0) { + val falseExp = Option.when(withExp)(FalseLit()()) + v.decider.handleFailedAssertion(False, falseExp, falseExp, analysisInfos, v.reportFurtherErrors()) + } + if (s.retryLevel == 0 && v.reportFurtherErrors() && Verifier.config.analyzeInfeasiblePaths) { + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) + failure combine Q(s, snap, v) + } else { + failure + } } } @@ -271,10 +313,11 @@ object chunkSupporter extends ChunkSupportRules { (chunks: Iterable[Chunk], id: ChunkIdentifer, args: Iterable[Term], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : Option[CH] = { val relevantChunks = findChunksWithID[CH](chunks, id) - findChunkLiterally(relevantChunks, args) orElse findChunkWithProver(relevantChunks, args, v) + findChunkLiterally(relevantChunks, args) orElse findChunkWithProver(relevantChunks, args, v, analysisInfos) } def findChunksWithID[CH <: NonQuantifiedChunk: ClassTag](chunks: Iterable[Chunk], id: ChunkIdentifer): Iterable[CH] = { @@ -311,9 +354,9 @@ object chunkSupporter extends ChunkSupportRules { chunks find (ch => ch.args == args) } - private def findChunkWithProver[CH <: NonQuantifiedChunk](chunks: Iterable[CH], args: Iterable[Term], v: Verifier) = { + private def findChunkWithProver[CH <: NonQuantifiedChunk](chunks: Iterable[CH], args: Iterable[Term], v: Verifier, analysisInfos: DependencyAnalysisInfos) = { chunks find (ch => args.size == ch.args.size && - v.decider.check(And(ch.args zip args map (x => x._1 === x._2)), Verifier.config.checkTimeout())) + v.decider.check(And(ch.args zip args map (x => x._1 === x._2)), Verifier.config.checkTimeout(), analysisInfos)) } } diff --git a/src/main/scala/rules/Consumer.scala b/src/main/scala/rules/Consumer.scala index 4bfbf859a..8852cce19 100644 --- a/src/main/scala/rules/Consumer.scala +++ b/src/main/scala/rules/Consumer.scala @@ -8,18 +8,21 @@ package viper.silicon.rules import viper.silicon.Config.JoinMode import viper.silicon.debugger.DebugExp - -import scala.collection.mutable -import viper.silver.ast -import viper.silver.ast.utility.QuantifiedPermissions.QuantifiedPermissionAssertion -import viper.silver.verifier.PartialVerificationError -import viper.silver.verifier.reasons._ +import viper.silicon.dependencyAnalysis._ import viper.silicon.interfaces.VerificationResult import viper.silicon.logger.records.data.{CondExpRecord, ConsumeRecord, ImpliesRecord} import viper.silicon.state._ +import viper.silicon.state.chunks.MagicWandIdentifier import viper.silicon.state.terms._ import viper.silicon.utils.ast.BigAnd import viper.silicon.verifier.Verifier +import viper.silver.ast +import viper.silver.ast.utility.QuantifiedPermissions.QuantifiedPermissionAssertion +import viper.silver.dependencyAnalysis.{AssumptionType, StringDependencyAnalysisSourceInfo} +import viper.silver.verifier.PartialVerificationError +import viper.silver.verifier.reasons._ + +import scala.collection.mutable trait ConsumptionRules extends SymbolicExecutionRules { @@ -30,13 +33,14 @@ trait ConsumptionRules extends SymbolicExecutionRules { * @param returnSnap Whether a snapshot should be returned or not. * @param pve The error to report in case the consumption fails. * @param v The verifier to use. + * @param analysisInfos Information for the dependency analysis. * @param Q The continuation to invoke if the consumption succeeded, with the following * arguments: state (1st argument) and verifier (3rd argument) resulting from the * consumption, and a heap snapshot (2bd argument )representing the values of the * consumed partial heap. * @return The result of the continuation. */ - def consume(s: State, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier) + def consume(s: State, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Option[Term], Verifier) => VerificationResult) : VerificationResult @@ -52,6 +56,7 @@ trait ConsumptionRules extends SymbolicExecutionRules { * @param pvef The error to report in case a consumption fails. Given assertions `as`, an error * `pvef(as_i)` will be reported if consuming assertion `as_i` fails. * @param v @see [[consume]] + * @param analysisInfos @see [[consume]] * @param Q @see [[consume]] * @return @see [[consume]] */ @@ -59,7 +64,8 @@ trait ConsumptionRules extends SymbolicExecutionRules { as: Seq[ast.Exp], returnSnap: Boolean, pvef: ast.Exp => PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Option[Term], Verifier) => VerificationResult) : VerificationResult } @@ -73,11 +79,11 @@ object consumer extends ConsumptionRules { */ /** @inheritdoc */ - def consume(s: State, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier) + def consume(s: State, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Option[Term], Verifier) => VerificationResult) : VerificationResult = { - consumeR(s, s.h, a.whenExhaling, returnSnap, pve, v)((s1, h1, snap, v1) => { + consumeR(s, s.h, a.whenExhaling, returnSnap, pve, v, analysisInfos)((s1, h1, snap, v1) => { val s2 = s1.copy(h = h1, partiallyConsumedHeap = s.partiallyConsumedHeap) Q(s2, snap, v1)}) @@ -88,7 +94,8 @@ object consumer extends ConsumptionRules { as: Seq[ast.Exp], returnSnap: Boolean, pvef: ast.Exp => PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -103,7 +110,7 @@ object consumer extends ConsumptionRules { allPves ++= pves }) - consumeTlcs(s, s.h, allTlcs.result(), returnSnap, allPves.result(), v)((s1, h1, snap1, v1) => { + consumeTlcs(s, s.h, allTlcs.result(), returnSnap, allPves.result(), v, analysisInfos)((s1, h1, snap1, v1) => { val s2 = s1.copy(h = h1, partiallyConsumedHeap = s.partiallyConsumedHeap) Q(s2, snap1, v1) @@ -115,7 +122,8 @@ object consumer extends ConsumptionRules { tlcs: Seq[ast.Exp], returnSnap: Boolean, pves: Seq[PartialVerificationError], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -126,10 +134,10 @@ object consumer extends ConsumptionRules { val pve = pves.head if (tlcs.tail.isEmpty) - wrappedConsumeTlc(s, h, a, returnSnap, pve, v)(Q) + wrappedConsumeTlc(s, h, a, returnSnap, pve, v, analysisInfos)(Q) else - wrappedConsumeTlc(s, h, a, returnSnap, pve, v)((s1, h1, snap1, v1) => { - consumeTlcs(s1, h1, tlcs.tail, returnSnap, pves.tail, v1)((s2, h2, snap2, v2) => + wrappedConsumeTlc(s, h, a, returnSnap, pve, v, analysisInfos)((s1, h1, snap1, v1) => { + consumeTlcs(s1, h1, tlcs.tail, returnSnap, pves.tail, v1, analysisInfos)((s2, h2, snap2, v2) => (snap1, snap2) match { case (Some(sn1), Some(sn2)) if returnSnap => Q(s2, h2, Some(Combine(sn1, sn2)), v2) @@ -140,14 +148,14 @@ object consumer extends ConsumptionRules { } } - private def consumeR(s: State, h: Heap, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier) + private def consumeR(s: State, h: Heap, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { val tlcs = a.topLevelConjuncts val pves = Seq.fill(tlcs.length)(pve) - consumeTlcs(s, h, tlcs, returnSnap, pves, v)(Q) + consumeTlcs(s, h, tlcs, returnSnap, pves, v, analysisInfos)(Q) } /** Wrapper/decorator for consume that injects the following operations: @@ -159,7 +167,8 @@ object consumer extends ConsumptionRules { a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -173,14 +182,15 @@ object consumer extends ConsumptionRules { val s1 = s0.copy(h = s.h) /* s1 is s, but the retrying flag might be set */ val sepIdentifier = v1.symbExLog.openScope(new ConsumeRecord(a, s1, v.decider.pcs)) + val analysisInfos1 = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v1.decider, analysisInfos, a.info, a) - consumeTlc(s1, h0, a, returnSnap, pve, v1)((s2, h2, snap2, v2) => { + consumeTlc(s1, h0, a, returnSnap, pve, v1, analysisInfos1)((s2, h2, snap2, v2) => { v2.symbExLog.closeScope(sepIdentifier) QS(s2, h2, snap2, v2)}) })(Q) } - private def consumeTlc(s: State, h: Heap, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier) + private def consumeTlc(s: State, h: Heap, a: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -191,6 +201,11 @@ object consumer extends ConsumptionRules { * time permissions have been consumed. */ + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + return Q(s, h, Option.when(returnSnap)(Unit), v) + } + v.logger.debug(s"\nCONSUME ${viper.silicon.utils.ast.sourceLineColumn(a)}: $a") v.logger.debug(v.stateFormatter.format(s, v.decider.pcs)) v.logger.debug("h = " + v.stateFormatter.format(h)) @@ -201,15 +216,15 @@ object consumer extends ConsumptionRules { case imp @ ast.Implies(e0, a0) if !a.isPure && s.moreJoins.id >= JoinMode.Impure.id => val impliesRecord = new ImpliesRecord(imp, s, v.decider.pcs, "consume") val uidImplies = v.symbExLog.openScope(impliesRecord) - consumeConditionalTlcMoreJoins(s, h, e0, a0, None, uidImplies, returnSnap, pve, v)(Q) + consumeConditionalTlcMoreJoins(s, h, e0, a0, None, uidImplies, returnSnap, pve, v, analysisInfos)(Q) case imp @ ast.Implies(e0, a0) if !a.isPure => val impliesRecord = new ImpliesRecord(imp, s, v.decider.pcs, "consume") val uidImplies = v.symbExLog.openScope(impliesRecord) - evaluator.eval(s, e0, pve, v)((s1, t0, e0New, v1) => - branch(s1, t0, (e0, e0New), v1)( - (s2, v2) => consumeR(s2, h, a0, returnSnap, pve, v2)((s3, h1, t1, v3) => { + evaluator.eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + branch(s1, t0, (e0, e0New), v1, analysisInfos)( + (s2, v2) => consumeR(s2, h, a0, returnSnap, pve, v2, analysisInfos)((s3, h1, t1, v3) => { v3.symbExLog.closeScope(uidImplies) Q(s3, h1, t1, v3) }), @@ -221,19 +236,19 @@ object consumer extends ConsumptionRules { case ite @ ast.CondExp(e0, a1, a2) if !a.isPure && s.moreJoins.id >= JoinMode.Impure.id => val condExpRecord = new CondExpRecord(ite, s, v.decider.pcs, "consume") val uidCondExp = v.symbExLog.openScope(condExpRecord) - consumeConditionalTlcMoreJoins(s, h, e0, a1, Some(a2), uidCondExp, returnSnap, pve, v)(Q) + consumeConditionalTlcMoreJoins(s, h, e0, a1, Some(a2), uidCondExp, returnSnap, pve, v, analysisInfos)(Q) case ite @ ast.CondExp(e0, a1, a2) if !a.isPure => val condExpRecord = new CondExpRecord(ite, s, v.decider.pcs, "consume") val uidCondExp = v.symbExLog.openScope(condExpRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - branch(s1, t0, (e0, e0New), v1)( - (s2, v2) => consumeR(s2, h, a1, returnSnap, pve, v2)((s3, h1, t1, v3) => { + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + branch(s1, t0, (e0, e0New), v1, analysisInfos)( + (s2, v2) => consumeR(s2, h, a1, returnSnap, pve, v2, analysisInfos)((s3, h1, t1, v3) => { v3.symbExLog.closeScope(uidCondExp) Q(s3, h1, t1, v3) }), - (s2, v2) => consumeR(s2, h, a2, returnSnap, pve, v2)((s3, h1, t1, v3) => { + (s2, v2) => consumeR(s2, h, a2, returnSnap, pve, v2, analysisInfos)((s3, h1, t1, v3) => { v3.symbExLog.closeScope(uidCondExp) Q(s3, h1, t1, v3) }))) @@ -243,17 +258,17 @@ object consumer extends ConsumptionRules { val resource = accPred.res(s.program) val ePerm = accPred.perm - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => - eval(s1, ePerm, pve, v1)((s1a, tPerm, ePermNew, v1a) => - permissionSupporter.assertNotNegative(s1a, tPerm, ePerm, ePermNew, pve, v1a)((s2, v2) => { + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => + eval(s1, ePerm, pve, v1, analysisInfos)((s1a, tPerm, ePermNew, v1a) => + permissionSupporter.assertNotNegative(s1a, tPerm, ePerm, ePermNew, pve, v1a, analysisInfos)((s2, v2) => { val loss = if (!Verifier.config.unsafeWildcardOptimization() || (resource.isInstanceOf[ast.Location] && s2.permLocations.contains(resource.asInstanceOf[ast.Location]))) PermTimes(tPerm, s2.permissionScalingFactor) else WildcardSimplifyingPermTimes(tPerm, s2.permissionScalingFactor) val lossExp = ePermNew.map(p => ast.PermMul(p, s2.permissionScalingFactorExp.get)(p.pos, p.info, p.errT)) - val s3 = v2.heapSupporter.triggerResourceIfNeeded(s2, accPred.loc, tArgs, eArgsNew, v2) - v2.heapSupporter.consumeSingle(s3, h, accPred.loc, tArgs, eArgsNew, loss, lossExp, returnSnap, pve, v2)((s4, h4, snap, v4) => { + val s3 = v2.heapSupporter.triggerResourceIfNeeded(s2, accPred.loc, tArgs, eArgsNew, v2, analysisInfos) + v2.heapSupporter.consumeSingle(s3, h, accPred.loc, tArgs, eArgsNew, loss, lossExp, returnSnap, pve, v2, analysisInfos)((s4, h4, snap, v4) => { val s5 = s4.copy(constrainableARPs = s.constrainableARPs, partiallyConsumedHeap = Some(h4)) Q(s5, h4, snap, v4) @@ -283,7 +298,7 @@ object consumer extends ConsumptionRules { if (forall.triggers.isEmpty) None else Some(forall.triggers) val s0 = s.copy(functionRecorder = s.functionRecorder.enterQuantifiedExp(qpa)) - evalQuantified(s0, Forall, forall.variables, Seq(cond), ePerm +: eArgs, optTrigger, qid, pve, v) { + evalQuantified(s0, Forall, forall.variables, Seq(cond), ePerm +: eArgs, optTrigger, qid, pve, v, analysisInfos) { case (s1, qvars, qvarExps, Seq(tCond), condNew, Some((Seq(tPerm, tArgs@_*), permArgsNew, tTriggers, (auxGlobals, auxNonGlobals), auxExps)), v1) => v1.heapSupporter.consumeQuantified( s = s1, @@ -311,7 +326,8 @@ object consumer extends ConsumptionRules { negativePermissionReason = NegativePermission(ePerm), notInjectiveReason = QPAssertionNotInjective(resAcc), insufficientPermissionReason = insuffReason, - v1)((s2, h2, snap, v2) => { + v1, + analysisInfos)((s2, h2, snap, v2) => { val s3 = s2.copy(constrainableARPs = s.constrainableARPs, functionRecorder = s2.functionRecorder.leaveQuantifiedExp(qpa)) Q(s3, h2, snap, v2) }) @@ -319,38 +335,38 @@ object consumer extends ConsumptionRules { } case let: ast.Let if !let.isPure => - letSupporter.handle[ast.Exp](s, let, pve, v)((s1, g1, body, v1) => { + letSupporter.handle[ast.Exp](s, let, pve, v, analysisInfos)((s1, g1, body, v1) => { val s2 = s1.copy(g = s1.g + g1) - consumeR(s2, h, body, returnSnap, pve, v1)(Q)}) + consumeR(s2, h, body, returnSnap, pve, v1, analysisInfos)(Q)}) case _: ast.InhaleExhaleExp => createFailure(viper.silicon.utils.consistency.createUnexpectedInhaleExhaleExpressionError(a), v, s, "valid AST") case _ => - evalAndAssert(s, a, returnSnap, pve, v)((s1, t, v1) => { + evalAndAssert(s, a, returnSnap, pve, v, analysisInfos)((s1, t, v1) => { Q(s1, h, t, v1) }) } - consumed } private def consumeConditionalTlcMoreJoins(s: State, h: Heap, e0: ast.Exp, a1: ast.Exp, a2: Option[ast.Exp], scopeUid: Int, returnSnap: Boolean, - pve: PartialVerificationError, v: Verifier) + pve: PartialVerificationError, v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - joiner.join[(Heap, Option[Term]), (Heap, Option[Term])](s1, v1, resetState = false)((s1, v1, QB) => { - branch(s1.copy(parallelizeBranches = false), t0, (e0, e0New), v1)( + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + joiner.join[(Heap, Option[Term]), (Heap, Option[Term])](s1, v1, analysisInfos, resetState = false)((s1, v1, QB) => { + branch(s1.copy(parallelizeBranches = false), t0, (e0, e0New), v1, analysisInfos)( (s2, v2) => - consumeR(s2.copy(parallelizeBranches = s1.parallelizeBranches), h, a1, returnSnap, pve, v2)((s3, h1, t1, v3) => { + consumeR(s2.copy(parallelizeBranches = s1.parallelizeBranches), h, a1, returnSnap, pve, v2, analysisInfos)((s3, h1, t1, v3) => { v3.symbExLog.closeScope(scopeUid) QB(s3, (h1, t1), v3) }), (s2, v2) => a2 match { - case Some(a2) => consumeR(s2.copy(parallelizeBranches = s1.parallelizeBranches), h, a2, returnSnap, pve, v2)((s3, h1, t1, v3) => { + case Some(a2) => consumeR(s2.copy(parallelizeBranches = s1.parallelizeBranches), h, a2, returnSnap, pve, v2, analysisInfos)((s3, h1, t1, v3) => { v3.symbExLog.closeScope(scopeUid) QB(s3, (h1, t1), v3) }) @@ -367,6 +383,8 @@ object consumer extends ConsumptionRules { State.mergeHeap( entry1.data._1, And(entry1.pathConditions.branchConditions), Option.when(withExp)(BigAnd(entry1.pathConditions.branchConditionExps.map(_._2.get))), entry2.data._1, And(entry2.pathConditions.branchConditions), Option.when(withExp)(BigAnd(entry2.pathConditions.branchConditionExps.map(_._2.get))), + v1, + analysisInfos.withSource(StringDependencyAnalysisSourceInfo("conditional join", e0.pos)) ), // Assume that entry1.pcs is inverse of entry2.pcs (entry1.data._2, entry2.data._2) match { @@ -387,7 +405,7 @@ object consumer extends ConsumptionRules { } - private def evalAndAssert(s: State, e: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier) + private def evalAndAssert(s: State, e: ast.Exp, returnSnap: Boolean, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -406,25 +424,24 @@ object consumer extends ConsumptionRules { exhaleExt = false) executionFlowController.tryOrFail0(s1, v)((s2, v1, QS) => { - eval(s2, e, pve, v1)((s3, t, eNew, v2) => { + eval(s2, e, pve, v1, analysisInfos)((s3, t, eNew, v2) => { val termToAssert = t match { case Quantification(q, vars, body, trgs, name, isGlob, weight) => val transformed = FunctionPreconditionTransformer.transform(body, s3.program) v2.decider.assume(Quantification(q, vars, transformed, trgs, name+"_precondition", isGlob, weight), - Option.when(withExp)(DebugExp.createInstance("Function preconditions hold in quantifier " + eNew.toString, true))) + Option.when(withExp)(DebugExp.createInstance("Function preconditions hold in quantifier " + eNew.toString, true)), analysisInfos) Quantification(q, vars, Implies(transformed, body), trgs, name, isGlob, weight) case _ => t } - v2.decider.assert(termToAssert) { + v2.decider.assert(termToAssert, analysisInfos) { case true => - v2.decider.assume(t, Option.when(withExp)(e), eNew) + v2.decider.assume(t, Option.when(withExp)(e), eNew, analysisInfos.withDependencyType(AssumptionType.Internal)) QS(s3, v2) case false => val failure = createFailure(pve dueTo AssertionFalse(e), v2, s3, termToAssert, eNew) - if (s3.retryLevel == 0 && v2.reportFurtherErrors()){ - v2.decider.assume(t, Option.when(withExp)(e), eNew) - failure combine QS(s3, v2) - } else failure}}) + if (s3.retryLevel == 0) v2.decider.handleFailedAssertion(t, Option.when(withExp)(e), eNew, analysisInfos, v2.reportFurtherErrors()) + if (s3.retryLevel == 0 && v2.reportFurtherErrors()) failure combine QS(s3, v2) else failure + }}) })((s4, v4) => { val s5 = s4.copy(h = s.h, reserveHeaps = s.reserveHeaps, diff --git a/src/main/scala/rules/ConsumptionResult.scala b/src/main/scala/rules/ConsumptionResult.scala index e7fcafedf..3c2a74d08 100644 --- a/src/main/scala/rules/ConsumptionResult.scala +++ b/src/main/scala/rules/ConsumptionResult.scala @@ -6,10 +6,11 @@ package viper.silicon.rules -import viper.silicon.state.terms.{Forall, Term, Var} +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.state.terms.perms.IsNonPositive -import viper.silver.ast +import viper.silicon.state.terms.{Forall, Term, Var} import viper.silicon.verifier.Verifier +import viper.silver.ast sealed trait ConsumptionResult { def isComplete: Boolean @@ -27,13 +28,13 @@ private case class Incomplete(permsNeeded: Term, permsNeededExp: Option[ast.Exp] } object ConsumptionResult { - def apply(term: Term, exp: Option[ast.Exp], qvars: Seq[Var], v: Verifier, timeout: Int): ConsumptionResult = { + def apply(term: Term, exp: Option[ast.Exp], qvars: Seq[Var], v: Verifier, timeout: Int, analysisInfos: DependencyAnalysisInfos): ConsumptionResult = { val toCheck = if (qvars.isEmpty) { IsNonPositive(term) } else { Forall(qvars, IsNonPositive(term), Seq()) } - if (v.decider.check(toCheck, timeout)) + if (v.decider.check(toCheck, timeout, analysisInfos)) Complete() else Incomplete(term, exp) diff --git a/src/main/scala/rules/Evaluator.scala b/src/main/scala/rules/Evaluator.scala index d5ac167b8..ce7048217 100644 --- a/src/main/scala/rules/Evaluator.scala +++ b/src/main/scala/rules/Evaluator.scala @@ -7,17 +7,14 @@ package viper.silicon.rules import viper.silicon -import viper.silicon.debugger.DebugExp import viper.silicon.Config.JoinMode -import viper.silver.ast -import viper.silver.verifier.{CounterexampleTransformer, PartialVerificationError, VerifierWarning} -import viper.silver.verifier.errors.{ErrorWrapperWithExampleTransformer, PreconditionInAppFalse} -import viper.silver.verifier.reasons._ import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis.{DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces._ -import viper.silicon.interfaces.state.ChunkIdentifer import viper.silicon.logger.records.data.{CondExpRecord, EvaluateRecord, ImpliesRecord} import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.state.terms.implicits._ import viper.silicon.state.terms.perms.IsPositive @@ -25,9 +22,14 @@ import viper.silicon.utils.ast._ import viper.silicon.utils.toSf import viper.silicon.verifier.Verifier import viper.silicon.{Map, TriggerSets} -import viper.silver.ast.{AnnotationInfo, LocalVarWithVersion, WeightedQuantifier} +import viper.silver.ast +import viper.silver.ast.{AnnotationInfo, FalseLit, LocalVarWithVersion, WeightedQuantifier} +import viper.silver.dependencyAnalysis._ import viper.silver.reporter.{AnnotationWarning, WarningsDuringVerification} import viper.silver.utility.Common.Rational +import viper.silver.verifier.errors.{ErrorWrapperWithExampleTransformer, PreconditionInAppFalse} +import viper.silver.verifier.reasons._ +import viper.silver.verifier.{CounterexampleTransformer, PartialVerificationError, VerifierWarning} /* TODO: With the current design w.r.t. parallelism, eval should never "move" an execution @@ -36,11 +38,11 @@ import viper.silver.utility.Common.Rational */ trait EvaluationRules extends SymbolicExecutionRules { - def evals(s: State, es: Seq[ast.Exp], pvef: ast.Exp => PartialVerificationError, v: Verifier) + def evals(s: State, es: Seq[ast.Exp], pvef: ast.Exp => PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, List[Term], Option[List[ast.Exp]], Verifier) => VerificationResult) : VerificationResult - def eval(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier) + def eval(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult @@ -52,7 +54,8 @@ trait EvaluationRules extends SymbolicExecutionRules { optTriggers: Option[Seq[ast.Trigger]], name: String, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Seq[Var], Option[Seq[ast.LocalVarDecl]], Seq[Term], Option[Seq[ast.Exp]], Option[(Seq[Term], Option[Seq[ast.Exp]], Seq[Trigger], (Seq[Term], Seq[Quantification]), Option[(InsertionOrderedSet[DebugExp], InsertionOrderedSet[DebugExp])])], Verifier) => VerificationResult) : VerificationResult } @@ -61,40 +64,41 @@ object evaluator extends EvaluationRules { import consumer._ import producer._ - def evals(s: State, es: Seq[ast.Exp], pvef: ast.Exp => PartialVerificationError, v: Verifier) + def evals(s: State, es: Seq[ast.Exp], pvef: ast.Exp => PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, List[Term], Option[List[ast.Exp]], Verifier) => VerificationResult) : VerificationResult = - evals2(s, es, Nil, pvef, v)(Q) + evals2(s, es, Nil, pvef, v, analysisInfos)(Q) - private def evals2(s: State, es: Seq[ast.Exp], ts: List[Term], pvef: ast.Exp => PartialVerificationError, v: Verifier) + private def evals2(s: State, es: Seq[ast.Exp], ts: List[Term], pvef: ast.Exp => PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, List[Term], Option[List[ast.Exp]], Verifier) => VerificationResult) : VerificationResult = { if (es.isEmpty) Q(s, ts.reverse, if (withExp) Some(List.empty) else None, v) else - eval(s, es.head, pvef(es.head), v)((s1, t, eNew, v1) => - evals2(s1, es.tail, t :: ts, pvef, v1)((s2, ts2, es2, v2) => Q(s2, ts2, eNew.map(eN => eN :: es2.get), v2))) + eval(s, es.head, pvef(es.head), v, analysisInfos)((s1, t, eNew, v1) => + evals2(s1, es.tail, t :: ts, pvef, v1, analysisInfos)((s2, ts2, es2, v2) => Q(s2, ts2, eNew.map(eN => eN :: es2.get), v2))) } /** Wrapper Method for eval, for logging. See Executor.scala for explanation of analogue. **/ @inline - def eval(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier) + def eval(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { val sepIdentifier = v.symbExLog.openScope(new EvaluateRecord(e, s, v.decider.pcs)) - eval3(s, e, pve, v)((s1, t, eNew, v1) => { + val analysisInfos1 = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, analysisInfos, e.info, e) + + eval3(s, e, pve, v, analysisInfos1)((s1, t, eNew, v1) => { v1.symbExLog.closeScope(sepIdentifier) Q(s1, t, eNew, v1)}) } - def eval3(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier) + def eval3(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { - /* For debugging only */ e match { case _: ast.TrueLit | _: ast.FalseLit | _: ast.NullLit | _: ast.IntLit | _: ast.FullPerm | _: ast.NoPerm @@ -124,7 +128,7 @@ object evaluator extends EvaluationRules { reserveHeaps = Nil, exhaleExt = false) - eval2(s1, e, pve, v)((s2, t, eNew, v1) => { + eval2(s1, e, pve, v, analysisInfos)((s2, t, eNew, v1) => { val s3 = if (s2.recordPossibleTriggers) e match { @@ -142,7 +146,7 @@ object evaluator extends EvaluationRules { Q(s4, t, eNew, v1)}) } - protected def eval2(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier) + protected def eval2(s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { val eOpt = Option.when(withExp)(e) @@ -153,9 +157,9 @@ object evaluator extends EvaluationRules { case _: ast.NullLit => Q(s, Null, eOpt, v) case ast.IntLit(bigval) => Q(s, IntLiteral(bigval), eOpt, v) - case ast.EqCmp(e0, e1) => evalBinOp(s, e0, e1, Equals, pve, v)((s1, t, e0New, e1New, v1) => + case ast.EqCmp(e0, e1) => evalBinOp(s, e0, e1, Equals, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, Option.when(withExp)(ast.EqCmp(e0New.get, e1New.get)(e.pos, e.info, e.errT)), v1)) - case ast.NeCmp(e0, e1) => evalBinOp(s, e0, e1, (p0: Term, p1: Term) => Not(Equals(p0, p1)), pve, v)((s1, t, e0New, e1New, v1) => + case ast.NeCmp(e0, e1) => evalBinOp(s, e0, e1, (p0: Term, p1: Term) => Not(Equals(p0, p1)), pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, Option.when(withExp)(ast.NeCmp(e0New.get, e1New.get)(e.pos, e.info, e.errT)), v1)) case x: ast.LocalVarWithVersion => @@ -170,8 +174,8 @@ object evaluator extends EvaluationRules { case ast.FractionalPerm(e0, e1) => var t1: Term = null - evalBinOp(s, e0, e1, (t0, _t1) => {t1 = _t1; FractionPerm(t0, t1)}, pve, v)((s1, tFP, e0New, e1New, v1) => - failIfDivByZero(s1, tFP, e1, e1New, t1, predef.Zero, pve, v1)((s2, t, v2) + evalBinOp(s, e0, e1, (t0, _t1) => {t1 = _t1; FractionPerm(t0, t1)}, pve, v, analysisInfos)((s1, tFP, e0New, e1New, v1) => + failIfDivByZero(s1, tFP, e1, e1New, t1, predef.Zero, pve, v1, analysisInfos)((s2, t, v2) => Q(s2, t, e0New.map(ast.FractionalPerm(_, e1New.get)(e.pos, e.info, e.errT)), v2))) case _: ast.WildcardPerm if s.assertReadAccessOnly => @@ -182,7 +186,7 @@ object evaluator extends EvaluationRules { case _: ast.WildcardPerm => val (tVar, tConstraints, eVar) = v.decider.freshARP() val constraintExp = Option.when(withExp)(DebugExp.createInstance(s"${eVar.get.toString} > none", true)) - v.decider.assumeDefinition(tConstraints, constraintExp) + v.decider.assumeDefinition(tConstraints, constraintExp, analysisInfos) /* TODO: Only record wildcards in State.constrainableARPs that are used in exhale * position. Currently, wildcards used in inhale position (only) may not be removed * from State.constrainableARPs (potentially inefficient, but should be sound). @@ -200,10 +204,10 @@ object evaluator extends EvaluationRules { Q(s1, tVar, eVar, v) case fa: ast.FieldAccess => - eval(s, fa.rcv, pve, v)((s1, tRcvr, eRcvr, v1) => { + eval(s, fa.rcv, pve, v, analysisInfos)((s1, tRcvr, eRcvr, v1) => { val ve = pve dueTo InsufficientPermission(fa) - v.heapSupporter.evalFieldAccess(s1, fa, tRcvr, eRcvr, ve, v1)((s2, snap, v2) => { + v.heapSupporter.evalFieldAccess(s1, fa, tRcvr, eRcvr, ve, v1, analysisInfos)((s2, snap, v2) => { val (debugHeapName, debugLabel) = v1.getDebugOldLabel(s2, fa.pos, Some(magicWandSupporter.getEvalHeap(s2))) val newFa = Option.when(withExp)({ if (s1.isEvalInOld) ast.FieldAccess(eRcvr.get, fa.field)(fa.pos, fa.info, fa.errT) @@ -217,15 +221,15 @@ object evaluator extends EvaluationRules { }) case ast.Not(e0) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, Not(t0), e0New.map(ast.Not(_)(e.pos, e.info, e.errT)), v1)) case ast.Minus(e0) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, Minus(0, t0), e0New.map(ast.Minus(_)(e.pos, e.info, e.errT)), v1)) case ast.Old(e0) => - evalInOldState(s, Verifier.PRE_STATE_LABEL, e0, pve, v)((s1, t0, e0New, v1) => + evalInOldState(s, Verifier.PRE_STATE_LABEL, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, t0, e0New.map(ast.Old(_)(e.pos, e.info, e.errT)), v1)) case old@ast.DebugLabelledOld(e0, lbl) => @@ -235,28 +239,34 @@ object evaluator extends EvaluationRules { lbl s.oldHeaps.get(heapName) match { case None => - createFailure(pve dueTo LabelledStateNotReached(ast.LabelledOld(e0, heapName)(old.pos, old.info, old.errT)), v, s, "labelled state reached") + val failure = createFailure(pve dueTo LabelledStateNotReached(ast.LabelledOld(e0, heapName)(old.pos, old.info, old.errT)), v, s, "labelled state reached") + if (s.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + val freshVar = v.decider.fresh(v.symbolConverter.toSort(old.typ), None) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, freshVar, None, v) else failure case _ => - evalInOldState(s, heapName, e0, pve, v)((s1, t0, _, v1) => + evalInOldState(s, heapName, e0, pve, v, analysisInfos)((s1, t0, _, v1) => Q(s1, t0, Some(old), v1)) } case old @ ast.LabelledOld(e0, lbl) => s.oldHeaps.get(lbl) match { case None => - createFailure(pve dueTo LabelledStateNotReached(old), v, s, "labelled state reached") + val failure = createFailure(pve dueTo LabelledStateNotReached(old), v, s, "labelled state reached") + if (s.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + val freshVar = v.decider.fresh(v.symbolConverter.toSort(old.typ), None) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, freshVar, None, v) else failure case _ => - evalInOldState(s, lbl, e0, pve, v)((s1, t0, e0New, v1) => + evalInOldState(s, lbl, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, t0, e0New.map(ast.LabelledOld(_, lbl)(old.pos, old.info, old.errT)), v1))} case l@ast.Let(x, e0, e1) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => { + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => { val t = v1.decider.appliedFresh("letvar", v1.symbolConverter.toSort(x.typ), s1.relevantQuantifiedVariables.map(_._1)) val debugExp = Option.when(withExp)(DebugExp.createInstance("letvar assignment", InsertionOrderedSet(DebugExp.createInstance(ast.EqCmp(x.localVar, e0)(), ast.EqCmp(x.localVar, e0New.get)())))) - v1.decider.assumeDefinition(BuiltinEquals(t, t0), debugExp) + v1.decider.assumeDefinition(BuiltinEquals(t, t0), debugExp, analysisInfos) val newFuncRec = s1.functionRecorder.recordFreshSnapshot(t.applicable.asInstanceOf[Function]).enterLet(l) val possibleTriggersBefore = if (s1.recordPossibleTriggers) s1.possibleTriggers else Map.empty - eval(s1.copy(g = s1.g + (x.localVar, (t0, e0New)), functionRecorder = newFuncRec), e1, pve, v1)((s2, t2, e1New, v2) => { + eval(s1.copy(g = s1.g + (x.localVar, (t0, e0New)), functionRecorder = newFuncRec), e1, pve, v1, analysisInfos)((s2, t2, e1New, v2) => { val newPossibleTriggers = if (s2.recordPossibleTriggers) { val addedTriggers = s2.possibleTriggers -- possibleTriggersBefore.keys val addedTriggersReplaced = addedTriggers.map(at => at._1.replace(x.localVar, e0) -> at._2) @@ -271,30 +281,30 @@ object evaluator extends EvaluationRules { /* Strict evaluation of AND */ case ast.And(e0, e1) if Verifier.config.disableShortCircuitingEvaluations() => - evalBinOp(s, e0, e1, (t1, t2) => And(t1, t2), pve, v)((s1, t, e0New, e1New, v1) => + evalBinOp(s, e0, e1, (t1, t2) => And(t1, t2), pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(ast.And(_, e1New.get)(e.pos, e.info, e.errT)), v1)) /* Short-circuiting evaluation of AND */ case ae @ ast.And(_, _) => val flattened = flattenOperator(ae, {case ast.And(e0, e1) => Seq(e0, e1)}) - evalSeqShortCircuit(And, s, flattened, pve, v)(Q) + evalSeqShortCircuit(And, s, flattened, pve, v, analysisInfos)(Q) /* Strict evaluation of OR */ case ast.Or(e0, e1) if Verifier.config.disableShortCircuitingEvaluations() => - evalBinOp(s, e0, e1, (t1, t2) => Or(t1, t2), pve, v)((s1, t, e0New, e1New, v1) => + evalBinOp(s, e0, e1, (t1, t2) => Or(t1, t2), pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(ast.Or(_, e1New.get)(e.pos, e.info, e.errT)), v1)) /* Short-circuiting evaluation of OR */ case oe @ ast.Or(_, _) => val flattened = flattenOperator(oe, {case ast.Or(e0, e1) => Seq(e0, e1)}) - evalSeqShortCircuit(Or, s, flattened, pve, v)(Q) + evalSeqShortCircuit(Or, s, flattened, pve, v, analysisInfos)(Q) case implies @ ast.Implies(e0, e1) => val impliesRecord = new ImpliesRecord(implies, s, v.decider.pcs, "Implies") val uidImplies = v.symbExLog.openScope(impliesRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - evalImplies(s1, t0, (e0, e0New), e1, implies.info == FromShortCircuitingAnd, pve, v1)((s2, t1, e1New, v2) => { + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + evalImplies(s1, t0, (e0, e0New), e1, implies.info == FromShortCircuitingAnd, pve, v1, analysisInfos)((s2, t1, e1New, v2) => { v2.symbExLog.closeScope(uidImplies) val implExpP = e0New.map(ast.Implies(_, e1New.get)(e.pos, e.info, e.errT)) Q(s2, t1, implExpP, v2) @@ -303,11 +313,11 @@ object evaluator extends EvaluationRules { case condExp @ ast.CondExp(e0, e1, e2) => val condExpRecord = new CondExpRecord(condExp, s, v.decider.pcs, "CondExp") val uidCondExp = v.symbExLog.openScope(condExpRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s1, v1)((s2, v2, QB) => - brancher.branch(s2.copy(parallelizeBranches = false), t0, (e0, e0New), v2)( - (s3, v3) => eval(s3.copy(parallelizeBranches = s2.parallelizeBranches), e1, pve, v3)((s4, t4, e4, v4) => QB(s4, (t4, e4), v4)), - (s3, v3) => eval(s3.copy(parallelizeBranches = s2.parallelizeBranches), e2, pve, v3)((s4, t4, e4, v4) => QB(s4, (t4, e4), v4))) + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s1, v1, analysisInfos)((s2, v2, QB) => + brancher.branch(s2.copy(parallelizeBranches = false), t0, (e0, e0New), v2, analysisInfos.withDependencyType(AssumptionType.Internal))( + (s3, v3) => eval(s3.copy(parallelizeBranches = s2.parallelizeBranches), e1, pve, v3, analysisInfos)((s4, t4, e4, v4) => QB(s4, (t4, e4), v4)), + (s3, v3) => eval(s3.copy(parallelizeBranches = s2.parallelizeBranches), e2, pve, v3, analysisInfos)((s4, t4, e4, v4) => QB(s4, (t4, e4), v4))) )(entries => { /* TODO: If branch(...) took orElse-continuations that are executed if a branch is dead, then then comparisons with t0/Not(t0) wouldn't be necessary. */ @@ -329,88 +339,88 @@ object evaluator extends EvaluationRules { /* Integers */ case ast.Add(e0, e1) => - evalBinOp(s, e0, e1, Plus, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.Add(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Plus, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.Add(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.Sub(e0, e1) => - evalBinOp(s, e0, e1, Minus, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.Sub(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Minus, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.Sub(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.Mul(e0, e1) => - evalBinOp(s, e0, e1, Times, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.Mul(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Times, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.Mul(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.Div(e0, e1) => - evalBinOp(s, e0, e1, Div, pve, v)((s1, tDiv, e0New, e1New, v1) => - failIfDivByZero(s1, tDiv, e1, e1New, tDiv.p1, 0, pve, v1)((s2, t, v2) + evalBinOp(s, e0, e1, Div, pve, v, analysisInfos)((s1, tDiv, e0New, e1New, v1) => + failIfDivByZero(s1, tDiv, e1, e1New, tDiv.p1, 0, pve, v1, analysisInfos)((s2, t, v2) => Q(s2, t, e0New.map(e0p => ast.Div(e0p, e1New.get)(e.pos, e.info, e.errT)), v2))) case ast.Mod(e0, e1) => - evalBinOp(s, e0, e1, Mod, pve, v)((s1, tMod, e0New, e1New, v1) => - failIfDivByZero(s1, tMod, e1, e1New, tMod.p1, 0, pve, v1)((s2, t, v2) + evalBinOp(s, e0, e1, Mod, pve, v, analysisInfos)((s1, tMod, e0New, e1New, v1) => + failIfDivByZero(s1, tMod, e1, e1New, tMod.p1, 0, pve, v1, analysisInfos)((s2, t, v2) => Q(s2, t, e0New.map(e0p => ast.Mod(e0p, e1New.get)(e.pos, e.info, e.errT)), v2))) case ast.LeCmp(e0, e1) => - evalBinOp(s, e0, e1, AtMost, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.LeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, AtMost, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.LeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.LtCmp(e0, e1) => - evalBinOp(s, e0, e1, Less, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.LtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Less, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.LtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.GeCmp(e0, e1) => - evalBinOp(s, e0, e1, AtLeast, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.GeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, AtLeast, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.GeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.GtCmp(e0, e1) => - evalBinOp(s, e0, e1, Greater, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.GtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Greater, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.GtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) /* Permissions */ case ast.PermAdd(e0, e1) => - evalBinOp(s, e0, e1, PermPlus, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermAdd(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, PermPlus, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermAdd(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.PermSub(e0, e1) => - evalBinOp(s, e0, e1, PermMinus, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermSub(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, PermMinus, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermSub(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.PermMinus(e0) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, PermMinus(NoPerm, t0), e0New.map(e0p => ast.PermMinus(e0p)(e.pos, e.info, e.errT)), v1)) case ast.PermMul(e0, e1) => - evalBinOp(s, e0, e1, PermTimes, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermMul(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, PermTimes, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermMul(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.DebugPermMin(e0, e1) => - evalBinOp(s, e0, e1, PermMin, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.DebugPermMin(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, PermMin, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.DebugPermMin(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.IntPermMul(e0, e1) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - eval(s1, e1, pve, v1)((s2, t1, e1New, v2) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + eval(s1, e1, pve, v1, analysisInfos)((s2, t1, e1New, v2) => Q(s2, IntPermTimes(t0, t1), e0New.map(e0p => ast.IntPermMul(e0p, e1New.get)(e.pos, e.info, e.errT)), v2))) case ast.PermDiv(e0, e1) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - eval(s1, e1, pve, v1)((s2, t1, e1New, v2) => - failIfDivByZero(s2, PermIntDiv(t0, t1), e1, e1New, t1, 0, pve, v2)((s3, t, v3) + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + eval(s1, e1, pve, v1, analysisInfos)((s2, t1, e1New, v2) => + failIfDivByZero(s2, PermIntDiv(t0, t1), e1, e1New, t1, 0, pve, v2, analysisInfos)((s3, t, v3) => Q(s3, t, e0New.map(e0p => ast.PermDiv(e0p, e1New.get)(e.pos, e.info, e.errT)), v3)))) case ast.PermPermDiv(e0, e1) => - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - eval(s1, e1, pve, v1)((s2, t1, e1New, v2) => - failIfDivByZero(s2, PermPermDiv(t0, t1), e1, e1New, t1, FractionPermLiteral(Rational(0, 1)), pve, v2)((s3, t, v3) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + eval(s1, e1, pve, v1, analysisInfos)((s2, t1, e1New, v2) => + failIfDivByZero(s2, PermPermDiv(t0, t1), e1, e1New, t1, FractionPermLiteral(Rational(0, 1)), pve, v2, analysisInfos)((s3, t, v3) => Q(s3, t, e0New.map(e0p => ast.PermPermDiv(e0p, e1New.get)(e.pos, e.info, e.errT)), v3)))) case ast.PermLeCmp(e0, e1) => - evalBinOp(s, e0, e1, AtMost, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermLeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, AtMost, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermLeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.PermLtCmp(e0, e1) => - evalBinOp(s, e0, e1, Less, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermLtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Less, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermLtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.PermGeCmp(e0, e1) => - evalBinOp(s, e0, e1, AtLeast, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermGeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, AtLeast, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermGeCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.PermGtCmp(e0, e1) => - evalBinOp(s, e0, e1, Greater, pve, v)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermGtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) + evalBinOp(s, e0, e1, Greater, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.PermGtCmp(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) /* Others */ /* Domains not handled directly */ case dfa @ ast.DomainFuncApp(funcName, eArgs, m) => - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => { + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => { val inSorts = tArgs map (_.sort) val outSort = v1.symbolConverter.toSort(dfa.typ) val fi = v1.symbolConverter.toFunction(s.program.findDomainFunction(funcName), inSorts :+ outSort, s.program) @@ -418,7 +428,7 @@ object evaluator extends EvaluationRules { Q(s1, App(fi, tArgs), dfaP, v1)}) case bf @ ast.BackendFuncApp(funcName, eArgs) => - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => { + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => { val func = s.program.findDomainFunction(funcName) val fi = v1.symbolConverter.toFunction(func, s.program) val bfP = Option.when(withExp)(ast.BackendFuncApp(funcName, eArgsNew.get)(bf.pos, bf.info, bf.typ, bf.interpretation, bf.errT)) @@ -426,8 +436,8 @@ object evaluator extends EvaluationRules { case ast.CurrentPerm(resacc) => val h = s.partiallyConsumedHeap.getOrElse(s.h) - evalResourceAccess(s, resacc, pve, v)((s1, identifier, args, eArgsNew, v1) => { - v1.heapSupporter.evalCurrentPerm(s1, h, resacc, identifier, args, eArgsNew, v1)((s2, t, v2) => + evalResourceAccess(s, resacc, pve, v, analysisInfos)((s1, identifier, args, eArgsNew, v1) => { + v1.heapSupporter.evalCurrentPerm(s1, h, resacc, identifier, args, eArgsNew, v1, analysisInfos)((s2, t, v2) => Q(s2, t, Option.when(withExp)(e), v2)) }) @@ -448,7 +458,7 @@ object evaluator extends EvaluationRules { val s2 = s1.copy(s1.g + gVars, quantifiedVariables = varPairs.map(v => v._1 -> Option.when(withExp)(v._2)) ++ s1.quantifiedVariables) - evals(s2, args, _ => pve, v)((s3, ts, es, v3) => { + evals(s2, args, _ => pve, v, analysisInfos)((s3, ts, es, v3) => { val possibleConds = v3.heapSupporter.collectForPermConditions(s3, resource, varPairs, ts, es) def evalOptions(s: State, @@ -461,7 +471,7 @@ object evaluator extends EvaluationRules { val impliesRecord = new ImpliesRecord(null, s, v.decider.pcs, "ForPerm") val uidImplies = v.symbExLog.openScope(impliesRecord) val (t, (e0, e1), qVars, defs, triggers) = conds.head - evalImplies(s.copy(g = s.g + defs), t, (e0, e1), body, false, pve, v)((sNext, tImplies, bodyNew, vNext) => { + evalImplies(s.copy(g = s.g + defs), t, (e0, e1), body, false, pve, v, analysisInfos)((sNext, tImplies, bodyNew, vNext) => { val tQuant = SimplifyingForall(qVars, tImplies, triggers) val eQuantNew = Option.when(withExp)(ast.Forall(varsNew.get, Seq(), ast.Implies(e0, bodyNew.get)())()) v.symbExLog.closeScope(uidImplies) @@ -528,18 +538,18 @@ object evaluator extends EvaluationRules { } val name = s"prog.$posString" val s0 = s.copy(functionRecorder = s.functionRecorder.enterQuantifiedExp(sourceQuant)) - evalQuantified(s0, qantOp, eQuant.variables, Nil, Seq(body), Some(eTriggers), name, pve, v){ + evalQuantified(s0, qantOp, eQuant.variables, Nil, Seq(body), Some(eTriggers), name, pve, v, analysisInfos) { case (s1, tVars, eVars, _, _, Some((Seq(tBody), bodyNew, tTriggers, (tAuxGlobal, tAux), auxExps)), v1) => val tAuxHeapIndep = tAux.flatMap(v.quantifierSupporter.makeTriggersHeapIndependent(_, v1.decider.fresh)) val auxGlobalsExp = auxExps.map(_._1) val auxNonGlobalsExp = auxExps.map(_._2) val commentGlobal = "Nested auxiliary terms: globals (aux)" v1.decider.prover.comment(commentGlobal) - v1.decider.assume(tAuxGlobal, Option.when(withExp)(DebugExp.createInstance(description=commentGlobal, children=auxGlobalsExp.get)), enforceAssumption = false) + val auxAnalysisInfos = analysisInfos.withDependencyType(AssumptionType.Internal).withMergeInfo(NoDependencyAnalysisMerge()) + v1.decider.assume(tAuxGlobal, Option.when(withExp)(DebugExp.createInstance(description=commentGlobal, children=auxGlobalsExp.get)), enforceAssumption = false, auxAnalysisInfos) val commentNonGlobals = "Nested auxiliary terms: non-globals (aux)" v1.decider.prover.comment(commentNonGlobals) - v1.decider.assume(tAuxHeapIndep/*tAux*/, Option.when(withExp)(DebugExp.createInstance(description=commentNonGlobals, children=auxNonGlobalsExp.get)), enforceAssumption = false) - + v1.decider.assume(tAuxHeapIndep/*tAux*/, Option.when(withExp)(DebugExp.createInstance(description=commentNonGlobals, children=auxNonGlobalsExp.get)), enforceAssumption = false, auxAnalysisInfos) if (qantOp == Exists) { // For universal quantification, the non-global auxiliary assumptions will contain the information that // forall vars :: all function preconditions are fulfilled. @@ -552,7 +562,7 @@ object evaluator extends EvaluationRules { val exp = ast.Forall(eQuant.variables, eTriggers, body)(sourceQuant.pos, sourceQuant.info, sourceQuant.errT) DebugExp.createInstance(exp, expNew) }) - v1.decider.assume(Quantification(Forall, tVars, FunctionPreconditionTransformer.transform(tBody, s1.program), tTriggers, name, quantWeight), debugExp) + v1.decider.assume(Quantification(Forall, tVars, FunctionPreconditionTransformer.transform(tBody, s1.program), tTriggers, name, quantWeight), debugExp, analysisInfos) } val tQuant = Quantification(qantOp, tVars, tBody, tTriggers, name, quantWeight) @@ -561,16 +571,37 @@ object evaluator extends EvaluationRules { Q(s2, tQuant, eQuantNew, v1) case (s1, _, _, _, _, None, v1) => // This should not happen unless the current path is dead. - if (v1.decider.checkSmoke(true)) { - Unreachable() + if (v1.decider.checkSmoke(analysisInfos, isAssert = true)) { + if (Verifier.config.analyzeInfeasiblePaths) { + val freshVar = v1.decider.fresh(v1.symbolConverter.toSort(sourceQuant.typ), None) + Q(s1, freshVar, None, v1) + } else Unreachable() } else { - createFailure(pve.dueTo(InternalReason(sourceQuant, "Quantifier evaluation failed.")), v1, s1, "quantifier could be evaluated") + val failure = createFailure(pve.dueTo(InternalReason(sourceQuant, "Quantifier evaluation failed.")), v1, s1, "quantifier could be evaluated") + if (s1.retryLevel == 0) v1.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v1.reportFurtherErrors()) + val freshVar = v1.decider.fresh(v1.symbolConverter.toSort(sourceQuant.typ), None) + if (s1.retryLevel == 0 && v1.reportFurtherErrors()) failure combine Q(s1, freshVar, None, v1) else failure } } case fapp @ ast.FuncApp(funcName, eArgs) => val func = s.program.findFunction(funcName) - evals2(s, eArgs, Nil, _ => pve, v)((s1, tArgs, eArgsNew, v1) => { + val pres = func.pres.map(_.transform { + /* [Malte 2018-08-20] Two examples of the test suite, one of which is the regression + * for Carbon issue #210, fail if the subsequent code that strips out triggers from + * exhaled function preconditions, is commented. The code was originally a work-around + * for Silicon issue #276. Removing triggers from function preconditions is OK-ish + * because they are consumed (exhaled), i.e. asserted. However, the triggers are + * also used to internally generated quantifiers, e.g. related to QPs. My hope is that + * this hack is no longer needed once heap-dependent triggers are supported. + */ + case q: ast.Forall => q.copy(triggers = Nil)(q.pos, q.info, q.errT) + }) + val fappSourceInfo = DependencyAnalysisSourceInfo.createAnalysisSourceInfo(fapp) + val presWithDAInfo = DependencyAnalysisMergeInfo.attachExpMergeInfo(pres.flatMap(_.topLevelConjuncts), Some(fappSourceInfo)) + val eArgsWithDAInfO = DependencyAnalysisMergeInfo.attachExpMergeInfo(eArgs, None) + + evals2(s, eArgsWithDAInfO, Nil, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => { // bookkeeper.functionApplications += 1 val joinFunctionArgs = tArgs //++ c2a.quantifiedVariables.filterNot(tArgs.contains) val (debugHeapName, debugLabel) = v1.getDebugOldLabel(s1, fapp.pos) @@ -592,18 +623,8 @@ object evaluator extends EvaluationRules { * Hence, the joinedFApp will take two arguments, namely, i*i and i, * although the latter is not necessary. */ - joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s1a, v1)((s2, v2, QB) => { - val pres = func.pres.map(_.transform { - /* [Malte 2018-08-20] Two examples of the test suite, one of which is the regression - * for Carbon issue #210, fail if the subsequent code that strips out triggers from - * exhaled function preconditions, is commented. The code was originally a work-around - * for Silicon issue #276. Removing triggers from function preconditions is OK-ish - * because they are consumed (exhaled), i.e. asserted. However, the triggers are - * also used to internally generated quantifiers, e.g. related to QPs. My hope is that - * this hack is no longer needed once heap-dependent triggers are supported. - */ - case q: ast.Forall => q.copy(triggers = Nil)(q.pos, q.info, q.errT) - }) + joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s1a, v1, analysisInfos)((s2, v2, QB) => { + /* Formal function arguments are instantiated with the corresponding actual arguments * by adding the corresponding bindings to the store. To avoid formals in error messages * and to report actuals instead, we have two choices: the first is two attach a reason @@ -650,13 +671,14 @@ object evaluator extends EvaluationRules { moreJoins = JoinMode.Off, assertReadAccessOnly = if (Verifier.config.respectFunctionPrePermAmounts()) s2.assertReadAccessOnly /* should currently always be false */ else true) - consumes(s3, pres, true, _ => pvePre, v2)((s4, snap, v3) => { + val precondAnalysisInfos = analysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Source, EdgeType.Up)) + consumes(s3, presWithDAInfo, true, _ => pvePre, v2, precondAnalysisInfos)((s4, snap, v3) => { val snap1 = snap.get.convert(sorts.Snap) val preFApp = App(functionSupporter.preconditionVersion(v3.symbolConverter.toFunction(func)), snap1 :: tArgs) val preExp = Option.when(withExp)({ DebugExp.createInstance(Some(s"precondition of ${func.name}(${eArgsNew.get.mkString(", ")}) holds"), None, None, InsertionOrderedSet.empty) }) - v3.decider.assume(preFApp, preExp) + v3.decider.assume(preFApp, preExp, analysisInfos) val funcAnn = func.info.getUniqueInfo[AnnotationInfo] val tFApp = funcAnn match { case Some(a) if a.values.contains("opaque") => @@ -685,10 +707,13 @@ object evaluator extends EvaluationRules { /* TODO: The join-function is heap-independent, and it is not obvious how a * joined snapshot could be defined and represented */ - })(join(func.typ, s"joined_${func.name}", joinFunctionArgs, joinExp, v1))((s6, r, v4) - => Q(s6, r._1, r._2, v4)) - }) - + })(join(func.typ, s"joined_${func.name}", joinFunctionArgs, joinExp, v1, analysisInfos))((s6, r, v4) + => { + DependencyAnalyzer.addCustomDependenciesBetweenMergeInfos(v4.decider, eArgsWithDAInfO, presWithDAInfo) + DependencyAnalyzer.addCustomDependenciesBetweenMergeInfos(v4.decider, presWithDAInfo, Seq(DependencyAnalysisMergeInfo.attachExpMergeInfo(fapp, analysisInfos.getMergeInfo))) + Q(s6, r._1, r._2, v4) + })} + ) case uf@ast.Unfolding( acc @ ast.PredicateAccessPredicate(pa @ ast.PredicateAccess(eArgs, predicateName), ePerm), eIn) => @@ -696,8 +721,8 @@ object evaluator extends EvaluationRules { val predicate = s.program.findPredicate(predicateName) if (s.cycles(predicate) < Verifier.config.recursivePredicateUnfoldings()) { v.decider.startDebugSubExp() - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => - eval(s1, ePerm.getOrElse(ast.FullPerm()()), pve, v1)((s2, tPerm, ePermNew, v2) => { + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => + eval(s1, ePerm.getOrElse(ast.FullPerm()()), pve, v1, analysisInfos)((s2, tPerm, ePermNew, v2) => { val (debugHeapName, debugLabel) = v1.getDebugOldLabel(s2, uf.pos) val unfoldingNew = eArgsNew.map(args => uf.copy(acc = acc.copy(loc = pa.copy(args = args)(pa.pos, pa.info, pa.errT), @@ -707,9 +732,9 @@ object evaluator extends EvaluationRules { else ast.DebugLabelledOld(unfoldingNew.get, debugLabel)(uf.pos, uf.info, uf.errT) }) val s2a = if (Verifier.config.enableDebugging()) s2.copy(oldHeaps = s2.oldHeaps + (debugHeapName -> s2.h)) else s2 - v2.decider.assert(IsPositive(tPerm)) { // TODO: Replace with permissionSupporter.assertNotNegative + v2.decider.assert(IsPositive(tPerm), analysisInfos) { // TODO: Replace with permissionSupporter.assertNotNegative case true => - joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s2a, v2)((s3, v3, QB) => { + joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s2a, v2, analysisInfos)((s3, v3, QB) => { val s4 = s3.incCycleCounter(predicate) .copy(recordVisited = true) /* [2014-12-10 Malte] The commented code should replace the code following @@ -721,7 +746,7 @@ object evaluator extends EvaluationRules { // val c4 = c3.decCycleCounter(predicate) // eval(σ1, eIn, pve, c4)((tIn, c5) => // QB(tIn, c5))}) - consume(s4, acc, true, pve, v3)((s5, snap, v4) => { + consume(s4, acc, true, pve, v3, analysisInfos)((s5, snap, v4) => { val fr6 = s5.functionRecorder.recordSnapshot(pa, v4.decider.pcs.branchConditions, snap.get) .changeDepthBy(+1) @@ -736,7 +761,7 @@ object evaluator extends EvaluationRules { if (!Verifier.config.disableFunctionUnfoldTrigger()) { val eArgsString = eArgsNew.mkString(", ") val debugExp = Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eArgsString))", isInternal_ = true)) - v4.decider.assume(App(s.predicateData(predicate.name).triggerFunction, snap.get.convert(terms.sorts.Snap) +: tArgs), debugExp) + v4.decider.assume(App(s.predicateData(predicate.name).triggerFunction, snap.get.convert(terms.sorts.Snap) +: tArgs), debugExp, analysisInfos.withDependencyType(AssumptionType.Trigger)) } val body = predicate.body.get /* Only non-abstract predicates can be unfolded */ val s7 = s6.scalePermissionFactor(tPerm, ePermNew) @@ -746,7 +771,7 @@ object evaluator extends EvaluationRules { if (s7a.predicateData(predicate.name).predContents.isDefined) { val toReplace: silicon.Map[Term, Term] = silicon.Map.from(s7a.predicateData(predicate.name).params.get.zip(Seq(snap.get) ++ tArgs)) - predicateSupporter.producePredicateContents(s7a, s7a.predicateData(predicate.name).predContents.get, toReplace, v4, true)((s8, v5) => { + predicateSupporter.producePredicateContents(s7a, s7a.predicateData(predicate.name).predContents.get, toReplace, v4, analysisInfos.withDependencyType(AssumptionType.Internal), true)((s8, v5) => { val s9 = s8.copy(g = s7.g, functionRecorder = s8.functionRecorder.changeDepthBy(-1), recordVisited = s3.recordVisited, @@ -755,10 +780,10 @@ object evaluator extends EvaluationRules { constrainableARPs = s1.constrainableARPs) .decCycleCounter(predicate) val s10 = v5.stateConsolidator(s9).consolidateOptionally(s9, v5) - eval(s10, eIn, pve, v5)((s9, t9, e9, v9) => QB(s9, (t9, e9), v9)) + eval(s10, eIn, pve, v5, analysisInfos)((s9, t9, e9, v9) => QB(s9, (t9, e9), v9)) }) } else { - produce(s7a, toSf(snap.get), body, pve, v4)((s8, v5) => { + produce(s7a, toSf(snap.get), body, pve, v4, analysisInfos)((s8, v5) => { val s9 = s8.copy(g = s7.g, functionRecorder = s8.functionRecorder.changeDepthBy(-1), recordVisited = s3.recordVisited, @@ -767,17 +792,21 @@ object evaluator extends EvaluationRules { constrainableARPs = s1.constrainableARPs) .decCycleCounter(predicate) val s10 = v5.stateConsolidator(s9).consolidateOptionally(s9, v5) - eval(s10, eIn, pve, v5)((s9, t9, e9, v9) => QB(s9, (t9, e9), v9))}) + eval(s10, eIn, pve, v5, analysisInfos)((s9, t9, e9, v9) => QB(s9, (t9, e9), v9))}) } }) })(join(eIn.typ, "joined_unfolding", s2a.relevantQuantifiedVariables.map(_._1), - joinExp, v2))((s12, r12, v7) + joinExp, v2, analysisInfos))((s12, r12, v7) => { v7.decider.finishDebugSubExp(s"unfolded(${predicate.name})") Q(s12, r12._1, r12._2, v7)}) case false => v2.decider.finishDebugSubExp(s"unfolded(${predicate.name})") - createFailure(pve dueTo NonPositivePermission(ePerm.get), v2, s2a, IsPositive(tPerm), ePermNew.map(p => ast.PermGtCmp(p, ast.NoPerm()())(p.pos, p.info, p.errT)))}})) + val failure = createFailure(pve dueTo NonPositivePermission(ePerm.get), v2, s2a, IsPositive(tPerm), ePermNew.map(p => ast.PermGtCmp(p, ast.NoPerm()())(p.pos, p.info, p.errT))) + if (s2a.retryLevel == 0) v2.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v2.reportFurtherErrors()) + val freshVar = v2.decider.fresh(v2.symbolConverter.toSort(e.typ), None) + if (s2a.retryLevel == 0 && v2.reportFurtherErrors() && Verifier.config.analyzeInfeasiblePaths) failure combine Q(s2a, freshVar, None, v2) else failure + }})) } else { val unknownValue = v.decider.appliedFresh("recunf", v.symbolConverter.toSort(eIn.typ), s.relevantQuantifiedVariables.map(_._1)) val newFuncRec = s.functionRecorder.recordFreshSnapshot(unknownValue.applicable.asInstanceOf[Function]) @@ -791,32 +820,32 @@ object evaluator extends EvaluationRules { else ast.DebugLabelledOld(apl, debugLabel)(apl.pos, apl.info, apl.errT) }) val sa = if (Verifier.config.enableDebugging()) s.copy(oldHeaps = s.oldHeaps + (debugHeapName -> s.h)) else s - joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](sa, v)((s1, v1, QB) => - magicWandSupporter.applyWand(s1, wand, pve, v1)((s2, v2) => { - eval(s2, eIn, pve, v2)((s3, t, eInNew, v3) => QB(s3, (t, eInNew), v3)) + joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](sa, v, analysisInfos)((s1, v1, QB) => + magicWandSupporter.applyWand(s1, wand, pve, v1, analysisInfos)((s2, v2) => { + eval(s2, eIn, pve, v2, analysisInfos)((s3, t, eInNew, v3) => QB(s3, (t, eInNew), v3)) }))(join(eIn.typ, "joined_applying", s.relevantQuantifiedVariables.map(_._1), - joinExp, v))((s4, r4, v4) + joinExp, v, analysisInfos))((s4, r4, v4) => Q(s4, r4._1, r4._2, v4)) case ast.Asserting(eAss, eIn) => - consume(s, eAss, false, pve, v)((s2, _, v2) => { + consume(s, eAss, false, pve, v, analysisInfos )((s2, _, v2) => { val s3 = s2.copy(g = s.g, h = s.h) - eval(s3, eIn, pve, v2)(Q) + eval(s3, eIn, pve, v2, analysisInfos)(Q) }) /* Sequences */ - case ast.SeqContains(e0, e1) => evalBinOp(s, e1, e0, SeqIn, pve, v)((s1, t, e1New, e0New, v1) => + case ast.SeqContains(e0, e1) => evalBinOp(s, e1, e0, SeqIn, pve, v, analysisInfos)((s1, t, e1New, e0New, v1) => Q(s1, t, e0New.map(e0p => ast.SeqContains(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) /* Note the reversed order of the arguments! */ case ast.SeqIndex(e0, e1) => - evals2(s, Seq(e0, e1), Nil, _ => pve, v)({case (s1, Seq(t0, t1), esNew, v1) => + evals2(s, Seq(e0, e1), Nil, _ => pve, v, analysisInfos)({case (s1, Seq(t0, t1), esNew, v1) => val eNew = esNew.map(es => ast.SeqIndex(es.head, es(1))(e.pos, e.info, e.errT)) if (s1.triggerExp) { Q(s1, SeqAt(t0, t1), eNew, v1) } else { - assertSeqIndexBounds(t1, t0, e1, e0, esNew, pve, s1, v1) match { + assertSeqIndexBounds(t1, t0, e1, e0, esNew, pve, s1, v1, analysisInfos) match { case Success() => Q(s1, SeqAt(t0, t1), eNew, v1) case failure: VerificationResult if s1.retryLevel == 0 && v1.reportFurtherErrors() => failure combine Q(s1, SeqAt(t0, t1), eNew, v1) @@ -824,25 +853,25 @@ object evaluator extends EvaluationRules { } }}) - case ast.SeqAppend(e0, e1) => evalBinOp(s, e0, e1, SeqAppend, pve, v)((s1, t, e0New, e1New, v1) => + case ast.SeqAppend(e0, e1) => evalBinOp(s, e0, e1, SeqAppend, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.SeqAppend(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case ast.SeqDrop(e0, e1) => evalBinOp(s, e0, e1, SeqDrop, pve, v)((s1, t, e0New, e1New, v1) => + case ast.SeqDrop(e0, e1) => evalBinOp(s, e0, e1, SeqDrop, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.SeqDrop(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case ast.SeqTake(e0, e1) => evalBinOp(s, e0, e1, SeqTake, pve, v)((s1, t, e0New, e1New, v1) => + case ast.SeqTake(e0, e1) => evalBinOp(s, e0, e1, SeqTake, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.SeqTake(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case ast.SeqLength(e0) => eval(s, e0, pve, v)((s1, t0, e0New, v1) => + case ast.SeqLength(e0) => eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, SeqLength(t0), e0New.map(e0p => ast.SeqLength(e0p)(e.pos, e.info, e.errT)), v1)) case ast.EmptySeq(typ) => Q(s, SeqNil(v.symbolConverter.toSort(typ)), Option.when(withExp)(e), v) - case ast.RangeSeq(e0, e1) => evalBinOp(s, e0, e1, SeqRanged, pve, v)((s1, t, e0New, e1New, v1) => + case ast.RangeSeq(e0, e1) => evalBinOp(s, e0, e1, SeqRanged, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.RangeSeq(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case ast.SeqUpdate(e0, e1, e2) => - evals2(s, Seq(e0, e1, e2), Nil, _ => pve, v)({ case (s1, Seq(t0, t1, t2), esNew, v1) => + evals2(s, Seq(e0, e1, e2), Nil, _ => pve, v, analysisInfos)({ case (s1, Seq(t0, t1, t2), esNew, v1) => val eNew = esNew.map(es => ast.SeqUpdate(es.head, es(1), es(2))(e.pos, e.info, e.errT)) if (s1.triggerExp) { Q(s1, SeqUpdate(t0, t1, t2), eNew, v1) } else { - assertSeqIndexBounds(t1, t0, e1, e0, esNew, pve, s1, v1) match { + assertSeqIndexBounds(t1, t0, e1, e0, esNew, pve, s1, v1, analysisInfos) match { case Success() => Q(s1, SeqUpdate(t0, t1, t2), eNew, v1) case failure: VerificationResult if s1.retryLevel == 0 && v1.reportFurtherErrors() => failure combine Q(s1, SeqUpdate(t0, t1, t2), eNew, v1) @@ -851,7 +880,7 @@ object evaluator extends EvaluationRules { }}) case seq@ast.ExplicitSeq(es) => - evals2(s, es, Nil, _ => pve, v)((s1, tEs, esNew, v1) => { + evals2(s, es, Nil, _ => pve, v, analysisInfos)((s1, tEs, esNew, v1) => { val tSeq = tEs.tail.foldLeft[SeqTerm](SeqSingleton(tEs.head))((tSeq, te) => SeqAppend(tSeq, SeqSingleton(te))) @@ -860,7 +889,7 @@ object evaluator extends EvaluationRules { val exp = ast.EqCmp(ast.SeqLength(seq)(), ast.IntLit(es.size)())(seq.pos, seq.info, seq.errT) DebugExp.createInstance(exp, expNew) }) - v1.decider.assume(SeqLength(tSeq) === IntLiteral(es.size), debugExp) + v1.decider.assume(SeqLength(tSeq) === IntLiteral(es.size), debugExp, analysisInfos) Q(s1, tSeq, esNew.map(en => ast.ExplicitSeq(en)(e.pos, e.info, e.errT)), v1)}) /* Sets and multisets */ @@ -871,68 +900,68 @@ object evaluator extends EvaluationRules { Q(s, EmptyMultiset(v.symbolConverter.toSort(typ)), Option.when(withExp)(e), v) case ast.ExplicitSet(es) => - evals2(s, es, Nil, _ => pve, v)((s1, tEs, esNew, v1) => { + evals2(s, es, Nil, _ => pve, v, analysisInfos)((s1, tEs, esNew, v1) => { val tSet = tEs.tail.foldLeft[SetTerm](SingletonSet(tEs.head))((tSet, te) => SetAdd(tSet, te)) Q(s1, tSet, esNew.map(es => ast.ExplicitSet(es)(e.pos, e.info, e.errT)), v1)}) case ast.ExplicitMultiset(es) => - evals2(s, es, Nil, _ => pve, v)((s1, tEs, esNew, v1) => { + evals2(s, es, Nil, _ => pve, v, analysisInfos)((s1, tEs, esNew, v1) => { val tMultiset = tEs.tail.foldLeft[MultisetTerm](SingletonMultiset(tEs.head))((tMultiset, te) => MultisetAdd(tMultiset, te)) Q(s1, tMultiset, esNew.map(es => ast.ExplicitMultiset(es)(e.pos, e.info, e.errT)), v1)}) case ast.AnySetUnion(e0, e1) => e.typ match { - case _: ast.SetType => evalBinOp(s, e0, e1, SetUnion, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.SetType => evalBinOp(s, e0, e1, SetUnion, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetUnion(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetUnion, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetUnion, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetUnion(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case _ => sys.error("Expected a (multi)set-typed expression but found %s (%s) of sort %s" .format(e, e.getClass.getName, e.typ)) } case ast.AnySetIntersection(e0, e1) => e.typ match { - case _: ast.SetType => evalBinOp(s, e0, e1, SetIntersection, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.SetType => evalBinOp(s, e0, e1, SetIntersection, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetIntersection(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetIntersection, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetIntersection, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetIntersection(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case _ => sys.error("Expected a (multi)set-typed expression but found %s (%s) of sort %s" .format(e, e.getClass.getName, e.typ)) } case ast.AnySetSubset(e0, e1) => e0.typ match { - case _: ast.SetType => evalBinOp(s, e0, e1, SetSubset, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.SetType => evalBinOp(s, e0, e1, SetSubset, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetSubset(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetSubset, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetSubset, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetSubset(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case _ => sys.error("Expected a (multi)set-typed expression but found %s (%s) of sort %s" .format(e, e.getClass.getName, e.typ)) } case ast.AnySetMinus(e0, e1) => e.typ match { - case _: ast.SetType => evalBinOp(s, e0, e1, SetDifference, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.SetType => evalBinOp(s, e0, e1, SetDifference, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetMinus(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetDifference, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.MultisetType => evalBinOp(s, e0, e1, MultisetDifference, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetMinus(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case _ => sys.error("Expected a (multi)set-typed expression but found %s (%s) of sort %s" .format(e, e.getClass.getName, e.typ)) } case ast.AnySetContains(e0, e1) => e1.typ match { - case _: ast.SetType => evalBinOp(s, e0, e1, SetIn, pve, v)((s1, t, e0New, e1New, v1) + case _: ast.SetType => evalBinOp(s, e0, e1, SetIn, pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetContains(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) - case _: ast.MultisetType => evalBinOp(s, e0, e1, (t0, t1) => MultisetCount(t1, t0), pve, v)((s1, t, e0New, e1New, v1) + case _: ast.MultisetType => evalBinOp(s, e0, e1, (t0, t1) => MultisetCount(t1, t0), pve, v, analysisInfos)((s1, t, e0New, e1New, v1) => Q(s1, t, e0New.map(e0p => ast.AnySetContains(e0p, e1New.get)(e.pos, e.info, e.errT)), v1)) case _ => sys.error("Expected a (multi)set-typed expression but found %s (%s) of sort %s" .format(e, e.getClass.getName, e.typ)) } case ast.AnySetCardinality(e0) => e0.typ match { - case _: ast.SetType => eval(s, e0, pve, v)((s1, t0, e0New, v1) + case _: ast.SetType => eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, SetCardinality(t0), e0New.map(e0p => ast.AnySetCardinality(e0p)(e.pos, e.info, e.errT)), v1)) - case _: ast.MultisetType => eval(s, e0, pve, v)((s1, t0, e0New, v1) + case _: ast.MultisetType => eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => Q(s1, MultisetCardinality(t0), e0New.map(e0p => ast.AnySetCardinality(e0p)(e.pos, e.info, e.errT)), v1)) case _ => sys.error("Expected a (multi)set-typed expression but found %s (%s) of type %s" .format(e0, e0.getClass.getName, e0.typ)) @@ -943,43 +972,39 @@ object evaluator extends EvaluationRules { case ast.EmptyMap(keyType, valueType) => Q(s, EmptyMap(v.symbolConverter.toSort(keyType), v.symbolConverter.toSort(valueType)), Option.when(withExp)(e), v) case em: ast.ExplicitMap => - eval(s, em.desugared, pve, v)((s1, t0, _, v1) => Q(s1, t0, Option.when(withExp)(em), v1)) + eval(s, em.desugared, pve, v, analysisInfos)((s1, t0, _, v1) => Q(s1, t0, Option.when(withExp)(em), v1)) case ast.MapCardinality(base) => - eval(s, base, pve, v)((s1, t0, baseNew, v1) => Q(s1, MapCardinality(t0), baseNew.map(ast.MapCardinality(_)(e.pos, e.info, e.errT)), v1)) + eval(s, base, pve, v, analysisInfos)((s1, t0, baseNew, v1) => Q(s1, MapCardinality(t0), baseNew.map(ast.MapCardinality(_)(e.pos, e.info, e.errT)), v1)) case ast.MapDomain(base) => - eval(s, base, pve, v)((s1, t0, baseNew, v1) => Q(s1, MapDomain(t0), baseNew.map(ast.MapDomain(_)(e.pos, e.info, e.errT)), v1)) + eval(s, base, pve, v, analysisInfos)((s1, t0, baseNew, v1) => Q(s1, MapDomain(t0), baseNew.map(ast.MapDomain(_)(e.pos, e.info, e.errT)), v1)) case ast.MapRange(base) => - eval(s, base, pve, v)((s1, t0, baseNew, v1) => Q(s1, MapRange(t0), baseNew.map(ast.MapRange(_)(e.pos, e.info, e.errT)), v1)) + eval(s, base, pve, v, analysisInfos)((s1, t0, baseNew, v1) => Q(s1, MapRange(t0), baseNew.map(ast.MapRange(_)(e.pos, e.info, e.errT)), v1)) case ml@ast.MapLookup(base, key) => - evals2(s, Seq(base, key), Nil, _ => pve, v)({ + evals2(s, Seq(base, key), Nil, _ => pve, v, analysisInfos)({ case (s1, Seq(baseT, keyT), esNew, v1) if s1.triggerExp => Q(s1, MapLookup(baseT, keyT), esNew.map(es => ast.MapLookup(es(0), es(1))(e.pos, e.info, e.errT)), v1) case (s1, Seq(baseT, keyT), esNew, v1) => val eNew = esNew.map(es => ast.MapLookup(es(0), es(1))(e.pos, e.info, e.errT)) - v1.decider.assert(SetIn(keyT, MapDomain(baseT))) { + v1.decider.assert(SetIn(keyT, MapDomain(baseT)), analysisInfos) { case true => Q(s1, MapLookup(baseT, keyT), eNew, v1) case false => val assertExp = Option.when(withExp)(ast.MapContains(key, base)(ml.pos, ml.info, ml.errT)) val assertExpNew = Option.when(withExp)(ast.MapContains(esNew.get(1), esNew.get(0))(ml.pos, ml.info, ml.errT)) val failure1 = createFailure(pve dueTo MapKeyNotContained(base, key), v1, s1, SetIn(keyT, MapDomain(baseT)), assertExpNew) - if (s1.retryLevel == 0 && v1.reportFurtherErrors()) { - v1.decider.assume(SetIn(keyT, MapDomain(baseT)), assertExp, assertExpNew) - failure1 combine Q(s1, MapLookup(baseT, keyT), eNew, v1) - } else { - failure1 - } + if (s1.retryLevel == 0) v1.decider.handleFailedAssertion(SetIn(keyT, MapDomain(baseT)), assertExp, assertExpNew, analysisInfos, v1.reportFurtherErrors()) + if (s1.retryLevel == 0 && v1.reportFurtherErrors()) failure1 combine Q(s1, MapLookup(baseT, keyT), eNew, v1) else failure1 } }) case ast.MapUpdate(base, key, value) => - evals2(s, Seq(base, key, value), Nil, _ => pve, v)({ + evals2(s, Seq(base, key, value), Nil, _ => pve, v, analysisInfos)({ case (s1, Seq(baseT, keyT, valueT), esNew, v1) => Q(s1, MapUpdate(baseT, keyT, valueT), esNew.map(es => ast.MapUpdate(es(0), es(1), es(2))(e.pos, e.info, e.errT)), v1) }) case ast.MapContains(key, base) => - evals2(s, Seq(key, base), Nil, _ => pve, v)({ + evals2(s, Seq(key, base), Nil, _ => pve, v, analysisInfos)({ case (s1, Seq(keyT, baseT), esNew, v1) => Q(s1, SetIn(keyT, MapDomain(baseT)), esNew.map(es => ast.MapContains(es(0), es(1))(e.pos, e.info, e.errT)), v1) }) @@ -1002,16 +1027,16 @@ object evaluator extends EvaluationRules { } - private def assertSeqIndexBounds(tIndex: Term, tSeq: Term, eIndex: ast.Exp, eSeq: ast.Exp, expNew: Option[Seq[ast.Exp]], pve: PartialVerificationError, s: State, v: Verifier): VerificationResult = { + private def assertSeqIndexBounds(tIndex: Term, tSeq: Term, eIndex: ast.Exp, eSeq: ast.Exp, expNew: Option[Seq[ast.Exp]], pve: PartialVerificationError, s: State, v: Verifier, analysisInfos: DependencyAnalysisInfos): VerificationResult = { def assertLtSeqLength(): VerificationResult = { val indexInBoundsTerm = Less(tIndex, SeqLength(tSeq)) val indexInBoundsExp = Option.when(withExp)(ast.LtCmp(eIndex, ast.SeqLength(eSeq)())(eIndex.pos, eIndex.info, eIndex.errT)) val indexInBoundsExpNew = Option.when(withExp)(ast.LtCmp(expNew.get(1), ast.SeqLength(expNew.get(0))())(eIndex.pos, eIndex.info, eIndex.errT)) - v.decider.assert(indexInBoundsTerm) { + v.decider.assert(indexInBoundsTerm, analysisInfos) { case true => Success() case false => - if (s.retryLevel == 0 && v.reportFurtherErrors()) v.decider.assume(indexInBoundsTerm, indexInBoundsExp, indexInBoundsExpNew) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(indexInBoundsTerm, indexInBoundsExp, indexInBoundsExpNew, analysisInfos, assumeFailedAssertion=v.reportFurtherErrors()) createFailure(pve dueTo SeqIndexExceedsLength(eSeq, eIndex), v, s, indexInBoundsTerm, indexInBoundsExpNew) } } @@ -1020,15 +1045,17 @@ object evaluator extends EvaluationRules { val indexGeZeroExp = Option.when(withExp)(ast.GeCmp(eIndex, ast.IntLit(0)())(eIndex.pos, eIndex.info, eIndex.errT)) val indexGeZeroExpNew = Option.when(withExp)(ast.GeCmp(expNew.get(1), ast.IntLit(0)())(eIndex.pos, eIndex.info, eIndex.errT)) val failureIdxNeg = createFailure(pve dueTo SeqIndexNegative(eSeq, eIndex), v, s, indexGeZeroTerm, indexGeZeroExpNew) - v.decider.assert(indexGeZeroTerm) { + v.decider.assert(indexGeZeroTerm, analysisInfos) { case true => assertLtSeqLength() - case false if s.retryLevel == 0 && v.reportFurtherErrors() => - v.decider.assume(indexGeZeroTerm, indexGeZeroExp, indexGeZeroExpNew) - assertLtSeqLength() match { - case Success() => failureIdxNeg - case failureIdxGeLen: VerificationResult => - failureIdxNeg combine failureIdxGeLen - } + case false if s.retryLevel == 0 => + v.decider.handleFailedAssertion(indexGeZeroTerm, indexGeZeroExp, indexGeZeroExpNew, analysisInfos, assumeFailedAssertion=v.reportFurtherErrors()) + if(v.reportFurtherErrors()) + assertLtSeqLength() match { + case Success() => failureIdxNeg + case failureIdxGeLen: VerificationResult => + failureIdxNeg combine failureIdxGeLen + } + else failureIdxNeg case false => failureIdxNeg } } @@ -1041,7 +1068,8 @@ object evaluator extends EvaluationRules { optTriggers: Option[Seq[ast.Trigger]], name: String, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Seq[Var], /* Variables from vars */ Option[Seq[ast.LocalVarDecl]], @@ -1069,18 +1097,18 @@ object evaluator extends EvaluationRules { type R = (State, Seq[Term], Option[Seq[ast.Exp]], Option[(Seq[Term], Option[Seq[ast.Exp]], Seq[Trigger], (Seq[Term], Seq[Quantification]), Option[(InsertionOrderedSet[DebugExp], InsertionOrderedSet[DebugExp])], Map[ast.Exp, Term])]) executionFlowController.locallyWithResult[R](s1, v)((s2, v1, QB) => { val preMark = v1.decider.setPathConditionMark() - evals(s2, es1, _ => pve, v1)((s3, ts1, es1New, v2) => { + evals(s2, es1, _ => pve, v1, analysisInfos)((s3, ts1, es1New, v2) => { val bc = And(ts1) // ME: If bc is unsatisfiable, we are assuming false here. In that case, evaluating es2 and the triggers // may not return any value (e.g. if es2 contains a field read for which we don't have permission, a smoke // check succeeds, then the continuation for evals(es2) is never invoked). This caused issue #842. // In this case, we return None. val expPair = (viper.silicon.utils.ast.BigAnd(es1), es1New.map(viper.silicon.utils.ast.BigAnd(_))) - v2.decider.setCurrentBranchCondition(bc, expPair) + v2.decider.setCurrentBranchCondition(bc, expPair, analysisInfos) var es2AndTriggerTerms: Option[(Seq[Term], Option[Seq[ast.Exp]], Seq[Trigger], (Seq[Term], Seq[Quantification]), Option[(InsertionOrderedSet[DebugExp], InsertionOrderedSet[DebugExp])], Map[ast.Exp, Term])] = None var finalState = s3 - val es2AndTriggerResult = evals(s3, es2, _ => pve, v2)((s4, ts2, es2New, v3) => { - evalTriggers(s4, optTriggers.getOrElse(Nil), pve, v3)((s5, tTriggers, _) => { // TODO: v4 isn't forward - problem? + val es2AndTriggerResult = evals(s3, es2, _ => pve, v2, analysisInfos)((s4, ts2, es2New, v3) => { + evalTriggers(s4, optTriggers.getOrElse(Nil), pve, v3, analysisInfos)((s5, tTriggers, _) => { // TODO: v4 isn't forward - problem? val (auxGlobals, auxNonGlobalQuants) = v3.decider.pcs.after(preMark).quantified(quant, tVars, tTriggers, s"$name-aux", isGlobal = false, bc) val auxExps = @@ -1093,7 +1121,7 @@ object evaluator extends EvaluationRules { })}) es2AndTriggerResult combine QB((finalState, ts1, es1New, es2AndTriggerTerms)) }) - }){ + }) { case (s2, ts1, es1New1, Some((ts2, es2New1, tTriggers, (tAuxGlobal, tAux), eAuxExps, additionalPossibleTriggers))) => val s3 = s.preserveAfterLocalEvaluation(s2).copy(possibleTriggers = s.possibleTriggers ++ additionalPossibleTriggers) Q(s3, tVars, Option.when(withExp)(varPairs map (e => ast.LocalVarDecl(e._2.get.name, e._2.get.typ)(e._2.get.pos, e._2.get.info, e._2.get.errT))), ts1, es1New1, Some((ts2, es2New1, tTriggers, (tAuxGlobal, tAux), Option.when(withExp)((eAuxExps.get._1, eAuxExps.get._2)))), v) @@ -1109,13 +1137,14 @@ object evaluator extends EvaluationRules { eRhs: ast.Exp, fromShortCircuitingAnd: Boolean, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { - joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s, v)((s1, v1, QB) => - brancher.branch(s1.copy(parallelizeBranches = false), tLhs, eLhs, v1, fromShortCircuitingAnd = fromShortCircuitingAnd)( - (s2, v2) => eval(s2.copy(parallelizeBranches = s1.parallelizeBranches), eRhs, pve, v2)((s2, tRhs, eRhsNew, v2) => QB(s2, (tRhs, eRhsNew), v2)), + joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s, v, analysisInfos)((s1, v1, QB) => + brancher.branch(s1.copy(parallelizeBranches = false), tLhs, eLhs, v1, analysisInfos, fromShortCircuitingAnd = fromShortCircuitingAnd)( + (s2, v2) => eval(s2.copy(parallelizeBranches = s1.parallelizeBranches), eRhs, pve, v2, analysisInfos)((s2, tRhs, eRhsNew, v2) => QB(s2, (tRhs, eRhsNew), v2)), (s2, v2) => QB(s2.copy(parallelizeBranches = s1.parallelizeBranches), (True, Option.when(withExp)(ast.TrueLit()())), v2)) )(entries => { assert(entries.length <= 2) @@ -1133,7 +1162,8 @@ object evaluator extends EvaluationRules { label: String, e: ast.Exp, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { @@ -1141,7 +1171,7 @@ object evaluator extends EvaluationRules { val s1 = s.copy(h = h, partiallyConsumedHeap = None, isEvalInOld = true, possibleTriggers = Map.empty) val s2 = v.stateConsolidator(s1).consolidateOptionally(s1, v) - eval(s2, e, pve, v)((s3, t, eNew, v1) => { + eval(s2, e, pve, v, analysisInfos)((s3, t, eNew, v1) => { val newPossibleTriggers = if (s.recordPossibleTriggers) { // For all new possible trigger expressions e and translated term t, // make sure we remember t as the term for old[label](e) instead. @@ -1170,10 +1200,10 @@ object evaluator extends EvaluationRules { Q(s4, t, eNew, v1)}) } - def evalResourceAccess(s: State, resacc: ast.ResourceAccess, pve: PartialVerificationError, v: Verifier) + def evalResourceAccess(s: State, resacc: ast.ResourceAccess, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, ChunkIdentifer, Seq[Term], Option[Seq[ast.Exp]], Verifier) => VerificationResult) : VerificationResult = { - evals(s, resacc.args(s.program), _ => pve, v)((s1, tArgs, eArgsNew, v1) => + evals(s, resacc.args(s.program), _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => Q(s1, ChunkIdentifier(resacc.res(s1.program), s1.program), tArgs, eArgsNew, v1)) } @@ -1182,11 +1212,12 @@ object evaluator extends EvaluationRules { e1: ast.Exp, termOp: ((Term, Term)) => T, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, T, Option[ast.Exp], Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { - evalBinOp(s, e0, e1, (t0, t1) => termOp((t0, t1)), pve, v)(Q) + evalBinOp(s, e0, e1, (t0, t1) => termOp((t0, t1)), pve, v, analysisInfos)(Q) } private def evalBinOp[T <: Term] @@ -1195,12 +1226,13 @@ object evaluator extends EvaluationRules { e1: ast.Exp, termOp: (Term, Term) => T, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, T, Option[ast.Exp], Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - eval(s1, e1, pve, v1)((s2, t1, e1New, v2) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + eval(s1, e1, pve, v1, analysisInfos)((s2, t1, e1New, v2) => Q(s2, termOp(t0, t1), e0New, e1New, v2))) } @@ -1211,32 +1243,33 @@ object evaluator extends EvaluationRules { tDivisor: Term, tZero: Term, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult) : VerificationResult = { - v.decider.assert(tDivisor !== tZero){ + v.decider.assert(tDivisor !== tZero, analysisInfos) { case true => Q(s, t, v) case false => - val (notZeroExp, notZeroExpNew) = if (withExp) { + val (notZeroExp, notZeroExpNew) = if (withExp) (Some(ast.NeCmp(eDivisor, ast.IntLit(0)())(eDivisor.pos, eDivisor.info, eDivisor.errT)), Some(ast.NeCmp(eDivisorNew.get, ast.IntLit(0)())(eDivisor.pos, eDivisor.info, eDivisor.errT))) - } else { (None, None) } + else + (None, None) val failure = createFailure(pve dueTo DivisionByZero(eDivisor), v, s, tDivisor !== tZero, notZeroExpNew) - if (s.retryLevel == 0 && v.reportFurtherErrors()) { - v.decider.assume(tDivisor !== tZero, notZeroExp, notZeroExpNew) - failure combine Q(s, t, v) - } else failure + if (s.retryLevel == 0) v.decider.handleFailedAssertion(tDivisor !== tZero, notZeroExp, notZeroExpNew, analysisInfos, assumeFailedAssertion=v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, t, v) else failure } } def evalTriggers(s: State, silverTriggers: Seq[ast.Trigger], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Seq[Trigger], Verifier) => VerificationResult) : VerificationResult = { - evalTriggers(s, silverTriggers map (_.exps), Nil, pve, v)((s1, tTriggersSets, v1) => { + evalTriggers(s, silverTriggers map (_.exps), Nil, pve, v, analysisInfos)((s1, tTriggersSets, v1) => { /* [2015-12-15 Malte] * Evaluating triggers that did not occur in the body (and whose corresponding term has * therefore not already been recorded in the context) might introduce new path conditions, @@ -1264,7 +1297,8 @@ object evaluator extends EvaluationRules { eTriggerSets: TriggerSets[ast.Exp], tTriggersSets: TriggerSets[Term], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, TriggerSets[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -1272,15 +1306,15 @@ object evaluator extends EvaluationRules { Q(s, tTriggersSets, v) else { if (eTriggerSets.head.collect{case fa: ast.FieldAccess => fa; case pa: ast.PredicateAccess => pa; case wand: ast.MagicWand => wand }.nonEmpty ) { - evalHeapTrigger(s, eTriggerSets.head, pve, v)((s1, ts, v1) => - evalTriggers(s1, eTriggerSets.tail, tTriggersSets :+ ts, pve, v1)(Q)) + evalHeapTrigger(s, eTriggerSets.head, pve, v, analysisInfos)((s1, ts, v1) => + evalTriggers(s1, eTriggerSets.tail, tTriggersSets :+ ts, pve, v1, analysisInfos)(Q)) } else { - evalTrigger(s, eTriggerSets.head, pve, v)((s1, ts, v1) => - evalTriggers(s1, eTriggerSets.tail, tTriggersSets :+ ts, pve, v1)(Q)) + evalTrigger(s, eTriggerSets.head, pve, v, analysisInfos)((s1, ts, v1) => + evalTriggers(s1, eTriggerSets.tail, tTriggersSets :+ ts, pve, v1, analysisInfos)(Q)) }} } - private def evalTrigger(s: State, exps: Seq[ast.Exp], pve: PartialVerificationError, v: Verifier) + private def evalTrigger(s: State, exps: Seq[ast.Exp], pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Seq[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -1353,7 +1387,7 @@ object evaluator extends EvaluationRules { */ val r = - evals(s.copy(triggerExp = true), remainingTriggerExpressions, _ => pve, v)((s1, remainingTriggerTerms, _, v1) => { + evals(s.copy(triggerExp = true), remainingTriggerExpressions, _ => pve, v, analysisInfos)((s1, remainingTriggerTerms, _, v1) => { optRemainingTriggerTerms = Some(remainingTriggerTerms) pcDelta = v1.decider.pcs.after(preMark).assumptions //decider.π -- πPre pcDeltaExp = v1.decider.pcs.after(preMark).assumptionExps @@ -1369,7 +1403,8 @@ object evaluator extends EvaluationRules { (r, optRemainingTriggerTerms) match { case (Success(), Some(remainingTriggerTerms)) => - v.decider.assume(pcDelta, Option.when(withExp)(DebugExp.createInstance("pcDeltaExp", children = pcDeltaExp)), enforceAssumption = false) + // TODO ake: wrap pcDelta with labels? + v.decider.assume(pcDelta, Option.when(withExp)(DebugExp.createInstance("pcDeltaExp", children = pcDeltaExp)), enforceAssumption = false, analysisInfos.withDependencyType(AssumptionType.Internal)) Q(s.copy(functionRecorder = functionRecorder), cachedTriggerTerms ++ remainingTriggerTerms, v) case _ => for (e <- remainingTriggerExpressions) @@ -1383,14 +1418,23 @@ object evaluator extends EvaluationRules { joinFunctionName: String, joinFunctionArgs: Seq[Term], joinedExp: Option[ast.Exp], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (entries: Seq[JoinDataEntry[(Term, Option[ast.Exp])]]) : (State, (Term, Option[ast.Exp])) = { val joinSort = v.symbolConverter.toSort(joinType) assert(entries.nonEmpty, "Expected at least one join data entry") - entries match { + // join entries coming from feasible paths only! + val feasibleEntries = entries filter (!Verifier.config.analyzeInfeasiblePaths || _.pathConditions.infeasibilityNodeId.isEmpty) + + feasibleEntries match { + case Seq() => + /* feasibility checks are disabled and all entries are infeasible, we can return any state and data */ + v.decider.pcs.markPathInfeasible(true) + v.decider.pcs.setCurrentInfeasibilityNode(entries.head.pathConditions.infeasibilityNodeId) + (entries.head.s, entries.head.data) case Seq(entry) => /* If there is only one entry, i.e. one branch to join, it is assumed that the other * branch was infeasible, and the branch conditions are therefore ignored. @@ -1410,13 +1454,13 @@ object evaluator extends EvaluationRules { var sJoined = entries.tail.foldLeft(entries.head.s)((sAcc, entry) => sAcc.merge(entry.s)) sJoined = sJoined.copy(functionRecorder = sJoined.functionRecorder.recordPathSymbol(joinSymbol)) - joinDefEqs foreach { case (t, exp, expNew) => v.decider.assume(t, exp, expNew)} + joinDefEqs foreach { case (t, exp, expNew) => v.decider.assume(t, exp, expNew, analysisInfos)} (sJoined, (joinTerm, joinedExp)) } } - def evalHeapTrigger(s: State, exps: Seq[ast.Exp], pve: PartialVerificationError, v: Verifier) + def evalHeapTrigger(s: State, exps: Seq[ast.Exp], pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Seq[Term], Verifier) => VerificationResult) : VerificationResult = { var triggers: Seq[Term] = Seq() var triggerAxioms: Seq[Term] = Seq() @@ -1424,18 +1468,18 @@ object evaluator extends EvaluationRules { exps foreach { case ra: ast.ResourceAccess if s.isUsedAsTrigger(ra.res(s.program)) => - val (axioms, trigs, _, smDef) = generateResourceTrigger(ra, s, pve, v) + val (axioms, trigs, _, smDef) = generateResourceTrigger(ra, s, pve, v, analysisInfos) triggers = triggers ++ trigs triggerAxioms = triggerAxioms ++ axioms smDefs = smDefs ++ smDef - case e => evalTrigger(s.copy(triggerExp = true), Seq(e), pve, v)((_, t, _) => { + case e => evalTrigger(s.copy(triggerExp = true), Seq(e), pve, v, analysisInfos)((_, t, _) => { triggers = triggers ++ t Success() }) } val triggerString = exps.mkString(", ") - v.decider.assume(triggerAxioms, Option.when(withExp)(DebugExp.createInstance(s"Heap Triggers ($triggerString)")), enforceAssumption = false) + v.decider.assume(triggerAxioms, Option.when(withExp)(DebugExp.createInstance(s"Heap Triggers ($triggerString)")), enforceAssumption = false, analysisInfos.withDependencyType(AssumptionType.Trigger)) var fr = s.functionRecorder for (smDef <- smDefs){ fr = fr.recordFvfAndDomain(smDef) @@ -1446,7 +1490,8 @@ object evaluator extends EvaluationRules { private def generateResourceTrigger(ra: ast.ResourceAccess, s: State, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : (Seq[Term], Seq[Term], Term, Seq[SnapshotMapDefinition]) = { var axioms = Seq.empty[Term] var triggers = Seq.empty[Term] @@ -1467,7 +1512,7 @@ object evaluator extends EvaluationRules { s, resource, codomainQVars, relevantChunks, v, optSmDomainDefinitionCondition) val s1 = s.copy(smCache = smCache1) - evals(s1.copy(triggerExp = true), eArgs, _ => pve, v)((_, tArgs, _, _) => { + evals(s1.copy(triggerExp = true), eArgs, _ => pve, v, analysisInfos)((_, tArgs, _, _) => { axioms = axioms ++ smDef1.valueDefinitions mostRecentTrig = ResourceTriggerFunction(resource, smDef1.sm, tArgs, s.program) triggers = triggers :+ mostRecentTrig @@ -1485,7 +1530,8 @@ object evaluator extends EvaluationRules { s: State, exps: Seq[ast.Exp], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Option[ast.Exp], Verifier) => VerificationResult) : VerificationResult = { assert( @@ -1496,16 +1542,16 @@ object evaluator extends EvaluationRules { val stop = if (constructor == Or) True else False - eval(s, exps.head, pve, v)((s1, t0, e0New, v1) => { + eval(s, exps.head, pve, v, analysisInfos)((s1, t0, e0New, v1) => { t0 match { case _ if exps.tail.isEmpty => Q(s1, t0, e0New, v1) // Done, if no expressions left (necessary) case `stop` => Q(s1, t0, e0New, v1) // Done, if last expression was true/false for or/and (optimisation) case _ => val expPair = if (constructor == Or) (exps.head, e0New) else (ast.Not(exps.head)(), e0New.map(ast.Not(_)())) - joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s1, v1)((s2, v2, QB) => - brancher.branch(s2.copy(parallelizeBranches = false), if (constructor == Or) t0 else Not(t0), expPair, v2, fromShortCircuitingAnd = true)( + joiner.join[(Term, Option[ast.Exp]), (Term, Option[ast.Exp])](s1, v1, analysisInfos)((s2, v2, QB) => + brancher.branch(s2.copy(parallelizeBranches = false), if (constructor == Or) t0 else Not(t0), expPair, v2, analysisInfos.withDependencyType(AssumptionType.Internal), fromShortCircuitingAnd = true)( (s3, v3) => QB(s3.copy(parallelizeBranches = s2.parallelizeBranches), (t0, e0New), v3), - (s3, v3) => evalSeqShortCircuit(constructor, s3.copy(parallelizeBranches = s2.parallelizeBranches), exps.tail, pve, v3)((s2, t2, e2, v2) => QB(s2, (t2, e2), v2))) + (s3, v3) => evalSeqShortCircuit(constructor, s3.copy(parallelizeBranches = s2.parallelizeBranches), exps.tail, pve, v3, analysisInfos)((s2, t2, e2, v2) => QB(s2, (t2, e2), v2))) ){case Seq(ent) => (ent.s, ent.data) case Seq(ent1, ent2) => diff --git a/src/main/scala/rules/ExecutionFlowController.scala b/src/main/scala/rules/ExecutionFlowController.scala index 96d4a4de7..d15a57106 100644 --- a/src/main/scala/rules/ExecutionFlowController.scala +++ b/src/main/scala/rules/ExecutionFlowController.scala @@ -6,7 +6,6 @@ package viper.silicon.rules -import viper.silver.ast import viper.silicon.Config.ExhaleMode import viper.silicon.interfaces._ import viper.silicon.logger.records.data.CommentRecord diff --git a/src/main/scala/rules/Executor.scala b/src/main/scala/rules/Executor.scala index 763ac1afa..60c42033f 100644 --- a/src/main/scala/rules/Executor.scala +++ b/src/main/scala/rules/Executor.scala @@ -6,26 +6,30 @@ package viper.silicon.rules -import viper.silicon.debugger.DebugExp -import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.Config.JoinMode - -import scala.annotation.unused -import viper.silver.cfg.silver.SilverCfg -import viper.silver.cfg.silver.SilverCfg.{SilverBlock, SilverEdge} -import viper.silver.verifier.{CounterexampleTransformer, NullPartialVerificationError, PartialVerificationError} -import viper.silver.verifier.errors._ -import viper.silver.verifier.reasons._ -import viper.silver.{ast, cfg} +import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp import viper.silicon.decider.RecordedPathConditions +import viper.silicon.dependencyAnalysis.{DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces._ import viper.silicon.logger.records.data.{CommentRecord, ConditionalEdgeRecord, ExecuteRecord, MethodCallRecord} import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.utils.ast.{BigAnd, extractPTypeFromExp, simplifyVariableName} import viper.silicon.utils.freshSnap import viper.silicon.verifier.Verifier +import viper.silver.ast.{FalseLit, NoPosition} +import viper.silver.cfg.silver.SilverCfg +import viper.silver.cfg.silver.SilverCfg.{SilverBlock, SilverEdge} import viper.silver.cfg.{ConditionalEdge, StatementBlock} +import viper.silver.dependencyAnalysis._ +import viper.silver.verifier.errors._ +import viper.silver.verifier.reasons._ +import viper.silver.verifier.{CounterexampleTransformer, NullPartialVerificationError, PartialVerificationError} +import viper.silver.{ast, cfg} + +import scala.annotation.unused trait ExecutionRules extends SymbolicExecutionRules { def exec(s: State, @@ -64,11 +68,12 @@ object executor extends ExecutionRules { val condEdgeRecord = new ConditionalEdgeRecord(ce.condition, s, v.decider.pcs) val sepIdentifier = v.symbExLog.openScope(condEdgeRecord) val s1 = handleOutEdge(s, edge, v) - eval(s1, ce.condition, IfFailed(ce.condition), v)((s2, tCond, condNew, v1) => + val analysisInfos = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, v.decider.defaultAnalysisInfos, ce.condition.info, ce.condition) + eval(s1, ce.condition, IfFailed(ce.condition), v, analysisInfos)((s2, tCond, condNew, v1) => /* Using branch(...) here ensures that the edge condition is recorded * as a branch condition on the pathcondition stack. */ - brancher.branch(s2.copy(parallelizeBranches = false), tCond, (ce.condition, condNew), v1)( + brancher.branch(s2.copy(parallelizeBranches = false), tCond, (ce.condition, condNew), v1, analysisInfos)( (s3, v3) => exec(s3.copy(parallelizeBranches = s2.parallelizeBranches), ce.target, ce.kind, v3, joinPoint)((s4, v4) => { v4.symbExLog.closeScope(sepIdentifier) @@ -88,8 +93,9 @@ object executor extends ExecutionRules { def handleOutEdge(s: State, edge: SilverEdge, v: Verifier): State = { edge.kind match { - case cfg.Kind.Out => - val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, s.h, s.invariantContexts.head, v) + case cfg.Kind.Out if !v.decider.isPathMarkedInfeasible => + val analysisInfos = v.decider.defaultAnalysisInfos + val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, s.h, s.invariantContexts.head, v, analysisInfos) val s1 = s.copy(functionRecorder = fr1, h = h1, invariantContexts = s.invariantContexts.tail) s1 @@ -118,7 +124,7 @@ object executor extends ExecutionRules { case (Seq(), _) => Q(s, v) case (Seq(edge), _) => follow(s, edge, v, joinPoint)(Q) case (Seq(edge1, edge2), Some(newJoinPoint)) if - s.moreJoins.id >= JoinMode.All.id && + (s.moreJoins.id >= JoinMode.All.id && // Can't directly match type because of type erasure ... edge1.isInstanceOf[ConditionalEdge[ast.Stmt, ast.Exp]] && edge2.isInstanceOf[ConditionalEdge[ast.Stmt, ast.Exp]] && @@ -126,8 +132,10 @@ object executor extends ExecutionRules { // this is the case if the source is a statement block, // as opposed to a loop head block. edge1.source.isInstanceOf[StatementBlock[ast.Stmt, ast.Exp]] && - edge2.source.isInstanceOf[StatementBlock[ast.Stmt, ast.Exp]] => + edge2.source.isInstanceOf[StatementBlock[ast.Stmt, ast.Exp]]) || + v.decider.isPathMarkedInfeasible => + val isPathMarkedInfeasibleBefore = v.decider.isPathMarkedInfeasible assert(edge1.source == edge2.source) val cedge1 = edge1.asInstanceOf[ConditionalEdge[ast.Stmt, ast.Exp]] @@ -140,10 +148,12 @@ object executor extends ExecutionRules { case _ => false }) - eval(s, cedge1.condition, pvef(cedge1.condition), v)((s1, t0, condNew, v1) => + val analysisInfos = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, v.decider.defaultAnalysisInfos, cedge1.condition.info, cedge1.condition) + + eval(s, cedge1.condition, pvef(cedge1.condition), v, analysisInfos)((s1, t0, condNew, v1) => // The type arguments here are Null because there is no need to pass any join data. - joiner.join[scala.Null, scala.Null](s1, v1, resetState = false)((s2, v2, QB) => { - brancher.branch(s2, t0, (cedge1.condition, condNew), v2)( + joiner.join[scala.Null, scala.Null](s1, v1, analysisInfos, resetState = false)((s2, v2, QB) => { + brancher.branch(s2, t0, (cedge1.condition, condNew), v2, analysisInfos)( // Follow only until join point. (s3, v3) => follow(s3, edge1, v3, Some(newJoinPoint))((s, v) => QB(s, null, v)), (s3, v3) => follow(s3, edge2, v3, Some(newJoinPoint))((s, v) => QB(s, null, v)) @@ -152,6 +162,8 @@ object executor extends ExecutionRules { val s2 = entries match { case Seq(entry) => // One branch is dead entry.s + case Seq(entry1, _) if isPathMarkedInfeasibleBefore => // no need to merge since path is dead anyway + entry1.s case Seq(entry1, entry2) => // Both branches are alive entry1.pathConditionAwareMerge(entry2, v1) case _ => @@ -172,8 +184,9 @@ object executor extends ExecutionRules { if Verifier.config.parallelizeBranches() && cond2 == ast.Not(cond1)() => val condEdgeRecord = new ConditionalEdgeRecord(thenEdge.condition, s, v.decider.pcs) val sepIdentifier = v.symbExLog.openScope(condEdgeRecord) - val res = eval(s, thenEdge.condition, IfFailed(thenEdge.condition), v)((s2, tCond, eCondNew, v1) => - brancher.branch(s2, tCond, (thenEdge.condition, eCondNew), v1)( + val analysisInfos = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, v.decider.defaultAnalysisInfos, thenEdge.condition.info, thenEdge.condition) + val res = eval(s, thenEdge.condition, IfFailed(thenEdge.condition), v, analysisInfos)((s2, tCond, eCondNew, v1) => + brancher.branch(s2, tCond, (thenEdge.condition, eCondNew), v1, analysisInfos)( (s3, v3) => { follow(s3, thenEdge, v3, joinPoint)(Q) }, @@ -246,6 +259,9 @@ object executor extends ExecutionRules { map.updated(x, xNew)})) val sBody = s.copy(g = gBody, h = v.heapSupporter.getEmptyHeap(s.program)) + val analysisInfosInv = v.decider.defaultAnalysisInfos + val analysisInfosLoopInternal = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo(s"Loop ${block.id}\"", NoPosition), AssumptionType.Internal) + val edges = s.methodCfg.outEdges(block) val (outEdges, otherEdges) = edges partition(_.kind == cfg.Kind.Out) val sortedEdges = otherEdges ++ outEdges @@ -258,7 +274,7 @@ object executor extends ExecutionRules { (executionFlowController.locally(sBody, v)((s0, v0) => { v0.decider.prover.comment("Loop head block: Check well-definedness of invariant") val mark = v0.decider.setPathConditionMark() - produces(s0, freshSnap, invs, ContractNotWellformed, v0)((s1, v1) => { + produces(s0, freshSnap, invs, ContractNotWellformed, v0, analysisInfosInv)((s1, v1) => { phase1data = phase1data :+ (s1, v1.decider.pcs.after(mark), v1.decider.freshFunctions /* [BRANCH-PARALLELISATION] */, @@ -267,7 +283,7 @@ object executor extends ExecutionRules { })}) combine executionFlowController.locally(s, v)((s0, v0) => { v0.decider.prover.comment("Loop head block: Establish invariant") - consumes(s0, invs, false, LoopInvariantNotEstablished, v0)((sLeftover, _, v1) => { + consumes(s0, invs, false, LoopInvariantNotEstablished, v0, analysisInfosInv)((sLeftover, _, v1) => { v1.decider.prover.comment("Loop head block: Execute statements of loop head block (in invariant state)") phase1data.foldLeft(Success(): VerificationResult) { case (result, _) if !result.continueVerification => result @@ -276,9 +292,11 @@ object executor extends ExecutionRules { intermediateResult combine executionFlowController.locally(s2, v1)((s3, v2) => { v2.decider.declareAndRecordAsFreshFunctions(ff1 -- v2.decider.freshFunctions) /* [BRANCH-PARALLELISATION] */ v2.decider.declareAndRecordAsFreshMacros(fm1.filter(!v2.decider.freshMacros.contains(_))) /* [BRANCH-PARALLELISATION] */ - v2.decider.assume(pcs.assumptions, Option.when(withExp)(DebugExp.createInstance("Loop invariant", pcs.assumptionExps)), false) + v2.decider.pcs.markPathInfeasible(v2.decider.pcs.isPathMarkedInfeasible || pcs.isPathMarkedInfeasible) + if (v2.decider.pcs.getCurrentInfeasibilityNode.isEmpty) v2.decider.pcs.setCurrentInfeasibilityNode(pcs.infeasibilityNodeId) + v2.decider.assume(pcs.assumptions map (t => DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v.decider, t, Set.empty, Set(t))), Some(pcs.assumptionExps), "Loop invariant", enforceAssumption=false, analysisInfosLoopInternal) v2.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterContract) - if (v2.decider.checkSmoke()) + if (!Verifier.config.analyzeInfeasiblePaths && v2.decider.checkSmoke(analysisInfosLoopInternal)) Success() else { execs(s3, stmts, v2)((s4, v3) => { @@ -288,7 +306,7 @@ object executor extends ExecutionRules { case (result, _) if !result.continueVerification => result case (intermediateResult, eCond) => intermediateResult combine executionFlowController.locally(s4, v3)((s5, v4) => { - eval(s5, eCond, WhileFailed(eCond), v4)((_, _, _, _) => + eval(s5, eCond, WhileFailed(eCond), v4, v4.decider.defaultAnalysisInfos)((_, _, _, _) => Success()) }) } @@ -302,7 +320,8 @@ object executor extends ExecutionRules { * attempting to re-establish the invariant. */ v.decider.prover.comment("Loop head block: Re-establish invariant") - consumes(s, invs, false, e => LoopInvariantNotPreserved(e), v)((_, _, _) => + val analysisInfosInv = v.decider.defaultAnalysisInfos + consumes(s, invs, false, e => LoopInvariantNotPreserved(e), v, analysisInfosInv)((_, _, _) => Success()) } } @@ -322,12 +341,14 @@ object executor extends ExecutionRules { (Q: (State, Verifier) => VerificationResult) : VerificationResult = { val sepIdentifier = v.symbExLog.openScope(new ExecuteRecord(stmt, s, v.decider.pcs)) - exec2(s, stmt, v)((s1, v1) => { + val analysisInfos = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, v.decider.defaultAnalysisInfos, stmt.info, stmt) + exec2(s, stmt, v, analysisInfos)((s1, v1) => { v1.symbExLog.closeScope(sepIdentifier) - Q(s1, v1)}) + Q(s1, v1) + }) } - def exec2(state: State, stmt: ast.Stmt, v: Verifier) + def exec2(state: State, stmt: ast.Stmt, v: Verifier, analysisInfos: DependencyAnalysisInfos) (continuation: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -361,8 +382,8 @@ object executor extends ExecutionRules { Q(s.copy(g = s.g + (x -> (t, newExp))), v) case ass @ ast.LocalVarAssign(x, rhs) => - eval(s, rhs, AssignmentFailed(ass), v)((s1, tRhs, rhsNew, v1) => { - val (t, e) = ssaifyRhs(tRhs, rhs, rhsNew, x.name, x.typ, v, s1) + eval(s, rhs, AssignmentFailed(ass), v, analysisInfos)((s1, tRhs, rhsNew, v1) => { + val (t, e) = ssaifyRhs(tRhs, rhs, rhsNew, x.name, x.typ, v, s1, analysisInfos) Q(s1.copy(g = s1.g + (x, (t, e))), v1)}) /* TODO: Encode assignments e1.f := e2 as @@ -374,10 +395,10 @@ object executor extends ExecutionRules { case ass @ ast.FieldAssign(ast.FieldAccess(eRcvr, field), rhs) => assert(!s.exhaleExt) val pve = AssignmentFailed(ass) - eval(s, eRcvr, pve, v)((s1, tRcvr, eRcvrNew, v1) => { - eval(s1, rhs, pve, v1)((s2, tRhs, eRhsNew, v2) => { - val (tSnap, _) = ssaifyRhs(tRhs, rhs, eRhsNew, field.name, field.typ, v2, s2) - v2.heapSupporter.execFieldAssign(s2, ass, tRcvr, eRcvrNew, tSnap, eRhsNew, pve, v2)(Q) + eval(s, eRcvr, pve, v, analysisInfos)((s1, tRcvr, eRcvrNew, v1) => { + eval(s1, rhs, pve, v1, analysisInfos)((s2, tRhs, eRhsNew, v2) => { + val (tSnap, _) = ssaifyRhs(tRhs, rhs, eRhsNew, field.name, field.typ, v2, s2, analysisInfos) + v2.heapSupporter.execFieldAssign(s2, ass, tRcvr, eRcvrNew, tSnap, eRhsNew, pve, v2, analysisInfos)(Q) }) }) @@ -386,10 +407,10 @@ object executor extends ExecutionRules { val debugExp = Option.when(withExp)(ast.NeCmp(x, ast.NullLit()())()) val debugExpSubst = Option.when(withExp)(ast.NeCmp(eRcvrNew.get, ast.NullLit()())()) - v.decider.assume(tRcvr !== Null, debugExp, debugExpSubst) + v.decider.assume(tRcvr !== Null, debugExp, debugExpSubst, analysisInfos) val eRcvr = Option.when(withExp)(Seq(x)) - val p = FullPerm + val p = terms.FullPerm val pExp = Option.when(withExp)(ast.FullPerm()(stmt.pos, stmt.info, stmt.errT)) def addFieldPerms(s: State, flds: Seq[ast.Field], v: Verifier) @@ -399,7 +420,7 @@ object executor extends ExecutionRules { } else { val fld = flds.head val snap = v.decider.fresh(fld.name, v.symbolConverter.toSort(fld.typ), Option.when(withExp)(extractPTypeFromExp(x))) - v.heapSupporter.produceSingle(s, fld, Seq(tRcvr), eRcvr, snap, None, p, pExp, NullPartialVerificationError, false, v)((s1, v1) => { + v.heapSupporter.produceSingle(s, fld, Seq(tRcvr), eRcvr, snap, None, p, pExp, NullPartialVerificationError, false, v, analysisInfos)((s1, v1) => { addFieldPerms(s1, flds.tail, v1)(QB) }) } @@ -410,37 +431,50 @@ object executor extends ExecutionRules { val s1 = s0.copy(g = s0.g + (x, (tRcvr, eRcvrNew))) val (debugHeapName, _) = v.getDebugOldLabel(s1, stmt.pos, Some(magicWandSupporter.getEvalHeap(s1))) val s2 = if (withExp) s1.copy(oldHeaps = s1.oldHeaps + (debugHeapName -> magicWandSupporter.getEvalHeap(s1))) else s1 - v0.decider.assume(ts, Option.when(withExp)(DebugExp.createInstance(Some("Reference Disjointness"), esNew, esNew, InsertionOrderedSet.empty)), enforceAssumption = false) + v0.decider.assume(ts, Option.when(withExp)(DebugExp.createInstance(Some("Reference Disjointness"), esNew, esNew, InsertionOrderedSet.empty)), enforceAssumption = false, analysisInfos) Q(s2, v0) }) case inhale @ ast.Inhale(a) => a match { - case _: ast.FalseLit => + case _: ast.FalseLit if !Verifier.config.analyzeInfeasiblePaths => /* We're done */ Success() + case _: ast.TrueLit => + Q(s, v) case _ => - produce(s, freshSnap, a, InhaleFailed(inhale), v)((s1, v1) => { + produce(s, freshSnap, a, InhaleFailed(inhale), v, analysisInfos)((s1, v1) => { v1.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterInhale) + if (Verifier.config.analyzeInfeasiblePaths && a.isInstanceOf[ast.FalseLit]) v1.decider.checkSmoke(analysisInfos) Q(s1, v1)}) } case exhale @ ast.Exhale(a) => val pve = ExhaleFailed(exhale) - consume(s, a, false, pve, v)((s1, _, v1) => + val analysisInfos1 = if (a.topLevelConjuncts.size > 1) analysisInfos.copy(sourceInfos = List.empty) else analysisInfos // needed to ensure that each top-level conjunct gets a dedicated assertion node + consume(s, a, false, pve, v, analysisInfos1)((s1, _, v1) => Q(s1, v1)) case assert @ ast.Assert(a: ast.FalseLit) if !s.isInPackage => /* "assert false" triggers a smoke check. If successful, we backtrack. */ executionFlowController.tryOrFail0(s.copy(h = magicWandSupporter.getEvalHeap(s)), v)((s1, v1, QS) => { - if (v1.decider.checkSmoke(true)) + if (v1.decider.checkSmoke(analysisInfos, isAssert = true)) QS(s1.copy(h = s.h), v1) + else { + val failure = createFailure(AssertFailed(assert) dueTo AssertionFalse(a), v1, s1, False, true, Option.when(withExp)(a)) + if (s1.retryLevel == 0) v1.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v1.reportFurtherErrors()) + if (s1.retryLevel == 0 && v1.reportFurtherErrors()) failure combine QS(s1, v1) else failure + } + })((s2, v2) => + if (Verifier.config.analyzeInfeasiblePaths) + Q(s2, v2) else - createFailure(AssertFailed(assert) dueTo AssertionFalse(a), v1, s1, False, true, Option.when(withExp)(a)) - })((_, _) => Success()) + Success() + ) case assert @ ast.Assert(a) if Verifier.config.disableSubsumption() => + val analysisInfos1 = if (a.topLevelConjuncts.size > 1) analysisInfos.copy(sourceInfos = List.empty) else analysisInfos // needed to ensure that each top-level conjunct gets a dedicated assertion node val r = - consume(s, a, false, AssertFailed(assert), v)((_, _, _) => + consume(s, a, false, AssertFailed(assert), v, analysisInfos1)((_, _, _) => Success()) r combine Q(s, v) @@ -448,6 +482,8 @@ object executor extends ExecutionRules { case assert @ ast.Assert(a) => val pve = AssertFailed(assert) + val analysisInfos1 = if (a.topLevelConjuncts.size > 1) analysisInfos.copy(sourceInfos = List.empty) else analysisInfos // needed to ensure that each top-level conjunct gets a dedicated assertion node + if (s.exhaleExt) { Predef.assert(s.h.values.isEmpty) Predef.assert(s.reserveHeaps.head.values.isEmpty) @@ -456,11 +492,11 @@ object executor extends ExecutionRules { * hUsed (reserveHeaps.head) instead of consuming them. hUsed is later discarded and replaced * by s.h. By copying hUsed to s.h the contained permissions remain available inside the wand. */ - consume(s, a, false, pve, v)((s2, _, v1) => { + consume(s, a, false, pve, v, analysisInfos1)((s2, _, v1) => { Q(s2.copy(h = s2.reserveHeaps.head), v1) }) } else - consume(s, a, false, pve, v)((s1, _, v1) => { + consume(s, a, false, pve, v, analysisInfos1)((s1, _, v1) => { val s2 = s1.copy(h = s.h, reserveHeaps = s.reserveHeaps) Q(s2, v1)}) @@ -476,11 +512,11 @@ object executor extends ExecutionRules { }.getOrElse(sys.error(s"Found $methodName, but no matching field or predicate $resourceName")) val h1 = Heap(s.h.values.map { case bc: BasicChunk if bc.id.name == member.name => - bc.withSnap(freshSnap(bc.snap.sort, v), None) + v.chunkFactory.withSnap(bc, freshSnap(bc.snap.sort, v), None, analysisInfos) case qfc: QuantifiedFieldChunk if qfc.id.name == member.name => - qfc.withSnapshotMap(freshSnap(qfc.fvf.sort, v)) + v.chunkFactory.withSnapshotMap(qfc,freshSnap(qfc.fvf.sort, v), analysisInfos) case qpc: QuantifiedPredicateChunk if qpc.id.name == member.name => - qpc.withSnapshotMap(freshSnap(qpc.psf.sort, v)) + v.chunkFactory.withSnapshotMap(qpc, freshSnap(qpc.psf.sort, v), analysisInfos) case other => other}) Q(s.copy(h = h1), v) @@ -499,11 +535,14 @@ object executor extends ExecutionRules { val pveCall = CallFailed(call) val pveCallTransformed = pveCall.withReasonNodeTransformed(reasonTransformer) + DependencyAnalyzer.addAssumption(v.decider, True, analysisInfos, None) // make sure method calls are represented as a node, even if there are no postconditions + val mcLog = new MethodCallRecord(call, s, v.decider.pcs) val sepIdentifier = v.symbExLog.openScope(mcLog) val paramLog = new CommentRecord("Parameters", s, v.decider.pcs) val paramId = v.symbExLog.openScope(paramLog) - evals(s, eArgs, _ => pveCall, v)((s1, tArgs, eArgsNew, v1) => { + val eArgsWithDAInfo = DependencyAnalysisMergeInfo.attachExpMergeInfo(eArgs, None) + evals(s, eArgsWithDAInfo, _ => pveCall, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => { v1.symbExLog.closeScope(paramId) val exampleTrafo = CounterexampleTransformer({ case ce: SiliconCounterexample => ce.withStore(s1.g) @@ -512,20 +551,41 @@ object executor extends ExecutionRules { val pvePre = ErrorWrapperWithExampleTransformer(PreconditionInCallFalse(call).withReasonNodeTransformed(reasonTransformer), exampleTrafo) val preCondLog = new CommentRecord("Precondition", s1, v1.decider.pcs) val preCondId = v1.symbExLog.openScope(preCondLog) - val argsWithExp = if (withExp) - tArgs zip (eArgsNew.get.map(Some(_))) - else - tArgs zip Seq.fill(tArgs.size)(None) - val s2 = s1.copy(g = Store(fargs.zip(argsWithExp)), + val argsWithExp: Seq[(Term, Option[ast.Exp])] = { + if (Verifier.config.dependencyAnalysis.isDefined) { + tArgs zip eArgsWithDAInfo.map(Some(_)) + } else if (withExp) + tArgs zip (eArgsNew.get.map(Some(_))) + else + tArgs zip Seq.fill(tArgs.size)(None) + } + // encode the method call as a sequence of assignments to fresh variables (one for each argument) and a method call using the fresh variables as arguments + val argsFreshVar = + if (Verifier.config.dependencyAnalysis.isDefined) { + argsWithExp.map(arg => { + val argNew = v1.decider.fresh(arg._1.sort, None) + v1.decider.assume(Equals(argNew, arg._1), None, analysisInfos.withMergeInfo(SimpleDependencyAnalysisMerge(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(arg._2.get)))) + (argNew, None) + }) + } else argsWithExp + val s2 = s1.copy(g = Store(fargs.zip(argsFreshVar)), recordVisited = true) - consumes(s2, meth.pres, false, _ => pvePre, v1)((s3, _, v2) => { + + + val presWithDAInfo = if (!Verifier.config.dependencyAnalysis.isDefined) meth.pres else DependencyAnalysisMergeInfo.attachExpMergeInfo(meth.pres.flatMap(_.topLevelConjuncts), Some(analysisInfos.getSourceInfo)) + + consumes(s2, presWithDAInfo, false, _ => pvePre, v1, analysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Source, EdgeType.Up)))((s3, _, v2) => { v2.symbExLog.closeScope(preCondId) val postCondLog = new CommentRecord("Postcondition", s3, v2.decider.pcs) val postCondId = v2.symbExLog.openScope(postCondLog) val outs = meth.formalReturns.map(_.localVar) val gOuts = Store(outs.map(x => (x, v2.decider.fresh(x))).toMap) val s4 = s3.copy(g = s3.g + gOuts, oldHeaps = s3.oldHeaps + (Verifier.PRE_STATE_LABEL -> magicWandSupporter.getEvalHeap(s1))) - produces(s4, freshSnap, meth.posts, _ => pveCallTransformed, v2)((s5, v3) => { + + val postsWithDAInfo = if (!Verifier.config.dependencyAnalysis.isDefined) meth.posts else DependencyAnalysisMergeInfo.attachExpMergeInfo(meth.posts.flatMap(_.topLevelConjuncts), Some(analysisInfos.getSourceInfo)) + // TODO ake: Assuming the postcondition should not introduce any proof obligations as they are guaranteed by the fact / assumption that the callee verifies. Hence, we mark them as internal assertions. + val analysisInfos2 = analysisInfos.copy(dependencyTypes = List(DependencyTypeInfo(DependencyType(analysisInfos.getDependencyType.assumptionType, AssumptionType.Internal)))).withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Sink, EdgeType.Down)) + produces(s4, freshSnap, postsWithDAInfo, _ => pveCallTransformed, v2, analysisInfos2)((s5, v3) => { v3.symbExLog.closeScope(postCondId) v3.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterContract) val gLhs = Store(lhs.zip(outs) @@ -533,6 +593,7 @@ object executor extends ExecutionRules { val s6 = s5.copy(g = s1.g + gLhs, oldHeaps = s1.oldHeaps, recordVisited = s1.recordVisited) + DependencyAnalyzer.addCustomDependenciesBetweenMergeInfos(v3.decider, presWithDAInfo, postsWithDAInfo) v3.symbExLog.closeScope(sepIdentifier) Q(s6, v3)})})}) @@ -540,12 +601,13 @@ object executor extends ExecutionRules { assert(s.constrainableARPs.isEmpty) v.decider.startDebugSubExp() val ePerm = pap.perm + val predicate = s.program.findPredicate(predicateName) val pve = FoldFailed(fold) - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => - eval(s1, ePerm, pve, v1)((s2, tPerm, ePermNew, v2) => - permissionSupporter.assertPositive(s2, tPerm, if (withExp) ePermNew.get else ePerm, pve, v2)((s3, v3) => { + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => + eval(s1, ePerm, pve, v1, analysisInfos)((s2, tPerm, ePermNew, v2) => + permissionSupporter.assertPositive(s2, tPerm, if (withExp) ePermNew.get else ePerm, pve, v2, analysisInfos)((s3, v3) => { val wildcards = s3.constrainableARPs -- s1.constrainableARPs - predicateSupporter.fold(s3, predAcc, tArgs, eArgsNew, tPerm, ePermNew, wildcards, pve, v3)((s4, v4) => { + predicateSupporter.fold(s3, predAcc, tArgs, eArgsNew, tPerm, ePermNew, wildcards, pve, v3, analysisInfos)((s4, v4) => { v3.decider.finishDebugSubExp(s"folded ${predAcc.toString}") Q(s4, v4) } @@ -557,13 +619,13 @@ object executor extends ExecutionRules { val ePerm = pap.perm val predicate = s.program.findPredicate(predicateName) val pve = UnfoldFailed(unfold) - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => - eval(s1, ePerm, pve, v1)((s2, tPerm, ePermNew, v2) => { - val s2a = v2.heapSupporter.triggerResourceIfNeeded(s2, pa, tArgs, eArgsNew, v2) + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => + eval(s1, ePerm, pve, v1, analysisInfos)((s2, tPerm, ePermNew, v2) => { + val s2a = v2.heapSupporter.triggerResourceIfNeeded(s2, pa, tArgs, eArgsNew, v2, analysisInfos) - permissionSupporter.assertPositive(s2a, tPerm, if (withExp) ePermNew.get else ePerm, pve, v2)((s3, v3) => { + permissionSupporter.assertPositive(s2a, tPerm, if (withExp) ePermNew.get else ePerm, pve, v2, analysisInfos)((s3, v3) => { val wildcards = s3.constrainableARPs -- s1.constrainableARPs - predicateSupporter.unfold(s3, predicate, tArgs, eArgsNew, tPerm, ePermNew, wildcards, pve, v3, pa)( + predicateSupporter.unfold(s3, predicate, tArgs, eArgsNew, tPerm, ePermNew, wildcards, pve, v3, pa, analysisInfos)( (s4, v4) => { v2.decider.finishDebugSubExp(s"unfolded ${pa.toString}") Q(s4, v4) @@ -573,7 +635,7 @@ object executor extends ExecutionRules { case pckg @ ast.Package(wand, proofScript) => val pve = PackageFailed(pckg) - magicWandSupporter.packageWand(s.copy(isInPackage = true), wand, proofScript, pve, v)((s1, chWand, v1) => { + magicWandSupporter.packageWand(s.copy(isInPackage = true), wand, proofScript, pve, v, analysisInfos)((s1, chWand, v1) => { val hOps = s1.reserveHeaps.head + chWand assert(s.exhaleExt || s1.reserveHeaps.length == 1) @@ -599,7 +661,7 @@ object executor extends ExecutionRules { val s3 = chWand match { case ch: QuantifiedMagicWandChunk => - v1.heapSupporter.triggerResourceIfNeeded(s2, wand, ch.singletonArgs.get, ch.singletonArgExps, v1) + v1.heapSupporter.triggerResourceIfNeeded(s2, wand, ch.singletonArgs.get, ch.singletonArgExps, v1, analysisInfos) case _ => s2 } @@ -608,13 +670,13 @@ object executor extends ExecutionRules { case apply @ ast.Apply(e) => val pve = ApplyFailed(apply) - magicWandSupporter.applyWand(s, e, pve, v)(Q) + magicWandSupporter.applyWand(s, e, pve, v, analysisInfos)(Q) case havoc: ast.Quasihavoc => - havocSupporter.execHavoc(havoc, v, s)(Q) + havocSupporter.execHavoc(havoc, v, s, analysisInfos)(Q) case havocall: ast.Quasihavocall => - havocSupporter.execHavocall(havocall, v, s)(Q) + havocSupporter.execHavocall(havocall, v, s, analysisInfos)(Q) case viper.silicon.extensions.TryBlock(body) => var bodySucceeded = false @@ -638,9 +700,9 @@ object executor extends ExecutionRules { executed } - private def ssaifyRhs(rhs: Term, rhsExp: ast.Exp, rhsExpNew: Option[ast.Exp], name: String, typ: ast.Type, v: Verifier, s : State): (Term, Option[ast.Exp]) = { + private def ssaifyRhs(rhs: Term, rhsExp: ast.Exp, rhsExpNew: Option[ast.Exp], name: String, typ: ast.Type, v: Verifier, s : State, analysisInfos: DependencyAnalysisInfos): (Term, Option[ast.Exp]) = { rhs match { - case _: Var | _: Literal => + case _: Var | _: Literal if !Verifier.config.dependencyAnalysis.isDefined => (rhs, rhsExpNew) case _ => @@ -665,7 +727,7 @@ object executor extends ExecutionRules { } else { (None, None) } - v.decider.assumeDefinition(BuiltinEquals(t, rhs), debugExp) + v.decider.assumeDefinition(BuiltinEquals(t, rhs), debugExp, analysisInfos) (t, eNew) } } diff --git a/src/main/scala/rules/HavocSupporter.scala b/src/main/scala/rules/HavocSupporter.scala index 8b3a1cc88..163f9ac2d 100644 --- a/src/main/scala/rules/HavocSupporter.scala +++ b/src/main/scala/rules/HavocSupporter.scala @@ -6,7 +6,9 @@ package viper.silicon.rules +import viper.silicon.state.chunks.MagicWandIdentifier import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.VerificationResult import viper.silicon.rules.evaluator.{eval, evalQuantified, evals} import viper.silicon.state._ @@ -33,7 +35,8 @@ object havocSupporter extends SymbolicExecutionRules { */ def execHavoc(havoc: ast.Quasihavoc, v: Verifier, - s: State) + s: State, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -42,15 +45,15 @@ object havocSupporter extends SymbolicExecutionRules { // If there is no havoc condition, use True as the condition val lhsExpr = havoc.lhs.getOrElse(ast.TrueLit()(havoc.pos)) - eval(s, lhsExpr, pve, v)((s0, lhsTerm, _, v0) => { - evals(s0, havoc.exp.args(s0.program), _ => pve, v0)((s1, tRcvrs, _, v1) => { + eval(s, lhsExpr, pve, v, analysisInfos)((s0, lhsTerm, _, v0) => { + evals(s0, havoc.exp.args(s0.program), _ => pve, v0, analysisInfos)((s1, tRcvrs, _, v1) => { val resource = havoc.exp.res(s1.program) // Call the havoc helper function, which returns a new heap, which is // partially havocked. Since we are executing a Havoc statement, we wrap // the HavocHelperData inside of a HavocOneData case (as opposed to HavocAllData). val condInfo = HavocOneData(tRcvrs) - val newHeap = v1.heapSupporter.havocResource(s1, lhsTerm, resource, condInfo, v1) + val newHeap = v1.heapSupporter.havocResource(s1, lhsTerm, resource, condInfo, v1, analysisInfos) Q(s1.copy(h = newHeap), v1) }) @@ -70,7 +73,8 @@ object havocSupporter extends SymbolicExecutionRules { */ def execHavocall(havocall: ast.Quasihavocall, v: Verifier, - s: State) + s: State, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -95,7 +99,8 @@ object havocSupporter extends SymbolicExecutionRules { optTriggers = None, // Triggers: none needed for Havocall name = qid, pve = pve, - v = v) + v = v, + analysisInfos = analysisInfos) { case (s1, tVars, eVars, Seq(tCond), _, Some((tArgs, eArgs, Seq(), _, _)), v1) => // Seq() represents an empty list of Triggers @@ -117,11 +122,14 @@ object havocSupporter extends SymbolicExecutionRules { v.decider.prover.comment("Check havocall receiver injectivity") val notInjectiveReason = QuasihavocallNotInjective(havocall) - - val injectivityDebugExp = Option.when(withExp)(DebugExp.createInstance("QP receiver injectivity check is well-defined", true)) - v.decider.assume(FunctionPreconditionTransformer.transform(receiverInjectivityCheck, s.program), injectivityDebugExp) - v.decider.assert(receiverInjectivityCheck) { - case false => createFailure(pve dueTo notInjectiveReason, v, s1, receiverInjectivityCheck, "QP receiver injective") + val comment = "QP receiver injectivity check is well-defined" + val injectivityDebugExp = Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)) + v.decider.assume(FunctionPreconditionTransformer.transform(receiverInjectivityCheck, s.program), injectivityDebugExp, analysisInfos) + v.decider.assert(receiverInjectivityCheck, analysisInfos) { + case false => + val failure = createFailure(pve dueTo notInjectiveReason, v, s1, receiverInjectivityCheck, "QP receiver injective") + if (s1.retryLevel == 0) v.decider.handleFailedAssertion(receiverInjectivityCheck, injectivityDebugExp.flatMap(_.originalExp), injectivityDebugExp.flatMap(_.finalExp), analysisInfos, v.reportFurtherErrors()) + if (s1.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s1, v1) else failure case true => // Generate the inverse axioms val (inverseFunctions, imagesOfCodomain) = quantifiedChunkSupporter.getFreshInverseFunctions( @@ -141,13 +149,13 @@ object havocSupporter extends SymbolicExecutionRules { ) val comment = "Definitional axioms for havocall inverse functions" v.decider.prover.comment(comment) - v.decider.assume(inverseFunctions.definitionalAxioms, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false) + v.decider.assume(inverseFunctions.definitionalAxioms, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) // Call the havoc helper function, which returns a new heap, which is // partially havocked. Since we are executing a Havocall statement, we wrap // the HavocHelperData inside of a HavocAllData case. val condInfo = HavocallData(inverseFunctions, codomainQVars, imagesOfCodomain) - val newHeap = v1.heapSupporter.havocResource(s1, tCond, resource, condInfo, v1) + val newHeap = v1.heapSupporter.havocResource(s1, tCond, resource, condInfo, v1, analysisInfos) Q(s1.copy(h = newHeap), v1) } diff --git a/src/main/scala/rules/HeapSupporter.scala b/src/main/scala/rules/HeapSupporter.scala index 649c1c629..d736884d9 100644 --- a/src/main/scala/rules/HeapSupporter.scala +++ b/src/main/scala/rules/HeapSupporter.scala @@ -9,12 +9,13 @@ package viper.silicon.rules import viper.silicon import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis._ import viper.silicon.interfaces.VerificationResult -import viper.silicon.interfaces.state.{ChunkIdentifer, NonQuantifiedChunk} import viper.silicon.resources.{FieldID, PredicateID} import viper.silicon.rules.havocSupporter.{HavocHelperData, HavocOneData, HavocallData} import viper.silicon.rules.quantifiedChunkSupporter.freshSnapshotMap -import viper.silicon.state.{BasicChunk, BasicChunkIdentifier, ChunkIdentifier, Heap, MagicWandChunk, MagicWandIdentifier, QuantifiedBasicChunk, QuantifiedFieldChunk, QuantifiedMagicWandChunk, QuantifiedPredicateChunk, State, Store} +import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.state.terms.perms.IsPositive import viper.silicon.state.terms.predef.{`?r`, `?s`} @@ -23,6 +24,8 @@ import viper.silicon.utils.ast.{BigAnd, replaceVarsInExp} import viper.silicon.utils.freshSnap import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.ast.FalseLit +import viper.silver.dependencyAnalysis.{AssumptionType, DependencyAnalysisSourceInfo, SimpleDependencyAnalysisMerge} import viper.silver.parser.PUnknown import viper.silver.verifier.reasons.{InsufficientPermission, MagicWandChunkNotFound} import viper.silver.verifier.{ErrorReason, PartialVerificationError, VerificationError} @@ -35,7 +38,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { tRcvr: Term, eRcvr: Option[ast.Exp], ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult) : VerificationResult @@ -45,7 +49,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { identifier: ChunkIdentifer, tArgs: Seq[Term], eArgs: Option[Seq[ast.Exp]], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult): VerificationResult def isPossibleTrigger(s: State, fa: ast.FieldAccess): Boolean @@ -57,7 +62,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { tRhs: Term, eRhsNew: Option[ast.Exp], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult @@ -65,7 +71,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { resAcc: ast.ResourceAccess, tArgs: Seq[Term], eArgs: Option[Seq[ast.Exp]], - v: Verifier): State + v: Verifier, + analysisInfos: DependencyAnalysisInfos): State def consumeSingle(s: State, h: Heap, @@ -76,7 +83,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { ePerm: Option[ast.Exp], returnSnap: Boolean, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult): VerificationResult def consumeQuantified(s: State, @@ -104,7 +112,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { negativePermissionReason: => ErrorReason, notInjectiveReason: => ErrorReason, insufficientPermissionReason: => ErrorReason, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult): VerificationResult def produceSingle(s: State, @@ -117,7 +126,8 @@ trait HeapSupportRules extends SymbolicExecutionRules { ePerm: Option[ast.Exp], pve: PartialVerificationError, mergeAndTrigger: Boolean, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult): VerificationResult def produceQuantified(s: State, @@ -144,14 +154,16 @@ trait HeapSupportRules extends SymbolicExecutionRules { pve: PartialVerificationError, negativePermissionReason: => ErrorReason, notInjectiveReason: => ErrorReason, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult): VerificationResult def havocResource(s: State, lhs: Term, resource: ast.Resource, condInfo: HavocHelperData, - v: Verifier): Heap + v: Verifier, + analysisInfos: DependencyAnalysisInfos): Heap def collectForPermConditions(s: State, resource: ast.Resource, @@ -175,18 +187,25 @@ class DefaultHeapSupportRules extends HeapSupportRules { tRhs: Term, eRhsNew: Option[ast.Exp], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, true, analysisInfos) + return Q(s, v) + } + val field = ass.lhs.field val ve = pve dueTo InsufficientPermission(ass.lhs) if (s.qpFields.contains(field)) { val (relevantChunks, otherChunks) = quantifiedChunkSupporter.splitHeap[QuantifiedFieldChunk](s.h, BasicChunkIdentifier(field.name)) val hints = quantifiedChunkSupporter.extractHints(None, Seq(tRcvr)) - val chunkOrderHeuristics = quantifiedChunkSupporter.singleReceiverChunkOrderHeuristic(Seq(tRcvr), hints, v) - val s2 = triggerResourceIfNeeded(s, ass.lhs, Seq(tRcvr), eRcvrNew.map(Seq(_)), v) + val chunkOrderHeuristics = quantifiedChunkSupporter.singleReceiverChunkOrderHeuristic(Seq(tRcvr), hints, v, analysisInfos) + val s2 = triggerResourceIfNeeded(s, ass.lhs, Seq(tRcvr), eRcvrNew.map(Seq(_)), v, analysisInfos) v.decider.clearModel() + val lhsSourceInfos = analysisInfos.withMergeInfo(SimpleDependencyAnalysisMerge(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(ass.lhs))) // splitting lhs and rhs to make permission flow analysis more precise val result = quantifiedChunkSupporter.removePermissions( s2, relevantChunks, @@ -199,7 +218,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { FullPerm, Option.when(withExp)(ast.FullPerm()()), chunkOrderHeuristics, - v + v, + lhsSourceInfos ) result match { case (Complete(), s3, remainingChunks) => @@ -207,25 +227,28 @@ class DefaultHeapSupportRules extends HeapSupportRules { val (sm, smValueDef) = quantifiedChunkSupporter.singletonSnapshotMap(s3, field, Seq(tRcvr), tRhs, v) v.decider.prover.comment("Definitional axioms for singleton-FVF's value") val debugExp = Option.when(withExp)(DebugExp.createInstance("Definitional axioms for singleton-FVF's value", isInternal_ = true)) - v.decider.assumeDefinition(smValueDef, debugExp) + v.decider.assumeDefinition(smValueDef, debugExp, analysisInfos) val ch = quantifiedChunkSupporter.createSingletonQuantifiedChunk(Seq(`?r`), Option.when(withExp)(Seq(ast.LocalVarDecl("r", ast.Ref)(ass.pos, ass.info, ass.errT))), - field, Seq(tRcvr), Option.when(withExp)(Seq(eRcvrNew.get)), FullPerm, Option.when(withExp)(ast.FullPerm()(ass.pos, ass.info, ass.errT)), sm, s.program) + field, Seq(tRcvr), Option.when(withExp)(Seq(eRcvrNew.get)), FullPerm, Option.when(withExp)(ast.FullPerm()(ass.pos, ass.info, ass.errT)), sm, s.program, v, lhsSourceInfos.withDependencyType(AssumptionType.Internal), isExhale=false) if (s3.heapDependentTriggers.contains(field)) { - val debugExp2 = Option.when(withExp)(DebugExp.createInstance(s"FieldTrigger(${eRcvrNew.toString()}.${field.name})")) - v.decider.assume(FieldTrigger(field.name, sm, tRcvr), debugExp2) + val debugExp2 = Option.when(withExp)(DebugExp.createInstance(s"FieldTrigger(${eRcvrNew.toString}.${field.name})")) + v.decider.assume(FieldTrigger(field.name, sm, tRcvr), debugExp2, lhsSourceInfos.withDependencyType(AssumptionType.Trigger)) } val s4 = s3.copy(h = h3 + ch) val (debugHeapName, _) = v.getDebugOldLabel(s4, ass.lhs.pos, Some(magicWandSupporter.getEvalHeap(s4))) val s5 = if (withExp) s4.copy(oldHeaps = s4.oldHeaps + (debugHeapName -> magicWandSupporter.getEvalHeap(s4))) else s4 Q(s5, v) case (Incomplete(_, _), s3, _) => - createFailure(ve, v, s3, "sufficient permission") + val failure = createFailure(ve, v, s3, "sufficient permission") + if (s3.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + if (s3.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s3, v) else failure } } else { val description = s"consume ${ass.pos}: $ass" - chunkSupporter.consume(s, s.h, field, Seq(tRcvr), eRcvrNew.map(Seq(_)), FullPerm, Option.when(withExp)(ast.FullPerm()(ass.pos, ass.info, ass.errT)), false, ve, v, description)((s3, h3, _, v3) => { + val lhsSourceInfos = analysisInfos.withMergeInfo(SimpleDependencyAnalysisMerge(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(ass.lhs))) // splitting lhs and rhs to make permission flow analysis more precise + chunkSupporter.consume(s, s.h, field, Seq(tRcvr), eRcvrNew.map(Seq(_)), FullPerm, Option.when(withExp)(ast.FullPerm()(ass.pos, ass.info, ass.errT)), false, ve, v, description, lhsSourceInfos)((s3, h3, _, v3) => { val id = BasicChunkIdentifier(field.name) - val newChunk = BasicChunk(FieldID, id, Seq(tRcvr), eRcvrNew.map(Seq(_)), tRhs, eRhsNew, FullPerm, Option.when(withExp)(ast.FullPerm()(ass.pos, ass.info, ass.errT))) + val newChunk = v3.chunkFactory.createBasicChunk(FieldID, id, Seq(tRcvr), eRcvrNew.map(Seq(_)), tRhs, eRhsNew, FullPerm, Option.when(withExp)(ast.FullPerm()(ass.pos, ass.info, ass.errT)), lhsSourceInfos.withDependencyType(AssumptionType.Internal)) chunkSupporter.produce(s3, h3, newChunk, v3)((s4, h4, v4) => { val s5 = s4.copy(h = h4) val (debugHeapName, _) = v4.getDebugOldLabel(s5, ass.lhs.pos, Some(magicWandSupporter.getEvalHeap(s5))) @@ -242,9 +265,15 @@ class DefaultHeapSupportRules extends HeapSupportRules { identifier: ChunkIdentifer, tArgs: Seq[Term], eArgs: Option[Seq[ast.Exp]], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult): VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + return Q(s, NoPerm, v) + } + val res = resAcc.res(s.program) /* It is assumed that, for a given field/predicate/wand identifier (res) * either only quantified or only non-quantified chunks are used. @@ -270,7 +299,7 @@ class DefaultHeapSupportRules extends HeapSupportRules { case w: ast.MagicWand => MagicWandIdentifier(w, s2.program).toString } DebugExp.createInstance(s"Resource trigger(${name}($argsString))", isInternal_ = true) - })) + }), analysisInfos.withDependencyType(AssumptionType.Trigger)) } val currentPermAmount = ResourcePermissionLookup(res, pmDef.pm, tArgs, s2.program) @@ -280,7 +309,7 @@ class DefaultHeapSupportRules extends HeapSupportRules { v.decider.prover.comment(s"perm($resAcc) ~~> assume upper permission bound") val (debugHeapName, debugLabel) = v.getDebugOldLabel(s2, resAcc.pos, Some(h)) val exp = Option.when(withExp)(ast.PermLeCmp(ast.DebugLabelledOld(ast.CurrentPerm(resAcc)(), debugLabel)(), ast.FullPerm()())()) - v.decider.assume(PermAtMost(currentPermAmount, FullPerm), exp, exp.map(s2.substituteVarsInExp(_))) + v.decider.assume(PermAtMost(currentPermAmount, FullPerm), exp, exp.map(s2.substituteVarsInExp(_)), analysisInfos.withDependencyType(AssumptionType.Internal)) val s3 = if (Verifier.config.enableDebugging()) s2.copy(oldHeaps = s2.oldHeaps + (debugHeapName -> h)) else s2 s3 case _ => s2 @@ -304,9 +333,19 @@ class DefaultHeapSupportRules extends HeapSupportRules { tRcvr: Term, eRcvr: Option[ast.Exp], ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult) : VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + + val sort = v.symbolConverter.toSort(fa.field.typ) + val newVar = v.decider.fresh(sort, None) // just make sure the returned term typechecks + val s2 = s.copy(functionRecorder = s.functionRecorder.recordConstrainedVar(newVar, True)) + return Q(s2, newVar, v) + } + if (s.qpFields.contains(fa.field)) { val (relevantChunks, _) = quantifiedChunkSupporter.splitHeap[QuantifiedFieldChunk](s.h, BasicChunkIdentifier(fa.field.name)) @@ -320,11 +359,11 @@ class DefaultHeapSupportRules extends HeapSupportRules { * quantifier in whose body field 'fa.field' was accessed) * which is protected by a trigger term that we currently don't have. */ - v.decider.assume(And(fvfDef.valueDefinitions), Option.when(withExp)(DebugExp.createInstance("Value definitions", isInternal_ = true))) + v.decider.assume(And(fvfDef.valueDefinitions), Option.when(withExp)(DebugExp.createInstance("Value definitions", isInternal_ = true)), analysisInfos.withDependencyType(AssumptionType.Internal)) if (s.heapDependentTriggers.contains(fa.field)) { val trigger = FieldTrigger(fa.field.name, fvfDef.sm, tRcvr) val triggerExp = Option.when(withExp)(DebugExp.createInstance(s"FieldTrigger(${eRcvr.toString()}.${fa.field.name})")) - v.decider.assume(trigger, triggerExp) + v.decider.assume(trigger, triggerExp, analysisInfos.withDependencyType(AssumptionType.Trigger)) } if (s.triggerExp) { val fvfLookup = Lookup(fa.field.name, fvfDef.sm, tRcvr) @@ -333,9 +372,13 @@ class DefaultHeapSupportRules extends HeapSupportRules { Q(s2, fvfLookup, v) } else { val toAssert = IsPositive(totalPermissions.replace(`?r`, tRcvr)) - v.decider.assert(toAssert) { + v.decider.assert(toAssert, analysisInfos) { case false => - createFailure(ve, v, s, toAssert, Option.when(withExp)(perms.IsPositive(ast.CurrentPerm(fa)())())) + val debugExp = Option.when(withExp)(perms.IsPositive(ast.CurrentPerm(fa)())()) + val failure = createFailure(ve, v, s, toAssert, debugExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(toAssert, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(fa.field, s, v), Option.when(withExp)(PUnknown())) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, snap, v) else failure case true => val fvfLookup = Lookup(fa.field.name, fvfDef.sm, tRcvr) val fr1 = s.functionRecorder.recordSnapshot(fa, v.decider.pcs.branchConditions, fvfLookup).recordFvfAndDomain(fvfDef) @@ -368,7 +411,7 @@ class DefaultHeapSupportRules extends HeapSupportRules { if (s2.heapDependentTriggers.contains(fa.field)) { val trigger = FieldTrigger(fa.field.name, sm, tRcvr) val triggerExp = Option.when(withExp)(DebugExp.createInstance(s"FieldTrigger(${eRcvr.toString()}.${fa.field.name})")) - v.decider.assume(trigger, triggerExp) + v.decider.assume(trigger, triggerExp, analysisInfos.withDependencyType(AssumptionType.Trigger)) } val (permCheck, permCheckExp, s3) = if (s2.triggerExp) { @@ -384,9 +427,12 @@ class DefaultHeapSupportRules extends HeapSupportRules { } (Implies(lhs, IsPositive(totalPerms)), Option.when(withExp)(perms.IsPositive(ast.CurrentPerm(fa)(fa.pos, fa.info, fa.errT))(fa.pos, fa.info, fa.errT)), s3) } - v.decider.assert(permCheck) { + v.decider.assert(permCheck,analysisInfos) { case false => - createFailure(ve, v, s3, permCheck, permCheckExp) + val failure = createFailure(ve, v, s3, permCheck, permCheckExp) + if (s3.retryLevel == 0) v.decider.handleFailedAssertion(permCheck, permCheckExp, permCheckExp, analysisInfos, v.reportFurtherErrors()) + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(fa.field, s3, v), Option.when(withExp)(PUnknown())) + if (s3.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s3, snap, v) else failure case true => val smLookup = Lookup(fa.field.name, sm, tRcvr) val fr2 = @@ -397,7 +443,7 @@ class DefaultHeapSupportRules extends HeapSupportRules { } } else { val resource = fa.res(s.program) - chunkSupporter.lookup(s, s.h, resource, Seq(tRcvr), Option.when(withExp)(Seq(eRcvr.get)), ve, v)((s2, h2, tSnap, v2) => { + chunkSupporter.lookup(s, s.h, resource, Seq(tRcvr), Option.when(withExp)(Seq(eRcvr.get)), ve, v, analysisInfos)((s2, h2, tSnap, v2) => { val fr = s2.functionRecorder.recordSnapshot(fa, v2.decider.pcs.branchConditions, tSnap) val s3 = s2.copy(h = h2, functionRecorder = fr) Q(s3, tSnap, v) @@ -409,7 +455,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { resAcc: ast.ResourceAccess, tArgs: Seq[Term], eArgs: Option[Seq[ast.Exp]], - v: Verifier): State = { + v: Verifier, + analysisInfos: DependencyAnalysisInfos): State = { if (s.isUsedAsTrigger(resAcc.res(s.program))) { val resource = resAcc.res(s.program) val chunkId = ChunkIdentifier(resource, s.program) @@ -431,7 +478,7 @@ class DefaultHeapSupportRules extends HeapSupportRules { val eArgsStr = eArgs.mkString(", ") val debugExp = Option.when(withExp)(DebugExp.createInstance(Some(s"Resource trigger(${name}($eArgsStr))"), Some(resAcc), Some(resAcc), None, isInternal_ = true, InsertionOrderedSet.empty)) - v.decider.assume(trigger(smDef1.sm), debugExp) + v.decider.assume(trigger(smDef1.sm), debugExp, analysisInfos.withDependencyType(AssumptionType.Trigger)) s.copy(smCache = smCache1, functionRecorder = s.functionRecorder.recordFvfAndDomain(smDef1)) } else { s @@ -448,7 +495,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { ePerm: Option[ast.Exp], pve: PartialVerificationError, mergeAndTrigger: Boolean, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { val useQPs = s.isQuantifiedResource(resource) if (useQPs) { @@ -456,17 +504,17 @@ class DefaultHeapSupportRules extends HeapSupportRules { val tFormalArgs = s.getFormalArgVars(resource, v) val eFormalArgs = Option.when(withExp)(s.getFormalArgDecls(resource)) quantifiedChunkSupporter.produceSingleLocation( - s, resource, tFormalArgs, eFormalArgs, tArgs, eArgs, tSnap, tPerm, ePerm, trigger, mergeAndTrigger, v)(Q) + s, resource, tFormalArgs, eFormalArgs, tArgs, eArgs, tSnap, tPerm, ePerm, trigger, mergeAndTrigger, v, analysisInfos)(Q) } else { resource match { case w: ast.MagicWand => - magicWandSupporter.createChunk(s, w, MagicWandSnapshot(tSnap), pve, v)((s2, chWand, v2) => + magicWandSupporter.createChunk(s, w, MagicWandSnapshot(tSnap), pve, v, analysisInfos)((s2, chWand, v2) => chunkSupporter.produce(s2, s2.h, chWand, v2)((s3, h3, v3) => Q(s3.copy(h = h3), v3))) case _ => val chunkId = ChunkIdentifier(resource, s.program) val (resId, snap1) = if (resource.isInstanceOf[ast.Field]) (FieldID, tSnap) else (PredicateID, tSnap.convert(sorts.Snap)) - val ch = BasicChunk(resId, chunkId.asInstanceOf[BasicChunkIdentifier], tArgs, eArgs, snap1, eSnap, tPerm, ePerm) + val ch = v.chunkFactory.createBasicChunk(resId, chunkId.asInstanceOf[BasicChunkIdentifier], tArgs, eArgs, snap1, eSnap, tPerm, ePerm, analysisInfos) if (mergeAndTrigger) { chunkSupporter.produce(s, s.h, ch, v)((s2, h2, v2) => { if (resource.isInstanceOf[ast.Predicate] && Verifier.config.enablePredicateTriggersOnInhale() && s2.functionRecorder == NoopFunctionRecorder @@ -474,7 +522,7 @@ class DefaultHeapSupportRules extends HeapSupportRules { val predicate = resource.asInstanceOf[ast.Predicate] val argsString = eArgs.mkString(", ") val debugExp = Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($argsString))", isInternal_ = true)) - v2.decider.assume(App(s2.predicateData(predicate.name).triggerFunction, snap1 +: tArgs), debugExp) + v2.decider.assume(App(s2.predicateData(predicate.name).triggerFunction, snap1 +: tArgs), debugExp, analysisInfos.withDependencyType(AssumptionType.Trigger)) } Q(s2.copy(h = h2), v2) }) @@ -494,22 +542,28 @@ class DefaultHeapSupportRules extends HeapSupportRules { ePerm: Option[ast.Exp], returnSnap: Boolean, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult): VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + return Q(s, h, Some(Unit), v) + } + val resource = resAcc.res(s.program) val useQPs = s.isQuantifiedResource(resource) if (useQPs) { val tFormalArgs = s.getFormalArgVars(resource, v) val eFormalArgs = Option.when(withExp)(s.getFormalArgDecls(resource)) quantifiedChunkSupporter.consumeSingleLocation( - s, h, tFormalArgs, eFormalArgs, tArgs, eArgs, resAcc, tPerm, ePerm, returnSnap, None, pve, v)(Q) + s, h, tFormalArgs, eFormalArgs, tArgs, eArgs, resAcc, tPerm, ePerm, returnSnap, None, pve, v, analysisInfos)(Q) } else { val ve = resAcc match { case l: ast.LocationAccess => pve dueTo InsufficientPermission(l) case w: ast.MagicWand => pve dueTo MagicWandChunkNotFound(w) } val description = s"consume ${resAcc.pos}: $resAcc" - chunkSupporter.consume(s, h, resource, tArgs, eArgs, tPerm, ePerm, returnSnap, ve, v, description)(Q) + chunkSupporter.consume(s, h, resource, tArgs, eArgs, tPerm, ePerm, returnSnap, ve, v, description, analysisInfos)(Q) } } @@ -537,7 +591,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { pve: PartialVerificationError, negativePermissionReason: => ErrorReason, notInjectiveReason: => ErrorReason, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult): VerificationResult = { val tSnap = resource match { case f: ast.Field => @@ -573,7 +628,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { pve, negativePermissionReason, notInjectiveReason, - v + v, + analysisInfos )(Q) } @@ -602,8 +658,14 @@ class DefaultHeapSupportRules extends HeapSupportRules { negativePermissionReason: => ErrorReason, notInjectiveReason: => ErrorReason, insufficientPermissionReason: => ErrorReason, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult): VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(true, false, analysisInfos) + return Q(s, h, Some(Unit), v) + } + quantifiedChunkSupporter.consume( s, h, @@ -630,7 +692,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { negativePermissionReason, notInjectiveReason, insufficientPermissionReason, - v + v, + analysisInfos )(Q) } @@ -638,11 +701,12 @@ class DefaultHeapSupportRules extends HeapSupportRules { lhs: Term, resource: ast.Resource, condInfo: HavocHelperData, - v: Verifier): Heap = { + v: Verifier, + analysisInfos: DependencyAnalysisInfos): Heap = { if (s.isQuantifiedResource(resource)) { - havocQuantifiedResource(s, lhs, resource, condInfo, v) + havocQuantifiedResource(s, lhs, resource, condInfo, v, analysisInfos) } else { - havocNonQuantifiedResource(s, lhs, resource, condInfo, v) + havocNonQuantifiedResource(s, lhs, resource, condInfo, v, analysisInfos) } } @@ -663,7 +727,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { lhs: Term, resource: ast.Resource, condInfo: HavocHelperData, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : Heap = { val id = ChunkIdentifier(resource, s.program) @@ -674,12 +739,12 @@ class DefaultHeapSupportRules extends HeapSupportRules { val havockedSnap = v.decider.fresh("mwsf", sorts.MagicWandSnapFunction, Option.when(withExp)(PUnknown())) val cond = replacementCond(lhs, ch.args, condInfo) val magicWandSnapshot = MagicWandSnapshot(Ite(cond, havockedSnap, ch.snap.mwsf)) - ch.withSnap(magicWandSnapshot, None) + v.chunkFactory.withSnap(ch, magicWandSnapshot, None, analysisInfos) case ch => val havockedSnap = freshSnap(ch.snap.sort, v) val cond = replacementCond(lhs, ch.args, condInfo) - ch.withSnap(Ite(cond, havockedSnap, ch.snap), None) + v.chunkFactory.withSnap(ch, Ite(cond, havockedSnap, ch.snap), None, analysisInfos) } Heap(otherChunks ++ newChunks) } @@ -705,7 +770,8 @@ class DefaultHeapSupportRules extends HeapSupportRules { lhs: Term, resource: ast.Resource, condInfo: HavocHelperData, - v: Verifier) : Heap = { + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : Heap = { // Quantified field chunks are of the form R(r; sm, pm). // Conceptually, quantified predicate/wand chunks look like R(r1, ..., rn; sm, pm). @@ -758,9 +824,9 @@ class DefaultHeapSupportRules extends HeapSupportRules { v.decider.prover.comment("axiomatized snapshot map after havoc") val debugExp = Option.when(withExp)(DebugExp.createInstance("havoc new axiom", isInternal_ = true)) - v.decider.assume(newAxiom, debugExp) + v.decider.assume(newAxiom, debugExp, analysisInfos) - ch.withSnapshotMap(newSm) + v.chunkFactory.withSnapshotMap(ch, newSm, analysisInfos) } Heap(newChunks ++ otherChunks) } diff --git a/src/main/scala/rules/Joiner.scala b/src/main/scala/rules/Joiner.scala index e61691995..6c79f5d25 100644 --- a/src/main/scala/rules/Joiner.scala +++ b/src/main/scala/rules/Joiner.scala @@ -6,9 +6,10 @@ package viper.silicon.rules -import viper.silicon.debugger.DebugExp import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp import viper.silicon.decider.RecordedPathConditions +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.{Success, VerificationResult} import viper.silicon.logger.records.structural.JoiningRecord import viper.silicon.state.State @@ -16,26 +17,29 @@ import viper.silicon.state.terms.{And, Or, Term} import viper.silicon.utils.ast.{BigAnd, BigOr} import viper.silicon.verifier.Verifier import viper.silver.ast - -import scala.annotation.unused +import viper.silver.ast.NoPosition +import viper.silver.dependencyAnalysis.{AssumptionType, StringDependencyAnalysisSourceInfo} case class JoinDataEntry[D](s: State, data: D, pathConditions: RecordedPathConditions) { + + private val analysisInfos = DependencyAnalysisInfos.DefaultInfos.withInfo(StringDependencyAnalysisSourceInfo("merge", NoPosition), AssumptionType.Internal) // TODO ake: is it fine to use DefaultInfos here? + // Instead of merging states by calling State.merge, // we can directly merge JoinDataEntries to obtain new States, // and the join data entries themselves provide information about the path conditions to State.merge. def pathConditionAwareMerge(other: JoinDataEntry[D], v: Verifier): State = { - val res = State.merge(this.s, this.pathConditions, other.s, other.pathConditions) + val res = State.merge(this.s, this.pathConditions, other.s, other.pathConditions, v, analysisInfos) v.stateConsolidator(s).consolidate(res, v) } - def pathConditionAwareMergeWithoutConsolidation(other: JoinDataEntry[D], @unused v: Verifier): State = { - State.merge(this.s, this.pathConditions, other.s, other.pathConditions) + def pathConditionAwareMergeWithoutConsolidation(other: JoinDataEntry[D], v: Verifier): State = { + State.merge(this.s, this.pathConditions, other.s, other.pathConditions, v, analysisInfos) } } trait JoiningRules extends SymbolicExecutionRules { - def join[D, JD](s: State, v: Verifier, resetState: Boolean = true) + def join[D, JD](s: State, v: Verifier, analysisInfos: DependencyAnalysisInfos, resetState: Boolean = true) (block: (State, Verifier, (State, D, Verifier) => VerificationResult) => VerificationResult) (merge: Seq[JoinDataEntry[D]] => (State, JD)) (Q: (State, JD, Verifier) => VerificationResult) @@ -43,7 +47,7 @@ trait JoiningRules extends SymbolicExecutionRules { } object joiner extends JoiningRules { - def join[D, JD](s: State, v: Verifier, resetState: Boolean = true) + def join[D, JD](s: State, v: Verifier, analysisInfos: DependencyAnalysisInfos, resetState: Boolean = true) (block: (State, Verifier, (State, D, Verifier) => VerificationResult) => VerificationResult) (merge: Seq[JoinDataEntry[D]] => (State, JD)) (Q: (State, JD, Verifier) => VerificationResult) @@ -102,13 +106,13 @@ object joiner extends JoiningRules { val pcsExp = Option.when(withExp)(entry.pathConditions.conditionalizedExp) val comment = "Joined path conditions" v.decider.prover.comment(comment) - v.decider.assume(pcs, Option.when(withExp)(DebugExp.createInstance(comment, InsertionOrderedSet(pcsExp.get))), enforceAssumption = false) + v.decider.assume(pcs, Option.when(withExp)(DebugExp.createInstance(comment, InsertionOrderedSet(pcsExp.get))), enforceAssumption = false, analysisInfos) feasibleBranches = And(entry.pathConditions.branchConditions) :: feasibleBranches feasibleBranchesExp = feasibleBranchesExp.map(fbe => BigAnd(entry.pathConditions.branchConditionExps.map(_._1)) :: fbe) feasibleBranchesExpNew = feasibleBranchesExpNew.map(fbe => BigAnd(entry.pathConditions.branchConditionExps.map(_._2.get)) :: fbe) }) // Assume we are in a feasible branch - v.decider.assume(Or(feasibleBranches), Option.when(withExp)(DebugExp.createInstance(Some("Feasible Branches"), feasibleBranchesExp.map(BigOr(_)), feasibleBranchesExpNew.map(BigOr(_)), InsertionOrderedSet.empty))) + v.decider.assume(Or(feasibleBranches), Option.when(withExp)(DebugExp.createInstance(Some("Feasible Branches"), feasibleBranchesExp.map(BigOr(_)), feasibleBranchesExpNew.map(BigOr(_)), InsertionOrderedSet.empty)), analysisInfos) Q(sJoined, dataJoined, v) } } diff --git a/src/main/scala/rules/LetSupporter.scala b/src/main/scala/rules/LetSupporter.scala index 8707efd9e..e04af3c73 100644 --- a/src/main/scala/rules/LetSupporter.scala +++ b/src/main/scala/rules/LetSupporter.scala @@ -6,21 +6,24 @@ package viper.silicon.rules -import viper.silver.ast -import viper.silver.verifier.PartialVerificationError +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.VerificationResult -import viper.silicon.state.{State, Store} import viper.silicon.state.terms.Term +import viper.silicon.state.{State, Store} import viper.silicon.verifier.Verifier +import viper.silver.ast +import viper.silver.verifier.PartialVerificationError trait LetSupportRules extends SymbolicExecutionRules { def handle[E <: ast.Exp] - (s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier) + (s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Store, E, Verifier) => VerificationResult) : VerificationResult def handle[E <: ast.Exp] - (s: State, let: ast.Let, pve: PartialVerificationError, v: Verifier) + (s: State, let: ast.Let, pve: PartialVerificationError, v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Store, E, Verifier) => VerificationResult) : VerificationResult } @@ -29,22 +32,24 @@ object letSupporter extends LetSupportRules { import evaluator._ def handle[E <: ast.Exp] - (s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier) + (s: State, e: ast.Exp, pve: PartialVerificationError, v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Store, E, Verifier) => VerificationResult) : VerificationResult = { e match { - case let: ast.Let => handle(s, Nil, let, pve, v)(Q) + case let: ast.Let => handle(s, Nil, let, pve, v, analysisInfos)(Q) case _ => Q(s, Store(), e.asInstanceOf[E], v) } } def handle[E <: ast.Exp] - (s: State, let: ast.Let, pve: PartialVerificationError, v: Verifier) + (s: State, let: ast.Let, pve: PartialVerificationError, v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Store, E, Verifier) => VerificationResult) : VerificationResult = { - handle(s, Nil, let, pve, v)(Q) + handle(s, Nil, let, pve, v, analysisInfos)(Q) } private def handle[E <: ast.Exp] @@ -52,17 +57,18 @@ object letSupporter extends LetSupportRules { bindings: Seq[(ast.AbstractLocalVar, (Term, Option[ast.Exp]))], let: ast.Let, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Store, E, Verifier) => VerificationResult) : VerificationResult = { val ast.Let(x, exp, body) = let - eval(s, exp, pve, v)((s1, t, expNew, v1) => { + eval(s, exp, pve, v, analysisInfos)((s1, t, expNew, v1) => { val bindings1 = bindings :+ (x.localVar, (t, expNew)) val s2 = s1.copy(s1.g + (x.localVar, (t, expNew))) body match { - case nestedLet: ast.Let => handle(s2, bindings1, nestedLet, pve, v1)(Q) + case nestedLet: ast.Let => handle(s2, bindings1, nestedLet, pve, v1, analysisInfos)(Q) case _ => Q(s2, Store(bindings1), body.asInstanceOf[E], v1)}}) } } diff --git a/src/main/scala/rules/MagicWandSupporter.scala b/src/main/scala/rules/MagicWandSupporter.scala index c5fc50dbd..0758181d1 100644 --- a/src/main/scala/rules/MagicWandSupporter.scala +++ b/src/main/scala/rules/MagicWandSupporter.scala @@ -6,20 +6,22 @@ package viper.silicon.rules -import viper.silicon.debugger.DebugExp import viper.silicon._ import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp import viper.silicon.decider.RecordedPathConditions +import viper.silicon.dependencyAnalysis.{DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces._ -import viper.silicon.interfaces.state._ import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.utils.{freshSnap, toSf} import viper.silicon.verifier.Verifier import viper.silver.ast -import viper.silver.ast.{Exp, Stmt} +import viper.silver.ast.{Exp, FalseLit, NoPosition, Stmt} import viper.silver.cfg.Edge import viper.silver.cfg.silver.SilverCfg.SilverBlock +import viper.silver.dependencyAnalysis.{AssumptionType, ExpDependencyAnalysisSourceInfo, StringDependencyAnalysisSourceInfo} import viper.silver.parser.PUnknown import viper.silver.verifier.PartialVerificationError @@ -83,19 +85,21 @@ object magicWandSupporter extends SymbolicExecutionRules { // } /** - * Evaluate the wand's arguments and create a [[viper.silicon.state.MagicWandChunk]] out of it. + * Evaluate the wand's arguments and create a [[MagicWandChunk]] out of it. */ def createChunk(s: State, wand: ast.MagicWand, snap: MagicWandSnapshot, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, MagicWandChunk, Verifier) => VerificationResult) : VerificationResult = { - evaluateWandArguments(s, wand, pve, v)((s1, ts, esNew, v1) => - Q(s1, MagicWandChunk(MagicWandIdentifier(wand, s.program), s1.g.values, ts, esNew, snap, FullPerm, - Option.when(withExp)(ast.FullPerm()(wand.pos, wand.info, wand.errT))), v1) - ) + evaluateWandArguments(s, wand, pve, v, analysisInfos)((s1, ts, esNew, v1) => { + val newChunk = v1.chunkFactory.createMagicWandChunk(MagicWandIdentifier(wand, s.program), s1.g.values, ts, esNew, snap, FullPerm, + Option.when(withExp)(ast.FullPerm()(wand.pos, wand.info, wand.errT)), analysisInfos) + Q(s1, newChunk, v1) + }) } /** @@ -111,14 +115,14 @@ object magicWandSupporter extends SymbolicExecutionRules { * @param v Verifier instance * @return Fresh instance of [[viper.silicon.state.terms.MagicWandSnapshot]] */ - def createMagicWandSnapshot(abstractLhs: Var, rhsSnapshot: Term, v: Verifier): MagicWandSnapshot = { + def createMagicWandSnapshot(abstractLhs: Var, rhsSnapshot: Term, v: Verifier, analysisInfos: DependencyAnalysisInfos): MagicWandSnapshot = { val mwsf = v.decider.fresh("mwsf", sorts.MagicWandSnapFunction, Option.when(withExp)(PUnknown())) val magicWandSnapshot = MagicWandSnapshot(mwsf) v.decider.assumeDefinition(Forall( abstractLhs, MWSFLookup(mwsf, abstractLhs) === rhsSnapshot, Trigger(MWSFLookup(mwsf, abstractLhs)) - ), Option.when(withExp)(DebugExp.createInstance("Magic wand snapshot definition", true))) + ), Option.when(withExp)(DebugExp.createInstance("Magic wand snapshot definition", isInternal_ = true)), analysisInfos) magicWandSnapshot } @@ -132,13 +136,14 @@ object magicWandSupporter extends SymbolicExecutionRules { def evaluateWandArguments(s: State, wand: ast.MagicWand, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Seq[Term], Option[Seq[ast.Exp]], Verifier) => VerificationResult) : VerificationResult = { val s1 = s.copy(exhaleExt = false) val es = wand.subexpressionsToEvaluate(s.program) - evals(s1, es, _ => pve, v)((s2, ts, esNew, v1) => { + evals(s1, es, _ => pve, v, analysisInfos)((s2, ts, esNew, v1) => { Q(s2.copy(exhaleExt = s.exhaleExt), ts, esNew, v1) }) } @@ -150,12 +155,13 @@ object magicWandSupporter extends SymbolicExecutionRules { pLossExp: Option[ast.Exp], failure: Failure, qvars: Seq[Var], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (consumeFunction: (State, Heap, Term, Option[ast.Exp], Verifier) => (ConsumptionResult, State, Heap, Option[CH])) (Q: (State, Stack[Heap], Stack[Option[CH]], Verifier) => VerificationResult) : VerificationResult = { - val initialConsumptionResult = ConsumptionResult(pLoss, pLossExp, qvars, v, Verifier.config.checkTimeout()) + val initialConsumptionResult = ConsumptionResult(pLoss, pLossExp, qvars, v, Verifier.config.checkTimeout(), analysisInfos) /* TODO: Introduce a dedicated timeout for the permission check performed by ConsumptionResult, * instead of using checkTimeout. Reason: checkTimeout is intended for checks that are * optimisations, e.g. detecting if a chunk provided no permissions or if a branch is @@ -181,7 +187,7 @@ object magicWandSupporter extends SymbolicExecutionRules { case (Some(ch1: QuantifiedBasicChunk), Some(ch2: QuantifiedBasicChunk)) => ch1.snapshotMap === ch2.snapshotMap case _ => True } - v.decider.assume(tEq, Option.when(withExp)(DebugExp.createInstance("Snapshots", isInternal_ = true))) + v.decider.assume(tEq, Option.when(withExp)(DebugExp.createInstance("Snapshots", isInternal_ = true)), analysisInfos) /* In the future it might be worth to recheck whether the permissions needed, in the case of * success being an instance of Incomplete, are zero. @@ -196,7 +202,7 @@ object magicWandSupporter extends SymbolicExecutionRules { * from heap, i.e. that tEq does not result in already having the required permissions before * consuming from heap. */ - if (v.decider.checkSmoke()) { + if (v.decider.checkSmoke(analysisInfos)) { (Complete(), sOut, h +: hps, cch +: cchs) } else { (success, sOut, h +: hps, cch +: cchs) @@ -207,7 +213,9 @@ object magicWandSupporter extends SymbolicExecutionRules { assert(heaps.length == hs.length) assert(consumedChunks.length == hs.length) Q(s1, heaps.reverse, consumedChunks.reverse, v) - case Incomplete(_, _) => failure + case Incomplete(_, _) => + if (s1.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + if (s1.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s1, heaps.reverse, consumedChunks.reverse, v) else failure } } @@ -234,10 +242,12 @@ object magicWandSupporter extends SymbolicExecutionRules { wand: ast.MagicWand, proofScript: ast.Seqn, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Chunk, Verifier) => VerificationResult) : VerificationResult = { + val s = if (state.exhaleExt) state else state.copy(reserveHeaps = v.heapSupporter.getEmptyHeap(state.program) :: state.h :: Nil) @@ -304,16 +314,17 @@ object magicWandSupporter extends SymbolicExecutionRules { def createWandChunkAndRecordResults(s: State, freshSnapRoot: Var, snapRhs: Term, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : VerificationResult = { val preMark = v.decider.setPathConditionMark() v.decider.prover.comment(s"Create MagicWandSnapFunction for wand $wand") - val wandSnapshot = this.createMagicWandSnapshot(freshSnapRoot, snapRhs, v) + val wandSnapshot = this.createMagicWandSnapshot(freshSnapRoot, snapRhs, v, analysisInfos) val bodyVars = wand.subexpressionsToEvaluate(s.program) - evals(s, bodyVars, _ => pve, v)((s2, tArgs, eArgsNew, v2) => { + evals(s, bodyVars, _ => pve, v, analysisInfos)((s2, tArgs, eArgsNew, v2) => { // Currently, the snapshot of a wand differs depending on whether it is a quantified magic wand or not. // Therefore, we have to keep the case distinction here and cannot leave everything but the chunk creation // to the HeapSupporter. @@ -324,14 +335,14 @@ object magicWandSupporter extends SymbolicExecutionRules { val (sm, smValueDef) = quantifiedChunkSupporter.singletonSnapshotMap(s2, wand, tArgs, snapshotTerm, v2) v2.decider.prover.comment("Definitional axioms for singleton-SM's value") val debugExp = Option.when(withExp)(DebugExp.createInstance("Definitional axioms for singleton-SM's value", true)) - v2.decider.assumeDefinition(smValueDef, debugExp) + v2.decider.assumeDefinition(smValueDef, debugExp, analysisInfos) val ch = quantifiedChunkSupporter.createSingletonQuantifiedChunk(formalVars, formalVarExps, wand, tArgs, - eArgsNew, FullPerm, Option.when(withExp)(ast.FullPerm()()), sm, s.program) + eArgsNew, FullPerm, Option.when(withExp)(ast.FullPerm()()), sm, s.program, v, analysisInfos, isExhale=false) val conservedPcs = s2.conservedPcs.head :+ v2.decider.pcs.after(preMark).definitionsOnly (s2, ch, conservedPcs.flatMap(_.conditionalized), Option.when(withExp)(conservedPcs.flatMap(_.conditionalizedExp)), v2) } else { - val ch = MagicWandChunk(MagicWandIdentifier(wand, s.program), s2.g.values, tArgs, eArgsNew, wandSnapshot, FullPerm, - Option.when(withExp)(ast.FullPerm()(wand.pos, wand.info, wand.errT))) + val ch = v2.chunkFactory.createMagicWandChunk(MagicWandIdentifier(wand, s.program), s2.g.values, tArgs, eArgsNew, wandSnapshot, FullPerm, + Option.when(withExp)(ast.FullPerm()(wand.pos, wand.info, wand.errT)), analysisInfos) val conservedPcs = s2.conservedPcs.head :+ v2.decider.pcs.after(preMark).definitionsOnly // Partition path conditions into a set which include the freshSnapRoot and those which do not val (pcsWithFreshSnapRoot, pcsWithoutFreshSnapRoot) = conservedPcs.flatMap(pcs => pcs.conditionalized).partition(_.contains(freshSnapRoot)) @@ -353,7 +364,9 @@ object magicWandSupporter extends SymbolicExecutionRules { }) } + var analysisLabels = InsertionOrderedSet[Term]().empty val tempResult = executionFlowController.locally(sEmp, v)((s1, v1) => { + val prePackageMark = v.decider.pcs.mark() /* A snapshot (binary tree) will be constructed using First/Second datatypes, * that preserves the original root. The leafs of this tree will later appear * in the snapshot of the RHS at the appropriate places. Thus equating @@ -363,7 +376,8 @@ object magicWandSupporter extends SymbolicExecutionRules { val freshSnapRoot = freshSnap(sorts.Snap, v1) // Produce the wand's LHS. - produce(s1.copy(conservingSnapshotGeneration = true), toSf(freshSnapRoot), wand.left, pve, v1)((sLhs, v2) => { + val analysisInfosLeft = analysisInfos.withSource(ExpDependencyAnalysisSourceInfo(wand.left, wand.left.pos)) + produce(s1.copy(conservingSnapshotGeneration = true), toSf(freshSnapRoot), wand.left, pve, v1, analysisInfosLeft)((sLhs, v2) => { val proofScriptCfg = proofScript.toCfg() val emptyHeap = v2.heapSupporter.getEmptyHeap(sLhs.program) @@ -399,10 +413,10 @@ object magicWandSupporter extends SymbolicExecutionRules { // This part indirectly calls the methods `this.transfer` and `this.consumeFromMultipleHeaps`. consume( proofScriptState.copy(oldHeaps = s2.oldHeaps, reserveCfgs = proofScriptState.reserveCfgs.tail), - wand.right, true, pve, proofScriptVerifier + wand.right, true, pve, proofScriptVerifier, analysisInfos )((s3, snapRhs, v3) => { - - createWandChunkAndRecordResults(s3.copy(exhaleExt = false, oldHeaps = s.oldHeaps), freshSnapRoot, snapRhs.get, v3) + analysisLabels = v.decider.pcs.after(prePackageMark).analysisLabels + createWandChunkAndRecordResults(s3.copy(exhaleExt = false, oldHeaps = s.oldHeaps), freshSnapRoot, snapRhs.get, v3, analysisInfos) }) }) }) @@ -414,9 +428,12 @@ object magicWandSupporter extends SymbolicExecutionRules { // Moreover, we need to set reserveHeaps to structurally match [State RHS] below. val emptyHeap = v.heapSupporter.getEmptyHeap(sEmp.program) val s1 = sEmp.copy(reserveHeaps = emptyHeap +: emptyHeap +: emptyHeap +: s.reserveHeaps.tail) - createWandChunkAndRecordResults(s1, freshSnap(sorts.Snap, v), freshSnap(sorts.Snap, v), v) + createWandChunkAndRecordResults(s1, freshSnap(sorts.Snap, v), freshSnap(sorts.Snap, v), v, analysisInfos) } + // some of the analysis labels, introduced while verifying the package statement, might be needed later on -> reassume them + analysisLabels foreach (l => v.decider.assume(DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v.decider, l, Set.empty, Set(l)), None, analysisInfos.withDependencyType(AssumptionType.Internal))) + recordedBranches.foldLeft(tempResult)((prevRes, recordedState) => { prevRes combine { val (state, branchConditions, branchConditionsExp, conservedPcs, magicWandChunk) = recordedState @@ -430,10 +447,10 @@ object magicWandSupporter extends SymbolicExecutionRules { val exp = viper.silicon.utils.ast.BigAnd(branchConditionsExp.map(_._1)) val expNew = Option.when(withExp)(viper.silicon.utils.ast.BigAnd(branchConditionsExp.map(_._2.get))) // Set the branch conditions - v1.decider.setCurrentBranchCondition(And(branchConditions), (exp, expNew)) + v1.decider.setCurrentBranchCondition(And(branchConditions map (t => DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v1.decider, t, Set.empty, Set(t)))), (exp, expNew), analysisInfos) // Recreate all path conditions in the Z3 proof script that we recorded for that branch - v1.decider.assume(conservedPcs._1, conservedPcs._2) + v1.decider.assume(conservedPcs._1 map (t => DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v1.decider, t, Set.empty, Set(t))), conservedPcs._2, analysisInfos.withDependencyType(AssumptionType.Internal)) // Execute the continuation Q Q(s2, magicWandChunk, v1) @@ -455,13 +472,14 @@ object magicWandSupporter extends SymbolicExecutionRules { def applyWand(s: State, wand: ast.MagicWand, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { // Consume the magic wand instance "A --* B". - consume(s, wand, true, pve, v)((s1, snapWand, v1) => { + consume(s, wand, true, pve, v, analysisInfos)((s1, snapWand, v1) => { // Consume the wand's LHS "A". - consume(s1, wand.left, true, pve, v1)((s2, snapLhs, v2) => { + consume(s1, wand.left, true, pve, v1, analysisInfos)((s2, snapLhs, v2) => { /* It is assumed that snap and MagicWandSnapshot.abstractLhs are structurally the same. * Equating the two snapshots is sound iff a wand is applied only once. * The old solution in this case did use this assumption: @@ -478,13 +496,13 @@ object magicWandSupporter extends SymbolicExecutionRules { case SortWrapper(snapshot: MagicWandSnapshot, _) => snapshot.applyToMWSF(snapLhs.get) // Fallback solution for quantified magic wands case predicateLookup: PredicateLookup => - v2.decider.assume(snapLhs.get === First(snapWand.get), Option.when(withExp)(DebugExp.createInstance("Magic wand snapshot", true))) + v2.decider.assume(snapLhs.get === First(snapWand.get), Option.when(withExp)(DebugExp.createInstance("Magic wand snapshot", isInternal_ = true)), analysisInfos) Second(predicateLookup) case _ => snapWand.get } // Produce the wand's RHS. - produce(s3.copy(conservingSnapshotGeneration = true), toSf(magicWandSnapshotLookup), wand.right, pve, v2)((s4, v3) => { + produce(s3.copy(conservingSnapshotGeneration = true), toSf(magicWandSnapshotLookup), wand.right, pve, v2, analysisInfos)((s4, v3) => { // Recreate old state without the magic wand, and the state with the oldHeap called lhs. val s5 = s4.copy(g = s1.g, conservingSnapshotGeneration = s3.conservingSnapshotGeneration) @@ -503,7 +521,8 @@ object magicWandSupporter extends SymbolicExecutionRules { permsExp: Option[ast.Exp], failure: Failure, qvars: Seq[Var], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (consumeFunction: (State, Heap, Term, Option[ast.Exp], Verifier) => (ConsumptionResult, State, Heap, Option[CH])) (Q: (State, Option[CH], Verifier) => VerificationResult) : VerificationResult = { @@ -520,13 +539,13 @@ object magicWandSupporter extends SymbolicExecutionRules { */ val preMark = v.decider.setPathConditionMark() executionFlowController.tryOrFail2[Stack[Heap], Stack[Option[CH]]](s, v)((s1, v1, QS) => - this.consumeFromMultipleHeaps(s1, s1.reserveHeaps.tail, perms, permsExp, failure, qvars, v1)(consumeFunction)(QS) + this.consumeFromMultipleHeaps(s1, s1.reserveHeaps.tail, perms, permsExp, failure, qvars, v1, analysisInfos)(consumeFunction)(QS) )((s2, hs2, chs2, v2) => { val conservedPcs = s2.conservedPcs.head :+ v2.decider.pcs.after(preMark) val s3 = s2.copy(conservedPcs = conservedPcs +: s2.conservedPcs.tail, reserveHeaps = s.reserveHeaps.head +: hs2) val usedChunks = chs2.flatten - val (fr4, hUsed) = v2.stateConsolidator(s2).merge(s3.functionRecorder, s2, s2.reserveHeaps.head, Heap(usedChunks), v2) + val (fr4, hUsed) = v2.stateConsolidator(s2).merge(s3.functionRecorder, s2, s2.reserveHeaps.head, Heap(usedChunks), v2, analysisInfos) val s4 = s3.copy(functionRecorder = fr4, reserveHeaps = hUsed +: s3.reserveHeaps.tail) @@ -571,8 +590,10 @@ object magicWandSupporter extends SymbolicExecutionRules { * is consumed from hOps and permissions for the predicate are added to the state's * heap. After a statement is executed those permissions are transferred to hOps. */ + + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo("merge", NoPosition), AssumptionType.Internal) val emptyHeap = v.heapSupporter.getEmptyHeap(newState.program) - val (fr, hOpsJoinUsed) = v.stateConsolidator(newState).merge(newState.functionRecorder, newState, newState.reserveHeaps(1), newState.h, v) + val (fr, hOpsJoinUsed) = v.stateConsolidator(newState).merge(newState.functionRecorder, newState, newState.reserveHeaps(1), newState.h, v, analysisInfos) newState.copy(functionRecorder = fr, h = emptyHeap, reserveHeaps = emptyHeap +: hOpsJoinUsed +: newState.reserveHeaps.drop(2)) } else newState diff --git a/src/main/scala/rules/MoreCompleteExhaleSupporter.scala b/src/main/scala/rules/MoreCompleteExhaleSupporter.scala index 1aa582efd..21f11fa17 100644 --- a/src/main/scala/rules/MoreCompleteExhaleSupporter.scala +++ b/src/main/scala/rules/MoreCompleteExhaleSupporter.scala @@ -7,21 +7,25 @@ package viper.silicon.rules import viper.silicon.debugger.DebugExp -import viper.silicon.interfaces.state._ +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.{Success, VerificationResult} import viper.silicon.resources.{FieldID, NonQuantifiedPropertyInterpreter, Resources} import viper.silicon.rules.chunkSupporter.findChunksWithID import viper.silicon.state._ +import viper.silicon.state.chunks.{BasicChunk, Chunk, ChunkIdentifier, NonQuantifiedChunk} import viper.silicon.state.terms._ import viper.silicon.state.terms.perms.{IsNonPositive, IsPositive} import viper.silicon.supporters.functions.NoopFunctionRecorder -import viper.silicon.utils.ast.{BigAnd, buildMinExp, removeKnownToBeTrueExp, replaceVarsInExp, simplifyVariableName} +import viper.silicon.utils.ast._ import viper.silicon.verifier.Verifier import viper.silicon.{MList, MMap} import viper.silver.ast +import viper.silver.ast.FalseLit +import viper.silver.dependencyAnalysis.{AssumptionType, StringDependencyAnalysisSourceInfo} import viper.silver.parser.PUnknown import viper.silver.verifier.VerificationError +import scala.annotation.unused import scala.collection.mutable.ListBuffer object moreCompleteExhaleSupporter extends SymbolicExecutionRules { @@ -106,6 +110,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { Implies(And(argumentEqualities, IsPositive(ch.perm)), `?s` === ch.snap) }) + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo("summarize"), AssumptionType.Internal) + val taggedSummarisingSnapshot = summarisingSnapshotDefinitions .collectFirst { @@ -120,8 +126,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { case None => // We have not yet checked for a definite alias val id = ChunkIdentifier(resource, s.program) - val potentialAlias = chunkSupporter.findChunk[NonQuantifiedChunk](relevantChunks, id, args, v) - potentialAlias.filter(c => v.decider.check(IsPositive(c.perm), Verifier.config.checkTimeout())).map(_.snap) + val potentialAlias = chunkSupporter.findChunk[NonQuantifiedChunk](relevantChunks, id, args, v, analysisInfos) + potentialAlias.filter(c => v.decider.check(IsPositive(c.perm), Verifier.config.checkTimeout(), analysisInfos)).map(_.snap) case Some(v) => // We have checked for a definite alias and may or may not have found one. v @@ -163,16 +169,18 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { // query to check if the permission amount we have is sufficient to get the correct counterexample. If we perform // the query in two parts (one part here, one part in our caller to see if the permission amount is sufficient), // the counterexample might be wrong. + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo("summarise"), AssumptionType.Internal) + if (relevantChunks.size == 1 && !Verifier.config.counterexample.isDefined) { val chunk = relevantChunks.head val argsEqual = And(chunk.args.zip(args).map { case (t1, t2) => t1 === t2 }) - if (v.decider.check(argsEqual, Verifier.config.checkTimeout())) { + if (v.decider.check(argsEqual, Verifier.config.checkTimeout(), analysisInfos)) { return Q(s, chunk.snap, chunk.perm, chunk.permExp, v) } } val (s1, taggedSnap, snapDefs, permSum, permSumExp) = summariseOnly(s, relevantChunks, resource, args, argsExp, knownValue, v) - v.decider.assumeDefinition(And(snapDefs), Option.when(withExp)(DebugExp.createInstance("Snapshot", true))) + v.decider.assumeDefinition(And(snapDefs), Option.when(withExp)(DebugExp.createInstance("Snapshot", isInternal_ = true)), analysisInfos.withDependencyType(AssumptionType.Internal)) // v.decider.assume(PermAtMost(permSum, FullPerm())) /* Done in StateConsolidator instead */ val s2 = @@ -195,7 +203,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { args: Seq[Term], argsExp: Option[Seq[ast.Exp]], ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Term, Verifier) => VerificationResult) : VerificationResult = { @@ -203,24 +212,36 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { val relevantChunks = findChunksWithID[NonQuantifiedChunk](h.values, id).toSeq if (relevantChunks.isEmpty) { - if (v.decider.checkSmoke(true)) { - if (s.isInPackage) { + if (v.decider.checkSmoke(analysisInfos, isAssert = true)) { + if (s.isInPackage || Verifier.config.analyzeInfeasiblePaths) { val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) Q(s, snap, v) } else { Success() // TODO: Mark branch as dead? } } else { - createFailure(ve, v, s, False, "branch is dead") + val failure = createFailure(ve, v, s, False, "branch is dead") + if (s.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors() && Verifier.config.analyzeInfeasiblePaths) { + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) + failure combine Q(s, snap, v) + } else { + failure + } } } else { - summarise(s, relevantChunks, resource, args, argsExp, None, v)((s1, snap, permSum, permSumExp, v1) => - v.decider.assert(IsPositive(permSum)) { + summarise(s, relevantChunks, resource, args, argsExp, None, v)((s1, snap, permSum, permSumExp, v1) => { + val termToAssert = IsPositive(permSum) + val debugExp = permSumExp.map(IsPositive(_)()) + v.decider.assert(termToAssert, analysisInfos) { case true => Q(s1, snap, v1) case false => - createFailure(ve, v, s1, IsPositive(permSum), permSumExp.map(IsPositive(_)())) - }) + val failure = createFailure(ve, v, s1, termToAssert, debugExp) + if (s1.retryLevel == 0) v1.decider.handleFailedAssertion(termToAssert, debugExp, debugExp, analysisInfos, v1.reportFurtherErrors()) + if (s1.retryLevel == 0 && v1.reportFurtherErrors()) failure combine Q(s1, snap, v1) else failure + } + }) } } @@ -233,14 +254,15 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { permsExp: Option[ast.Exp], returnSnap: Boolean, ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { if (!s.assertReadAccessOnly) - actualConsumeComplete(s, h, resource, args, argsExp, perms, permsExp, returnSnap, ve, v)(Q) + actualConsumeComplete(s, h, resource, args, argsExp, perms, permsExp, returnSnap, ve, v, analysisInfos)(Q) else - summariseHeapAndAssertReadAccess(s, h, resource, perms, args, argsExp, returnSnap, ve, v)(Q) + summariseHeapAndAssertReadAccess(s, h, resource, perms, args, argsExp, returnSnap, ve, v, analysisInfos)(Q) } private def summariseHeapAndAssertReadAccess(s: State, @@ -251,7 +273,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { argsExp: Option[Seq[ast.Exp]], returnSnap: Boolean, ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -259,20 +282,29 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { val relevantChunks = findChunksWithID[NonQuantifiedChunk](h.values, id).toSeq if (returnSnap) { - summarise(s, relevantChunks, resource, args, argsExp, None, v)((s1, snap, permSum, permSumExp, v1) => - v.decider.assert(Implies(IsPositive(perm), IsPositive(permSum))) { + summarise(s, relevantChunks, resource, args, argsExp, None, v)((s1, snap, permSum, permSumExp, v1) => { + val termToAssert = Implies(IsPositive(perm), IsPositive(permSum)) + val debugExp = permSumExp.map(IsPositive(_)()) + v.decider.assert(termToAssert, analysisInfos) { case true => Q(s1, h, Some(snap), v1) case false => - createFailure(ve, v, s1, IsPositive(permSum), permSumExp.map(IsPositive(_)())) - }) + val failure = createFailure(ve, v, s1, IsPositive(permSum), debugExp) + if (s1.retryLevel == 0) v1.decider.handleFailedAssertion(termToAssert, debugExp, debugExp, analysisInfos, v1.reportFurtherErrors()) + if (s1.retryLevel == 0 && v1.reportFurtherErrors()) failure combine Q(s1, h, Some(snap), v1) else failure + } + }) } else { val (s1, permSum, permSumExp) = permSummariseOnly(s, relevantChunks, resource, args, argsExp) - v.decider.assert(Implies(IsPositive(perm), IsPositive(permSum))) { + val termToAssert = Implies(IsPositive(perm), IsPositive(permSum)) + val debugExp = permSumExp.map(IsPositive(_)()) + v.decider.assert(termToAssert, analysisInfos) { case true => Q(s1, h, None, v) case false => - createFailure(ve, v, s1, IsPositive(permSum), permSumExp.map(IsPositive(_)())) + val failure = createFailure(ve, v, s1, IsPositive(permSum), debugExp) + if (s1.retryLevel == 0) v.decider.handleFailedAssertion(termToAssert, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + if (s1.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s1, h, None, v) else failure } } } @@ -286,7 +318,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { permsExp: Option[ast.Exp], returnSnap: Boolean, ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -300,13 +333,17 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { if (relevantChunks.isEmpty) { // if no permission is exhaled, return none - v.decider.assert(perms === NoPerm) { + v.decider.assert(perms === NoPerm, analysisInfos) { case true => Q(s, h, None, v) - case false => createFailure(ve, v, s, perms === NoPerm, permsExp.map(pe => ast.EqCmp(pe, ast.NoPerm()())(pe.pos, pe.info, pe.errT))) + case false => + val debugExp = permsExp.map(pe => ast.EqCmp(pe, ast.NoPerm()())(pe.pos, pe.info, pe.errT)) + val failure = createFailure(ve, v, s, perms === NoPerm, debugExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(perms === NoPerm, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, h, None, v) else failure } } else { if (!terms.utils.consumeExactRead(perms, s.constrainableARPs)) { - actualConsumeCompleteConstrainable(s, relevantChunks, resource, args, argsExp, perms, permsExp, returnSnap, ve, v)((s1, updatedChunks, optSnap, v2) => { + actualConsumeCompleteConstrainable(s, relevantChunks, resource, args, argsExp, perms, permsExp, returnSnap, ve, v, analysisInfos)((s1, updatedChunks, optSnap, v2) => { Q(s1, Heap(updatedChunks ++ otherChunks), optSnap, v2) }) } else { @@ -317,8 +354,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { val newChunks = ListBuffer[NonQuantifiedChunk]() var moreNeeded = true - val definiteAlias = chunkSupporter.findChunk[NonQuantifiedChunk](relevantChunks, id, args, v).filter(c => - v.decider.check(IsPositive(c.perm), Verifier.config.checkTimeout()) + val definiteAlias = chunkSupporter.findChunk[NonQuantifiedChunk](relevantChunks, id, args, v, analysisInfos).filter(c => + v.decider.check(IsPositive(c.perm), Verifier.config.checkTimeout(), analysisInfos) ) val sortFunction: (NonQuantifiedChunk, NonQuantifiedChunk) => Boolean = (ch1, ch2) => { @@ -359,15 +396,16 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { pSum = PermPlus(pSum, Ite(eq, ch.perm, NoPerm)) pSumExp = eqExp.map(eq => ast.PermAdd(pSumExp.get, ast.CondExp(eq, ch.permExp.get, ast.NoPerm()())(eq.pos, eq.info, eq.errT))()) - val newChunk = ch.withPerm(PermMinus(ch.perm, pTaken), permsExp.map(pe => ast.PermSub(ch.permExp.get, pTakenExp.get)(pe.pos, pe.info, pe.errT))) + val newChunk = v.chunkFactory.withPerm(ch, PermMinus(ch.perm, pTaken), permsExp.map(pe => ast.PermSub(ch.permExp.get, pTakenExp.get)(pe.pos, pe.info, pe.errT)), analysisInfos).asInstanceOf[NonQuantifiedChunk] + val _ = v.chunkFactory.withPerm(ch, pTaken, None, analysisInfos, isExhale=true) pNeeded = PermMinus(pNeeded, pTaken) pNeededExp = permsExp.map(pe => ast.PermSub(pNeededExp.get, pTakenExp.get)(pe.pos, pe.info, pe.errT)) - if (!v.decider.check(IsNonPositive(newChunk.perm), Verifier.config.splitTimeout())) { + if (!v.decider.check(IsNonPositive(newChunk.perm), Verifier.config.splitTimeout(), analysisInfos.withDependencyType(AssumptionType.Internal))) { newChunks.append(newChunk) } - moreNeeded = !v.decider.check(pNeeded === NoPerm, Verifier.config.splitTimeout()) + moreNeeded = !v.decider.check(pNeeded === NoPerm, Verifier.config.splitTimeout(), analysisInfos) } else { newChunks.append(ch) } @@ -380,7 +418,7 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { newChunks foreach { ch => val resource = Resources.resourceDescriptions(ch.resourceID) val pathCond = interpreter.buildPathConditionsForChunk(ch, resource.instanceProperties(s.mayAssumeUpperBounds)) - pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)))) + pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)), analysisInfos)) } val newHeap = Heap(allChunks) @@ -390,7 +428,7 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { if (returnSnap) { summarise(s0, relevantChunks.toSeq, resource, args, argsExp, Some(definiteAlias.map(_.snap)), v)((s1, snap, _, _, v1) => { - val condSnap = Some(if (v1.decider.check(IsPositive(perms), Verifier.config.checkTimeout())) { + val condSnap = Some(if (v1.decider.check(IsPositive(perms), Verifier.config.checkTimeout(), analysisInfos.withDependencyType(AssumptionType.Internal))) { snap } else { Ite(IsPositive(perms), snap.convert(sorts.Snap), Unit) @@ -398,11 +436,14 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { if (!moreNeeded) { Q(s1, newHeap, condSnap, v1) } else { - v1.decider.assert(pNeeded === NoPerm) { + v1.decider.assert(pNeeded === NoPerm, analysisInfos) { case true => Q(s1, newHeap, condSnap, v1) case false => - createFailure(ve, v1, s1, pNeeded === NoPerm, pNeededExp.map(pn => ast.EqCmp(pn, ast.NoPerm()())(pn.pos, pn.info, pn.errT))) + val debugExp = pNeededExp.map(pn => ast.EqCmp(pn, ast.NoPerm()())(pn.pos, pn.info, pn.errT)) + val failure = createFailure(ve, v1, s1, pNeeded === NoPerm, debugExp) + if (s1.retryLevel == 0) v1.decider.handleFailedAssertion(pNeeded === NoPerm, debugExp, debugExp, analysisInfos, v1.reportFurtherErrors()) + if (s1.retryLevel == 0 && v1.reportFurtherErrors()) failure combine Q(s1, newHeap, condSnap, v1) else failure } } }) @@ -410,11 +451,14 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { if (!moreNeeded) { Q(s0, newHeap, None, v) } else { - v.decider.assert(pNeeded === NoPerm) { + v.decider.assert(pNeeded === NoPerm, analysisInfos) { case true => Q(s0, newHeap, None, v) case false => - createFailure(ve, v, s0, pNeeded === NoPerm, pNeededExp.map(pn => ast.EqCmp(pn, ast.NoPerm()())(pn.pos, pn.info, pn.errT))) + val debugExp = pNeededExp.map(pn => ast.EqCmp(pn, ast.NoPerm()())(pn.pos, pn.info, pn.errT)) + val failure = createFailure(ve, v, s0, pNeeded === NoPerm, debugExp) + if (s0.retryLevel == 0) v.decider.handleFailedAssertion(pNeeded === NoPerm, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + if (s0.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s0, newHeap, None, v) else failure } } } @@ -431,7 +475,8 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { permsExp: Option[ast.Exp], returnSnap: Boolean, ve: VerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, ListBuffer[NonQuantifiedChunk], Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -444,7 +489,7 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { var newFr = s.functionRecorder - val updatedChunks = + val updatedChunks: ListBuffer[NonQuantifiedChunk] = relevantChunks map (ch => { val eqCmps = ch.args.zip(args).map { case (t1, t2) => t1 === t2 } val eq = And(eqCmps) @@ -468,11 +513,13 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { ast.Implies(ast.Not(eqExp.get)(), ast.EqCmp(permTakenExp.get, ast.NoPerm()())())(pe.pos, pe.info, pe.errT)))) - v.decider.assume(constraint, Option.when(withExp)(DebugExp.createInstance(constraintExp, constraintExp))) + v.decider.assume(constraint, Option.when(withExp)(DebugExp.createInstance(constraintExp, constraintExp)), analysisInfos) newFr = newFr.recordPathSymbol(permTaken.applicable.asInstanceOf[Function]).recordConstraint(constraint) - ch.withPerm(PermMinus(ch.perm, permTaken), permsExp.map(pe => ast.PermSub(ch.permExp.get, permTakenExp.get)(pe.pos, pe.info, pe.errT))) + @unused // required in order to ensure a sound dependency analysis + val _ = v.chunkFactory.withPermNonQuantifiedChunk(ch, permTaken, None, analysisInfos, isExhale=true) + v.chunkFactory.withPermNonQuantifiedChunk(ch, PermMinus(ch.perm, permTaken), permsExp.map(pe => ast.PermSub(ch.permExp.get, permTakenExp.get)(pe.pos, pe.info, pe.errT)), analysisInfos) }) val totalTakenBounds = @@ -484,16 +531,18 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { val constraintExp = permsExp.map(pe => ast.Implies(ast.NeCmp(totalPermSumExp.get, ast.NoPerm()())(), ast.And(ast.PermLeCmp(ast.NoPerm()(), totalPermTakenExp.get)(), ast.PermLeCmp(totalPermTakenExp.get, totalPermSumExp.get)())(pe.pos, pe.info, pe.errT))()) - v.decider.assume(totalTakenBounds, constraintExp, constraintExp) + v.decider.assume(totalTakenBounds, constraintExp, constraintExp, analysisInfos) newFr = newFr.recordConstraint(totalTakenBounds) val s1 = s.copy(functionRecorder = newFr) - v.decider.assert(Implies(PermLess(NoPerm, perms), totalPermTaken !== NoPerm)) { + val termToAssert = Implies(PermLess(NoPerm, perms), totalPermTaken !== NoPerm) + val debugExp = totalPermTakenExp.map(tpt => ast.NeCmp(tpt, ast.NoPerm()())()) + v.decider.assert(termToAssert, analysisInfos) { case true => val constraintExp = permsExp.map(pe => ast.EqCmp(pe, totalPermTakenExp.get)()) - v.decider.assume(perms === totalPermTaken, Option.when(withExp)(DebugExp.createInstance(constraintExp, constraintExp))) + v.decider.assume(perms === totalPermTaken, Option.when(withExp)(DebugExp.createInstance(constraintExp, constraintExp)), analysisInfos) if (returnSnap) { summarise(s1, relevantChunks.toSeq, resource, args, argsExp, None, v)((s2, snap, _, _, v1) => Q(s2, updatedChunks, Some(snap), v1)) @@ -502,14 +551,21 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { } case false => v.decider.finishDebugSubExp(s"consume permissions for ${resource.toString()}") - createFailure(ve, v, s, totalPermTaken !== NoPerm, totalPermTakenExp.map(tpt => ast.NeCmp(tpt, ast.NoPerm()())())) + val failure = createFailure(ve, v, s, totalPermTaken !== NoPerm, debugExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(termToAssert, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) { + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) + failure combine Q(s1, updatedChunks, if (returnSnap) Some(snap) else None, v) + } else { + failure + } } } private val freeReceiver = Var(Identifier("?rcvr"), sorts.Ref, false) private val freeReceiverExp = ast.LocalVar("?rcvr", ast.Ref)() - def assumeFieldPermissionUpperBounds(h: Heap, v: Verifier): Unit = { + def assumeFieldPermissionUpperBounds(h: Heap, v: Verifier, analysisInfos: DependencyAnalysisInfos): Unit = { // TODO: Instead of "manually" assuming such upper bounds, appropriate PropertyInterpreters // should be used, see StateConsolidator val relevantChunksPerField = MMap.empty[String, MList[BasicChunk]] @@ -537,7 +593,7 @@ object moreCompleteExhaleSupporter extends SymbolicExecutionRules { relevantChunks foreach (chunk => { val instantiatedPermSum = permissionSum.replace(freeReceiver, chunk.args.head) val exp = permissionSumExp.map(pse => ast.PermLeCmp(replaceVarsInExp(pse, Seq(freeReceiverExp.name), Seq(chunk.argsExp.get.head)), ast.FullPerm()())()) - v.decider.assume(PermAtMost(instantiatedPermSum, FullPerm), exp, exp) + v.decider.assume(PermAtMost(instantiatedPermSum, FullPerm), exp, exp, analysisInfos.withDependencyType(AssumptionType.Internal)) }) } } diff --git a/src/main/scala/rules/PermissionSupporter.scala b/src/main/scala/rules/PermissionSupporter.scala index 58c386fe9..a39f3a796 100644 --- a/src/main/scala/rules/PermissionSupporter.scala +++ b/src/main/scala/rules/PermissionSupporter.scala @@ -6,16 +6,17 @@ package viper.silicon.rules -import viper.silver.ast -import viper.silver.verifier.PartialVerificationError +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.VerificationResult import viper.silicon.state.State import viper.silicon.state.terms.{Term, Var, perms} import viper.silicon.verifier.Verifier +import viper.silver.ast +import viper.silver.verifier.PartialVerificationError import viper.silver.verifier.reasons.{NegativePermission, NonPositivePermission} object permissionSupporter extends SymbolicExecutionRules { - def assertNotNegative(s: State, tPerm: Term, ePerm: ast.Exp, ePermNew: Option[ast.Exp], pve: PartialVerificationError, v: Verifier) + def assertNotNegative(s: State, tPerm: Term, ePerm: ast.Exp, ePermNew: Option[ast.Exp], pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -23,16 +24,19 @@ object permissionSupporter extends SymbolicExecutionRules { case k: Var if s.constrainableARPs.contains(k) => Q(s, v) case _ => - v.decider.assert(perms.IsNonNegative(tPerm)) { + val termToAssert = perms.IsNonNegative(tPerm) + val debugExp = ePermNew.map(ep => perms.IsNonNegative(ep)(ep.pos, ep.info, ep.errT)) + v.decider.assert(termToAssert, analysisInfos) { case true => Q(s, v) case false => - val assertExp = ePermNew.map(ep => perms.IsNonNegative(ep)(ep.pos, ep.info, ep.errT)) - createFailure(pve dueTo NegativePermission(ePerm), v, s, perms.IsNonNegative(tPerm), assertExp) + val failure = createFailure(pve dueTo NegativePermission(ePerm), v, s, termToAssert, debugExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(termToAssert, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, v) else failure } } } - def assertPositive(s: State, tPerm: Term, ePerm: ast.Exp, pve: PartialVerificationError, v: Verifier) + def assertPositive(s: State, tPerm: Term, ePerm: ast.Exp, pve: PartialVerificationError, v: Verifier, analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -40,9 +44,14 @@ object permissionSupporter extends SymbolicExecutionRules { case k: Var if s.constrainableARPs.contains(k) => Q(s, v) case _ => - v.decider.assert(perms.IsPositive(tPerm)) { + val termToAssert = perms.IsPositive(tPerm) + val debugExp = Option.when(withExp)(perms.IsPositive(ePerm)()) + v.decider.assert(termToAssert, analysisInfos) { case true => Q(s, v) - case false => createFailure(pve dueTo NonPositivePermission(ePerm), v, s, perms.IsPositive(tPerm), Option.when(withExp)(perms.IsPositive(ePerm)())) + case false => + val failure = createFailure(pve dueTo NonPositivePermission(ePerm), v, s, termToAssert, debugExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(termToAssert, debugExp, debugExp, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, v) else failure } } } diff --git a/src/main/scala/rules/PredicateSupporter.scala b/src/main/scala/rules/PredicateSupporter.scala index 65ff902e9..5e82341ab 100644 --- a/src/main/scala/rules/PredicateSupporter.scala +++ b/src/main/scala/rules/PredicateSupporter.scala @@ -8,18 +8,20 @@ package viper.silicon.rules import viper.silicon import viper.silicon.Config.JoinMode -import viper.silicon.debugger.DebugExp import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.interfaces.VerificationResult -import viper.silicon.interfaces.state.{ChunkIdentifer, GeneralChunk, NonQuantifiedChunk} import viper.silicon.resources.FieldID import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.state.terms.predef.`?r` -import viper.silicon.supporters.{PredicateBranchNode, PredicateLeafNode, PredicateContentsTree} +import viper.silicon.supporters.{PredicateBranchNode, PredicateContentsTree, PredicateLeafNode} import viper.silicon.utils.toSf import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.dependencyAnalysis.AssumptionType import viper.silver.verifier.PartialVerificationError trait PredicateSupportRules extends SymbolicExecutionRules { @@ -31,7 +33,8 @@ trait PredicateSupportRules extends SymbolicExecutionRules { ePerm: Option[ast.Exp], constrainableWildcards: InsertionOrderedSet[Var], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult @@ -44,7 +47,8 @@ trait PredicateSupportRules extends SymbolicExecutionRules { constrainableWildcards: InsertionOrderedSet[Var], pve: PartialVerificationError, v: Verifier, - pa: ast.PredicateAccess) + pa: ast.PredicateAccess, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult } @@ -61,7 +65,8 @@ object predicateSupporter extends PredicateSupportRules { ePerm: Option[ast.Exp], constrainableWildcards: InsertionOrderedSet[Var], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -75,33 +80,33 @@ object predicateSupporter extends PredicateSupportRules { val s1 = s.copy(g = gIns, smDomainNeeded = true) .scalePermissionFactor(tPerm, ePerm) - consume(s1, body, true, pve, v)((s1a, snap, v1) => { + consume(s1, body, true, pve, v, analysisInfos)((s1a, snap, v1) => { if (!Verifier.config.disableFunctionUnfoldTrigger()) { val predTrigger = App(s1a.predicateData(predicate.name).triggerFunction, snap.get.convert(terms.sorts.Snap) +: tArgs) val eArgsString = eArgs.mkString(", ") - v1.decider.assume(predTrigger, Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eArgsString))"))) + v1.decider.assume(predTrigger, Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eArgsString))")), analysisInfos) } val s2 = s1a.copy(g = s.g, smDomainNeeded = s.smDomainNeeded, permissionScalingFactor = s.permissionScalingFactor, permissionScalingFactorExp = s.permissionScalingFactorExp).setConstrainable(constrainableWildcards, false) - v1.heapSupporter.produceSingle(s2, predicate, tArgs, eArgs, snap.get.convert(s2.predicateSnapMap(predicate.name)), None, tPerm, ePerm, pve, true, v1)((s3, v3) => { - val s4 = v3.heapSupporter.triggerResourceIfNeeded(s3, pa, tArgs, eArgs, v3) + v1.heapSupporter.produceSingle(s2, predicate, tArgs, eArgs, snap.get.convert(s2.predicateSnapMap(predicate.name)), None, tPerm, ePerm, pve, true, v1, analysisInfos)((s3, v3) => { + val s4 = v3.heapSupporter.triggerResourceIfNeeded(s3, pa, tArgs, eArgs, v3, analysisInfos) Q(s4, v3) }) }) } - def producePredicateContents(s: State, tree: PredicateContentsTree, toReplace: silicon.Map[Term, Term], v: Verifier, isUnfolding: Boolean = false) + def producePredicateContents(s: State, tree: PredicateContentsTree, toReplace: silicon.Map[Term, Term], v: Verifier, analysisInfos: DependencyAnalysisInfos, isUnfolding: Boolean = false) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { tree match { case PredicateLeafNode(h, assumptions) => val debugExp = Option.when(withExp)(DebugExp.createInstance("Assumption from unfolded predicate body")) - v.decider.assume(assumptions.map(a => (a.replace(toReplace), debugExp)).toSeq) - val substChunks = h.values.map(_.substitute(toReplace).asInstanceOf[GeneralChunk].permScale(s.permissionScalingFactor, s.permissionScalingFactorExp)) + assumptions.foreach(a => v.decider.assume(a.replace(toReplace), debugExp, analysisInfos)) + val substChunks = h.values.map(chunk => v.chunkFactory.permScale(v.chunkFactory.substitute(chunk.asInstanceOf[GeneralChunk], toReplace, analysisInfos), s.permissionScalingFactor, s.permissionScalingFactorExp, analysisInfos)) val quantifiedResourceIdentifiers: Set[ChunkIdentifer] = s.qpPredicates.map(p => BasicChunkIdentifier(p.name)) ++ s.qpFields.map(f => BasicChunkIdentifier(f.name)) ++ s.qpMagicWands @@ -115,28 +120,28 @@ object predicateSupporter extends PredicateSupportRules { case _ => s.program.findPredicate(bc.id.name) } val (sm, smValueDef) = quantifiedChunkSupporter.singletonSnapshotMap(s, resource, bc.args, bc.snap, v) - v.decider.assumeDefinition(smValueDef, None) + v.decider.assumeDefinition(smValueDef, None, analysisInfos) val codQvars = bc.resourceID match { case FieldID => Seq(`?r`) case _ => s.predicateFormalVarMap(resource.asInstanceOf[ast.Predicate].name) } newFr = newFr.recordFvfAndDomain(SnapshotMapDefinition(resource, sm, Seq(smValueDef), Seq())) - quantifiedChunkSupporter.createSingletonQuantifiedChunk(codQvars, None, resource, bc.args, None, bc.perm, None, sm, s.program) + quantifiedChunkSupporter.createSingletonQuantifiedChunk(codQvars, None, resource, bc.args, None, bc.perm, None, sm, s.program, v, analysisInfos, isExhale=false) case mwc: MagicWandChunk => val wand = mwc.id.ghostFreeWand val bodyVars = wand.subexpressionsToEvaluate(s.program) val codQvars = bodyVars.indices.toList.map(i => Var(Identifier(s"x$i"), v.symbolConverter.toSort(bodyVars(i).typ), false)) val (sm, smValueDef) = quantifiedChunkSupporter.singletonSnapshotMap(s, wand, mwc.args, mwc.snap, v) - v.decider.assumeDefinition(smValueDef, None) + v.decider.assumeDefinition(smValueDef, None, analysisInfos) newFr = newFr.recordFvfAndDomain(SnapshotMapDefinition(wand, sm, Seq(smValueDef), Seq())) - quantifiedChunkSupporter.createSingletonQuantifiedChunk(codQvars, None, wand, mwc.args, None, mwc.perm, None, sm, s.program) + quantifiedChunkSupporter.createSingletonQuantifiedChunk(codQvars, None, wand, mwc.args, None, mwc.perm, None, sm, s.program, v, analysisInfos, isExhale=false) } } else { c } }) val substHeap = Heap(substChunksOptQps) - val (fr1, h1) = v.stateConsolidator(s).merge(newFr, s, s.h, substHeap, v) + val (fr1, h1) = v.stateConsolidator(s).merge(newFr, s, s.h, substHeap, v, analysisInfos) val s1 = s.copy(h = h1, functionRecorder = fr1) Q(s1, v) @@ -144,13 +149,13 @@ object predicateSupporter extends PredicateSupportRules { val substCond = cond.replace(toReplace) if (!isUnfolding && s.moreJoins.id >= JoinMode.Impure.id) { - joiner.join[scala.Null, scala.Null](s, v, resetState = false)((s1, v1, QB) => { - brancher.branch(s1, substCond, condExp, v1)( + joiner.join[scala.Null, scala.Null](s, v, analysisInfos, resetState = false)((s1, v1, QB) => { + brancher.branch(s1, substCond, condExp, v1, analysisInfos)( (s2, v2) => { - producePredicateContents(s2, left, toReplace, v2, isUnfolding)((s3, v3) => QB(s3, null, v3)) + producePredicateContents(s2, left, toReplace, v2, analysisInfos, isUnfolding)((s3, v3) => QB(s3, null, v3)) }, (s2, v2) => { - producePredicateContents(s2, right, toReplace, v2, isUnfolding)((s3, v3) => QB(s3, null, v3)) + producePredicateContents(s2, right, toReplace, v2, analysisInfos, isUnfolding)((s3, v3) => QB(s3, null, v3)) } ) }) (entries => { @@ -165,12 +170,12 @@ object predicateSupporter extends PredicateSupportRules { (s2, null) }) ((sp, _, vp) => Q(sp, vp)) } else { - brancher.branch(s, substCond, condExp, v)( + brancher.branch(s, substCond, condExp, v, analysisInfos)( (s1, v1) => { - producePredicateContents(s1, left, toReplace, v1, isUnfolding)(Q) + producePredicateContents(s1, left, toReplace, v1, analysisInfos, isUnfolding)(Q) }, (s2, v2) => { - producePredicateContents(s2, right, toReplace, v2, isUnfolding)(Q) + producePredicateContents(s2, right, toReplace, v2, analysisInfos, isUnfolding)(Q) } ) } @@ -186,7 +191,8 @@ object predicateSupporter extends PredicateSupportRules { constrainableWildcards: InsertionOrderedSet[Var], pve: PartialVerificationError, v: Verifier, - pa: ast.PredicateAccess) + pa: ast.PredicateAccess, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -198,19 +204,19 @@ object predicateSupporter extends PredicateSupportRules { val body = predicate.body.get /* Only non-abstract predicates can be unfolded */ val s1 = s.scalePermissionFactor(tPerm, ePerm) - v.heapSupporter.consumeSingle(s1, s1.h, pa, tArgs, eArgs, tPerm, ePerm, true, pve, v)((s2, h2, snap, v1) => { + v.heapSupporter.consumeSingle(s1, s1.h, pa, tArgs, eArgs, tPerm, ePerm, true, pve, v, analysisInfos)((s2, h2, snap, v1) => { val s3 = s2.copy(g = gIns, h = h2) .setConstrainable(constrainableWildcards, false) if (s3.predicateData(predicate.name).predContents.isDefined) { val toReplace: silicon.Map[Term, Term] = silicon.Map.from(s3.predicateData(predicate.name).params.get.zip(Seq(snap.get) ++ tArgs)) - producePredicateContents(s3, s3.predicateData(predicate.name).predContents.get, toReplace, v1, false)((s4, v4) => { + producePredicateContents(s3, s3.predicateData(predicate.name).predContents.get, toReplace, v1, analysisInfos, false)((s4, v4) => { v4.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterUnfold) if (!Verifier.config.disableFunctionUnfoldTrigger()) { val predicateTrigger = App(s4.predicateData(predicate.name).triggerFunction, snap.get.convert(terms.sorts.Snap) +: tArgs) val eargs = eArgs.mkString(", ") - v4.decider.assume(predicateTrigger, Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eargs))"))) + v4.decider.assume(predicateTrigger, Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eargs))")), analysisInfos.withDependencyType(AssumptionType.Trigger)) } Q(s4.copy(g = s.g, permissionScalingFactor = s.permissionScalingFactor, @@ -218,14 +224,14 @@ object predicateSupporter extends PredicateSupportRules { v4) }) } else { - produce(s3, toSf(snap.get), body, pve, v1)((s4, v2) => { + produce(s3, toSf(snap.get), body, pve, v1, analysisInfos)((s4, v2) => { v2.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterUnfold) if (!Verifier.config.disableFunctionUnfoldTrigger()) { val predicateTrigger = App(s4.predicateData(predicate.name).triggerFunction, snap.get.convert(terms.sorts.Snap) +: tArgs) val eargs = eArgs.mkString(", ") - v2.decider.assume(predicateTrigger, Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eargs))"))) + v2.decider.assume(predicateTrigger, Option.when(withExp)(DebugExp.createInstance(s"PredicateTrigger(${predicate.name}($eargs))")), analysisInfos.withDependencyType(AssumptionType.Trigger)) } Q(s4.copy(g = s.g, permissionScalingFactor = s.permissionScalingFactor, diff --git a/src/main/scala/rules/Producer.scala b/src/main/scala/rules/Producer.scala index 2c6f072a9..d3a0154d3 100644 --- a/src/main/scala/rules/Producer.scala +++ b/src/main/scala/rules/Producer.scala @@ -6,20 +6,23 @@ package viper.silicon.rules -import viper.silicon.debugger.DebugExp import viper.silicon.Config.JoinMode - -import scala.collection.mutable -import viper.silver.ast -import viper.silver.ast.utility.QuantifiedPermissions.QuantifiedPermissionAssertion -import viper.silver.verifier.PartialVerificationError +import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis.{DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces.{Unreachable, VerificationResult} import viper.silicon.logger.records.data.{CondExpRecord, ImpliesRecord, ProduceRecord} import viper.silicon.state._ +import viper.silicon.state.chunks.MagicWandIdentifier import viper.silicon.state.terms._ import viper.silicon.verifier.Verifier +import viper.silver.ast +import viper.silver.ast.utility.Expressions +import viper.silver.ast.utility.QuantifiedPermissions.QuantifiedPermissionAssertion +import viper.silver.verifier.PartialVerificationError import viper.silver.verifier.reasons.{NegativePermission, QPAssertionNotInjective} +import scala.collection.mutable + trait ProductionRules extends SymbolicExecutionRules { /** Produce assertion `a` into state `s`. @@ -29,6 +32,7 @@ trait ProductionRules extends SymbolicExecutionRules { * @param a The assertion to produce. * @param pve The error to report in case the production fails. * @param v The verifier to use. + * @param assumptionType The assumption type used for analysis purposes * @param Q The continuation to invoke if the production succeeded, with the state and * the verifier resulting from the production as arguments. * @return The result of the continuation. @@ -37,7 +41,8 @@ trait ProductionRules extends SymbolicExecutionRules { sf: (Sort, Verifier) => Term, a: ast.Exp, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult @@ -53,6 +58,7 @@ trait ProductionRules extends SymbolicExecutionRules { * @param pvef The error to report in case the production fails. Given assertions `as`, an error * `pvef(as_i)` will be reported if producing assertion `as_i` fails. * @param v @see [[produce]] + * @param assumptionType @see [[produce]] * @param Q @see [[produce]] * @return @see [[produce]] */ @@ -60,7 +66,8 @@ trait ProductionRules extends SymbolicExecutionRules { sf: (Sort, Verifier) => Term, as: Seq[ast.Exp], pvef: ast.Exp => PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult } @@ -99,18 +106,20 @@ object producer extends ProductionRules { sf: (Sort, Verifier) => Term, a: ast.Exp, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = - produceR(s, sf, a.whenInhaling, pve, v)(Q) + produceR(s, sf, a.whenInhaling, pve, v, analysisInfos)(Q) /** @inheritdoc */ def produces(s: State, sf: (Sort, Verifier) => Term, as: Seq[ast.Exp], pvef: ast.Exp => PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -125,14 +134,15 @@ object producer extends ProductionRules { allPves ++= pves }) - produceTlcs(s, sf, allTlcs.result(), allPves.result(), v)(Q) + produceTlcs(s, sf, allTlcs.result(), allPves.result(), v, analysisInfos)(Q) } private def produceTlcs(s: State, sf: (Sort, Verifier) => Term, as: Seq[ast.Exp], pves: Seq[PartialVerificationError], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -142,26 +152,35 @@ object producer extends ProductionRules { val a = as.head.whenInhaling val pve = pves.head + val analysisInfos1 = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, analysisInfos, a.info, a) + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(!Expressions.isKnownWellDefined(a, Some(s.program)), hasAssumptions=true, analysisInfos1) + return Q(s, v) + } + if (as.tail.isEmpty) - wrappedProduceTlc(s, sf, a, pve, v)(Q) + wrappedProduceTlc(s, sf, a, pve, v, analysisInfos1)((s1, v1) => { + Q(s1, v1) + }) else { try { val (sf0, sf1) = - v.snapshotSupporter.createSnapshotPair(s, sf, a, viper.silicon.utils.ast.BigAnd(as.tail), v) + v.snapshotSupporter.createSnapshotPair(s, sf, a, viper.silicon.utils.ast.BigAnd(as.tail), v, analysisInfos1) /* TODO: Refactor createSnapshotPair s.t. it can be used with Seq[Exp], * then remove use of BigAnd; for one it is not efficient since * the tail of the (decreasing list parameter as) is BigAnd-ed * over and over again. */ - wrappedProduceTlc(s, sf0, a, pve, v)((s1, v1) => - produceTlcs(s1, sf1, as.tail, pves.tail, v1)(Q)) + wrappedProduceTlc(s, sf0, a, pve, v, analysisInfos1)((s1, v1) => { + produceTlcs(s1, sf1, as.tail, pves.tail, v1, analysisInfos)(Q) + }) } catch { // We will get an IllegalArgumentException from createSnapshotPair if sf(...) returns Unit. // This should never happen if we're in a reachable state, so here we check for that // (without timeout, since there is no fallback) and stop verifying the current branch. - case _: IllegalArgumentException if v.decider.check(False, Verifier.config.assertTimeout.getOrElse(0)) => - Unreachable() + case _: IllegalArgumentException if v.decider.checkSmoke(analysisInfos) => + if (Verifier.config.analyzeInfeasiblePaths) Q(s,v) else Unreachable() } } @@ -172,14 +191,15 @@ object producer extends ProductionRules { sf: (Sort, Verifier) => Term, a: ast.Exp, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { val tlcs = a.topLevelConjuncts val pves = Seq.fill(tlcs.length)(pve) - produceTlcs(s, sf, tlcs, pves, v)(Q) + produceTlcs(s, sf, tlcs, pves, v, analysisInfos)(Q) } /** Wrapper/decorator for consume that injects the following operations: @@ -189,12 +209,18 @@ object producer extends ProductionRules { sf: (Sort, Verifier) => Term, a: ast.Exp, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { + if (v.decider.isPathMarkedInfeasible) { + v.decider.handleInfeasiblePath(!Expressions.isKnownWellDefined(a, Some(s.program)), hasAssumptions=true, analysisInfos) + return Q(s, v) + } + val sepIdentifier = v.symbExLog.openScope(new ProduceRecord(a, s, v.decider.pcs)) - produceTlc(s, sf, a, pve, v)((s1, v1) => { + produceTlc(s, sf, a, pve, v, analysisInfos)((s1, v1) => { v1.symbExLog.closeScope(sepIdentifier) Q(s1, v1)}) } @@ -203,10 +229,10 @@ object producer extends ProductionRules { sf: (Sort, Verifier) => Term, a: ast.Exp, pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (continuation: (State, Verifier) => VerificationResult) : VerificationResult = { - v.logger.debug(s"\nPRODUCE ${viper.silicon.utils.ast.sourceLineColumn(a)}: $a") v.logger.debug(v.stateFormatter.format(s, v.decider.pcs)) @@ -218,16 +244,16 @@ object producer extends ProductionRules { val impliesRecord = new ImpliesRecord(imp, s, v.decider.pcs, "produce") val uidImplies = v.symbExLog.openScope(impliesRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => // The type arguments here are Null because there is no need to pass any join data. - joiner.join[scala.Null, scala.Null](s1, v1, resetState = false)((s1, v1, QB) => - branch(s1.copy(parallelizeBranches = false), t0, (e0, e0New), v1)( - (s2, v2) => produceR(s2.copy(parallelizeBranches = s1.parallelizeBranches), sf, a0, pve, v2)((s3, v3) => { + joiner.join[scala.Null, scala.Null](s1, v1, analysisInfos, resetState = false)((s1, v1, QB) => + branch(s1.copy(parallelizeBranches = false), t0, (e0, e0New), v1, analysisInfos)( + (s2, v2) => produceR(s2.copy(parallelizeBranches = s1.parallelizeBranches), sf, a0, pve, v2, analysisInfos)((s3, v3) => { v3.symbExLog.closeScope(uidImplies) QB(s3, null, v3) }), (s2, v2) => { - v2.decider.assume(sf(sorts.Snap, v2) === Unit, Option.when(withExp)(DebugExp.createInstance("Empty snapshot", true))) + v2.decider.assume(sf(sorts.Snap, v2) === Unit, Option.when(withExp)(DebugExp.createInstance("Empty snapshot", true)), analysisInfos) /* TODO: Avoid creating a fresh var (by invoking) `sf` that is not used * otherwise. In order words, only make this assumption if `sf` has * already been used, e.g. in a snapshot equality such as `s0 == (s1, s2)`. @@ -251,14 +277,14 @@ object producer extends ProductionRules { val impliesRecord = new ImpliesRecord(imp, s, v.decider.pcs, "produce") val uidImplies = v.symbExLog.openScope(impliesRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - branch(s1, t0, (e0, e0New), v1)( - (s2, v2) => produceR(s2, sf, a0, pve, v2)((s3, v3) => { + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + branch(s1, t0, (e0, e0New), v1, analysisInfos)( + (s2, v2) => produceR(s2, sf, a0, pve, v2, analysisInfos)((s3, v3) => { v3.symbExLog.closeScope(uidImplies) Q(s3, v3) }), (s2, v2) => { - v2.decider.assume(sf(sorts.Snap, v2) === Unit, Option.when(withExp)(DebugExp.createInstance("Empty snapshot", true))) + v2.decider.assume(sf(sorts.Snap, v2) === Unit, Option.when(withExp)(DebugExp.createInstance("Empty snapshot", isInternal_ = true)), analysisInfos) /* TODO: Avoid creating a fresh var (by invoking) `sf` that is not used * otherwise. In order words, only make this assumption if `sf` has * already been used, e.g. in a snapshot equality such as `s0 == (s1, s2)`. @@ -271,15 +297,15 @@ object producer extends ProductionRules { val condExpRecord = new CondExpRecord(ite, s, v.decider.pcs, "produce") val uidCondExp = v.symbExLog.openScope(condExpRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => // The type arguments here are Null because there is no need to pass any join data. - joiner.join[scala.Null, scala.Null](s1, v1, resetState = false)((s1, v1, QB) => - branch(s1.copy(parallelizeBranches = false), t0, (e0, e0New), v1)( - (s2, v2) => produceR(s2.copy(parallelizeBranches = s1.parallelizeBranches), sf, a1, pve, v2)((s3, v3) => { + joiner.join[scala.Null, scala.Null](s1, v1, analysisInfos, resetState = false)((s1, v1, QB) => + branch(s1.copy(parallelizeBranches = false), t0, (e0, e0New), v1, analysisInfos)( + (s2, v2) => produceR(s2.copy(parallelizeBranches = s1.parallelizeBranches), sf, a1, pve, v2, analysisInfos)((s3, v3) => { v3.symbExLog.closeScope(uidCondExp) QB(s3, null, v3) }), - (s2, v2) => produceR(s2.copy(parallelizeBranches = s1.parallelizeBranches), sf, a2, pve, v2)((s3, v3) => { + (s2, v2) => produceR(s2.copy(parallelizeBranches = s1.parallelizeBranches), sf, a2, pve, v2, analysisInfos)((s3, v3) => { v3.symbExLog.closeScope(uidCondExp) QB(s3, null, v3) })) @@ -299,29 +325,29 @@ object producer extends ProductionRules { val condExpRecord = new CondExpRecord(ite, s, v.decider.pcs, "produce") val uidCondExp = v.symbExLog.openScope(condExpRecord) - eval(s, e0, pve, v)((s1, t0, e0New, v1) => - branch(s1, t0, (e0, e0New), v1)( - (s2, v2) => produceR(s2, sf, a1, pve, v2)((s3, v3) => { + eval(s, e0, pve, v, analysisInfos)((s1, t0, e0New, v1) => + branch(s1, t0, (e0, e0New), v1, analysisInfos)( + (s2, v2) => produceR(s2, sf, a1, pve, v2, analysisInfos)((s3, v3) => { v3.symbExLog.closeScope(uidCondExp) Q(s3, v3) }), - (s2, v2) => produceR(s2, sf, a2, pve, v2)((s3, v3) => { + (s2, v2) => produceR(s2, sf, a2, pve, v2, analysisInfos)((s3, v3) => { v3.symbExLog.closeScope(uidCondExp) Q(s3, v3) }))) case let: ast.Let if !let.isPure => - letSupporter.handle[ast.Exp](s, let, pve, v)((s1, g1, body, v1) => - produceR(s1.copy(g = s1.g + g1), sf, body, pve, v1)(Q)) + letSupporter.handle[ast.Exp](s, let, pve, v, analysisInfos)((s1, g1, body, v1) => + produceR(s1.copy(g = s1.g + g1), sf, body, pve, v1, analysisInfos)(Q)) case accPred: ast.AccessPredicate => val eArgs = accPred.loc.args(s.program) val ePerm = accPred.perm val resource = accPred.res(s.program) - evals(s, eArgs, _ => pve, v)((s1, tArgs, eArgsNew, v1) => - eval(s1, ePerm, pve, v1)((s1a, tPerm, ePermNew, v1a) => - permissionSupporter.assertNotNegative(s1a, tPerm, ePerm, ePermNew, pve, v1a)((s1b, v2) => { + evals(s, eArgs, _ => pve, v, analysisInfos)((s1, tArgs, eArgsNew, v1) => + eval(s1, ePerm, pve, v1, analysisInfos)((s1a, tPerm, ePermNew, v1a) => + permissionSupporter.assertNotNegative(s1a, tPerm, ePerm, ePermNew, pve, v1a, analysisInfos)((s1b, v2) => { val s2 = s1b.copy(constrainableARPs = s.constrainableARPs) val snap = sf(v2.snapshotSupporter.optimalSnapshotSort(resource, s2, v2), v2) val gain = if (!Verifier.config.unsafeWildcardOptimization() || @@ -330,7 +356,7 @@ object producer extends ProductionRules { else WildcardSimplifyingPermTimes(tPerm, s2.permissionScalingFactor) val gainExp = ePermNew.map(p => ast.PermMul(p, s2.permissionScalingFactorExp.get)(p.pos, p.info, p.errT)) - v2.heapSupporter.produceSingle(s2, resource, tArgs, eArgsNew, snap, None, gain, gainExp, pve, true, v2)(Q) + v2.heapSupporter.produceSingle(s2, resource, tArgs, eArgsNew, snap, None, gain, gainExp, pve, true, v2, analysisInfos)(Q) }))) @@ -354,12 +380,12 @@ object producer extends ProductionRules { if (forall.triggers.isEmpty) None else Some(forall.triggers) val s0 = s.copy(functionRecorder = s.functionRecorder.enterQuantifiedExp(qpa)) - evalQuantified(s0, Forall, forall.variables, Seq(cond), ePerm +: eArgs, optTrigger, qid, pve, v) { + evalQuantified(s0, Forall, forall.variables, Seq(cond), ePerm +: eArgs, optTrigger, qid, pve, v, analysisInfos) { case (s1, qvars, qvarExps, Seq(tCond), eCondNew, Some((Seq(tPerm, tArgs@_*), permArgs, tTriggers, (auxGlobals, auxNonGlobals), auxExps)), v1) => val s1a = s1.copy(constrainableARPs = s.constrainableARPs) v1.heapSupporter.produceQuantified(s1a, sf, forall, resource, qvars, qvarExps, tFormalArgs, eFormalArgs, qid, optTrigger, tTriggers, auxGlobals, auxNonGlobals, auxExps.map(_._1), auxExps.map(_._2), tCond, eCondNew.map(_.head), tArgs, permArgs.map(_.tail), tPerm, permArgs.map(_.head), pve, NegativePermission(ePerm), - QPAssertionNotInjective(resAcc), v1)((s2, v2) => { + QPAssertionNotInjective(resAcc), v1, analysisInfos)((s2, v2) => { Q(s2.copy(functionRecorder = s2.functionRecorder.leaveQuantifiedExp(qpa)), v2) }) case (s1, _, _, _, _, None, v1) => Q(s1.copy(constrainableARPs = s.constrainableARPs), v1) @@ -371,9 +397,9 @@ object producer extends ProductionRules { /* Any regular expressions, i.e. boolean and arithmetic. */ case _ => v.decider.assume(sf(sorts.Snap, v) === Unit, - Option.when(withExp)(DebugExp.createInstance("Empty snapshot", true))) /* TODO: See comment for case ast.Implies above */ - eval(s, a, pve, v)((s1, t, aNew, v1) => { - v1.decider.assume(t, Option.when(withExp)(a), aNew) + Option.when(withExp)(DebugExp.createInstance("Empty snapshot", true)), analysisInfos) /* TODO: See comment for case ast.Implies above */ + eval(s, a, pve, v, analysisInfos)((s1, t, aNew, v1) => { + v1.decider.assume(t, Option.when(withExp)(a), aNew, analysisInfos) Q(s1, v1)}) } diff --git a/src/main/scala/rules/QuantifiedChunkSupport.scala b/src/main/scala/rules/QuantifiedChunkSupport.scala index b1a12e07e..b9634d45a 100644 --- a/src/main/scala/rules/QuantifiedChunkSupport.scala +++ b/src/main/scala/rules/QuantifiedChunkSupport.scala @@ -7,24 +7,27 @@ package viper.silicon.rules import viper.silicon -import viper.silicon.debugger.DebugExp import viper.silicon.Map import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis._ import viper.silicon.interfaces.VerificationResult -import viper.silicon.interfaces.state._ import viper.silicon.logger.records.data.CommentRecord import viper.silicon.resources.{NonQuantifiedPropertyInterpreter, QuantifiedPropertyInterpreter, Resources} import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.state.terms.perms.{BigPermSum, IsPositive} import viper.silicon.state.terms.predef.`?r` import viper.silicon.state.terms.utils.consumeExactRead import viper.silicon.supporters.functions.{FunctionRecorder, NoopFunctionRecorder} import viper.silicon.utils.ast.{BigAnd, buildMinExp} -import viper.silicon.utils.notNothing.NotNothing import viper.silicon.utils.freshSnap +import viper.silicon.utils.notNothing.NotNothing import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.ast.{FalseLit, NoPosition} +import viper.silver.dependencyAnalysis.{AssumptionType, NoDependencyAnalysisMerge, StringDependencyAnalysisSourceInfo} import viper.silver.parser.PUnknown import viper.silver.reporter.InternalWarningMessage import viper.silver.verifier.reasons.{InsufficientPermission, MagicWandChunkNotFound} @@ -187,7 +190,10 @@ trait QuantifiedChunkSupport extends SymbolicExecutionRules { permissions: Term, permissionsExp: Option[ast.Exp], sm: Term, - program: ast.Program) + program: ast.Program, + v: Verifier, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean) : QuantifiedBasicChunk /** Creates a quantified chunk corresponding to the assertion @@ -227,7 +233,9 @@ trait QuantifiedChunkSupport extends SymbolicExecutionRules { userProvidedTriggers: Option[Seq[Trigger]], qidPrefix: String, v: Verifier, - program: ast.Program) + program: ast.Program, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean) : (QuantifiedBasicChunk, InverseFunctions) def splitHeap[CH <: QuantifiedBasicChunk : NotNothing : ClassTag] @@ -239,7 +247,7 @@ trait QuantifiedChunkSupport extends SymbolicExecutionRules { def hintBasedChunkOrderHeuristic(hints: Seq[Term]) : Seq[QuantifiedBasicChunk] => Seq[QuantifiedBasicChunk] - def findChunk(chunks: Iterable[Chunk], chunk: QuantifiedChunk, v: Verifier): Option[QuantifiedChunk] + def findChunk(chunks: Iterable[Chunk], chunk: QuantifiedChunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): Option[QuantifiedChunk] /** Merge the snapshots of two quantified heap chunks that denote the same field locations * @@ -284,7 +292,10 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { permissions: Term, permissionsExp: Option[ast.Exp], sm: Term, - program: ast.Program) + program: ast.Program, + v: Verifier, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean) : QuantifiedBasicChunk = { val condition = @@ -312,7 +323,10 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { Some(arguments), argumentsExp, hints, - program) + program, + v, + analysisInfos, + isExhale) } /** @inheritdoc [[QuantifiedChunkSupport.createQuantifiedChunk]] */ @@ -334,7 +348,9 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { userProvidedTriggers: Option[Seq[Trigger]], qidPrefix: String, v: Verifier, - program: ast.Program) + program: ast.Program, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean) : (QuantifiedBasicChunk, InverseFunctions) = { val (inverseFunctions, imagesOfCodomain) = @@ -375,7 +391,10 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { None, None, hints, - program) + program, + v, + analysisInfos, + isExhale) (ch, inverseFunctions) } @@ -417,7 +436,10 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optSingletonArguments: Option[Seq[Term]], optSingletonArgumentsExp: Option[Seq[ast.Exp]], hints: Seq[Term], - program: ast.Program) + program: ast.Program, + v: Verifier, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean) : QuantifiedBasicChunk = { resource match { @@ -426,7 +448,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { assert(optSingletonArguments.fold(true)(_.length == 1)) assert(optSingletonArgumentsExp.fold(true)(_.length == 1)) - QuantifiedFieldChunk( + v.chunkFactory.createQuantifiedFieldChunk( BasicChunkIdentifier(field.name), sm, condition, @@ -436,10 +458,12 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optInverseFunctions, optSingletonArguments.map(_.head), optSingletonArgumentsExp.map(_.head), - hints) + hints, + analysisInfos, + isExhale) case predicate: ast.Predicate => - QuantifiedPredicateChunk( + v.chunkFactory.createQuantifiedPredicateChunk( BasicChunkIdentifier(predicate.name), codomainQVars, codomainQVarExps, @@ -451,12 +475,14 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optInverseFunctions, optSingletonArguments, optSingletonArgumentsExp, - hints) + hints, + analysisInfos, + isExhale) case wand: ast.MagicWand => val conditionalizedPermissions = Ite(condition, permissions, NoPerm) val conditionalizedPermissionsExp = conditionExp.map(ce => ast.CondExp(ce, permissionsExp.get, ast.NoPerm()())(ce.pos, ce.info, ce.errT)) - QuantifiedMagicWandChunk( + v.chunkFactory.createQuantifiedMagicWandChunk( MagicWandIdentifier(wand, program), codomainQVars, codomainQVarExps, @@ -466,7 +492,9 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optInverseFunctions, optSingletonArguments, optSingletonArgumentsExp, - hints) + hints, + analysisInfos, + isExhale) case other => sys.error(s"Found yet unsupported resource $other (${other.getClass.getSimpleName})") @@ -645,16 +673,16 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { smDef: SnapshotMapDefinition, v: Verifier) : (PermMapDefinition, PmCache) = { - + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo.createUnique("summarizing heap"), AssumptionType.Internal) Verifier.config.mapCache(s.pmCache.get(resource, relevantChunks)) match { case Some(pmDef) => - v.decider.assume(pmDef.valueDefinitions, Option.when(withExp)(DebugExp.createInstance("value definitions", isInternal_ = true)), enforceAssumption = false) + v.decider.assume(pmDef.valueDefinitions, Option.when(withExp)(DebugExp.createInstance("value definitions", isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) (pmDef, s.pmCache) case _ => val (pm, valueDef) = quantifiedChunkSupporter.summarisePerm(s, relevantChunks, formalQVars, resource, smDef, v) val pmDef = PermMapDefinition(resource, pm, valueDef) - v.decider.assume(valueDef, Option.when(withExp)(DebugExp.createInstance("value definitions", isInternal_ = true)), enforceAssumption = false) + v.decider.assume(valueDef, Option.when(withExp)(DebugExp.createInstance("value definitions", isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) (pmDef, s.pmCache + ((resource, relevantChunks) -> pmDef)) } } @@ -683,6 +711,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optSmDomainDefinitionCondition: Option[Term] = None, optQVarsInstantiations: Option[Seq[Term]] = None) : (SnapshotMapDefinition, SnapshotMapCache) = { + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo.createUnique("summarizing heap"), AssumptionType.Internal) def emitSnapshotMapDefinition(s: State, smDef: SnapshotMapDefinition, @@ -695,7 +724,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { case None => val comment = "Definitional axioms for snapshot map domain" v.decider.prover.comment(comment) - v.decider.assume(smDef.domainDefinitions, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false) + v.decider.assume(smDef.domainDefinitions, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) case Some(_instantiations) => // TODO: Avoid pattern matching on resource val instantiations = resource match { @@ -707,7 +736,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.prover.comment(comment) // TODO: Avoid cast to Quantification v.decider.assume(smDef.domainDefinitions.map(_.asInstanceOf[Quantification].instantiate(instantiations)), - Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false) + Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) } } @@ -715,7 +744,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { case None => val comment = "Definitional axioms for snapshot map values" v.decider.prover.comment(comment) - v.decider.assume(smDef.valueDefinitions, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false) + v.decider.assume(smDef.valueDefinitions, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) case Some(_instantiations) => // TODO: Avoid pattern matching on resource val instantiations = resource match { @@ -727,7 +756,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.prover.comment(comment) // TODO: Avoid cast to Quantification v.decider.assume(smDef.valueDefinitions.map(_.asInstanceOf[Quantification].instantiate(instantiations)), - Option.when(withExp)(DebugExp.createInstance(comment, true)), enforceAssumption = false) + Option.when(withExp)(DebugExp.createInstance(comment, true)), enforceAssumption = false, analysisInfos=analysisInfos) } } @@ -753,7 +782,6 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { } emitSnapshotMapDefinition(s, smDef, v, optQVarsInstantiations) - (smDef, smCache) } @@ -765,7 +793,6 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optSmDomainDefinitionCondition: Option[Term] = None, optQVarsInstantiations: Option[Seq[Term]] = None) : (State, SnapshotMapDefinition, PermMapDefinition) = { - val (smDef, smCache) = summarisingSnapshotMap( s, resource, codomainQVars, relevantChunks, v, optSmDomainDefinitionCondition, optQVarsInstantiations) @@ -777,7 +804,6 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { s1, resource, codomainQVars, relevantChunks, smDef, v) val s2 = s1.copy(pmCache = pmCache, functionRecorder = s1.functionRecorder.recordFvfAndDomain(smDef).recordPermMap(pmDef)) - (s2, smDef, pmDef) } @@ -790,14 +816,12 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { relevantChunks: Seq[QuantifiedBasicChunk], v: Verifier) : (State, PermMapDefinition) = { - val s1 = s val (pmDef, pmCache) = quantifiedChunkSupporter.summarisingPermissionMap( s1, resource, codomainQVars, relevantChunks, null, v) val s2 = s1.copy(pmCache = pmCache) - (s2, pmDef) } @@ -829,7 +853,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { pve: PartialVerificationError, negativePermissionReason: => ErrorReason, notInjectiveReason: => ErrorReason, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -859,7 +884,9 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { userProvidedTriggers = optTrigger.map(_ => tTriggers), qidPrefix = qid, v = v, - program = s.program) + program = s.program, + analysisInfos = analysisInfos, + isExhale = false) val (effectiveTriggers, effectiveTriggersQVars, effectiveTriggersQVarExps) = optTrigger match { case Some(_) => @@ -904,8 +931,9 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val commentGlobals = "Nested auxiliary terms: globals" v.decider.prover.comment(commentGlobals) + val analysisInfosGlobals = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo(commentGlobals, NoPosition), AssumptionType.Internal).withMergeInfo(NoDependencyAnalysisMerge()) v.decider.assume(auxGlobals, Option.when(withExp)(DebugExp.createInstance(description=commentGlobals, children=auxGlobalsExp.get)), - enforceAssumption = false) + enforceAssumption = false, analysisInfos=analysisInfosGlobals) val commentNonGlobals = "Nested auxiliary terms: non-globals" v.decider.prover.comment(commentNonGlobals) @@ -913,13 +941,13 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { auxNonGlobals.map(_.copy( vars = effectiveTriggersQVars, triggers = effectiveTriggers)), - Option.when(withExp)(DebugExp.createInstance(description=commentNonGlobals, children=auxNonGlobalsExp.get)), enforceAssumption = false) + Option.when(withExp)(DebugExp.createInstance(description=commentNonGlobals, children=auxNonGlobalsExp.get)), enforceAssumption = false, analysisInfos=analysisInfos.withDependencyType(AssumptionType.Internal)) val nonNegImplication = Implies(tCond, perms.IsNonNegative(tPerm)) val nonNegImplicationExp = eCond.map(c => ast.Implies(c, ast.PermGeCmp(ePerm.get, ast.NoPerm()())())(c.pos, c.info, c.errT)) val nonNegTerm = Forall(qvars, Implies(FunctionPreconditionTransformer.transform(nonNegImplication, s.program), nonNegImplication), Nil) // TODO: Replace by QP-analogue of permissionSupporter.assertNotNegative - v.decider.assert(nonNegTerm) { + v.decider.assert(nonNegTerm, analysisInfos) { case true => /* TODO: Can we omit/simplify the injectivity check in certain situations? */ @@ -941,7 +969,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.prover.comment(comment) val completeReceiverInjectivityCheck = Implies(FunctionPreconditionTransformer.transform(receiverInjectivityCheck, s.program), receiverInjectivityCheck) - v.decider.assert(completeReceiverInjectivityCheck) { + v.decider.assert(completeReceiverInjectivityCheck, analysisInfos) { case true => val ax = inverseFunctions.axiomInversesOfInvertibles val inv = inverseFunctions.copy(axiomInversesOfInvertibles = Forall(ax.vars, ax.body, effectiveTriggers)) @@ -950,8 +978,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.prover.comment(comment) val definitionalAxiomMark = v.decider.setPathConditionMark() v.decider.assume(inv.definitionalAxioms.map(a => FunctionPreconditionTransformer.transform(a, s.program)), - Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false) - v.decider.assume(inv.definitionalAxioms, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false) + Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) + v.decider.assume(inv.definitionalAxioms, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) val conservedPcs = if (s.recordPcs) (s.conservedPcs.head :+ v.decider.pcs.after(definitionalAxiomMark)) +: s.conservedPcs.tail else s.conservedPcs @@ -974,9 +1002,9 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { triggers = effectiveTriggers, qidPrefix = qid ) - v.decider.assume(pcsForChunk, pcsForChunkExp, pcsForChunkExp) + v.decider.assume(pcsForChunk, pcsForChunkExp, pcsForChunkExp, analysisInfos.withDependencyType(AssumptionType.Internal)) }) - val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, s.h, Heap(Seq(ch)), v) + val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, s.h, Heap(Seq(ch)), v, v.decider.defaultAnalysisInfos) val (smCache1, fr2) = if (s.isUsedAsTrigger(resource)){ // TODO: Why not formalQVars? Used as codomainVars, see above. @@ -998,7 +1026,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val qvarsToInv = inv.qvarsToInversesOf(codomainVars) val condOfInv = tCond.replace(qvarsToInv) v.decider.assume(Forall(codomainVars, Implies(condOfInv, trigger), Trigger(inv.inversesOf(codomainVars))), - Option.when(withExp)(DebugExp.createInstance("Inverse Trigger", true))) + Option.when(withExp)(DebugExp.createInstance("Inverse Trigger", true)), analysisInfos.withDependencyType(AssumptionType.Trigger)) val newFuncRec = fr1.recordFvfAndDomain(smDef1) (smCache1, newFuncRec) } else { @@ -1010,12 +1038,17 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { conservedPcs = conservedPcs, smCache = smCache1) Q(s1, v) - case false => { - createFailure(pve dueTo notInjectiveReason, v, s, receiverInjectivityCheck, "QP receiver is injective") - } + case false => + val failure = createFailure(pve dueTo notInjectiveReason, v, s, receiverInjectivityCheck, "QP receiver is injective") + if (s.retryLevel == 0) v.decider.handleFailedAssertion(completeReceiverInjectivityCheck, None, None, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, v) else failure } case false => - createFailure(pve dueTo negativePermissionReason, v, s, nonNegImplication, nonNegImplicationExp)} + val failure = createFailure(pve dueTo negativePermissionReason, v, s, nonNegImplication, nonNegImplicationExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(nonNegTerm, nonNegImplicationExp, nonNegImplicationExp, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) failure combine Q(s, v) else failure + } + } def produceSingleLocation(s: State, @@ -1029,7 +1062,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { ePerm: Option[ast.Exp], resourceTriggerFactory: Term => Term, /* Trigger with some snapshot */ mergeAndTrigger: Boolean, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Verifier) => VerificationResult) : VerificationResult = { @@ -1038,19 +1072,19 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val comment = "Definitional axioms for singleton-SM's value" v.decider.prover.comment(comment) val definitionalAxiomMark = v.decider.setPathConditionMark() - v.decider.assumeDefinition(smValueDef, Option.when(withExp)(DebugExp.createInstance(comment, true))) + v.decider.assumeDefinition(smValueDef, Option.when(withExp)(DebugExp.createInstance(comment, isInternal_ = true)), analysisInfos) val conservedPcs = if (s.recordPcs) (s.conservedPcs.head :+ v.decider.pcs.after(definitionalAxiomMark)) +: s.conservedPcs.tail else s.conservedPcs - val ch = quantifiedChunkSupporter.createSingletonQuantifiedChunk(formalQVars, formalQVarsExp, resource, tArgs, eArgs, tPerm, ePerm, sm, s.program) + val ch = quantifiedChunkSupporter.createSingletonQuantifiedChunk(formalQVars, formalQVarsExp, resource, tArgs, eArgs, tPerm, ePerm, sm, s.program, v, analysisInfos, isExhale=false) val s1 = if (mergeAndTrigger) { - val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, s.h, Heap(Seq(ch)), v) + val (fr1, h1) = v.stateConsolidator(s).merge(s.functionRecorder, s, s.h, Heap(Seq(ch)), v, v.decider.defaultAnalysisInfos) val interpreter = new NonQuantifiedPropertyInterpreter(h1.values, v) val resourceDescription = Resources.resourceDescriptions(ch.resourceID) val pcs = interpreter.buildPathConditionsForChunk(ch, resourceDescription.instanceProperties(s.mayAssumeUpperBounds)) - pcs.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)))) + pcs.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)), analysisInfos)) val smCache1 = if (s.isUsedAsTrigger(resource)) { val (relevantChunks, _) = @@ -1058,7 +1092,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val (smDef1, smCache1) = quantifiedChunkSupporter.summarisingSnapshotMap( s, resource, formalQVars, relevantChunks, v) - v.decider.assume(resourceTriggerFactory(smDef1.sm), Option.when(withExp)(DebugExp.createInstance("Resource Trigger", true))) + v.decider.assume(resourceTriggerFactory(smDef1.sm), Option.when(withExp)(DebugExp.createInstance("Resource Trigger", true)), analysisInfos.withDependencyType(AssumptionType.Trigger)) smCache1 } else { s.smCache @@ -1101,7 +1135,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { negativePermissionReason: => ErrorReason, notInjectiveReason: => ErrorReason, insufficientPermissionReason: => ErrorReason, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -1137,7 +1172,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val comment = "Nested auxiliary terms: globals" v.decider.prover.comment(comment) - v.decider.assume(auxGlobals, Option.when(withExp)(DebugExp.createInstance(description=comment, children=auxGlobalsExp.get)), enforceAssumption = false) + v.decider.assume(auxGlobals, Option.when(withExp)(DebugExp.createInstance(description=comment, children=auxGlobalsExp.get)), enforceAssumption = false, analysisInfos=v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo.createUnique(comment), AssumptionType.Internal)) val comment2 = "Nested auxiliary terms: non-globals" v.decider.prover.comment(comment2) @@ -1147,10 +1182,10 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.assume( auxNonGlobals.map(_.copy( vars = effectiveTriggersQVars, - triggers = effectiveTriggers)), Option.when(withExp)(DebugExp.createInstance(description=comment2, children=auxNonGlobalsExp.get)), enforceAssumption = false) + triggers = effectiveTriggers)), Option.when(withExp)(DebugExp.createInstance(description=comment2, children=auxNonGlobalsExp.get)), enforceAssumption = false, analysisInfos=analysisInfos.withDependencyType(AssumptionType.Internal)) case Some(_) => /* Explicit triggers were provided. */ - v.decider.assume(auxNonGlobals, Option.when(withExp)(DebugExp.createInstance(description=comment2, children=auxNonGlobalsExp.get)), enforceAssumption = false) + v.decider.assume(auxNonGlobals, Option.when(withExp)(DebugExp.createInstance(description=comment2, children=auxNonGlobalsExp.get)), enforceAssumption = false, analysisInfos=analysisInfos.withDependencyType(AssumptionType.Internal)) } val nonNegImplication = Implies(tCond, perms.IsNonNegative(tPerm)) @@ -1158,11 +1193,11 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val nonNegTerm = Forall(qvars, Implies(FunctionPreconditionTransformer.transform(nonNegImplication, s.program), nonNegImplication), Nil) val nonNegExp = qvarExps.map(qv => ast.Forall(qv, Nil, nonNegImplicationExp.get)()) // TODO: Replace by QP-analogue of permissionSupporter.assertNotNegative - v.decider.assert(nonNegTerm) { + v.decider.assert(nonNegTerm, analysisInfos) { case true => val hints = quantifiedChunkSupporter.extractHints(Some(tCond), tArgs) val chunkOrderHeuristics = - qpAppChunkOrderHeuristics(inverseFunctions.invertibles, qvars, hints, v) + qpAppChunkOrderHeuristics(inverseFunctions.invertibles, qvars, hints, v, analysisInfos) val loss = if (!Verifier.config.unsafeWildcardOptimization() || (resource.isInstanceOf[ast.Location] && s.permLocations.contains(resource.asInstanceOf[ast.Location]))) PermTimes(tPerm, s.permissionScalingFactor) @@ -1194,7 +1229,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { program = s.program) v.decider.prover.comment("Check receiver injectivity") val completeReceiverInjectivityCheck = Implies(FunctionPreconditionTransformer.transform(receiverInjectivityCheck, s.program), receiverInjectivityCheck) - v.decider.assert(completeReceiverInjectivityCheck) { + v.decider.assert(completeReceiverInjectivityCheck, analysisInfos) { case true => val qvarsToInvOfLoc = inverseFunctions.qvarsToInversesOf(formalQVars) val condOfInvOfLoc = tCond.replace(qvarsToInvOfLoc) @@ -1208,8 +1243,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.prover.comment("Definitional axioms for inverse functions") v.decider.assume(inverseFunctions.definitionalAxioms.map(a => FunctionPreconditionTransformer.transform(a, s.program)), - Option.when(withExp)(DebugExp.createInstance("Inverse Function Axioms", isInternal_ = true)), enforceAssumption = false) - v.decider.assume(inverseFunctions.definitionalAxioms, Option.when(withExp)(DebugExp.createInstance("Inverse function axiom", isInternal_ = true)), enforceAssumption = false) + Option.when(withExp)(DebugExp.createInstance("Inverse Function Axioms", isInternal_ = true)), enforceAssumption = false, analysisInfos = analysisInfos) + v.decider.assume(inverseFunctions.definitionalAxioms, Option.when(withExp)(DebugExp.createInstance("Inverse function axiom", isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) if (s.isUsedAsTrigger(resource)){ v.decider.assume( @@ -1217,7 +1252,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { formalQVars, Implies(condOfInvOfLoc, ResourceTriggerFunction(resource, smDef1.get.sm, formalQVars, s.program)), Trigger(inverseFunctions.inversesOf(formalQVars)))), - Option.when(withExp)(DebugExp.createInstance("Inverse Function", isInternal_ = true)), enforceAssumption = false) + Option.when(withExp)(DebugExp.createInstance("Inverse Function", isInternal_ = true)), enforceAssumption = false, analysisInfos=analysisInfos) } @@ -1230,7 +1265,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { lossExp, createFailure(pve dueTo insufficientPermissionReason/*InsufficientPermission(acc.loc)*/, v, s, "consuming QP"), formalQVars, - v)((s2, heap, rPerm, rPermExp, v2) => { + v, + analysisInfos)((s2, heap, rPerm, rPermExp, v2) => { val (relevantChunks, otherChunks) = quantifiedChunkSupporter.splitHeap[QuantifiedBasicChunk]( heap, ChunkIdentifier(resource, s.program)) @@ -1247,7 +1283,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { rPerm, rPermExp, chunkOrderHeuristics, - v2) + v2, + analysisInfos) val optSmDomainDefinitionCondition2 = if (s3.smDomainNeeded) Some(And(condOfInvOfLoc, IsPositive(lossOfInvOfLoc), And(imagesOfFormalQVars))) else None @@ -1278,14 +1315,16 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { optTrigger.map(_ => tTriggers), qid, v2, - s.program + s.program, + analysisInfos, + isExhale = true ) - val debugExp = Option.when(withExp)(DebugExp.createInstance("Inverse functions for quantified permission", true)) - v.decider.assume(FunctionPreconditionTransformer.transform(inverseFunctions.axiomInvertiblesOfInverses, s3.program), debugExp) - v.decider.assume(inverseFunctions.axiomInvertiblesOfInverses, debugExp) + val debugExp = Option.when(withExp)(DebugExp.createInstance("Inverse functions for quantified permission", isInternal_ = true)) + v.decider.assume(FunctionPreconditionTransformer.transform(inverseFunctions.axiomInvertiblesOfInverses, s3.program), debugExp, analysisInfos) + v.decider.assume(inverseFunctions.axiomInvertiblesOfInverses, debugExp, analysisInfos) val substitutedAxiomInversesOfInvertibles = inverseFunctions.axiomInversesOfInvertibles.replace(formalQVars, tArgs) - v.decider.assume(FunctionPreconditionTransformer.transform(substitutedAxiomInversesOfInvertibles, s3.program), debugExp) - v.decider.assume(substitutedAxiomInversesOfInvertibles, debugExp) + v.decider.assume(FunctionPreconditionTransformer.transform(substitutedAxiomInversesOfInvertibles, s3.program), debugExp, analysisInfos) + v.decider.assume(substitutedAxiomInversesOfInvertibles, debugExp, analysisInfos) val h2 = Heap(remainingChunks ++ otherChunks) val s4 = s3.copy(smCache = smCache2, constrainableARPs = s.constrainableARPs) @@ -1313,7 +1352,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { lossOfInvOfLoc, lossExp, chunkOrderHeuristics, - v + v, + analysisInfos ) permissionRemovalResult match { case (Complete(), s2, remainingChunks) => @@ -1339,12 +1379,31 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { Q(s2, h3, None, v) } case (Incomplete(_, _), s2, _) => - createFailure(pve dueTo insufficientPermissionReason, v, s2, "QP consume")} + val failure = createFailure(pve dueTo insufficientPermissionReason, v, s2, "QP consume") + if (s2.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + if (s2.retryLevel == 0 && v.reportFurtherErrors() && Verifier.config.analyzeInfeasiblePaths) failure combine Q(s2, s2.h, None, v) else failure + } } case false => - createFailure(pve dueTo notInjectiveReason, v, s, receiverInjectivityCheck, "QP receiver injective")} + val failure = createFailure(pve dueTo notInjectiveReason, v, s, receiverInjectivityCheck, "QP receiver injective") + if (s.retryLevel == 0) v.decider.handleFailedAssertion(receiverInjectivityCheck, None, None, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) { + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) + failure combine Q(s, s.h, if (returnSnap) Some(snap) else None, v) + } else { + failure + } + } case false => - createFailure(pve dueTo negativePermissionReason, v, s, nonNegTerm, nonNegExp)} + val failure = createFailure(pve dueTo negativePermissionReason, v, s, nonNegTerm, nonNegExp) + if (s.retryLevel == 0) v.decider.handleFailedAssertion(nonNegTerm, nonNegExp, nonNegExp, analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) { + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) + failure combine Q(s, s.h, if (returnSnap) Some(snap) else None, v) + } else { + failure + } + } } def consumeSingleLocation(s: State, @@ -1359,7 +1418,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { returnSnap: Boolean, optChunkOrderHeuristic: Option[Seq[QuantifiedBasicChunk] => Seq[QuantifiedBasicChunk]], pve: PartialVerificationError, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) (Q: (State, Heap, Option[Term], Verifier) => VerificationResult) : VerificationResult = { @@ -1371,7 +1431,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { heuristics case None => quantifiedChunkSupporter.singleReceiverChunkOrderHeuristic(arguments, - quantifiedChunkSupporter.extractHints(None, arguments), v) + quantifiedChunkSupporter.extractHints(None, arguments), v, analysisInfos) } if (s.exhaleExt) { @@ -1380,7 +1440,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { case wand: ast.MagicWand => createFailure(pve dueTo MagicWandChunkNotFound(wand), v, s, "single QP consume inside package") case _ => sys.error(s"Found resource $resourceAccess, which is not yet supported as a quantified resource.") } - magicWandSupporter.transfer(s, permissions, permissionsExp, failure, Seq(), v)((s1, h1, rPerm, rPermExp, v1) => { + magicWandSupporter.transfer(s, permissions, permissionsExp, failure, Seq(), v, analysisInfos)((s1, h1, rPerm, rPermExp, v1) => { val (relevantChunks, otherChunks) = quantifiedChunkSupporter.splitHeap[QuantifiedBasicChunk](h1, chunkIdentifier) val (result, s2, remainingChunks) = quantifiedChunkSupporter.removePermissions( @@ -1395,7 +1455,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { rPerm, rPermExp, chunkOrderHeuristics, - v + v, + analysisInfos ) val h2 = Heap(remainingChunks ++ otherChunks) val (smDef1, smCache1) = @@ -1414,7 +1475,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { } val consumedChunk = quantifiedChunkSupporter.createSingletonQuantifiedChunk( - codomainQVars, codomainQVarsExp, resource, arguments, argumentsExp, permsTaken, permsTakenExp, smDef1.sm, s.program) + codomainQVars, codomainQVarsExp, resource, arguments, argumentsExp, permsTaken, permsTakenExp, smDef1.sm, s.program, v1, analysisInfos, isExhale=true) val s3 = s2.copy(functionRecorder = s2.functionRecorder.recordFvfAndDomain(smDef1), smCache = smCache1) (result, s3, h2, Some(consumedChunk)) @@ -1444,7 +1505,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { permissions, permissionsExp, chunkOrderHeuristics, - v + v, + analysisInfos ) result match { case (Complete(), s1, remainingChunks) => @@ -1467,11 +1529,16 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { Q(s1, h1, None, v) } case (Incomplete(_, _), _, _) => - resourceAccess match { + val failure = resourceAccess match { case locAcc: ast.LocationAccess => createFailure(pve dueTo InsufficientPermission(locAcc), v, s, "single QP consume") case wand: ast.MagicWand => createFailure(pve dueTo MagicWandChunkNotFound(wand), v, s, "single QP consume") case _ => sys.error(s"Found resource $resourceAccess, which is not yet supported as a quantified resource.") } + if (s.retryLevel == 0) v.decider.handleFailedAssertion(False, Option.when(withExp)(FalseLit()()), Option.when(withExp)(FalseLit()()), analysisInfos, v.reportFurtherErrors()) + if (s.retryLevel == 0 && v.reportFurtherErrors()) { + val snap = v.decider.fresh(v.snapshotSupporter.optimalSnapshotSort(resource, s, v), Option.when(withExp)(PUnknown())) + failure combine Q(s, s.h, Some(snap), v) + } else failure } } } @@ -1482,7 +1549,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { condition: Term, perms: Term, permsExp: Option[ast.Exp], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : ConsumptionResult = { var permsAvailable: Term = NoPerm @@ -1499,7 +1567,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { // final check val result = - if (v.decider.check(tookEnoughCheck, Verifier.config.assertTimeout.getOrElse(0)) /* This check is a must-check, i.e. an assert */ ) + if (v.decider.check(tookEnoughCheck, Verifier.config.assertTimeout.getOrElse(0), analysisInfos) /* This check is a must-check, i.e. an assert */ ) Complete() else Incomplete(PermMinus(permsAvailable, perms), permsAvailableExp.map(pa => ast.PermSub(pa, permsExp.get)())) @@ -1522,7 +1590,8 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { perms: Term, // p(rs) permsExp: Option[ast.Exp], // p(rs) chunkOrderHeuristic: Seq[QuantifiedBasicChunk] => Seq[QuantifiedBasicChunk], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : (ConsumptionResult, State, Seq[QuantifiedBasicChunk]) = { val rmPermRecord = new CommentRecord("removePermissions", s, v.decider.pcs) @@ -1539,7 +1608,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val constrainPermissions = !consumeExactRead(perms, s.constrainableARPs) if (s.assertReadAccessOnly) { - val result = assertReadPermission(s, candidates, codomainQVars, condition, perms, permsExp, v) + val result = assertReadPermission(s, candidates, codomainQVars, condition, perms, permsExp, v, analysisInfos) return (result, s, relevantChunks) } @@ -1603,21 +1672,23 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { if (constrainPermissions) { v.decider.prover.comment(s"Constrain original permissions $perms") - v.decider.assume(permissionConstraint, permissionConstraintExp, permissionConstraintExp) + v.decider.assume(permissionConstraint, permissionConstraintExp, permissionConstraintExp, analysisInfos) remainingChunks = - remainingChunks :+ ithChunk.permMinus(ithPTaken, ithPTakenExp) + remainingChunks :+ v.chunkFactory.permMinus(ithChunk, ithPTaken, ithPTakenExp, analysisInfos) } else { v.decider.prover.comment(s"Chunk depleted?") - val chunkDepleted = v.decider.check(depletedCheck, Verifier.config.splitTimeout()) + val chunkDepleted = v.decider.check(depletedCheck, Verifier.config.splitTimeout(), analysisInfos.withDependencyType(AssumptionType.Internal)) if (!chunkDepleted) { val unusedCheck = Forall(codomainQVars, ithPTaken === NoPerm, Nil) - val chunkUnused = v.decider.check(unusedCheck, Verifier.config.checkTimeout()) + val chunkUnused = v.decider.check(unusedCheck, Verifier.config.checkTimeout(), analysisInfos.withDependencyType(AssumptionType.Internal)) if (chunkUnused) { remainingChunks = remainingChunks :+ ithChunk } else { remainingChunks = - remainingChunks :+ ithChunk.permMinus(ithPTaken, ithPTakenExp) + remainingChunks :+ v.chunkFactory.permMinus(ithChunk, ithPTaken, ithPTakenExp, analysisInfos) } + } else { + val _ = v.chunkFactory.withPerm(ithChunk, ithPTaken, None, analysisInfos, isExhale=true) } } @@ -1630,7 +1701,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { Forall(codomainQVars, Implies(condition, ithPNeeded === NoPerm), Nil) v.decider.prover.comment(s"Intermediate check if already taken enough permissions") - success = if (v.decider.check(tookEnoughCheck, Verifier.config.splitTimeout())) { + success = if (v.decider.check(tookEnoughCheck, Verifier.config.splitTimeout(), analysisInfos)) { Complete() } else { Incomplete(ithPNeeded, ithPNeededExp) @@ -1640,7 +1711,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { v.decider.prover.comment("Final check if taken enough permissions") success = - if (success.isComplete || v.decider.check(tookEnoughCheck, Verifier.config.assertTimeout.getOrElse(0)) /* This check is a must-check, i.e. an assert */) + if (success.isComplete || v.decider.check(tookEnoughCheck, Verifier.config.assertTimeout.getOrElse(0), analysisInfos) /* This check is a must-check, i.e. an assert */) Complete() else success @@ -1959,7 +2030,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { matchingChunks ++ otherChunks } - override def findChunk(chunks: Iterable[Chunk], chunk: QuantifiedChunk, v: Verifier): Option[QuantifiedChunk] = { + override def findChunk(chunks: Iterable[Chunk], chunk: QuantifiedChunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): Option[QuantifiedChunk] = { val lr = chunk match { case qfc: QuantifiedFieldChunk if qfc.invs.isDefined => val qvarsAndInverses = qfc.invs.get.qvarsToInverses.map(qvi => (qvi._1, App(qvi._2, qfc.invs.get.additionalArguments.toSeq ++ qfc.quantifiedVars))) @@ -2000,11 +2071,11 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { // Hence, we need to compare the conditions for equality in addition to verifying that the receivers match. val equalityCond = And(cond.replace(chunk.quantifiedVars, singletonArguments), cCond.replace(ch.quantifiedVars, cSingletonArguments)) - val result = v.decider.check(And(equalityCond, equalityTerm), Verifier.config.checkTimeout()) + val result = v.decider.check(And(equalityCond, equalityTerm), Verifier.config.checkTimeout(), analysisInfos) if (result) { // Learn the equality - val debugExp = Option.when(withExp)(DebugExp.createInstance("Chunks alias", true)) - v.decider.assume(equalityTerm, debugExp) + val debugExp = Option.when(withExp)(DebugExp.createInstance("Chunks alias", isInternal_ = true)) + v.decider.assume(equalityTerm, debugExp, analysisInfos.withDependencyType(AssumptionType.Internal)) } result case _ => false @@ -2030,11 +2101,11 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { val condReplaced = cCond.replace(cQvars, quantVars) val secondReplaced = p._2.replace(cQvars, quantVars) val equalityTerm = SimplifyingForall(quantVars, And(Seq(p._1 === secondReplaced, cond === condReplaced)), Seq()) - val result = v.decider.check(equalityTerm, Verifier.config.checkTimeout()) + val result = v.decider.check(equalityTerm, Verifier.config.checkTimeout(), analysisInfos) if (result) { // Learn the equality - val debugExp = Option.when(withExp)(DebugExp.createInstance("Chunks alias", true)) - v.decider.assume(equalityTerm, debugExp) + val debugExp = Option.when(withExp)(DebugExp.createInstance("Chunks alias", isInternal_ = true)) + v.decider.assume(equalityTerm, debugExp, analysisInfos.withDependencyType(AssumptionType.Internal)) } result } else { @@ -2096,7 +2167,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { (fr2, sm, Forall(qVars, smDef, triggers)) } - def qpAppChunkOrderHeuristics(receiverTerms: Seq[Term], quantVars: Seq[Var], hints: Seq[Term], v: Verifier) + def qpAppChunkOrderHeuristics(receiverTerms: Seq[Term], quantVars: Seq[Var], hints: Seq[Term], v: Verifier, analysisInfos: DependencyAnalysisInfos) : Seq[QuantifiedBasicChunk] => Seq[QuantifiedBasicChunk] = { // Heuristics that looks for quantified chunks that have the same shape (as in, the same number and types of // quantified variables) and identical receiver terms. @@ -2122,7 +2193,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { receiverTerms.zip(cInvertibles).forall(p => { if (cQvars.length == quantVars.length && cQvars.zip(quantVars).forall(vars => vars._1.sort == vars._2.sort)) { val secondReplaced = p._2.replace(cQvars, quantVars) - v.decider.check(SimplifyingForall(quantVars, p._1 === secondReplaced, Seq()), Verifier.config.checkTimeout()) + v.decider.check(SimplifyingForall(quantVars, p._1 === secondReplaced, Seq()), Verifier.config.checkTimeout(), analysisInfos) } else { false } @@ -2138,7 +2209,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { } } - def singleReceiverChunkOrderHeuristic(receiver: Seq[Term], hints: Seq[Term], v: Verifier) + def singleReceiverChunkOrderHeuristic(receiver: Seq[Term], hints: Seq[Term], v: Verifier, analysisInfos: DependencyAnalysisInfos) : Seq[QuantifiedBasicChunk] => Seq[QuantifiedBasicChunk] = { // Heuristic that emulates greedy Silicon behavior for consuming single-receiver permissions. // First: Find singleton chunks that have the same receiver syntactically. @@ -2155,7 +2226,7 @@ object quantifiedChunkSupporter extends QuantifiedChunkSupport { } else { val greedyMatch = chunks.find(c => c.singletonArguments match { case Some(args) if args.length == receiver.length => - args.zip(receiver).forall(ts => v.decider.check(ts._1 === ts._2, Verifier.config.checkTimeout())) + args.zip(receiver).forall(ts => v.decider.check(ts._1 === ts._2, Verifier.config.checkTimeout(), analysisInfos)) case _ => false }).toSeq diff --git a/src/main/scala/rules/StateConsolidator.scala b/src/main/scala/rules/StateConsolidator.scala index 663282d3c..4bbcb7656 100644 --- a/src/main/scala/rules/StateConsolidator.scala +++ b/src/main/scala/rules/StateConsolidator.scala @@ -6,30 +6,32 @@ package viper.silicon.rules -import viper.silicon.debugger.DebugExp import viper.silicon.Config import viper.silicon.common.collections.immutable.InsertionOrderedSet -import viper.silicon.interfaces.state._ +import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis._ import viper.silicon.logger.records.data.{CommentRecord, SingleMergeRecord} import viper.silicon.resources.{NonQuantifiedPropertyInterpreter, Resources} import viper.silicon.state._ +import viper.silicon.state.chunks._ import viper.silicon.state.terms._ import viper.silicon.state.terms.perms._ import viper.silicon.state.terms.predef.`?r` import viper.silicon.supporters.functions.FunctionRecorder import viper.silicon.verifier.Verifier import viper.silver.ast +import viper.silver.dependencyAnalysis.{AssumptionType, StringDependencyAnalysisSourceInfo} import scala.annotation.unused trait StateConsolidationRules extends SymbolicExecutionRules { def consolidate(s: State, v: Verifier): State def consolidateOptionally(s: State, v: Verifier): State - def merge(fr: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier): (FunctionRecorder, Heap) - def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier): (FunctionRecorder, Heap) + def merge(fr: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) + def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) - protected def assumeUpperPermissionBoundForQPFields(s: State, v: Verifier): State - protected def assumeUpperPermissionBoundForQPFields(s: State, heaps: Seq[Heap], v: Verifier): State + protected def assumeUpperPermissionBoundForQPFields(s: State, v: Verifier, analysisInfos: DependencyAnalysisInfos): State + protected def assumeUpperPermissionBoundForQPFields(s: State, heaps: Seq[Heap], v: Verifier, analysisInfos: DependencyAnalysisInfos): State } /** Performs the minimal work necessary for any consolidator: merging two heaps combines the chunk @@ -41,15 +43,15 @@ class MinimalStateConsolidator extends StateConsolidationRules { def consolidate(s: State, @unused v: Verifier): State = s def consolidateOptionally(s: State, @unused v: Verifier): State = s - def merge(fr: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier): (FunctionRecorder, Heap) = + def merge(fr: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) = (fr, Heap(h.values ++ newH.values)) - def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier): (FunctionRecorder, Heap) = + def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) = (fr, h + ch) - protected def assumeUpperPermissionBoundForQPFields(s: State, @unused v: Verifier): State = s + protected def assumeUpperPermissionBoundForQPFields(s: State, @unused v: Verifier, @unused analysisInfos: DependencyAnalysisInfos): State = s - protected def assumeUpperPermissionBoundForQPFields(s: State, @unused heaps: Seq[Heap], @unused v: Verifier): State = s + protected def assumeUpperPermissionBoundForQPFields(s: State, @unused heaps: Seq[Heap], @unused v: Verifier, @unused analysisInfos: DependencyAnalysisInfos): State = s } /** Default implementation that merges as many known-alias chunks as possible, and deduces various @@ -57,8 +59,11 @@ class MinimalStateConsolidator extends StateConsolidationRules { */ class DefaultStateConsolidator(protected val config: Config) extends StateConsolidationRules { def consolidate(s: State, v: Verifier): State = { + if (v.decider.isPathMarkedInfeasible) return s + val comLog = new CommentRecord("state consolidation", s, v.decider.pcs) val sepIdentifier = v.symbExLog.openScope(comLog) + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo.createUnique("state consolidation"), AssumptionType.Internal) v.decider.prover.comment("[state consolidation]") v.decider.prover.saturate(config.proverSaturationTimeouts.beforeIteration) @@ -78,9 +83,9 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol val roundLog = new CommentRecord("Round " + fixedPointRound, s, v.decider.pcs) val roundSepIdentifier = v.symbExLog.openScope(roundLog) - val (_functionRecorder, _mergedChunks, _, snapEqs) = singleMerge(functionRecorder, destChunks, newChunks, s.functionRecorderQuantifiedVariables().map(_._1), v) + val (_functionRecorder, _mergedChunks, _newChunks, snapEqs) = singleMerge(functionRecorder, destChunks, newChunks, s.functionRecorderQuantifiedVariables().map(_._1), v, analysisInfos) - snapEqs foreach (t => v.decider.assume(t, Option.when(withExp)(DebugExp.createInstance("Snapshot Equations", true)))) + snapEqs foreach (t => v.decider.assume(t, Option.when(withExp)(DebugExp.createInstance("Snapshot Equations", true)), analysisInfos)) functionRecorder = _functionRecorder mergedChunks = _mergedChunks @@ -98,12 +103,12 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol mergedChunks.filter(_.isInstanceOf[BasicChunk]) foreach { case ch: BasicChunk => val resource = Resources.resourceDescriptions(ch.resourceID) val pathCond = interpreter.buildPathConditionsForChunk(ch, resource.instanceProperties(s.mayAssumeUpperBounds)) - pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)))) + pathCond.foreach(p => v.decider.assume(DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v.decider, p._1, Set(ch)), Option.when(withExp)(DebugExp.createInstance(p._2, p._2)), analysisInfos)) } Resources.resourceDescriptions foreach { case (id, desc) => val pathCond = interpreter.buildPathConditionsForResource(id, desc.delayedProperties(s.mayAssumeUpperBounds)) - pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)))) + pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)), analysisInfos)) } v.symbExLog.closeScope(sepIdentifier) @@ -114,8 +119,7 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol h = mergedHeaps.head, reserveHeaps = mergedHeaps.tail) - val s2 = assumeUpperPermissionBoundForQPFields(s1, v) - + val s2 = assumeUpperPermissionBoundForQPFields(s1, v, analysisInfos) s2 } @@ -123,22 +127,26 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol if (s.retrying) consolidate(s, v) else s - def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier): (FunctionRecorder, Heap) = { - merge(fr, s, h, Heap(Seq(ch)), v) + def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) = { + merge(fr, s, h, Heap(Seq(ch)), v, analysisInfos) } - def merge(fr1: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier): (FunctionRecorder, Heap) = { + def merge(fr1: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) = { + if (v.decider.isPathMarkedInfeasible) return (fr1, h) + + val analysisInfos1 = analysisInfos.addInfo("merge", ast.NoPosition, AssumptionType.Internal.asDepType()) + val mergeLog = new CommentRecord("Merge", null, v.decider.pcs) val sepIdentifier = v.symbExLog.openScope(mergeLog) - val (fr2, mergedChunks, newlyAddedChunks, snapEqs) = singleMerge(fr1, h.values.toSeq, newH.values.toSeq, s.functionRecorderQuantifiedVariables().map(_._1), v) + val (fr2, mergedChunks, newlyAddedChunks, snapEqs) = singleMerge(fr1, h.values.toSeq, newH.values.toSeq, s.functionRecorderQuantifiedVariables().map(_._1), v, analysisInfos1) - v.decider.assume(snapEqs, Option.when(withExp)(DebugExp.createInstance("Snapshot", isInternal_ = true)), enforceAssumption = false) + v.decider.assume(snapEqs, Option.when(withExp)(DebugExp.createInstance("Snapshot", isInternal_ = true)), enforceAssumption = false, analysisInfos1.withDependencyType(AssumptionType.Internal)) val interpreter = new NonQuantifiedPropertyInterpreter(mergedChunks, v) newlyAddedChunks.filter(_.isInstanceOf[BasicChunk]) foreach { case ch: BasicChunk => val resource = Resources.resourceDescriptions(ch.resourceID) val pathCond = interpreter.buildPathConditionsForChunk(ch, resource.instanceProperties(s.mayAssumeUpperBounds)) - pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)))) + pathCond.foreach(p => v.decider.assume(p._1, Option.when(withExp)(DebugExp.createInstance(p._2, p._2)), analysisInfos1.withDependencyType(AssumptionType.Internal))) } v.symbExLog.closeScope(sepIdentifier) @@ -149,7 +157,8 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol destChunks: Seq[Chunk], newChunks: Seq[Chunk], qvars: Seq[Var], - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : (FunctionRecorder, Seq[Chunk], Seq[Chunk], @@ -168,10 +177,12 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol * nextChunk: current chunk from the sequence of new chunks/of chunks to merge into the * sequence of destination chunks */ - - findMatchingChunk(accMergedChunks, nextChunk, v) match { + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo.createUnique("state_consolidation"), AssumptionType.Internal) + findMatchingChunk(accMergedChunks, nextChunk, v, analysisInfos) match { case Some(ch) => - mergeChunks(fr1, ch, nextChunk, qvars, v) match { + val resMerge = mergeChunks(fr1, ch, nextChunk, qvars, v, analysisInfos) + + resMerge match { case Some((fr2, newChunk, snapEq)) => (fr2, newChunk +: accMergedChunks.filterNot(_ == ch), newChunk +: accNewChunks, accSnapEqs + snapEq) case None => @@ -185,22 +196,27 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol result } - private def findMatchingChunk(chunks: Iterable[Chunk], chunk: Chunk, v: Verifier): Option[Chunk] = { + private def findMatchingChunk(chunks: Iterable[Chunk], chunk: Chunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): Option[Chunk] = { chunk match { case chunk: BasicChunk => - chunkSupporter.findChunk[BasicChunk](chunks, chunk.id, chunk.args, v) - case chunk: QuantifiedChunk => quantifiedChunkSupporter.findChunk(chunks, chunk, v) + chunkSupporter.findChunk[BasicChunk](chunks, chunk.id, chunk.args, v, analysisInfos) + case chunk: QuantifiedChunk => quantifiedChunkSupporter.findChunk(chunks, chunk, v, analysisInfos) case _ => None } } // Merges two chunks that are aliases (i.e. that have the same id and the args are proven to be equal) // and returns the merged chunk or None, if the chunks could not be merged - private def mergeChunks(fr1: FunctionRecorder, chunk1: Chunk, chunk2: Chunk, qvars: Seq[Var], v: Verifier): Option[(FunctionRecorder, Chunk, Term)] = (chunk1, chunk2) match { + private def mergeChunks(fr1: FunctionRecorder, chunk1: Chunk, chunk2: Chunk, qvars: Seq[Var], v: Verifier, analysisInfos: DependencyAnalysisInfos): Option[(FunctionRecorder, Chunk, Term)] = { + val result = mergeChunks1(fr1, chunk1, chunk2, qvars, v, analysisInfos) + result.map({case (fRec, ch, snapEq) => + (fRec, ch, DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v.decider, snapEq, Set(chunk1, chunk2)))}) + } + + private def mergeChunks1(fr1: FunctionRecorder, chunk1: Chunk, chunk2: Chunk, qvars: Seq[Var], v: Verifier, analysisInfos: DependencyAnalysisInfos): Option[(FunctionRecorder, Chunk, Term)] = (chunk1, chunk2) match { case (BasicChunk(rid1, id1, args1, args1Exp, snap1, snap1Exp, perm1, perm1Exp), BasicChunk(_, _, _, _, snap2, _, perm2, perm2Exp)) => val (fr2, combinedSnap, snapEq) = combineSnapshots(fr1, snap1, snap2, perm1, perm2, qvars, v) - - Some(fr2, BasicChunk(rid1, id1, args1, args1Exp, combinedSnap, snap1Exp, PermPlus(perm1, perm2), perm1Exp.map(p1 => ast.PermAdd(p1, perm2Exp.get)())), snapEq) + Some(fr2, v.chunkFactory.createBasicChunk(rid1, id1, args1, args1Exp, combinedSnap, snap1Exp, PermPlus(perm1, perm2), perm1Exp.map(p1 => ast.PermAdd(p1, perm2Exp.get)()), analysisInfos.withDependencyType(AssumptionType.Internal)), snapEq) case (l@QuantifiedFieldChunk(id1, fvf1, condition1, condition1Exp, perm1, perm1Exp, invs1, singletonRcvr1, singletonRcvr1Exp, hints1), r@QuantifiedFieldChunk(_, fvf2, _, _, perm2, perm2Exp, _, _, _, hints2)) => assert(l.quantifiedVars == Seq(`?r`)) @@ -210,14 +226,14 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol val permSum = PermPlus(perm1, perm2) val permSumExp = perm1Exp.map(p1 => ast.PermAdd(p1, perm2Exp.get)()) val bestHints = if (hints1.nonEmpty) hints1 else hints2 - Some(fr2, QuantifiedFieldChunk(id1, combinedSnap, condition1, condition1Exp, permSum, permSumExp, invs1, singletonRcvr1, singletonRcvr1Exp, bestHints), snapEq) + Some(fr2, v.chunkFactory.createQuantifiedFieldChunk(id1, combinedSnap, condition1, condition1Exp, permSum, permSumExp, invs1, singletonRcvr1, singletonRcvr1Exp, bestHints, analysisInfos.withDependencyType(AssumptionType.Internal)), snapEq) case (l@QuantifiedPredicateChunk(id1, qVars1, qVars1Exp, psf1, _, _, perm1, perm1Exp, _, _, _, _), r@QuantifiedPredicateChunk(_, qVars2, qVars2Exp, psf2, condition2, condition2Exp, perm2, perm2Exp, invs2, singletonArgs2, singletonArgs2Exp, hints2)) => val (fr2, combinedSnap, snapEq) = quantifiedChunkSupporter.combinePredicateSnapshotMaps(fr1, id1.name, qVars2, qvars, psf1, psf2, l.perm.replace(qVars1, qVars2), r.perm, v) val permSum = PermPlus(perm1.replace(qVars1, qVars2), perm2) val permSumExp = perm1Exp.map(p1 => ast.PermAdd(p1.replace(qVars1Exp.get.zip(qVars2Exp.get).toMap), perm2Exp.get)()) - Some(fr2, QuantifiedPredicateChunk(id1, qVars2, qVars2Exp, combinedSnap, condition2, condition2Exp, permSum, permSumExp, invs2, singletonArgs2, singletonArgs2Exp, hints2), snapEq) + Some(fr2, v.chunkFactory.createQuantifiedPredicateChunk(id1, qVars2, qVars2Exp, combinedSnap, condition2, condition2Exp, permSum, permSumExp, invs2, singletonArgs2, singletonArgs2Exp, hints2, analysisInfos.withDependencyType(AssumptionType.Internal)), snapEq) case _ => None } @@ -250,10 +266,10 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol } } - protected def assumeUpperPermissionBoundForQPFields(s: State, v: Verifier): State = - assumeUpperPermissionBoundForQPFields(s, s.h +: s.reserveHeaps, v) + protected def assumeUpperPermissionBoundForQPFields(s: State, v: Verifier, analysisInfos: DependencyAnalysisInfos): State = + assumeUpperPermissionBoundForQPFields(s, s.h +: s.reserveHeaps, v, analysisInfos) - protected def assumeUpperPermissionBoundForQPFields(s: State, heaps: Seq[Heap], v: Verifier): State = { + protected def assumeUpperPermissionBoundForQPFields(s: State, heaps: Seq[Heap], v: Verifier, analysisInfos: DependencyAnalysisInfos): State = { heaps.foldLeft(s) { case (si, heap) => val chunks: Seq[QuantifiedFieldChunk] = heap.values.collect({ case ch: QuantifiedFieldChunk => ch }).to(Seq) @@ -285,7 +301,7 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol Some(DebugExp.createInstance(exp, exp)) } else { None } v.decider.assume( - Forall(receiver, PermAtMost(currentPermAmount, FullPerm), Trigger(trigger), "qp-fld-prm-bnd"), debugExp) + Forall(receiver, PermAtMost(currentPermAmount, FullPerm), Trigger(trigger), "qp-fld-prm-bnd"), debugExp, analysisInfos.withDependencyType(AssumptionType.Internal)) } else { /* If we don't use heap-dependent triggers, the trigger x.f does not work. Instead, we assume the permission @@ -302,7 +318,7 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol val exp = ast.PermLeCmp(permExp, ast.FullPerm()())() Some(DebugExp.createInstance(exp, exp)) } else { None } - v.decider.assume(PermAtMost(PermLookup(field.name, pmDef.pm, chunk.singletonRcvr.get), FullPerm), debugExp) + v.decider.assume(PermAtMost(PermLookup(field.name, pmDef.pm, chunk.singletonRcvr.get), FullPerm), debugExp, analysisInfos.withDependencyType(AssumptionType.Internal)) } else { val chunkReceivers = chunk.invs.get.inverses.map(i => App(i, chunk.invs.get.additionalArguments ++ chunk.quantifiedVars)) val triggers = chunkReceivers.map(r => Trigger(r)).toSeq @@ -318,7 +334,7 @@ class DefaultStateConsolidator(protected val config: Config) extends StateConsol Some(DebugExp.createInstance(exp, exp)) } else { None } v.decider.assume( - Forall(chunk.quantifiedVars, PermAtMost(currentPermAmount, FullPerm), triggers, "qp-fld-prm-bnd"), debugExp) + Forall(chunk.quantifiedVars, PermAtMost(currentPermAmount, FullPerm), triggers, "qp-fld-prm-bnd"), debugExp, analysisInfos.withDependencyType(AssumptionType.Internal)) } } @@ -390,15 +406,15 @@ class LastRetryFailOnlyStateConsolidator(config: Config) extends LastRetryStateC * - Merging heaps and assuming QP permission bounds is equivalent to [[MinimalStateConsolidator]] */ class MinimalRetryingStateConsolidator(config: Config) extends RetryingStateConsolidator(config) { - override def merge(fr: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier): (FunctionRecorder, Heap) = + override def merge(fr: FunctionRecorder, s: State, h: Heap, newH: Heap, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) = (fr, Heap(h.values ++ newH.values)) - override def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier): (FunctionRecorder, Heap) = + override def merge(fr: FunctionRecorder, s: State, h: Heap, ch: NonQuantifiedChunk, v: Verifier, analysisInfos: DependencyAnalysisInfos): (FunctionRecorder, Heap) = (fr, h + ch) - override protected def assumeUpperPermissionBoundForQPFields(s: State, @unused v: Verifier): State = s + override protected def assumeUpperPermissionBoundForQPFields(s: State, @unused v: Verifier, analysisInfos: DependencyAnalysisInfos): State = s - override protected def assumeUpperPermissionBoundForQPFields(s: State, @unused heaps: Seq[Heap], @unused v: Verifier): State = s + override protected def assumeUpperPermissionBoundForQPFields(s: State, @unused heaps: Seq[Heap], @unused v: Verifier, analysisInfos: DependencyAnalysisInfos): State = s } /** A variant of [[DefaultStateConsolidator]] that aims to work best when Silicon is run in @@ -418,10 +434,12 @@ class MoreComplexExhaleStateConsolidator(config: Config) extends DefaultStateCon // silver\src\test\resources\quantifiedpermissions\sets\generalised_shape.sil // to fail. + val analysisInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo.createUnique("state consolidation"), AssumptionType.Internal) + if (s.retrying) { // TODO: apply to all heaps (s.h +: s.reserveHeaps, as done below) // NOTE: Doing this regardless of s.retrying might improve completeness in certain (rare) cases - moreCompleteExhaleSupporter.assumeFieldPermissionUpperBounds(s.h, v) + moreCompleteExhaleSupporter.assumeFieldPermissionUpperBounds(s.h, v, analysisInfos) } s diff --git a/src/main/scala/state/Heap.scala b/src/main/scala/state/Heap.scala index 302de3dc3..09792ce09 100644 --- a/src/main/scala/state/Heap.scala +++ b/src/main/scala/state/Heap.scala @@ -6,7 +6,7 @@ package viper.silicon.state -import viper.silicon.interfaces.state.Chunk +import viper.silicon.state.chunks.Chunk trait Heap { def values: Iterable[Chunk] diff --git a/src/main/scala/state/SnapshotMapCache.scala b/src/main/scala/state/SnapshotMapCache.scala index 5672e453b..f2b5ec064 100644 --- a/src/main/scala/state/SnapshotMapCache.scala +++ b/src/main/scala/state/SnapshotMapCache.scala @@ -6,7 +6,9 @@ package viper.silicon.state +import viper.silicon.state.chunks.QuantifiedBasicChunk import viper.silver.ast + import scala.collection.immutable.HashMap import viper.silicon.rules.SnapshotMapDefinition import viper.silicon.state.terms.Term diff --git a/src/main/scala/state/State.scala b/src/main/scala/state/State.scala index 22043c79a..bb4a12929 100644 --- a/src/main/scala/state/State.scala +++ b/src/main/scala/state/State.scala @@ -6,24 +6,23 @@ package viper.silicon.state +import viper.silicon.state.chunks.{Chunk, GeneralChunk, MagicWandIdentifier} import viper.silicon.Config.JoinMode import viper.silicon.Config.JoinMode.JoinMode -import viper.silver.ast -import viper.silver.cfg.silver.SilverCfg import viper.silicon.common.Mergeable import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.decider.RecordedPathConditions -import viper.silicon.interfaces.state.GeneralChunk +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.state.State.OldHeaps -import viper.silicon.state.terms.{Term, Var} -import viper.silicon.interfaces.state.Chunk import viper.silicon.state.terms.predef.`?r` -import viper.silicon.state.terms.{And, Ite} +import viper.silicon.state.terms.{And, Ite, Term, Var} import viper.silicon.supporters.PredicateData import viper.silicon.supporters.functions.{FunctionData, FunctionRecorder, NoopFunctionRecorder} import viper.silicon.utils.ast.BigAnd import viper.silicon.verifier.Verifier import viper.silicon.{Map, Stack} +import viper.silver.ast +import viper.silver.cfg.silver.SilverCfg import viper.silver.utility.Sanitizer final case class State(g: Store = Store(), @@ -326,33 +325,32 @@ object State { } // Puts a collection of chunks under a condition. - private def conditionalizeChunks(h: Iterable[Chunk], cond: Term, condExp: Option[ast.Exp]): Iterable[Chunk] = { + private def conditionalizeChunks(h: Iterable[Chunk], cond: Term, condExp: Option[ast.Exp], v: Verifier, analysisInfos: DependencyAnalysisInfos): Iterable[Chunk] = { h map (c => { c match { - case c: GeneralChunk => - c.applyCondition(cond, condExp) + case c: GeneralChunk => v.chunkFactory.applyCondition(c, cond, condExp, analysisInfos) case _ => sys.error("Chunk type not conditionalizable.") } }) } // Puts a heap under a condition. - private def conditionalizeHeap(h: Heap, cond: Term, condExp: Option[ast.Exp]): Heap = { - Heap(conditionalizeChunks(h.values, cond, condExp)) + private def conditionalizeHeap(h: Heap, cond: Term, condExp: Option[ast.Exp], v: Verifier, analysisInfos: DependencyAnalysisInfos): Heap = { + Heap(conditionalizeChunks(h.values, cond, condExp, v, analysisInfos)) } // Merges two heaps together, by putting h1 under condition cond1, // and h2 under cond2. // Assumes that cond1 is the negation of cond2. - def mergeHeap(h1: Heap, cond1: Term, cond1Exp: Option[ast.Exp], h2: Heap, cond2: Term, cond2Exp: Option[ast.Exp]): Heap = { + def mergeHeap(h1: Heap, cond1: Term, cond1Exp: Option[ast.Exp], h2: Heap, cond2: Term, cond2Exp: Option[ast.Exp], v: Verifier, analysisInfos: DependencyAnalysisInfos): Heap = { val (unconditionalHeapChunks, h1HeapChunksToConditionalize) = h1.values.partition(c1 => h2.values.exists(_ == c1)) val h2HeapChunksToConditionalize = h2.values.filter(c2 => !unconditionalHeapChunks.exists(_ == c2)) - val h1ConditionalizedHeapChunks = conditionalizeChunks(h1HeapChunksToConditionalize, cond1, cond1Exp) - val h2ConditionalizedHeapChunks = conditionalizeChunks(h2HeapChunksToConditionalize, cond2, cond2Exp) + val h1ConditionalizedHeapChunks = conditionalizeChunks(h1HeapChunksToConditionalize, cond1, cond1Exp, v, analysisInfos) + val h2ConditionalizedHeapChunks = conditionalizeChunks(h2HeapChunksToConditionalize, cond2, cond2Exp, v, analysisInfos) Heap(unconditionalHeapChunks) + Heap(h1ConditionalizedHeapChunks) + Heap(h2ConditionalizedHeapChunks) } - def merge(s1: State, pc1: RecordedPathConditions, s2: State, pc2: RecordedPathConditions): State = { + def merge(s1: State, pc1: RecordedPathConditions, s2: State, pc2: RecordedPathConditions, v: Verifier, analysisInfos: DependencyAnalysisInfos): State = { s1 match { /* Decompose state s1 */ case State(g1, h1, program, member, @@ -433,15 +431,17 @@ object State { val g3 = mergeStore(g1, g2) - val h3 = mergeHeap(h1, conditions1, conditions1Exp, h2, conditions2, conditions2Exp) + val h3 = mergeHeap(h1, conditions1, conditions1Exp, h2, conditions2, conditions2Exp, v, analysisInfos) val partiallyConsumedHeap3 = (partiallyConsumedHeap1, partiallyConsumedHeap2) match { case (None, None) => None - case (Some(pch1), None) => Some(conditionalizeHeap(pch1, conditions1, conditions1Exp)) - case (None, Some(pch2)) => Some(conditionalizeHeap(pch2, conditions2, conditions2Exp)) + case (Some(pch1), None) => Some(conditionalizeHeap(pch1, conditions1, conditions1Exp, v, analysisInfos)) + case (None, Some(pch2)) => Some(conditionalizeHeap(pch2, conditions2, conditions2Exp, v, analysisInfos)) case (Some(pch1), Some(pch2)) => Some(mergeHeap( pch1, conditions1, conditions1Exp, pch2, conditions2, conditions2Exp, + v, + analysisInfos )) } @@ -450,18 +450,18 @@ object State { None }) ((heap1, cond1, heap2, cond2) => { - Some(mergeHeap(heap1, cond1._1, cond1._2, heap2, cond2._1, cond2._2)) + Some(mergeHeap(heap1, cond1._1, cond1._2, heap2, cond2._1, cond2._2, v, analysisInfos)) })) assert(invariantContexts1.length == invariantContexts2.length) val invariantContexts3 = invariantContexts1 .zip(invariantContexts2) - .map({case (h1, h2) => mergeHeap(h1, conditions1, conditions1Exp, h2, conditions2, conditions2Exp)}) + .map({case (h1, h2) => mergeHeap(h1, conditions1, conditions1Exp, h2, conditions2, conditions2Exp, v, analysisInfos)}) assert(reserveHeaps1.length == reserveHeaps2.length) val reserveHeaps3 = reserveHeaps1 .zip(reserveHeaps2) - .map({case (h1, h2) => mergeHeap(h1, conditions1, conditions1Exp, h2, conditions2, conditions2Exp)}) + .map({case (h1, h2) => mergeHeap(h1, conditions1, conditions1Exp, h2, conditions2, conditions2Exp, v, analysisInfos)}) assert(conservedPcs1.length == conservedPcs2.length) diff --git a/src/main/scala/state/Terms.scala b/src/main/scala/state/Terms.scala index e3b3dcad9..71f6dbf7b 100644 --- a/src/main/scala/state/Terms.scala +++ b/src/main/scala/state/Terms.scala @@ -6,17 +6,18 @@ package viper.silicon.state.terms -import java.util.concurrent.atomic.AtomicInteger -import scala.annotation.tailrec -import scala.reflect.ClassTag -import viper.silver.ast import viper.silicon.common.collections.immutable.InsertionOrderedSet -import viper.silicon.{Map, Stack, state, toMap} -import viper.silicon.state.{Identifier, MagicWandChunk, MagicWandIdentifier, SortBasedIdentifier} +import viper.silicon.state.chunks.{MagicWandChunk, MagicWandIdentifier} +import viper.silicon.state.{Identifier, SortBasedIdentifier} import viper.silicon.verifier.Verifier +import viper.silicon.{Map, Stack, state, toMap} +import viper.silver.ast import viper.silver.utility.Common.Rational +import java.util.concurrent.atomic.AtomicInteger +import scala.annotation.tailrec import scala.collection.concurrent.TrieMap +import scala.reflect.ClassTag sealed trait Node { def toString: String @@ -874,7 +875,7 @@ class Times(val p0: Term, val p1: Term) extends ArithmeticTerm } object Times extends CondFlyweightTermFactory[(Term, Term), Times] { - import predef.{Zero, One} + import predef.{One, Zero} override def apply(v0: (Term, Term)) =v0 match { case (_, Zero) => Zero @@ -1331,7 +1332,7 @@ class IntPermTimes private[terms] (val p0: Term, val p1: Term) } object IntPermTimes extends CondFlyweightTermFactory[(Term, Term), IntPermTimes] { - import predef.{Zero, One} + import predef.{One, Zero} override def apply(v0: (Term, Term)) = v0 match { case (Zero, _) => NoPerm diff --git a/src/main/scala/state/Utils.scala b/src/main/scala/state/Utils.scala index 642dbceb7..492f76e09 100644 --- a/src/main/scala/state/Utils.scala +++ b/src/main/scala/state/Utils.scala @@ -6,6 +6,8 @@ package viper.silicon.state +import viper.silicon.state.chunks.{BasicChunk, QuantifiedFieldChunk} + import scala.collection.mutable import viper.silicon.state.terms._ import viper.silver.ast diff --git a/src/main/scala/state/chunks/ChunkFactory.scala b/src/main/scala/state/chunks/ChunkFactory.scala new file mode 100644 index 000000000..b98dc2425 --- /dev/null +++ b/src/main/scala/state/chunks/ChunkFactory.scala @@ -0,0 +1,196 @@ +// 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.silicon.state.chunks + +import viper.silicon +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos +import viper.silicon.resources.BaseID +import viper.silicon.rules.InverseFunctions +import viper.silicon.state.terms.{MagicWandSnapshot, Term, Var} +import viper.silver.ast + +trait ChunkFactory { + def createBasicChunk(resourceID: BaseID, + id: BasicChunkIdentifier, + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: Term, + snapExp: Option[ast.Exp], + perm: Term, + permExp: Option[ast.Exp], + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): BasicChunk + + def createQuantifiedFieldChunk(id: BasicChunkIdentifier, + fvf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonRcvr: Option[Term], + singletonRcvrExp: Option[ast.Exp], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedFieldChunk + + def createQuantifiedPredicateChunk(id: BasicChunkIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + psf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedPredicateChunk + + def createQuantifiedMagicWandChunk(id: MagicWandIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + wsf: Term, + perm: Term, + permExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedMagicWandChunk + + def createMagicWandChunk(id: MagicWandIdentifier, + bindings: Map[ast.AbstractLocalVar, (Term, Option[ast.Exp])], + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: MagicWandSnapshot, + perm: Term, + permExp: Option[ast.Exp], + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): MagicWandChunk + + def applyCondition(chunk: GeneralChunk, newCond: Term, newCondExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): GeneralChunk + + def permMinus(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): GeneralChunk + def permMinus(chunk: QuantifiedBasicChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): QuantifiedBasicChunk + + def permPlus(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk + + def withPerm(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk + + def withPermNonQuantifiedChunk(chunk: NonQuantifiedChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): NonQuantifiedChunk + + def permScale(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk + + def substitute(chunk: GeneralChunk, terms: silicon.Map[Term, Term], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk + + def withSnapshotMap(chunk: QuantifiedChunk, snap: Term, analysisInfos: DependencyAnalysisInfos): QuantifiedChunk + + def withSnap(chunk: NonQuantifiedChunk, snap: Term, snapExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): NonQuantifiedChunk + +} + +class DefaultChunkFactory extends ChunkFactory { + override def createBasicChunk(resourceID: BaseID, + id: BasicChunkIdentifier, + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: Term, + snapExp: Option[ast.Exp], + perm: Term, + permExp: Option[ast.Exp], + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): BasicChunk = + BasicChunk(resourceID, id, args, argsExp, snap, snapExp, perm, permExp) + + override def createQuantifiedFieldChunk(id: BasicChunkIdentifier, + fvf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonRcvr: Option[Term], + singletonRcvrExp: Option[ast.Exp], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedFieldChunk = + QuantifiedFieldChunk(id, fvf, condition, conditionExp, permValue, permValueExp, invs, singletonRcvr, singletonRcvrExp, hints) + + override def createQuantifiedPredicateChunk(id: BasicChunkIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + psf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedPredicateChunk = + QuantifiedPredicateChunk(id, quantifiedVars, quantifiedVarExps, psf, condition, conditionExp, permValue, permValueExp, invs, singletonArgs, singletonArgExps, hints) + + override def createQuantifiedMagicWandChunk(id: MagicWandIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + wsf: Term, + perm: Term, + permExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedMagicWandChunk = + QuantifiedMagicWandChunk(id, quantifiedVars, quantifiedVarExps, wsf, perm, permExp, invs, singletonArgs, singletonArgExps, hints) + + override def createMagicWandChunk(id: MagicWandIdentifier, + bindings: Map[ast.AbstractLocalVar, (Term, Option[ast.Exp])], + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: MagicWandSnapshot, + perm: Term, + permExp: Option[ast.Exp], + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): MagicWandChunk = + MagicWandChunk(id, bindings, args, argsExp, snap, perm, permExp) + + def applyCondition(chunk: GeneralChunk, newCond: Term, newCondExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): GeneralChunk = + chunk.applyCondition(newCond, newCondExp) + + def permMinus(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): GeneralChunk = + chunk.permMinus(newPerm, newPermExp) + + def permMinus(chunk: QuantifiedBasicChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): QuantifiedBasicChunk = + chunk.permMinus(newPerm, newPermExp) + + def permPlus(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = + chunk.permPlus(newPerm, newPermExp) + + def withPerm(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = + chunk.withPerm(newPerm, newPermExp) + + def withPermNonQuantifiedChunk(chunk: NonQuantifiedChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): NonQuantifiedChunk = + chunk.withPerm(newPerm, newPermExp) + + def permScale(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = + chunk.permScale(newPerm, newPermExp) + + def substitute(chunk: GeneralChunk, terms: silicon.Map[Term, Term], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = + chunk.substitute(terms) + + def withSnapshotMap(chunk: QuantifiedChunk, snap: Term, analysisInfos: DependencyAnalysisInfos): QuantifiedChunk = chunk.withSnapshotMap(snap) + + def withSnap(chunk: NonQuantifiedChunk, snap: Term, snapExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): NonQuantifiedChunk = chunk.withSnap(snap, snapExp) + +} diff --git a/src/main/scala/state/Chunks.scala b/src/main/scala/state/chunks/Chunks.scala similarity index 63% rename from src/main/scala/state/Chunks.scala rename to src/main/scala/state/chunks/Chunks.scala index a4942f237..b92b309d1 100644 --- a/src/main/scala/state/Chunks.scala +++ b/src/main/scala/state/chunks/Chunks.scala @@ -4,16 +4,16 @@ // // Copyright (c) 2011-2019 ETH Zurich. -package viper.silicon.state +package viper.silicon.state.chunks import viper.silicon -import viper.silver.ast -import viper.silicon.interfaces.state._ import viper.silicon.resources._ import viper.silicon.rules.InverseFunctions +import viper.silicon.state.terms import viper.silicon.state.terms._ import viper.silicon.state.terms.predef.`?r` import viper.silicon.verifier.Verifier +import viper.silver.ast object ChunkIdentifier { def apply(from: ast.Resource, program: ast.Program): ChunkIdentifer = { @@ -29,7 +29,18 @@ case class BasicChunkIdentifier(name: String) extends ChunkIdentifer { override def toString = name } -case class BasicChunk(resourceID: BaseID, +object BasicChunk { + private[chunks] def apply(resourceID: BaseID, + id: BasicChunkIdentifier, + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: Term, + snapExp: Option[ast.Exp], + perm: Term, + permExp: Option[ast.Exp]): BasicChunk = new BasicChunk(resourceID, id, args, argsExp, snap, snapExp, perm, permExp) +} + +case class BasicChunk private[chunks] (resourceID: BaseID, id: BasicChunkIdentifier, args: Seq[Term], argsExp: Option[Seq[ast.Exp]], @@ -45,45 +56,61 @@ case class BasicChunk(resourceID: BaseID, case PredicateID => require(snap.sort == sorts.Snap, s"A predicate chunk's snapshot ($snap) is expected to be of sort Snap, but found ${snap.sort}") } - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = + + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): BasicChunk = withPerm(Ite(newCond, perm, NoPerm), newCondExp.map(nce => ast.CondExp(nce, permExp.get, ast.NoPerm()())())) - override def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]): BasicChunk = withPerm(PermMinus(perm, newPerm), newPermExp.map(npe => ast.PermSub(permExp.get, npe)())) - override def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]): BasicChunk = withPerm(PermPlus(perm, newPerm), newPermExp.map(npe => ast.PermAdd(permExp.get, npe)())) - override def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermTimes(perm, newPerm), permExp.map(pe => ast.PermMul(pe, newPermExp.get)())) - override def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = BasicChunk(resourceID, id, args, argsExp, snap, snapExp, newPerm, newPermExp) - override def withSnap(newSnap: Term, newSnapExp: Option[ast.Exp]) = BasicChunk(resourceID, id, args, argsExp, newSnap, newSnapExp, perm, permExp) + override protected[chunks] def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]): BasicChunk = BasicChunk(resourceID, id, args, argsExp, snap, snapExp, newPerm, newPermExp) + override protected[chunks] def withSnap(newSnap: Term, newSnapExp: Option[ast.Exp]): BasicChunk = BasicChunk(resourceID, id, args, argsExp, newSnap, newSnapExp, perm, permExp) override lazy val toString = resourceID match { case FieldID => s"${args.head}.$id -> $snap # $perm" case PredicateID => s"$id($snap; ${args.mkString(",")}) # $perm" } - override def substitute(terms: silicon.Map[Term, Term]) = { + override protected[chunks] def substitute(terms: silicon.Map[Term, Term]): BasicChunk = { copy(args = args.map(_.replace(terms)), snap = snap.replace(terms), perm = perm.replace(terms)) } } sealed trait QuantifiedBasicChunk extends QuantifiedChunk { override val id: ChunkIdentifer - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): QuantifiedBasicChunk - override def permMinus(perm: Term, permExp: Option[ast.Exp]): QuantifiedBasicChunk - override def permPlus(perm: Term, permExp: Option[ast.Exp]): QuantifiedBasicChunk - override def withSnapshotMap(snap: Term): QuantifiedBasicChunk + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): QuantifiedBasicChunk + override protected[chunks] def permMinus(perm: Term, permExp: Option[ast.Exp]): QuantifiedBasicChunk + override protected[chunks] def permPlus(perm: Term, permExp: Option[ast.Exp]): QuantifiedBasicChunk + override protected[chunks] def withSnapshotMap(snap: Term): QuantifiedBasicChunk def singletonArguments: Option[Seq[Term]] def singletonArgumentExps: Option[Seq[ast.Exp]] def hints: Seq[Term] } + +object QuantifiedFieldChunk { + private[chunks] def apply(id: BasicChunkIdentifier, + fvf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonRcvr: Option[Term], + singletonRcvrExp: Option[ast.Exp], + hints: Seq[Term] = Nil): QuantifiedFieldChunk = + new QuantifiedFieldChunk(id, fvf, condition, conditionExp, permValue, permValueExp, invs, singletonRcvr, singletonRcvrExp, hints) +} + /* TODO: Instead of using the singletonRcvr to differentiate between QP chunks that * provide permissions to a single location and those providing permissions * to potentially multiple locations, consider using regular, non-quantified * chunks instead. */ -case class QuantifiedFieldChunk(id: BasicChunkIdentifier, +case class QuantifiedFieldChunk protected[chunks] (id: BasicChunkIdentifier, fvf: Term, condition: Term, conditionExp: Option[ast.Exp], @@ -118,28 +145,44 @@ case class QuantifiedFieldChunk(id: BasicChunkIdentifier, Lookup(id.name, fvf, arguments.head) } - def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = QuantifiedFieldChunk(id, fvf, condition, conditionExp, newPerm, newPermExp, invs, singletonRcvr, singletonRcvrExp, hints) - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = QuantifiedFieldChunk(id, fvf, terms.And(newCond, condition), newCondExp.map(nce => ast.And(nce, conditionExp.get)()), permValue, permValueExp, invs, singletonRcvr, singletonRcvrExp, hints) - override def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermMinus(permValue, newPerm), newPermExp.map(npe => ast.PermSub(permValueExp.get, npe)())) - override def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermPlus(permValue, newPerm), newPermExp.map(npe => ast.PermAdd(permValueExp.get, npe)())) - override def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermTimes(perm, newPerm), permExp.map(pe => ast.PermMul(pe, newPermExp.get)())) - override def withSnapshotMap(newFvf: Term) = + override protected[chunks] def withSnapshotMap(newFvf: Term) = QuantifiedFieldChunk(id, newFvf, condition, conditionExp, permValue, permValueExp, invs, singletonRcvr, singletonRcvrExp, hints) override lazy val toString = s"${terms.Forall} ${`?r`} :: ${`?r`}.$id -> $fvf # $perm" - override def substitute(terms: silicon.Map[Term, Term]): Chunk = + override protected[chunks] def substitute(terms: silicon.Map[Term, Term]): QuantifiedFieldChunk = copy(fvf = fvf.replace(terms), condition = condition.replace(terms), permValue = permValue.replace(terms), singletonRcvr = singletonRcvr.map(_.replace(terms)), hints = hints.map(_.replace(terms)), invs = invs.map(_.substitute(terms))) } -case class QuantifiedPredicateChunk(id: BasicChunkIdentifier, +object QuantifiedPredicateChunk { + private[chunks] def apply(id: BasicChunkIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + psf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil): QuantifiedPredicateChunk = + new QuantifiedPredicateChunk(id, quantifiedVars, quantifiedVarExps, psf, condition, conditionExp, permValue, permValueExp, invs, singletonArgs, singletonArgExps, hints) +} + +case class QuantifiedPredicateChunk protected[chunks] (id: BasicChunkIdentifier, quantifiedVars: Seq[Var], quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], psf: Term, @@ -166,28 +209,42 @@ case class QuantifiedPredicateChunk(id: BasicChunkIdentifier, override def valueAt(args: Seq[Term]) = PredicateLookup(id.name, psf, args) - def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = QuantifiedPredicateChunk(id, quantifiedVars, quantifiedVarExps, psf, condition, conditionExp, newPerm, newPermExp, invs, singletonArgs, singletonArgExps, hints) - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = QuantifiedPredicateChunk(id, quantifiedVars, quantifiedVarExps, psf, terms.And(newCond, condition), newCondExp.map(nce => ast.And(nce, conditionExp.get)()), permValue, permValueExp, invs, singletonArgs, singletonArgExps, hints) - override def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermMinus(permValue, newPerm), newPermExp.map(npe => ast.PermSub(permValueExp.get, npe)())) - override def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermPlus(permValue, newPerm), newPermExp.map(npe => ast.PermAdd(permValueExp.get, npe)())) - override def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermTimes(perm, newPerm), permExp.map(pe => ast.PermMul(pe, newPermExp.get)())) - override def withSnapshotMap(newPsf: Term) = + override protected[chunks] def withSnapshotMap(newPsf: Term) = QuantifiedPredicateChunk(id, quantifiedVars, quantifiedVarExps, newPsf, condition, conditionExp, permValue, permValueExp, invs, singletonArgs, singletonArgExps, hints) override lazy val toString = s"${terms.Forall} ${quantifiedVars.mkString(",")} :: $id(${quantifiedVars.mkString(",")}) -> $psf # $perm" - override def substitute(terms: silicon.Map[Term, Term]): Chunk = + override protected[chunks] def substitute(terms: silicon.Map[Term, Term]): QuantifiedPredicateChunk = copy(psf = psf.replace(terms), condition = condition.replace(terms), permValue = permValue.replace(terms), singletonArgs = singletonArgs.map(_.map(_.replace(terms))), hints = hints.map(_.replace(terms)), invs = invs.map(_.substitute(terms))) } -case class QuantifiedMagicWandChunk(id: MagicWandIdentifier, +object QuantifiedMagicWandChunk { + private[chunks] def apply(id: MagicWandIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + wsf: Term, + perm: Term, + permExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil): QuantifiedMagicWandChunk = + new QuantifiedMagicWandChunk(id, quantifiedVars, quantifiedVarExps, wsf, perm, permExp, invs, singletonArgs, singletonArgExps, hints) +} + +case class QuantifiedMagicWandChunk protected[chunks] (id: MagicWandIdentifier, quantifiedVars: Seq[Var], quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], wsf: Term, @@ -210,23 +267,23 @@ case class QuantifiedMagicWandChunk(id: MagicWandIdentifier, override def valueAt(args: Seq[Term]) = PredicateLookup(id.toString, wsf, args) - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = withPerm(Ite(newCond, perm, NoPerm), newCondExp.map(nce => ast.CondExp(nce, permExp.get, ast.NoPerm()())())) - override def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermMinus(perm, newPerm), newPermExp.map(npe => ast.PermSub(permExp.get, npe)())) - override def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermPlus(perm, newPerm), newPermExp.map(npe => ast.PermAdd(permExp.get, npe)())) - override def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermTimes(perm, newPerm), permExp.map(pe => ast.PermMul(pe, newPermExp.get)())) - def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = QuantifiedMagicWandChunk(id, quantifiedVars, quantifiedVarExps, wsf, newPerm, newPermExp, invs, singletonArgs, singletonArgExps, hints) - override def withSnapshotMap(newWsf: Term) = + override protected[chunks] def withSnapshotMap(newWsf: Term) = QuantifiedMagicWandChunk(id, quantifiedVars, quantifiedVarExps, newWsf, perm, permExp, invs, singletonArgs, singletonArgExps, hints) override lazy val toString = s"${terms.Forall} ${quantifiedVars.mkString(",")} :: $id(${quantifiedVars.mkString(",")}) -> $wsf # $perm" - override def substitute(terms: silicon.Map[Term, Term]): Chunk = + override protected[chunks] def substitute(terms: silicon.Map[Term, Term]): QuantifiedMagicWandChunk = copy(wsf = wsf.replace(terms), perm = perm.replace(terms), singletonArgs = singletonArgs.map(_.map(_.replace(terms))), hints = hints.map(_.replace(terms)), invs = invs.map(_.substitute(terms))) } @@ -248,7 +305,18 @@ object MagicWandIdentifier { } } -case class MagicWandChunk(id: MagicWandIdentifier, +object MagicWandChunk { + private[chunks] def apply(id: MagicWandIdentifier, + bindings: Map[ast.AbstractLocalVar, (Term, Option[ast.Exp])], + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: MagicWandSnapshot, + perm: Term, + permExp: Option[ast.Exp]): MagicWandChunk = + new MagicWandChunk(id, bindings, args, argsExp, snap, perm, permExp) +} + +case class MagicWandChunk protected[chunks] (id: MagicWandIdentifier, bindings: Map[ast.AbstractLocalVar, (Term, Option[ast.Exp])], args: Seq[Term], argsExp: Option[Seq[ast.Exp]], @@ -261,18 +329,18 @@ case class MagicWandChunk(id: MagicWandIdentifier, override val resourceID = MagicWandID - override def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]) = withPerm(Ite(newCond, perm, NoPerm), newCondExp.map(nce => ast.CondExp(nce, permExp.get, ast.NoPerm()())())) - override def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permMinus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermMinus(perm, newPerm), newPermExp.map(npe => ast.PermSub(permExp.get, npe)())) - override def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permPlus(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermPlus(perm, newPerm), newPermExp.map(npe => ast.PermAdd(permExp.get, npe)())) - override def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = + override protected[chunks] def permScale(newPerm: Term, newPermExp: Option[ast.Exp]) = withPerm(PermTimes(perm, newPerm), permExp.map(pe => ast.PermMul(pe, newPermExp.get)())) override def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]) = MagicWandChunk(id, bindings, args, argsExp, snap, newPerm, newPermExp) - override def withSnap(newSnap: Term, newSnapExp: Option[ast.Exp]) = { + override protected[chunks] def withSnap(newSnap: Term, newSnapExp: Option[ast.Exp]) = { assert(newSnapExp.isEmpty) newSnap match { case s: MagicWandSnapshot => MagicWandChunk(id, bindings, args, argsExp, s, perm, permExp) @@ -288,7 +356,7 @@ case class MagicWandChunk(id: MagicWandIdentifier, s"wand@$pos[$snap; ${args.mkString(", ")}]" } - override def substitute(terms: silicon.Map[Term, Term]) = { + override protected[chunks] def substitute(terms: silicon.Map[Term, Term]): MagicWandChunk = { copy(args = args.map(_.replace(terms)), snap = snap.replace(terms).asInstanceOf[MagicWandSnapshot], perm = perm.replace(terms)) } } diff --git a/src/main/scala/state/chunks/ChunksInterfaces.scala b/src/main/scala/state/chunks/ChunksInterfaces.scala new file mode 100644 index 000000000..24ad14a21 --- /dev/null +++ b/src/main/scala/state/chunks/ChunksInterfaces.scala @@ -0,0 +1,59 @@ +// 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-2019 ETH Zurich. + +package viper.silicon.state.chunks + +import viper.silicon +import viper.silicon.resources.ResourceID +import viper.silicon.state.terms.{Term, Var} +import viper.silver.ast + +trait Chunk { + val perm: Term + val permExp: Option[ast.Exp] + + protected[chunks] def substitute(terms: silicon.Map[Term, Term]): Chunk +} + +trait ChunkIdentifer + +trait GeneralChunk extends Chunk { + val resourceID: ResourceID + val id: ChunkIdentifer + + protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): GeneralChunk + protected[chunks] def permMinus(perm: Term, permExp: Option[ast.Exp]): GeneralChunk + protected[chunks] def permPlus(perm: Term, permExp: Option[ast.Exp]): GeneralChunk + protected[chunks] def withPerm(newPerm: Term, newPermExp: Option[ast.Exp]): GeneralChunk + + protected[chunks] def permScale(perm: Term, permExp: Option[ast.Exp]): GeneralChunk + + protected[chunks] def substitute(terms: silicon.Map[Term, Term]): GeneralChunk + + val permExp: Option[ast.Exp] +} + +trait NonQuantifiedChunk extends GeneralChunk { + val args: Seq[Term] + val argsExp: Option[Seq[ast.Exp]] + val snap: Term + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): NonQuantifiedChunk + override protected[chunks] def permMinus(perm: Term, permExp: Option[ast.Exp]): NonQuantifiedChunk + override protected[chunks] def permPlus(perm: Term, permExp: Option[ast.Exp]): NonQuantifiedChunk + override protected[chunks] def withPerm(perm: Term, permExp: Option[ast.Exp]): NonQuantifiedChunk + protected[chunks] def withSnap(snap: Term, snapExp: Option[ast.Exp]): NonQuantifiedChunk +} + +trait QuantifiedChunk extends GeneralChunk { + val quantifiedVars: Seq[Var] + val quantifiedVarExps: Option[Seq[ast.LocalVarDecl]] + def snapshotMap: Term + def valueAt(arguments: Seq[Term]): Term + override protected[chunks] def applyCondition(newCond: Term, newCondExp: Option[ast.Exp]): QuantifiedChunk + override protected[chunks] def permMinus(perm: Term, permExp: Option[ast.Exp]): QuantifiedChunk + override protected[chunks] def permPlus(perm: Term, permExp: Option[ast.Exp]): QuantifiedChunk + protected[chunks] def withSnapshotMap(snap: Term): QuantifiedChunk +} diff --git a/src/main/scala/state/chunks/DependencyAnalysisAwareChunkFactory.scala b/src/main/scala/state/chunks/DependencyAnalysisAwareChunkFactory.scala new file mode 100644 index 000000000..d1423cb12 --- /dev/null +++ b/src/main/scala/state/chunks/DependencyAnalysisAwareChunkFactory.scala @@ -0,0 +1,176 @@ +// 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.silicon.state.chunks + +import viper.silicon +import viper.silicon.decider.{Decider, DependencyAnalysisHandler} +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos +import viper.silicon.resources.BaseID +import viper.silicon.rules.InverseFunctions +import viper.silicon.state.terms.{MagicWandSnapshot, Term, Var} +import viper.silver.ast +import viper.silver.dependencyAnalysis.AssumptionType + +import scala.annotation.unused + +class DependencyAnalysisAwareChunkFactory(decider: Decider with DependencyAnalysisHandler) extends ChunkFactory { + override def createBasicChunk(resourceID: BaseID, + id: BasicChunkIdentifier, + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: Term, + snapExp: Option[ast.Exp], + perm: Term, + permExp: Option[ast.Exp], + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): BasicChunk = { + decider.registerChunk[BasicChunk]({finalPerm => + BasicChunk(resourceID, id, args, argsExp, snap, snapExp, finalPerm, permExp) + }, + perm, analysisInfos, isExhale) + } + + override def createQuantifiedFieldChunk(id: BasicChunkIdentifier, + fvf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonRcvr: Option[Term], + singletonRcvrExp: Option[ast.Exp], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedFieldChunk = { + decider.registerChunk[QuantifiedFieldChunk]({perm => + QuantifiedFieldChunk(id, fvf, condition, conditionExp, perm, permValueExp, invs, singletonRcvr, singletonRcvrExp, hints) + }, + permValue, analysisInfos, isExhale) + } + + override def createQuantifiedPredicateChunk(id: BasicChunkIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + psf: Term, + condition: Term, + conditionExp: Option[ast.Exp], + permValue: Term, + permValueExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedPredicateChunk = { + decider.registerChunk[QuantifiedPredicateChunk]({ finalPerm => + QuantifiedPredicateChunk(id, quantifiedVars, quantifiedVarExps, psf, condition, conditionExp, finalPerm, permValueExp, invs, singletonArgs, singletonArgExps, hints) + }, permValue, analysisInfos, isExhale) + } + + override def createQuantifiedMagicWandChunk(id: MagicWandIdentifier, + quantifiedVars: Seq[Var], + quantifiedVarExps: Option[Seq[ast.LocalVarDecl]], + wsf: Term, + perm: Term, + permExp: Option[ast.Exp], + invs: Option[InverseFunctions], + singletonArgs: Option[Seq[Term]], + singletonArgExps: Option[Seq[ast.Exp]], + hints: Seq[Term] = Nil, + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): QuantifiedMagicWandChunk = { + decider.registerChunk[QuantifiedMagicWandChunk]({ finalPerm => + QuantifiedMagicWandChunk(id, quantifiedVars, quantifiedVarExps, wsf, finalPerm, permExp, invs, singletonArgs, singletonArgExps, hints) + }, perm, analysisInfos, isExhale) + } + + override def createMagicWandChunk(id: MagicWandIdentifier, + bindings: Map[ast.AbstractLocalVar, (Term, Option[ast.Exp])], + args: Seq[Term], + argsExp: Option[Seq[ast.Exp]], + snap: MagicWandSnapshot, + perm: Term, + permExp: Option[ast.Exp], + analysisInfos: DependencyAnalysisInfos, + isExhale: Boolean = false): MagicWandChunk = { + decider.registerChunk[MagicWandChunk]({ finalPerm => + MagicWandChunk(id, bindings, args, argsExp, snap, finalPerm, permExp) + }, perm, analysisInfos, isExhale) + } + + def applyCondition(chunk: GeneralChunk, newCond: Term, newCondExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): GeneralChunk = { + decider.registerDerivedChunk(Set(chunk), {_ => + chunk.applyCondition(newCond, newCondExp)}, + chunk.perm, analysisInfos, isExhale=false, createLabel=false) + } + + def permMinus(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): GeneralChunk = { + val newChunk = decider.registerDerivedChunk(Set(chunk), {finalPerm => + chunk.permMinus(finalPerm, newPermExp)}, + newPerm, analysisInfos.withDependencyType(AssumptionType.Internal), isExhale=false, createLabel=false) // TODO ake: assumption type? maybe for exhale we want to have Implicit? + @unused // we need to register the chunk to have a sound analysis + val exhaledChunk = decider.registerDerivedChunk(Set(chunk), { finalPerm => + chunk.withPerm(finalPerm, newPermExp)}, + newPerm, analysisInfos, isExhale=true, createLabel=false) + newChunk + } + + def permMinus(chunk: QuantifiedBasicChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): QuantifiedBasicChunk = { + val newChunk = decider.registerDerivedChunk(Set(chunk), {finalPerm => + chunk.permMinus(finalPerm, newPermExp)}, + newPerm, analysisInfos.withDependencyType(AssumptionType.Internal), isExhale=false, createLabel=false) // TODO ake: assumption type? maybe for exhale we want to have Implicit? + @unused // we need to register the chunk to have a sound analysis + val exhaledChunk = decider.registerDerivedChunk(Set(chunk), { finalPerm => + chunk.withPerm(finalPerm, newPermExp)}, + newPerm, analysisInfos, isExhale=true, createLabel=false) + newChunk + } + + def permPlus(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = { + decider.registerDerivedChunk(Set(chunk), {finalPerm => + chunk.permPlus(finalPerm, newPermExp)}, + newPerm, analysisInfos, isExhale) + } + + def withPerm(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = { + decider.registerDerivedChunk(Set(chunk), {finalPerm => + chunk.withPerm(finalPerm, newPermExp)}, + newPerm, analysisInfos, isExhale) + } + + def withPermNonQuantifiedChunk(chunk: NonQuantifiedChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): NonQuantifiedChunk = { + decider.registerDerivedChunk(Set(chunk), {finalPerm => + chunk.withPerm(finalPerm, newPermExp)}, + newPerm, analysisInfos, isExhale) + } + + def permScale(chunk: GeneralChunk, newPerm: Term, newPermExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = { + decider.registerDerivedChunk(Set(chunk), {finalPerm => + chunk.permScale(finalPerm, newPermExp)}, + newPerm, analysisInfos, isExhale) + } + + def substitute(chunk: GeneralChunk, terms: silicon.Map[Term, Term], analysisInfos: DependencyAnalysisInfos, isExhale: Boolean=false): GeneralChunk = { + val newChunk = chunk.substitute(terms) + decider.registerDerivedChunk(Set(chunk), {finalPerm => + newChunk.withPerm(finalPerm, newChunk.permExp)}, + newChunk.perm, analysisInfos, isExhale) + } + + def withSnapshotMap(chunk: QuantifiedChunk, snap: Term, analysisInfos: DependencyAnalysisInfos): QuantifiedChunk = { + decider.registerDerivedChunk[QuantifiedChunk](Set(chunk), {_ => + chunk.withSnapshotMap(snap) + }, + chunk.perm, analysisInfos, isExhale=false, createLabel=false) + } + + def withSnap(chunk: NonQuantifiedChunk, snap: Term, snapExp: Option[ast.Exp], analysisInfos: DependencyAnalysisInfos): NonQuantifiedChunk = { + decider.registerDerivedChunk[NonQuantifiedChunk](Set(chunk), {_ => + chunk.withSnap(snap, snapExp)}, + chunk.perm, analysisInfos, isExhale=false, createLabel=false) + } +} \ No newline at end of file diff --git a/src/main/scala/state/package.scala b/src/main/scala/state/package.scala index 5cb59c877..b5642d809 100644 --- a/src/main/scala/state/package.scala +++ b/src/main/scala/state/package.scala @@ -6,11 +6,11 @@ package viper.silicon -import viper.silicon.interfaces.state.NonQuantifiedChunk -import viper.silver.ast import viper.silicon.rules.PermMapDefinition import viper.silicon.rules.moreCompleteExhaleSupporter.TaggedSummarisingSnapshot +import viper.silicon.state.chunks.{NonQuantifiedChunk, QuantifiedBasicChunk} import viper.silicon.state.terms.Term +import viper.silver.ast package object state { type PmCache = diff --git a/src/main/scala/supporters/DependencyAnalysisAwareMethodVerificationUnitProvider.scala b/src/main/scala/supporters/DependencyAnalysisAwareMethodVerificationUnitProvider.scala new file mode 100644 index 000000000..ee81a4809 --- /dev/null +++ b/src/main/scala/supporters/DependencyAnalysisAwareMethodVerificationUnitProvider.scala @@ -0,0 +1,50 @@ +// 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.silicon.supporters + +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silicon.interfaces.{Failure, VerificationResult} +import viper.silicon.state.State +import viper.silicon.state.terms.True +import viper.silicon.verifier.DependencyAnalysisAwareVerifier +import viper.silver.ast.Method +import viper.silver.dependencyAnalysis._ + +trait DependencyAnalysisAwareMethodVerificationUnitProvider extends DefaultMethodVerificationUnitProvider { v: DependencyAnalysisAwareVerifier => + override def methodSupporter: MethodSupporter = DependencyAnalysisAwareMethodSupporter + + private object DependencyAnalysisAwareMethodSupporter extends MethodSupporter { + + override def verify(sInit: State, method: Method): Seq[VerificationResult] = { + + val presAssertionNodeForJoin = method.pres.flatMap(_.topLevelConjuncts).map(pc => { + val analysisSourceInfo = DependencyAnalysisSourceInfo.createAnalysisSourceInfo(pc) + DependencyNodeFactory.createSimpleAssertionNode(True, + analysisSourceInfo, + AssumptionType.Precondition, + SimpleDependencyAnalysisMerge(analysisSourceInfo), + List(SimpleDependencyAnalysisJoin(analysisSourceInfo, JoinType.Sink, EdgeType.Up)), + method.name) + }) + presAssertionNodeForJoin foreach decider.getDependencyAnalyzer.addAssertionNode + + val result = super.verify(sInit, method) + + if (method.body.isEmpty) + decider.getDependencyAnalyzer.addDependenciesForAbstractMembers(method.pres.flatMap(_.topLevelConjuncts), method.posts.flatMap(_.topLevelConjuncts), decider.defaultAnalysisInfos) + + result foreach (r => { + val allErrors = (r :: r.previous.toList).filter(_.isInstanceOf[Failure]).map(_.asInstanceOf[Failure]) + r.dependencyGraphInterpreter = decider.getDependencyAnalyzer.buildFinalGraph().map(new DependencyGraphInterpreter(method.name, _, allErrors, Some(method))) + }) + + result + } + } + +} diff --git a/src/main/scala/supporters/Domains.scala b/src/main/scala/supporters/Domains.scala index bb8916f77..b317f86c8 100644 --- a/src/main/scala/supporters/Domains.scala +++ b/src/main/scala/supporters/Domains.scala @@ -6,14 +6,16 @@ package viper.silicon.supporters -import viper.silver.ast import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.common.collections.immutable.MultiMap._ -import viper.silicon.toMap +import viper.silicon.decider.DependencyAnalysisProverHandler +import viper.silicon.dependencyAnalysis.{DependencyAnalysisAxiomInfo, DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces.PreambleContributor import viper.silicon.interfaces.decider.ProverLike -import viper.silicon.state.{FunctionPreconditionTransformer, SymbolConverter, terms} import viper.silicon.state.terms.{Distinct, DomainFun, Sort, Term} +import viper.silicon.state.{FunctionPreconditionTransformer, SymbolConverter, terms} +import viper.silicon.toMap +import viper.silver.ast import viper.silver.ast.NamedDomainAxiom trait DomainsContributor[SO, SY, AX, UA] extends PreambleContributor[SO, SY, AX] { @@ -27,7 +29,7 @@ class DefaultDomainsContributor(symbolConverter: SymbolConverter, private var collectedSorts = InsertionOrderedSet[Sort]() private var collectedFunctions = InsertionOrderedSet[terms.DomainFun]() - private var collectedAxioms = InsertionOrderedSet[Term]() + private var collectedAxioms = InsertionOrderedSet[(Term, DependencyAnalysisAxiomInfo)]() private var uniqueSymbols = MultiMap.empty[Sort, DomainFun] /* Lifetime */ @@ -98,10 +100,14 @@ class DefaultDomainsContributor(symbolConverter: SymbolConverter, } }) + val isAnalysisForDomainEnabled = DependencyAnalyzer.extractEnableAnalysisFromInfo(domain.info).getOrElse(true) + domain.axioms foreach (axiom => { val tAx = domainTranslator.translateAxiom(axiom, symbolConverter.toSort) val tAxPres = FunctionPreconditionTransformer.transform(tAx, program) - collectedAxioms += terms.And(tAxPres, tAx) + val enableAnalysis = DependencyAnalyzer.extractEnableAnalysisFromInfo(axiom.info).getOrElse(isAnalysisForDomainEnabled) + collectedAxioms = collectedAxioms.incl((terms.And(tAxPres, tAx), + DependencyAnalysisAxiomInfo(DependencyAnalysisInfos.DefaultInfos.addInfo(axiom.exp.info, axiom.exp).withEnabled(enableAnalysis), domain.name))) }) }) } @@ -118,10 +124,13 @@ class DefaultDomainsContributor(symbolConverter: SymbolConverter, collectedFunctions foreach (f => sink.declare(terms.FunctionDecl(f))) } - def axiomsAfterAnalysis: Iterable[terms.Term] = collectedAxioms + def axiomsAfterAnalysis: Iterable[terms.Term] = collectedAxioms.map(_._1) - def emitAxiomsAfterAnalysis(sink: ProverLike): Unit = { - sink.assumeAxioms(collectedAxioms, "Domain axioms") + def emitAxiomsAfterAnalysis(sink: ProverLike): Unit = sink match { + case daSink: DependencyAnalysisProverHandler => + daSink.assumeAxiomsWithAnalysisInfo(collectedAxioms, "Domain axioms") + case _ => + sink.assumeAxioms(collectedAxioms.map(_._1), "Domain axioms") } def uniquenessAssumptionsAfterAnalysis: Iterable[Term] = diff --git a/src/main/scala/supporters/MethodSupporter.scala b/src/main/scala/supporters/MethodSupporter.scala index 71a2eb44a..ff5ca579b 100644 --- a/src/main/scala/supporters/MethodSupporter.scala +++ b/src/main/scala/supporters/MethodSupporter.scala @@ -7,18 +7,19 @@ package viper.silicon.supporters import com.typesafe.scalalogging.Logger -import viper.silver.ast -import viper.silver.components.StatefulComponent -import viper.silver.verifier.errors._ -import viper.silicon.interfaces._ +import viper.silicon.Map import viper.silicon.decider.Decider +import viper.silicon.interfaces._ import viper.silicon.logger.records.data.WellformednessCheckRecord import viper.silicon.rules.{consumer, executionFlowController, executor, producer} -import viper.silicon.state.{State, Store} import viper.silicon.state.State.OldHeaps -import viper.silicon.verifier.{Verifier, VerifierComponent} +import viper.silicon.state.{State, Store} import viper.silicon.utils.freshSnap -import viper.silicon.Map +import viper.silicon.verifier.{Verifier, VerifierComponent} +import viper.silver.ast +import viper.silver.components.StatefulComponent +import viper.silver.dependencyAnalysis._ +import viper.silver.verifier.errors._ /* TODO: Consider changing the DefaultMethodVerificationUnitProvider into a SymbolicExecutionRule */ @@ -28,10 +29,14 @@ trait DefaultMethodVerificationUnitProvider extends VerifierComponent { v: Verif def logger: Logger def decider: Decider - object methodSupporter extends MethodVerificationUnit with StatefulComponent { + def methodSupporter: MethodSupporter = DefaultMethodSupporter + + private object DefaultMethodSupporter extends MethodSupporter + + trait MethodSupporter extends MethodVerificationUnit with StatefulComponent { + import consumer._ import executor._ import producer._ - import consumer._ private var _units: Seq[ast.Method] = _ @@ -76,27 +81,30 @@ trait DefaultMethodVerificationUnitProvider extends VerifierComponent { v: Verif new java.io.File(s"${Verifier.config.tempDirectory()}/${method.name}.dot")) } + val analysisInfosPrecondition = v.decider.defaultAnalysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Sink, EdgeType.Up)) + val analysisInfosPostcondition = v.decider.defaultAnalysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Source, EdgeType.Down)) + errorsReportedSoFar.set(0) val result = /* Combined the well-formedness check and the execution of the body, which are two separate * rules in Smans' paper. */ executionFlowController.locally(s, v)((s1, v1) => { - produces(s1, freshSnap, pres, ContractNotWellformed, v1)((s2, v2) => { + produces(s1, freshSnap, pres, ContractNotWellformed, v1, analysisInfosPrecondition)((s2, v2) => { v2.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterContract) val s2a = s2.copy(oldHeaps = s2.oldHeaps + (Verifier.PRE_STATE_LABEL -> s2.h)) ( executionFlowController.locally(s2a, v2)((s3, v3) => { val s4 = s3.copy(h = v3.heapSupporter.getEmptyHeap(s3.program)) val impLog = new WellformednessCheckRecord(posts, s, v.decider.pcs) val sepIdentifier = symbExLog.openScope(impLog) - produces(s4, freshSnap, posts, ContractNotWellformed, v3)((_, _) => { + produces(s4, freshSnap, posts, ContractNotWellformed, v3, analysisInfosPostcondition)((_, _) => { symbExLog.closeScope(sepIdentifier) Success()})}) && { executionFlowController.locally(s2a, v2)((s3, v3) => { - exec(s3, body, v3)((s4, v4) => - consumes(s4, posts, false, postViolated, v4)((_, _, _) => - Success()))}) } )})}) + exec(s3, body, v3)((s4, v4) => { + consumes(s4, posts, false, postViolated, v4, analysisInfosPostcondition)((_, _, _) => + Success())})}) } )})}) v.decider.resetProverOptions() diff --git a/src/main/scala/supporters/PredicateVerificationUnit.scala b/src/main/scala/supporters/PredicateVerificationUnit.scala index 9e3e4c2a3..5c1a3ad1e 100644 --- a/src/main/scala/supporters/PredicateVerificationUnit.scala +++ b/src/main/scala/supporters/PredicateVerificationUnit.scala @@ -8,21 +8,21 @@ package viper.silicon.supporters import com.typesafe.scalalogging.Logger import viper.silicon.common.collections.immutable.InsertionOrderedSet -import viper.silver.ast -import viper.silver.ast.Program -import viper.silver.components.StatefulComponent -import viper.silver.verifier.errors._ import viper.silicon.decider.Decider -import viper.silicon.{Map, toMap} +import viper.silicon.interfaces._ import viper.silicon.interfaces.decider.ProverLike -import viper.silicon.state._ +import viper.silicon.rules.executionFlowController import viper.silicon.state.State.OldHeaps +import viper.silicon.state._ import viper.silicon.state.terms._ -import viper.silicon.interfaces._ -import viper.silicon.rules.executionFlowController import viper.silicon.supporters.functions.{ActualFunctionRecorder, FunctionRecorder, FunctionRecorderHandler, NoopFunctionRecorder} -import viper.silicon.verifier.{Verifier, VerifierComponent} import viper.silicon.utils.{freshSnap, toSf} +import viper.silicon.verifier.{Verifier, VerifierComponent} +import viper.silicon.{Map, toMap} +import viper.silver.ast +import viper.silver.ast.Program +import viper.silver.components.StatefulComponent +import viper.silver.verifier.errors._ class PredicateData(val predicate: ast.Predicate) /* Note: Holding a reference to a fixed symbol converter (instead of going @@ -115,7 +115,6 @@ trait DefaultPredicateVerificationUnitProvider extends VerifierComponent { v: Ve var branchResults: Seq[(Seq[Term], Seq[(ast.Exp, Option[ast.Exp])], Heap, InsertionOrderedSet[Term])] = Seq() - val result = predicate.body match { case None => Success() @@ -123,7 +122,7 @@ trait DefaultPredicateVerificationUnitProvider extends VerifierComponent { v: Ve /* locallyXXX { magicWandSupporter.checkWandsAreSelfFraming(σ.γ, σ.h, predicate, c)} &&*/ executionFlowController.locally(s, v)((s1, _) => { - produce(s1, toSf(snap), body, err, v)((s2, v2) => { + produce(s1, toSf(snap), body, err, v, v.decider.defaultAnalysisInfos)((s2, v2) => { val branchConds = v2.decider.pcs.branchConditions.reverse val branchCondExps = v2.decider.pcs.branchConditionExps.reverse assert(branchConds.length == branchCondExps.length) diff --git a/src/main/scala/supporters/SnapshotSupporter.scala b/src/main/scala/supporters/SnapshotSupporter.scala index 8f3f6efa8..c8b3c76cb 100644 --- a/src/main/scala/supporters/SnapshotSupporter.scala +++ b/src/main/scala/supporters/SnapshotSupporter.scala @@ -6,9 +6,11 @@ package viper.silicon.supporters +import viper.silicon.state.chunks.MagicWandIdentifier import viper.silicon.debugger.DebugExp +import viper.silicon.dependencyAnalysis.DependencyAnalysisInfos import viper.silicon.state.terms.{Combine, First, Second, Sort, Term, Unit, sorts} -import viper.silicon.state.{MagicWandIdentifier, State, SymbolConverter} +import viper.silicon.state.{State, SymbolConverter} import viper.silicon.utils.toSf import viper.silicon.verifier.Verifier import viper.silver.ast @@ -26,7 +28,8 @@ trait SnapshotSupporter { sf: (Sort, Verifier) => Term, a0: ast.Exp, a1: ast.Exp, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : ((Sort, Verifier) => Term, (Sort, Verifier) => Term) } @@ -117,10 +120,11 @@ class DefaultSnapshotSupporter(symbolConverter: SymbolConverter) extends Snapsho sf: (Sort, Verifier) => Term, a0: ast.Exp, a1: ast.Exp, - v: Verifier) + v: Verifier, + analysisInfos: DependencyAnalysisInfos) : ((Sort, Verifier) => Term, (Sort, Verifier) => Term) = { - val (snap0, snap1) = createSnapshotPair(s, sf(sorts.Snap, v), a0, a1, v) + val (snap0, snap1) = createSnapshotPair(s, sf(sorts.Snap, v), a0, a1, v, analysisInfos) val sf0 = toSf(snap0) val sf1 = toSf(snap1) @@ -128,7 +132,7 @@ class DefaultSnapshotSupporter(symbolConverter: SymbolConverter) extends Snapsho (sf0, sf1) } - private def createSnapshotPair(@unused s: State, snap: Term, @unused a0: ast.Exp, @unused a1: ast.Exp, v: Verifier): (Term, Term) = { + private def createSnapshotPair(@unused s: State, snap: Term, @unused a0: ast.Exp, @unused a1: ast.Exp, v: Verifier, analysisInfos: DependencyAnalysisInfos): (Term, Term) = { /* [2015-11-17 Malte] If both fresh snapshot terms and first/second datatypes * are used, then the overall test suite verifies in 2min 10sec, whereas * it takes 2min 20sec when only first/second datatypes are used. Might be @@ -160,7 +164,7 @@ class DefaultSnapshotSupporter(symbolConverter: SymbolConverter) extends Snapsho (snap0, snap1, snap === Combine(snap0, snap1)) } - v.decider.assume(snapshotEq, Option.when(Verifier.config.enableDebugging())(DebugExp.createInstance("Snapshot", true))) + v.decider.assume(snapshotEq, Option.when(Verifier.config.enableDebugging())(DebugExp.createInstance("Snapshot", true)), analysisInfos) (snap0, snap1) } diff --git a/src/main/scala/supporters/functions/DependencyAnalysisAwareFunctionVerificationUnitProvider.scala b/src/main/scala/supporters/functions/DependencyAnalysisAwareFunctionVerificationUnitProvider.scala new file mode 100644 index 000000000..793d99b72 --- /dev/null +++ b/src/main/scala/supporters/functions/DependencyAnalysisAwareFunctionVerificationUnitProvider.scala @@ -0,0 +1,65 @@ +// 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.silicon.supporters.functions + +import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.decider.DependencyAnalysisProverHandler +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.DependencyGraphInterpreter +import viper.silicon.dependencyAnalysis.{DependencyAnalysisAxiomInfo, DependencyAnalyzer} +import viper.silicon.interfaces.decider.ProverLike +import viper.silicon.interfaces.{Failure, VerificationResult} +import viper.silicon.state.State +import viper.silicon.state.terms.{Term, True, Var} +import viper.silicon.verifier.{DependencyAnalysisAwareVerifier, Verifier} +import viper.silver.ast +import viper.silver.dependencyAnalysis._ + +trait DependencyAnalysisAwareFunctionVerificationUnitProvider extends DefaultFunctionVerificationUnitProvider { v: DependencyAnalysisAwareVerifier => + + override def functionsSupporter: FunctionsSupporter = DependencyAnalysisAwareFunctionSupporter + + object DependencyAnalysisAwareFunctionSupporter extends FunctionsSupporter { + override protected def handleFunction(sInit: State, function: ast.Function): VerificationResult = { + + val presAssertionNodeForJoin = function.pres.flatMap(_.topLevelConjuncts).map(pc => DependencyNodeFactory.createSimpleAssertionNode(True, DependencyAnalysisSourceInfo.createAnalysisSourceInfo(pc), AssumptionType.Precondition, SimpleDependencyAnalysisMerge(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(pc)), List(SimpleDependencyAnalysisJoin(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(pc), JoinType.Sink, EdgeType.Up)), function.name)) + presAssertionNodeForJoin foreach decider.getDependencyAnalyzer.addAssertionNode + + val result = super.handleFunction(sInit, function) + + if (function.body.isEmpty) { + decider.getDependencyAnalyzer.addNodes(decider.prover.getPreambleAnalysisNodes) + decider.getDependencyAnalyzer.addDependenciesForAbstractMembers(function.pres.flatMap(_.topLevelConjuncts), function.posts.flatMap(_.topLevelConjuncts), decider.defaultAnalysisInfos) + } + + val allErrors = (result :: result.previous.toList).filter(_.isInstanceOf[Failure]).map(_.asInstanceOf[Failure]) + result.dependencyGraphInterpreter = decider.getDependencyAnalyzer.buildFinalGraph().map(new DependencyGraphInterpreter(function.name, _, + allErrors, Some(function))) + + result + } + + override protected def emitAndRecordFunctionAxioms(axiom: (Term, DependencyAnalysisAxiomInfo)*): Unit = { + val cleanAxiom = + if (!Verifier.config.dependencyAnalysis.isDefined) axiom + else axiom.map(a => (a._1.transform { + case Var(name, _, _) if name.name.startsWith(DependencyAnalyzer.analysisLabelName) => True // replace dependency analysis labels by True to avoid errors + }(), a._2)) + decider.prover.assumeAxiomsWithAnalysisInfo(InsertionOrderedSet(cleanAxiom), "Function axioms") + + emittedFunctionAxiomsWithInfo = emittedFunctionAxiomsWithInfo ++ cleanAxiom + } + + override def emitAxiomsAfterVerification(sink: ProverLike): Unit = sink match { + case daSink: DependencyAnalysisProverHandler => + daSink.assumeAxiomsWithAnalysisInfo(InsertionOrderedSet(emittedFunctionAxiomsWithInfo), "Function axioms") + case _ => super.emitAxiomsAfterVerification(sink) + } + + } + +} diff --git a/src/main/scala/supporters/functions/FunctionData.scala b/src/main/scala/supporters/functions/FunctionData.scala index 06f762873..15177bb7d 100644 --- a/src/main/scala/supporters/functions/FunctionData.scala +++ b/src/main/scala/supporters/functions/FunctionData.scala @@ -6,24 +6,26 @@ package viper.silicon.supporters.functions -import scala.annotation.unused import com.typesafe.scalalogging.LazyLogging -import viper.silicon.state.FunctionPreconditionTransformer -import viper.silver.ast -import viper.silver.ast.utility.Functions import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.dependencyAnalysis.{DependencyAnalysisAxiomInfo, DependencyAnalysisInfos, DependencyAnalyzer} import viper.silicon.interfaces.FatalResult import viper.silicon.rules.{InverseFunctions, PermMapDefinition, SnapshotMapDefinition, functionSupporter} import viper.silicon.state.terms._ import viper.silicon.state.terms.predef._ -import viper.silicon.state.{Identifier, IdentifierFactory, SymbolConverter} +import viper.silicon.state.{FunctionPreconditionTransformer, Identifier, IdentifierFactory, SymbolConverter} import viper.silicon.supporters.PredicateData import viper.silicon.utils.ast.simplifyVariableName import viper.silicon.verifier.Verifier import viper.silicon.{Config, Map, toMap} +import viper.silver.ast import viper.silver.ast.LocalVarWithVersion +import viper.silver.ast.utility.Functions +import viper.silver.dependencyAnalysis._ import viper.silver.parser.PUnknown -import viper.silver.reporter.Reporter +import viper.silver.reporter.{InternalWarningMessage, Reporter} + +import scala.annotation.unused trait FunctionRecorderHandler { @@ -82,7 +84,6 @@ trait FunctionRecorderHandler { case other => sys.error(s"Unexpected permission map $other of type ${other.getClass.getSimpleName}") }) } - } class FunctionData(val programFunction: ast.Function, @@ -130,18 +131,40 @@ class FunctionData(val programFunction: ast.Function, else Seq.fill(1 + formalArgs.size)(None) + val isAnalysisEnabled: Boolean = Verifier.config.dependencyAnalysis.isDefined && DependencyAnalyzer.extractEnableAnalysisFromInfo(programFunction.info).getOrElse(true) + val defaultAnalysisInfo: DependencyAnalysisInfos = DependencyAnalysisInfos.DefaultInfos.withEnabled(isAnalysisEnabled) + val functionApplication = App(function, `?s` +: formalArgs.values.toSeq) val limitedFunctionApplication = App(limitedFunction, `?s` +: formalArgs.values.toSeq) val triggerFunctionApplication = App(statelessFunction, formalArgs.values.toSeq) val preconditionFunctionApplication = App(preconditionFunction, `?s` +: formalArgs.values.toSeq) - val limitedAxiom = - Forall(arguments, + + private val bodyAnalysisInfo: DependencyAnalysisAxiomInfo = { + val analysisInfos = if (programFunction.body.isDefined) { + defaultAnalysisInfo.addInfo(programFunction.body.get.info, programFunction.body.get) + .withJoinInfo(SimpleDependencyAnalysisJoin(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(programFunction.body.get), JoinType.Sink, EdgeType.Down)) + } else + defaultAnalysisInfo.withInfo(StringDependencyAnalysisSourceInfo("unverified function body"), AssumptionType.Internal) + + DependencyAnalysisAxiomInfo(analysisInfos, programFunction.name) + } + + + + val limitedAxiomWithInfo: (Quantification, DependencyAnalysisAxiomInfo) = + (Forall(arguments, BuiltinEquals(limitedFunctionApplication, functionApplication), - Trigger(functionApplication)) + Trigger(functionApplication)), + DependencyAnalysisAxiomInfo(defaultAnalysisInfo.withInfo(StringDependencyAnalysisSourceInfo("Limited Axiom"), AssumptionType.Internal), programFunction.name)) - val triggerAxiom = - Forall(arguments, triggerFunctionApplication, Trigger(limitedFunctionApplication)) + val limitedAxiom: Quantification = limitedAxiomWithInfo._1 + + val triggerAxiomWithInfo: (Quantification, DependencyAnalysisAxiomInfo) = + (Forall(arguments, triggerFunctionApplication, Trigger(limitedFunctionApplication)), + DependencyAnalysisAxiomInfo(defaultAnalysisInfo.withInfo(StringDependencyAnalysisSourceInfo("Trigger Axiom"), AssumptionType.Trigger), programFunction.name)) + + val triggerAxiom: Quantification = triggerAxiomWithInfo._1 /* * Data collected during phases 1 (well-definedness checking) and 2 (verification) @@ -167,31 +190,36 @@ class FunctionData(val programFunction: ast.Function, private def generateNestedDefinitionalAxioms: InsertionOrderedSet[Term] = { val freshSymbols: Set[Identifier] = freshSymbolsAcrossAllPhases.map(_.id) - val nested = ( + val nestedTmp = ( freshFieldInvs.flatMap(_.definitionalAxioms) ++ freshFvfsAndDomains.flatMap (fvfDef => fvfDef.domainDefinitions ++ fvfDef.valueDefinitions) ++ freshPermMaps.flatMap (pmDef => pmDef.valueDefinitions) ++ freshConstrainedVars.map(_._2) ++ freshConstraints) + val nested = if (!Verifier.config.dependencyAnalysis.isDefined) nestedTmp + else nestedTmp.map(_.transform{ + case Var(name, _, _) if name.name.startsWith(DependencyAnalyzer.analysisLabelName) => True // replace dependency analysis labels by True to avoid errors + }()) + // Filter out nested definitions that contain free variables. // This should not happen, but currently can, due to bugs in the function axiomatisation code. // Fixing these bugs with the current way functions are axiomatised will be very difficult, // but they should be resolved with Mauro's current work on heap snapshots. - // Once his changes are merged in, the commented warnings below should be turned into errors. + // Once his changes are merged in, the warnings below should be turned into errors. nested.filter(term => { val freeVars = term.freeVariables -- arguments val unknownVars = freeVars.filterNot(v => freshSymbols.contains(v.id)) - //if (unknownVars.nonEmpty) { - // val messageText = ( - // s"Found unexpected free variables $unknownVars " - // + s"in term $term during axiomatisation of function " - // + s"${programFunction.name}") - // - // reporter report InternalWarningMessage(messageText) - // logger warn messageText - //} + if (unknownVars.nonEmpty) { + val messageText = ( + s"Found unexpected free variables $unknownVars " + + s"in term $term during axiomatisation of function " + + s"${programFunction.name}") + + reporter report InternalWarningMessage(messageText) + logger warn messageText + } unknownVars.isEmpty }) @@ -207,29 +235,47 @@ class FunctionData(val programFunction: ast.Function, expressionTranslator.translatePrecondition(program, programFunction.pres, this) } + def postConditionInfo(post: ast.Exp): DependencyAnalysisAxiomInfo = + DependencyAnalysisAxiomInfo(defaultAnalysisInfo.addInfo(post.info, post) + .withJoinInfo(SimpleDependencyAnalysisJoin(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(post), JoinType.Sink, EdgeType.Down)), programFunction.name) + lazy val translatedPosts = { + translatedPostsWithInfo.map(_._1) + } + + lazy val translatedPostsWithInfo: Seq[(Term, DependencyAnalysisAxiomInfo)] = { assert(phase == 1, s"Postcondition axiom must be generated in phase 1, current phase is $phase") if (programFunction.posts.nonEmpty) { - expressionTranslator.translatePostcondition(program, programFunction.posts, this) + programFunction.posts.flatMap(_.topLevelConjuncts) map (p => (expressionTranslator.translatePostcondition(program, Seq(p), this).head, postConditionInfo(p))) } else { Seq() } } - lazy val postAxiom: Option[Term] = { + lazy val postAxiomWithInfo: Seq[(Term, DependencyAnalysisAxiomInfo)] = { assert(phase == 1, s"Postcondition axiom must be generated in phase 1, current phase is $phase") if (programFunction.posts.nonEmpty) { val pre = preconditionFunctionApplication - val innermostBody = And(generateNestedDefinitionalAxioms ++ List(Implies(pre, And(translatedPosts)))) val bodyBindings: Map[Var, Term] = Map(formalResult -> limitedFunctionApplication) - val body = Let(toMap(bodyBindings), innermostBody) - Some(Forall(arguments, body, Trigger(limitedFunctionApplication))) + def wrapBody(body: Term): Term = Let(toMap(bodyBindings), body) + + if (isAnalysisEnabled) { + (Forall(arguments, wrapBody(And(generateNestedDefinitionalAxioms)), Trigger(limitedFunctionApplication)), bodyAnalysisInfo) +: + translatedPostsWithInfo.map({p => + (And(Forall(arguments, wrapBody(Implies(pre, p._1)), Trigger(limitedFunctionApplication)), True), p._2) + }) + } else { + val innermostBody = And(generateNestedDefinitionalAxioms ++ List(Implies(pre, And(translatedPosts)))) + Seq((Forall(arguments, wrapBody(innermostBody), Trigger(limitedFunctionApplication)), DependencyAnalysisAxiomInfo(defaultAnalysisInfo, programFunction.name))) + } } else - None + Seq.empty } + lazy val postAxiom: Seq[Term] = postAxiomWithInfo.map(_._1) + /* * Properties resulting from phase 2 (verification) */ @@ -281,7 +327,7 @@ class FunctionData(val programFunction: ast.Function, expressionTranslator.translate(program, programFunction, this) } - lazy val definitionalAxiom: Option[Term] = { + lazy val definitionalAxiomWithInfo: Option[(Term, DependencyAnalysisAxiomInfo)] = { assert(phase == 2, s"Definitional axiom must be generated in phase 2, current phase is $phase") optBody.map(translatedBody => { @@ -296,25 +342,33 @@ class FunctionData(val programFunction: ast.Function, val allTriggers = ( Seq(Trigger(functionApplication)) ++ actualPredicateTriggers) - Forall(arguments, body, allTriggers)}) + (Forall(arguments, body, allTriggers), + bodyAnalysisInfo) + }) } - lazy val bodyPreconditionPropagationAxiom: Seq[Term] = { + lazy val definitionalAxiom: Option[Term] = definitionalAxiomWithInfo.map(_._1) + + lazy val bodyPreconditionPropagationAxiomWithInfo: Seq[(Term, DependencyAnalysisAxiomInfo)] = { val pre = preconditionFunctionApplication val bodyPreconditions = if (programFunction.body.isDefined) optBody.map(translatedBody => { val body = Implies(pre, FunctionPreconditionTransformer.transform(translatedBody, program)) - Forall(arguments, body, Seq(Trigger(functionApplication))) + (Forall(arguments, body, Seq(Trigger(functionApplication))), bodyAnalysisInfo) }) else None bodyPreconditions.toSeq } - lazy val postPreconditionPropagationAxiom: Seq[Term] = { + lazy val bodyPreconditionPropagationAxiom: Seq[Term] = bodyPreconditionPropagationAxiomWithInfo.map(_._1) + + lazy val postPreconditionPropagationAxiomWithInfo: Seq[(Term, DependencyAnalysisAxiomInfo)] = { val pre = preconditionFunctionApplication val postPreconditions = if (programFunction.posts.nonEmpty) { val bodyBindings: Map[Var, Term] = Map(formalResult -> limitedFunctionApplication) - val bodies = translatedPosts.map(tPost => Let(bodyBindings, Implies(pre, FunctionPreconditionTransformer.transform(tPost, program)))) - bodies.map(b => Forall(arguments, b, Seq(Trigger(limitedFunctionApplication)))) + val bodies = translatedPostsWithInfo.map(tPost => (Let(bodyBindings, Implies(pre, FunctionPreconditionTransformer.transform(tPost._1, program))), tPost._2)) + bodies.map(b => (Forall(arguments, b._1, Seq(Trigger(limitedFunctionApplication))),b._2)) } else Seq() postPreconditions } + + lazy val postPreconditionPropagationAxiom: Seq[Term] = postPreconditionPropagationAxiomWithInfo.map(_._1) } diff --git a/src/main/scala/supporters/functions/FunctionVerificationUnit.scala b/src/main/scala/supporters/functions/FunctionVerificationUnit.scala index 3cc1c934a..4d24ec223 100644 --- a/src/main/scala/supporters/functions/FunctionVerificationUnit.scala +++ b/src/main/scala/supporters/functions/FunctionVerificationUnit.scala @@ -7,27 +7,29 @@ package viper.silicon.supporters.functions import com.typesafe.scalalogging.Logger +import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.debugger.DebugExp -import viper.silver.ast -import viper.silver.ast.utility.Functions -import viper.silver.components.StatefulComponent -import viper.silver.verifier.errors.{ContractNotWellformed, FunctionNotWellformed, PostconditionViolated} -import viper.silicon.{Map, Stack, toMap} -import viper.silicon.interfaces.decider.ProverLike +import viper.silicon.decider.Decider +import viper.silicon.dependencyAnalysis._ import viper.silicon.interfaces._ -import viper.silicon.state._ +import viper.silicon.interfaces.decider.ProverLike +import viper.silicon.rules.{consumer, evaluator, executionFlowController, producer} import viper.silicon.state.State.OldHeaps +import viper.silicon.state._ import viper.silicon.state.terms._ import viper.silicon.state.terms.predef.`?s` -import viper.silicon.common.collections.immutable.InsertionOrderedSet -import viper.silicon.decider.Decider -import viper.silicon.rules.{consumer, evaluator, executionFlowController, producer} import viper.silicon.supporters.{AnnotationSupporter, PredicateData} import viper.silicon.utils.ast.{BigAnd, simplifyVariableName} -import viper.silicon.verifier.{Verifier, VerifierComponent} import viper.silicon.utils.{freshSnap, toSf} -import viper.silver.ast.LocalVarWithVersion +import viper.silicon.verifier.{Verifier, VerifierComponent} +import viper.silicon.{Map, Stack, toMap} +import viper.silver.ast +import viper.silver.ast._ +import viper.silver.ast.utility.Functions +import viper.silver.components.StatefulComponent +import viper.silver.dependencyAnalysis._ import viper.silver.parser.PType +import viper.silver.verifier.errors.{ContractNotWellformed, FunctionNotWellformed, PostconditionViolated} import scala.annotation.unused @@ -41,19 +43,25 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver private case class Phase1Data(sPre: State, bcsPre: Stack[Term], bcsPreExp: Stack[(ast.Exp, Option[ast.Exp])], pcsPre: InsertionOrderedSet[Term], pcsPreExp: Option[InsertionOrderedSet[DebugExp]]) - object functionsSupporter + def functionsSupporter: FunctionsSupporter = DefaultFunctionsSupporter + + object DefaultFunctionsSupporter extends FunctionsSupporter + + trait FunctionsSupporter extends FunctionVerificationUnit[Sort, Decl, Term] with StatefulComponent { - import producer._ import consumer._ import evaluator._ + import producer._ @unused private var program: ast.Program = _ /*private*/ var functionData: Map[String, FunctionData] = Map.empty - private var emittedFunctionAxioms: Vector[Term] = Vector.empty + protected var emittedFunctionAxiomsWithInfo: Vector[(Term, DependencyAnalysisAxiomInfo)] = Vector.empty + protected def emittedFunctionAxioms: Vector[Term] = emittedFunctionAxiomsWithInfo.map(_._1) private var freshVars: Vector[Var] = Vector.empty - private var postConditionAxioms: Vector[Term] = Vector.empty + private var postConditionAxiomsWithInfo: Vector[(Term, DependencyAnalysisAxiomInfo)] = Vector.empty + private def postConditionAxioms: Vector[Term] = postConditionAxiomsWithInfo.map(_._1) private val expressionTranslator = { def resolutionFailureMessage(exp: ast.Positioned, data: FunctionData): String = ( @@ -171,7 +179,7 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver res } - private def handleFunction(sInit: State, function: ast.Function): VerificationResult = { + protected def handleFunction(sInit: State, function: ast.Function): VerificationResult = { val data = functionData(function.name) val s = sInit.copy(functionRecorder = ActualFunctionRecorder(Left(data)), conservingSnapshotGeneration = true, @@ -181,15 +189,13 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver checkSpecificationWelldefinedness(s, function) match { case (result1: FatalResult, _) => data.verificationFailures = data.verificationFailures :+ result1 - result1 - case (result1, phase1data) => - emitAndRecordFunctionAxioms(data.limitedAxiom) - emitAndRecordFunctionAxioms(data.triggerAxiom) - emitAndRecordFunctionAxioms(data.postAxiom.toSeq: _*) - emitAndRecordFunctionAxioms(data.postPreconditionPropagationAxiom: _*) - this.postConditionAxioms = this.postConditionAxioms ++ data.postAxiom.toSeq + emitAndRecordFunctionAxioms(data.limitedAxiomWithInfo) + emitAndRecordFunctionAxioms(data.triggerAxiomWithInfo) + emitAndRecordFunctionAxioms(data.postAxiomWithInfo: _*) + emitAndRecordFunctionAxioms(data.postPreconditionPropagationAxiomWithInfo: _*) + this.postConditionAxiomsWithInfo = this.postConditionAxiomsWithInfo ++ data.postAxiomWithInfo if (function.body.isEmpty) { result1 @@ -201,8 +207,8 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver case fatalResult: FatalResult => data.verificationFailures = data.verificationFailures :+ fatalResult case _ => - emitAndRecordFunctionAxioms(data.definitionalAxiom.toSeq: _*) - emitAndRecordFunctionAxioms(data.bodyPreconditionPropagationAxiom: _*) + emitAndRecordFunctionAxioms(data.definitionalAxiomWithInfo.toSeq: _*) + emitAndRecordFunctionAxioms(data.bodyPreconditionPropagationAxiomWithInfo: _*) } result1 && result2 @@ -226,19 +232,22 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver val g = Store(argsStore + (function.result -> (data.formalResult, data.valFormalResultExp))) val s = sInit.copy(g = g, h = v.heapSupporter.getEmptyHeap(sInit.program), oldHeaps = OldHeaps()) + val analysisInfosPrecondition = v.decider.defaultAnalysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Sink, EdgeType.Up)) + val analysisInfosPostcondition = v.decider.defaultAnalysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Source, EdgeType.Down)) + var phase1Data: Seq[Phase1Data] = Vector.empty var recorders: Seq[FunctionRecorder] = Vector.empty val result = executionFlowController.locally(s, v)((s0, _) => { val preMark = decider.setPathConditionMark() - produces(s0, toSf(`?s`), pres, ContractNotWellformed, v)((s1, _) => { + produces(s0, toSf(`?s`), pres, ContractNotWellformed, v, analysisInfosPrecondition)((s1, _) => { val relevantPathConditionStack = decider.pcs.after(preMark) phase1Data :+= Phase1Data(s1, relevantPathConditionStack.branchConditions, relevantPathConditionStack.branchConditionExps, relevantPathConditionStack.assumptions, Option.when(evaluator.withExp)(relevantPathConditionStack.assumptionExps)) // The postcondition must be produced with a fresh snapshot (different from `?s`) because // the postcondition's snapshot structure is most likely different than that of the // precondition - produces(s1, freshSnap, posts, ContractNotWellformed, v)((s2, _) => { + produces(s1, freshSnap, posts, ContractNotWellformed, v, analysisInfosPostcondition)((s2, _) => { recorders :+= s2.functionRecorder Success()})})}) @@ -262,21 +271,28 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver var recorders: Seq[FunctionRecorder] = Vector.empty val wExp = evaluator.withExp + val precondAnalysisSourceInfos = v.decider.defaultAnalysisInfos.withInfo(StringDependencyAnalysisSourceInfo("preconditions", NoPosition), AssumptionType.Internal) + val analysisInfosPostcondition = v.decider.defaultAnalysisInfos.withJoinInfo(EvalStackDependencyAnalysisJoin(JoinType.Source, EdgeType.Down)) + val analysisInfosBody = DependencyAnalyzer.handleAndGetUpdatedAnalysisInfos(v.decider, v.decider.defaultAnalysisInfos, body.info, body) + .withJoinInfo(SimpleDependencyAnalysisJoin(DependencyAnalysisSourceInfo.createAnalysisSourceInfo(body), JoinType.Source, EdgeType.Down)) + val result = phase1data.foldLeft(Success(): VerificationResult) { case (fatalResult: FatalResult, _) => fatalResult case (intermediateResult, Phase1Data(sPre, bcsPre, bcsPreExp, pcsPre, pcsPreExp)) => intermediateResult && executionFlowController.locally(sPre, v)((s1, _) => { - decider.setCurrentBranchCondition(And(bcsPre), (BigAnd(bcsPreExp.map(_._1)), Option.when(wExp)(BigAnd(bcsPreExp.map(_._2.get))))) - decider.assume(pcsPre, Option.when(wExp)(DebugExp.createInstance(s"precondition of ${function.name}", pcsPreExp.get)), enforceAssumption = false) + val labelledBcsPre = terms.And(bcsPre map (t => DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v.decider, t, Set.empty, Set(t)))) + decider.setCurrentBranchCondition(labelledBcsPre, (BigAnd(bcsPreExp.map(_._1)), Option.when(wExp)(BigAnd(bcsPreExp.map(_._2.get)))), precondAnalysisSourceInfos) + val labelledPcsPre = pcsPre map (t => DependencyAnalyzer.wrapWithDependencyAnalysisLabel(v.decider, t, Set.empty, Set(t))) + decider.assume(labelledPcsPre, pcsPreExp, s"precondition of ${function.name}", enforceAssumption=false, precondAnalysisSourceInfos) v.decider.prover.saturate(Verifier.config.proverSaturationTimeouts.afterContract) - eval(s1, body, FunctionNotWellformed(function), v)((s2, tBody, bodyNew, _) => { + eval(s1, body, FunctionNotWellformed(function), v, analysisInfosBody)((s2, tBody, bodyNew, _) => { val debugExp = if (wExp) { val e = ast.EqCmp(ast.Result(function.typ)(), body)(function.pos, function.info, function.errT) val eNew = ast.EqCmp(ast.Result(function.typ)(), bodyNew.get)(function.pos, function.info, function.errT) Some(DebugExp.createInstance(e, eNew)) } else { None } - decider.assume(BuiltinEquals(data.formalResult, tBody), debugExp) - consumes(s2, posts, false, postconditionViolated, v)((s3, _, _) => { + decider.assume(BuiltinEquals(data.formalResult, tBody), debugExp, analysisInfosBody) + consumes(s2, posts, false, postconditionViolated, v, analysisInfosPostcondition)((s3, _, _) => { recorders :+= s3.functionRecorder Success()})})})} @@ -285,9 +301,9 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver result } - private def emitAndRecordFunctionAxioms(axiom: Term*): Unit = { - decider.prover.assumeAxioms(InsertionOrderedSet(axiom), "Function axioms") - emittedFunctionAxioms = emittedFunctionAxioms ++ axiom + protected def emitAndRecordFunctionAxioms(axiom: (Term, DependencyAnalysisAxiomInfo)*): Unit = { + decider.prover.assumeAxioms(InsertionOrderedSet(axiom.map(_._1)), "Function axioms") + emittedFunctionAxiomsWithInfo = emittedFunctionAxiomsWithInfo ++ axiom } private def generateFunctionSymbolsAfterVerification: Iterable[Either[String, Decl]] = { @@ -332,7 +348,7 @@ trait DefaultFunctionVerificationUnitProvider extends VerifierComponent { v: Ver def reset(): Unit = { program = null functionData = Map.empty - emittedFunctionAxioms = Vector.empty + emittedFunctionAxiomsWithInfo = Vector.empty freshVars = Vector.empty } diff --git a/src/main/scala/supporters/qps/PredicateAndWandSnapFunctionsContributor.scala b/src/main/scala/supporters/qps/PredicateAndWandSnapFunctionsContributor.scala index 3da5802d1..3e6a96e5f 100644 --- a/src/main/scala/supporters/qps/PredicateAndWandSnapFunctionsContributor.scala +++ b/src/main/scala/supporters/qps/PredicateAndWandSnapFunctionsContributor.scala @@ -10,13 +10,13 @@ package viper.silicon.supporters.qps +import viper.silicon.state.chunks.MagicWandIdentifier import viper.silver.ast import viper.silver.ast.utility.QuantifiedPermissions.QuantifiedPermissionAssertion import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.{Config, Map} import viper.silicon.interfaces.{PreambleContributor, PreambleReader} import viper.silicon.interfaces.decider.{ProverLike, TermConverter} -import viper.silicon.state.MagicWandIdentifier import viper.silicon.state.terms.{Sort, SortDecl, sorts} import viper.silver.ast.PredicateAccess diff --git a/src/main/scala/verifier/BaseVerifier.scala b/src/main/scala/verifier/BaseVerifier.scala index 7c1cf2081..7d7b2571d 100644 --- a/src/main/scala/verifier/BaseVerifier.scala +++ b/src/main/scala/verifier/BaseVerifier.scala @@ -9,18 +9,17 @@ package viper.silicon.verifier import com.typesafe.scalalogging.Logger import org.slf4j.LoggerFactory import viper.silicon.Config.StateConsolidationMode -import viper.silver.components.StatefulComponent -import viper.silicon.{utils, _} import viper.silicon.decider.{DefaultDeciderProvider, TermToSMTLib2Converter} import viper.silicon.logger.{MemberSymbExLogger, NoopMemberSymbExLog} +import viper.silicon.reporting.DefaultStateFormatter +import viper.silicon.rules._ import viper.silicon.state._ +import viper.silicon.state.chunks.{ChunkFactory, DefaultChunkFactory} import viper.silicon.state.terms.{AxiomRewriter, TriggerGenerator} import viper.silicon.supporters._ -import viper.silicon.reporting.DefaultStateFormatter -import viper.silicon.rules.{DefaultStateConsolidator, LastRetryFailOnlyStateConsolidator, LastRetryStateConsolidator, MinimalRetryingStateConsolidator, MinimalStateConsolidator, MoreComplexExhaleStateConsolidator, RetryingFailOnlyStateConsolidator, RetryingStateConsolidator, StateConsolidationRules} import viper.silicon.utils.Counter -import viper.silver.ast -import viper.silver.reporter.AnnotationWarning +import viper.silicon.{utils, _} +import viper.silver.components.StatefulComponent import scala.collection.mutable @@ -54,6 +53,7 @@ abstract class BaseVerifier(val config: Config, val termConverter = new TermToSMTLib2Converter(/*bookkeeper*/) val domainTranslator = new DefaultDomainsTranslator() val identifierFactory = new DefaultIdentifierFactory(uniqueId) + val chunkFactory: ChunkFactory = new DefaultChunkFactory() val triggerGenerator = new TriggerGenerator() val axiomRewriter = new AxiomRewriter(new utils.Counter()/*, bookkeeper.logfiles(s"axiomRewriter")*/, triggerGenerator) val quantifierSupporter = new DefaultQuantifierSupporter(triggerGenerator) diff --git a/src/main/scala/verifier/DefaultMainVerifier.scala b/src/main/scala/verifier/DefaultMainVerifier.scala index f650ecac9..f8531d89c 100644 --- a/src/main/scala/verifier/DefaultMainVerifier.scala +++ b/src/main/scala/verifier/DefaultMainVerifier.scala @@ -6,18 +6,10 @@ package viper.silicon.verifier -import viper.silicon.debugger.SiliconDebugger import viper.silicon.Config.{ExhaleMode, JoinMode} - -import java.text.SimpleDateFormat -import java.util.concurrent._ -import scala.annotation.unused -import scala.collection.mutable -import scala.util.Random -import viper.silver.ast -import viper.silver.components.StatefulComponent import viper.silicon._ import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.debugger.SiliconDebugger import viper.silicon.decider.SMTLib2PreambleReader import viper.silicon.extensions.ConditionalPermissionRewriter import viper.silicon.interfaces._ @@ -25,18 +17,27 @@ import viper.silicon.interfaces.decider.ProverLike import viper.silicon.logger.{MemberSymbExLogger, SymbExLogger} import viper.silicon.reporting.{MultiRunRecorders, condenseToViperResult} import viper.silicon.state._ +import viper.silicon.state.chunks.MagicWandIdentifier import viper.silicon.state.terms.{Decl, Sort, Term, sorts} -import viper.silicon.supporters.{AnnotationSupporter, DefaultDomainsContributor, DefaultMapsContributor, DefaultMultisetsContributor, DefaultPredicateVerificationUnitProvider, DefaultSequencesContributor, DefaultSetsContributor, MagicWandSnapFunctionsContributor, PredicateData} -import viper.silicon.supporters.qps._ +import viper.silicon.supporters._ import viper.silicon.supporters.functions.{DefaultFunctionVerificationUnitProvider, FunctionData} +import viper.silicon.supporters.qps._ import viper.silicon.utils.Counter +import viper.silver.ast import viper.silver.ast.utility.rewriter.Traverse import viper.silver.ast.{BackendType, Member} import viper.silver.cfg.silver.SilverCfg +import viper.silver.components.StatefulComponent import viper.silver.frontend.FrontendStateCache import viper.silver.reporter._ import viper.silver.verifier.VerifierWarning +import java.text.SimpleDateFormat +import java.util.concurrent._ +import scala.annotation.unused +import scala.collection.mutable +import scala.util.Random + /* TODO: Extract a suitable MainVerifier interface, probably including * - def verificationPoolManager: VerificationPoolManager) * - def uniqueIdCounter: String) @@ -46,6 +47,7 @@ trait MainVerifier extends Verifier { def nextUniqueVerifierId(): String def verificationPoolManager: VerificationPoolManager def rootSymbExLogger: SymbExLogger[_ <: MemberSymbExLogger] + def createWorkerVerifier(): WorkerVerifier } class DefaultMainVerifier(config: Config, @@ -67,6 +69,8 @@ class DefaultMainVerifier(config: Config, symbExLog = rootSymbExLogger.openMemberScope(member, decider.pcs) } + def createWorkerVerifier(): WorkerVerifier = new WorkerVerifier(this, nextUniqueVerifierId(), reporter, debugMode) + protected val preambleReader = new SMTLib2PreambleReader protected val sequencesContributor = new DefaultSequencesContributor(domainTranslator, config) @@ -79,17 +83,17 @@ class DefaultMainVerifier(config: Config, protected val predicateAndWandSnapFunctionsContributor = new DefaultPredicateAndWandSnapFunctionsContributor(preambleReader, termConverter, predSnapGenerator, config) protected val magicWandSnapFunctionsContributor = new MagicWandSnapFunctionsContributor(preambleReader) - private val _verificationPoolManager: VerificationPoolManager = new VerificationPoolManager(this) + protected lazy val _verificationPoolManager: VerificationPoolManager = new VerificationPoolManager(this) def verificationPoolManager: VerificationPoolManager = _verificationPoolManager - private val statefulSubcomponents = List[StatefulComponent]( + private lazy val statefulSubcomponents = List[StatefulComponent]( uniqueIdCounter, sequencesContributor, setsContributor, multisetsContributor, mapsContributor, domainsContributor, fieldValueFunctionsContributor, predSnapGenerator, predicateAndWandSnapFunctionsContributor, magicWandSnapFunctionsContributor, functionsSupporter, predicateSupporter, - _verificationPoolManager, + verificationPoolManager, MultiRunRecorders /* In lieu of a better place, include MultiRunRecorders singleton here */ ) @@ -116,7 +120,11 @@ class DefaultMainVerifier(config: Config, /* Verifier orchestration */ - private object allProvers extends ProverLike { + def allProvers: AllProvers = DefaultAllProvers + + protected object DefaultAllProvers extends AllProvers + + trait AllProvers extends ProverLike { def emit(content: String): Unit = { decider.prover.emit(content) _verificationPoolManager.pooledVerifiers.emit(content) @@ -127,6 +135,11 @@ class DefaultMainVerifier(config: Config, _verificationPoolManager.pooledVerifiers.emit(contents) } + def assume(term: Term, label: String): Unit = { + decider.prover.assume(term, label) + _verificationPoolManager.pooledVerifiers.assume(term, label) + } + def assume(term: Term): Unit = { decider.prover.assume(term) _verificationPoolManager.pooledVerifiers.assume(term) @@ -165,6 +178,10 @@ class DefaultMainVerifier(config: Config, /* Program verification */ + def verifyMember(doVerify: Unit => Seq[VerificationResult], v: Verifier, member: ast.Member): Seq[VerificationResult] = { + doVerify() + } + def verify(originalProgram: ast.Program, cfgs: Seq[SilverCfg], inputFile: Option[String]): List[VerificationResult] = { /** Trigger computation is currently not thread-safe; hence, all triggers are computed * up-front, before the program is verified in parallel. @@ -225,8 +242,8 @@ class DefaultMainVerifier(config: Config, val startTime = System.currentTimeMillis() var results: Seq[VerificationResult] = null try { - results = functionsSupporter.verify(createInitialState(function, program, functionData, predicateData), function) - .flatMap(extractAllVerificationResults) + results = verifyMember(_ => functionsSupporter.verify(createInitialState(function, program, functionData, predicateData), function) + .flatMap(extractAllVerificationResults), this, function) } catch { case e : Throwable => logger error s"An exception was thrown while verifying function `${function.name}`." @@ -242,8 +259,8 @@ class DefaultMainVerifier(config: Config, val startTime = System.currentTimeMillis() var results: Seq[VerificationResult] = null try { - results = predicateSupporter.verify(createInitialState(predicate, program, functionData, predicateData), predicate) - .flatMap(extractAllVerificationResults) + results = verifyMember(_ => predicateSupporter.verify(createInitialState(predicate, program, functionData, predicateData), predicate) + .flatMap(extractAllVerificationResults), this, predicate) } catch { case e: Throwable => logger error s"An exception was thrown while verifying predicate `${predicate.name}`." @@ -277,8 +294,7 @@ class DefaultMainVerifier(config: Config, val startTime = System.currentTimeMillis() var results: Seq[VerificationResult] = null try { - results = v.methodSupporter.verify(s, method) - .flatMap(extractAllVerificationResults) + results = verifyMember(_ => v.methodSupporter.verify(s, method).flatMap(extractAllVerificationResults), v, method) } catch { case e: Throwable => logger error s"An exception was thrown while verifying method `${method.name}`." @@ -325,18 +341,22 @@ class DefaultMainVerifier(config: Config, } reporter report VerificationTerminationMessage() - val verificationResults = ( functionVerificationResults - ++ predicateVerificationResults - ++ methodVerificationResults) + val verificationResults = (functionVerificationResults + ++ predicateVerificationResults + ++ methodVerificationResults) if (Verifier.config.enableDebugging()){ val debugger = new SiliconDebugger(verificationResults, identifierFactory, reporter, FrontendStateCache.resolver, FrontendStateCache.pprogram, FrontendStateCache.translator, this) debugger.startDebugger() } + afterVerification(verificationResults, program, inputFile) + verificationResults } + def afterVerification(verificationResults: List[VerificationResult], program: ast.Program, inputFile: Option[String]): Unit = {} + private def createInitialState(member: ast.Member, program: ast.Program, functionData: Map[String, FunctionData], diff --git a/src/main/scala/verifier/DependencyAnalysisAwareVerificationPoolManager.scala b/src/main/scala/verifier/DependencyAnalysisAwareVerificationPoolManager.scala new file mode 100644 index 000000000..601aba4d4 --- /dev/null +++ b/src/main/scala/verifier/DependencyAnalysisAwareVerificationPoolManager.scala @@ -0,0 +1,31 @@ +// 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.silicon.verifier + +import viper.silicon.common.collections.immutable.InsertionOrderedSet +import viper.silicon.decider.DependencyAnalysisProverHandler +import viper.silicon.dependencyAnalysis.DependencyAnalysisAxiomInfo +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.state.terms.Term + +class DependencyAnalysisAwareVerificationPoolManager(mainVerifier: MainVerifier) extends VerificationPoolManager(mainVerifier) { + + def getAllDAAxiomNodes: Seq[DependencyAnalysisNode] = workerVerifiers.flatMap(v => v.decider.prover match { + case d: DependencyAnalysisProverHandler => d.getPreambleAnalysisNodes + case _ => Set.empty + }) + + override def pooledVerifiers: DefaultPooledVerifiers with DependencyAnalysisProverHandler = DependencyAnalysisAwarePooledVerifiers + + private object DependencyAnalysisAwarePooledVerifiers extends DefaultPooledVerifiers with DependencyAnalysisProverHandler { + override def assumeAxiomsWithAnalysisInfo(axioms: InsertionOrderedSet[(Term, DependencyAnalysisAxiomInfo)], description: String): Unit = + workerVerifiers foreach { + case daV: DependencyAnalysisAwareWorkerVerifier => daV.decider.prover.assumeAxiomsWithAnalysisInfo(axioms, description) + case _ => super.assumeAxioms(axioms.map(_._1), description) + } + } +} diff --git a/src/main/scala/verifier/DependencyAnalysisAwareVerifier.scala b/src/main/scala/verifier/DependencyAnalysisAwareVerifier.scala new file mode 100644 index 000000000..1e6b7ac1d --- /dev/null +++ b/src/main/scala/verifier/DependencyAnalysisAwareVerifier.scala @@ -0,0 +1,71 @@ +// 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.silicon.verifier + +import viper.silicon.Config +import viper.silicon.decider.{DependencyAnalysisAwareDeciderProvider, DependencyAnalysisProverHandler} +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.cliTool.DependencyAnalysisTool +import viper.silicon.interfaces.VerificationResult +import viper.silicon.logger.{MemberSymbExLogger, SymbExLogger} +import viper.silicon.state.chunks._ +import viper.silicon.supporters.DependencyAnalysisAwareMethodVerificationUnitProvider +import viper.silicon.supporters.functions.DependencyAnalysisAwareFunctionVerificationUnitProvider +import viper.silver.ast +import viper.silver.reporter.Reporter + +trait DependencyAnalysisAwareVerifier extends BaseVerifier with DependencyAnalysisAwareDeciderProvider { + + override val chunkFactory: ChunkFactory = new DependencyAnalysisAwareChunkFactory(DADecider) + +} + +class DependencyAnalysisAwareMainVerifier(config: Config, + override val reporter: Reporter, + override val rootSymbExLogger: SymbExLogger[_ <: MemberSymbExLogger]) + extends DefaultMainVerifier(config, reporter, rootSymbExLogger) + with DependencyAnalysisAwareVerifier + with DependencyAnalysisAwareFunctionVerificationUnitProvider { + + override protected lazy val _verificationPoolManager: DependencyAnalysisAwareVerificationPoolManager = new DependencyAnalysisAwareVerificationPoolManager(this) + + override def verificationPoolManager: DependencyAnalysisAwareVerificationPoolManager = _verificationPoolManager + + override def createWorkerVerifier(): DependencyAnalysisAwareWorkerVerifier = new DependencyAnalysisAwareWorkerVerifier(this, nextUniqueVerifierId(), reporter, debugMode) + + override def allProvers: AllProvers with DependencyAnalysisProverHandler = DependencyAnalysisAwareAllProvers + + private object DependencyAnalysisAwareAllProvers extends AllProvers with DependencyAnalysisProverHandler + + override def verifyMember(doVerify: Unit => Seq[VerificationResult], v: Verifier, member: ast.Member): Seq[VerificationResult] = { + v match { + case daVerifier: DependencyAnalysisAwareVerifier => + daVerifier.decider.initDependencyAnalyzer(member, allProvers.getPreambleAnalysisNodes ++ daVerifier.decider.prover.getPreambleAnalysisNodes + ++ _verificationPoolManager.getAllDAAxiomNodes) + val result = super.verifyMember(doVerify, daVerifier, member) + daVerifier.decider.resetDependencyAnalyzer() + result + case _ => super.verifyMember(doVerify, v, member) + } + } + + override def afterVerification(verificationResults: List[VerificationResult], program: ast.Program, inputFile: Option[String]): Unit = { + val dependencyAnalysisResult = DependencyAnalysisTool.runDependencyAnalysisWorkflow(verificationResults, program, inputFile) + (reporter, dependencyAnalysisResult) match { + case (analysisReporter: DependencyAnalysisReporter, Some(res)) => + analysisReporter.joinedDependencyGraphInterpreter = Some(res.dependencyGraphInterpreter) + case _ => + } + } +} + +class DependencyAnalysisAwareWorkerVerifier(mainVerifier: DependencyAnalysisAwareMainVerifier, + uniqueId: String, + override val reporter: Reporter, + override val debugMode: Boolean) + extends WorkerVerifier(mainVerifier, uniqueId, reporter, debugMode) + with DependencyAnalysisAwareVerifier with DependencyAnalysisAwareMethodVerificationUnitProvider diff --git a/src/main/scala/verifier/VerificationPoolManager.scala b/src/main/scala/verifier/VerificationPoolManager.scala index f694fb4ca..b35e07272 100644 --- a/src/main/scala/verifier/VerificationPoolManager.scala +++ b/src/main/scala/verifier/VerificationPoolManager.scala @@ -6,15 +6,16 @@ package viper.silicon.verifier -import java.util.concurrent._ -import org.apache.commons.pool2.{BasePooledObjectFactory, ObjectPool, PoolUtils, PooledObject} import org.apache.commons.pool2.impl.{DefaultPooledObject, GenericObjectPool, GenericObjectPoolConfig} +import org.apache.commons.pool2.{BasePooledObjectFactory, ObjectPool, PoolUtils, PooledObject} import viper.silicon.Config import viper.silicon.common.collections.immutable.InsertionOrderedSet import viper.silicon.interfaces.VerificationResult -import viper.silver.components.StatefulComponent import viper.silicon.interfaces.decider.ProverLike import viper.silicon.state.terms.{Decl, Term} +import viper.silver.components.StatefulComponent + +import java.util.concurrent._ class VerificationPoolManager(mainVerifier: MainVerifier) extends StatefulComponent { private val numberOfWorkers: Int = Verifier.config.numberOfParallelVerifiers() @@ -23,10 +24,15 @@ class VerificationPoolManager(mainVerifier: MainVerifier) extends StatefulCompon /*private*/ var threadPool: ForkJoinPool = _ /*private*/ var workerVerifierPool: ObjectPool[WorkerVerifier] = _ - private[verifier] object pooledVerifiers extends ProverLike { + def pooledVerifiers: DefaultPooledVerifiers = _pooledVerifiers + + private[verifier] object _pooledVerifiers extends DefaultPooledVerifiers + + trait DefaultPooledVerifiers extends ProverLike { def emit(content: String): Unit = workerVerifiers foreach (_.decider.prover.emit(content)) override def emit(contents: Iterable[String]): Unit = workerVerifiers foreach (_.decider.prover.emit(contents)) def assume(term: Term): Unit = workerVerifiers foreach (_.decider.prover.assume(term)) + def assume(term: Term, label: String): Unit = workerVerifiers foreach (_.decider.prover.assume(term, label)) override def assumeAxioms(terms: InsertionOrderedSet[Term], description: String): Unit = workerVerifiers foreach (_.decider.prover.assumeAxioms(terms, description)) def declare(decl: Decl): Unit = workerVerifiers foreach (_.decider.prover.declare(decl)) def comment(content: String): Unit = workerVerifiers foreach (_.decider.prover.comment(content)) @@ -85,7 +91,7 @@ class VerificationPoolManager(mainVerifier: MainVerifier) extends StatefulCompon private object workerVerifierPoolableObjectFactory extends BasePooledObjectFactory[WorkerVerifier] { def create(): WorkerVerifier = { - val worker = new WorkerVerifier(mainVerifier, mainVerifier.nextUniqueVerifierId(), mainVerifier.reporter, Verifier.config.enableDebugging()) + val worker = mainVerifier.createWorkerVerifier() workerVerifiers = worker +: workerVerifiers worker diff --git a/src/main/scala/verifier/Verifier.scala b/src/main/scala/verifier/Verifier.scala index d012082f7..799061459 100644 --- a/src/main/scala/verifier/Verifier.scala +++ b/src/main/scala/verifier/Verifier.scala @@ -7,15 +7,16 @@ package viper.silicon.verifier import com.typesafe.scalalogging.Logger +import viper.silicon.Config import viper.silicon.decider.Decider +import viper.silicon.logger.MemberSymbExLogger import viper.silicon.reporting.StateFormatter -import viper.silicon.state.terms.{AxiomRewriter, TriggerGenerator} import viper.silicon.rules.{HeapSupportRules, StateConsolidationRules, defaultHeapSupporter} -import viper.silicon.state.{Heap, IdentifierFactory, State, SymbolConverter} +import viper.silicon.state._ +import viper.silicon.state.chunks.ChunkFactory +import viper.silicon.state.terms.{AxiomRewriter, TriggerGenerator} import viper.silicon.supporters.{QuantifierSupporter, SnapshotSupporter} import viper.silicon.utils.Counter -import viper.silicon.Config -import viper.silicon.logger.MemberSymbExLogger import viper.silver.ast import viper.silver.reporter.Reporter @@ -24,6 +25,8 @@ import java.util.concurrent.atomic.AtomicInteger trait Verifier { def uniqueId: String + def config: Config + def symbExLog: MemberSymbExLogger def openSymbExLogger(member: ast.Member): Unit def logger: Logger @@ -34,6 +37,7 @@ trait Verifier { def symbolConverter: SymbolConverter def stateFormatter: StateFormatter def identifierFactory: IdentifierFactory + def chunkFactory: ChunkFactory def triggerGenerator: TriggerGenerator def axiomRewriter: AxiomRewriter def quantifierSupporter: QuantifierSupporter diff --git a/src/test/resources/dependencyAnalysisTests/README.md b/src/test/resources/dependencyAnalysisTests/README.md new file mode 100644 index 000000000..c2e46b9d9 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/README.md @@ -0,0 +1,35 @@ +# Dependency Analysis Test + +## Soundness Tests + +The programs for annotated tests can be found in the folders `all`, `unitTests`, and `mce`. + +Pruning tests are executed against all annotated tests and additionally the programs in folder `real-world-examples`. + +Both are executed using `src/test/scala/DependencyAnalysisTests.scala`. + +By default, only expected dependencies are checked (`@dependency` annotations). +Setting `CHECK_PRECISION=true` additionally checks precision through `@irrelevant` annotations and reports false positives as an error. + +## Precision Benchmark + +The base programs are defined in folder `unitTests` and interferences in `precisionTests\scripts\interferences.txt`. + +Executing the benchmark: + +1. Generate test programs: `python precisionTests\scripts\precision_test_generator.py` + 1. Generates a number of subfolders in `precisionTests`. + +1. Execute precision benchmark using `src/test/scala/DependencyAnalysisPrecisionBenchmark.scala`. + 1. Results are written to `precisionTests\results\results_{timestamp}.out` + +1. Plot the result: `python precisionTests\scripts\precision_benchmark_plotter.py` + 1. Input file name: name of the result file (e.g., `result_2025-09-22_20-07-27.out`) + 1. Each table cell represents the precision computed as `#(real dependencies) / #(reported dependencies)`. + Real dependencies are computed as `#(reported dependencies) - #(reported dependencies annotated as irrelevant)`. + + +The manually crafted examples correspond to programs found in folders `all` and `real-world-examples`. + +To analyze the cause of imprecision, `src/test/scala/DependencyAnalysisTests.scala` with `CHECK_PRECISION=true` can be executed on the program of interest. +Imprecise results are reported as `Unexpected dependency` and make the test fail. \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/aliasing.vpr b/src/test/resources/dependencyAnalysisTests/all/aliasing.vpr new file mode 100644 index 000000000..cd715d84f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/aliasing.vpr @@ -0,0 +1,48 @@ +field f: Int + + +method maybeAlias(a: Ref, b: Ref, c: Bool, n: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L2")((acc(a.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L3")((acc(b.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L4")((c ==> a == b)) + requires @dependencyInfo("assumptionType:Precondition, label:L5")((a.f > 0 && n > 0 && b.f >= 0)) + requires @dependencyInfo("assumptionType:Precondition, label:L6")((a.f < 100)) + requires @dependencyInfo("assumptionType:Precondition, label:L7")((!c ==> a.f < b.f)) +{ + if(@dependencyInfo("assumptionType:PathCondition, label:L8")(c)){ + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L9, expectedDependencies:[L2,L3,L4,L8]") + a.f := n + 1 + } +} + +method aliasing1(x: Ref, n: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L10")((acc(x.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L11")((n > 0)) + requires @dependencyInfo("assumptionType:Precondition, label:L12")((x.f > n)) +{ + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L13") + x.f := n + 1 + + var y: Ref + + @dependencyInfo("assumptionType:SourceCode, label:L14") + y := x + + @dependencyInfo("assertionType:Explicit, label:L15, expectedDependencies:[L10,L13,L14]") + assert y.f > n +} + + +method aliasing2(x: Ref, y: Ref, n: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L16")((acc(x.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L17")((acc(y.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L18")((n > 0)) + requires @dependencyInfo("assumptionType:Precondition, label:L19")((x.f > n)) +{ + if(@dependencyInfo("assumptionType:PathCondition, label:L20")((x == y))){ + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L21, expectedDependencies:[L16,L17,L20]") + x.f := n + 1 + } +} + + diff --git a/src/test/resources/dependencyAnalysisTests/all/branches.vpr b/src/test/resources/dependencyAnalysisTests/all/branches.vpr new file mode 100644 index 000000000..93a6f4548 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/branches.vpr @@ -0,0 +1,78 @@ +method branches1(a: Int, b: Int) +{ + var n:Int, c: Bool + + assume @dependencyInfo("assumptionType:Explicit, label:L1")(0 < a) + assume @dependencyInfo("assumptionType:Explicit, label:L2")(b > 4) + + assume @dependencyInfo("assumptionType:Explicit, label:L3")(a < 100) + assume @dependencyInfo("assumptionType:Explicit, label:L4")(b < 50) + assume @dependencyInfo("assumptionType:Explicit, label:L5")(!c ==> a > 5) + + if(@dependencyInfo("assumptionType:PathCondition, label:L6")(c)){ + @dependencyInfo("assumptionType:SourceCode, label:L7") + n := a + 1 + }else{ + @dependencyInfo("assumptionType:SourceCode, label:L8") + n := b + 1 + } + + @dependencyInfo("assertionType:Explicit, label:L9, expectedDependencies:[L1,L2,L7,L8]") + assert n > 1 +} + +method branches2(a: Int, b: Int) +{ + var n:Int, c: Bool + + assume @dependencyInfo("assumptionType:Explicit, label:L10")(((0 < a))) + assume @dependencyInfo("assumptionType:Explicit, label:L11")(((a < 100))) + assume @dependencyInfo("assumptionType:Explicit, label:L12")(((a < b))) + assume @dependencyInfo("assumptionType:Explicit, label:L13")(((b < 50))) + + var x: Int + if(@dependencyInfo("assumptionType:PathCondition, label:L14")(((a >= n)))){ + @dependencyInfo("assumptionType:SourceCode, label:L15") + x := a + b + }else{ + @dependencyInfo("assumptionType:SourceCode, label:L16") + x := n + 1 + @dependencyInfo("assertionType:Explicit, label:L17, expectedDependencies:[L10,L14,L16]") + assert x > 1 + } + + @dependencyInfo("assertionType:Explicit, label:L22, expectedDependencies:[L16,L15,L14,L12]") + assert x > n +} + + +method nestedBranches1(a: Int, b: Int) +{ + var n:Int, c: Bool + + assume @dependencyInfo("assumptionType:Explicit, label:L23")(((0 < a))) + assume @dependencyInfo("assumptionType:Explicit, label:L24")(((a < 100))) + assume @dependencyInfo("assumptionType:Explicit, label:L25")(((0 < b))) + assume @dependencyInfo("assumptionType:Explicit, label:L26")(((b < 50))) + assume @dependencyInfo("assumptionType:Explicit, label:L27")(((c ==> a > 5))) + + if(@dependencyInfo("assumptionType:PathCondition, label:L28")(((c)))){ + if(@dependencyInfo("assumptionType:PathCondition, label:L29")(((a > b)))){ + @dependencyInfo("assumptionType:SourceCode, label:L30") + n := a - b + }else{ + @dependencyInfo("assumptionType:SourceCode, label:L31") + n := a + b + } + @dependencyInfo("assumptionType:SourceCode, label:L32") + n := n - 1 + }else{ + @dependencyInfo("assumptionType:SourceCode, label:L33") + n := a + b + } + + @dependencyInfo("assertionType:Explicit, label:L34, expectedDependencies:[L33,L32,L31,L30,L25]") + assert n <= a + b + @dependencyInfo("assertionType:Explicit, label:L35, expectedDependencies:[L28,L25,L30,L31,L32]") + assert c ==> n < a + b +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/divBy0.vpr b/src/test/resources/dependencyAnalysisTests/all/divBy0.vpr new file mode 100644 index 000000000..88a2711e8 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/divBy0.vpr @@ -0,0 +1,58 @@ +field f: Int + +method sum(x: Int, y: Int) returns(res: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L2, expectedDependencies:[L15]")(x >= 0) + requires @dependencyInfo("assumptionType:Precondition, label:L3, expectedDependencies:[L15,L17]")(y >= 0) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L4, expectedDependencies:[L7]")(res == x + y) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L5, expectedDependencies:[L6,L3,L7,L15,L17]")(res > 100) +{ + assume @dependencyInfo("assumptionType:Explicit, label:L6")(x > 100) + @dependencyInfo("assumptionType:SourceCode, label:L7") + res := x + y +} + +method divBy0() +{ + var x: Int + var y: Int + assume @dependencyInfo("assumptionType:Explicit, label:L8")(y > 0) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L9, expectedDependencies:[L8]") + x := x/y +} + +method basic() +{ + var x: Int + var y: Int + assume @dependencyInfo("assumptionType:Explicit, label:L10")(x > 0) + assume @dependencyInfo("assumptionType:Explicit, label:L11")(y > 0) + assume @dependencyInfo("assumptionType:Explicit, label:L12")(y < x) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L13") + x := x/y + @dependencyInfo("assertionType:Explicit, label:L14, expectedDependencies:[L11,L12,L13]") + assert x >= 1 +} + + +method sumClient(x: Int, y: Int) +{ + assume @dependencyInfo("assumptionType:Explicit, label:L15")(x >= 0) + assume @dependencyInfo("assumptionType:Explicit, label:L17")(x < y) + var n: Int + @dependencyInfo("assumptionType:MethodCall,assertionType:MethodCall, label:L18, expectedDependencies:[L15,L17]") + n := sum(x, y) + + // the following stmt reports dependency on n := sum(x, y) because (x < y && n == x + y && n > 100 ==> y != 0) + // although you could also prove it via (0 <= x && x < y ==> y != 0) + var n2: Int + var arg1: Int + @dependencyInfo("assumptionType:SourceCode,assertionType:SourceCode, label:L19, expectedDependencies:[L15,L17]") + arg1 := x/y + @dependencyInfo("assumptionType:MethodCall,assertionType:MethodCall, label:L20, expectedDependencies:[L15,L17,L19]") + n2 := sum(arg1, y) + @dependencyInfo("assumptionType:SourceCode, label:L21") + n := n + n2 + + @dependencyInfo("assertionType:Explicit, label:L22, expectedDependencies:[L15,L17,L18,L19,L20,L21,L4,L7]") + assert n >= x + 2*y +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/domain.vpr b/src/test/resources/dependencyAnalysisTests/all/domain.vpr new file mode 100644 index 000000000..a01046801 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/domain.vpr @@ -0,0 +1,71 @@ +field f: Int +field val: Int +define access(a) + (forall j: Int :: 0 <= j && j < len(a) ==> acc(slot(a,j).val)) + + +domain IArray { + function slot(a: IArray, i: Int): Ref + function len(a: IArray): Int + function first(r: Ref): IArray + function second(r: Ref): Int + + + axiom all_diff { + @dependencyInfo("assumptionType:DomainAxiom, label:L1")( + forall a: IArray, i: Int :: { slot(a,i) } + first(slot(a,i)) == a && second(slot(a,i)) == i + ) + } + + axiom len_nonneg { + @dependencyInfo("assumptionType:DomainAxiom, label:L2")( + forall a: IArray :: { len(a) } + len(a) >= 0 + ) + } +} + +method domain1() +{ + var a: IArray + + @dependencyInfo("assertionType:Explicit, label:L3a, expectedDependencies:[L2]") + assert len(a) >= 0 + + @dependencyInfo("assumptionType:Explicit, label:L3") + inhale len(a) == 3 + + @dependencyInfo("assumptionType:Explicit, label:L4") + inhale access(a) + + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L5a, expectedDependencies:[L3,L4]") + slot(a,0).val := 0 + + @dependencyInfo("assertionType:Explicit, label:L5b, expectedDependencies:[L3,L4,L5a,L1]") + assert slot(a,0).val == 0 + +} + +method domain3() +{ + var a: IArray + @dependencyInfo("assumptionType:Explicit, label:L6") + inhale len(a) == 3 + @dependencyInfo("assumptionType:Explicit, label:L7") + inhale access(a) + + var i: Int + @dependencyInfo("assumptionType:SourceCode, label:L8") + i := 0 + while (@dependencyInfo("assumptionType:PathCondition, label:L9")(i < len(a))) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L10, expectedDependencies:[L7]")(access(a)) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L11, expectedDependencies:[L8, L14]")(0 <= i) + invariant @dependencyInfo("assumptionType:LoopInvariant, label:L12, expectedDependencies:[L6,L8,L9,L14]")(i <= len(a)) + { + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L13, expectedDependencies:[L7,L10,L9,L11,L8,L14]") + slot(a,i).val := -i + @dependencyInfo("assumptionType:SourceCode, label:L14") + i := i + 1 + } +} diff --git a/src/test/resources/dependencyAnalysisTests/all/function-args.vpr b/src/test/resources/dependencyAnalysisTests/all/function-args.vpr new file mode 100644 index 000000000..be359d465 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/function-args.vpr @@ -0,0 +1,23 @@ +function foo(x: Int, y: Int) : Int + requires @dependencyInfo("assumptionType:Precondition, label:L1, expectedDependencies:[L4]")(x > 0) // importantly, L5 is NOT a dependency + requires @dependencyInfo("assumptionType:Precondition, label:L2, expectedDependencies:[L4, L5]")(y < 100) +{ + @dependencyInfo("assumptionType:FunctionBody, label:L3")(x) +} + +method clientFoo(){ + var x: Int, y: Int, z: Int + + @dependencyInfo("assumptionType:SourceCode, label:L4") + x := 19 + + @dependencyInfo("assumptionType:SourceCode, label:L5") + y := 10 + + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L6, expectedDependencies:[L4, L5]") + z := foo(x, x/y) + + @dependencyInfo("assertionType:Explicit, label:L7, expectedDependencies:[L6, L3, L5, L4]") + assert z == x + +} diff --git a/src/test/resources/dependencyAnalysisTests/all/function-sum.vpr b/src/test/resources/dependencyAnalysisTests/all/function-sum.vpr new file mode 100644 index 000000000..bab00300a --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/function-sum.vpr @@ -0,0 +1,52 @@ +field f: Int + +function sum(x: Int, y: Int) : Int + requires @dependencyInfo("assumptionType:Precondition, label:L2, expectedDependencies:[L11,L19]")(x >= 0) + requires @dependencyInfo("assumptionType:Precondition, label:L3, expectedDependencies:[L12,L20]")(y >= 0) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L4, expectedDependencies:[L6]")(result == x + y) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L5, expectedDependencies:[L6,L2,L3,L11,L12,L19,L20]")(result >= 0) +{ + @dependencyInfo("assumptionType:FunctionBody, label:L6")(x + y) +} + +function sumUnverified(x: Int, y: Int): Int + requires @dependencyInfo("assumptionType:Precondition, label:L7, expectedDependencies:[L15,L19]")(x >= 0) + requires @dependencyInfo("assumptionType:Precondition, label:L8, expectedDependencies:[L16,L20]")(y >= 0) + ensures @dependencyInfo("assertionType:ExplicitPostcondition, label:L9, expectedDependencies:[L7,L8,L15,L16,L19,L20]")(result == x + y) + ensures @dependencyInfo("assertionType:ExplicitPostcondition, label:L10, expectedDependencies:[L7,L8,L15,L16,L19,L20]")(result >= 0) + +method call2(){ + var x: Int, y: Int, z: Int + assume @dependencyInfo("assumptionType:Explicit, label:L11")(x > 10) + assume @dependencyInfo("assumptionType:Explicit, label:L12")(y > 0) + + @dependencyInfo("assumptionType:SourceCode, label:L13, expectedDependencies:[L11,L12]") + z := sum(x, y) + + @dependencyInfo("assertionType:Explicit, label:L14, expectedDependencies:[L11,L12,L13,L6]") + assert z == x + y +} + +method call2Unv(){ + var x: Int, y: Int, z: Int + assume @dependencyInfo("assumptionType:Explicit, label:L15")(x > 10) + assume @dependencyInfo("assumptionType:Explicit, label:L16")(y > 0) + + @dependencyInfo("assumptionType:SourceCode, label:L17, expectedDependencies:[L15,L16]") + z := sumUnverified(x, y) + + @dependencyInfo("assertionType:Explicit, label:L18, expectedDependencies:[L15,L16,L17,L7,L8,L9]") + assert z == x + y +} + +method client2() { + var x: Int, y: Int, z: Int + assume @dependencyInfo("assumptionType:Explicit, label:L19")(x > 10) + assume @dependencyInfo("assumptionType:Explicit, label:L20")(y > 0) + + @dependencyInfo("assumptionType:SourceCode, label:L21, expectedDependencies:[L19,L20]") + z := sum(x, y) + sumUnverified(x+x, y) + 4 + + @dependencyInfo("assertionType:Explicit, label:L22, expectedDependencies:[L19,L20,L21,L6,L7,L8,L9]") + assert z == 3*x + 2*y + 4 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/infeasible.vpr b/src/test/resources/dependencyAnalysisTests/all/infeasible.vpr new file mode 100644 index 000000000..35068099c --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/infeasible.vpr @@ -0,0 +1,118 @@ +field f: Int + +method infeasibleBranch1(a: Int, b: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L2")(a > 0) +{ + if(@dependencyInfo("assumptionType:PathCondition, label:L3")(a < 0)){ + @dependencyInfo("assertionType:Explicit, label:L4, expectedDependencies:[L2,L3]") + assert false + } +} + +method infeasibleBranch2(a: Int, b: Int) returns (res: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L5")(a > 0) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L9b, expectedDependencies:[L5,L6,L8]")(res > 0) +{ + if(@dependencyInfo("assumptionType:PathCondition, label:L6")(a < 0)){ + assume @dependencyInfo("assumptionType:Explicit, label:L7")(res < 0) + }else{ + assume @dependencyInfo("assumptionType:Explicit, label:L8")(res > 0) + } + @dependencyInfo("assertionType:Explicit, label:L9, expectedDependencies:[L5,L6,L8]") + assert res > 0 +} + +method clientInfeasibleBranch2() { + var a: Int, b: Int, res: Int + @dependencyInfo("assumptionType:SourceCode, label:LC1") + a := 10 + @dependencyInfo("assumptionType:MethodCall, assertionType:MethodCall, label:LC2, expectedDependencies:[LC1]") + res := infeasibleBranch2(a, b) + + @dependencyInfo("assertionType:Explicit, label:LC3, expectedDependencies:[LC1,LC2,L5,L6,L8,L9b]") + assert res > 0 +} + + +method infeasibleBranchPerm(a: Ref, b: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L10")(acc(a.f, 1/2)) + requires @dependencyInfo("assumptionType:Precondition, label:L11, expectedDependencies:[L10]")(a.f > 0) +{ + if(@dependencyInfo("assumptionType:PathCondition, label:L12, expectedDependencies:[L10]")(a.f < 0)){ + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L13, expectedDependencies:[L11,L12]") + a.f := a.f + 1 + } +} + + +method infeasibleBranchNoPerm(a: Int, b: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L14")(a > 0) +{ + var res: Int + if(@dependencyInfo("assumptionType:PathCondition, label:L15")(a < 0)){ + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L16, expectedDependencies:[L14,L15]") + res := b.f + 1 + @dependencyInfo("assertionType:Explicit, label:L17, expectedDependencies:[L14,L15]") + assert false + } +} + +method implicationTest() +{ + var a: Int + @dependencyInfo("assumptionType:SourceCode, label:L18") + a := 10 + @dependencyInfo("assertionType:Explicit, label:L19, expectedDependencies:[L18]") + assert a == 0 ==> false +} + +method implicationTest2() +{ + var a: Int + var x: Ref + @dependencyInfo("assumptionType:SourceCode, label:L20") + a := 10 + @dependencyInfo("assertionType:Explicit, label:L21, expectedDependencies:[L20]") + exhale a == 0 ==> acc(x.f) +} + +method clientA(b: Bool, x: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L22")(b ==> acc(x.f)) +{ + + while(@dependencyInfo("assumptionType:PathCondition, label:L23")(b)) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L24, expectedDependencies:[L22,L26]")(b ==> acc(x.f)) + { + @dependencyInfo("assertionType:Explicit, label:L25, expectedDependencies:[L22,L23,L24]") + exhale acc(x.f) + inhale @dependencyInfo("assumptionType:Explicit, label:L26")(acc(x.f)) + } +} + +field next : Ref +field val : Int + +predicate list(start : Ref) +{ + acc(start.val) && acc(start.next) +} + +method infeasible_unfold(l1 : Ref) + { + + assume @dependencyInfo("assumptionType:Explicit, label:L27, expectedDependencies:[]")(false) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L28, expectedDependencies:[L27]") + unfold list(l1) + @dependencyInfo("assertionType:Explicit, label:L29, expectedDependencies:[L27]") + assert l1.next == l1 + } + + +method assumeFalse() +{ + var a: Int + assume @dependencyInfo("assumptionType:Explicit, label:L30")(false) + + @dependencyInfo("assertionType:Explicit, label:L31, expectedDependencies:[L30]") + assert a == 2 +} diff --git a/src/test/resources/dependencyAnalysisTests/all/magicWands.vpr b/src/test/resources/dependencyAnalysisTests/all/magicWands.vpr new file mode 100644 index 000000000..69067568b --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/magicWands.vpr @@ -0,0 +1,108 @@ +field next : Ref +field val : Int +field f: Int + +predicate list(start : Ref) +{ + acc(start.val) && acc(start.next) && + (start.next != null && list(start.next)) +} + + +method basicApply() +{ + var x: Int + var y: Int + @dependencyInfo("assumptionType:Explicit, label:L1") + inhale x > 0 + @dependencyInfo("assumptionType:Explicit, label:L2") + inhale x > 0 --* y > 0 + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L3, expectedDependencies:[L1, L2]") + apply x > 0 --* y > 0 + + @dependencyInfo("assertionType:Explicit, label:L4, expectedDependencies:[L1, L2, L3]") + assert y > 0 +} + + +method basicPackage(l: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L5")(list(l)) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L6, expectedDependencies:[L5, L7, L8, L10, L11]")(list(l)) + { + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L7") + unfold list(l) + var tmp : Ref + @dependencyInfo("assumptionType:SourceCode, label:L8") + tmp := l.next + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L9, expectedDependencies:[L5,L7,L8]") + package list(tmp) --* list(l) + { + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L10") + fold list(l) + } + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L11, expectedDependencies:[L5, L7, L8, L10]") + apply list(tmp) --* list(l) + + @dependencyInfo("assertionType:Explicit, label:L12, expectedDependencies:[L5, L7, L8, L10, L11]") + assert list(l) +} + + +predicate greater0(a: Int){ + a > 0 +} + +method advancedPackage(a: Int, b: Int) + { + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L13") + package (greater0(a) && greater0(b)) --* greater0(a + b) + { + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L14") + unfold greater0(a) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L15") + unfold greater0(b) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L16, expectedDependencies:[L14,L15]") + fold greater0(a + b) + } + + @dependencyInfo("assumptionType:Explicit, label:L17") + inhale greater0(a) + @dependencyInfo("assumptionType:Explicit, label:L18") + inhale greater0(b) + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L19, expectedDependencies:[L14,L15,L16,L17,L18]") + apply (greater0(a) && greater0(b)) --* greater0(a + b) + + @dependencyInfo("assertionType:Explicit, label:L20, expectedDependencies:[L14,L15,L16,L17,L18,L19]") + assert greater0(a + b) +} + +method quantifiedWand(xs: Seq[Int], b: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L21")(|xs| > 2) + requires @dependencyInfo("assumptionType:Precondition, label:L22")((forall x: Int :: x in xs ==> (greater0(x) --* greater0(x + b)))) + { + @dependencyInfo("assumptionType:Explicit, label:L23") + inhale greater0(xs[0]) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L24, expectedDependencies:[L21,L22,L23]") + apply greater0(xs[0]) --* greater0(xs[0] + b) + + @dependencyInfo("assertionType:Explicit, label:L25, expectedDependencies:[L21,L22,L23,L24]") + assert greater0(xs[0] + b) +} + +method applying1() +{ + var x: Int + var y: Int + @dependencyInfo("assumptionType:Explicit, label:L35") + inhale x > 0 + @dependencyInfo("assumptionType:Explicit, label:L36") + inhale x > 0 --* y > 0 + + @dependencyInfo("assertionType:Explicit, label:L37, expectedDependencies:[L35,L36]") + assert applying (x > 0 --* y > 0) in y > 0 +} diff --git a/src/test/resources/dependencyAnalysisTests/all/method-sum.vpr b/src/test/resources/dependencyAnalysisTests/all/method-sum.vpr new file mode 100644 index 000000000..d668317a1 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/method-sum.vpr @@ -0,0 +1,57 @@ +field f: Int + +method sum(x: Int, y: Int) returns (res: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L2, expectedDependencies:[L11,L19]")(x >= 0) + requires @dependencyInfo("assumptionType:Precondition, label:L3, expectedDependencies:[L12,L20]")(y >= 0) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L4, expectedDependencies:[L6]")(res == x + y) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L5, expectedDependencies:[L6,L2,L3,L11,L12,L19,L20]")(res >= 0) +{ + @dependencyInfo("assumptionType:SourceCode, label:L6") + res := x + y +} + +method sumUnverified(x: Int, y: Int) returns (res: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L7, expectedDependencies:[L15,L19]")(x >= 0) + requires @dependencyInfo("assumptionType:Precondition, label:L8, expectedDependencies:[L16,L20]")(y >= 0) + ensures @dependencyInfo("assertionType:ExplicitPostcondition, label:L9, expectedDependencies:[L7,L8,L15,L16,L19,L20]")(res == x + y) + ensures @dependencyInfo("assertionType:ExplicitPostcondition, label:L10, expectedDependencies:[L7,L8,L15,L16,L19,L20]")(res >= 0) + +method call2(){ + var x: Int, y: Int, z: Int + assume @dependencyInfo("assumptionType:Explicit, label:L11")(x > 10) + assume @dependencyInfo("assumptionType:Explicit, label:L12")(y > 0) + + @dependencyInfo("assumptionType:MethodCall, assertionType:MethodCall, label:L13, expectedDependencies:[L11,L12]") + z := sum(x, y) + + @dependencyInfo("assertionType:Explicit, label:L14, expectedDependencies:[L11,L12,L13,L6,L4]") + assert z == x + y +} + +method call2Unv(){ + var x: Int, y: Int, z: Int + assume @dependencyInfo("assumptionType:Explicit, label:L15")(x > 10) + assume @dependencyInfo("assumptionType:Explicit, label:L16")(y > 0) + + @dependencyInfo("assumptionType:MethodCall, assertionType:MethodCall, label:L17, expectedDependencies:[L15,L16]") + z := sumUnverified(x, y) + + @dependencyInfo("assertionType:Explicit, label:L18, expectedDependencies:[L15,L16,L17,L7,L8,L9]") + assert z == x + y +} + +method client2() { + var x: Int, y: Int, z1: Int, z2: Int, z3: Int + assume @dependencyInfo("assumptionType:Explicit, label:L19")(x > 10) + assume @dependencyInfo("assumptionType:Explicit, label:L20")(y > 0) + + @dependencyInfo("assumptionType:MethodCall, assertionType:MethodCall, label:L21a, expectedDependencies:[L19,L20]") + z1 := sum(x, y) + @dependencyInfo("assumptionType:MethodCall, assertionType:MethodCall, label:L21b, expectedDependencies:[L19,L20]") + z2 := sumUnverified(x+x, y) + @dependencyInfo("assumptionType:SourceCode, label:L21c, expectedDependencies:[L19,L20]") + z3 := z1 + z2 + 4 + + @dependencyInfo("assertionType:Explicit, label:L22, expectedDependencies:[L19,L20,L21a,L21b,L21c,L4,L6,L7,L8,L9]") + assert z3 == 3*x + 2*y + 4 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/predicate-tuples.vpr b/src/test/resources/dependencyAnalysisTests/all/predicate-tuples.vpr new file mode 100644 index 000000000..4375f61de --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/predicate-tuples.vpr @@ -0,0 +1,50 @@ +field left: Int +field right: Int + +predicate tuple(this: Ref) { + acc(this.left) && acc(this.right) +} + +method setTuple(this: Ref, l: Int, r: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L4")((tuple(this))) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L5, expectedDependencies:[L4,L6]")(tuple(this)) +{ + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L6, expectedDependencies:[L4]") + unfold tuple(this) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L7, expectedDependencies:[L4,L6]") + this.left := l + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L8, expectedDependencies:[L4,L6]") + this.right := r + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L9, expectedDependencies:[L4,L6]") + fold tuple(this) +} + +predicate tupleB(this: Ref) { + acc(this.left) && acc(this.right) && this.left > 0 && this.right > 0 +} + +method addTuple(this: Ref) returns (sum: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L10")((acc(tupleB(this), 1/2))) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L11, expectedDependencies:[L10,L12,L14]")(acc(tupleB(this), 1/2)) +{ + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L12, expectedDependencies:[L10]") + unfold acc(tupleB(this), 1/2) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L13, expectedDependencies:[L10,L12]") + sum := this.left + this.right + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L14, expectedDependencies:[L10,L12]") + fold acc(tupleB(this), 1/2) +} + +method addTupleAndUpdate(this: Ref) returns (sum: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L15")((acc(tupleB(this)))) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L16, expectedDependencies:[L15,L17,L18,L19,L20]")(acc(tupleB(this))) +{ + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L17, expectedDependencies:[L15]") + unfold acc(tupleB(this)) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L18, expectedDependencies:[L15,L17]") + sum := this.left + this.right + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L19, expectedDependencies:[L15,L17]") + this.left := sum + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L20, expectedDependencies:[L15,L17,L18,L19]") + fold acc(tupleB(this)) +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/quantified-perm.vpr b/src/test/resources/dependencyAnalysisTests/all/quantified-perm.vpr new file mode 100644 index 000000000..abb5e9ba8 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/quantified-perm.vpr @@ -0,0 +1,36 @@ +field f: Int +field first : Ref +field second : Ref + + + +method quantifiedSum(nodes: Set[Ref], x: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L15")(forall n:Ref :: { n.first } n in nodes ==> + acc(n.first) && + (n.first != null ==> n.first in nodes)) + requires @dependencyInfo("assumptionType:Precondition, label:L18")(forall n:Ref :: { n.f } n in nodes ==> + acc(n.f) && 0 <= n.f && n.f <= 100) + requires @dependencyInfo("assumptionType:Precondition, label:L20")((x in nodes)) +{ + var a: Int + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L21") + a := x.f + if(@dependencyInfo("assumptionType:PathCondition, label:L22")((x.first != null))) { + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L23, expectedDependencies:[L20,L15,L18,L22]") + a := a + x.first.f + } + @dependencyInfo("assertionType:Explicit, label:L24, expectedDependencies:[L21,L23,L20,L15,L18,L22]") + assert a >= 0 +} + +field b: Bool + +function F(r: Ref): Bool + requires @dependencyInfo("assumptionType:Precondition, label:L25, expectedDependencies:[L26]")(acc(r.b, write)) +{ true } + +function Target(i: Ref): Bool + requires @dependencyInfo("assumptionType:Precondition, label:L26")(forall r: Ref :: acc(r.b, write)) +{ + @dependencyInfo("assumptionType:FunctionBody, label:L27, expectedDependencies:[L26]")(F(i)) +} diff --git a/src/test/resources/dependencyAnalysisTests/all/quantifiers-triggers.vpr b/src/test/resources/dependencyAnalysisTests/all/quantifiers-triggers.vpr new file mode 100644 index 000000000..9545c50ba --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/quantifiers-triggers.vpr @@ -0,0 +1,15 @@ +field val: Bool +function toBool(s: Ref): Bool +predicate GlobalMem() { + forall k: Ref :: { toBool(k) } toBool(k) ==> false +} + +method client(i: Ref, k: Ref) + requires toBool(k) + requires GlobalMem() + ensures GlobalMem() +{ + unfold GlobalMem() + assert false // should verify + fold GlobalMem() +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/sequences.vpr b/src/test/resources/dependencyAnalysisTests/all/sequences.vpr new file mode 100644 index 000000000..4fa512775 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/sequences.vpr @@ -0,0 +1,31 @@ + +method sequenceUpdate(xs: Seq[Int], ys: Seq[Int]) returns (res: Seq[Int]) + requires @dependencyInfo("assumptionType:Precondition, label:L1")((|xs| > 5)) + requires @dependencyInfo("assumptionType:Precondition, label:L2")((|ys| > 3)) +{ + inhale @dependencyInfo("assumptionType:Explicit, label:L3")(forall x: Int :: x in xs ==> x > 0) + inhale @dependencyInfo("assumptionType:Explicit, label:L4")(forall y: Int :: y in ys ==> y >= 0) + + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L5, expectedDependencies:[L1]") + res := xs[0 := 1] + + @dependencyInfo("assertionType:Explicit, label:L6, expectedDependencies:[L5,L1]") + assert res[0] == 1 + + @dependencyInfo("assertionType:Explicit, label:L12, expectedDependencies:[L5,L1,L3]") + assert forall x: Int :: x in res ==> x > 0 +} + +method sequence2(xs: Seq[Int], ys: Seq[Int]) returns (res: Seq[Int]) + requires @dependencyInfo("assumptionType:Precondition, label:L13")((|xs| > 5)) + requires @dependencyInfo("assumptionType:Precondition, label:L14")((|ys| > 3)) +{ + inhale @dependencyInfo("assumptionType:Explicit, label:L15")(forall x: Int :: x in xs ==> x > 0) + inhale @dependencyInfo("assumptionType:Explicit, label:L16")(forall y: Int :: y in ys ==> y >= 0) + + @dependencyInfo("assumptionType:SourceCode, label:L17") + res := xs[0 := 1] + + @dependencyInfo("assertionType:Explicit, label:L18, expectedDependencies:[L16]") + assert forall y: Int :: y in ys ==> y >= 0 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/state_consolidation.vpr b/src/test/resources/dependencyAnalysisTests/all/state_consolidation.vpr new file mode 100644 index 000000000..7d4f727e7 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/state_consolidation.vpr @@ -0,0 +1,42 @@ +field f: Int + +method stateConsolidation(x: Ref, y: Ref, z: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L2")((acc(x.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L3")((acc(y.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L4")((acc(z.f, 1/2))) +{ + var a: Ref + inhale @dependencyInfo("assumptionType:Explicit, label:L5")(acc(a.f, 1/2)) + + assume @dependencyInfo("assumptionType:Explicit, label:L6")(a == x) + + @dependencyInfo("assertionType:Explicit, label:L7, expectedDependencies:[L2,L3,L5,L6]") + assert a != y // triggers state consolidation + + @dependencyInfo("assertionType:Explicit, label:L13, expectedDependencies:[L2,L5,L6]") + assert perm(x.f) == write +} + + +method stateConsolidation3(x: Ref, y: Ref, z: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L14")((acc(x.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L15")((acc(y.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L16")((acc(z.f, 1/2))) +{ + var a: Ref + inhale @dependencyInfo("assumptionType:Explicit, label:L17")(acc(x.f, 1/2)) + + @dependencyInfo("assertionType:Explicit, label:L18, expectedDependencies:[L14,L17]") + assert perm(x.f) == write +} + + +method noAlias(a: Ref, b: Ref, c: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L19")((acc(a.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L20")((acc(b.f, 1/2))) + requires @dependencyInfo("assumptionType:Precondition, label:L21")((acc(c.f, 1/2))) +{ + + @dependencyInfo("assertionType:Explicit, label:L22, expectedDependencies:[L19,L20]") + assert a != b +} diff --git a/src/test/resources/dependencyAnalysisTests/all/sum-loops.vpr b/src/test/resources/dependencyAnalysisTests/all/sum-loops.vpr new file mode 100644 index 000000000..67c24a364 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/sum-loops.vpr @@ -0,0 +1,48 @@ +field f: Int + +method sum(n: Int) returns (res: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L2")((0 <= n)) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L3, expectedDependencies:[L6,L8,L4,L5,L7,L9,L10]")(res == n * (n + 1) / 2) +{ + @dependencyInfo("assumptionType:SourceCode, label:L4") + res := 0 + var i: Int + @dependencyInfo("assumptionType:SourceCode, label:L5") + i := 0; + while(@dependencyInfo("assumptionType:PathCondition, label:L6")((i <= n))) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L7, expectedDependencies:[L2,L5,L6,L10]")((i <= (n + 1))) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L8, expectedDependencies:[L4,L5,L9,L10]")((res == (i - 1) * i / 2)) + { + @dependencyInfo("assumptionType:SourceCode, label:L9") + res := res + i + @dependencyInfo("assumptionType:SourceCode, label:L10") + i := i + 1 + } + + @dependencyInfo("assertionType:Explicit, label:L11, expectedDependencies:[L6,L8,L4,L5,L9,L10]") + assert res == n * (n + 1) / 2 +} + + +method sumPerm1(n: Int, res: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L12")((acc(res.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L13")((0 <= n)) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L14, expectedDependencies:[L12,L19]")(acc(res.f)) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L15, expectedDependencies:[L12,L19,L16,L17,L22,L23,L18,L20,L21]")(res.f == n * (n + 1) / 2) +{ + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L16, expectedDependencies:[L12]") + res.f := 0 + var i: Int + @dependencyInfo("assumptionType:SourceCode, label:L17") + i := 0 + while(@dependencyInfo("assumptionType:PathCondition, label:L18")((i <= n))) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L19, expectedDependencies:[L12]")((acc(res.f))) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L20, expectedDependencies:[L13,L17,L18,L23]")((i <= (n + 1))) + invariant @dependencyInfo("assumptionType:LoopInvariant, assertionType:LoopInvariant, label:L21, expectedDependencies:[L12,L19,L16,L17,L22,L23]")((res.f == (i - 1) * i / 2)) + { + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L22, expectedDependencies:[L12]") + res.f := res.f + i + @dependencyInfo("assumptionType:SourceCode, label:L23") + i := i + 1 + } +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/all/unfolding.vpr b/src/test/resources/dependencyAnalysisTests/all/unfolding.vpr new file mode 100644 index 000000000..71b3beaca --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/all/unfolding.vpr @@ -0,0 +1,41 @@ +field f: Int + +predicate pred(x: Ref) +{ + acc(x.f) && x.f > 0 +} + + method foo(x: Ref) returns(res: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L3, expectedDependencies:[L9,L10,L11]")((pred(x))) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L4, expectedDependencies:[L9,L10,L11,L3,L6,L8]")((pred(x))) + ensures @dependencyInfo("assertionType:ImplicitPostcondition, label:L5, expectedDependencies:[L9,L10,L11,L3,L6,L7]")((res > 0)) + { + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L6, expectedDependencies:[L9,L10,L11,L3]") + unfold pred(x) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L7, expectedDependencies:[L9,L10,L11,L3,L6]") + res := x.f + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L8, expectedDependencies:[L9,L10,L11,L3,L6]") + fold pred(x) + } + + +method client1(x: Ref) +{ + var b: Int + inhale @dependencyInfo("assumptionType:Explicit, label:L9")((acc(x.f))) + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Annotation, label:L10, expectedDependencies:[L9]")((x.f > 0)) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L11, expectedDependencies:[L9,L10]") + fold pred(x) + + @dependencyInfo("assumptionType:MethodCall, label:L12, expectedDependencies:[L9,L10,L11]") + b := foo(x) + + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L13, expectedDependencies:[L9,L10,L11,L3,L6,L8,L12]") + b := unfolding pred(x) in x.f + + @dependencyInfo("assertionType:Explicit, label:L14, expectedDependencies:[L9,L10,L11,L3,L6,L8,L12,L13]") + assert b > 0 + + @dependencyInfo("assertionType:Explicit, label:L15, expectedDependencies:[L9,L10,L11,L3,L6,L8,L12]") + exhale pred(x) + } diff --git a/src/test/resources/dependencyAnalysisTests/guidance/assumptions-equal.vpr b/src/test/resources/dependencyAnalysisTests/guidance/assumptions-equal.vpr new file mode 100644 index 000000000..e7aa319b8 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/assumptions-equal.vpr @@ -0,0 +1,17 @@ +method client() +{ + var a1: Int, a2: Int, a3: Int, b1: Int, b2: Int, b3: Int, b4: Int + a1 := 10 + a2 := 20 + a3 := 30 + + assume @guidedAssumption("3")(b1 == 1) // least useful - is used besides three other facts in last assertion + assume @guidedAssumption("2")(b2 == 2) + assume @guidedAssumption("1")(b3 == 3) // most useful - used on its own in first assertion + assume @guidedAssumption("2")(b4 == 4) + + assert b3 == 3 + assert a1 + b2 == 12 + assert a1 + b4 == 14 + assert a1 + a2 + a3 + b1 == 61 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/guidance/assumptions.vpr b/src/test/resources/dependencyAnalysisTests/guidance/assumptions.vpr new file mode 100644 index 000000000..350c6332f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/assumptions.vpr @@ -0,0 +1,13 @@ +method client() +{ + var a: Int, b: Int, c: Int, d: Int + assume @guidedAssumption("3")(a == 1) // used for last assert + assume @guidedAssumption("1")(b == 2) // used for every assert + assume @guidedAssumption("2")(c == 3) // used for last two asserts + + d := a + b + + assert b == 2 + assert b + c == 5 + assert d + c == 6 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/guidance/assumptions2.vpr b/src/test/resources/dependencyAnalysisTests/guidance/assumptions2.vpr new file mode 100644 index 000000000..a8c53ceb0 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/assumptions2.vpr @@ -0,0 +1,18 @@ +method client() +{ + var a1: Int, a2: Int, a3: Int, b1: Int, b2: Int, b3: Int, b4: Int + a1 := 10 + a2 := 20 + a3 := 30 + + assume @guidedAssumption("4")(b1 == 1) // least useful - is used besides three other facts in last assertion + assume @guidedAssumption("2")(b2 == 2) + assume @guidedAssumption("1")(b3 == 3) // most useful - used on its own in first assertion + assume @guidedAssumption("3")(b4 == 4) + assume 2 == 2 // is not used at all + + assert b3 == 3 + assert a1 + b2 == 12 + assert a1 + a2 + b4 == 34 + assert a1 + a2 + a3 + b1 == 61 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/guidance/mix.vpr b/src/test/resources/dependencyAnalysisTests/guidance/mix.vpr new file mode 100644 index 000000000..f96643dcd --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/mix.vpr @@ -0,0 +1,33 @@ +method add(a: Int, b: Int) returns (res: Int) + requires b != 0 + ensures @guidedAssumption("1")(res == a + b) + + +method sub(a: Int, b: Int) returns (res: Int) + requires b != 0 + ensures @guidedAssumption("2")(res == a - b) + + +method client() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + b := 3 + @uncovered() + d := 5 + + c := add(a, b) + c := add(a, c) + c := sub(c, b) + + assert c == 4 +} + +method uncoveredClient() +{ + var e: Int, f: Int + @uncovered() + e := 6 + @uncovered() + f := 7 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods-equal.vpr b/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods-equal.vpr new file mode 100644 index 000000000..e2d955845 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods-equal.vpr @@ -0,0 +1,114 @@ +method m1() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + b := 3 + c := 4 + d := 5 + + a := a + 1 + b := b + 1 + c := c + 1 + d := d + 1 + + assert a != d + assert b != c +} + +method m2() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + @uncovered() + b := 3 + @uncovered() + c := 4 + d := 5 + + a := a + 1 + @uncovered() + b := b + 1 + @uncovered() + c := c + 1 + d := d + 1 + + assert a != d +} + +method m3() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + b := 3 + c := 4 + d := 5 + + assert a != d + + a := a + 1 + b := b + 1 + c := c + 1 + @uncovered() + d := d + 1 + + assert b != c + assert a != 0 +} + +method m4() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + @uncovered() + b := 3 + c := 4 + d := 5 + + a := a + 1 + @uncovered() + b := b + 1 + c := c + 1 + d := d + 1 + + assert a != d + assert c == 5 +} + +method m5() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + b := 3 + c := 4 + d := 5 + + assert a != d + + @uncovered() + a := a + 1 + b := b + 1 + c := c + 1 + d := d + 1 + + assert b != c + assert d != 0 +} + +method m6() +{ + var a: Int, b: Int, c: Int, d: Int + @uncovered() + a := 2 + b := 3 + c := 4 + d := 5 + + @uncovered() + a := a + 1 + b := b + 1 + c := c + 1 + d := d + 1 + + assert b != c + assert d == 6 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods.vpr b/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods.vpr new file mode 100644 index 000000000..49b9b219c --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods.vpr @@ -0,0 +1,75 @@ +method m1() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + b := 3 + c := 4 + d := 5 + + a := a + 1 + b := b + 1 + c := c + 1 + d := d + 1 + + assert a != d + assert b != c +} + +method m2() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + @uncovered() + b := 3 + @uncovered() + c := 4 + d := 5 + + a := a + 1 + @uncovered() + b := b + 1 + @uncovered() + c := c + 1 + d := d + 1 + + assert a != d +} + +method m3() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + b := 3 + c := 4 + d := 5 + + assert a != d + + a := a + 1 + b := b + 1 + c := c + 1 + @uncovered() + d := d + 1 + + assert b != c + assert a != 0 +} + +method m4() +{ + var a: Int, b: Int, c: Int, d: Int + a := 2 + @uncovered() + b := 3 + c := 4 + d := 5 + + a := a + 1 + @uncovered() + b := b + 1 + c := c + 1 + d := d + 1 + + assert a != d + assert c == 5 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods2.vpr b/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods2.vpr new file mode 100644 index 000000000..476680d39 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/guidance/uncovered-methods2.vpr @@ -0,0 +1,34 @@ +method helper() returns (res: Int) + ensures res == 10 +{ + res := 10 +} + +method m1() +{ + var a: Int + @uncovered() + a := helper() +} + +method m2() +{ + var a: Int + a := helper() + + assert a == 10 +} + +method m3() +{ + var a: Int + a := helper() + assert a == 10 + + @uncovered() + a := helper() + @uncovered() + a := helper() + @uncovered() + a := helper() +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/missingAnnotations/implicitUpperBounds.vpr b/src/test/resources/dependencyAnalysisTests/missingAnnotations/implicitUpperBounds.vpr new file mode 100644 index 000000000..88db5389f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/missingAnnotations/implicitUpperBounds.vpr @@ -0,0 +1,45 @@ +field f: Int + +method implicitUpperBounds(x: Ref, y: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L2")((acc(x.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L3")((acc(y.f, wildcard))) +{ + var z: Ref + inhale @dependencyInfo("assumptionType:Explicit, label:L4")(true --* acc(z.f)) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L5") + apply true --* acc(z.f) + + + @dependencyInfo("assertionType:Explicit, label:L6, expectedDependencies:[]") + assert x != y +} + +method implicitUpperBounds_quantified(gen_xs: Seq[Ref], y: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L7")((|gen_xs|> 2)) + requires @dependencyInfo("assumptionType:Precondition, label:L8")((forall gen_xi: Ref :: gen_xi in gen_xs ==> acc(gen_xi.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L9")((acc(y.f, wildcard))) +{ + var z: Ref + inhale @dependencyInfo("assumptionType:Explicit, label:L10")(true --* acc(z.f)) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L11") + apply true --* acc(z.f) + + + @dependencyInfo("assertionType:Explicit, label:L12, expectedDependencies:[]") + assert gen_xs[0] != y +} + +method implicitUpperBounds_quantified_2(gen_xs: Seq[Ref], y: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L13")((|gen_xs|> 2)) + requires @dependencyInfo("assumptionType:Precondition, label:L14")((forall gen_xi: Ref :: gen_xi in gen_xs ==> acc(gen_xi.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L15")((acc(y.f, 1/2))) +{ + var z: Ref + inhale @dependencyInfo("assumptionType:Explicit, label:L16")(true --* acc(z.f)) + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L17") + apply true --* acc(z.f) + + + @dependencyInfo("assertionType:Explicit, label:L18, expectedDependencies:[]") + assert gen_xs[0] != y +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/missingAnnotations/imprecision-fixed.vpr b/src/test/resources/dependencyAnalysisTests/missingAnnotations/imprecision-fixed.vpr new file mode 100644 index 000000000..f071143ea --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/missingAnnotations/imprecision-fixed.vpr @@ -0,0 +1,99 @@ +field f: Int + +method permTest(a: Ref, b: Ref, n: Int) + requires @dependencyInfo("assumptionType:Precondition, label:L2")(acc(a.f)) + requires @dependencyInfo("assumptionType:Precondition, label:L3")(acc(b.f)) + requires @dependencyInfo("assumptionType:Precondition, label:L3")(b.f > 0) +{ + assume @dependencyInfo("assumptionType:Explicit, label:L4")(n > 0) + // fixed imprecision (field assign) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L6") + a.f := b.f + 2 + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L7") + a.f := n + @dependencyInfo("assertionType:Explicit, label:L8, expectedDependencies:[L4,L7]") + assert a.f >= 0 +} + + +method quantifiedPerm4(xs: Seq[Ref], ys: Seq[Ref]) + requires @dependencyInfo("assumptionType:Precondition, label:L9")(|xs| > 5) + requires @dependencyInfo("assumptionType:Precondition, label:L10")(|ys| > 3) +{ + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L11")(forall x: Ref :: x in xs ==> acc(x.f) && x.f > 0) + + // fixed imprecision (heap summary) + inhale @dependencyInfo("assumptionType:Explicit, label:L13")(forall y: Ref :: y in ys ==> acc(y.f)) + + @dependencyInfo("assertionType:Explicit, label:L14, expectedDependencies:[L11]") + assert xs[0].f > 0 +} + +// issue #02 - imprecision due to field assign with qp +// issue: access to a quantified field depended on all writes to any location covered by this qp resource +// expl: when exhaling permission to a single location of a qp resource (e.g. for field assign), +// permissions for other locations are inhaled. The inhale of full permission after the field assign +// depends on this inhale. +// solution: +// - inhale of remaining permissions / full permission after assignments are labelled internal +// - exhale is implicit +// - lhs and rhs are split such that transitive edges only go from lhs to rhs +method quantifiedPerm5(xs: Seq[Ref], ys: Seq[Ref]) + requires @dependencyInfo("assumptionType:Precondition, label:L15")(|xs| > 5) + requires @dependencyInfo("assumptionType:Precondition, label:L16")(|ys| > 3) +{ + inhale @dependencyInfo("assumptionType:Explicit, label:L17")(forall x: Ref :: x in xs ==> acc(x.f)) + inhale @dependencyInfo("assumptionType:Explicit, label:L18")(forall y: Ref :: y in ys ==> acc(y.f)) + + // fixed imprecision (field assign with qp) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L20") + xs[0].f := ys[0].f + 2 + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L21") + xs[0].f := 2 +} + +// issue #03 +// Imprecision due to state consolidation triggered by assertion +// state consolidation infers pair-wise non-aliasing of all 3 resources in one assumption. +// This assumption depends on all preconditions. +// The assertion depends on this assumption. +// solution: wrap with analysis labels (see viper.silicon.resources.NonQuantifiedPropertyInterpreter.buildForEach) +// and disable transitive edges (see viper.silicon.rules.DefaultStateConsolidator.consolidate) +method noAlias(a: Ref, b: Ref, c: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L22")(acc(a.f)) + requires @dependencyInfo("assumptionType:Precondition, label:L23")(acc(b.f, 1/2)) + requires @dependencyInfo("assumptionType:Precondition, label:L24")(acc(c.f, 1/2)) // fixed imprecision (state consolidation)) +{ + @dependencyInfo("assertionType:Explicit, label:L25, expectedDependencies:[]") + assert a != b +} + +method fieldAccessPrecision(){ + var x: Ref + var y: Ref + + inhale @dependencyInfo("assumptionType:Explicit, label:L26")(acc(x.f)) + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L27")(x.f > 0) + + inhale @dependencyInfo("assumptionType:Explicit, label:L28")(acc(y.f)) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L29") + y.f := y.f + 1 + + @dependencyInfo("assertionType:Explicit, label:L30, expectedDependencies:[L26,L27]") + assert x.f >= 0 +} + +method fieldAccessPrecision2(){ + var x: Ref + var y: Ref + + inhale @dependencyInfo("assumptionType:Explicit, label:L31")(acc(x.f)) + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L32")(x.f > 0) + + inhale @dependencyInfo("assumptionType:Explicit, label:L33")(acc(y.f)) + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L34") + x.f := y.f + 1 + + @dependencyInfo("assertionType:Explicit, label:L35, expectedDependencies:[L31]") + exhale acc(x.f) +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/missingAnnotations/imprecision.vpr b/src/test/resources/dependencyAnalysisTests/missingAnnotations/imprecision.vpr new file mode 100644 index 000000000..8ba3b92cd --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/missingAnnotations/imprecision.vpr @@ -0,0 +1,175 @@ +field f: Int +field g: Int + +// When executing the branch, the 1st inhale is reported as a dependency, the 2nd inhale is ignored. +// If the branch is not executed, the 2nd inhale is reported as a dependency +// In reality, the 2nd inhale is all we need for the assertion. +method nonUniqueUnsatCore(x: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L3")(x != null ==> acc(x.f)) +{ + var a: Int + if(@dependencyInfo("assumptionType:PathCondition, label:L4")(x == null)){ // imprecise + inhale @dependencyInfo("assumptionType:Explicit, label:L6")(a >= 0) + } + + inhale @dependencyInfo("assumptionType:Explicit, label:L7")(a >= 0) + + @dependencyInfo("assertionType:Explicit, label:L8, expectedDependencies:[L7]") + assert a >= 0 +} + +method unprovableInfeasibility(){ + var a: Int + + if(@dependencyInfo("assumptionType:PathCondition, label:L9")(exists x: Int, y: Int, z: Int :: x > 0 && y > 0 && z > 0 && x*x*x + y*y*y == z*z*z)) { + // complicated condition that is equivalent to false, but can’t be proven by solver + // effectively unreachable code + assume @dependencyInfo("assumptionType:Explicit, label:L10")(a == 0) + } else { + assume @dependencyInfo("assumptionType:Explicit, label:L11")(a == 0) + } + + @dependencyInfo("assertionType:Explicit, label:L12, expectedDependencies:[L11]") + assert a == 0 +} + +// Implication causes branching. +// If x == null is assumed, only the assignment is reported as a dependency +// If x != null, the branch is infeasible, but we executed it anyways. The implication and branch condition are reported as dependencies. +method infeasible2(x: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L13")(x != null ==> acc(x.f)) // unexpected dependency +{ + if(@dependencyInfo("assumptionType:PathCondition, label:L14")(x == null)){ // unexpected dependency + var a: Int + + @dependencyInfo("assumptionType:SourceCode, label:L15") + a := 0 + + @dependencyInfo("assertionType:Explicit, label:L16, expectedDependencies:[L15]") + assert a >= 0 + } +} + +method exhaleImprecision(){ + var x: Ref + + inhale @dependencyInfo("assumptionType:Explicit, label:L17")(acc(x.f)) + + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L18")(x.f > 0) + + @dependencyInfo("assumptionType:SourceCode, assertionType:SourceCode, label:L19") + x.f := x.f + 1 + + // unexpected dependency + @dependencyInfo("assertionType:Explicit, label:L21") + exhale acc(x.f, 1/2) + + @dependencyInfo("assertionType:Explicit, label:L22, expectedDependencies:[L17,L18,L19]") + assert x.f > 1 +} + +// this is precise as opposed to later examples with wildcards +method quantifiedPerm2(xs: Seq[Ref], ys: Seq[Ref]) + requires @dependencyInfo("assumptionType:Precondition, label:L23")(|xs| > 5) + requires @dependencyInfo("assumptionType:Precondition, label:L24")(|ys| > 3) +{ + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L25")(forall x: Ref :: x in xs ==> (acc(x.f, wildcard) && x.f > 0)) + // this is precise (as opposed to quantifiedPerm3 + inhale @dependencyInfo("assumptionType:Explicit, label:L27")(forall y: Ref :: y in ys ==> acc(y.f, wildcard)) + + @dependencyInfo("assertionType:Explicit, label:L28, expectedDependencies:[L23, L25]") + assert xs[0].f > 0 +} + +// issue #01 - imprecision due to wildcards +// when accessing a qp resource, we check Z < (x in xs? k1:Z) + (y in ys? k2:Z) the unsat core contains dependencies to all qp perm amounts (e.g. wildcard) +method quantifiedPerm3(xs: Seq[Ref], ys: Seq[Ref]) + requires @dependencyInfo("assumptionType:Precondition, label:L29")(|xs| > 5) + requires @dependencyInfo("assumptionType:Precondition, label:L30")(|ys| > 3) +{ + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L31")(forall x: Ref :: x in xs ==> (acc(x.f, wildcard) && x.f > 0)) + // unexpected dependency (wildcards) + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L33")(forall y: Ref :: y in ys ==> (acc(y.f, wildcard) && y.f > 0)) + + @dependencyInfo("assertionType:Explicit, label:L34, expectedDependencies:[L31]") + assert xs[0].f > 0 +} + +// issue #01 - imprecision due to perm amounts +// when accessing a qp resource, we check Z < (x in xs? k1:Z) + (y in ys? k2:Z) the unsat core contains dependencies to all qp perm amounts +method quantifiedPerm4(xs: Seq[Ref], ys: Seq[Ref]) + requires @dependencyInfo("assumptionType:Precondition, label:L35")(|xs| > 5) + requires @dependencyInfo("assumptionType:Precondition, label:L36")(|ys| > 3) +{ + var p1: Perm + assume @dependencyInfo("assumptionType:Explicit, label:L37")(none < p1) + assume @dependencyInfo("assumptionType:Explicit, label:L37b")(p1 < 1/2) + + var p2: Perm + assume @dependencyInfo("assumptionType:Explicit, label:L38")(none < p2) // unexpected dependency + assume @dependencyInfo("assumptionType:Explicit, label:L38b")(p2 < 1/2) + + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L39")(forall x: Ref :: x in xs ==> (acc(x.f, p1) && x.f > 0)) + + // unexpected dependency + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L41")(forall y: Ref :: y in ys ==> (acc(y.f, p2) && y.f > 0)) + + @dependencyInfo("assertionType:Explicit, label:L42, expectedDependencies:[L35,L37,L39]") + assert xs[0].f > 0 +} + +// havocs all resources of the given fields conditionally on whether it can prove +// non-aliasing or not (e.g. x.f --> x == y? new snap : old snap) +method quasihavoc1(x: Ref, y: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L43")((acc(x.f))) + requires @dependencyInfo("assumptionType:Precondition, label:L44")((x.f == 3)) + requires @dependencyInfo("assumptionType:Precondition, label:L45")((x != y)) +{ + // unexpected dependency + quasihavoc y.f // does nothing. we have no permission + @dependencyInfo("assertionType:Explicit, label:L48, expectedDependencies:[]") + assert x.f == 3 +} + + +method inhaleImprecision(){ + var x: Ref + + inhale @dependencyInfo("assumptionType:Explicit, label:L49")(acc(x.f, 1/2)) + inhale @dependencyInfo("assumptionType:Explicit, assertionType:Implicit, label:L50")(x.f > 0) + + inhale @dependencyInfo("assumptionType:Explicit, label:L51")(acc(x.f, 1/2)) + + @dependencyInfo("assertionType:Explicit, label:L52, expectedDependencies:[L49,L50]") + assert x.f > 0 +} + +method packageExhale2(x: Ref) + requires @dependencyInfo("assumptionType:Precondition, label:L53")(acc(x.f)) + { + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L54") + package acc(x.f, 1/2) --* acc(x.f) + + @dependencyInfo("assumptionType:Rewrite, assertionType:Rewrite, label:L55") + apply acc(x.f, 1/2) --* acc(x.f) + + @dependencyInfo("assertionType:Explicit, label:L56, expectedDependencies:[]") + exhale acc(x.f) +} + +method quantifiedFieldAssign(){ + var a: Int + var idx: Int + var xs: Seq[Ref] + + inhale @dependencyInfo("assumptionType:Explicit, label:L57")(|xs| > 2 ) // imprecise + @dependencyInfo("assumptionType:SourceCode, label:L58") + idx := 0 // imprecise + inhale @dependencyInfo("assumptionType:Explicit, label:L59")(forall xi: Ref :: xi in xs ==> acc(xi.f)) + @dependencyInfo("assumptionType:SourceCode, label:L60") + xs[idx].f := a // internal dependency + + @dependencyInfo("assertionType:Explicit, label:L61, expectedDependencies:[L57,L59]") + exhale acc(xs[0].f) +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/arrays.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/arrays.vpr new file mode 100644 index 000000000..e0fc9353c --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/arrays.vpr @@ -0,0 +1,124 @@ +/* Finding the maximum in an array by elimination. + * Problem 1 from http://foveoos2011.cost-ic0701.org/verification-competition + */ + +define access(a) forall j: Int :: 0 <= j && j < len(a) ==> acc(loc(a, j).val) +define untouched(a) forall j: Int :: 0 <= j && j < len(a) ==> loc(a, j).val == old(loc(a, j).val) +define is_max(i, a, u) forall j: Int :: 0 <= j && j < u ==> loc(a, j).val <= loc(a, i).val + +method max(a: IArray) returns (x: Int) + requires access(a) + ensures access(a) + ensures untouched(a) + ensures len(a) == 0 ? x == -1 : (0 <= x && x < len(a)) + ensures is_max(x, a, len(a)) +{ + if (len(a) == 0) { + x := -1 + } else { + var y: Int + x := 0; + y := len(a) - 1; + + while (x != y) + invariant access(a) + invariant untouched(a) + invariant 0 <= x + invariant x <= y + invariant y < len(a) + invariant (forall i: Int :: + ((0 <= i && i < x) || (y < i && i < len(a))) + ==> loc(a, i).val < loc(a, x).val) + || (forall i: Int :: + ((0 <= i && i < x) || (y < i && i < len(a))) + ==> loc(a, i).val <= loc(a, y).val) + { + if (loc(a, x).val <= loc(a, y).val) { + x := x + 1 + } else { + y := y - 1 + } + } + } +} + +method client() { + var a: IArray + inhale len(a) == 3 + inhale access(a) + inhale forall i: Int :: 0 <= i && i < len(a) ==> loc(a, i).val == i + + var x: Int + x := max(a) + + assert loc(a, 0).val <= x + + @trigger() + assert x == loc(a, len(a) - 1).val + /* Necessary to prove the final assertion (due to triggering) */ + + assert x == 2 + + assert loc(a, 1).val < x +} + +/* Task A */ + +// used to write matching trigger terms below +function offset(k:Int, x:Int, y:Int) : Int { + y + k - x +} + +// longest common prefix +method lcp(a: IArray, x: Int, y: Int) returns (n: Int) + requires access(a) + requires 0 <= x + requires 0 <= y + requires x < len(a) + requires y < len(a) + ensures access(a) + ensures 0 <= n + ensures x + n <= len(a) + ensures y + n <= len(a) + ensures forall k: Int :: {loc(a, offset(k,x,y))} // instantiation trigger term + x <= k && k < x + n ==> loc(a, k).val == loc(a, offset(k,x,y)).val + /* The following postcondition is logically equivalent to the previous one, and a bit + * easier to understand. However, it can currently not be used because it contains + * no possible triggers (due to the arithmetic operations inside the loc-expressions). + */ + // ensures forall k: Int :: 0 <= k && k < n ==> loc(a, x + k).val == loc(a, y + k).val + ensures x + n < len(a) && y + n < len(a) ==> loc(a, x + n).val != loc(a, y + n).val +{ + n := 0 + while (x + n < len(a) && y + n < len(a) && loc(a, x + n).val == loc(a, y + n).val) + invariant n >= 0 + invariant access(a) + invariant x + n <= len(a) + invariant y + n <= len(a) + invariant forall k: Int :: {loc(a, offset(k,x,y))} // instantiation trigger term + x <= k && k < x + n ==> loc(a, k).val == loc(a, offset(k,x,y)).val + { + n := n + 1 + } +} + + +/* Encoding of arrays */ + +field val: Int + +domain IArray { + function loc(a: IArray, i: Int): Ref + function len(a: IArray): Int + function first(r: Ref): IArray + function second(r: Ref): Int + + axiom all_diff { + forall a: IArray, i: Int :: {loc(a, i)} + first(loc(a, i)) == a && second(loc(a, i)) == i + } + + axiom len_nonneg { + forall a: IArray :: len(a) >= 0 + } +} diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/binarySearchSeq.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/binarySearchSeq.vpr new file mode 100644 index 000000000..5b67833da --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/binarySearchSeq.vpr @@ -0,0 +1,33 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ + + +// taken from silver test suite +method binary_search(array: Seq[Int], key: Int) returns (index: Int) + requires forall i: Int, j: Int :: 0 <= i && j < |array| && i < j ==> array[i] <= array[j] + ensures -1 <= index && index < |array| + ensures 0 <= index ==> array[index] == key + ensures -1 == index ==> (forall i: Int :: 0 <= i && i < |array| ==> array[i] != key) +{ + var low: Int := 0 + var high: Int := |array| + var mid : Int + + index := -1 + while (low < high) + invariant 0 <= low && low <= high && high <= |array| + invariant forall i: Int :: (0 <= i && i < |array| && !(low <= i && i < high)) ==> array[i] != key + { + mid := (low + high) \ 2; + if (array[mid] < key) { + low := mid + 1; + } else { + if (key < array[mid]) { + high := mid; + } else { + index := mid + } + } + } + index := -1 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/gaussian.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/gaussian.vpr new file mode 100644 index 000000000..177a9e277 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/gaussian.vpr @@ -0,0 +1,81 @@ +field f: Int + +method gaussianSimple(n: Int) returns (res: Int) + requires 0 <= n + requires @irrelevant("Precondition")(n <= 5) +{ + res := 0 + var i: Int := 0 + while(@dependency("PathCondition")(i <= n)) + invariant @dependency("LoopInvariant")(i <= (n + 1)) + invariant @irrelevant("LoopInvariant")(i <= 6) + invariant @dependency("LoopInvariant")(res == (i - 1) * i / 2) + { + @dependency("SourceCode") + res := res + i + @dependency("SourceCode") + i := i + 1 + } + + @testAssertion("Explicit") + assert res == n * (n + 1) / 2 +} + +method gaussianPerm(a: Ref, p: Perm) returns (res: Int) + requires @dependency("Precondition")(none < p) && p < write + requires @dependency("Precondition")(acc(a.f, p)) + requires 0 <= a.f + requires a.f <= 5 + ensures acc(a.f, p) +{ + res := 0 + var i: Int := 0 + while(@dependency("PathCondition")(i <= a.f)) + invariant @dependency("LoopInvariant")(acc(a.f, p)) + invariant 0 <= a.f && a.f <= 5 + invariant @dependency("LoopInvariant")(i <= (a.f + 1)) + invariant i <= 6 + invariant @dependency("LoopInvariant")(res == (i - 1) * i / 2) + { + @dependency("SourceCode") + res := res + i + @dependency("SourceCode") + i := i + 1 + } + + @testAssertion("Explicit") + assert res == a.f * (a.f + 1) / 2 +} + +predicate gaussianEq(res: Int, n: Int){ + res == (n - 1) * n / 2 && n >= 0 +} + +method gaussianPred(n: Int) returns (res: Int) + requires 0 <= n + requires n <= 5 +{ + res := 0 + var i: Int + @dependency("SourceCode") + i := 0 + @dependency("Rewrite") + fold gaussianEq(res, i) + while(i <= n) + invariant i <= (n + 1) + invariant i <= 6 + invariant @dependency("LoopInvariant")(gaussianEq(res, i)) + { + @dependency("Rewrite") + unfold gaussianEq(res, i) + @dependency("SourceCode") + res := res + i + @dependency("SourceCode") + i := i + 1 + @dependency("Rewrite") + fold gaussianEq(res, i) + } + assert i == n+1 + @testAssertion("Explicit") + assert gaussianEq(res, n+1) +} diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/graph-copy.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/graph-copy.vpr new file mode 100644 index 000000000..33524ddba --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/graph-copy.vpr @@ -0,0 +1,175 @@ +field val: Int +field edges: IEdges + +// Total function that returns null for those elements that are not present. +domain IEdges { + function edge_lookup(e: IEdges, i: Int): Ref + function has_edge(e: IEdges, i: Int): Bool + function insert_edge(e: IEdges, i: Int, node: Ref): IEdges + function edges_domain(e: IEdges): Set[Int] + function empty_edges(): IEdges + + // INSERTION + + axiom inserted_edge_present { + forall e: IEdges, i: Int, node: Ref :: edge_lookup(insert_edge(e, i, node), i) == node + } + + axiom other_edges_preserved_after_insertion { + forall e: IEdges, i: Int, node: Ref, j: Int :: i != j ==> edge_lookup(e, j) == edge_lookup(insert_edge(e, i, node), j) + } + + axiom inserted_edge_defined { + forall e: IEdges, i: Int, node: Ref :: has_edge(insert_edge(e, i, node), i) + } + + // HAS EDGE + + axiom has_edge_complete { + forall e: IEdges, i: Int :: has_edge(e, i) <==> edge_lookup(e, i) != null + } + + // DOMAIN + + axiom edges_domain_defined { + forall e: IEdges, i: Int :: i in edges_domain(e) <==> has_edge(e, i) + } + + // EMPTY MAP + + axiom empty_edges_has_no_nodes { + forall i: Int :: !(has_edge(empty_edges(), i)) + } +} + +domain INodeMap { + function lookup(node_map: INodeMap, node: Ref): Ref + function has_node(node_map: INodeMap, node: Ref): Bool + function insert(node_map: INodeMap, key_node: Ref, val_node: Ref): INodeMap + function map_domain(node_map: INodeMap): Seq[Ref] + function empty_map(): INodeMap + + // INSERTION + + axiom inserted_node_present { + forall node_map: INodeMap, key_node: Ref, val_node: Ref :: lookup(insert(node_map, key_node, val_node), key_node) == val_node + } + + axiom other_nodes_preserved_after_insertion { + forall node_map: INodeMap, key_node: Ref, val_node: Ref, node: Ref :: node != key_node ==> lookup(node_map, node) == lookup(insert(node_map, key_node, val_node), node) + } + + axiom inserted_node_defined { + forall node_map: INodeMap, key_node: Ref, val_node: Ref :: has_node(insert(node_map, key_node, val_node), key_node) + } + + // HAS NODE + + axiom has_node_complete { + forall node_map: INodeMap, node: Ref :: has_node(node_map, node) <==> lookup(node_map, node) != null + } + + // DOMAIN + + axiom domain_is_defined { + forall node_map: INodeMap, key: Ref:: key in map_domain(node_map) <==> has_node(node_map, key) + } + + // EMPTY MAP + + axiom empty_map_has_no_nodes { + forall node: Ref :: !(has_node(empty_map(), node)) + } +} + +method graph_copy_rec(this: Ref, node_map: INodeMap, setOfRef: Set[Ref], node_map_image: Set[Ref], rd: Perm) + returns (nodeCopy: Ref, res_node_map: INodeMap, res_copy_nodes: Set[Ref]) + + requires none < rd + requires this != null + // Precondition about setOfRef + requires this in setOfRef + requires |setOfRef intersection node_map_image| == 0 + requires forall x: Ref :: x in setOfRef ==> acc(x.val, rd) + requires forall x: Ref :: x in setOfRef ==> acc(x.edges, rd) + requires forall x: Ref, i: Int :: x in setOfRef && i in edges_domain(x.edges) ==> edge_lookup(x.edges, i) in setOfRef + // Precondition about node_map_image + requires forall x: Ref :: x in map_domain(node_map) ==> lookup(node_map, x) in node_map_image + requires forall x: Ref :: x in node_map_image ==> acc(x.val) + requires forall x: Ref :: x in node_map_image ==> acc(x.edges) + + ensures nodeCopy != null && nodeCopy in res_copy_nodes + ensures |setOfRef intersection res_copy_nodes| == 0 + ensures forall x: Ref :: x in setOfRef ==> acc(x.val, rd) + ensures forall x: Ref :: x in setOfRef ==> x.val == old(x.val) + ensures forall x: Ref :: x in setOfRef ==> acc(x.edges, rd) + ensures forall x: Ref :: x in setOfRef ==> x.edges == old(x.edges) + ensures forall x: Ref, i: Int :: x in setOfRef && i in edges_domain(x.edges) ==> edge_lookup(x.edges, i) in setOfRef + ensures res_copy_nodes == res_copy_nodes union old(node_map_image) + ensures forall x: Ref :: x in map_domain(res_node_map) ==> lookup(res_node_map,x) in res_copy_nodes + ensures forall x: Ref :: x in res_copy_nodes ==> acc(x.val) + ensures forall x: Ref :: x in res_copy_nodes ==> acc(x.edges) +{ + var i: Int // an edge index + var S: Set[Int] + + if (has_node(node_map, this)) { + nodeCopy := lookup(node_map, this) + res_node_map := node_map + res_copy_nodes := node_map_image + } else { + nodeCopy := new(val, edges) + nodeCopy.val := this.val + + res_node_map := insert(node_map, this, nodeCopy) + + res_copy_nodes := node_map_image union Set(nodeCopy) + + /* The next assert is needed in order to prove that the loop invariant + * assert |setOfRef intersection res_copy_nodes| == 0 + * holds before the loop. + * + * See https://bitbucket.org/viperproject/silver/issues/111 + */ + @trigger() + assert (setOfRef intersection node_map_image) union (setOfRef intersection Set(nodeCopy)) + == setOfRef intersection res_copy_nodes + + S := edges_domain(this.edges) + + while (|S| > 0) + invariant nodeCopy in res_copy_nodes + invariant this in setOfRef + invariant forall x: Ref :: x in setOfRef ==> acc(x.val, rd) + invariant forall x: Ref :: x in setOfRef ==> x.val == old(x.val) + invariant forall x: Ref :: x in setOfRef ==> acc(x.edges, rd) + invariant forall x: Ref :: x in setOfRef ==> x.edges == old(x.edges) + invariant forall j: Int :: j in S ==> edge_lookup(this.edges, j) in setOfRef + invariant forall r: Ref, j: Int :: r in setOfRef && j in edges_domain(r.edges) ==> edge_lookup(r.edges, j) in setOfRef + invariant node_map_image subset res_copy_nodes + invariant |setOfRef intersection res_copy_nodes| == 0 + invariant forall r: Ref :: r in map_domain(res_node_map) ==> lookup(res_node_map,r) in res_copy_nodes + invariant forall r: Ref :: r in res_copy_nodes ==> acc(r.val) + invariant forall r: Ref :: r in res_copy_nodes ==> acc(r.edges) + { + S, i := pop(S) + + var newNode: Ref + var newResultMap: INodeMap + var nodeForId: Ref + + nodeForId := edge_lookup(this.edges, i) + + newNode, res_node_map, res_copy_nodes := graph_copy_rec(nodeForId, res_node_map, setOfRef, res_copy_nodes, rd/2) + + nodeCopy.edges := insert_edge(nodeCopy.edges, i, newNode) + } + } +} + +/* Non-deterministically remove an int from s1, yielding an updated s2 and the removed int i */ +method pop(s1: Set[Int]) returns (s2: Set[Int], i: Int) + requires 0 < |s1| + ensures i in s1 + ensures s2 == s1 setminus Set(i) +{ assume false /* Effectively an unimplemented helper method */ } diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/iterativeTreeDelete.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/iterativeTreeDelete.vpr new file mode 100644 index 000000000..018bd298a --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/iterativeTreeDelete.vpr @@ -0,0 +1,95 @@ + + +/* This example shows how magic wands can be used to specify the + * imperative version of challenge 3 from the VerifyThis@FM2012 + * verification competition. Method tree_delete_min below is an + * iterative implementation of the removal of the minimal element + * in a binary search tree. + * + * The example contains two assertions (marked with "TODO") that + * help overcoming an incompleteness with respect to sequences. + * + * At present, this example uses syntax which is only supported + * by the default Viper verifier (Silicon). + */ + +field v: Int +field l: Ref +field r: Ref + +predicate Tree(x: Ref) { + x == null + ? true + : acc(x.v) + && acc(x.l) && acc(Tree(x.l)) + && acc(x.r) && acc(Tree(x.r)) +} + +function val(x: Ref): Int + requires x != null && acc(Tree(x)) +{ unfolding acc(Tree(x)) in x.v } + +function vals(x: Ref): Seq[Int] + requires acc(Tree(x)) +{ x == null ? Seq[Int]() : unfolding acc(Tree(x)) in vals(x.l) ++ Seq(x.v) ++ vals(x.r) } + +/* Deletes the minimal element of a binary tree, assuming that the + * tree is a binary search tree (which, for simplicity, is not made + * explicit in the definition of predicate Tree). + */ +method tree_delete_min(x: Ref) returns (z: Ref) + requires x != null && acc(Tree(x)) + ensures acc(Tree(z)) /* POST1 */ + ensures vals(z) == old(vals(x))[1..] /* POST2 */ +{ + var p: Ref := x + var plvs: Seq[Int] + + define A(p, plvs) acc(p.l) && acc(Tree(p.l)) && vals(p.l) == plvs[1..] + define B acc(Tree(x)) && vals(x) == old(vals(x))[1..] + + unfold acc(Tree(p)) + plvs := vals(p.l) + + if (p.l == null) { + z := p.r + + assert vals(x.l) == Seq[Int]() /* TODO: Required by Silicon for POST2 */ + } else { + package A(p, plvs) --* B { + fold acc(Tree(p)) + } + + + while (unfolding acc(Tree(p.l)) in p.l.l != null) + invariant p != null && acc(p.l) + invariant acc(Tree(p.l)) + invariant p.l != null + invariant plvs == vals(p.l) + invariant A(p, plvs) --* B + { + var prevP: Ref := p + var prevPlvs: Seq[Int] := plvs + + unfold acc(Tree(p.l)) + p := p.l + plvs := vals(p.l) + + package A(p, plvs) --* B { + fold Tree(p) + apply A(prevP, prevPlvs) --* B + } + + } + + unfold acc(Tree(p.l)) + @trigger() + assert vals(p.l.l) == Seq[Int]() /* TODO: Required by Silicon for POST2 */ + + p.l := p.l.r + + apply A(p, plvs) --* B + + z := x + } +} diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/listAppend.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/listAppend.vpr new file mode 100644 index 000000000..039a9d5e4 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/listAppend.vpr @@ -0,0 +1,110 @@ +field elem: Int +field next: Ref + +predicate list(this: Ref) { + acc(this.elem) && acc(this.next) && + (this.next != null ==> list(this.next)) +} + +function listLength(l:Ref) : Int + requires list(l) + ensures result > 0 +{ + unfolding list(l) in l.next == null ? 1 : 1 + listLength(l.next) +} + +method appendList1(this: Ref, e: Int) + requires @irrelevant("Precondition")(list(this)) + requires @irrelevant("Precondition")(0 <= e && e < 100) + ensures list(this) +{ + @irrelevant("Rewrite") + unfold list(this) + @irrelevant("Explicit") + assume 0 <= this.elem && this.elem < 100 + + if (@irrelevant("PathCondition")(this.next == null)) { + var n: Ref + + @dependency("SourceCode") + n := new(elem, next) + @irrelevant("SourceCode") + n.elem := e + @dependency("SourceCode") + n.next := null + @irrelevant("SourceCode") + this.next := n + @testAssertion("Implicit") + fold list(n) + } else { + var l_next: Ref := this.next + appendList1(l_next, e) + } + + fold list(this) +} + +method appendList2(this: Ref, e: Int) + requires @dependency("Precondition")(list(this)) + requires @dependency("Precondition")(0 <= e && e < 100) + ensures list(this) +{ + @dependency("Rewrite") + unfold list(this) + @irrelevant("Explicit") + assume 0 <= this.elem && this.elem < 100 + + if (@dependency("PathCondition")(this.next == null)) { + var n: Ref + + @irrelevant("SourceCode") + n := new(elem, next) + @irrelevant("SourceCode") + n.elem := e + @irrelevant("SourceCode") + n.next := null + @irrelevant("SourceCode") + this.next := n + @irrelevant("Rewrite") + fold list(n) + } else { + var l_next: Ref := this.next + @testAssertion("Implicit") + appendList2(l_next, e) + } + + fold list(this) +} + +method appendListFull(this: Ref, e: Int) + requires @dependency("Precondition")(list(this)) + requires @dependency("Precondition")(0 <= e && e < 100) + ensures list(this) +{ + @dependency("Rewrite") + unfold list(this) + @irrelevant("Explicit") + assume 0 <= this.elem && this.elem < 100 + + if (@dependency("PathCondition")(this.next == null)) { + var n: Ref + + @dependency("SourceCode") + n := new(elem, next) + @irrelevant("SourceCode") + n.elem := e + @dependency("SourceCode") + n.next := null + @dependency("SourceCode") + this.next := n + @dependency("Rewrite") + fold list(n) + } else { + var l_next: Ref := this.next + @dependency("MethodCall") + appendListFull(l_next, e) + } + + @testAssertion("Implicit") + fold list(this) +} diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/listAppend_wands.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/listAppend_wands.vpr new file mode 100644 index 000000000..b0162963f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/listAppend_wands.vpr @@ -0,0 +1,65 @@ +field next : Ref +field val : Int + +predicate list(start : Ref) +{ + acc(start.val) && acc(start.next) && + (start.next != null ==> list(start.next)) +} + +function elems(start: Ref) : Seq[Int] + requires list(start) +{ + unfolding list(start) in ( + (start.next == null ? Seq(start.val) : + Seq(start.val) ++ elems(start.next) )) +} + +method appendit_wand(l1 : Ref, l2: Ref) + requires list(l1) + requires list(l2) + requires l2 != null + ensures list(l1) + ensures elems(l1) == old(elems(l1) ++ elems(l2)) + { + unfold list(l1) + if(l1.next == null) { + l1.next := l2 + fold list(l1) + } else { + var tmp : Ref + var index : Int + tmp := l1.next + index := 1 + + package list(tmp) --* list(l1) && elems(l1) == old(elems(l1)[..index]) ++ old[lhs](elems(tmp)) + { + fold list(l1) + } + + while(unfolding list(tmp) in tmp.next != null) + invariant index >= 0 + invariant list(tmp) + invariant elems(tmp) == old(elems(l1))[index..] + invariant list(tmp) --* list(l1) && elems(l1) == old(elems(l1)[..index]) ++ old[lhs](elems(tmp)) + { + var prev : Ref + var old_index: Int + unfold list(tmp) + prev := tmp + old_index := index + tmp := tmp.next + index := index + 1 + + package list(tmp) --* list(l1) && elems(l1) == old(elems(l1)[..index]) ++ old[lhs](elems(tmp)) + { + fold list(prev) + apply list(prev) --* list(l1) && elems(l1) == old(elems(l1)[..old_index]) ++ old[lhs](elems(prev)) + } + } + unfold list(tmp) + tmp.next := l2 + fold list(tmp) + apply list(tmp) --* list(l1) && elems(l1) == old(elems(l1)[..index]) ++ old[lhs](elems(tmp)) + } + } \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/seqClient.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/seqClient.vpr new file mode 100644 index 000000000..3ed3062fa --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/seqClient.vpr @@ -0,0 +1,24 @@ +import "seqMerge.vpr" +import "binarySearchSeq.vpr" + +method client() +{ + var idx: Int + + var as: Seq[Int] := Seq(4, 7, 9, 11, 14) + var bs: Seq[Int] := Seq(1, 3, 7, 10, 17) + + // idx := binary_search(as, 7) + // assert idx == 1 + + var cs: Seq[Int] + cs := seq_merge(as, bs) + + assert 10 in cs + assert 1 in cs + + assert |cs| == 11 + // idx := binary_search(cs, 1) + // assert idx == 1 + +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/real-world-examples/seqMerge.vpr b/src/test/resources/dependencyAnalysisTests/real-world-examples/seqMerge.vpr new file mode 100644 index 000000000..085d6b204 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/real-world-examples/seqMerge.vpr @@ -0,0 +1,53 @@ + +method seq_merge(as: Seq[Int], bs: Seq[Int]) returns (cs: Seq[Int]) + requires |as| >= 0 && |bs| >= 0 + requires forall i: Int :: {as[i]} 0 <= i && i <|as| ==> 0 <= as[i] + requires forall i: Int :: {as[i]} 0 < i && i <|as| ==> as[i-1] <= as[i] + requires forall j: Int :: {bs[j]} 0 <= j && j <|bs| ==> 0 <= bs[j] + requires forall j: Int :: {bs[j]} 0 < j && j <|bs| ==> bs[j-1] <= bs[j] + + ensures |cs| == |as| + |bs| + 1 + ensures forall k: Int :: {cs[k]} 0 < k && k < |cs| ==> 0 <= cs[k] + ensures forall k: Int :: {cs[k]} 0 < k && k < |cs| ==> cs[k-1] <= cs[k] + ensures forall i: Int :: {as[i]} 0 <= i && i < |as| ==> as[i] in cs + ensures forall j: Int :: {bs[j]} 0 <= j && j < |bs| ==> bs[j] in cs +{ + var i: Int := 0 + var j: Int := 0 + var k: Int := 1 + + inhale |cs| == |as| + |bs| + 1 + cs := cs[0 := 0] // index 0 is a dummy value + + while(k < |cs|) + invariant 0 <= i && i <= |as| + invariant 0 <= j && j <= |bs| + invariant 0 <= k && k <= |cs| + invariant |cs| == |as| + |bs| + 1 + invariant i + j + 1 == k + invariant forall k0: Int :: {cs[k0]} 0 <= k0 && k0 < k ==> 0 <= cs[k0] + invariant forall k0: Int :: {cs[k0]} 0 <= k0 && k0 < k && i < |as| ==> cs[k0] <= as[i] + invariant forall k0: Int :: {cs[k0]} 0 <= k0 && k0 < k && j < |bs| ==> cs[k0] <= bs[j] + invariant forall k0: Int :: {cs[k0]} 0 < k0 && k0 < k ==> cs[k0-1] <= cs[k0] + invariant forall i0: Int :: {as[i0]} 0 <= i0 && i0 < i ==> as[i0] in cs[..k] + invariant forall j0: Int :: {bs[j0]} 0 <= j0 && j0 < j ==> bs[j0] in cs[..k] + { + if(i < |as| && (j >= |bs| || as[i] < bs[j])){ + var tmp: Int := as[i] + cs := cs[k := tmp] + i := i + 1 + }else { + var tmp: Int := bs[j] + cs := cs[k := tmp] + j := j + 1 + } + k := k + 1 + } + + // assert i == |as| && j == |bs| && k == |cs| + // assert k == |as| + |bs| + 1 + + // assert forall i0: Int :: {as[i0]} 0 <= i0 && i0 < i ==> as[i0] in cs + // cs := cs[1..] // remove dummy value + // assert forall i0: Int :: {as[i0]} 0 <= i0 && i0 < i ==> as[i0] in cs +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/assertion-combinations.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/assertion-combinations.vpr new file mode 100644 index 000000000..19004f096 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/assertion-combinations.vpr @@ -0,0 +1,16 @@ +// Spec quality: 1 +// Proof quality: 5/7 +// Progress: 5/7 + +method client() +{ + var a: Int + var b: Int + a := 10 + assume b == 10 + + assert a > 5 && a < 15 && a != 30 && b > 7 // PQ: 1/1 1/1 1/1 0/1 + assert a > 15 || a < 15 // PQ: 1/1 + assert a > 15 || b < 15 // PQ: 0/1 + assert a == 10 || b < 15 // PQ: 1/1 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/assume-false.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/assume-false.vpr new file mode 100644 index 000000000..026bad153 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/assume-false.vpr @@ -0,0 +1,37 @@ +// Spec quality: 3/4 +// Proof quality: 155/168 +// Progress: 155/224 + +field val: Int +define access(a) + (forall j: Int :: 0 <= j && j < len(a) ==> acc(slot(a,j).val)) + +domain IArray { + function slot(a: IArray, i: Int): Ref + function len(a: IArray): Int + function first(r: Ref): IArray + function second(r: Ref): Int + + axiom all_diff { + forall a: IArray, i: Int :: { slot(a,i) } + first(slot(a,i)) == a && second(slot(a,i)) == i + } +} + + +method domain3() +{ + var a: IArray + inhale len(a) == 3 + inhale access(a) // PQ: 1/1 + + var i: Int + i := 0 + while (i < len(a)) + invariant access(a) // PQ: 5/6 + invariant 0 <= i // PQ: 1/1 + { + slot(a,i).val := -i // PQ: 6/7 + i := i + 1 + } +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch1.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch1.vpr new file mode 100644 index 000000000..dbe6322da --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch1.vpr @@ -0,0 +1,17 @@ +// Spec quality: 2/3 +// Proof quality: 2/3 +// Progress: 4/9 + +method branch1() { + var x: Int, y: Int, z: Int + + if (x > 0) { + assume y == x + 1 + } + else { + y := -x + 1 + z := x * y + } + + assert y > 0 // PQ: 2/3 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch2.vpr new file mode 100644 index 000000000..edc103f31 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch2.vpr @@ -0,0 +1,16 @@ +// Spec quality: 0 +// Proof quality: 0 +// Progress: 0 + +method branch2() { + var x: Int, y: Int + + if (x > 0) { + assume y > 0 + } + else { + assume y >= 10 + } + + assert y > 0 // PQ: 0/2 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch3.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch3.vpr new file mode 100644 index 000000000..2fd7ca58f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch3.vpr @@ -0,0 +1,16 @@ +// Spec quality: 2/3 +// Proof quality: 1 +// Progress: 2/3 + +method branch3() { + var x: Int, y: Int + + if (x > 10) { + y := x + 1 + } else { + y := 10 + assume y > 0 + } + + assert x > 10 ==> y > 0 // PQ: 2/2 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch4.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch4.vpr new file mode 100644 index 000000000..6a40e58a1 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/branch4.vpr @@ -0,0 +1,14 @@ +// Spec quality: 2/3 +// Proof quality: 1 +// Progress: 2/3 + +method branch4() { + var x: Int, y: Int + + if (x > 0) { + y := x + 1 + } else { + y := 10 - x + assert y > 0 // PQ: 2/2 + } +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions1.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions1.vpr new file mode 100644 index 000000000..6727d872c --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions1.vpr @@ -0,0 +1,19 @@ +// Spec quality: 0 +// Proof quality: 3/10 +// Progress: 0 + +function sum(x: Int, y: Int): Int + requires x >= 0 && y >= 0 + ensures result >= 0 // PQ: 3/5 +{ + x + y +} + +method client() +{ + var x: Int, y: Int + assume x > 10 + assume y > 0 + + x := sum(x, y) // PQ: 0/2 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions2.vpr new file mode 100644 index 000000000..6da44cada --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions2.vpr @@ -0,0 +1,7 @@ +// Spec quality: 1 +// Proof quality: 0 +// Progress: 0 + +function sumUnverified(x: Int, y: Int): Int + requires x >= 0 && y >= 0 + ensures result == x + y diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions3.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions3.vpr new file mode 100644 index 000000000..7d957ae0f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/functions3.vpr @@ -0,0 +1,35 @@ +// Spec quality: 1 +// Proof quality: 0.663492 +// Progress: 0.663492 + +function div(x: Int, y: Int) : Int + requires x > 0 && y > 0 + ensures result >= 0 // PQ: 5/7 +{ + x / y // PQ: 2/3 +} + + +method client1() +{ + var a: Int + assume a > 0 + var b: Int := 100 + + var res: Int + res := div(a, b) // PQ: 1/2 + + assert res == a / 100 // PQ: 4/5 +} + +method client2() +{ + var a: Int := 100 + var b: Int + assume b > 0 + + var res: Int + res := div(a, b) // PQ: 1/2 + + assert res == 100 / b // PQ: 4/5 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/implications.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/implications.vpr new file mode 100644 index 000000000..67f2c3466 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/implications.vpr @@ -0,0 +1,14 @@ +// Spec quality: 1/2 +// Proof quality: 1/2 +// Progress: 1/4 + +method client() +{ + var a: Int, b: Int, c: Int + a := 10 + assume b == 10 + c := 10 + + assert a == 0 ==> false // PQ: 1/1 + assert b == 0 ==> false // PQ: 0/1 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/inhale1.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/inhale1.vpr new file mode 100644 index 000000000..79fe6c74e --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/inhale1.vpr @@ -0,0 +1,14 @@ +// Spec quality: 1 +// Proof quality: 3/4 +// Progress: 3/4 + +field f: Int + +method exhaleInhale(a: Ref) + requires acc(a.f) +{ + exhale acc(a.f, 1/2) // PQ: 1/1 + inhale acc(a.f, 1/2) + + assert perm(a.f) == write // PQ: 1/2 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/inhale2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/inhale2.vpr new file mode 100644 index 000000000..6e6590652 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/inhale2.vpr @@ -0,0 +1,16 @@ +// Spec quality: 1 +// Proof quality: 1/6 +// Progress: 1/6 + +field f: Int + +method exhale1(){ + var x: Ref + + inhale acc(x.f) + x.f := 1 // PQ: 0/1 + + exhale acc(x.f, 1/2) // PQ: 0/1 + + assert x.f > 0 // PQ: 1/2 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/loops.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/loops.vpr new file mode 100644 index 000000000..df83cde08 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/loops.vpr @@ -0,0 +1,20 @@ +// Spec quality: 1 +// Proof quality: 7/10 +// Progress: 7/10 + +method loop1() { + var i: Int + var res: Int + res := 0 + assume i == 10 + while (i > 0) + invariant i <= 10 // PQ: 1/2 + invariant i >= 0 // PQ: 2/3 + invariant res >= 0 // PQ: 4/5 + { + res := res + i + i := i - 1 + } + + assert res >= 0 // PQ: 5/6 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-add.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-add.vpr new file mode 100644 index 000000000..1c2fa98ac --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-add.vpr @@ -0,0 +1,18 @@ +// Spec quality: 1/2 +// Proof quality: 1/2 +// Progress: 1/4 + +method add(a: Int, b: Int) returns (res: Int) + requires b != 0 + ensures res == a + b // PQ: 1/1 +{ + res := a + b +} + +method client() +{ + var a: Int, b: Int, res: Int + assume b > 0 + + res := add(a, b) // PQ: 0/1 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-add2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-add2.vpr new file mode 100644 index 000000000..84df5083f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-add2.vpr @@ -0,0 +1,7 @@ +// Spec quality: 1 +// Proof quality: 0 +// Progress: 0 + +method add(a: Int, b: Int) returns (res: Int) + requires b != 0 + ensures res == a + b \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-divide.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-divide.vpr new file mode 100644 index 000000000..89ccb876f --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/method-divide.vpr @@ -0,0 +1,19 @@ +// Spec quality: 1/3 +// Proof quality: 7/18 +// Progress: 7/54 + +method divide(a: Int, b: Int) returns (res: Int) + requires b != 0 + ensures res == a / b // PQ: 2/3 +{ + res := a / b // PQ: 1/2 +} + +method client() +{ + var a: Int, b: Int, res: Int + a := 6 // uncovered + assume b == 3 + + res := divide(a, b) // PQ: 0/1 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-branches-weaker.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-branches-weaker.vpr new file mode 100644 index 000000000..17ed36d57 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-branches-weaker.vpr @@ -0,0 +1,24 @@ +// Spec quality: 4/6 +// Proof quality: 4/5 +// Progress: 16/30 + +method branches(x: Bool, y: Bool) +{ + var a: Int + var b: Int + assume x == y + + if (x) { + a := 1 + } else { + a := -1 + } + + if (y) { + b := 1 + } else { + b := -1 + } + + assert x ==> a * b > 0 // PQ: 4/5 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-branches.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-branches.vpr new file mode 100644 index 000000000..e4779d022 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-branches.vpr @@ -0,0 +1,24 @@ +// Spec quality: 1 +// Proof quality: 6/7 +// Progress: 6/7 + +method branches(x: Bool, y: Bool) +{ + var a: Int + var b: Int + assume x == y + + if (x) { + a := 1 + } else { + a := -1 + } + + if (y) { + b := 1 + } else { + b := -1 + } + + assert a * b > 0 // PQ: 6/7 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-gaussian.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-gaussian.vpr new file mode 100644 index 000000000..ab73deb04 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-gaussian.vpr @@ -0,0 +1,21 @@ +// Spec quality: 1 +// Proof quality: 61/72 +// Progress: 61/72 + +method gaussianSum(n: Int) returns (res: Int) + requires n <= 1000 + ensures res == n * (n + 1) / 2 // PQ: 7/8 +{ + res := 0 + var i: Int := 0 + + assume i <= n + + while (i < n) + invariant i <= n // PQ: 2/3 + invariant res == i * (i + 1) / 2 // PQ: 4/4 + { + i := i + 1 + res := res + i + } +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-no-obligations.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-no-obligations.vpr new file mode 100644 index 000000000..8353faf42 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/paper-no-obligations.vpr @@ -0,0 +1,24 @@ +// Spec quality: 0 +// Proof quality: 1 +// Progress: 0 + +// Border case - there are no proof obligations + +method branches(x: Bool, y: Bool) +{ + var a: Int + var b: Int + assume x == y + + if (x) { + a := 1 + } else { + a := -1 + } + + if (y) { + b := 1 + } else { + b := -1 + } +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-quantified1.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-quantified1.vpr new file mode 100644 index 000000000..a1340e166 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-quantified1.vpr @@ -0,0 +1,14 @@ +// Spec quality: 1/2 +// Proof quality: 1/2 +// Progress: 1/4 + +field f: Int + +method quantifiedPerm() { + var xs: Seq[Ref] + xs := Seq(null, null, null, null, null) + + assume forall x: Ref :: x in xs ==> acc(x.f) // only used in line 14 + + xs[0].f := 10 // PQ: 1/2 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-quantified2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-quantified2.vpr new file mode 100644 index 000000000..e83077db2 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-quantified2.vpr @@ -0,0 +1,17 @@ +// Spec quality: 1 +// Proof quality: 3/4 +// Progress: 3/4 + +field f: Int + +method quantifiedExhaleFully() { + var r1: Ref, r2: Ref, r3: Ref + r1 := new(f) + r2 := new(f) + inhale acc(r3.f) + + var xs: Seq[Ref] + xs := Seq(r1, r2, r3) + + assert forall x: Ref :: x in xs ==> acc(x.f) // PQ: 3/4 +} diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-wildcard.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-wildcard.vpr new file mode 100644 index 000000000..ccf9b162b --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions-wildcard.vpr @@ -0,0 +1,16 @@ +// Spec quality: 0 +// Proof quality: 11/12 +// Progress: 0 + +field f: Int +field g: Int + +method setFive(x: Ref) + requires acc(x.f) + requires acc(x.g) + ensures acc(x.f, wildcard) // PQ: 1/1 + ensures x.f == 5 // PQ: 2/3 +{ + assume x.f == 5 // PQ: 1/1 + x.g := 5 // PQ: 1/1 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions1.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions1.vpr new file mode 100644 index 000000000..4bec86923 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions1.vpr @@ -0,0 +1,14 @@ +// Spec quality: 1 +// Proof quality: 1/2 +// Progress: 1/2 + +field f: Int + +method permissions(x: Ref) + requires acc(x.f, 1/2) + ensures acc(x.f, 3/4) // PQ: 1/2 +{ + inhale acc(x.f, 1/4) + + assert perm(x.f) >= 3/4 // PQ: 1/2 +} diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions2.vpr new file mode 100644 index 000000000..652405f09 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/permissions2.vpr @@ -0,0 +1,25 @@ +// Spec quality: 1 +// Proof quality: 14/45 +// Progress: 14/45 + +field f: Int + +method permissions(x: Ref) + requires acc(x.f, 1/2) + ensures acc(x.f, 3/4) // PQ: 1/3 +{ + inhale acc(x.f, 1/4) +} + +method client() +{ + var y: Ref + inhale acc(y.f, 1/2) + + inhale true + exhale true + + permissions(y) // PQ: 0/1 + + assert perm(y.f) >= 3/4 // PQ: 3/5 +} diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates1.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates1.vpr new file mode 100644 index 000000000..222252ab8 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates1.vpr @@ -0,0 +1,17 @@ +// Spec quality: 1 +// Proof quality: 2/3 +// Progress: 2/3 + +predicate greater0(n: Int){ + n > 0 +} + +method foldP(n: Int) + requires n > 10 +{ + var x: Int, y: Int + x := 1 + assume y == 1 + + fold greater0(x + y + n) // PQ: 2/3 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates2.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates2.vpr new file mode 100644 index 000000000..36ebda46c --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates2.vpr @@ -0,0 +1,20 @@ +// Spec quality: 1 +// Proof quality: 3/8 +// Progress: 3/8 + +predicate greater0(n: Int){ + n > 0 +} + +method unfoldP(n: Int) +{ + var x: Int + x := 100 + + inhale greater0(n) + unfold greater0(n) // PQ: 0/1 + + x := n + x + + assert x > 100 // PQ: 3/4 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates3.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates3.vpr new file mode 100644 index 000000000..05e17d861 --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates3.vpr @@ -0,0 +1,18 @@ +// Spec quality: 1 +// Proof quality: 7/8 +// Progress: 7/8 + +predicate greater0(n: Int){ + n > 0 +} + +method unfoldP(n: Int) + requires greater0(n) +{ + var x: Int + assume x == 1 + unfold greater0(n) // PQ: 1/1 + x := n + x + + assert x > 1 // PQ: 3/4 +} \ No newline at end of file diff --git a/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates4.vpr b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates4.vpr new file mode 100644 index 000000000..95c82ca6c --- /dev/null +++ b/src/test/resources/dependencyAnalysisTests/verificationProgressTests/predicates4.vpr @@ -0,0 +1,17 @@ +// Spec quality: 1 +// Proof quality: 29/36 +// Progress: 29/36 + +predicate greater0(n: Int){ + n > 0 +} + +method unfoldFoldP(a: Int, b: Int) // 29/36 + requires greater0(b) + ensures greater0(a + b) // PQ: 3/4 +{ + assume a >= 0 + unfold greater0(b) // PQ: 1/1 + + fold greater0(a + b) // PQ: 2/3 +} \ No newline at end of file diff --git a/src/test/scala/DependencyAnalysisTestFramework.scala b/src/test/scala/DependencyAnalysisTestFramework.scala new file mode 100644 index 000000000..09717faf8 --- /dev/null +++ b/src/test/scala/DependencyAnalysisTestFramework.scala @@ -0,0 +1,376 @@ +// 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.silicon.tests + +import viper.silicon.SiliconFrontend +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.{DependencyAnalysisProgressSupporter, DependencyAnalysisPruningSupporter, DependencyGraphInterpreter} +import viper.silver.ast.Program +import viper.silver.ast.utility.ViperStrategy +import viper.silver.verifier.VerificationResult +import viper.silver.{ast, verifier} + +import java.io.PrintWriter +import java.nio.file.{Files, Path, Paths} +import scala.annotation.unused +import scala.jdk.CollectionConverters.IterableHasAsScala + +trait DependencyAnalysisTestFramework { + val irrelevantKeyword = "irrelevant" + val dependencyKeyword = "dependency" + val testAssertionKeyword = "testAssertion" + val EXPORT_PRUNED_PROGRAMS = false + + val ignores: Seq[String] + var baseCommandLineArguments: Seq[String] = Seq("--timeout", "300" /* seconds */) + var analysisCommandLineArguments: Seq[String] = + baseCommandLineArguments ++ Seq("--proverArgs", "proof=true unsat-core=true") + + def visitFiles(dirName: String, function: (String, String) => Unit): Unit = { + val path = Paths.get(getClass.getClassLoader.getResource(dirName).toURI) + visitFiles(path, dirName, function) + } + + def visitFiles(path: Path, dirName: String, function: (String, String) => Unit): Unit = { + val directoryStream = Files.newDirectoryStream(path).asScala + val dirContent = directoryStream.toList + + for (filePath: Path <- dirContent.sorted + if Files.isReadable(filePath)) { + if (Files.isDirectory(filePath)) { + visitFiles(filePath, dirName + "/" + filePath.getFileName.toString, function) + } else { + val rawFileName = filePath.getFileName.toString + if (rawFileName.endsWith(".vpr")) { + val fileName = rawFileName.replace(".vpr", "") + if (!ignores.contains(fileName)) + function(dirName, fileName) + } + } + } + } + + var frontend: SiliconFrontend = createFrontend(analysisCommandLineArguments) + + def createFrontend(commandLineArgs: Seq[String]): SiliconFrontend = { + val reporter = DependencyAnalysisReporter() + val fe = new SiliconFrontend(reporter) + val backend = fe.createVerifier("") + backend.parseCommandLine(commandLineArgs ++ List("--ignoreFile", "dummy.sil")) + fe.init(backend) + fe.setVerifier(backend) + backend.start() + fe + } + + def resetFrontend(additionalArguments: Seq[String] = Seq.empty): Unit = { + frontend.verifier.stop() + frontend = createFrontend(analysisCommandLineArguments ++ additionalArguments) + } + + var baselineFrontend: SiliconFrontend = createFrontend(baseCommandLineArguments) + + def resetBaselineFrontend(): Unit = { + baselineFrontend.verifier.stop() + baselineFrontend = createFrontend(baseCommandLineArguments) + } + + /** + * (Almost) Fully automated test, which takes a program and its dependency analysis results and, + * for each explicit assertion, builds a new program that only contains said assertion and + * all its dependencies. The test passes if all new programs verify successfully. + * + * Statements that are only required as a trigger need to be manually annotated with @trigger() by the user. + */ + class PruningTest(fileName: String, program: Program, fullGraphInterpreter: DependencyGraphInterpreter[Final]) { + lazy val pruningSupporter = new DependencyAnalysisPruningSupporter(fullGraphInterpreter) + + def execute(): Unit = { + val triggerNodeLines = fullGraphInterpreter.getNodes.filter(node => node.getUserLevelRepresentation.contains("@trigger()")).flatMap(_.sourceInfo.getLineNumber) + var id: Int = 0 + // TODO ake: it would be better to work with position string instead of line numbers + val testCases = fullGraphInterpreter.getExplicitAssertionNodes flatMap (_.sourceInfo.getLineNumber) + testCases foreach {line => + pruneAndVerify(Set(line) ++ triggerNodeLines, "src/test/resources/" + fileName + s"_test$id.out") + id += 1 + } + println(s"Pruning tests: Passed ${testCases.size}/${testCases.size} tests.") + } + + protected def pruneAndVerify(relevantLines: Set[Int], exportFileName: String): Unit = { + val relevantNodes = relevantLines.flatMap(line => fullGraphInterpreter.getNodesByLine(line)) + + val dependencies = fullGraphInterpreter.computeNonInternalDependencies(relevantNodes) + + val crucialNodes = relevantNodes ++ dependencies + val (newProgram, pruningFactor) = pruningSupporter.getPrunedProgram(crucialNodes, program) + resetBaselineFrontend() + val result = baselineFrontend.verifier.verify(newProgram) + if(EXPORT_PRUNED_PROGRAMS) exportPrunedProgram(exportFileName, newProgram, pruningFactor, result) + assert(!result.isInstanceOf[verifier.Failure], s"Failed to verify new program. ${result.transformedResult()}\n${newProgram.toString()}") + } + + protected def exportPrunedProgram(exportFileName: String, newProgram: Program, pruningFactor: Double, result: VerificationResult): Unit = { + val writer = new PrintWriter(exportFileName) + writer.println("// test result: " + !result.isInstanceOf[verifier.Failure]) + writer.println("// pruning factor: " + pruningFactor) + writer.println(newProgram.toString()) + writer.close() + } + } + + + + /** + * Takes a Viper program and its verification progress results and checks whether they match + * with expected results as indicated in commented lines (at the top of the file). + * + * Comment types (parentheses indicate optional text): + * + * // Spec(ification) quality: [value] + * // Proof quality: [value] + * // (Verification) Progress: [value] + * + * [value] can be either a decimal number (e.g., 0.75) or a fraction (e.g., 5/6) + */ + class VerificationProgressTest(fileName: String, fullGraphInterpreter: DependencyGraphInterpreter[Final]) { + private val epsilon = 1e-6 + + def execute(): Unit = { + val (expectedSpecQuality, expectedProofQualityLea, expectedProgress) = readExpectedValues() + val (_, actualProgressLea) = new DependencyAnalysisProgressSupporter(fullGraphInterpreter).computeVerificationProgressOptimized() + + // If a metric type does not exist, it is ignored + expectedSpecQuality.foreach { expected => + val actual = actualProgressLea.specQuality + assert(Math.abs(actual - expected) <= epsilon, + s"specQuality mismatch: expected $expected, got $actual") + } + expectedProofQualityLea.foreach { expected => + val actual = actualProgressLea.proofQuality + assert(Math.abs(actual - expected) <= epsilon, + s"proofQualityLea mismatch: expected $expected, got $actual") + } + expectedProgress.foreach { expected => + val actual = actualProgressLea.progress + assert(Math.abs(actual - expected) <= epsilon, + s"progress mismatch: expected $expected, got ${actual}") + } + println("Progress test: Passed.") + } + + // Finds a relevant metric line by prefix, extracts its metric value + private def parseLine(output: String, prefix: String): Option[Double] = { + output.linesIterator + .find(_.toLowerCase.contains(prefix.toLowerCase)) + .flatMap(line => extractMetricValue(line)) + } + + // Reads expected metric values that are written at the top of .vpr test files + private def readExpectedValues(): (Option[Double], Option[Double], Option[Double]) = { + val resourcePath = fileName.replaceAll("/+", "/").stripPrefix("/") + ".vpr" + val url = getClass.getClassLoader.getResource(resourcePath) + val lines = Files.readAllLines(Paths.get(url.toURI)).asScala.take(5).toList + val commentText = lines.filter(_.trim.startsWith("//")).map(_.trim.stripPrefix("//").trim).mkString("\n") + + val specQuality = parseLine(commentText, "spec quality") + .orElse(parseLine(commentText, "specification quality")) + .orElse(parseLine(commentText, "spec:")) + .orElse(parseLine(commentText, "specification:")) + + val proofQualityLea = parseLine(commentText, "proof quality") + .orElse(parseLine(commentText, "proof:")) + + val progress = parseLine(commentText, "progress:") + .orElse(parseLine(commentText, "verification:")) + .orElse(parseLine(commentText, "verification progress")) + + (specQuality, proofQualityLea, progress) + } + + // Extract value from text with either "metric: value" or "metric = value" + private def extractMetricValue(line: String): Option[Double] = { + val delimiterIdx = List(line.lastIndexOf('='), line.lastIndexOf(':')).filter(_ >= 0).maxOption.getOrElse(-1) + if (delimiterIdx < 0) None + else parseValue(line.substring(delimiterIdx + 1).trim) + } + + // Value can be either a decimal number or a fraction + private def parseValue(str: String): Option[Double] = { + if (str.contains("/")) { + val parts = str.split("/").map(_.trim) + if (parts.length == 2) + try Some(parts(0).toDouble / parts(1).toDouble) + catch { case _: NumberFormatException => None } + else None + } else { + try Some(str.toDouble) + catch { case _: NumberFormatException => None } + } + } + } + + + + /** + * Tests the verification guidance output against expected values annotated inline in the .vpr file. + * + * Annotation types: + * - @guidedAssumption("N") -> this assumption should appear at rank N in the guidance output (1 = most important) + * - @uncovered() -> this statement should appear in the uncovered statements of its method + * + * Method ordering is checked implicitly: methods annotated with more @uncovered statements should + * rank higher (i.e., appear earlier) in the actual uncovered-statements-per-method ranking. + */ + class GuidanceTest(program: Program, + fullGraphInterpreter: DependencyGraphInterpreter[Final]) { + + val guidedAssumptionKeyword = "guidedAssumption" + val uncoveredKeyword = "uncovered" + + def execute(): Unit = { + val actualAssumptionRanking = fullGraphInterpreter.progressSupporter.computeAssumptionRanking().filter(_._2 > 0.0) + // Compute uncovered per method once, suppressing stdout side effect + val actualUncoveredByMethod: Map[String, (Int, String)] = fullGraphInterpreter.progressSupporter + .computeUncoveredStatementsPerMember().map{ case (member, sources) => + (member, (sources.size, s"$member\n\t${sources.mkString("\n\t")}"))} + + val errorMsgs = + checkAssumptionRanking(actualAssumptionRanking) ++ + checkUncoveredStatements(actualUncoveredByMethod) ++ + checkMethodOrder(actualUncoveredByMethod) + + assert(errorMsgs.isEmpty, "\n" + errorMsgs.mkString("\n")) + println("Guidance test: Passed.") + } + + private def checkAssumptionRanking(actualRanking: List[(String, Double)]): Seq[String] = { + val annotated = extractAnnotatedStmts(_.values.contains(guidedAssumptionKeyword)) + val ranked: List[(Int, Int)] = annotated.toList.flatMap { node => + val rankStr = node.info.getUniqueInfo[ast.AnnotationInfo] + .flatMap(_.values.get(guidedAssumptionKeyword).flatMap(_.headOption)) + .getOrElse("") + val rankOpt = try Some(rankStr.toInt) catch { case _: NumberFormatException => None } + val line = extractSourceLine(node.asInstanceOf[ast.Positioned].pos) + rankOpt.map(rank => (rank, line)) + }.distinct.sortBy(_._1) + + if (ranked.isEmpty) return Seq.empty + + // Find each annotated assumption in the actual ranking; match by "line N)" in the toString + // Carry actual score so equal-scored pairs can be skipped in the ordering check. + val posResults: List[Either[String, (Int, Int, Double)]] = ranked.map { case (rank, line) => + val idx = actualRanking.indexWhere(_._1.contains(s"line $line)")) + if (idx < 0) + Left(s"@guidedAssumption($rank) at line $line not found in assumption ranking.\nActual ranking:\n\t${actualRanking.mkString("\n\t")}") + else + Right((rank, idx, actualRanking(idx)._2)) + } + + val missingErrors = posResults.collect { case Left(err) => err } + if (missingErrors.nonEmpty) return missingErrors + + val positions = posResults.collect { case Right(p) => p } + // Pairwise: smaller annotated rank should appear earlier in actual ranking, + // unless the two assumptions have equal actual scores (any order is valid then). + (for { + i <- positions.indices + j <- i + 1 until positions.size + (rankI, idxI, scoreI) = positions(i) + (rankJ, idxJ, scoreJ) = positions(j) + if rankI < rankJ && scoreI != scoreJ && idxI > idxJ + } yield s"Wrong assumption order: @guidedAssumption($rankI) at actual position $idxI should come before @guidedAssumption($rankJ) at position $idxJ").toSeq + } + + private def checkUncoveredStatements(actualUncoveredByMethod: Map[String, (Int, String)]): Seq[String] = { + val errors = scala.collection.mutable.ListBuffer.empty[String] + for (method <- program.methods) { + val annotatedLines = uncoveredAnnotatedLinesInMethod(method) + actualUncoveredByMethod.get(method.name) match { + case None => + if (annotatedLines.nonEmpty) + errors += s"No member interpreter found for method '${method.name}'" + case Some((actualCount, output)) => + for (line <- annotatedLines) { + if (!output.contains(s"line $line)")) + errors += s"@uncovered() at line $line not found in uncovered statements of method '${method.name}'.\nActual uncovered output:\n$output" + } + if (actualCount != annotatedLines.size) + errors += s"Method '${method.name}': expected ${annotatedLines.size} uncovered statement(s), got $actualCount" + } + } + errors.toSeq + } + + private def checkMethodOrder(actualUncoveredByMethod: Map[String, (Int, String)]): Seq[String] = { + // Derive expected order from @uncovered annotation counts per method (descending) + val expectedOrder: List[(String, Int)] = program.methods + .map(m => (m.name, uncoveredAnnotatedLinesInMethod(m).size)) + .filter(_._2 > 0) + .sortBy(-_._2) + .toList + + if (expectedOrder.size < 2) return Seq.empty + + val actualOrder: List[String] = actualUncoveredByMethod.toList + .filter(_._2._1 > 0) + .sortBy(-_._2._1) + .map(_._1) + + // Pairwise: method with strictly more @uncovered annotations should appear before one with fewer. + // Methods with equal counts may appear in any order. + (for { + i <- expectedOrder.indices + j <- i + 1 until expectedOrder.size + (methodA, countA) = expectedOrder(i) + (methodB, countB) = expectedOrder(j) + if countA != countB + idxA = actualOrder.indexOf(methodA) + idxB = actualOrder.indexOf(methodB) + if idxA >= 0 && idxB >= 0 && idxA > idxB + } yield s"Wrong method order: '$methodA' ($countA @uncovered) should appear before '$methodB' ($countB @uncovered), but actual order is: ${actualOrder.mkString(", ")}").toSeq + } + + private def uncoveredAnnotatedLinesInMethod(method: ast.Method): List[Int] = { + val lines = scala.collection.mutable.ListBuffer.empty[Int] + @unused + val _ignored: ast.Node = ViperStrategy.Slim({ + case s: ast.Seqn => s + case n: ast.Infoed => + val hasAnnotation = n.info.getUniqueInfo[ast.AnnotationInfo] + .exists(_.values.contains(uncoveredKeyword)) + if (hasAnnotation) + lines += extractSourceLine(n.asInstanceOf[ast.Positioned].pos) + n + }).execute(method) + lines.toList + } + + protected def extractAnnotatedStmts(annotationFilter: ast.AnnotationInfo => Boolean): Set[ast.Infoed] = { + var nodesWithAnnotation: Set[ast.Infoed] = Set.empty + @unused + val newP: ast.Program = ViperStrategy.Slim({ + case s: ast.Seqn => s + case n: ast.Infoed => + val annotationInfo = n.info.getUniqueInfo[ast.AnnotationInfo] + .filter(annotationFilter) + if (annotationInfo.isDefined) + nodesWithAnnotation += n + n + }).execute(program) + nodesWithAnnotation + } + + protected def extractSourceLine(pos: ast.Position): Int = { + pos match { + case column: ast.HasLineColumn => column.line + case _ => -1 + } + } + } +} \ No newline at end of file diff --git a/src/test/scala/DependencyAnalysisTests.scala b/src/test/scala/DependencyAnalysisTests.scala new file mode 100644 index 000000000..c5ad8c610 --- /dev/null +++ b/src/test/scala/DependencyAnalysisTests.scala @@ -0,0 +1,87 @@ +// 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.silicon.tests + +import org.scalatest.funsuite.AnyFunSuite +import viper.silicon.dependencyAnalysis._ +import viper.silicon.dependencyAnalysis.cliTool.DependencyGraphImporter +import viper.silicon.dependencyAnalysis.graph._ +import viper.silicon.dependencyAnalysis.graphInterpretation.{DependencyGraphInterpreter, DependencyGraphTestSupporter} +import viper.silver.ast._ +import viper.silver.frontend.SilFrontend +import viper.silver.verifier + +class DependencyAnalysisTests extends AnyFunSuite with DependencyAnalysisTestFramework { + + val EXECUTE_TEST = true + val TEST_IMPORTER = false // if true, the tests are executed on the graph that got exported and then imported via the GraphImporter + override val EXPORT_PRUNED_PROGRAMS: Boolean = false + val ignores: Seq[String] = Seq() + val depAnalysisModeArg = + if(TEST_IMPORTER) Seq("--dependencyAnalysis=export>testExports") + else Seq("--dependencyAnalysis=computeGraphOnly") + + analysisCommandLineArguments = analysisCommandLineArguments ++ depAnalysisModeArg + val testDirectories: Seq[String] = Seq( + "dependencyAnalysisTests/all", + "dependencyAnalysisTests/real-world-examples", + "dependencyAnalysisTests/verificationProgressTests", + "dependencyAnalysisTests/guidance", + ) + + if (EXECUTE_TEST) { + testDirectories foreach (dir => visitFiles(dir, createSingleTest)) + // TODO ake: more complete exhale tests +// analysisCommandLineArguments = Seq("--enableMoreCompleteExhale") ++ analysisCommandLineArguments +// visitFiles("dependencyAnalysisTests/mce", createSingleTest) + } + + private def createSingleTest(dirName: String, fileName: String): Unit = { + test(dirName + "/" + fileName) { + try{ + resetFrontend() + executeTest(dirName + "/", fileName, frontend) + }catch{ + case t: Throwable => fail(t.getMessage, t) + } + } + } + + def executeTest(filePrefix: String, + fileName: String, + frontend: SilFrontend): Unit = { + println(s"$filePrefix$fileName") + + val program: Program = tests.loadProgram(filePrefix, fileName, frontend) + val result = frontend.verifier.verify(program) + if (result.isInstanceOf[verifier.Failure]) { + cancel(f"Program does not verify. Skip test.\n$result") + return + } + + val name = frontend.reporter.asInstanceOf[DependencyAnalysisReporter].joinedDependencyGraphInterpreter.map(_.getName).getOrElse("graph") + + val fullGraphInterpreter = if (TEST_IMPORTER) { + println("--------\nTesting via the graph importer.") + val importedGraph = DependencyGraphImporter.importGraphFromCsv(s"testExports") + new DependencyGraphInterpreter[Final](name, importedGraph, List.empty, None) + } else { + frontend.reporter.asInstanceOf[DependencyAnalysisReporter].joinedDependencyGraphInterpreter.get + } + + val testSupporter = new DependencyGraphTestSupporter(fullGraphInterpreter) + testSupporter.testDependencies() + testSupporter.testNodeTypes() + new PruningTest(filePrefix + "/" + fileName, program, fullGraphInterpreter).execute() + + if (filePrefix.contains("verificationProgressTests")) { + new VerificationProgressTest(filePrefix + "/" + fileName, fullGraphInterpreter).execute() + } else if (filePrefix.contains("guidance")) { + new GuidanceTest(program, fullGraphInterpreter).execute() + } + } +}